探讨“临时对象”(temporary object)

本秂侑毒 提交于 2019-12-10 04:12:35

MSDN中对VS2012版本的临时对象的说明如下:

    在某些情况下,编译器有必要产生临时对象。

  1.     当初始化一个 常量引用 const reference)时,如果给定的初始化对象类型与目标引用类型不同(但是两者  能够相互转换),需要产生临时对象;
  2.     当函数的返回值是用户自定义类型,且程序中未将此返回值拷贝到其他对象中时,需要产生临时对象;
  3.     当给定的对象显式向自定义对象类型转换时,产生临时对象;

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是不够的,此时常常会利用到其他的寄存器(例如ECXEDX)等来进行转存容器。自定义的类对象,通常通过在栈中开辟一块空间(大小由编译器根据类对象的大小自动设定)来转存返回值。所以可以简单的认为在栈中开辟的转存空间就是一份返回值的副本,即“临时对象”。通过寄存器的转存,我们不能说是“临时对象”,因为CPU的一切数据操作都是通过各种寄存器来完成的,况且寄存器中的数据也不存在声明周期问题,随时有可能被覆盖掉,当然如果进行了进栈操作,那就可以叫做临时对象了。

示例代码如下:

main()函数中测试代码:


                                  图一

Point是自定义的一个二维平面点类(因为如果直接定义成简单的POD类,编译器会直接将类进行优化,当做两个单独的整型数据成员来看,所以在定义类时,添加了自己的构造函数、析构函数、operator +、取值和设定函数)。类结构如下:



                             图二

程序的直接运行(CTRL+F5)结果如下:



由输出结果可以直接看出,构造函数与析构函数不对称,其中0012FE54处的对象只调用了析构函数,但是没有调用构造函数。所以可以怀疑0012FE54应该是一个“temporary object”。(但是为什么可以不调用构造函数,却要调用析构函数呢?这一点我还没搞明白,有待考究一下)。

下面单步调试进入到函数体中,看看究竟:

首先进入到(图一)中的(1)处:

其反汇编代码如下:

其中,004111C2是构造函数Point(int x, int y)在跳转表中的位置,如下:



如图所示,再次跳转到Pointint x, int y)构造函数00411720处。

Pointint x, int y)构造函数反汇编代码如下:


可以用下面的示意图来表示有参构造函数的操作过程:



同样,运行到(图一)中(2)时,结果完全相同:

示意图如下:



然后到(图一)中的(3)处:

此时调用的是无参构造函数,其反汇编代码如下:



操作过程示意图如下;



最后,到了我们此部分的重点,p3=p1+p2;

其反汇编代码如下:



可以看得出来,在调用operator+00411113)函数时,压入了三个参数进栈:分别是p1的地址、p2的地址,和0x0012FF54(——我们所找的临时对象)。下面就进入到了oeprator+友元函数体中,其反汇编代码如下:



从反汇编代码可以看出,在return p3;语句时,将局部对象p30x0012FE10)拷贝到临时对象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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!