XSLT存档  

不及格的程序员-八神

 查看分类:  ASP.NET XML/XSLT JavaScripT   我的MSN空间Blog

(栈帧和函数调用三)函数返回值的传递

 

 

 

     在本系列第一篇文章的总结中,我们提到过,从该例子的汇编代码中可以看到,函数的返回值是保存在eax寄存器中,在x86环境中,eax是一个32位的寄存器,只占有4个字节,对于函数返回值超过4个字节的情况又是如何传递的呢?下面我们分为三种情况进行分析:(本文基于32位的程序分析)
(1)函数返回值在5-8字节之间
(2)函数返回值超过8字节

一,函数返回值在5-8字节之间

     对于返回5-8字节对象的情况,几乎所有的调用惯例都是采用eax和edx联合返回的方式进行的。其中,eax寄存器存储要返回的低4字节,而edx寄存器存储要返回的高4字节,此处暂不分析,后面有时间再补上。

二,函数返回值超过8字节

     将上述代码中的char大小调整为128,进入调试模式,查看反汇编代码为:
在这里插入图片描述     在调用Test函数(call Test)之前,先将栈上的一个地址(ebp-254h)存储在eax中,接着将这个地址压入栈中,接着再调用Test函数,从上一节我们分析的函数入参方式来看,这种形式很明显就是将该地址作为一个隐形参数传入了Test()中。
     5-8行表达的意思是将eax地址指向的位置上的20h个双字(4个字节)拷贝到ebp-1CCh的位置上。8-10行表达的意思是将ebp-1CCh地址指向位置上的20个h字节拷贝到info中,而info就是接收函数返回值的临时变量。
     从main函数的反汇编中,可以看到Test函数返回值是先存到ebp-1CCh指向的地址中,再由ebp-1CCh拷贝到info中的,那么Test函数是如何将返回值存于ebp-1CCh中的呢?
     我们再来看Test中的反汇编代码:
在这里插入图片描述
     直接看最后5行,将info(这是Test中的临时变量info)的地址存在esi寄存器中,再将esi的地址拷贝20h个双字(128个字节)到ebp+8中,再将ebp+8赋值给eax。此时的ebp实际指向栈上保存的旧的ebp,因此ebp+4指向压入栈中的返回地址,ebp+8则指向函数的参数(不明白原因的可以看第一篇文章((栈帧和函数调用一)栈帧,函数调用与栈的关系))。前面说过,Test函数是没有参数的,只有一个隐形参数由函数的调用方传入,那就是ebp-1CCh(这个ebp是调用Test之前的ebp,称为为old_ebp)。

     至此整个调用思路就很清晰了:
(1)函数调用方在栈上额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,我们称之为temp。
(2)将temp的地址作为隐藏参数传给被调用的函数Test()
(3)Test函数将数据拷贝给temp对象,并将temp对象的地址用存于eax寄存器中传出。
(4)Test函数返回后,函数调用方将eax指向的temp对象的内容拷贝给变量info

三,使用对象作为函数返回值

     从上面的分析中,我们可以看到,当返回值的类型过大时,C语言在函数调用返回时会使用一个临时的栈上内存区域作为中转,会导致返回值对象被拷贝两次:第一次是从被调用函数内部拷贝到栈上中转区,第二次是从栈上中转区拷贝到调用方的接收返回值中。下面我们以一个简单的例子来进行验证:

#include <iostream>

struct Info
{
	Info()
	{
		std::cout << "Info" <<std::endl;
	}
	Info(const Info& rhs)
	{
		std::cout << "Info(const Info& rhs)" << std::endl;
	}
	Info& operator=(const Info &rhs)
	{
		std::cout << "Info& operator=" << std::endl;
	}
};

Info Test()
{
	Info info;
	return info;
}

int main()
{
	Info info;
	info = Test();
	return 0;
}

     输出如下:
在这里插入图片描述
     当然上面的写法是为了展示一下效果,我们还可以将main函数中改一下来减少一次拷贝:

Info info = Test();

     那么此时就只会调用一次拷贝构造函数。此时汇编代码如下:
在这里插入图片描述
     可以看到在main函数中是直接将info的地址赋值给eax,再将eax入栈。
在这里插入图片描述
     而在Test中,在调用了拷贝构造函数之后,直接将ebp+8赋值给eax,而此时eax是等于ebp+8的,相当于直接将Test函数的返回值拷贝给了eax,也就是main中info变量的地址。

四,总结

1,正是由于函数返回对象造成的两次拷贝开销,当我们的对象较大时,这个开销会更加明显,因此,在C/C++编程中,对于超出8字节的对象,不要用作函数的返回值,而需要考虑使用指针或引用。
2,本文样例的环境是win7,vs2015,不同编译器对函数返回对象的拷贝清空不尽相同。

posted on 2022-12-05 22:43  不及格的程序员-八神  阅读(15)  评论(0编辑  收藏  举报