从汇编看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 = ;
y._y = ;
z._x = ;
z._y = ;
z._z = ;
//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\n", &X::_x);
printf("X::*xmp1 = %d\n", xmp1);
cout << "输出y的成员指针值:" << endl;
printf("&Y::_y = %d\n", &Y::_y);
printf("Y::*ymp1 = %d\n", ymp1);
cout << "输出z的成员指针值:" << endl;
printf("&Z::_x = %d\n", &Z::_x);
printf("&Z::_y = %d\n", &Z::_y);
printf("&Z::_z = %d\n", &Z::_z);
printf("Z::*zmp1 = %d\n", zmp1);
printf("Z::*zmp2 = %d\n", zmp2);
printf("Z::*zmp3 = %d\n", zmp3); //将基类X,Y的成员变量指针和派生类Z绑定
Z* zp = &z;
cout << "输出由z绑定x的成员指针:" << endl;
cout << "由对象绑定:" << endl;
z.*xmp1 = ;//对象绑定
cout << "x.*xmp1 = " << x.*xmp1 << endl;
cout << "z.*xmp1 = " << z.*xmp1 << endl;
cout << "x._x = " << x._x << endl; cout << "由指针绑定:" << endl;
zp->*xmp1 = ;
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 = ;
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 = ;
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 = ;
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 = ;
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 = ;
zp->*ymp1 = ;
成员变量指针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], ;将zp指针的值和0比较,以防zp指针为空
je SHORT $LN11@main;如果上面的比较为0,就跳转到标号$LN11@main处执行,否则顺序执行 这里顺序执行
mov eax, DWORD PTR _zp$[ebp];将对象z首地址(保存在zp中)给寄存器eax
add eax, ;对象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给临时变量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写入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,
mov DWORD PTR tv410[ebp], eax
jmp SHORT $LN4@main
$LN3@main:
mov DWORD PTR tv410[ebp],
$LN4@main:
mov ecx, DWORD PTR tv410[ebp]
add ecx, DWORD PTR _ymp1$[ebp]
mov DWORD PTR [ecx],
可以看到,编译器在内部实际上做了转换 换成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], -;将ymp1的值和-1比较,如果ymp1等于-1,这说明ymp1还没有指向任何成员变量
jne SHORT $LN13@main;如果ymp1不等于-1,就跳转到标号$LN13@main处执行,否则,顺序执行 这里是顺序执行(因为ymp1 = 0)
mov DWORD PTR tv532[ebp], -;将-1给临时变量tv532
jmp SHORT $LN14@main;跳转到标号$LN14@main处执行
$LN13@main:
mov edx, DWORD PTR _ymp1$[ebp];将ymp1的值给寄存器edx
add edx, ;将寄存器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。
从汇编看c++中指向成员变量的指针(一)的更多相关文章
- 从汇编看c++中指向成员变量的指针(二)
在从汇编看c++中指向成员变量的指针(一)中讨论的情形没有虚拟继承,下面来看看,当加入了虚拟继承的时候,指向成员变量的指针有什么变化. 下面是c++源码: #include <iostream& ...
- 为什么c++中返回成员变量的指针,会破坏了封装?
上述代码中,get()函数返回的是类成员变量的name的地址,这是很危险的,name是私有的,意味这不想被客户访问,但是如果返回name的地址,那么外部函数就可以修改name,这就破坏了封装性. 为什 ...
- 从汇编看c++中的多态
http://www.cnblogs.com/chaoguo1234/archive/2013/05/19/3079078.html 在c++中,当一个类含有虚函数的时候,类就具有了多态性.构造函数的 ...
- 从汇编看c++中临时对象的析构时机
http://www.cnblogs.com/chaoguo1234/archive/2013/05/12/3074425.html c++中,临时对象一旦不需要,就会调用析构函数,释放其占有的资源: ...
- C++中使用初始化列表比在构造函数中对成员变量赋值更高效
这是在面试中遇到的一个问题,没有答出来,后来上网上查了一些资料,终于弄明白了: 一.首先c++标准规定成员变量必须在调用构造函数前进行初始化(这一点很重要) 二.如果我们在构造函数中对成员变量进行初始 ...
- C++中,如何定义和使用指向成员函数的指针
/*** 定义指向成员函数的指针变量的形式 : 成员函数返回类型 (类名∷*指针变量名)(参数列表)* 成员函数指针变量值的形式 : &类名∷成员函数名;* 成员函数指针变量使用形式 : (对 ...
- 继承的基本概念: (1)Java不支持多继承,也就是说子类至多只能有一个父类。 (2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法。 (3)子类中定义的成员变量和父类中定义的成员变量相同时,则父类中的成员变量不能被继承。 (4)子类中定义的成员方法,并且这个方法的名字返回类型,以及参数个数和类型与父类的某个成员方法完全相同,则父类的成员方法不能被继承。 分析以上程
继承的基本概念: (1)Java不支持多继承,也就是说子类至多只能有一个父类. (2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法.(3)子类中定义的成员变量和父类中定义的 ...
- Java学习笔记十五:Java中的成员变量和局部变量
Java中的成员变量和局部变量 一:成员变量: 成员变量在类中定义,用来描述对象将要有什么 成员变量可以被本类的方法使用,也可以被其他类的方法使用,成员变量的作用域在整个类内部都是可见的 二:局部变量 ...
- Java接口中的成员变量为什么必须声明为public static final?
我想对于每个Java程序员来说,接口都不陌生,接口中的方法也经常使用.而接口中的成员变量,就显得用得少一点,而对于成员变量为什么必须声明为public static final,可能就更不清楚了,而且 ...
随机推荐
- Python学习笔记1(基础语法)
1.Python的文件类型: 源代码:扩展名以py结尾.python写的程序不需要编译成二进制代码,可以直接运行.pyw是Windows下开发图形界面的源文件. 字节代码:扩展名以pyc结尾,是编译过 ...
- virtual box 改变已经创建的虚拟系统分配的硬盘
启动cmd,进入virtualbox安装的目录 :cd E:\Program Files\Oracle\VirtualBox 然后输入VBoxManage.exe list hdds ,可以看到 D: ...
- 一个数n的最大质因子
#include<cstdio> #include<cmath> using namespace std; #define Max(x, y) (x > y ? x : ...
- html 学习笔记--基础篇
最近被部门经理要求看一下html,重新看发现好多以前看过的只是都忘记了或者以前走马观花看过没有记得住的东西,正好趁此机会在博客上记录一下,顺便的如果以后需要查找,这里有记录的话可能会比上网查快一点(也 ...
- [转]MySQL 5.6 全局事务 ID(GTID)实现原理(一)
原文作者:淘长源 原文连接:http://qing.blog.sina.com.cn/1757661907/68c3cad333002qhe.html 转载注明以上信息 MySQL 5.6 的新特 ...
- 70道android面试题汇总
1. 下列哪些语句关于内存回收的说明是正确的? (b ) A. 程序员必须创建一个线程来释放内存 B. 内存回收程序负责释放无用内存 C. 内存回收程序允许程序员直接释放内存 D. 内存回收程序可以在 ...
- mysql的client和sever之间通信password的传输方式
本文想要说明的是,当我们用mysql -uroot -p1234567 -h127.0.0.1 -P3306 去连接mysql server时密码是通过什么样的形式传过去的呢? 首先密码这种东西明文传 ...
- QT字体的设置
摘要: QT4.7.0在移植到开发板上的时候,中文支持是必不可少的,如何让QT支持中文,如何制作QT支持的字体文件,如何使QT UI编辑器中的字号与开发板中的字号一致.作者通过实验进行了一一验证. 介 ...
- tr 替换删除字符
1.关于tr 通过使用 tr,您可以非常容易地实现 sed 的许多最基本功能.您可以将 tr 看作为 sed 的(极其)简化的变体:它可以用一个字符来替换另一个字符,或者可以完全除去一些字符.您 ...
- JS代码混淆 支持PHP .NET PERL
官方 http://dean.edwards.name/packer/ Also available as .NET, perl and PHP applications. .NET实例下载地址:h ...