有关于返回值的理解产生了许多问题:函数定义和声明时指定返回值的作用是什么?返回值以什么方式返回上层函数?什么时候函数返回产生临时对象?等等。
下面是我的一些个人理解,如有错误欢迎指正。
函数如何返回一个值给调用者?
没耐心看上面的博客也可以直接看下面的结论:
了解了函数的调用过程后,我们其实就可以发现,在被调用的函数调用结束之际,会把想要返回给上层的值存放在寄存器中(返回类型是引用或者返回的是大型对象,就把返回值的地址存在rax(不一定是rax)中;返回值为值返回类型的小型对象就把返回值拷贝到eax寄存器(不一定是eax)中),而后调用者访问这个寄存器就可以获取返回值了。
接下来证明一下上述所说:
1.返回值类型为引用类型:
class A { public: A() = default; A(const int& x) { _a = x; } private: int _a; }; int& func1() { int a = 0; return a; } const int& func2() { int a = 0; return 2; } const A& func3() { return A(); } const A& func4() { return 2; }
func1中return的汇编如下:
lea rax,[a] 这行汇编的意思就是将a的地址拷贝到rax寄存器里。
func2中return的汇编如下:
mov dword ptr [rbp+0E4h],2 表示将2这个常量拷贝到[rbp+0E4h]这个地址所指向的空间处
lea rax,[rbp+0E4h] 表示将[rbp+0E4h]这个地址拷贝到rax寄存器。
有意思的是,如果返回值是常量,并且返回类型是引用,那么系统会开辟一个临时空间把常量拷贝进去,而rax存放的就是临时空间的地址!-----其实,这就是我们所说的临时对象,以及它是如何被创建的。
func3中return的汇编如下:
其他指令用于初始化对象不用关心,最终仍旧是把匿名对象的地址拷贝到rax寄存器。匿名对象也是一种临时对象
func4中return的汇编如下:
lea rcx,[rbp+0E4h]是把2传给构造函数当做参数
lea rcx,[rbp+0c4h] 是把对象的地址传给构造函数(传this指针),也就是说系统开辟了一个临时空间并且在这个空间存放用2构造的临时对象。
最后把临时对象的地址存入rax。
2.返回值为值返回类型:
int func5() { int a = 0; return a; } int func6() { return 2; } A func7() { return A(); } A func8() { return 2; }
func5中return的汇编如下:
mov eax,dword ptr [a] 表示把a的值拷贝到eax寄存器
func6中return的汇编如下:
mov eax,2 表示把2拷贝到eax寄存器
func7中return的汇编如下:
其他指令用于初始化对象不用关心,最终仍旧是把匿名对象的地址拷贝到rax寄存器。匿名对象也是一种临时对象(虽然是传值返回类型,但因为返回的是大型对象,所以返回的是地址而不是把值直接拷贝到寄存器)。
func8中return的汇编如下:
汇编代码类似func4,也是构造了一个临时对象。(虽然是传值返回类型,但因为返回的是大型对象,所以返回的是地址而不是把值直接拷贝到寄存器)。
函数定义和声明时指定返回值类型的作用是什么?
从上面的例子也可以看出,返回值类型的一个重要作用就是决定编译器如何传递返回值。当然,还有类型检查等多个作用。
临时对象什么时候产生?
接下来我将调用上述所有函数并查看他们的汇编代码:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> using namespace std; class A { public: A() = default; A(const int& x) { _a = x; } private: int _a; }; int& func1() { int a = 0; return a; } const int& func2() { int a = 0; return 2; } const A& func3() { return A(); } const A& func4() { return 2; } int func5() { int a = 0; return a; } int func6() { return 2; } A func7() { return A(); } A func8() { return 2; } int main() { int x1 = func1(); int& y1 = func1(); int x2 = func2(); const int& y2 = func2(); A x3 = func3(); const A& y3 = func3(); A x4 = func4(); const A& y4 = func4(); int x5 = func5(); const int& y5 = func5(); int x6 = func6(); const int& y6 = func6(); A x7 = func7(); const A& y7 = func7(); A x8 = func8(); const A& y8 = func8(); }
x1,y1,x2,y2,x3,y3,x4,y4,x5,x6,x7,x8在调用相应函数赋值的时候都不产生临时对象,直接操作寄存器即可。
y5,y6把寄存器eax中的值拷贝到一个另外的空间,然后引用这个空间,因为直接引用eax寄存器明显不合理,他只是个工具,不是存储者。这个过程中产生了临时对象,也就是新开辟的空间。
y7,y8这里其实是应用了优化。函数的返回值是函数销毁之前创建的临时对象,正常来说这个临时对象应该是存储在本函数的栈帧中,因为当前函数栈是本函数,一般在本函数内操作比较容易,并且为了防止临时对象随本函数栈销毁而销毁,在调用结束后,如果调用者要用到返回值,需要在被调用函数栈销毁之前将临时对象拷贝到调用者的函数栈里,以延长其生命周期。优化就是根据调用,直接告诉被调用的函数,你应该把临时对象返回值存在我的(即调用者)函数栈的哪个位置,这样就直接把临时对象构造在调用者函数栈里,节省了一次拷贝。
y7,y8中关于return的第一句汇编代码就是把调用者的栈帧中的一块空间的地址给被调用的函数,告诉它把返回值构造在这儿。
分析了函数返回值获取的过程,以及调用函数的过程。可以发现,这两个过程中都有可能产生临时对象。总之,临时对象在需要的时候产生。比方说要引用的值在eax中,这就需要创建临时对象把eax的值拷贝下来,因为eax一般不能被引用。再比如func8()返回值的类型和函数返回类型不同,需要先构建临时对象,将返回值转换成需要的类型。