19.了解临时对象的来源

前言

 
程序员习惯于把短暂需要的变量称为临时变量:

1
2
3
4
5
template<class T>
void swap(T& object1, T& object2){
T temp = object1;//往往被称为临时对象
object1 = object2;object2 = temp;
}

对于c++而言,temp根本不是临时对象,它只是一个函数的局部对象。


临时对象

 
在c++中真正的临时对象是看不见的,它不出现在任何源码中。建立一个未命名的non-heap对象会产生临时对象,它一般只会出现在两种条件下:

  1. 为了使函数成功调用而进行隐式转换时
  2. 函数返回对象时

我们之所以需要关注临时对象,是因为构造和析构它们带来的成本会对程序的性能造成很大影响。


隐式转换所产生的临时对象

问题实例

以下是一个记录字符在字符串中出现次数的函数:

1
2
3
4
5
size_t countChar(const string& str, char ch);
char buffer[MAX_STRING_LEN];
char c;
cin >> c >> setw(MAX_STRING_LEN) >> buffer;//避免缓存溢出
cout << countChar(buffer, c) << endl;

显然,countChar需要的是一个string对象,但实际传入的是一个字符数组(C API 字符串),因此编译器不得不建立一个string的临时对象,以buffer作为参数来初始化它,str被绑定到了这个临时对象,当函数返回时,临时对象释放。

解决方案

这种类型转换在方便之余也会存在一些风险(More Effective C++ 5),此外,临时对象的构造与析构也是一笔很大的开销,修改方法无非两种:

  1. 重新设计程序,禁止发生类型转换,详见More Effective C++ 5。
  2. 通过修改程序保证不再需要类型转换,详见More Effective C++ 21。

仅仅在传值与传常量引用(reference-to-const)时会发生上述情况,因为c++禁止为非常量引用生成临时对象(如果允许存在非常量引用,在实际使用时无法改变真正传入的参数,一切操作均作用于临时对象)。


返回对象

 
返回对象会生成临时对象是很容易理解的,并且More Effective C++ 20给出了优化方案。


临时对象的发现

 
训练自己发现临时对象的能力是很有必要的。任何时候只要看到常量引用参数,就存在建立一个临时对象绑定于其上的可能性。任何时候只要见到一个函数返回对象,就会明白必然存在一个临时对象被构造和析构。


关于常量引用的补充

 
如果对一个常量进行引用,那么编译器首先建立一个临时变量,然后将该常量的值置入临时变量中,对该引用的操作就是对该临时变量的操作。对常量的引用可以用其它任何引用来初始化,但不能改变。
关于引用的初始化有两点值得注意:

  1. 当初始化值是一个左值(可以取得地址)时,没有任何问题。
  2. 当初始化值不是一个左值时,则只能对一个const T&(常量引用)赋值。

常量引用的初始化过程为:首先将右值隐式转换到类型T,然后将这个转换结果存放在一个临时对象里,最后用这个临时对象来初始化这个引用变量。

1
2
3
4
5
double& dr = 1;//error 需要左值
const double& cdr = 1; // ok
//实际过程等价于
double temp = double(1);
const double& cdr = temp;