MSDN中对VS2012版本的临时对象的说明如下:
在某些情况下,编译器有必要产生临时对象。
-
当初始化一个常量引用(const reference)时,如果给定的初始化对象类型与目标引用类型不同(但是两者 能够相互转换),需要产生临时对象;
-
当函数的返回值是用户自定义类型,且程序中未将此返回值拷贝到其他对象中时,需要产生临时对象;
-
当给定的对象显式向自定义对象类型转换时,产生临时对象;
IBM官网上的给出的描述如下:
C++中编译器有些时候有必要产生临时对象。通常在初始化引用、计算(评估evaluation)含有标砖类型转换的表达式、参数传递、函数返回、评估异常抛出表达式(throw expression)。
参考资料:
http://msdn.microsoft.com/en-us/library/a8kfxa78(v=vs.110).aspx
http://publib.boulder.ibm.com/infocenter/comphelp/v7v91/index.jsp?topic=%2Fcom.ibm.vacpp7a.doc%2Flanguage%2Fref%2Fclrc03cplr116.htm
总之,读完之后,是不是还感觉临时对象捉摸不定呢?的确,C++标准中没有明确给出临时对象的产生规则和条件,由编译器自动产生的,不管是处于效率还是其他原因,各编译器之间的产生时机和方式都略有不同,下面就MSVC编译器进行一些基本的探讨,下面的代码都是在VS2008环境下编译通过的。
情况一:
通过不同设数据类型来初始化常量引用
在(1)代码处,设置断点,进行调试,查看反汇编如下:
其中fild dword ptr[ebp-8] 是将整型变量iNum转换成浮点型(float)并压栈(相当于复制了一份iNum的数据存储到浮点寄存器中)。【有关浮点数的汇编指令,可参见百度百科相关说明】。然后fstp dwrod ptr[ebp-20h]是出栈指令,将刚才存储的浮点数据转存到ebp-20h栈空间中。也就是说ebp-20h处就是我们要找的“临时变量”。通过对比const float& r10=fNum;一行的反汇编指令,结果就更明显了。
情况二:当函数返回值是自定义类型时。
我们知道,通常函数的返回值有两种方式来传递:寄存器和栈。内置数据类型,通常都是通过寄存器(MSVC下是EAX)来直接作为返回值的存储容器,将结果由被调用函数传递给调用者。但是如果返回值是字符串、数组等比较大的数据结构时,一个32位的寄存器EAX是不够的,此时常常会利用到其他的寄存器(例如ECX、EDX)等来进行转存容器。自定义的类对象,通常通过在栈中开辟一块空间(大小由编译器根据类对象的大小自动设定)来转存返回值。所以可以简单的认为在栈中开辟的转存空间就是一份返回值的副本,即“临时对象”。通过寄存器的转存,我们不能说是“临时对象”,因为CPU的一切数据操作都是通过各种寄存器来完成的,况且寄存器中的数据也不存在声明周期问题,随时有可能被覆盖掉,当然如果进行了进栈操作,那就可以叫做临时对象了。
示例代码如下:
main()函数中测试代码:
图一
Point是自定义的一个二维平面点类(因为如果直接定义成简单的POD类,编译器会直接将类进行优化,当做两个单独的整型数据成员来看,所以在定义类时,添加了自己的构造函数、析构函数、operator +、取值和设定函数)。类结构如下:
图二
程序的直接运行(CTRL+F5)结果如下:
由输出结果可以直接看出,构造函数与析构函数不对称,其中0012FE54处的对象只调用了析构函数,但是没有调用构造函数。所以可以怀疑0012FE54应该是一个“temporary object”。(但是为什么可以不调用构造函数,却要调用析构函数呢?这一点我还没搞明白,有待考究一下)。
下面单步调试进入到函数体中,看看究竟:
首先进入到(图一)中的(1)处:
其反汇编代码如下:
其中,004111C2是构造函数Point(int x, int y)在跳转表中的位置,如下:
如图所示,再次跳转到Point(int x, int y)构造函数00411720处。
Point(int x, int y)构造函数反汇编代码如下:
可以用下面的示意图来表示有参构造函数的操作过程:
同样,运行到(图一)中(2)时,结果完全相同:
示意图如下:
然后到(图一)中的(3)处:
此时调用的是无参构造函数,其反汇编代码如下:
操作过程示意图如下;
最后,到了我们此部分的重点,p3=p1+p2;
其反汇编代码如下:
可以看得出来,在调用operator+(00411113)函数时,压入了三个参数进栈:分别是p1的地址、p2的地址,和0x0012FF54(——我们所找的临时对象)。下面就进入到了oeprator+友元函数体中,其反汇编代码如下:
从反汇编代码可以看出,在return p3;语句时,将局部对象p3(0x0012FE10)拷贝到临时对象0x0012FE54(进入函数前压入栈中的第三个参数——我们要找的临时对象)中。
拷贝完成后,局部变量p3(0x0012FE10)调用其析构函数"call 00411037"。此时,由于p3=p1+p2求值表达式(evaluation expression)还未完成,所以临时对象(0012FE54)并未调用其析构函数。
接着往下看。
然后将临时对象中的值拷贝到了主函数的局部对象p3中(如图中红色代码所示)。至此,求值表达式运算完成,意味着临时对象(0x0012FE54)的生存已失去意义,遂将0x0012FE54复制到ECX中压栈,然后调用析构函数。
函数最后:return 0;时,主函数中的p1,p2,p3生命周期也就结束了,所以按照与构造函数相反的顺序依次调用其析构函数,反汇编代码如下:
参考书籍:
《深度探索C++对象模型》
《C++反汇编与逆向分析技术揭秘》
注:文中是个人学习过程中的笔记,欢迎大家批评指正,交流沟通才能进步。
个人邮箱:zssure@163.com