一个关于临时对象的BUG
译注:由WDN 2003年6月的BUG++翻译,有删减。
我相信任何一个使用C++超过一定时间的程序员都不会否认这样一个事实:使用C++需要有足够的技巧。它充满了有各种各样的难以识别的陷阱,顷刻就可以让一段看起来毫无破绽的代码崩溃。例如,对C/C++的新手而言,学会如何考虑对象的生存期就是他们必须跨越的一个障碍,这方面最典型的问题,就是对对象指针的使用,特别是在使用一个已经被删除了的对象指针的时候:
MyClass *mc = new MyClass;
// Do some stuff
delete mc;
mc->a = 1; // Uh oh...mc is no longer valid!
一些更玄妙的事情发生在函数返回的时候,我们假设一个函数,例如foo()返回一个MyClass类型的对象引用:
MyClass &foo()
{
MyClass mc;
// Do some things
return mc;
}
这段有问题的代码实际上是完全合法的,当函数foo()的生存期还没有结束的时候,mc就会被销毁掉,但函数返回的是它的一个引用,这样一来,函数的调用者将得到一个引用,它指向一个已经不存在对象,如果你运气够好,你也许可以得到一个来自编译器的警告(例如VC 7.0将给出这样一个警告:“warning C4172: returning address of local or temporary.”),但注意不是每种编译器都会这么友好。
这是一个很常见的例子,我相信每个C++程序员都至少犯过一次这样的错误。然而,对于C++的临时对象而言,事情变得稍微有点复杂。如果我将foo的定义稍稍变一下,让它返回一个对象的拷贝,而不是引用,会发生什么情况?
MyClass foo()
{
MyClass mc;
return mc;
}
现在,当foo返回的时候,它将生成一个临时对象,这个临时对象将被赋给调用函数指定的一个变量。为了看看这一切是如何发生的,我们看看Listing 1代码的执行结果:
Default constructor
Copy constructor
Destructor
Returned from foo
Destructor
Listing 1 Function returning a temporary object
// Demonstrates returning a temporary object.
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass(const MyClass &)
{ cout << "Copy constructor\n"; }
MyClass()
{ cout << "Default constructor\n"; }
MyClass &operator=(const MyClass &)
{
cout << "Assignment operator\n";
return *this;
}
MyClass::~MyClass()
{
cout << "Destructor\n";
}
};
MyClass foo()
{
MyClass mc;
// Return a copy of mc.
return mc;
}
int main()
{
// This code generates the temporary
// object directly in the location
// of retval;
MyClass rv1 = foo();
// This code generates a temporary
// object, which then is copied
// into rv2 using the assignment
// operator.
//MyClass rv2;
//rv2 = foo();
cout << "Returned from foo\n";
return 0;
}
你也许会想,这里是不是有一个对象丢失了,毕竟,如果当你看了伪代码以后,你会认为这样一些事情是应该发生的:
在foo中,mc被声明了,它调用了缺省的构造函数,然后,foo返回了一个临时对象,这个临时对象是对mc的拷贝,并因此调用了拷贝构造函数,这个临时对象被赋值给了rv1,并再次调用拷贝构造函数。
但是请等一下,我们查看应用程序的输出,拷贝构造函数却只被调用了一次!而本来应该有三个对象生成:mc(在foo函数中),一个临时对象,以及rv1。为什么不是调用三次构造函数?这个问题的答案就是:C++标准所允许的代码优化欺骗了我们,这样做的目的是为了避免代码过于低效,如果一个临时对象作为返回值被立即赋给另一个对象,这个临时对象本身将被构造到被赋值对象在内存中的位置。这样避免了一次无谓的构造函数调用,当构造函数需要做很多初始化工作的话,这样可以节省不少时间(如果你对这方面的内容很感兴趣,请参考C++标准的第12.2节,第3段)。
另外有一个相关的例子,例如,当rv已经被声明了:
MyClass rv;
rv = foo();
这时候,临时对象将不被构造到rv的位置,因为发生foo调用的时候,rv已经被构造过了,因此,这个临时的返回值必须被做为一个独立的对象来构造,然后再赋值给rv。实际上,如果你将Listing 1代码中的注释打开,你将会得到这样一些期待的结果(括号中的注释是我加上的):
Default constructor (rv2)
Default constructor (mc)
Copy constructor (temporary)
Destructor (mc)
Assignment operator (rv2 = temporary)
Destructor (temporary)
Returned from foo
Destructor (rv2)
这里需要注意的是临时对象在它被生成的表达式执行结束的时候被销毁,换句话说,析构函数将在这句话执行的末尾被调用:
rv = foo(); // Temporary is destroyed here
(未完待续)