在从汇编看c++中指向成员变量的指针(一)中讨论的情形没有虚拟继承,下面来看看,当加入了虚拟继承的时候,指向成员变量的指针有什么变化。
下面是c++源码:
#include <iostream>
#include <cstdio>
using namespace std; class Top {
public:
int _top;
};
class Left : public virtual Top {
public:
int _left;
}; class Right : public virtual Top {
public:
int _right;
}; class Bottom : public Left, public Right {
public:
int _bottom; }; int main() {
Bottom b;
Bottom* bp = &b;
Top* tp = bp;
Left* lp = bp;
Right* rp = bp; //虚基类Top中的成员变量指针
int Top::*tmp1 = &Top::_top;
//Left中的成员变量指针
int Left::*lmp1 = &Left::_top;
int Left::*lmp2 = &Left::_left;
//Right中的成员变量指针
int Right::*rmp1 = &Right::_top;
int Right::*rmp2 = &Right::_right;
//Bottom中的成员变量指针
int Bottom::*bmp1 = &Bottom::_top;
int Bottom::*bmp2 = &Bottom::_left;
int Bottom::*bmp3 = &Bottom::_right;
int Bottom::*bmp4 = &Bottom::_bottom; //输出各成员变量指针的大小
cout << "各成员变量指针的大小" << endl;
cout << "sizeof(tmp1) = " << sizeof(tmp1) << endl;
cout << "sizeof(lmp1) = " << sizeof(lmp1) << endl;
cout << "sizeof(lmp2) = " << sizeof(lmp2) << endl;
cout << "sizeof(rmp1) = " << sizeof(rmp1) << endl;
cout << "sizeof(rmp2) = " << sizeof(rmp2) << endl;
cout << "sizeof(bmp1) = " << sizeof(bmp1) << endl;
cout << "sizeof(bmp2) = " << sizeof(bmp2) << endl;
cout << "sizeof(bmp3) = " << sizeof(bmp3) << endl;
cout << "sizeof(bmp4) = " << sizeof(bmp4) << endl; //输出个成员变量指针的值
cout << "各成员变量指针的值" << endl;
printf("&Top::_top = %d\n", &Top::_top);
printf("tmp1 = %d\n", tmp1);
cout << endl;
printf("&Left::_top = %d\n", &Left::_top);
printf("lmp1 = %d\n", lmp1);
printf("&Left::_left = %d\n", &Left::_left);
printf("lmp2 = %d\n", lmp2);
cout << endl;
printf("&Right::_top = %d\n", &Right::_top);
printf("rmp1 = %d\n", rmp1);
printf("&Right::_right = %d\n", &Right::_right);
printf("rmp2 = %d\n", rmp2);
cout << endl;
printf("&Bottom::_top = %d\n", &Bottom::_top);
printf("bmp1 = %d\n", bmp1);
printf("&Bottom::_left = %d\n", &Bottom::_left);
printf("bmp2 = %d\n", bmp2);
printf("&Bottom::_right = %d\n", &Bottom::_right);
printf("bmp3 = %d\n", bmp3);
printf("&Bottom::_bottom = %d\n", &Bottom::_bottom);
printf("bmp4 = %d\n", bmp4); bp->*bmp1 = ;
bp->*bmp2 = ;
bp->*bmp3 = ;
bp->*bmp4 = ; bmp1 = tmp1;
bmp2 = lmp2;
bmp3 = rmp2; }
下面是程序运行的结果:
通过程序运行结果,我们可以得到2种信息:
1 包含虚拟继承的时候,成员变量指针仍然指向的不是成员变量在内存中的真正地址(bmp3和&Bttom::_right的值仍然不一样)
2 包含虚拟继承的时候,成员变量指针都为8字节,而不是4字节,比如c++代码中除了tmp1成员变量指针之外的其他成员变量指针的大小
那么,包含虚拟继承的时候,成员变量指针存储的值是什么,为什么会是8字节?下面通过分析来分析类Bottom中成员变量指针定义的汇编代码:
; 40 : //Bottom中的成员变量指针
; 41 : int Bottom::*bmp1 = &Bottom::_top; mov DWORD PTR $T24720[ebp], ;将0写入临时对象ST24720的首地址处内存
mov DWORD PTR $T24720[ebp+], ;将4写入偏移临时对象ST24720首地址4byte处内存
mov ecx, DWORD PTR $T24720[ebp];将临时对象ST24720首地址处内存内容给寄存器ecx
mov DWORD PTR _bmp1$[ebp], ecx;将寄存器ecx的值写入bmp1首地址处内存
mov edx, DWORD PTR $T24720[ebp+];将偏移临时对象ST24720首地址4byte处内存内容给寄存器edx
mov DWORD PTR _bmp1$[ebp+], edx;将寄存器edx的内容给偏移bmp1首地址4byte处内存 ; 42 : int Bottom::*bmp2 = &Bottom::_left;
;过程同bmp1,只是存储的值不同 mov DWORD PTR $T24721[ebp],
mov DWORD PTR $T24721[ebp+],
mov eax, DWORD PTR $T24721[ebp]
mov DWORD PTR _bmp2$[ebp], eax
mov ecx, DWORD PTR $T24721[ebp+]
mov DWORD PTR _bmp2$[ebp+], ecx ; 43 : int Bottom::*bmp3 = &Bottom::_right;
;和bmp1有点不一样 这里仅就汇编程序的实际执行流程来分析,其它的忽略
xor edx, edx;将寄存器edx里面的内容异或运算,这时,不管edx里面是什么,此时存的值一定是0
cmp edx, -;将edx的值同-1比较
jne SHORT $LN9@main;根据比较结果,如果edx的值不是-1,就跳到标号$LN9@main处执执行,这里显然是要跳转执行
mov DWORD PTR $T24722[ebp],
mov DWORD PTR $T24722[ebp+], -
mov eax, DWORD PTR $T24722[ebp]
mov DWORD PTR $T24726[ebp], eax
mov ecx, DWORD PTR $T24722[ebp+]
mov DWORD PTR $T24726[ebp+], ecx
jmp SHORT $LN10@main
$LN9@main:
xor edx, edx;将edx寄存器里面的内容异或运算,这时,不管edx里面是什么,此时存的值一定是0
jne SHORT $LN7@main;如果异或的结果不为零,就跳转到标号$LN7@main处执行,否则,顺序执行,这里显然是顺序执行
mov DWORD PTR tv89[ebp], ;将8给临时变量tv89(8刚好是sizof(Left))
jmp SHORT $LN8@main;跳转到标号$LN8@main处执行
$LN7@main:
mov DWORD PTR tv89[ebp],
$LN8@main:
mov eax, DWORD PTR tv89[ebp];将tv89的值给寄存器eax
add eax, ;将寄存器eax里面的值加上4 (4刚好是父类Right子对象中vbtable指针的大小)
mov DWORD PTR $T24723[ebp], eax;将寄存器eax的值写入临时对象ST24723首地址处内存
mov DWORD PTR $T24723[ebp+], ;将0写入偏移临时对象首地址4byte处内存
mov ecx, DWORD PTR $T24723[ebp];将临时对象ST24723首地址处内容给寄存器ecx
mov DWORD PTR $T24726[ebp], ecx;将寄存器ecx的值写入临时对象ST24726的首地址处内存
mov edx, DWORD PTR $T24723[ebp+];将偏移临时对象ST24723首地址4byte处内存内容给寄存器edx
mov DWORD PTR $T24726[ebp+], edx;将寄存器edx的内容给偏移临时对象ST24726首地址4byte处内存
$LN10@main:
mov eax, DWORD PTR $T24726[ebp];将临时对象ST24726首地址处内存给寄存器eax
mov DWORD PTR _bmp3$[ebp], eax;将寄存器eax的内容给bmp3首地址处内存
mov ecx, DWORD PTR $T24726[ebp+];将偏移临时对象ST24726首地址4byte处内存内容给寄存器ecx
mov DWORD PTR _bmp3$[ebp+], ecx;将寄存器ecx的内容给偏移bmp3首地址4byte处内存内容 ; 44 : int Bottom::*bmp4 = &Bottom::_bottom;
;过程同bmp1,只是存储的值不同 mov DWORD PTR $T24729[ebp], ; 00000010H
mov DWORD PTR $T24729[ebp+],
mov edx, DWORD PTR $T24729[ebp]
mov DWORD PTR _bmp4$[ebp], edx
mov eax, DWORD PTR $T24729[ebp+]
mov DWORD PTR _bmp4$[ebp+], eax
可以看到,bmp1~bmp4被当成了对象看待,其里面存储的值如下:
里面存储的值知道了,但是到底都有什么意义呢?下面我们就来看用着4个成员变量指针操作相应成员变量的汇编码:
: bp->*bmp1 = ; mov ecx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器ecx
mov edx, DWORD PTR [ecx];将对象b首地址处内容(即vbtable的首地址)给寄存器edx
mov eax, DWORD PTR _bmp1$[ebp+];将偏移bmp1首地址4byte处内容(即4)给寄存器eax
mov ecx, DWORD PTR _bp$[ebp];将对象b首地址给寄存器ecx
add ecx, DWORD PTR [edx+eax];寄存器edx里面存储vbtable首地址,而eax存储的是4
;因此edx+eax仍然是偏移vbtable首地址4byte处内存地址,所以这条指令是
;获取偏移vbtable首地址4byte处内存内容(存储的是vbtable指针偏移虚基类Top子对象首地址的偏移量,为20)
;与寄存器ecx内容相加,结果保存到寄存器ecx
;所以,ecx里面保存的是虚基类Top子对象的首地址
mov edx, DWORD PTR _bmp1$[ebp];将bmp1首地址处内存内容(即0)给寄存器edx
mov DWORD PTR [ecx+edx], ;寄存器ecx保存虚基类Top子对象首地址,寄存器edx里面内容为0 所以ecx+edx仍然是虚基类
;Top子对象首地址,这里将1写入虚基类首地址处内存,即给对象b成员变量_top赋值 ; 83 : bp->*bmp2 = 2; mov eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
mov ecx, DWORD PTR [eax];获取对象b首地址内容(即vbtable的首地址)给寄存器ecx
mov edx, DWORD PTR _bmp2$[ebp+];将偏移bmp2首地址4byte处内存内容(即0)给寄存器edx
mov eax, DWORD PTR _bp$[ebp];将对象b首地址给寄存器eax
add eax, DWORD PTR [ecx+edx];寄存器ecx里面存的是vbtable首地址,edx里面存的是0 所以ecx+edx
;仍然是vbtable首地址,这里获取的是vbtable首地址处内容(即vbtable指针偏移对象b首地址的偏移量,为0)
;所以,这里取vbtable首地址处内存内容,在和寄存器eax里面内容相加,结果保存到eax里面
;此时eax保存的是对象b的首地址
mov ecx, DWORD PTR _bmp2$[ebp];将bmp2首地址处的内容(即4)给寄存器ecx
mov DWORD PTR [eax+ecx], ;eax保存对象b的首地址 ecx内容为4 eax+ecx为对象b成员变量_left实际的内存地址
;因此这里是给对象b的成员变量_left赋值 ; 84 : bp->*bmp3 = 3;
;和bp->*bmp3 = 2的操作相似,只是数据不同 mov edx, DWORD PTR _bp$[ebp]
mov eax, DWORD PTR [edx]
mov ecx, DWORD PTR _bmp3$[ebp+]
mov edx, DWORD PTR _bp$[ebp]
add edx, DWORD PTR [eax+ecx]
mov eax, DWORD PTR _bmp3$[ebp]
mov DWORD PTR [edx+eax], ; 85 : bp->*bmp4 = 4;
;和bp->*bmp2 = 2的操作相似,只是数据不同 mov ecx, DWORD PTR _bp$[ebp]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR _bmp4$[ebp+]
mov ecx, DWORD PTR _bp$[ebp]
add ecx, DWORD PTR [edx+eax]
mov edx, DWORD PTR _bmp4$[ebp]
mov DWORD PTR [ecx+edx],
通过汇编码我们可以发现,bmp1~bmp4中所存储的值的意义,第一项仍然是偏移成员变量所属类对象首地址的偏移量(但是虚基类成员变量指针有点不一样,比如bmp1第一项存储的是0,是对象b的_top成员变量相对于虚基类Top子对象首地址的偏移量,而不是相对于对象b的首地址偏移量);而第二项是偏移vtable首地址的偏移量。成员变量指针通过这两个数据,以及绑定的对象或者对象指针(仍然相当于this指针容器)来计算出成员变量在内存中的真正地址。至于lmp1~lmp2 rmp1~rmp2所存储的值,和bmp1~bmp2类似。下面是Top Left Right Bottom的内存布局:
之所以包含虚拟继承的成员变量指针需要额外的字节来存储信息,就是因为当存在虚基类的时候,虚基类的位置是不固定的。比如,如果Bottom又派生了一个SubBottom子类(非虚拟继承),且该子类引入了一个新的成员变量int _subBottom,那么SubBottom的内存布局中,_subBottom就会加到_bottom的后面,_top的前面。这样,_top在类Bottom中距离首地址是20,在SubBottom中就会成为24,而_left和_right位置不会变,在SubBottom中距离其首地址仍然是4和12。类似的,如果SubBottom也派生了一个子类(非虚拟继承)SubSubBotom,且该子类也引入了一个成员变量int _subSubBottom,类SubSubBottom的内存布局中,subSubBottom就会加到subBottom后面,_top前面,这样,_top距离SubSubBottom的首地址偏移量变成了28,而_left _right仍然是4和12.但是,其在虚基类Top里面的偏移量是固定的,总是0.所以通过虚基类成员变量指针,比如bmp1来操作虚基类成员变量,总是要先定位相应的虚基类首地址,然后通过虚基类成员变量(_top)相对于虚基类(Top)首地址的偏移量来得出虚基类成员变量(_top)在内存中的真正地址。而要定位虚基类(Top)首地址,就要有相应的vbtable信息,这就是成员变量指针中的额外字节存储的信息。
与从汇编看c++中指向成员变量的指针(一)中一样,基类成员变量指针可以绑定到派生类对象或者对象指针上面,同时也可以绑定到由派生类对象指针向上转型到基类的指针上面,编译器内部做和从汇编看c++中指向成员变量的指针(一)中一样的转化。
成员变量指针之间的转换
这种情况之下,基类成员变量指针也可以转换成派生类成员变量指针,因为基类中的成员变量一定存在于派生类中,但是,派生类成员变量指针无法转换成基类成员变量指针,因为派生类中存在的成员变量,基类中不一定存在。和从汇编看c++中指向成员变量的指针(一)中讲的一样,对于bmp1(8byte) = tmp1(4byte) bmp2 = lmp2 bmp3 = rmp3的转化,并不是将后者的值赋给前者,而是编译器内部进行转化,下面给出汇编码:
: bmp1 = tmp1; cmp DWORD PTR _tmp1$[ebp], -;比较tmp1的值和-1的大小 如果tmp1等于-1 那么说明tmp1还为指向任何成员变量
jne SHORT $LN11@main;如果不想等,就跳转到标号$LN11@main处执行 否则,顺序执行 这里顺序执行
mov DWORD PTR $T24736[ebp], ;将0写入临时对象ST24736的首地址处内存
mov DWORD PTR $T24736[ebp+], -;将-1写入偏移临时对象ST24736首地址4byte处内存
mov eax, DWORD PTR $T24736[ebp];将临时对象ST24736首地址处内存内容给寄存器eax
mov DWORD PTR $T24738[ebp], eax;将寄存器eax的值给临时对象ST24738首地址处内存
mov ecx, DWORD PTR $T24736[ebp+];将偏移临时对象首地址4byte处内存内容给寄存器ecx
mov DWORD PTR $T24738[ebp+], ecx;将ecx的值写入偏移临时对象ST24738首地址4byte处内存
jmp SHORT $LN12@main;跳转到标号$LN12@main处执行
$LN11@main:
mov edx, DWORD PTR _tmp1$[ebp];将tmp1的值给寄存器edx
mov DWORD PTR $T24737[ebp], edx;将寄存器edx的值给临时对象ST24737的首地址处内存
mov DWORD PTR $T24737[ebp+], ;将4偏移临时对象ST24737首地址4byte处内存
mov eax, DWORD PTR $T24737[ebp];将临时对象ST24737首地址处内存内容给寄存器eax
mov DWORD PTR $T24738[ebp], eax;将寄存器eax的值写入临时对象ST24738首地址处内存
mov ecx, DWORD PTR $T24737[ebp+];将偏移临时对象ST24737首地址4byte处内存内容给寄存器ecx
mov DWORD PTR $T24738[ebp+], ecx;将寄存器ecx的内容给偏移临时对象ST42738首地址4byte处内存
$LN12@main:
mov edx, DWORD PTR $T24738[ebp];将临时对象ST24738首地址处内存内容给寄存器edx
mov DWORD PTR _bmp1$[ebp], edx;将edx的内容给bmp1首地址处内存
mov eax, DWORD PTR $T24738[ebp+];将偏移临时对象ST24738首地址4byte处内存内容给寄存器eax
mov DWORD PTR _bmp1$[ebp+], eax;将eax的内容给偏移bmp1首地址4byte处内存 ; 88 : bmp2 = lmp2; mov ecx, DWORD PTR _lmp2$[ebp];将lmp2首地址处内存内容给寄存器ecx
mov DWORD PTR _bmp2$[ebp], ecx;将ecx的内容给bmp2首地址处的内存
mov edx, DWORD PTR _lmp2$[ebp+];将偏移lmp2首地址4byte处内存内容给寄存器edx
mov DWORD PTR _bmp2$[ebp+], edx;将edx的内容给偏移bmp2首地址4byte处内存 ; 89 : bmp3 = rmp2; cmp DWORD PTR _rmp2$[ebp+], -;将偏移rmp2首地址4byte处内存内容与-1比较,如果等于-1 说明rmp2没有指向任何成员变量
jne SHORT $LN13@main;如果比较结果相等,顺序执行,否则,跳转到标号$LN13@main处执行,这里跳转到标号执行
mov DWORD PTR $T24741[ebp], ;将0写入临时对象ST24741首地址处内存
mov DWORD PTR $T24741[ebp+], -;将-1写入偏移临时对象ST24741首地址4byte处内存
mov eax, DWORD PTR $T24741[ebp];将临时对象ST24741首地址处内存内容给寄存器eax
mov DWORD PTR $T24743[ebp], eax;将寄存器eax内容给临时对象ST24743首地址处内存
mov ecx, DWORD PTR $T24741[ebp+];将偏移临时对象ST24741首地址4byte处内存内容给寄存器ecx
mov DWORD PTR $T24743[ebp+], ecx;将ecx的内容给偏移临时对象ST24743首地址4byte处内存内容
jmp SHORT $LN14@main;跳转到标号$LN14@main处执行
$LN13@main:
mov edx, DWORD PTR _rmp2$[ebp+];将偏移rmp2首地址4byte处内存内容给寄存器edx
neg edx;这条指令的作用是求edx里面内容的补码,即0-edx内容 并且影响标志寄存器里面的标志位CF 结果存于edx
sbb edx, edx;这条指令是带标志位减法,即被减数-减数-标志位CF,结果存于edx
and edx, - ; 将edx里面内容和-8相与,结果存于edx
add edx, ;将edx里面的内容和8相加,结果存于edx
add edx, DWORD PTR _rmp2$[ebp];将edx里面的内容和rmp2首地址处内容相加,结果存于edx
mov DWORD PTR $T24742[ebp], edx;将edx的值给临时对象ST24742首地址处内存
mov eax, DWORD PTR _rmp2$[ebp+];将偏移rmp2首地址4byte处内存内容给寄存器eax
mov DWORD PTR $T24742[ebp+], eax;将eax的值给偏移临时丢向ST24742首地址4byte处内存内容
mov ecx, DWORD PTR $T24742[ebp];将临时对象ST24742首地址处内存内容给寄存器ecx
mov DWORD PTR $T24743[ebp], ecx;将ecx的值给临时对象ST24743的首地址处内存
mov edx, DWORD PTR $T24742[ebp+];将偏移临时对象ST24743首地址4byte处内存内容给寄存器edx
mov DWORD PTR $T24743[ebp+], edx;将edx的值给偏移临时对象ST24743首地址4byte处内存
$LN14@main:
mov eax, DWORD PTR $T24743[ebp];将临时对象ST24743首地址处内存内容给寄存器eax
mov DWORD PTR _bmp3$[ebp], eax;将eax的值给bmp3首地址处内存
mov ecx, DWORD PTR $T24743[ebp+];将偏移临时对象ST24743首地址4byte处内存内容给寄存器ecx
mov DWORD PTR _bmp3$[ebp+], ecx;将ecx的值给偏移bmp3首地址4byte处内存
通过汇编码可以看到,虽然bmp1为8字节,tmp1为4字节,但是编译器内部仍然能够执行正确的转换操作。而且转换后bmp1 bmp2 bmp3的值和直接让bmp1 = &Bottom::_top bmp2 = &Bottom::_left bmp3 = &Bottom::_right效果一样。
下面来看进行bmp2 = rmp2转换时的一段汇编码:
mov edx, DWORD PTR _rmp2$[ebp+];将偏移rmp2首地址4byte处内存内容给寄存器edx
neg edx;这条指令的作用是求edx里面内容的补码,即0-edx内容 并且影响标志寄存器里面的标志位CF 结果存于edx
sbb edx, edx;这条指令是带标志位减法,即被减数-减数-标志位CF,结果存于edx
and edx, - ; 将edx里面内容和-8相与,结果存于edx
add edx, ;将edx里面的内容和8相加,结果存于edx
add edx, DWORD PTR _rmp2$[ebp];将edx里面的内容和rmp2首地址处内容相加,结果存于edx
rmp2是类Right的成员变量指针,而一个类的成员变量指针可以指向该类的任何成员变量,因此rmp2既可能指向类Right中的_right(本例中的情况),也可能指向的是类Right中的_top成员变量,而指向虚基类成员变量的指针存储的值的意义和非虚基类成员变量指针存储的值的意义是不一样的,编译器并不清楚rmp2到底指向的是谁,因此要做一些处理。
如果rmp2指向的是类Right中的_right成员变量(非虚基类成员变量),当执行上面第1行汇编码的时候,edx的值为0;当执行第2行汇编码的时候,0-0 = 0,不产生借位,因此CF置0,edx = 0;当执行第3行汇编的时候 0 - 0 - 0 = 0,因此edx = 0;当执行地4行汇编码的时候,0 & -8 = 0,因此edx = 0;当执行第5行汇编码的时候,0 + 8 = 8,因此edx = 8;当执行第6行汇编码的时候,8 + 4 = 12,因此edx = 12,转化的时候原来的偏移量增加了8(类Left的大小)这个值刚好是类Bottom中_right成员变量相对于对象b首地址的偏移量。
如果rmp2指向的是类Rihgt的_top成员变量(x虚基类成员变量),当执行第一行汇编码时,edx = 4;当执行第二行汇编码时,0 - 4 = -4,产生了借位,因此CF被置1,edx = fffffff4(-4的补码);当执行第3行汇编码时 fffffff4 - fffffff4 - 1 = -1,因此edx = ffffffff(-1的补码);当执行第4行汇编码时 ffffffff & -8 = -8 因此edx = fffffff8(-8的补码);当执行第5行汇编码时,-8 + 8 = 0,因此edx = 0;当执行第6行汇编码时 0 + 0 = 0,因此edx = 0,转换的时候原来存储的偏移量未变。其值刚好是类Bottom中的_top成员变量相对于虚基类子对象Top首地址的偏移量。
也就是说,不管rmp2指向的到底是哪个成员变量,编译器都能保证正确的转换。
bmp2 = lmp2没有这个过程的原因是,父类Left子对象和对象b拥有相同的首地址,因此,不管lmp2指向的是_left,还是_top,原来的偏移量都不需要改变。
附 tmp1 lmp1 lmp2 rmp1 rmp2成员变量指针存储的值