从汇编看c++多重继承中this指针的变化
先来看一下下面的c++源码:
#include <iostream>
using namespace std; class X {
public:
virtual void print1() {
cout << "X::print1 = " << (long)this << endl;
}
virtual void print2() {
cout << "X::print2 = " << (long)this << endl;
}
}; class Y {
public:
virtual void print3() {
cout << "Y::print3 = " << (long)this << endl;
}
virtual void print4() {
cout << "Y::print4 = " << (long)this << endl;
}
}; class Z : public X, public Y {
public:
virtual void print2() {
cout << "Z::print2 = " << (long)this << endl;
}
virtual void print4() {
cout << "Z::print4 = " << (long)this << endl;
}
}; int main() {
Z z;
Z* zp = &z;
X* xp = zp;
Y* yp = zp;
cout << "zp = " << (long)zp << endl;
cout << "xp = " << (long)xp << endl;
cout << "yp = " << (long)yp << endl;
/*******************以派生类指针调用基类虚函数*******************/
zp->print1();
zp->print3();
/*******************以派生里指针调用派生类虚函数**************/
zp->print2();
zp->print4();
/*******************以基类指针调用基类虚函数***************/
xp->print1();
yp->print3();
/***********************以基类指针调用派生类虚函数***********/
xp->print2();
yp->print4();
}
类Z多重继承与类X和类Y,类X和类Y各有两个虚函数,分别输出this指针的值。其中,类Z覆写了类X和类Y中的两个虚函数。
下面是这段代码的输出结果:

