之前学习c++的时候,对返回值的问题一直弄得不是很明白,今天自己下决心研究一下返回值的问题,返回值的类型大概可以分为这几类:基本类型,自定义类型,指针,引用(引用的本质其实就是指针)。
1、先看返回为基本类型的
int类型的
1、先看返回基本类型的
1: #include<iostream.h>
2: #include<string.h>
3: int fun()
4: {
00401080 push ebp
00401081 mov ebp,esp
00401083 sub esp,44h
00401086 push ebx
00401087 push esi
00401088 push edi
00401089 lea edi,[ebp-44h]
0040108C mov ecx,11h
00401091 mov eax,0CCCCCCCCh
00401096 rep stos dword ptr [edi]
//前面的所有的代码都是为了保存现场和初始化该函数的栈空间。
5: int a=0x12345;
00401098 mov dword ptr [ebp-4],12345h
6: return a;
0040109F mov eax,dword ptr [ebp-4]
7: }//后面的代码为将寄存器恢复到调用之前状态,即恢复现场。
004010A2 pop edi
004010A3 pop esi
004010A4 pop ebx
004010A5 mov esp,ebp
004010A7 pop ebp
004010A8 ret
根据反汇编的代码,非浮点数的基本类型的返回值放在了eax寄存器里面
浮点数类型的
8: double *dfun()
9: {
//略去初始化代码后面不再陈述
10: double a=1.5;
004010D8 mov dword ptr [ebp-8],0
004010DF mov dword ptr [ebp-4],3FF80000h
11: return a;
004010E6 fld qword ptr [ebp-8]
13: }
double类型的返回值放在浮点寄存器里面,fld IN指令表示将数IN压入ST(0)中,ST(0)-ST(7)8个栈空间来实现浮点寄存器。有关浮点数栈空间的介绍可以参考其他资料。
2、返回值为指针类型的
(1)指向临时变量
浮点类型指针
9: double a=1.5;
004010D8 mov dword ptr [ebp-8],0
004010DF mov dword ptr[ebp-4],3FF80000h
10: return &a;
004010E6 lea eax,[ebp-8]
int类型指针
15: int * pNum;
16: int a =8;
00401118 mov dword ptr [ebp-8],8
17: pNum=&a;
0040111F lea eax,[ebp-8]
00401122 mov dword ptr [ebp-4],eax
18: return pNum;
00401125 mov eax,dword ptr [ebp-4]
由上面可见,浮点类型指针和int类型指针的返回值都是放在寄存器里面,被调用函数的临时变量的值都存放被调用函数的栈空间里面,临时变量的指针指向的地址也就是被调用函数的栈空间地址,当函数调用结束后,被调用函数的栈空间就会被清空,临时变量被销毁,此时,指向临时变量的指针已经没有意义,如果在调用函数中使用该指针来访问里面的数据就会引发错误。
(2)指向常量
22: char* pchar;
23: pchar="hello world";
00401158 mov dword ptr [ebp-4],offsetstring "hello world" (0042c01c)
24: return pchar;
0040115F mov eax,dword ptr [ebp-4]
常量存在于常量存储区,如上面的的的地址就是0042c01c,最后返回的时候,会把常量的地址返回给调用函数,常量在被调用函数结束后是不会被销毁的,在调用函数中用指针来访问该常量能得到正确的结果。静态局部变量虽然没有测试过,但是道理和常量应该是一样的。返回局部静态变量的指针也能得到正确的结果。
(3)指向动态申请的空间
23: char* pchar=new char[10];
00401148 push 0Ah
0040114A call operator new (00401230)
0040114F add esp,4
00401152 mov dword ptr [ebp-8],eax
00401155 mov eax,dword ptr [ebp-8]
00401158 mov dword ptr [ebp-4],eax
24: strcpy(pchar,"go back");
0040115B push offset string "go back" (0042c01c)
00401160 mov ecx,dword ptr [ebp-4]
00401163 push ecx
00401164 call strcpy (0040d380)
00401169 add esp,8
25: return pchar;
0040116C mov eax,dword ptr [ebp-4]
26: }
我们可以查看eax的值,这里为0x3382e68,而此时esp的值为0012ffeb,很明显eax中的地址不在该函数的栈空间中。由于动态申请的空间是在堆内存中,不在被调用函数的栈空间中,所以调用结束后不会被销毁,返回的结果后可以正确使用。但是,必须记住在调用函数里面将动态申请的空间释放掉。不然会引起内存泄露
(4)返回从调用者传递过来的指针
如果没有重新指向被调用函数的局部变量(动态申请、静态、常量除外),则返回结果是正确的。
3、返回引用
4: int a=1131;
00401098 mov dword ptr [ebp-4],46Bh
5: return a;
0040109F lea eax,[ebp-4]
注意到返回的就是变量a的地址,所以返回指针和返回引用道理是相同的。
4、返回对象
#include<iostream.h>
#include<memory.h>
class Set
{//集合类
private:
int*pNum;
int count;//元素个数
public:
Set();
~Set();
Set(constSet&);
voidAdd(int num);//添加元素
voidRemove(int num);//移除元素
Setoperator+(const Set&);
Set&operator=(const Set&);
boolcheckSameNum(int Num);//判断元素是否在集合中
friendostream& operator<<(ostream & out,const Set&);
};
Set::Set()
{
pNum=newint[100];
memset(pNum,0,100*sizeof(int));
count=0;
cout<<"Incounstructor\n";
}
Set::~Set()
{
delete[]pNum;
cout<<"Indestructor\n";
}
Set::Set(const Set& set)
{
inti;
pNum=newint[100];
count=set.count;
for(i=0;i<set.count;i++)
{
pNum[i]=set.pNum[i];
}
cout<<"Incopy constructor\n";
}
bool Set::checkSameNum(int Num)
{
inti;
for(i=0;i<count;i++)
{
if(pNum[i]==Num)return true;
}
returnfalse;
}
void Set::Add(int Num)
{
if(count>=99)return;
if(!checkSameNum(Num))
{
//count++;
pNum[count++]=Num;
}
}
void Set::Remove(int Num)
{
inti;
for(i=0;i<count;i++)
{
if(pNum[i]==Num)
{
pNum[i]=pNum[--count];
return;
}
}
}
Set Set::operator+(const Set& set)
{
inti;
Sets(*this);
for(i=0;i<set.count&&s.count<100;i++)
{
//s.count=i;
s.Add(set.pNum[i]);
}
returns;
}
Set& Set::operator=(const Set& set)
{
inti;
for(i=0;i<set.count;i++)
{
pNum[i]=set.pNum[i];
}
count=set.count;
return*this;
}
ostream& operator<<(ostream&out,const Set& set)
{
inti;
for(i=0;i<set.count;i++)
{
out<<set.pNum[i]<<" ";
if((i+1)%5==0)out<<endl;
}
returnout;
}
int main()
{
Sets0,s1,s2;
s0.Add(3);
s0.Add(7);
s1.Add(9);
s2=s0+s1;//测试句子
cout<<s0<<endl;
cout<<s1<<endl;
cout<<s2<<endl;
return0;
}
图(1)
图(2)返回局部对象s,调用者为main函数
表达式s0+s1返回局部变量s(最终返回的对象并不是s,而是一个临时的对象),s的地址(0x0012fed0)位于operator+的栈空间中,s的pNum指向的地址存在于堆空间中,函数返回的时候,先调用拷贝构造函数,将s拷贝到调用函数的栈空间中。
图(3)在main函数栈空间中拷贝构造函数,调用者为operator+
在main函数栈空间中构造临时对象,地址为(0x0012ff54)。如果自定义对象中存在动态申请的内存,必须自己重载拷贝构造函数,不然系统默认的拷贝构造函数仅仅拷贝地址,而不去开辟内存,就会有两个对象中的变量指向同一片内存区,当一个对象被析构掉后,申请的内存会被释放,再去析构另一个对象时,会发生释放已经释放的内存,出现严重的错误。
图(4)析构函数,调用者为operator+
调用析构函数,将局部变量s(0x12fed0)给析构掉,this指针指向的地址为s的地址。所以真正返回的并不是s,s在这里已经被析构掉了,而是临时对象,临时对象的值和s是一样的。
图(5)调用operator=,调用者为main函数
将临时对象(0x0012ff54)赋值给s2(0x12ff5c),如自定义对象中存在动态申请的内存,必须重载赋值运算符,原理同上面的拷贝构造函数。
图(6)临时对象(Set*)0x0012ff54
在main函数观察临时对象,将临时对象的地址强制转换成Set*即可观察到。
图(7)临时对象(0x0012ff54)调用析构函数
当赋值结束后,会将临时对象销毁,调用临时对象的析构函数。
总结:
(1)返回非指针类型,在调用函数中能得到正确的结果。字符型和整形变量返回值存放在寄存器eax中,浮点数返回值存放在浮点计数器中,自定义对象返回值存放在调用函数的栈空间中。
(2)返回指向非静态局部变量的指针或者引用,在调用函数中得不到正确的结果。
(3)返回指向常量、动态申请的内存、静态局部变量的指针时,在调用函数中能得到正确的结果,返回动态申请的内存是,一定要记得在调用函数中将动态申请的空间释放掉。