从汇编看c++成员函数指针(二)
下面先看一段c++源码:
#include <cstdio> using namespace std; class X { public: virtual int get1() { return 1; } virtual int get2() { return 2; } }; class Y { public: virtual int get3() { return 3; } virtual int get4() { return 4; } }; class Z : public X, public Y { public: int get2() { return 5; } int get4() { return 6; } }; 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+4];跳转到偏移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+4], 0;将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+4];将偏移临时对象$T4081首地址4byte处内存内容给寄存器edx mov DWORD PTR _zgp1$[ebp+4], 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+4], 0;偏移量为0 mov eax, DWORD PTR $T4082[ebp] mov DWORD PTR _zgp2$[ebp], eax mov ecx, DWORD PTR $T4082[ebp+4] mov DWORD PTR _zgp2$[ebp+4], 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], 4;偏移量为4 mov edx, DWORD PTR $T4083[ebp] mov DWORD PTR _zgp3$[ebp], edx mov eax, DWORD PTR $T4083[ebp+4] mov DWORD PTR _zgp3$[ebp+4], 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], 4;偏移量为4 mov ecx, DWORD PTR $T4084[ebp] mov DWORD PTR _zgp4$[ebp], ecx mov edx, DWORD PTR $T4084[ebp+4] mov DWORD PTR _zgp4$[ebp+4], 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+4];将偏移对象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+4] 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], 0;减查zp指针是否和0相等,即是否为空 防止由于zp为空指针而使得调整后的指针指向错误内存 je SHORT $LN5@main;如果zp为空指针,跳到标号$LN5@main处执行 否则 顺序执行 这里是顺序执行 mov ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx add ecx, 4;对象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], 0;若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+4];将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], 0 je SHORT $LN7@main mov edx, DWORD PTR _zp$[ebp] add edx, 4 mov DWORD PTR tv202[ebp], edx jmp SHORT $LN8@main $LN7@main: mov DWORD PTR tv202[ebp], 0 $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+4] 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+4];将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], 4;将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+4];将偏移临时对象ST4092首地址4byte处内存内容给寄存器edx mov DWORD PTR _zgp4$[ebp+4], edx;将edx的值给偏移zgp4首地址4byte处内存
通过给出的汇编码可以看到,转换的时候并不是仅仅将ygp4的值赋给zgp4,而是编译器内部做了转换(虽然zgp4为8字节,ygp4为4字节,仍然能够保证转换成功),使得zgp4的拥有正确的值,这种转换的效果和直接让zgp4 = &Z::get4是一样的。注意,这里并不是仅能让ygp4转化为zgp4,一个类的成员函数指针可以指向类中存在的任意成员函数,因此,也可以让zgp4 = xgp1,这样转化的效果和直接让zgp4 = &Z::get1一样。
附:
类的继承关系图:
类X 类Y 类Z的内存布局
各个成员函数指针