- 前言
对于性能来说,实际上许多的问题都需要和出现的频率以及本身执行一次的开销挂钩,有些问题虽然看似比较开销较大,但是很少会执行到,那么也不会对程序有着非常大的影响;同样的一个很小的开销的函数执行很频繁,同样会对程序的执行效率有很大的影响。这一章中作者主要是根据临时对象来阐述这样一个观点。
- 对象定义
先看下文章中提到的例子:
class Rational
{
friend Rational operator + (const Rotional& a, const Rotional& b);
public:
Rotional (int a = 0, int b = 0):m(a),n(b){}
private:
int m;
int n;
};
// 初始化方法
Rotional r1(100);
Rotional r3 = Rotional(100);
Rotional r3 = 100;
上面写的三种初始化Rotional的方法只有第一种在最终执行的过程中不会产生临时的对象,其他两种方式根据编译器的实现不同,都有可能产生临时对象。
- 类型不匹配
上面例子中的第三种实际上是常见的类型不匹配的情况,因此需要编译器在构造的过程中现将整型转换成Rotional的形式。在这个过程中就有可能会产生临时的对象。文中还将编译器可能进行的转换步骤进行了说明,如下:
// 原本形式
Rotional r = 100;
// 编译器在编译时会改变现有的表达式,转换成如下的形式
Rotional _temp;
_temp.Rotional::Rotional(100, 0);
r. Rotional::operator = (_temp);
_temp.Rotional::~Rotional();
虽然上面这种方式看似会直接解决类型不匹配的问题,但是现在新的c++语法加入了explicit关键字,用处就是强制使用自己定义的构造函数,默认的构造函数就不会存在。如果构造函数明确使用了该关键字,那么如果还想实现类型转换就需要自己写operator=的构造函数。并且自己申明函数的话也可以实现临时对象的减少。
Rotioanl& operator = (int a)
{
m = a;
n = 0;
return *this;
}
类型的不匹配经常会造成类似的临时对象的创建,如下:
void g(const string& s)
{
...
}
上面的这个函数接受的是string类型的参数,如果传入一个char*的类型,就会产生一个string类型的临时对象,同样的情况也会在下面的函数中产生
Complex a,b;
for (int i = 0; i < 100; i++)
{
a = i * b + 1.0;
}
上述的函数在100次循环的过程中,正常都会执行100次的1.0 double 型转换为Complex的内部转换,这样的转换是可以避免的,最简单的方法就是先申明好,Complex one(1.0),这样的话每次都是用的是Complex类型的数据进行计算。
- 按值传递
按值传递相信大家在学习基础的函数语法时都遇到过,编译器会在调用的时候创建一个临时的对象,并拷贝构造,这个创建出的临时对象再当作实参传递给函数。但是临时创建对象以及清除对象的开销是比较大的,一般比较推荐的做法就是指针以及引用的方式。
- 按值返回
另一个会导致临时对象创建的地方就是按值返回,原理其实我觉得和形参产生临时对象的原理是相似的,解决的方式也大都是使用引用的方式解决。
- 使用op=的方式解决临时对象
之前类型不匹配的时候提到,为了防止匹配类型产生临时对象的开销,我们一般会自己先构建一个该类型的对象。那么相似的原理也可以用在这个上面。比如:
string s1,s2,s3;
s3=s1+s2;
为了达到极致的性能优化,一般可以重栽=和+=,
s3 = s1;
s3 += s2;
这样在实现重载之后执行这样的步骤就可以减少临时对象的创建,虽然这样的计算看似多此一举,但是对于性能要求很高的程序来说就需要舍弃一些程序的优雅性。
- 要点
临时对象会以构造函数和析构函数的方式损失两倍的性能。
把构造函数申明为explicit,可以阻止编译器在背后使用类型转换。
编译器经常会为了解决类型不匹配问题创建临时对象,通过函数重载可以解决这一问题
在函数传参以及返回值的定义中,尽量使用引用的方式
在可能是+、-、*、/的情况下使用=可以消除临时对象