zoukankan      html  css  js  c++  java
  • 从汇编看c++中指向成员变量的指针(一)

    在c++中,指向类成员变量的指针存储的并不是该成员变量所在内存的地址,而仅仅是该成员变量在该类对象中相对于对象首地址的偏移量。因此,它必须绑定到某一个对象或者对象指针上面,这里的对象和对象指针,就相当于充当了this指针的容器。

    下面先看c++源码以及输出结果:

    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    class X {
    public:
       int _x;
    };
    class Y {
    public:
        int _y;
    };
    
    class Z : public X, public Y {
    public:
        int _z;
       
    };
    
    int main() {
      //构造x,y,z三个对象
       X x;
       Y y;
       Z z;
       //将三个对象里面的成员值置为1
       x._x = 1;
       y._y = 1;
       z._x = 1;
       z._y = 1;
       z._z = 1;
       //x的成员变量指针
       int X::*xmp1 = &X::_x;
       //y的成员变量指针
       int Y::*ymp1 = &Y::_y;
       //z的成员变量指针
       int Z::*zmp1 = &Z::_x;
       int Z::*zmp2 = &Z::_y;
       int Z::*zmp3 = &Z::_z;
       cout << "输出成员变量指针的大小" << endl;
       cout << "sizeof(xmp1) = " << sizeof(xmp1) << endl;
       cout << "sizeof(ymp1) = " << sizeof(ymp1) << endl;
       cout << "sizeof(zmp1) = " << sizeof(zmp1) << endl;
       cout << "sizeof(zmp2) = " << sizeof(zmp2) << endl;
       cout << "sizeof(zmp3) = " << sizeof(zmp3) << endl;
       //输出x的成员指针值
       cout << "输出x的成员指针值:" << endl;
       printf("&X::_X = %d
    ", &X::_x);
       printf("X::*xmp1 = %d
    ", xmp1);
       cout << "输出y的成员指针值:" << endl;
       printf("&Y::_y = %d
    ", &Y::_y);
       printf("Y::*ymp1 = %d
    ", ymp1);
       cout << "输出z的成员指针值:" << endl;
       printf("&Z::_x = %d
    ", &Z::_x);
       printf("&Z::_y = %d
    ", &Z::_y);
       printf("&Z::_z = %d
    ", &Z::_z);
       printf("Z::*zmp1 = %d
    ", zmp1);
       printf("Z::*zmp2 = %d
    ", zmp2);
       printf("Z::*zmp3 = %d
    ", zmp3);
       
       //将基类X,Y的成员变量指针和派生类Z绑定
       Z* zp = &z;
       cout << "输出由z绑定x的成员指针:" << endl;
       cout << "由对象绑定:" << endl;
       z.*xmp1 = 2;//对象绑定
       cout << "x.*xmp1 = " << x.*xmp1 << endl;
       cout << "z.*xmp1 = " << z.*xmp1 << endl;
       cout << "x._x = " << x._x << endl;
    
       cout << "由指针绑定:" << endl;
       zp->*xmp1 = 4;
       cout << "x.*xmp1 = " << x.*xmp1 << endl;
       cout << "zp->*xmp1 = " << zp->*xmp1 << endl;
       cout << "x._x = " << x._x << endl;
       cout << "z._x = " << z._x << endl;
      
       cout << "输出由z绑定y的成员指针: " << endl;
       cout << "由对象绑定:" << endl;
       z.*ymp1 = 6;
       cout << "y.*ymp1 = " << y.*ymp1 << endl;
       cout << "z.*ymp1 = " << z.*ymp1 << endl;
       cout << "y._y = " << y._y << endl;
       cout << "z._y = " << z._y << endl;
       
       cout << "由指针绑定:" << endl;
       zp->*ymp1 = 8;
       cout << "y.*ymp1 = " << y.*ymp1 << endl;
       cout << "zp->*ymp1 = " << zp->*ymp1 << endl;
       cout << "y._y = " << y._y << endl;
       cout << "z._y = " << z._y << endl;
       
       //将zp指针向上转换为X* 和 Y* 再操作它们的成员指针
       cout << "将zp向上转换为X*:" << endl;
       X* xp = zp;
       xp->*xmp1 = 10;
       cout << "x.*xmp1 = " << x.*xmp1 << endl;
       cout << "xp->*xmp1 = " << xp->*xmp1 << endl;
       cout << "x._x = " << x._x << endl;
       cout << "z._x = " << z._x << endl;
       
       Y* yp = zp;
       yp->*ymp1 = 12;
       cout << "y.*ymp1 = " << y.*ymp1 << endl;
       cout << "yp->*ymp1 = " << yp->*ymp1 << endl;
       cout << "y._y = " << y._y << endl;
       cout << "z._y = " << z._y << endl;  
    
    }

    下面是输出的结果,由于运行结果在cmd上比较长,因此分开截图:

    从上面的输出结果我们可以得到下面三条信息:

    1 成员变量指针的大小为4byte

    2 成员变量指针存储的确是成员变量偏移所属类对象首地址的偏移量,这可以通过下面的类的内存布局可以可能出来。但是有一点很奇怪,为什么直接输出&Z::_y其值为0,但是输出zmp2的时候值为4?这一点还不清楚,根据《深度探索c++对象模型》p133侯捷的译注,似乎是编译器做了处理,但这也只是猜测。

    3 将基类成员指针绑定到派生类对象本身或者派生类对象指针上面,操作的仍是派生类对象里面的对应的成员变量。

    下面是类的继承关系图:

    下面是每一个类的内存布局:

          


    下面来看看,为什么当将基类成员变量指针绑定到派生类对象上的时候,操作的仍是派生类对象中对应的成员变量。就像下面的代码:

     z.*ymp1 = 6;
     zp->*ymp1 = 8;

    成员变量指针ypm1本来存储的是类Z的基类Y的成员_y的偏移量0,但是如果将它绑定到派生类Z上,不管是用对象z本身或者是对象指针zp绑定,按理应该操作的是对象z中的_x成员,但是,从输出结果来看,仍然操作的是对象z中的_y成员变量。到底是什么原因,下面是zp->*ymp1对应的汇编码(z.*ymp1的原理一样):

    ; 103  :    zp->*ymp1 = 8;
    
        cmp    DWORD PTR _zp$[ebp], 0;将zp指针的值和0比较,以防zp指针为空
        je    SHORT $LN11@main;如果上面的比较为0,就跳转到标号$LN11@main处执行,否则顺序执行 这里顺序执行
        mov    eax, DWORD PTR _zp$[ebp];将对象z首地址(保存在zp中)给寄存器eax
        add    eax, 4;对象z的首地址加上4 得到是对象z中父类Y对象的首地址,存于寄存器eax
        mov    DWORD PTR tv519[ebp], eax;将父类Y对象的首地址给临时变量tv519
        jmp    SHORT $LN12@main;跳转到标号$LN12@main处执行
    $LN11@main:
        mov    DWORD PTR tv519[ebp], 0;将0给临时变量tv519
    $LN12@main:
        mov    ecx, DWORD PTR tv519[ebp];将临时变量tv519的值给寄存器ecx 如果zp不为空,此时ecx中存储的是父类Y对象的首地址
        add    ecx, DWORD PTR _ymp1$[ebp];将成员指针ymp1的值加上父类Y对象的首地址,得到_y在对象z中的真正内存地址,存于ecx
        mov    DWORD PTR [ecx], 8;将8写入ecx存储的内存单元里面,即将对象z中的_y成员变量赋值为8

    下面是z.*ymp1对应的汇编码:

    ; 92   :    z.*ymp1 = 6;
    
        lea    edx, DWORD PTR _z$[ebp];取对象z的首地址,存放到寄存器edx
        test    edx, edx;测试edx存储的值是否为0 即看对象的首地址是否为空
                        ;下面的汇编代码基本与zp->*ymp1 = 8的一样
        je    SHORT $LN3@main
        lea    eax, DWORD PTR _z$[ebp]
        add    eax, 4
        mov    DWORD PTR tv410[ebp], eax
        jmp    SHORT $LN4@main
    $LN3@main:
        mov    DWORD PTR tv410[ebp], 0
    $LN4@main:
        mov    ecx, DWORD PTR tv410[ebp]
        add    ecx, DWORD PTR _ymp1$[ebp]
        mov    DWORD PTR [ecx], 6

    可以看到,编译器在内部实际上做了转换 换成c++的表达形式即: zp ? (zp + sizeof(X)) : 0  zp + ymp1 也就是说编译器内部先做了指针调整,使其指向了正确的父类对象首地址,然后再根据成员变量指针存储的偏移量找到对应的成员变量。这里,编译器将zp的类型从Z*转换成了Y*(向上转换),这是允许的,但是,如果有一个Y* yp指针,这样操作yp->*zmp3这样是不允许的,因为这实际上是要将yp的类型从Y*转换成了Z*(向下转换)。

    至于将zp指针分别转化成xp和yp指针,在操作基类成员变量指针,原理和上面一样,只不过转换指针的过程由我们自己完成,即分别将zp的类型转换成了X*和Y*。

    成员变量指针间的转换

    成员变量指针间的转换是允许的,但是,只能由基类成员变量指针转化到派生类成员变量指针,这是因为,基类中存在的成员变量,一定存在于派生类当中,所以,这种转换是安全的,比如:

     zmp2 = ymp1;//zmp2指向派生类Z里面的成员变量y,ymp1指向基类Y里面的成员变量

    通过文章开始时c++程序的输出结果,我们知道,ymp1的值为0,那么,这样转换后,zmp2的值是多少呢,是0,还是4?下面来看这行代码的汇编码:

    ; 106  :    zmp2 = ymp1;//zmp2指向派生类Z里面的成员变量y,ymp1指向基类Y里面的成员变量
    
        cmp    DWORD PTR _ymp1$[ebp], -1;将ymp1的值和-1比较,如果ymp1等于-1,这说明ymp1还没有指向任何成员变量
        jne    SHORT $LN13@main;如果ymp1不等于-1,就跳转到标号$LN13@main处执行,否则,顺序执行 这里是顺序执行(因为ymp1 = 0)
        mov    DWORD PTR tv532[ebp], -1;将-1给临时变量tv532
        jmp    SHORT $LN14@main;跳转到标号$LN14@main处执行
    $LN13@main:
        mov    edx, DWORD PTR _ymp1$[ebp];将ymp1的值给寄存器edx
        add    edx, 4;将寄存器edx里面的值加4,此时edx里面的值为4
                  ;这也是上面判断ymp1是否等于-1的原因,因为如果不做判断,假如ymp1等于-1,即还没指向任何成员变量
                  ;这里将得到错误的结果
        mov    DWORD PTR tv532[ebp], edx;将寄存器edx里面的值给临时变量tv532
    $LN14@main:
        mov    eax, DWORD PTR tv532[ebp];将临时变量tv532里面的值给寄存器eax
        mov    DWORD PTR _zmp2$[ebp], eax;将寄存器eax的值给zmp2

    通过分析汇编程序,可以得知,zmp2的值不是0,而是4。也就是说,这里的转换并不是简单的将ymp1里面的偏移量赋给zmp2,而是编译器内部做了转化(这种转化的效果和直接zmp2 = &Z::_y)是一样的,使得zmp2存储的是成员变量y在派生类对象z里面的偏移量4。这样,当zmp2绑定到对象z或者其对象指针上时,操作的还是父类Y子对象里面的成员变量y。

  • 相关阅读:
    php smarty使用..
    深圳面试!
    jQuery 源码分析..
    jQuery 使用技巧!!!
    TOMCAT 访问过程...
    准备使用Delphi调用WCF
    Delphi 中Format的字符串格式化使用说明(转载http://hi.baidu.com/test__123/blog/item/e3bba1599d717a2d2834f092.html)
    电脑启动程序自动启动
    判断输入字符串是否等效Int32位数字的两种方法
    vs2005 快捷键
  • 原文地址:https://www.cnblogs.com/chaoguo1234/p/3151099.html
Copyright © 2011-2022 走看看