[转]细述临时对象

 
临时对象(Temporary Object)

一、什么情况下可能出现临时对象?
1、传值参数(pass-by-value parameter)
代码:
   
   
void func(String str) { cout << str; }
当进行这样一个调用时:
代码:
   
   
String s; func(s);
s将被拷贝构造一个备份(临时对象),这个备份参与函数体内的运算,原件不会被改动。

2、返回值(return value)
代码:
   
   
String func() { String s("Hello, world"); return s; }
当s被返回时,会被拷贝构造一个备份(临时对象),这个变量被返回,函数体内的局部变量被销毁。

3、隐式类型转换(Implicitly Typecast)
代码:
   
   
class String { public: String(); String(const char *str); operator=(const String &rhs); };
当实行一个赋值动作:
代码:
   
   
String s; s = "Hello, world";
时,因为String: perator=并没有针对char *和const char *的重载,而最可能的做法就是将const char *隐式转换为一个String对象。在这个过程中就会产生一个String类的临时对象。

4、++ 和 --
代码:
   
   
std::list<int>::iterator iter; iter++;
因为iter++的返回值需要返回原值,所以这个值在增1之前必须保留,这个保留就必须使用一个临时对象。这相当于是返回值的一个典型情况。

5、值存储的STL容器
主要针对std::vector,因为vector具有自动扩充容量的功能,在每次扩充容量的时候,大部分实现的算法是new一个新的内存块,将原有的每个元素拷贝复制到新的内存,然后删除原有的内存块。这期间要使用大量的临时对象。

二、临时对象的弊端
临时对象实际上就是通过拷贝构造函数产生的,没有变量名的const对象,所以每产生这样一个对象,都会调用一次拷贝构造函数。如果这个类很“大”,那么会占用相当的资源和时间,从而降低效率。特别是在第5种情形中,会产生大量的临时对象,情况尤其严重。

三、临时对象的应对之道
解决临时对象的主要方法就是使用指针和引用,减少构造函数调用的机会。

1、避免使用传值参数
实际上绝大部分的传值参数都可以使用常量引用(const &)来代替,而常量引用的参数不会产生临时对象。
代码:
   
   
void func(const String& str) { cout << str; }
这样的写法可以有效的避免在参数传递过程中产生临时对象。

但是,引用通常是由指针实现的,对于基本类型来说,指针操作比直接操作要慢一些,并且增加了操作的复杂性。而基本类型的操作都不复杂,而且很快,所以,对于基本类型来说,没有必要使用常量引用手法。

2、隐式类型转换
隐式类型转换的问题是定义了针对某个类型的构造函数,但是没有定义针对该类型的operator=所导致的,所以,构造函数和operator=必须成对出现。

3、++ 和 --
这个问题已经讨论过很多了,
代码:
   
   
operator++() 的实现方法是 { return ++obj.value; } operator++(int) 的实现方法是 { Class tmp = obj; ++obj.value; return tmp; }
因此,在可能的情况下,使用++i代替i++,就可以避免出现临时对象。

4、在STL容器(尤其是vector)中使用对象指针而不是对象实体作为元素
vector因为有重新分配内存的动作,在这个动作中会产生大量的临时对象,极大的降低效率。解决方法就是使用对象指针作为vector的元素,避免采用对象实体。另外,用reserve事先分配内存也是避免内存交换的方法。
代码:
   
   
std::vector<String *> str_vector;
但是要注意,STL使用的是值(value)语意,所以对容器中指针的管理要自己负责,比如容器析构和copy时,可能要自己多做一些工作,以保证数据的正确性。

更好的方法当然是对指针语意的vector做一个封装。

5、函数的返回值
函数的返回值不能以引用或指针的方式返回函数内部的局部变量,如果需要返回局部变量的值,必须以Object形式返回。在这种情况下,临时对象是不可避免的。这个问题上,《Effective C++》Item 23 给出了更详细的讨论。
对于无法避免的临时对象,我们至少可以将“危害”降到最低点。

利用编译器优化
a) 上面提到,函数必须以值的形式返回局部变量。由于这种形式比较普遍并且无法避免,现代的编译器大部分都可以实行一个叫做 Named Return Value(NRV)的优化。这个优化可以避免不必要的临时对象的产生。在编译器实施NRV优化时,如下的代码:

代码:
   
   
X bar() { X xx; //... operate xx return xx; }
将被优化为类似这样:
代码:
   
   
void bar( X& __result) { __result.X::X(); //... operate __result return; }
很明显的,这个优化减少了一个临时对象的产生。

NRV优化由编译器自动实施,并没有什么编译选项,也无法人工干预。所以,不能依赖于该优化,不过该优化在某些情况下的确可以提高效率。

对NRV的讨论,可以参考《深度探索C++对象模型》一书的“在编译器层面做优化”一章。

b) 目前几乎所有的编译器都可以保证
代码:
   
   
Class c = a + b;
这样的形式将不会产生临时对象。

6、其它
尽可能少的调用
这个问题最典型的现象就是循环的终止条件。
代码:
   
   
for(std::vector<String *>::iterator iter = str_vector.begin(); iter != str_vector.end(); ++iter) { //... }
在这里,每循环一次,iter != str_vector.end() 这个判断就要进行一次,因为str_vector.end()是一个函数调用,因为它的返回值是一个iterator的Object,所以每次调用都会产生一个临时对象。如果写成这样,临时对象就会只被生成一次:
代码:
   
   
std::vector<String *>::iterator end_iter = str_vector.end(); for(std::vector<String *>::iterator iter = str_vector.begin(); iter != end_iter; ++iter) { //... }
四、其它问题
临时对象的生命周期
临时对象是不可见的,但是,在特定的情况下,它的生存周期问题可能导致问题。对这个问题,标准规格上的定义是:

临时对象的被摧毁,应该是对完整表达式求值过程中的最后一个步骤。该完整表达式造成临时对象的产生(Section 12.2)

对这个问题的更详细的讨论,可以参见《深度探索C++对象模型》 6.3节,临时性对象。另外,在《C++设计与演化》一书中也有论述。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值