您现在的位置是:首页 > 文章详情

一个关于临时对象的BUG

日期:2018-11-28点击:396

转自:https://blog.csdn.net/TeddyWing/article/details/13170

博主看完这篇博客之后,感觉自己不会C++了,呜呜呜

 

我相信任何一个使用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代码的执行结果:

// ConsoleApplication2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include "pch.h" #include <iostream> using namespace std; // 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() { 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(); cout << "----------------------------\n"; // 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

这一切看起来都非常美妙,但是如果是下面这个例子,会发生什么情况呢?

MyClass &mc = foo();

上面那句在我的编译器visual studio里面报错了

现在将不是将临时对象拷贝到新的对象上面,我仅仅是将它赋值给一个引用,(请注意,这和最开始那个例子有一点区别,在第一个例子里面,我将一个局部变量的引用做为了函数返回值,而在这个例子里,我是将一个函数返回的临时变量的引用赋值给一个变量)。那么,现在将会发生什么情况呢?临时对象将在什么时候被销毁呢?如果它还是在表达式执行的执行结束的时候被销毁,就如同上面那个例子一样,这段代码将因为一个指向不存在的对象的应用而彻底完蛋。但是,请注意,C++标准同意对这种情况提出一种不同寻常的解决办法:如果一个临时对象被赋值给一个引用,这个临时对象在这个引用的生命周期中将不能被销毁。换句话说,不同于返回一个局部变量的引用,将一个引用绑定到一个临时对象上是完全合法的,任何时候使用这个引用,这个对象都应该是有效的。
 

 

The BUG


我猜我只能说仅仅当编译器恰当的实现了C++标准,这个引用才可能是有效的。Eugene Gershnik将Listing 2所列的代码发送给了我,它有一个叫foo的函数,返回了一个std::vector<char>类型的临时对象,并且这个对象被赋值给了一个引用,当这段程序在VC7的Release模式下编译并运行时并没有问题,但是当它在Debug模式下运行时,我得到了这样一个错误:
 

“The instruction at “0x004121b5”referenced memory at “0x00000000”.The memory could not be “read”.
#include "pch.h" #include <iostream> #include <cstdio> #include<vector> //Assigning a reference to a temporary object // Problem with reference bound to temporary // The function foo returns a temporary object // of type std::vector<char>, which is then bound // to a reference of type const std::vector<char>&. // When the expression "int m[80] = {0};" is // executed, // the reference bar no longer seems to be valid, // and the program will crash in the call to // printf. // Removing the line "int m[80] = {0};" eliminates // the problem. // // Compile with VC7, with the "Program Database // for Edit and Continue" debug option. std::vector<char> foo() { std::vector<char> ret(20); return ret; } int main() { const std::vector<char> &bar = foo(); int m[80] = { 0 }; std::printf("%d\n", bar[0]); return 0; } 

(博主的visual studio并没有报错,还给了返回的结果)

 

开始的时候我猜想由于Release版本对“int m[80]= {0};”这一行的优化造成了这种结果,因为这行代码确实什么也没有做,我认为这种优化消除了在Release下的错误。但是当我将代码发给Microsoft(我去,都是大佬啊),并征求他们的意见的时候,Jeff Peil给我回了信,并指出了真正的原因所在:

这个问题是由于编辑-继续调试支持(edit-and-continue debugging support)的功能而造成的。这个错误将在即将释放的下一版的Visual C++中得到修正,你可以通过将编辑-继续调试支持从编译选项中去掉而避免这个错误发生(编译时仍然会产生调试信息,你需要做的仅仅是将/ZI选项替换成 /Zi选项)。当然,另外一个解决办法就是不将引用绑定到临时对象上去, 这样你可以继续使用编辑-继续调试支持功能,就像下面的代码那样:

int main() { std::vector<char> bar; std::swap(bar,foo()); int m[80]={0}; std::printf(“%d/n”, bar[0]); return 0; }                                                                              —Jeff Peil

虽然性能上有一点点损失,但Jeff的代码工作的很好。交换这两个Vector的内容粗看起来是一个代价昂贵的操作,但实际上所有的交换工作就是交换两个数组中很少的内部变量,这其中并不涉及到缓冲区的拷贝,因此交换将在常量时间内完成。

当然如果你想简单的解决这个问题,可以将表达式“int m[80]= {0};”移动到声明变量bar之前。因为它们之间不存在什么倚赖关系,先声明任何一个都是没有关系的。

原文链接:https://yq.aliyun.com/articles/681116
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章