zoukankan      html  css  js  c++  java
  • 对象的传值与返回

    对象的传值与返回

     

    说起函数,就不免要谈谈函数的参数和返回值。一般的,我们习惯把函数看作一个处理的封装(比如黑箱),而参数和返回值一般对应着处理过程的输入和输出。这种情况下,参数和返回值都是值类型的,也就是说,函数和它的调用者的信息交流方式是用过数据的拷贝来完成,即我们习惯上称呼的“值传递”。但是自从引入了“引用”的概念后,函数的传统模型就不再那么“和谐”了。引用的传递可以允许函数和调用者共享数据对象,它们之间的信息交流不再使用信息拷贝的方式,而是使用更有效率的信息共享的方式,引用导致函数的参数并有输入和输出的双重功能。然而,事物总有两面性,信息共享带来方便的同时也带来了一定的不安全性。我们这里并不讨论函数的使用和设计,我们关注与函数参数和返回值的传递方式。

    对于内置数据类型的参数和返回值,函数实际参数的传递一般是通过压栈完成,函数执行时会从栈内取出参数的值进行计算。在32处理器上,push指令一次只能压入4个字节的数据,那么对于long long就需要两次压栈指令了,而double类型参数就需要sub esp,8结合mov指令完成参数进栈的操作。函数带有返回值时,若返回值不大于4字节,则会把返回值存储在eax寄存器中,而long long类型返回值回保存在edx:eax寄存器中,double类型的数据会被协处理器栈保存。

    相对于内置类型的参数传递和返回值,对象的传值和返回可能更复杂一点。当然,如果使用对象的引用或者指针作为参数传递和返回值的方式,这里和上述的内置类型并无多大区别,因为指针总是4个字节。如果不使用引用和指针,单纯传递纯粹的对象时,编译器会如何处理呢?

    为此,我们定义一个简单的类A,为了防止编译器对我们的代码优化处理(参考我的前一篇博文),我们自己定义构造函数、复制构造函数和赋值运算符重载函数。

    class A
    {
        int x;
        int y;
        int z;
    public:
        A(){}
        A(const A&a)
        {
            x=a.x;
            y=a.y;
            z=a.z;
        }
        const A&operator=(const A&a)
        {
            x=a.x;
            y=a.y;
            z=a.z;
        }
    };

    定义一个简单的具有对象参数和返回值的函数,以及测试代码。

    A fun(A x)
    {
        return x;
    }
    A a;
    a=fun(a);

    试想一下,如果A不是自定义类型,而是int类型的话,这段测试代码会有怎样的效果。

    mov eax,[a];//取出a的值
    push eax;//a值进栈
    call fun;//调用fun
    add esp,4;//恢复栈指针
    mov [a],eax;//返回值写入a
    ;//而fun内部无非也是把参数x的值写入eax,然后返回而已。
    mov eax,[a]
    ret

    事实是这样的吗?我们看一下VS2010的反汇编。

    和我们的预期完全一致!

    现在,我们回到对象的问题上来。由于对象是值传递方式,因此,对象传递之前需要进行一次对象拷贝(从原对象到实参)。函数调用结束后还需要将返回值对象进行一次拷贝。我们看看VS2010的处理方式。

     

    对象a定义是需要调用它的构造函数A::A(0A1112Ch)

    对象A包含三个整形数据成员,因此它的大小是120x0C)字节。sub esp,0Ch正是开辟12个字节存储从对象a拷贝出来的12字节数据。

    mov ecx,esp记录了被拷贝的参数对象的地址(this指针),push eax压入的是a的地址,也就是拷贝构造函数调用时参数对象的地址(引用)。拷贝构造函数(A::A(0A11131h))会把a地址记录的对象数据拷贝到ecx记录的this对应的参数对象内。调用结束后,使用ret 4指令将刚才压入的a的地址弹出栈,这样栈顶保存着完整参数对象(刚才开辟的12个字节)。这样参数对象被完整的复制出来了。

    push ecx压入了内存地址ebp-58h,这个地址既不是a的地址,也不是拷贝出参数对象的地址,而是要保存返回对象的地址!调用fun之前将该地址压栈,就是为了保存fun处理结束后的返回值对象。fun调用结束后将esp指针恢复了16字节,正好是参数对象的大小(12字节)加上返回值对象的地址(4字节)之和!要获得fun的返回值,直接访问eax即可,因为它保存着返回值对象的地址(ebp-58h)!

    最后一步是对象的赋值,这里需要调用对象的赋值运算符重载函数。而参数正是刚才fun调用结束后eax的值,因为它存储了返回值对象的地址。ecx记录this指针,正是被赋值对象的地址(a的地址)。赋值运算符重载函数调用结束后,完成返回值对象的赋值操作。

    按照编译器产生的fun函数的语义,我们使用高级语言可以将它的意思描述如下。

    A a;//定义a
    a.A();//默认构造
    A x;//开辟x的12字节空间
    x.(a);//对象复制到实际参数
    A*pret=&ret;//取返回值对象地址(已经开辟过了)
    fun(pret,x);//传递返回值指针pret和参数对象x
    a=*pret;//把返回值对象赋值给对象a
    //这样原本fun的函数形式就有所变化了。
    void fun(A*pret,A x)
    {
        pret->A(x);//将返回值拷贝到返回值对象内
        return;//啥也不返回了
    }

    我们看一下fun的汇编代码。

    参数对象的地址被x记录了下来,ebp+8记录的正是函数第一个参数的内容,即返回值对象的地址!在拷贝构造函数调用之前,ecx保存的this指针正是返回值对象的,进栈的参数是x的地址,和我们预期的一样!

    因此,我们可以针对对象的传值和返回得出如下结论:

    1. 对象参数传递之前需要进行一次对象拷贝,将原对象的内容完整的拷贝到参数对象内部,函数执行时访问的是参数对象,而不是原对象。

    2. 对象返回时,也需要将函数处理的结果进行一次对象拷贝,不过被拷贝的返回值对象内存已经在函数调用之前已经开辟出来了,函数只需要记录它的地址即可,然后调用拷贝构造函数初始化它。

    3. 函数调用结束后,eax保存了返回值对象的地址,供调用者使用。

    通过本文的描述,相信读者对对象作为函数参数和返回值时,编译器的内部处理机制有个更清晰的了解。



  • 相关阅读:
    阿里云前端周刊
    在没有DOM操作的日子里,我是怎么熬过来的(中)
    【php学习】数组操作
    App之百度云推送
    L2-008. 最长对称子串
    整数划分问题之递归法
    分治法之归并排序(递归+分治)
    L2-005. 集合相似度
    分治法之棋盘覆盖问题
    L2-003. 月饼
  • 原文地址:https://www.cnblogs.com/fanzhidongyzby/p/2858568.html
Copyright © 2011-2022 走看看