引入
C语言和C++在程序执行中,都是通过调用一系列的函数来实现的。并且,很多时候,编译器会帮助我们做一系列的事情,比如(在编译类的成员方法的时候,编译器默认添加 this 指针,以此来确定是哪一个对象调用了该成员方法)。得益于编译器或者说系统帮助我们做的一系列事情,我们可以更加方便地使用C++。但是凡事有利必有弊,因为系统有时候会自己调用一系列的函数,从另一个角度来说,也一定程度上降低了效率。
而我们想要提高C++的执行效率,就需要了解程序(主要是对象)使用过程中都调用了哪些方法。
测试环境
VS2019 x86环境下,编译器会影响最终结果的,如下面那个例子,VS2019 x86环境下是11个函数,而g++编译器则是9个函数,多余的优化下例有讲。
例子
#include <iostream>
using namespace std;
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;
}
int getdata()const { return ma; }
private:
int ma;
};
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(const Test&)
Test(int)
Test(const Test&)
~Test()
~Test()
operator=
~Test()
~Test()
~Test()
共调用了11个函数
我们可以来追踪一下
Test GetObject(Test t) //3.Test(const Test& t)
{
int val = t.getdata();
Test tmp(val); //4.Test(int)
return tmp; //5.Test(const Test&) 因为tmp是局部对象,是出不了该函数这个花括
//号的,故需要临时传给外面main函数中的一个临时对象才能传出。(最
//起码vs2019编译器是这样做的)经测试g++编译器会对此进行优化,少了
//main函数的那个临时对象,因此总共就9个函数
} //6.tmp析构 7.t的析构
int main()
{
Test t1; //1.Test(int)
Test t2; //2.Test(int)
t2 = GetObject(t1);//8.Test& operator=(const Test& t) 9.用来传递的那个临时对象的析构
return 0;
} //10和11分别为 t1 t2 的析构
接下来可以进行讲解:
int main()
{
Test t1; //调用的是正常的构造函数
Test t2(t1); //Test(const Test&)
Test t3 = t1; //Test(const Test&)
Test t4 = Test(20); //显式生成临时对象,但是与Test t1(20)没有区别
t4 = t1; //operator=
t4=Test(30); //Test(int) operator=
return 0;
}
Test(int)
Test(const Test&)
Test(const Test&)
Test(int)
operator=
Test(int)
operator=
~Test()
~Test()
~Test()
~Test()
~Test()
故对此可以总结:
c++编译器对于对象的构造的优化:用临时对象生成新对象的时候,临时对象就不产生了,直接用临时对象的值去构造新对象。
记住:只要是用临时对象去生成新对象的时候,临时对象都不会产生,不管是什么形式的临时对象。然而用临时对象去赋值已有对象时,临时对象便会生成。
故基于如上讲解我们可以对刚开始的例子进行优化(注释的地方即为改动)
Test GetObject(Test& t)
{
int val = t.getdata();
//Test tmp(val);
return Test(val);
}
int main()
{
Test t1;
//Test t2;
//t2 = GetObject(t1);
Test t2 = GetObject(t1);
return 0;
}
- 我们首先将函数的参数改为引用,对此节约了一个临时变量,函数-2
- 再取消tmp变量的构建,直接返回临时变量,同时把主函数中的t2的赋值操作改为初始化操作,触发优化条件:当用临时变量生成新变量的时候会进行优化,故省略掉了 tem变量 和 主函数中用来传递返回值的临时变量,函数-5(tmp 和 临时变量的构造和析构 t2的赋值)
故得到如下结果:
Test(int)
Test(int)
~Test()
~Test()
拓展
Test *p = &Test(20); //p指向的是临时对象了,到下一行时临时对象会析构,造成悬垂指针
const Test &ref = Test(20); //引用相当于给产生的临时对象赋予了别名,故临时对象不会销毁
结论:用指针指向临时对象是不安全的,用引用指向临时对象是安全的。
总结
于是我们总结出对象优化的原则:
- 不能返回局部的或者临时对象的指针或引用
- 函数参数传递过程中,对象优先按引用传递,不要按值传递
- 函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
- 接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收