关闭

返回值研究

194人阅读 评论(1) 收藏 举报

之前学习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)


刚开始时,s0,s1,s2在main函数栈空间中的地址,对象占8个字节(int和int*分别占4个字节)。

 

图(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)返回指向常量、动态申请的内存、静态局部变量的指针时,在调用函数中能得到正确的结果,返回动态申请的内存是,一定要记得在调用函数中将动态申请的空间释放掉。
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1279次
    • 积分:28
    • 等级:
    • 排名:千里之外
    • 原创:2篇
    • 转载:0篇
    • 译文:0篇
    • 评论:2条
    文章分类
    文章存档
    阅读排行
    评论排行
    最新评论