从上面的输出结果可以看到,以派生类指针zp调用基类虚函数print1和print3,输出的this指针值分别为父类X对象和父类Y对象的首地址;而已基类指针xp和zp调用派生类虚函数print2和print4则都输出的是派生类Z对象的首地址,那么,这当中this指针是如何调整的呢?先来看一下main函数里面的汇编码(只看调用函数的部分,其他部分省略):
; 42 : /*******************以派生类指针调用基类虚函数*******************/
; 43 : zp->print1(); mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx
mov edx, DWORD PTR [ecx];将对象z的首地址处内存内容(即vftable首地址)给寄存器edx
mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址(也是父类X对象首地址)给寄存器ecx,作为隐含参数传递给虚函数print1
mov eax, DWORD PTR [edx];将vftable首地址处内存内容(即print1的地址)给寄存器eax
call eax;调用虚函数print1 ; 44 : zp->print3(); mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址给就寄存器ecx
add ecx, ;将寄存器ecx里面的额内容加4,得到父类Y对象首地址,存于ecx中,做为隐含参数传递给虚函数print3
mov edx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器edx
mov eax, DWORD PTR [edx+];将偏移对象z首地址4byte处内存内容(即vftable首地址)给寄存器eax
mov edx, DWORD PTR [eax];将vftable首地址处内存内容(即print3的地址)给寄存器edx
call edx;调用虚函数print3 ; 45 : /*******************以派生里指针调用派生类虚函数**************/
; 46 : zp->print2(); mov eax, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器eax
mov edx, DWORD PTR [eax];将对象z首地址处的内存内容(即vftable首地址)给寄存器edx
mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址(也是父类X对象的首地址)给寄存器ecx,作为隐含参数传递给虚函数print2
mov eax, DWORD PTR [edx+];将偏移vftable首地址4byte处内存内容(即print2的地址)给寄存器eax
call eax;调用虚函数print2 ; 47 : zp->print4(); mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx
add ecx, ;将ecx里面的值加上4,得到父类Y对象的首地址,存放到ecx,作为隐含参数,传递给虚函数print4
mov edx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器edx
mov eax, DWORD PTR [edx+];将偏移对象z首地址4byte处内存内容(即父类Y对象首地址处内存内容)给寄存器eax,eax存放vftable首地址
mov edx, DWORD PTR [eax+];将偏移vftable首地址4byte处内存内容(即print4的地址)给寄存器eax
call edx;调用虚函数print4 ; 48 : /*******************以基类指针调用基类虚函数***************/
; 49 : xp->print1(); mov eax, DWORD PTR _xp$[ebp];将父类X对象首地址给寄存器eax
mov edx, DWORD PTR [eax];将父类X对象首地址处内存内容(即vftable首地址)给寄存器edx
mov ecx, DWORD PTR _xp$[ebp];将父类X对象首地址给寄存器ecx,作为隐含参数传递给虚函数print1
mov eax, DWORD PTR [edx];将vftable首地址处内容(即print1的地址)给寄存器eax
call eax;调用虚函数print1 ; 50 : yp->print3(); mov ecx, DWORD PTR _yp$[ebp];将父类Y对象的首地址给寄存器ecx
mov edx, DWORD PTR [ecx];将父类Y对象首地址处内存内容(即vftable首地址)给寄存器edx
mov ecx, DWORD PTR _yp$[ebp];将父类Y对象首地址给寄存器ecx,作为隐含参数传递给虚函数print3
mov eax, DWORD PTR [edx];将vftable首地址处内存内容(即print3的地址)给寄存器eax
call eax;调用虚函数print3 ; 51 : /***********************以基类指针调用派生类虚函数***********/
; 52 : xp->print2(); mov ecx, DWORD PTR _xp$[ebp];将父类X对象的首地址给寄存器ecx
mov edx, DWORD PTR [ecx];将父类X对象首地址处内存内容(即vftable首地址)给寄存器edx
mov ecx, DWORD PTR _xp$[ebp];将父类X对象的首地址给寄存器ecx,作为隐含参数传递给虚函数print2
mov eax, DWORD PTR [edx+];将偏移vftable首地址4byte处的内存内容(即print2的首地址)给寄存器eax
call eax;调用虚函数print2 ; 53 : yp->print4(); mov ecx, DWORD PTR _yp$[ebp];将父类Y对象的首地址给寄存器ecx
mov edx, DWORD PTR [ecx];将父类Y对象首地址处内存内容(即vftable首地址)给寄存器edx
mov ecx, DWORD PTR _yp$[ebp];将父类Y对象首地址给寄存器ecx,作为隐含参数传递给虚函数print4
mov eax, DWORD PTR [edx+];将偏移vftable首地址4byte处内存内容(即print4的首地址)给寄存器eax
call eax;调用虚函数print4
从汇编代码可以看出,不管使用哪种指针调用哪类虚函数,传递给虚函数的this指针都是引入这个虚函数的类对象首地址。比如,以基类指针zp调用基类Y的虚函数print4,传递给print4的this指针就是父类Y对象的首地址;以基类指针yp调用派生类Z的虚函数print4,传递给print4的this指针也是父类Y对象的首地址。那么,既然传递给虚函数print4的this指针是父类Y对象的首地址,虚函数是如何保证正确输出的呢?下面来看一下print4的汇编码,了解其内部过程:
?print4@Z@@UAEXXZ PROC ; Z::print4, COMDAT
; _this$ = ecx ; 29 : virtual void print4() { push ebp
mov ebp, esp
push ecx;压栈ecx寄存器是为保留this指针预留空间
mov DWORD PTR _this$[ebp], ecx;寄存器ecx里面存放父类Y对象首地址,存于刚才预留的空间 ; 30 : cout << "Z::print4 = " << (long)this << endl; push OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
mov eax, DWORD PTR _this$[ebp];将父类Y对象的首地址给寄存器eax
sub eax, ;eax里面的值减4,这是eax里面存放的是对象z的首地址,存于寄存器eax
push eax;压栈eax寄存器,为输出传递参数
push OFFSET ??_C@_0N@BGKDGLPK@Z?3?3print4?5?$DN?5?$AA@
push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
add esp,
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<< ; 31 : } mov esp, ebp
pop ebp
ret
?print4@Z@@UAEXXZ ENDP ; Z::print4
主要看注释的部分,这里,虽然传递给print4的this指针(由寄存器ecx传过来)是父类Y对象的首地址,但是,在进行输出的时候,编译器为我们进行了this指着的调整,即上面汇编代码中sub eax 4的部分,将this指针重新调整到指向对象z的首地址处,因此能够正确输出。那么,编译器又是如何知道要调整的大小的呢,比如,编译器怎么知道要调整4byte呢?通过在命令行查看对象z的内存布局,打印出如下语句:

