从汇编看c++成员函数指针(二)
下面先看一段c++源码:
#include <cstdio>
using namespace std; class X {
public:
virtual int get1() {
return ;
}
virtual int get2() {
return ;
}
}; class Y {
public:
virtual int get3() {
return ;
}
virtual int get4() {
return ;
}
}; class Z : public X, public Y {
public:
int get2() {
return ;
}
int get4() {
return ;
}
}; int main() {
Z z;
Z* zp = &z;
X* xp = zp;
Y* yp = zp;
int(X::*xgp1)() = &X::get1;
int(X::*xgp2)() = &X::get2;
int(Y::*ygp3)() = &Y::get3;
int(Y::*ygp4)() = &Y::get4;
int(Z::*zgp1)() = &Z::get1;
int(Z::*zgp2)() = &Z::get2;
int(Z::*zgp3)() = &Z::get3;
int(Z::*zgp4)() = &Z::get4;
/*****************************输出各个成员函数指针的值***********************/
printf("&X::get1 = %lu\n", &X::get1);
printf("&X::get2 = %lu\n", &X::get2);
printf("&Y::get3 = %lu\n", &Y::get3);
printf("&Y::get4 = %lu\n", &Y::get4);
printf("&Z::get1 = %lu\n", &Z::get1);
printf("&Z::get2 = %lu\n", &Z::get2);
printf("&Z::get3 = %lu\n", &Z::get3);
printf("&Z::get4 = %lu\n", &Z::get4);
printf("\n");
printf("xgp1 = %lu\n", xgp1);
printf("xgp2 = %lu\n", xgp2);
printf("ygp3 = %lu\n", ygp3);
printf("ygp4 = %lu\n", ygp4);
printf("zgp1 = %lu\n", zgp1);
printf("zgp2 = %lu\n", zgp2);
printf("zgp3 = %lu\n", zgp3);
printf("zgp4 = %lu\n", zgp4); /**********************用成员函数指针类X中的虚函数*******************/
(zp->*xgp1)();
(zp->*zgp1)();
(xp->*xgp1)();
(zp->*xgp2)();
(zp->*zgp2)();
(xp->*xgp2)(); /********************用成员函数指针调用Y中的虚函数*********************/
(zp->*ygp3)();
(zp->*zgp3)();
(yp->*ygp3)();
(zp->*ygp4)();
(zp->*zgp4)();
(yp->*ygp4)(); }
类Z多重继承与类X和类Y,类X和类Y分别有两个虚函数,而类Z重写了他们当中的各一个。main函数主要是输出各个成员函数指针的值以及用成员函数指针调用相应的函数
下面是输出结果:
通过从汇编看c++中成员函数指针(一)我们知道,指向虚函数的成员指针保存的是相应vcall函数的地址,从结果中我们可以看到,这8个成员函数指针只保留了两个vcall函数的地址。下面是vcall函数的汇编码:
_TEXT SEGMENT
??_9X@@$BA@AE PROC ; X::`vcall'{0}', COMDAT
mov eax, DWORD PTR [ecx];寄存器ecx里面存有对象首地址,这里将对象首地址处内容(即vftable首地址)给寄存器eax
jmp DWORD PTR [eax];跳转到vftable首地址内存(里面存放虚函数的地址)所存地址处执行
??_9X@@$BA@AE ENDP ; X::`vcall'{0}'
; Function compile flags: /Odtp
_TEXT ENDS
; COMDAT ??_9X@@$B3AE
_TEXT SEGMENT
??_9X@@$B3AE PROC ; X::`vcall'{4}', COMDAT
mov eax, DWORD PTR [ecx];存器ecx里面存有对象首地址,这里将对象首地址处内容(即vftable首地址)给寄存器eax
jmp DWORD PTR [eax+];跳转到偏移vftable首地址4byte处内存(里面存放虚函数首地址)所存地址处执行
??_9X@@$B3AE ENDP ; X::`vcall'{4}'
_TEXT ENDS
从汇编码可以看到,不管是vcall{0}还是vcall{4},它们所做的工作都是类似的。根据存于寄存器ecx里面对象的首地址,找到相应的虚表,然后跳转到相应的虚函数去执行。而这里所有的虚函数要么相对于虚表的首地址偏移量为0 要么是4, 因此,只要这两个vcall函数就够用了,
,每一个vcall函数只负责根据一个偏移量查找虚表。只要保证传进来的对象首地址正确,能够找到正确的虚表即可。
接下来让我们来看一下定义各个成员函数指针的汇编码:
; 39 : int(X::*xgp1)() = &X::get1; mov DWORD PTR _xgp1$[ebp], OFFSET ??_9X@@$BA@AE ; X::`vcall'{0}' 将X::vcall{0}的地址给xgp1 ; 40 : int(X::*xgp2)() = &X::get2; mov DWORD PTR _xgp2$[ebp], OFFSET ??_9X@@$B3AE ; X::`vcall'{4}' 将X::vcall{4}的地址给xgp2 ; 41 : int(Y::*ygp3)() = &Y::get3; mov DWORD PTR _ygp3$[ebp], OFFSET ??_9X@@$BA@AE ; X::`vcall'{0}' 将X::vcall{0}的地址给ypg3 ; 42 : int(Y::*ygp4)() = &Y::get4; mov DWORD PTR _ygp4$[ebp], OFFSET ??_9X@@$B3AE ; X::`vcall'{4}' 将X::vcall{4}的地址给ygp4 ; 43 : int(Z::*zgp1)() = &Z::get1; mov DWORD PTR $T4081[ebp], OFFSET ??_9X@@$BA@AE ; X::`vcall'{0}' 将X::vcall{0}的地址写给临时对象$T4081首地址处内存
mov DWORD PTR $T4081[ebp+], ;将0写给偏移临时对象$T4081说地址4byte处内存 这里的0是相对于对象z首地址的偏移量
mov ecx, DWORD PTR $T4081[ebp];将临时对象$T4081首地址处的值给寄存器ecx
mov DWORD PTR _zgp1$[ebp], ecx;将ecx的值给对象zpg1对象首地址处内存
mov edx, DWORD PTR $T4081[ebp+];将偏移临时对象$T4081首地址4byte处内存内容给寄存器edx
mov DWORD PTR _zgp1$[ebp+], edx;将edx的内容给偏移对象zgp1首地址4byet处内存
;上面这一段完成了从临时对象$T4081到对象zpg1的拷贝
;可以看到zpg1是一个对象,它的首地址存储的是相应vcall函数的地址
;紧挨着的内存存储的是其所指虚函数所属类相对于对象z首地址的偏移量
;接下来的zgp2 zgp3 zgp4和zgp1的过程类似 存储的都是相应vcall函数的地址和
;其所指虚函数所属类相对于对象z首地址偏移量 ; 44 : int(Z::*zgp2)() = &Z::get2; mov DWORD PTR $T4082[ebp], OFFSET ??_9X@@$B3AE ; X::`vcall'{4}' ;存vcall地址
mov DWORD PTR $T4082[ebp+], ;偏移量为0
mov eax, DWORD PTR $T4082[ebp]
mov DWORD PTR _zgp2$[ebp], eax
mov ecx, DWORD PTR $T4082[ebp+]
mov DWORD PTR _zgp2$[ebp+], ecx ; 45 : int(Z::*zgp3)() = &Z::get3; mov DWORD PTR $T4083[ebp], OFFSET ??_9X@@$BA@AE ; X::`vcall'{0}';存vcall地址
mov DWORD PTR $T4083[ebp+], ;偏移量为4
mov edx, DWORD PTR $T4083[ebp]
mov DWORD PTR _zgp3$[ebp], edx
mov eax, DWORD PTR $T4083[ebp+]
mov DWORD PTR _zgp3$[ebp+], eax ; 46 : int(Z::*zgp4)() = &Z::get4; mov DWORD PTR $T4084[ebp], OFFSET ??_9X@@$B3AE ; X::`vcall'{4}' 存vcall地址
mov DWORD PTR $T4084[ebp+], ;偏移量为4
mov ecx, DWORD PTR $T4084[ebp]
mov DWORD PTR _zgp4$[ebp], ecx
mov edx, DWORD PTR $T4084[ebp+]
mov DWORD PTR _zgp4$[ebp+], edx
从汇编码可以看到,xgp1 xgp2 ygp3 ygp4和从汇编看c++中成员函数指针(一)所讲的一样,仅仅存储的是相应vcall的地址;而zgp1 zgp2 zgp3 zgp4却是一个对象,它们存储了两类信息,在其首地址处存储的是相应vcall函数的地址,而接下来的内存中存储的是成员函数指针所指成员函数所在类相对于对象z首地址的偏移量。因此,可以发现,如果是单一的类,成员函数指针仅仅存储的是vcall函数的地址(对于单一继承也一样,因为这种情况下派生类中基类和派生类拥有相同的首地址,所以可以不保存这个偏移量),但是多重继承还要多存一个偏移量,那么这个多存的偏移量有什么作用呢?下面来看用成员函数指针调用虚函数的汇编代码:
; 67 : (zp->*xgp1)(); mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址(也是父类X首地址)给寄存器ecx,作为隐含参数传递给相应的vcall函数
call DWORD PTR _xgp1$[ebp];调用vcall函数 ; 68 : (zp->*zgp1)(); mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址(也是父类X首地址)给寄存器ecx
add ecx, DWORD PTR _zgp1$[ebp+];将偏移对象zgp1首地址4byte处内存内容取出(即父类X对象相对于对象z首地址偏移量)
;加到ecx寄存器里面的值上 得到指向父类X对象首地址(也是对象z的首地址)
;作为隐含参数传给相应的vcall函数
call DWORD PTR _zgp1$[ebp];调用vcall函数 zgp1首地址存有vcall函数地址 ; 69 : (xp->*xgp1)(); mov ecx, DWORD PTR _xp$[ebp];将父类X对象首地址(也是对象z的首地址)给寄存器ecx,作为隐含参数传递给vcall函数
call DWORD PTR _xgp1$[ebp];调用vcall函数 ;接下来的调用和上面的一样
; 70 : (zp->*xgp2)(); mov ecx, DWORD PTR _zp$[ebp]
call DWORD PTR _xgp2$[ebp] ; 71 : (zp->*zgp2)(); mov ecx, DWORD PTR _zp$[ebp]
add ecx, DWORD PTR _zgp2$[ebp+]
call DWORD PTR _zgp2$[ebp] ; 72 : (xp->*xgp2)(); mov ecx, DWORD PTR _xp$[ebp]
call DWORD PTR _xgp2$[ebp] ; 73 :
; 74 : /********************用成员函数指针调用Y中的虚函数*********************/
; 75 : (zp->*ygp3)(); cmp DWORD PTR _zp$[ebp], ;减查zp指针是否和0相等,即是否为空 防止由于zp为空指针而使得调整后的指针指向错误内存
je SHORT $LN5@main;如果zp为空指针,跳到标号$LN5@main处执行 否则 顺序执行 这里是顺序执行
mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx
add ecx, ;对象z的首地址加4 得到是父类Y对象的首地址,存到寄存器ecx
mov DWORD PTR tv192[ebp], ecx;将ecx的值给临时变量tv192
jmp SHORT $LN6@main;跳转到标号$LN6@main处执行
$LN5@main:
mov DWORD PTR tv192[ebp], ;若zp指针为空,临时变来那个tv192赋0
$LN6@main:
mov ecx, DWORD PTR tv192[ebp];将tv192的值给寄存器ecx(这里是父类Y对象的首地址),作为vcall函数的隐含参数
call DWORD PTR _ygp3$[ebp];调用相应的vcall函数 ; 76 : (zp->*zgp3)(); mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx
add ecx, DWORD PTR _zgp3$[ebp+];将zgp3存储的偏移量加在ecx的值上 可以得到父类Y对象的首地址 作为隐含参数传递给vcall函数
call DWORD PTR _zgp3$[ebp];调用相应的vcall函数 ; 77 : (yp->*ygp3)(); mov ecx, DWORD PTR _yp$[ebp];将父类Y对象的首地址给寄存器ecx 作为隐含参数传递给vcall函数
call DWORD PTR _ygp3$[ebp];调用vcall函数 ;下面的调用和上面的类似 ; 78 : (zp->*ygp4)(); cmp DWORD PTR _zp$[ebp],
je SHORT $LN7@main
mov edx, DWORD PTR _zp$[ebp]
add edx,
mov DWORD PTR tv202[ebp], edx
jmp SHORT $LN8@main
$LN7@main:
mov DWORD PTR tv202[ebp],
$LN8@main:
mov ecx, DWORD PTR tv202[ebp]
call DWORD PTR _ygp4$[ebp] ; 79 : (zp->*zgp4)(); mov ecx, DWORD PTR _zp$[ebp]
add ecx, DWORD PTR _zgp4$[ebp+]
call DWORD PTR _zgp4$[ebp] ; 80 : (yp->*ygp4)(); mov ecx, DWORD PTR _yp$[ebp]
call DWORD PTR _ygp4$[ebp]
通过汇编代码可以看出,多重继承下的成员函数指针多存储的偏移量信息,是为了在调用相应的vcall函数之前,将this指针调整到相应的位置,以便传给vcall函数后,以此找到成员函数所在的正确虚表。比如上面汇编代码中的第79行:
; 79 : (zp->*zgp4)(); mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx
add ecx, DWORD PTR _zgp4$[ebp+];将zgp4存储的偏移量加到对象z的首地址上 从定义zgp4的汇编代码可知
;zgp4存储的偏移量为4 刚好是父类X对象的大小,因此此时ecx里面存储的是
;父类Y对象的首地址
call DWORD PTR _zgp4$[ebp];调用vcall函数
从上面的汇编代码还可以发现,基类成员变量指针绑定到派生类对象或者对象指针上面是允许的,比如zp->*ymp3,从汇编码可以看出,是编译器在内部做了做了转换,将zp指针先调整为指向父类Y子对象首地址的指针(相当于向上转换),然后调用相应的vcall函数。但是反过来不行,就是不能yp->*zgp4,因为这样做的话就好要先将yp向下转换为Z*类型,而这是不允许的。至于像这种形式的调用yp->*ymp3(yp右zp转换而来),和zp->*ymp3的原理是一样的,只不过从zp到yp的转换由我们自己完成。
成员函数指针之间的转换
我们可以将基类的成员函数指针赋给派生类的成员函数指针,以为存在于基类中的成员函数一定存在于派生类中,但是,反过来就不行,即不能讲派生类的成员函数指针赋给基类成员函数指针,因为派生类中存在的成员函数不一定存在于基类中。下面来看一个转化实例的汇编码:
zgp4 = ygp4; mov eax, DWORD PTR _ygp4$[ebp];将ygp4的值给寄存器eax
mov DWORD PTR $T4092[ebp], eax;将eax的值给临时对象ST4092的首地址处内存
mov DWORD PTR $T4092[ebp+], ;将4给偏移临时对象ST4092首地址4byte处内存
mov ecx, DWORD PTR $T4092[ebp];将临时对象ST4092首地址处内存内容给ecx寄存器
mov DWORD PTR _zgp4$[ebp], ecx;将ecx寄存器的值给zgp4首地址处内存
mov edx, DWORD PTR $T4092[ebp+];将偏移临时对象ST4092首地址4byte处内存内容给寄存器edx
mov DWORD PTR _zgp4$[ebp+], edx;将edx的值给偏移zgp4首地址4byte处内存
通过给出的汇编码可以看到,转换的时候并不是仅仅将ygp4的值赋给zgp4,而是编译器内部做了转换(虽然zgp4为8字节,ygp4为4字节,仍然能够保证转换成功),使得zgp4的拥有正确的值,这种转换的效果和直接让zgp4 = &Z::get4是一样的。注意,这里并不是仅能让ygp4转化为zgp4,一个类的成员函数指针可以指向类中存在的任意成员函数,因此,也可以让zgp4 = xgp1,这样转化的效果和直接让zgp4 = &Z::get1一样。
附:
类的继承关系图:
类X 类Y 类Z的内存布局
各个成员函数指针
从汇编看c++成员函数指针(二)的更多相关文章
- 从汇编看c++成员函数指针(三)
前面的从汇编看c++中成员函数指针(一)和从汇编看c++成员函数指针(二)讨论的要么是单一类,要么是普通的多重继承,没有讨论虚拟继承,下面就来看一看,当引入虚拟继承之后,成员函数指针会有什么变化. 下 ...
- 函数指针和成员函数指针有什么不同,反汇编带看清成员函数指针的本尊(gcc@x64平台)
函数指针是什么,可能会答指向函数的指针. 成员函数指针是什么,答指向成员函数的指针. 成员函数指针和函数指针有什么不同? 虚函数指针和非虚成员函数指针有什么不同? 你真正了解成员函数指针了吗? 本篇带 ...
- 成员函数指针与高性能C++委托
1 引子 标准C++中没有真正的面向对象的函数指针.这一点对C++来说是不幸的,因为面向对象的指针(也叫做“闭包(closure)”或“委托(delegate)”)在一些语言中已经证明了它宝贵的价值. ...
- 自制反汇编工具使用实例 其二(使用xmm寄存器初始化对象,以及空的成员函数指针)
在反汇编代码中,当看到xmm寄存器,第一反应是将要进行浮点操作或访问,但是更加多的情况是在使用xmm寄存器初始化局部对象. 下面是自制反汇编工具翻译出来的代码: // -[CALayer setAll ...
- 从汇编看c++中成员函数指针(一)
下面先来看c++的源码: #include <cstdio> using namespace std; class X { public: int get1() { ; } virtual ...
- 成员函数指针与高效C++委托 (delegate)
下载实例源代码 - 18.5 Kb 下载开发包库文件 - 18.6 Kb 概要 很遗憾, C++ 标准中没能提供面向对象的函数指针. 面向对象的函数指针也被称为闭包(closures) 或委托(del ...
- C++成员函数指针的应用
C++中,成员指针是最为复杂的语法结构.但在事件驱动和多线程应用中被广泛用于调用回叫函数.在多线程应用中,每个线程都通过指向成员函数的指针来调用该函数.在这样的应用中,如果不用成员指针,编程是非常困 ...
- [转]成员函数指针与高性能的C++委托
原文(作者:Don Clugston):Member Function Pointers and the Fastest Possible C++ Delegates 译文(作者:周翔): 成员函数指 ...
- c++ 成员函数指针
C++中,成员指针是最为复杂的语法结构.但在事件驱动和多线程应用中被广泛用于调用回叫函数.在多线程应用中,每个线程都通过指向成员函数的指针来调用该函数.在这样的应用中,如果不用成员指针,编程是非常困难 ...
随机推荐
- 客户端调用web中js方法(C调B)跨域问题
这几天遇到了个棘手问题(c调b),经过排错查出了问题. 一,问题描述如下: 1.客户端需要调用father.html中一个js方法,特殊之处在于 这个father.html中有个iframe嵌套了一个 ...
- (转) Special members
原地址:http://www.cplusplus.com/doc/tutorial/classes2/ Special members [NOTE: This chapter requires p ...
- 课堂里学不到的C与C++那些事(一)
首先,声明一下这是一个系列的文章.至于整个系列有多少篇,笔者也不知道,不知道有多少篇,也不知道多久会更新一篇.反正只有一个原则,写出来的文 章能见得人才会公布出来.另外,我不是叫你逃课,而是觉得听课只 ...
- Shell glob
在Linux中,glob是用来匹配路径名的通配符,glob主要包含以下4种: Wildcard Matching * 匹配0个或者多个字符,比如d*,可以匹配d, d1, dd1 ?匹配单个字符,比如 ...
- setInterval && setTimeout || 定时器
来自w3school的解释 定时器setInterval() 方法可按照指定的周期(以毫秒计)来调用函数或计算表达式. setInterval() 方法会不停地调用函数,直到 clearInterva ...
- HttpURLConnection详解
HttpURLConnection详解 07. 五 / J2EE / 没有评论 HttpURLConnection类的作用是通过HTTP协议向服务器发送请求,并可以获取服务器发回的数据. Http ...
- iis10,php 5.2 isapi 扩展
1.添加isapi映射模块: 2.设置独立应用程序池,注意php版本,5.2,要设置程序池32位. 64位下配置IIS+PHP出现404.17错误的解决办法
- 让操作javascript对象数组像.net lamda表达式一样
让操作javascript对象数组像.net lamda表达式一样 随着web应用程序的富客户端化.ajax的广泛使用及复杂的前端业务逻辑.对js对象数组.json数组的各种操作越来越多.越来越复杂. ...
- 火星A+B
火星A+B Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submi ...
- sql 添加约束
在表中添加约束,基本常用的有两种类型,一个是创建表时同时添加约束,另一个是创建好表通过修改表添加约束,在这里是创建表时同时添加约束,但是有两种不同的用写法. 在这里列举出一些创建约束的形式,共参考(均 ...