原文链接:C++编译器对于对象的优化_c++编译器对于对象构造的优化:用临时对象生成新对象的时候,-CSDN博客
文章详细探讨了C++编译器在对象构造过程中的优化,
例如在某些情况下直接构造新对象时候,将会避免临时对象的生成(将临时对象优化掉了,就不产生临时对象了);但是,如果是给已经生成的对象进行赋值,将不会对优化掉 临时对象。
此外,还提到了对象构造和析构的调用顺序,以及函数参数传递和返回对象时的优化原则,强调了使用引用和初始化接收返回值的重要性。
1、C++编译器对于对象构造的优化
1.1、用临时对象生成新对象时, 临时对象就不产生了,直接构造新对象即可
class Test
{
public:
Test(int a = 10) :ma(a)
{
cout << "Test(int)" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
Test(const Test& t) :ma(t.ma)
{
cout << "Test(const Test&)" << endl;
}
Test& operator=(const Test& t)
{
cout << "operator=" << endl;
ma = t.ma;
return *this;
}
private:
int ma;
};
int main()
{
Test t1;
Test t2(t1);
Test t3 = t1;
// Test(20)显式生成临时对象
// C++ 编译器对于对象构造优化:用临时对象生成新对象时,不会产生临时对象,直接构造
Test t4 = Test(20); // 等价于 Test t4(20); 只调用一个构造函数
cout << "----------------------" << endl;
return 0;
}
输出结果
Test(int)
Test(const Test&)
Test(const Test&)
Test(int)
----------------------
~Test()
~Test()
~Test()
~Test()
1.2、但是如果是赋值,就会创建临时对象
t4 = t2;
//t4已经存在,属于赋值
//1.会调用构造函数生成临时对象,2.并调用赋值运算符,3.该行语句执行完会调用析构函数析构临时对象
t4 = Test(30);
执行结果
operator=
Test(int)
operator=
~Test()
2、显式和隐式生成临时对象
int类型30 =》转换为Test类型:
// 显示生成临时对象
t4 = Test(30); //1.会调用构造函数生成临时对象,2.并调用赋值运算符
// 隐式生成临时对象
t4 = (Test)30;//1.会调用“带有int参数的Test构造函数”生成临时对象,2.并调用赋值运算符
t4 = 30; //1.会调用“带有int参数的Test构造函数”生成临时对象,2.并调用赋值运算符
执行结果
Test(int)
operator=
~Test()
Test(int)
operator=
~Test()
Test(int)
operator=
~Test()
3、注意:
3.1、指针指向临时对象,是不安全的!
如果用指针指向一个对象,那么这个对象必须是“左值”,不能是“右值”,或者说不能是“临时对象”!
因为,临时对象的生命周期只在当前语句内,出了这条语句,临时对象析构 不复存在。此时,指针p指向一个不存在的对象,就变成 野指针了!!
Test* p = &Test(40); // error
3.2、引用指向临时对象是安全的。
const引用可以接收临时构造的对象
const Test& ref = Test(50); //常引用扩展了临时对象的生命周期!(出了这条语句,临时对象仍存在)
4、函数中不同类型的对象 构造、析构先后顺序
分析案例1:
class Test
{
public:
// Test() Test(10) Test(10, 10)
Test(int a = 5, int b = 5)
:ma(a), mb(b)
{
cout << "Test(int, int)" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
Test(const Test& src)
:ma(src.ma), mb(src.mb)
{
cout << "Test(const Test&)" << endl;
}
void operator=(const Test& src)
{
ma = src.ma;
mb = src.mb;
cout << "operator=" << endl;
}
private:
int ma;
int mb;
};
Test t1(10, 10); // 1.Test(int, int)
int main()
{
Test t2(20, 20); // 3.Test(int, int)
Test t3 = t2; // 4.Test(const Test&)
// static Test t4(30, 30); static是在局部对象后面析构
static Test t4 = Test(30, 30); // 5.Test(int, int)
t2 = Test(40, 40); // 6.Test(int, int) operator= ~Test()
// (50, 50) = (Test)50; Test(int)
t2 = (Test)(50, 50); // 7.Test(int,int) operator= ~Test()
t2 = 60; //Test(int) 8.Test(int,int) operator= ~Test()
Test* p1 = new Test(70, 70); // 9. Test(int,int)
Test* p2 = new Test[2]; // 10. Test(int,int) Test(int,int)
//Test* p3 = &Test(80, 80); // 11. Test(int,int) ~Test()
const Test& p4 = Test(90, 90); // 11. Test(int,int),常引用扩展了对象的生命期
delete p1; // 12.~Test()
delete[]p2; // 13. ~Test() ~Test()
}
// 14. p4::~Test()
// 15. t3::~Test()
// 16. t2::~Test()
// 17. t4::~Test()
// 18. t5::~Test()
// 19. t1::~Test()
Test t5(100, 100); // 2.Test(int, int)
1. 和2.全局变量先构造,在main()函数之前做好初始化;t1、t5
4.t2拷贝构造t3;
5.静态变量,程序运行的时候,它的内存已经存在了,因为数据段内存是事先就分配好了。但是,静态变量的初始化是在第一次运行到它(按照变量前后顺序)的时候,才初始化。没有运行到,就不会先初始化;t4(临时对象被优化掉,直接调用一次构造函数,构造t4)
6.显示生成临时对象,然后给t2赋值,之后析构临时对象;
11.p4指针指向临时对象;
main()函数结束后,如何继续析构?(后生成的 先析构)
14. 和15. 和16. p4, t3, t2。(函数结束后,函数内栈上数据先析构)
17. 和18. 和19. 析构t4,t5,t1(整个程序运行结束后,数据段上的数据才析构!)
5、对象构造和析构调用顺序
注意:不能返回 局部的或者临时对象 的指针或引用!!
对象构造和析构调用顺序分析案例2
class Test
{
public:
Test(int data = 10) :ma(data)
{
cout << "Test(int)" << endl;
}
~Test()
{
cout << "~Test(int)" << endl;
}
Test(const Test &t) :ma(t.ma)
{
cout << "Test(const Test &t)" << endl;
}
void operator=(const Test& t)
{
cout << "operator=" << endl;
ma = t.ma;
}
int getData() const { return ma; }
private:
int ma;
};
/*Test* GetObject(Test t)
{
int val = t.getData();
Test tmp(val);
return &tmp;//不能返回 局部的或者临时对象 的指针或引用
}
Test* GetObject(Test t)
{
int val = t.getData();
static Test tmp(val);
return &tmp;//可以正常返回:静态局部的或者临时对象的引用(生命周期是整个程序)
}*/
Test GetObject(Test t)
{
int val = t.getData();
Test tmp(val);
return tmp;
}
int main()
{
Test t1;
Test t2;
t2 = GetObject(t1);//3.由t1拷贝构造形参t
return 0;
}
构造析构顺序如下图所示:
3.由t1拷贝构造形参t
5.在main()函数栈帧上构造一个临时对象(没有名字),之后,由局部变量tmp拷贝构造这个临时对象;
6.出了GetObject(){}函数,局部变量tmp析构,
7.之后,是形参t析构;
8.之后,是回到main()函数中,用在main()函数栈帧上构造的临时对象给t2赋值;
9.出了赋值语句,就要将main()函数栈帧上构造的临时对象进行析构;
10.main函数结束后,是t2析构,之后是t1析构;
6、对象的三条优化原则
1.函数参数传递过程中,对象优先按引用传递,不要按值传递;
2.函数返回对象的时候,应该优先返回一个临时对象而不是返回一个定义过的对象;
3.接收返回值是对象的函数调用,优先按初始化方式接收,不要按赋值方式接收;
实例验证:
1.将上面的代码函数形参做修改为引用,将会节省一次的拷贝构造 和 析构的开销,原来t要拷贝构造和析构
Test GetObject(Test &t) // 这里参数被修改为引用
{
int val = t.getData();
Test tmp(val);
return tmp;
}
int main()
{
Test t1;
Test t2;
t2 = GetObject(t1);
return 0;
}
输出结果为:
Test(int)
Test(int)
Test(int)
Test(const Test &t)
~Test(int)
operator=
~Test(int)
~Test(int)
~Test(int)
可以发现少了两次的调用,3.和7.两个函数调用;
函数参数传递引用: GetObject(Test t)=>GetObject(Test &t)
好处是:传递引用 不用使用实参t1拷贝构造形参t(3.),就不用调用拷贝构造函数、离开函数作用域也就不用调用形参t的析构函数(7.),这样就少调用了2个函数,效率提高!
2.进一步进行优化,在GetObject 中返回临时对象
Test GetObject(Test &t)
{
int val = t.getData();
/*Test tmp(val);
return tmp;*/
// 这里正常情况会使用临时对象拷贝构造一个新对象
// 而c++编译器优化则会导致只调用构造函数生成main函数中GetObject(t1)返回的的临时对象
// 这样就又减少一对拷贝构造和析构
return Test(val);
}
输出结果:
Test(int)
Test(int)
Test(int)
operator=
~Test(int)
~Test(int)
~Test(int)
将 Test tmp(val);return tmp; => return Test(val);
用临时对象Test(val)拷贝构造一个新对象,临时对象都是要被优化掉。即不会创建临时对象。这里就是直接构造main函数栈帧上的临时对象(用这个临时对象,赋值给t2)。
可以发现:少了tmp的构造和析构,就是4.和6.两个函数调用;
3.再进行优化,将main函数中 GetObject(t1)直接赋值给t2
int main()
{
Test t1;
//Test t2;
//t2 = GetObject(t1);
//因为这里用临时对象构造同类型对象t2,所以会进行优化,只调用一次构造函数,而没有调用右边临时对象的构造
Test t2 = GetObject(t1);
return 0;
}
输出结果:
Test(int)
Test(int)
~Test(int)
~Test(int)
将 Test t2; t2=GetObject(t1); => Test t2=GetObject(t1);
用临时对象Test(val)拷贝构造一个同类型的新对象t2,临时对象都是要被优化掉。即不会创建临时对象。
少了构造main函数栈帧上的临时对象(5.) 和 8.赋值和 9.临时对象析构
原文链接:https://blog.csdn.net/qq_42120843/article/details/130573471