拷贝对象时的一些编译器优化
- 拷贝对象时的一些编译器优化
- 案例1:仅使用类中的成员函数
- 案例2:案例1减少一次拷贝构造
- 案例3:临时对象也具有常属性
- 案例4:const引用延长生命周期
- 案例5:传匿名对象传参
- 案例6:函数传值返回时的优化
- 案例7:优化的条件
- 案例8:隐式类型转换的优化
- 再次理解封装
拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化(也就是说有的不做优化),减少对象的拷贝,这个在一些场景下还是非常有用的。
这里只举几个案例,详细见书籍《深度探索c++对象模型》。
在20世纪末流行的编译器(例如,vc++6.0)不会对这种情况进行优化。
案例1:仅使用类中的成员函数
很多时候,生成这个对象的目的仅仅是为了调用类中的某个函数。此时没必要生成一个对象,特别是生成一个对象作为实参上传给普通函数。
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}void print() {using std::cout;cout << a << "\n";}
private:int a;
};//若调用拷贝构造仅仅是为了调用这个函数,完全没必要传值传参
void f1_1(A a) {a.print();
}//所以直接加引用
void f1_2(A& a) {a.print();
}void f1() {A a;f1_1(a);cout << endl;f1_2(a);cout << endl;
}int main() {f1();return 0;
}
案例2:案例1减少一次拷贝构造
首先,const
对象不能调用非const
成员函数。所以const
对象也要准备对应的const
函数重载。
其次,引用和const
一般在一起,为了避免别名修改原来的对象(变量)。
最后,形参使用引用可以减少一次拷贝构造。
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}//相应函数也要对这个类的成员函数进行限制防止权限放大void print() const {using std::cout;cout << a << "\n";}
private:int a;
};//为了支持生成临时对象,使用const引用
void f2_1(const A& a) {a.print();
}void f2_2(A& a) {a.print();
}void f2_3(A& a) {//非const形参,不具有常属性a.print();
}void f2() {A a;f2_1(a);//权限缩小cout << endl;f2_2(a);//权限平移cout << endl;//f2_3(A());//权限放大f2_1(A());//形参也具有常属性时权限平移,可以调用cout << endl;
}int main() {f2();return 0;
}
输出:
A(int a)
66A(int a)
6
~A()~A()
f2_3(A());
无法编译通过,因为临时对象、匿名对象都有常属性,上传无常属性形参的函数,权限放大。
案例3:临时对象也具有常属性
在案例2已经证明匿名对象具有常属性。隐式类型转换的临时对象也具有常属性。
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}//相应函数也要对这个类的成员函数进行限制防止权限放大void print() const {using std::cout;cout << a << "\n";}
private:int a;
};//const引用能很好的支持生成临时对象
void f3_1(const A& a) {//这个地方引用和const一般同时出现防止不小心修改a.print();
}void f3() {//少调用一次拷贝构造f3_1(A());//匿名对象有常属性cout << endl;f3_1(A(4));cout << endl;f3_1(3);//临时对象也具有常属性cout << endl;
}int main() {f3();return 0;
}
输出:
A(int a)
6
~A()A(int a)
4
~A()A(int a)
3
~A()
它们都被优化成了只调用一次构造函数。
案例4:const引用延长生命周期
const
引用可以延长临时对象的生命周期,本质是将临时对象变成有名对象,这样临时对象就可以像有名对象一样生命周期在局部。
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}//相应函数也要对这个类的成员函数进行限制防止权限放大void print() const {using std::cout;cout << a << "\n";}
private:int a;
};//缺省值为匿名对象
//const延长生命周期使得匿名对象存在于局部
void f4_1(const A& a = A()) {a.print();
}void f4() {f4_1();cout << endl;//这里只有ref出了作用域,//临时对象的生命周期才终止const A& ref = A();cout << endl;ref.print();//还在{}也就是作用域内,可以使用cout << endl;
}int main() {f4();return 0;
}
案例5:传匿名对象传参
编译器优化情况1:隐式类型转换作为实参,此时会调用两次构造。编译器将连续的两次构造(构造+拷贝构造)优化为直接构造。
c++标准并没有对这种情况进行优化说明,这个其实还是编译器本身的行为。在一些年代比较久远的编译器(比如20世纪末)就不会。
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}//相应函数也要对这个类的成员函数进行限制防止权限放大void print() const {using std::cout;cout << a << "\n";}
private:int a;
};void f5_1(A a) {a.print();
}//析构void f5_2(const A a) {a.print();
}A f5_3() {A a;return a;
}//隐式类型,连续构造(两次及以上)->优化为直接构造
void f5() {//传值传参//正常情况A a;//构造f5_1(a);//拷贝构造cout << endl;// 一个表达式中,构造+拷贝构造->优化为一个构造f5_1(A());//匿名对象构造+拷贝构造被优化cout << endl;f5_1(A(3));cout << endl;f5_1(4);//隐式类型转换cout << endl;//这个也是构造+拷贝构造A b = A(3);cout << endl;
}int main() {f5();return 0;
}
输出:
A(int a)
A(const A& aa)
6
~A()A(int a)
6
~A()A(int a)
3
~A()A(int a)
4
~A()A(int a)~A()
~A()
分析:
f5_1(A());
,f5_1(A(3));
:匿名对象调用构造函数,加拷贝构造生成形参。
f5_1(4);
:隐式转换,一次构造加拷贝构造。
A b = A(3);
:一次构造加拷贝构造。
这三种情况,都被优化为一次构造。
案例6:函数传值返回时的优化
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}//相应函数也要对这个类的成员函数进行限制防止权限放大void print() const {using std::cout;cout << a << "\n";}
private:int a;
};A f6_1() {A a;//构造return a;//拷贝构造生成临时对象
}A& f6_2() {A a;return a;
}void f6() {A a;cout << endl;f6_1();cout << endl;a = f6_1();cout << endl;A ret = f6_1();cout << endl;A ret2 = f6_2();cout << endl;
}int main() {f6();return 0;
}
输出:
A(int a)A(int a)
A(const A& aa)
~A()
~A()A(int a)
A(const A& aa)
~A()
A& operator=(const A& aa)
~A()A(int a)
A(const A& aa)
~A()A(int a)
~A()
A(const A& aa)~A()
~A()
~A()
单独看A ret = f6_1();
这种情况:
A f6_1()
在return
语句会生成临时对象,但编译器进行了优化,直接将这个a
在生命周期结束前拷贝给ret
。
所以在一个表达式的连续两个步骤里,局部对象构造 + 传值返回生成临时对象调用拷贝构造,两次调用构造被优化为一次。
而A ret2 = f6_2();
因为f6_2
是传引用返回,所以直接省去了return
语句的一次拷贝构造,在析构前生成临时对象,之后通过拷贝构造将对象拷贝给ret2
。
案例7:优化的条件
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}//相应函数也要对这个类的成员函数进行限制防止权限放大void print() const {using std::cout;cout << a << "\n";}
private:int a;
};A f7_1() {A a;return a;
}void f7() {//这种情况编译器不会再优化A ret2;ret2 = f7_1();
}int main() {f7();return 0;
}
f7()
这种情况不能优化,两个原因:
- 同类型才能优化(都是构造或都是拷贝构造才能优化,这里是构造和赋值)。
- 不在同一步骤(声明对象和赋值重载是两个语句或者说步骤)。
案例8:隐式类型转换的优化
和案例6的情况相似,都是构造临时对象并返回,只是存在隐式类型转换。所以被优化为一次构造。
#include<iostream>
#include<cstdlib>
using namespace std;class A {
public:A(int a = 6):a(a) {cout << "A(int a)" << endl;}A(const A& aa):a(aa.a) {cout << "A(const A& aa)" << endl;}A& operator=(const A& aa) {cout << "A& operator=(const A& aa)" << endl;if (this != &aa) {a = aa.a;}return *this;}~A() {cout << "~A()" << endl;}//相应函数也要对这个类的成员函数进行限制防止权限放大void print() const {using std::cout;cout << a << "\n";}
private:int a;
};//被优化为直接构造
//构造匿名对象加临时对象,两次构造被优化为1次
A f8_1() {return A();
}A f8_2() {return 8;
}A f8_3() {return A(1);
}void f8() {A a1 = f8_1();cout << endl;A a2 = f8_2();//隐式类型转换cout << endl;A a3 = f8_3();cout << endl;
}int main() {f8();return 0;
}
所以就有了这样一个特性:局部对象都只能传值返回,因此可以的话尽可能使用临时对象返回或隐式类型转换,可以减少拷贝调用次数。
再次理解封装
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。
比如想要让计算机认识洗衣机,就需要:
-
用户先要对现实中洗衣机实体进行抽象——即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
-
经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面向对象的语言(比如:c++、java、python等)将洗衣机用类来进行描述,并输入到计算机中。
-
经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
-
用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
所以类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。