下面先看一段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++成员函数指针(二)的更多相关文章

  1. 从汇编看c++成员函数指针(三)

    前面的从汇编看c++中成员函数指针(一)和从汇编看c++成员函数指针(二)讨论的要么是单一类,要么是普通的多重继承,没有讨论虚拟继承,下面就来看一看,当引入虚拟继承之后,成员函数指针会有什么变化. 下 ...

  2. 函数指针和成员函数指针有什么不同,反汇编带看清成员函数指针的本尊(gcc@x64平台)

    函数指针是什么,可能会答指向函数的指针. 成员函数指针是什么,答指向成员函数的指针. 成员函数指针和函数指针有什么不同? 虚函数指针和非虚成员函数指针有什么不同? 你真正了解成员函数指针了吗? 本篇带 ...

  3. 成员函数指针与高性能C++委托

    1 引子 标准C++中没有真正的面向对象的函数指针.这一点对C++来说是不幸的,因为面向对象的指针(也叫做“闭包(closure)”或“委托(delegate)”)在一些语言中已经证明了它宝贵的价值. ...

  4. 自制反汇编工具使用实例 其二(使用xmm寄存器初始化对象,以及空的成员函数指针)

    在反汇编代码中,当看到xmm寄存器,第一反应是将要进行浮点操作或访问,但是更加多的情况是在使用xmm寄存器初始化局部对象. 下面是自制反汇编工具翻译出来的代码: // -[CALayer setAll ...

  5. 从汇编看c++中成员函数指针(一)

    下面先来看c++的源码: #include <cstdio> using namespace std; class X { public: int get1() { ; } virtual ...

  6. 成员函数指针与高效C++委托 (delegate)

    下载实例源代码 - 18.5 Kb 下载开发包库文件 - 18.6 Kb 概要 很遗憾, C++ 标准中没能提供面向对象的函数指针. 面向对象的函数指针也被称为闭包(closures) 或委托(del ...

  7. C++成员函数指针的应用

     C++中,成员指针是最为复杂的语法结构.但在事件驱动和多线程应用中被广泛用于调用回叫函数.在多线程应用中,每个线程都通过指向成员函数的指针来调用该函数.在这样的应用中,如果不用成员指针,编程是非常困 ...

  8. [转]成员函数指针与高性能的C++委托

    原文(作者:Don Clugston):Member Function Pointers and the Fastest Possible C++ Delegates 译文(作者:周翔): 成员函数指 ...

  9. c++ 成员函数指针

    C++中,成员指针是最为复杂的语法结构.但在事件驱动和多线程应用中被广泛用于调用回叫函数.在多线程应用中,每个线程都通过指向成员函数的指针来调用该函数.在这样的应用中,如果不用成员指针,编程是非常困难 ...

随机推荐

  1. (转) Class

    Classes are an expanded concept of data structures: like data structures, they can contain data memb ...

  2. XML的特殊字符

    XML中共有5个特殊的字符,分别是:&<>“’.如果配置文件中的注入值包括这些特殊字符,就需要进行特别处理.有两种解决方法: 其一,采用本例中的<![CDATA[ ]]> ...

  3. thinkphp使用模块/控制器/操作访问时出现No input file specified.解决方式

    thinkphp使用 http://serverName/index.php/模块/控制器/操作 访问时,出现了 No input file specified. 的错误 解决办法: 一: 开启cgi ...

  4. Ubuntu安装字体的方法

    基本步骤如下: 1. 将要安装的字体放在一个文件夹下,以/home/UsrName/Download/Font为例 2.在终端中输入 sudo cp -r /home/UsrName/Download ...

  5. STL之二分查找 (Binary search in STL)

    STL之二分查找 (Binary search in STL) Section I正确区分不同的查找算法count,find,binary_search,lower_bound,upper_bound ...

  6. C语言之预处理命令

    /**************************************************************************** Title:C之预处理命令 Time:201 ...

  7. Typecho 代码阅读笔记(三) - 插件机制

    转载请注明出处:http://blog.csdn.net/jh_zzz 以 index.php 为例: /** 初始化组件 */ Typecho_Widget:: widget('Widget_Ini ...

  8. WordPress教程之判断文章所属分类函数in_category、is_category

    最近自己在修改一个采用Wordpress程序的博客的时候需要用到一个特殊的功能:我需要判断这篇文章是属于哪些分类,如果属于我设定的分类下的文章,则输出一个DIV内容.按道理说实现这个功能应该不算太难, ...

  9. 【转】WPF - 第三方控件

    WPF - 第三方控件 目前第三方控件在网上形成巨大的共享资源,其中包括收费的也有免费的,有开源的也有不开源的,合理的使用第三方控件将使项目组的工作事半功倍.比如项目中有些复杂的业务逻辑.有些绚丽的效 ...

  10. ASP.NET用户自定义控件配置

    一直以来开发中碰到要写自定义控件的时候总是习惯性的找度娘,而没有自己记住,结果今天就悲剧了,找了半天才找到,想想还是自己积累起来吧! 第一种配置方式: 配置写在webconfig文件中,位置如下: w ...