类和对象——拷贝对象时的一些编译器优化
拷贝对象时的一些编译器优化
- 拷贝对象时的一些编译器优化
- 案例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)
6
6
A(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之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
-
用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
所以类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。