本篇博客,我们来看看,在C/C++里面函数的return 关键字究竟做了什么工作,我们从return 基本的数据类型 像int/char/void */,到带构造函数的类,一步步分析。
return int/char,void* 以及他们的引用···
#include <stdio.h>
#include <stdlib.h>
int bfunc()
{
int rst = 0;
return rst;
}
void * bfunc2()
{
void * p = NULL;
return p;
}
char & bfun3()
{
char ch = 0;
return ch;
}
int main()
{
int ret1 = bfunc();
bfunc2();
void * p = bfunc2();
bfun3();
char ch = bfun3();
system("pause");
}
代码很简单,从int ret1=bfunc()开始,该函数返回一个基本类型int,我们来看看汇编结果:
45: int ret1 = bfunc();
00DD15EE call bfunc (0DD11FEh)
00DD15F3 mov dword ptr [ret1],eax
在00DD15EE 调用bfunc()
我们进入bfun():
12: int bfunc()
13: {
00DD1490 push ebp
00DD1491 mov ebp,esp
00DD1493 sub esp,0CCh
00DD1499 push ebx
00DD149A push esi
00DD149B push edi
00DD149C lea edi,[ebp-0CCh]
00DD14A2 mov ecx,33h
00DD14A7 mov eax,0CCCCCCCCh
00DD14AC rep stos dword ptr es:[edi]
14: int rst = 0;
00DD14AE mov dword ptr [rst],0
15: return rst;
00DD14B5 mov eax,dword ptr [rst]
16: }
00DD14B8 pop edi
00DD14B9 pop esi
00DD14BA pop ebx
00DD14BB mov esp,ebp
00DD14BD pop ebp
00DD14BE ret
我们看最后的几步:
15: return rst;
00DD14B5 mov eax,dword ptr [rst]
可知 return rst是把局部变量rst的值保存在eax寄存器;
最后,函数调用完毕,执行
00DD15F3 mov dword ptr [ret1],eax
将刚刚寄存器eax里面的内容给[ret1].
这个例子相当简单,但是不妨碍它说明了函数return 一个基本数据类型的时候是先把返回结果放在eax这一规则。
ok,继续看。
接下来是
bfunc2();
void * p = bfunc2();
其中bfunc2()的返回值并没有保存下来,void *p =bfun2()则将返回值保存到p里面了。
我们看看汇编结果:
47: bfunc2();
00FA15F6 call bfunc2 (0FA1208h)
48: void * p = bfunc2();
00FA15FB call bfunc2 (0FA1208h)
00FA1600 mov dword ptr [p],eax
首先 bfun();和void * p=bfunc2()区别在于 是否有把eax的值mov到指定的内存。
然后 我们深入bfun():
18: void * bfunc2()
19: {
00FA1440 push ebp
00FA1441 mov ebp,esp
00FA1443 sub esp,0CCh
00FA1449 push ebx
00FA144A push esi
00FA144B push edi
00FA144C lea edi,[ebp-0CCh]
00FA1452 mov ecx,33h
00FA1457 mov eax,0CCCCCCCCh
00FA145C rep stos dword ptr es:[edi]
20: void * p = NULL;
00FA145E mov dword ptr [p],0
21: return p;
00FA1465 mov eax,dword ptr [p]
22: }
00FA1468 pop edi
00FA1469 pop esi
00FA146A pop ebx
00FA146B mov esp,ebp
00FA146D pop ebp
00FA146E ret
与return int的类似,也是在return的时候把return 后面的表达式的值保存到eax寄存器中。
这是因为void * 指针 变量其实也是c/C++的基本数据类型。
ok,那我们看看返回引用的那个函数的调用:
49: bfun3();
00FA1603 call bfun3 (0FA1203h)
50: char ch = bfun3();
00FA1608 call bfun3 (0FA1203h)
00FA160D mov al,byte ptr [eax]
00FA160F mov byte ptr [ch],al
首先在调用上,char ch=bfun3()比bfunc3()多了
mov al,byte ptr[eax];
mov byte ptr [ch],al
为什么是与al相关呢?这是因为 char是8位的,只需要al就可以了。
我们看内部:
23: char & bfun3()
24: {
00FA1560 push ebp
00FA1561 mov ebp,esp
00FA1563 sub esp,0D0h
00FA1569 push ebx
00FA156A push esi
00FA156B push edi
00FA156C lea edi,[ebp-0D0h]
00FA1572 mov ecx,34h
00FA1577 mov eax,0CCCCCCCCh
00FA157C rep stos dword ptr es:[edi]
00FA157E mov eax,dword ptr ds:[00FA8000h]
00FA1583 xor eax,ebp
00FA1585 mov dword ptr [ebp-4],eax
25: char ch = 0;
00FA1588 mov byte ptr [ch],0
26: return ch;
00FA158C lea eax,[ch]
27: }
00FA158F push edx
00FA1590 mov ecx,ebp
00FA1592 push eax
00FA1593 lea edx,ds:[0FA15B4h]
00FA1599 call @_RTC_CheckStackVars@8 (0FA108Ch)
00FA159E pop eax
00FA159F pop edx
00FA15A0 pop edi
00FA15A1 pop esi
00FA15A2 pop ebx
00FA15A3 mov ecx,dword ptr [ebp-4]
00FA15A6 xor ecx,ebp
00FA15A8 call @__security_check_cookie@4 (0FA1023h)
00FA15AD mov esp,ebp
00FA15AF pop ebp
00FA15B0 ret
哈哈 关键来啦:
26: return ch;
00FA158C lea eax,[ch]
lea eax,[ch] ====>emmmmm,取ch的有效地址。
so,这下子应该心里有底了吧!返回引用的时候,其实是返回 ch变量的有效地址,而这个变量是个局部变量,该有效地址所指向的内存是有可能被下一次函数调用的函数栈帧破坏的,所以返回一个局部变量的引用可以说是很危险的。
返回地址 后那怎么搞嘞?
50: char ch = bfun3();
00FA1608 call bfun3 (0FA1203h)
00FA160D mov al,byte ptr [eax]
00FA160F mov byte ptr [ch],al
注意看:调用完 bfun3后,eax存着bfun3()函数里面ch的有效地址,接着首先通过mov al, byte ptr[eax],指令将eax的值视为一个byte类型的有效地址,取出这个地址下的内存的值,保存到al中,然后再将al的值mov到 main函数的ch变量中。
下面我们看一点高级的:
return struct/class,以及它们的引用
先看实例代码:
#include <stdio.h>
#include <stdlib.h>
#pragma pack(1)
struct Person
{
int age;
int id;
int somethingelse;
char name[12];
void display()
{
printf("age :%X id:%X\n", age, id);
printf("age:%X \n", &age);
printf("id:%X \n", &id);
printf("somethingelse:%X \n", &somethingelse);
printf("name:%X \n", name);
}
Person()
{
printf("Call Person()\n");
}
Person(const Person&)
{
printf("Call Person(const Person&)\n");
}
};
Person sfunc2()
{
Person p ;
return p;
}
Person sfunc3()
{
int length = sizeof(Person);
char * rst = new char[length];
for (int i = 0; i < length; i++)
{
rst[i] = 0xBB;
}
return *((Person*)(rst));
}
Person& sfunc4()
{
Person* p = new Person();
return *p;
}
int main()
{
Person p;
sfunc2().display();
p = sfunc2();
p.display();
sfunc3().display();
sfunc4().display();
p = sfunc4();
p.display();
system("pause");
return 0;
}
其中有一个Person的结构体,里面有4+4+4+12=24个字节的变量,以及定义了一个默认构造函数和复制构造函数。
然后分别有三个函数sfunc(),sfunc1(),sfunc2(),代表了返回栈对象,返回动态对象,返回动态对象的引用的情况。
main函数里面,获取返回值的也有不获取返回值的。
ok,我们先看sfunc2().display()的情况。
这个过程先调用sfunc2(),sfunc2()返回一个Person对象,然后再调用这个对象的display()方法。
51: sfunc2().display();
013F1890 lea eax,[ebp-140h]
013F1896 push eax
013F1897 call sfunc2 (013F11E0h)
013F189C add esp,4
013F189F mov ecx,eax
013F18A1 call Person::display (013F1046h)
其过程为:
取[ebp-140h]的有效地址保存到eax,然后把eax压栈,然后再去调用sfunc2()。我们知道一般调用函数前的push的都是函数的参数,但是明明sfunc2()无须参数,那为什么会把ebp-140hpush进去嘞?我们接着看:
先记录一下刚刚压入的ebp-140H的值为0x00eff9f8
,待会我们还会看到这个地址。
进入Person sfunc2()内部:
28: Person sfunc2()
29: {
013F1610 push ebp
013F1611 mov ebp,esp
013F1613 sub esp,0E4h
013F1619 push ebx
013F161A push esi
013F161B push edi
013F161C lea edi,[ebp-0E4h]
013F1622 mov ecx,39h
013F1627 mov eax,0CCCCCCCCh
013F162C rep stos dword ptr es:[edi]
013F162E mov eax,dword ptr ds:[013FA000h]
013F1633 xor eax,ebp
013F1635 mov dword ptr [ebp-4],eax
30: Person p ;
013F1638 lea ecx,[p]
013F163B call Person::Person (013F11FEh)
31: return p;
013F1640 lea eax,[p]
013F1643 push eax
013F1644 mov ecx,dword ptr [ebp+8]
013F1647 call Person::Person (013F102Dh)
013F164C mov eax,dword ptr [ebp+8]
32: }
013F164F push edx
013F1650 mov ecx,ebp
013F1652 push eax
013F1653 lea edx,ds:[13F1680h]
32: }
013F1659 call @_RTC_CheckStackVars@8 (013F1096h)
013F165E pop eax
013F165F pop edx
013F1660 pop edi
013F1661 pop esi
013F1662 pop ebx
013F1663 mov ecx,dword ptr [ebp-4]
013F1666 xor ecx,ebp
013F1668 call @__security_check_cookie@4 (013F101Eh)
013F166D add esp,0E4h
013F1673 cmp ebp,esp
013F1675 call __RTC_CheckEsp (013F1154h)
013F167A mov esp,ebp
013F167C pop ebp
013F167D ret
重点看return p后面的代码:
30: Person p ;
013F1638 lea ecx,[p]
013F163B call Person::Person (013F11FEh)
31: return p;
013F1640 lea eax,[p]
013F1643 push eax
013F1644 mov ecx,dword ptr [ebp+8]
013F1647 call Person::Person (013F102Dh)
013F164C mov eax,dword ptr [ebp+8]
32: }
首先 取p的地址保存到eax寄存器,然后把eax的地址压栈。
接着取ebp+8的地址到ecx寄存器,我们都知道ebp+4是函数返回地址,而ebp+8其实是函数的传入参数,也就是在main函数中汇编代码压入的
0x00eff9f8
,
接下来call Person::Person(013F102Dh)
22: Person(const Person&)
013F1420 push ebp
013F1421 mov ebp,esp
013F1423 sub esp,0CCh
013F1429 push ebx
013F142A push esi
013F142B push edi
013F142C push ecx
013F142D lea edi,[ebp-0CCh]
013F1433 mov ecx,33h
013F1438 mov eax,0CCCCCCCCh
013F143D rep stos dword ptr es:[edi]
013F143F pop ecx
013F1440 mov dword ptr [this],ecx
23: {
24: printf("Call Person(const Person&)\n");
013F1443 mov esi,esp
013F1445 push 13F78BCh
013F144A call dword ptr ds:[13FB118h]
013F1450 add esp,4
013F1453 cmp esi,esp
013F1455 call __RTC_CheckEsp (013F1154h)
25: }
013F145A mov eax,dword ptr [this]
013F145D pop edi
013F145E pop esi
013F145F pop ebx
013F1460 add esp,0CCh
013F1466 cmp ebp,esp
013F1468 call __RTC_CheckEsp (013F1154h)
013F146D mov esp,ebp
013F146F pop ebp
013F1470 ret 4
关键代码在:
013F142C push ecx
013F142D lea edi,[ebp-0CCh]
013F1433 mov ecx,33h
013F1438 mov eax,0CCCCCCCCh
013F143D rep stos dword ptr es:[edi]
013F143F pop ecx
013F1440 mov dword ptr [this],ecx
首先把ecx的值压入栈顶,后面又给弹回来到ecx寄存器,而此时这个ecx的值其实就是0x00eff9f8
,
然后就是 013F1440 mov dword ptr [this],ecx 这一句揭示了ecx的0x00eff9f8
其实就当前构造函数的this指针。
我们回想一下0x00eff9f8
这个地址其实是在刚刚main()函数栈帧里面的某一个地址,因为调用的是返回对象的函数,所以编译器会先在调用这个函数(在上面的例子中是main函数)的所在的函数栈上搞出一个匿名的对象出来,并将这个对象的指针动作这个返回对象函数(如sfunc2())的一个参数压入栈中。在sfunc2()函数的return的时候,就要调用这个调用这个匿名对象的复制构造函数将匿名对象的各成员进行初始化或者赋值。
我们再看sfunc2()在调用完这个复制构造函数后:
31: return p;
013F1640 lea eax,[p]
013F1643 push eax
013F1644 mov ecx,dword ptr [ebp+8]
013F1647 call Person::Person (013F102Dh)
013F164C mov eax,dword ptr [ebp+8]
32: }
mov eax, dword ptr [ebp+8]
将匿名对象的地址放到eax寄存。这样sfunc2()就执行完了,接着:
51: sfunc2().display();
013F1890 lea eax,[ebp-140h]
013F1896 push eax
013F1897 call sfunc2 (013F11E0h)
013F189C add esp,4
013F189F mov ecx,eax
013F18A1 call Person::display (013F1046h)
将匿名对象的地址由eax存到ecx,再调用display()函数,在display函数中通过ecx寄存可以知道当前display内部的this指针是0x00eff9f8
013F189F mov ecx,eax
013F18A1 call Person::display (013F1046h) 。
return 栈对象的流程为:
1。在调用返回一个对象的函数前,会在当前的函数所在的作用域生成一块栈内存区域用于存储一个匿名的对象,这个对象没有调用过构造函数。编译器将这个匿名对象的内存首地址作为一个参数压入栈顶,共该返回对象的函数使用。
2。return的时候,用return 后面表达式的对象作为参数来调用 1 中的匿名对象的复制构造函数。
3。把匿名对象的地址存入eax
4。ret 返回原函数。
然后原函数通过eax里面的匿名对象的指针来获取这个对象的数据。
而
p = sfunc2();
p.display();
比sfunc2().display()多了一步将函数调用后eax所指向的匿名对象赋值给p的过程。其他一样。
sfunc2()与sfunc3()一样,
但是sfunc4()有点特殊。
调用过程:
55: sfunc4().display();
013F18F6 call sfunc4 (013F1082h)
013F18FB mov ecx,eax
013F18FD call Person::display (013F1046h)
没有像上面返回对象的函数那样将 某个匿名对象的地址压栈。
函数内部:
43: Person& sfunc4()
44: {
013F1770 push ebp
013F1771 mov ebp,esp
013F1773 push 0FFFFFFFFh
013F1775 push 13F512Eh
013F177A mov eax,dword ptr fs:[00000000h]
013F1780 push eax
013F1781 sub esp,0E8h
013F1787 push ebx
013F1788 push esi
013F1789 push edi
013F178A lea edi,[ebp-0F4h]
013F1790 mov ecx,3Ah
013F1795 mov eax,0CCCCCCCCh
013F179A rep stos dword ptr es:[edi]
013F179C mov eax,dword ptr ds:[013FA000h]
013F17A1 xor eax,ebp
013F17A3 push eax
013F17A4 lea eax,[ebp-0Ch]
013F17A7 mov dword ptr fs:[00000000h],eax
45: Person* p = new Person();
013F17AD push 18h
013F17AF call operator new (013F11A4h)
013F17B4 add esp,4
013F17B7 mov dword ptr [ebp-0E0h],eax
013F17BD mov dword ptr [ebp-4],0
013F17C4 cmp dword ptr [ebp-0E0h],0
013F17CB je sfunc4+70h (013F17E0h)
013F17CD mov ecx,dword ptr [ebp-0E0h]
013F17D3 call Person::Person (013F11FEh)
013F17D8 mov dword ptr [ebp-0F4h],eax
013F17DE jmp sfunc4+7Ah (013F17EAh)
013F17E0 mov dword ptr [ebp-0F4h],0
013F17EA mov eax,dword ptr [ebp-0F4h]
013F17F0 mov dword ptr [ebp-0ECh],eax
013F17F6 mov dword ptr [ebp-4],0FFFFFFFFh
013F17FD mov ecx,dword ptr [ebp-0ECh]
013F1803 mov dword ptr [p],ecx
46: return *p;
013F1806 mov eax,dword ptr [p]
47: }
它没有调用匿名对象的复制构造函数,(因为它根本不需要匿名对象了),而是直接将p的有效地址保存到eax.
这也就解释了为什么 函数返回对象的引用一般比返回对象更高效一些:
返回引用无匿名对象产生,无须调用匿名对象的构造函数,自然也就没有匿名对象的析构问题。
总结
1.return 基本数据类型(char,int,void*,float,double)是,将中间结果保存至eax,调用者通过访问eax寄存器来获取函数返回结果。
2.返回引用其实返回的是被引用的对象的地址
3.A函数调用返回对象的函数F时,会在A的栈帧生成一个匿名对象,然后再F return的时候调用这个匿名对象的复制构造函数将return 表达式里面对象的成员变量的值传送到匿名对象中。
4.A函数调用返回对象引用的函数F时,F会将待引用的地址返回,无匿名对象,无匿名对象的复制构造函数一说。