可以看到,this指着要调整的值,被记录在相应的虚表之中(如何用命令行查看一个对象的内存布局,请参看
c+中如何查看一个类的内存布局)
下面是print1函数的汇编码:
?print1@X@@UAEXXZ PROC ; X::print1, COMDAT
; _this$ = ecx ; 6 : virtual void print1() { push ebp
mov ebp, esp
push ecx;压栈寄存器ecx的目的是为了保存this指针预留空间
mov DWORD PTR _this$[ebp], ecx;ecx里面保存有父类X对象的首地址(也是对象z的首地址),存于刚才预留的空间 ; 7 : cout << "X::print1 = " << (long)this << endl; push OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
mov eax, DWORD PTR _this$[ebp];将父类对象首地址(也是对象z的首地址)传给寄存器eax
push eax;将寄存器eax的值压栈,为输出传递参数
push OFFSET ??_C@_0N@LOBPFBMB@X?3?3print1?5?$DN?5?$AA@
push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
add esp,
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<< ; 8 : } mov esp, ebp
pop ebp
ret
?print1@X@@UAEXXZ ENDP ; X::print1
下面是print2函数的汇编码:
; 26 : virtual void print2() {
push ebp
mov ebp, esp
push ecx;压栈寄存器ecx的目的是为了保存this指针预留空间
mov DWORD PTR _this$[ebp], ecx;寄存器ecx里面存放父类X对象的首地址(也是z对象首地址),保存到刚才预留的空间
; 27 : cout << "Z::print2 = " << (long)this << endl;
push OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
mov eax, DWORD PTR _this$[ebp];将父类X对象首地址(也是z对象首地址)给寄存器eax
push eax;压栈寄存器eax,为输出传递参数
push OFFSET ??_C@_0N@JJODJOFK@Z?3?3print2?5?$DN?5?$AA@
push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
add esp,
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
; 28 : }
mov esp, ebp
pop ebp
ret
?print2@Z@@UAEXXZ ENDP ; Z::print2
下面是print3函数的汇编码:
?print3@Y@@UAEXXZ PROC ; Y::print3, COMDAT
; _this$ = ecx ; 16 : virtual void print3() { push ebp
mov ebp, esp
push ecx;压栈ecx的目的是为包存this指针预留空间
mov DWORD PTR _this$[ebp], ecx;寄存器ecx里面保存父类Y对象的首地址,存于刚才预留的空间里面 ; 17 : cout << "Y::print3 = " << (long)this << endl; push OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
mov eax, DWORD PTR _this$[ebp];将父类Y对象的首地址给寄存器eax
push eax;压栈eax寄存器,为输出传递参数
push OFFSET ??_C@_0N@BJEJNLCE@Y?3?3print3?5?$DN?5?$AA@
push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
add esp,
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
mov ecx, eax
call ???$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<< ; 18 : } mov esp, ebp
pop ebp
ret
?print3@Y@@UAEXXZ ENDP ; Y::print3
可以看到,print1和print3函数里面没有this指针的调整,因为它们本身就是父类X和父类Y的虚函数,传过来的this指针是正确的,所以无需调整,而print2和print4被类Z重写了,成了类Z的虚函数,而传递过来的还是父类X和父类Y的首地址,因此需要调整。但是在print2函数里面看不到调整的汇编代码,这是因为,父类X对象的首地址和对象z的首地址一样,所以也可以不调整。
从汇编看c++多重继承中this指针的变化的更多相关文章
- 从汇编看c++中指向成员变量的指针(二)
在从汇编看c++中指向成员变量的指针(一)中讨论的情形没有虚拟继承,下面来看看,当加入了虚拟继承的时候,指向成员变量的指针有什么变化. 下面是c++源码: #include <iostream& ...
- 从汇编看c++中成员函数指针(一)
下面先来看c++的源码: #include <cstdio> using namespace std; class X { public: int get1() { ; } virtual ...
- 从汇编看c++成员函数指针(三)
前面的从汇编看c++中成员函数指针(一)和从汇编看c++成员函数指针(二)讨论的要么是单一类,要么是普通的多重继承,没有讨论虚拟继承,下面就来看一看,当引入虚拟继承之后,成员函数指针会有什么变化. 下 ...
- 从汇编看c++成员函数指针(二)
下面先看一段c++源码: #include <cstdio> using namespace std; class X { public: virtual int get1() { ; } ...
- c++中this指针的用法
1. this指针的用处: 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果.this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将 ...
- C++中this指针的用法详解
转自 http://blog.chinaunix.net/uid-21411227-id-1826942.html 1. this指针的用处: 一个对象的this指针并不是对象本身的一部分,不会影响s ...
- C++中this指针的用法详解(转)
原文地址:http://blog.chinaunix.net/uid-21411227-id-1826942.html 1. this指针的用处: 一个对象的this指针并不是对象本身的一部分,不会影 ...
- 【转】C++中this指针的用法详解
1.this指针的用处 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果.this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象 ...
- C++中this指针
原文 . this指针的用处: 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果.this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自 ...
随机推荐
- PHP图片加文字水印和图片水印方法
文字水印 $dst_path = 'dst.jpg'; //创建图片的实例$dst = imagecreatefromstring(file_get_contents($dst_path)); //打 ...
- 关于《Cocos2d-x建工程时避免copy文件夹和库》的更新
在前几篇博文中大概了解了Cocos2d-x引擎的基本结构后打算开始实际操作,便在网上转载了一篇关于VS新建Cocos2d-x项目的文章.今天实际操作的时候发现博主使用的引擎版本和我的不一致(<C ...
- Jmeter -- 初体验
一.Jmeter参数 在命令行输入Jmeter --help得到以下信息: To run Apache JMeter in GUI mode:Double-click on the ApacheJMe ...
- 利用树莓派跑python爬虫的简单教程——从无到有
因为学校项目的原因入手了树莓派,到手先折腾了两天,发现网上的教程大都是拿他搭建服务器,mail,或者媒体服务器之类,对于在学校限时的宽带来说有点不太现实,不过低功耗适合一直开着的确启发了我.所以想到拿 ...
- android Unable to resolve target 'android-XX'错误和conversion to dalvik format failed with error 1错误
当用eclipse 导入一个已经存在的项目时,经常会遇见:Unable to resolve target 'android-XX' 类似的错误.这是因为导入的项目代码中project.propert ...
- 理解JMS规范中消息的传输模式和消息持久化
http://blog.csdn.net/aitangyong/article/category/2175061 http://blog.csdn.net/aitangyong/article/det ...
- Asp.net 处理程序(第五篇)
HttpApplication有19个标准事件,当到达第8个事件PostMapRequestHandler触发的时候,标志着已经获取到了处理请求的处理程序对象,在第11个事件PreRequestHan ...
- C# 父子类_实例_静态成员变量_构造函数的执行顺序
今天去面试的时候被一道题问得一点脾气都没有,今天特地来研究下. 子类成员变量,子类静态成员变量,子类构造函数,父类成员变量,父类静态成员变量,父类构造函数的执行顺序. 现在贴上从另外一个.net程序员 ...
- VS2010下WPF开发ARCGIS ENGINE 10的带Ribbon控件项目
原文 http://blog.sina.com.cn/s/blog_47522f7f0100nq5t.html 题目好长,但是集目前最新的工具于一身..VS是最新的2010版,不过用的是.net3.5 ...
- java中spring提供的属性copy方法
BeanUtils.copyProperties(source, target); 今天用到属性的copy方法