先看第一种最简单的情形,所有类中没有任何虚函数的菱形继承。

下面是c++源码:

class Top {//虚基类
public:
int i;
Top(int ii) {
i = ii;
}
}; class Left : public virtual Top {
public:
int j;
Left(int jj, int ii) : Top(ii) {
j = jj;
}
}; class Right : public virtual Top {
public:
int k;
Right(int kk, int ii) : Top(ii) {
k = kk;
}
}; class Bottom : public Left, public Right {
public:
int l;
Bottom(int ll, int jj, int kk, int ii) : Top(ii), Left(jj, ii), Right(kk, ii) {
l = ll;
}
}; int main() {
Bottom b(, , , );
Bottom* bp = &b;
//访问自身成员变量
b.l = ;
bp->l = ;
//访问父类Left的成员变量
Left* lp = bp;
b.j = ;
bp->j = ;
lp->j = ;
//访问父类Right的成员变量
Right* rp = bp;
b.k = ;
bp->k = ;
rp->k = ;
//访问虚基类Top的成员变量
Top* tp = bp;
b.i = ;
bp->i = ;
tp->i = ; };

让我们来看看,汇编代码里面是怎样的情形,先看main函数里面的汇编码:

; 33   : int main() {

    push    ebp
mov ebp, esp
sub esp, ; 为对象程序所需变量预留空间,其中对象b只栈24byte ; 34 : Bottom b(1, 2, 3, 4); push ;压入标志1,作为判断是否调用虚基类构造函数的依据 1表示调用,0表示不调用
push ;压栈4,为对象b的构造函数传递参数
push ;压栈3,为对象b的构造函数传递参数
push ;压栈2,为对象b的构造函数传递参数
push ;压栈1,为对象b的构造函数传递参数
lea ecx, DWORD PTR _b$[ebp];获取对象b的首地址,传给寄存器ecx,作为隐含参数传递给对象b的构造函数
call ??0Bottom@@QAE@HHHH@Z ; 调用对象b的构造函数 ; 35 : Bottom* bp = &b; lea eax, DWORD PTR _b$[ebp];将对象b的首地址给寄存器eax
mov DWORD PTR _bp$[ebp], eax;将对象b的首地址给指针变量bp ; 36 :
; 37 : b.l = 1; mov DWORD PTR _b$[ebp+], ;将1写入偏移对象首地址16字节处内存,即为对象b的成员变量l赋值1 ; 38 : bp->l = 2; mov ecx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器ecx
mov DWORD PTR [ecx+], ;将2写入偏移对象首地址16字节处内存,即为对象b的成员变量l赋值2
;可以看到,无论是用对象本身,还是对象指针访问对象b的成员变量
;其成员变量的偏移量都在编译期固定了,为16字节
;且两种方式访问没有差别 ; 39 :
; 40 : Left* lp = bp; mov edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
mov DWORD PTR _lp$[ebp], edx;将对象b的首地址给对象指针lp,此时lp指向父对象Left的首地址
;从下面的内存布局图可以看到,父对象Left的首地址和Bottom一样 ; 41 : b.j = 1; mov DWORD PTR _b$[ebp+], ;将1赋给偏移对象b首地址4byte处内存,即为
;继承来的成员变量j赋值1 ; 42 : bp->j = 2; mov eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
mov DWORD PTR [eax+], ;将2写入偏移对象b首地址4byte处内存,即为继承来的成员变量j赋值2
;可以看到,无论使用b对象本身,还是指针访问继承来的成员变量j,
;其成员变量的偏移量都是编译器固定了,都为4byte
;且两种方式访问无差别 ; 43 : lp->j = 3; mov ecx, DWORD PTR _lp$[ebp];将父类Left对象的首地址给寄存器ecx
mov DWORD PTR [ecx+], ;将3赋给偏移Left对象首地址4byte处内存,即为父对象Left的成员变量j赋值3
;可以看到,用这种方式访问父对象Left的成员变量,其偏移量也是编译器固定
;为4byte ; 44 :
; 45 : Right* rp = bp; cmp DWORD PTR _bp$[ebp], ;比较指针的值是否为0,也就是判断bp是否为空指针
je SHORT $LN3@main;如果bp为空指针,则跳转到标号$LN3@main处执行,否则顺序执行,这里是顺序执行
mov edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
add edx, ;寄存器edx里面的内容加8,现在edx里面保存的地址偏移了对象b的首地址8byte,即指向了对象Right的首地址
mov DWORD PTR tv90[ebp], edx;将寄存器edx内容存入临时变量tv90
jmp SHORT $LN4@main;跳转到标号$LN4@main处执行
$LN3@main:
mov DWORD PTR tv90[ebp], ;将临时变量tv90赋值为空指针,这是在上面判断bp指针为空的情况下执行,这里不执行这一句
$LN4@main:
mov eax, DWORD PTR tv90[ebp];将临时变量tv90里面的值赋给寄存器eax,eax保存了对象Right的首地址
mov DWORD PTR _rp$[ebp], eax;将寄存器eax里面的值赋给指针rp
;到这里,完成了从指针bp到指针rp的转化,这里之所以有对bp指针为空的判断
;是因为,rp里面的地址值是由bp里面的地址值加8byte得来,如果不进行判断,一旦bp为空指针
;即bp不指向任何对象,那么rp将指向错误的内存,这种转换就有危险,编译器必须避免这种情况 ; 46 : b.k = 1; mov DWORD PTR _b$[ebp+], ;将1写入偏移对象b首地址12byte处,即将1赋给继承来的成员变量k ; 47 : bp->k = 2; mov ecx, DWORD PTR _bp$[ebp];将对象b首地址给寄存器ecx
mov DWORD PTR [ecx+], ;将2写入偏移对象b首地址12byte处,即将2赋给继承来的成员变量k
;可以看到,这里其成员变量的偏移量也是编译器固定,为2byte
;且两种方式访问没有差别 ; 48 : rp->k = 3; mov edx, DWORD PTR _rp$[ebp];将父对象Right首地址给寄存器edx
mov DWORD PTR [edx+], ;将3写给偏移父对象Right首地址4byte处,即将3赋给成员变量k ; 49 :
; 50 : Top* tp = bp; cmp DWORD PTR _bp$[ebp], ;比较bp指针的值是否为0,也就是判断bp是否为空,原因同上
jne SHORT $LN5@main;如果不为空,就跳转到标号处$LN5@main执行,否则顺序执行,这里跳转到标号处执行
mov DWORD PTR tv145[ebp], ;如果bp为空指针,就将0赋给临时变量tv145,这里不执行这一句
jmp SHORT $LN6@main;跳转到标号处$LN6@main执行
$LN5@main:
mov eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
mov ecx, DWORD PTR [eax];将对象b首地址里面的内容给寄存器ecx,对象b首地址处的值是vtable的地址,关于vtable将在下面解释
mov edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
add edx, DWORD PTR [ecx+];ecx里面存有vtable的首地址,这里取偏移vtable首地址4byte处内存内容(即对象b,或者父对象Left首地址到虚基类首地址的偏移量), 然后加上对象b的首地址
;得到虚基类对象Top的首地址
mov DWORD PTR tv145[ebp], edx;寄存器edx里面保存虚基类对象Top的首地址,保存到临时变量tv145里面
$LN6@main:
mov eax, DWORD PTR tv145[ebp];将临时变量tv145里面的值给寄存器eax
mov DWORD PTR _tp$[ebp], eax;寄存器eax里面含有虚基类对象Top首地址,给指针tp
;这里完成了从指针bp到tp的转换 ; 51 : b.i = 1; mov ecx, DWORD PTR _b$[ebp];将对象b的首地址的内容给寄存器ecx,ecx里面是vtable的首地址
mov edx, DWORD PTR [ecx+];取偏移vtable首地址4byte处的内容,即对象b首地址到虚基类Top首地址偏移量给寄存器edx
mov DWORD PTR _b$[ebp+edx], ;将对象首地址加上edx里面的偏移量,得到虚基类Top首地址,将1写入这给地址所指内存,ji
;为继承自虚基类的成员变量i赋值 ; 52 : bp->i = 2; mov eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
mov ecx, DWORD PTR [eax];将对象b首地址处内容给寄存器ecx,即将vtable的首地址给寄存器ecx
mov edx, DWORD PTR [ecx+];将偏移vtable首地址4byte处内存内容给寄存器edx,即将对象b首地址到虚基类Top首地址偏移量给edx
mov eax, DWORD PTR _bp$[ebp];将对象b首地址给寄存器eax
mov DWORD PTR [eax+edx], ;将对象b首地址加上刚才取出的偏移量,得到虚基类Top的首地址,将2写入改地址所处内存处,
;即为继承自虚基类的成员变量i赋值2 ; 53 : tp->i = 3; mov ecx, DWORD PTR _tp$[ebp];将虚基类对象Top的首地址给寄存器ecx
mov DWORD PTR [ecx], ;将3赋给虚基类对象Top首地址处内才能,即为成员变量i赋值 ; 54 :
; 55 : }; xor eax, eax
mov esp, ebp
pop ebp
ret
_main ENDP

接下来是Bottom构造函数的汇编码:

??0Bottom@@QAE@HHHH@Z PROC                ; Bottom::Bottom, COMDAT
; _this$ = ecx ; 28 : Bottom(int ll, int jj, int kk, int ii) : Top(ii), Left(jj, ii), Right(kk, ii) { push ebp
mov ebp, esp
push ecx;压栈ecx的目的是为保存对象b的首地址预留空间
mov DWORD PTR _this$[ebp], ecx;ecx里面保存这对象b的首地址,存放到刚才空间
cmp DWORD PTR _$initVBases$[ebp], ;_$initVBases所代表的内存里面的内容存放的是调用Bottom构造器时压入的标志,其值为1
;这里与0进行比较
je SHORT $LN1@Bottom;如果上面比较结果相等,就跳到标号处$LN1@Bottom执行,否则顺序执行,这里是顺序执行
mov eax, DWORD PTR _this$[ebp];将对象b的首地址给寄存器eax
mov DWORD PTR [eax], OFFSET ??_8Bottom@@7BLeft@@@;将Bottom-Left的vtable首地址写入对象b首地址处内存
mov ecx, DWORD PTR _this$[ebp];将对象b的首地址给寄存器ecx
mov DWORD PTR [ecx+], OFFSET ??_8Bottom@@7BRight@@@;将Bottom-Right的vtable首地址写入偏移对象b首地址8byte处
;即写入对象Right的首地址处内存
mov edx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器edx
push edx;压栈寄存器edx,作为参数传递给虚基类的构造函数Top
mov ecx, DWORD PTR _this$[ebp];将对象b的首地址给寄存器ecx
add ecx, ; 将对象b的首地址加上20,得到虚基类Top的首地址,存放到寄存器ecx,作为隐含参数传递给虚基类Top的构造函数
call ??0Top@@QAE@H@Z ; 调用虚基类Top的构造函数
$LN1@Bottom:;
push ;标志0,说明已经调用过虚基类Top的构造函数,在调用Right和Left的构造函数时,就不会再调用了。
mov eax, DWORD PTR _ii$[ebp];将参数ii的值给寄存器eax
push eax;压栈eax,给Left的构造函数传递参数
mov ecx, DWORD PTR _jj$[ebp];将参数jj的值给寄存器ecx
push ecx;压栈ecx,给Left的构造函数传递参数
mov ecx, DWORD PTR _this$[ebp];将对象b的首地址(也就是对象Left的首地址)给寄存器ecx,作为隐含参数传递给Left构造函数
call ??0Left@@QAE@HH@Z ; 调用Left构造函数
push ;压栈标志0,说明已经调用过虚基类Top的构造函数,在调用Right和Left的构造函数时,就不会再调用了
mov edx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器edx
push edx;压栈edx,给Right构造函数传递参数
mov eax, DWORD PTR _kk$[ebp];将参数kk的值给寄存器eax
push eax;压栈eax,给Right的构造函数传递参数
mov ecx, DWORD PTR _this$[ebp];将对象b的首地址给ecx
add ecx, ;将对象b的首地址加上8,得到对象Right的首地址,存入寄存器ecx,作为隐含参数传递给Right构造函数
call ??0Right@@QAE@HH@Z ; 调用Right构造函数 ; 29 : l = ll; mov ecx, DWORD PTR _this$[ebp];将对象b的首地址给寄存器ecx
mov edx, DWORD PTR _ll$[ebp];将参数ll的值给寄存器edx
mov DWORD PTR [ecx+], edx;将寄存器edx的内容写入偏移对象b首地址16byte处,即给对象b的成员变量l赋值 ; 30 : } mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret ; 00000014H
??0Bottom@@QAE@HHHH@Z ENDP

下面是Left构造函数的汇编码:

??0Left@@QAE@HH@Z PROC                    ; Left::Left, COMDAT
; _this$ = ecx ; 12 : Left(int jj, int ii) : Top(ii) { push ebp
mov ebp, esp
push ecx;压栈ecx寄存器,是为保存对象Left的首地址预留空间
mov DWORD PTR _this$[ebp], ecx;寄存器ecx里面含有对象Left的首地址,存入刚才预留空间
cmp DWORD PTR _$initVBases$[ebp], ;_$initVBases所代表的内存,里面含有调用Left构造函数传入的标志,其值为0
;这里是将它的值和0作比较
je SHORT $LN1@Left;如果上面比较相等,则跳转到标号$LN1@Left处执行,否则顺序执行,这里跳转到标号执行,因此不会调用
;虚基类Top的构造函数,避免重复调用
;标号之前的语句在构造对象b的时候都不会执行
mov eax, DWORD PTR _this$[ebp];将Left对象的首地址给eax寄存器
mov DWORD PTR [eax], OFFSET ??_8Left@@7B@;将??_8Left@@7B@所带表的内存地址(即Left的vtable首地址)写入对象Left的首地址处内存
;由于这一句在构造对象b时不执行,设置无效
mov ecx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器ecx
push ecx;将ecx压栈,给虚基类Top构造函数传递参数,但是这一句在构造对象b时不执行,因此传参无效
mov ecx, DWORD PTR _this$[ebp];将对象Left的首地址给ecx寄存器
add ecx, ;将Left的首地址加上8,得到Top对象的首地址,作为隐含参数传递给Top的构造函数
call ??0Top@@QAE@H@Z ; 调用Top的构造函数,但是在构造对象b时,这句不执行,因此调用无效
$LN1@Left: ; 13 : j = jj; mov edx, DWORD PTR _this$[ebp];将对象Left的首地址给寄存器edx
mov eax, DWORD PTR _jj$[ebp];将参数jj给寄存器eax
mov DWORD PTR [edx+], eax;将eax寄存器里面的内容写入偏移对象Left首地址4byte处内存,即给成员变量j赋值jj ; 14 : } mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret ; 0000000cH
??0Left@@QAE@HH@Z ENDP

下面是Right构造函数的汇编码:

??0Right@@QAE@HH@Z PROC                    ; Right::Right, COMDAT
; _this$ = ecx ; 20 : Right(int kk, int ii) : Top(ii) { push ebp
mov ebp, esp
push ecx;压栈ecx的目的是为了保存对象Right的首地址预留空间
mov DWORD PTR _this$[ebp], ecx;ecx寄存器保存有对象Right的首地址,存放到刚才预留空间
cmp DWORD PTR _$initVBases$[ebp], ;_$initVBases所代表的内存存放调用Right构造函数时传入的标志,其值为0,说明
;这里将其值与0比较
je SHORT $LN1@Right;如果比较相等,就跳转到标号处执行$LN1@Right,不会调用虚基类的构造函数,否则,顺序执行,这里跳转到标号执行
;所有标号之前的语句在构造对象b时都不会执行
mov eax, DWORD PTR _this$[ebp];将对象Right的首地址给寄存器eax
mov DWORD PTR [eax], OFFSET ??_8Right@@7B@;将??_8Right@@7B@的所带表的内存地址(即Right的vtable首地址)写入到对象Right的首地址处内存
mov ecx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器ecx
push ecx;压栈ecx,为调用Top构造函数传递参数
mov ecx, DWORD PTR _this$[ebp];将对象Right首地址给寄存器ecx
add ecx, ;将对象Right的首地址加8,得到对象Top首地址,作为隐含参数传递给Top的构造函数
call ??0Top@@QAE@H@Z ; 调用Top构造函数
$LN1@Right: ; 21 : k = kk; mov edx, DWORD PTR _this$[ebp];将Right首地址给寄存器edx
mov eax, DWORD PTR _kk$[ebp];将参数kk的值给寄存器eax
mov DWORD PTR [edx+], eax;将eax里面的值写入偏移对象Right首地址4byte处,即为成员变量k赋值kk ; 22 : } mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret ; 0000000cH
??0Right@@QAE@HH@Z ENDP

下面是Top函数的汇编码:

??0Top@@QAE@H@Z PROC                    ; Top::Top, COMDAT
; _this$ = ecx ; 4 : Top(int ii) { push ebp
mov ebp, esp
push ecx;压栈的目的是为保留对象Top的首地址预留空间
mov DWORD PTR _this$[ebp], ecx;ecx寄存器里面含有对象Top的首地址,存到刚才预留的空间 ; 5 : i = ii; mov eax, DWORD PTR _this$[ebp];将对象Top的首地址给寄存器eax
mov ecx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器ecx
mov DWORD PTR [eax], ecx;将ecx的值写入对象Top首地址处,即给成员变量i赋值ii ; 6 : } mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret
??0Top@@QAE@H@Z ENDP

下面是类之间的继承关系图:

图1 菱形继承,所有类不含任何虚函数

下面是每个类的内存布局

Left(12byte)

Right(12byte)

Bottom(24byte)

上面代码中,Left和Right类对象首地址处都含有一个vbtable(误写为了vtable)指针,指向一个vbtable,vbtable里面只有两项:第一项是vbtable指针所属类的虚表指针vptr(没有就从对象首地址开始算)相对于vbtale指针的偏移量;第二项是其父类虚表指针vptr(没有的话就是对象首地址)相对于vbtable的偏移量。

从Bottom Left Right的构造函数可以看出来,在每次调用相应的构造函数之前,都会有编译器传入一个标志,以此来防止虚基类构造函数被多次调用。这就是为什么虚基类只有一份实例的原因。虚基类的构造函数总是由当前正构造的对象的构造函数调用,比如这里构造Bottom对象时,就由Bottom构造函数调用,Left和Right构造函数不会调用。

从汇编看c++的虚拟继承以及其内存布局(一)的更多相关文章

  1. 浅析GCC下C++多重继承 & 虚拟继承的对象内存布局

    继承是C++作为OOD程序设计语言的三大特征(封装,继承,多态)之一,单一非多态继承是比较好理解的,本文主要讲解GCC环境下的多重继承和虚拟继承的对象内存布局. 一.多重继承 先看几个类的定义: 01 ...

  2. c++继承中的内存布局

    今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的.看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用. ——谈VC++对象模型(美)简.格雷程化    译 译者前言 一个C ...

  3. C++各种类继承关系的内存布局

    body, table{font-family: 微软雅黑; font-size: 10pt} table{border-collapse: collapse; border: solid gray; ...

  4. C++对象模型:单继承,多继承,虚继承,菱形虚继承,及其内存布局图

    C++目前使用的对象模型: 此模型下,nonstatic数据成员被置于每一个类的对象中,而static数据成员则被置于类对象之外,static和nonstatic函数也都放在类对象之外(通过函数指针指 ...

  5. 转: c++继承中的内存布局

    英文原文: http://www.openrce.org/articles/files/jangrayhood.pdf 翻译: http://blog.csdn.net/jiangyi711/arti ...

  6. 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。

    本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...

  7. 虚继承之单继承的内存布局(VC在编译时会把vfptr放到类的头部,这和Delphi完全一致)

    C++2.0以后全面支持虚函数与虚继承,这两个特性的引入为C++增强了不少功能,也引入了不少烦恼.虚函数与虚继承有哪些特性,今天就不记录了,如果能搞了解一下编译器是如何实现虚函数和虚继承,它们在类的内 ...

  8. 从汇编看c++中的虚拟继承及内存布局(二)

    下面是c++源码: class Top {//虚基类 public: int i; Top(int ii) { i = ii; } virtual int getTop() { cout <&l ...

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

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

随机推荐

  1. .Net平台下ActiveMQ入门实例(转)

    1.ActiveMQ简介 先分析这么一个场景:当我们在网站上购物时,必须经过,下订单.发票创建.付款处理.订单履行.航运等.但是,当用户下单后,立即跳转到"感谢那您的订单" 页面. ...

  2. (原)Ubuntu14中安装GraphicsMagick

    转载请注明出处: http://www.cnblogs.com/darkknightzh/p/5661439.html 参考网址: http://comments.gmane.org/gmane.co ...

  3. R语言数据合并使用merge数据追加使用rbind和cbind

    R语言中的横向数据合并merge及纵向数据合并rbind的使用 我们经常会遇到两个数据框拥有相同的时间或观测值,但这些列却不尽相同.处理的办法就是使用merge(x, y ,by.x = ,by.y ...

  4. Parallel并行编程

    Parallel并行编程 Parallel并行编程可以让我们使用极致的使用CPU.并行编程与多线程编程不同,多线程编程无论怎样开启线程,也是在同一个CPU上切换时间片.而并行编程则是多CPU核心同时工 ...

  5. centos 6.5 安装 redis

    下载软件: wget wget http://download.redis.io/releases/redis-2.8.7.tar.gz 2.解压软件并编译安装: tar -zxvf redis-2. ...

  6. Java中的深复制与浅复制

    1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅复制仅仅复制所考虑的对象,而不 复制它所引用的对象. ...

  7. Apache监控

    Apache性能监控 http://www.cnblogs.com/fnng/archive/2012/11/11/2765463.html 要监控apache的性能,我们需要修改配置文件,允许查看a ...

  8. hdu 1208 Pascal's Travels

    http://acm.hdu.edu.cn/showproblem.php?pid=1208 #include <cstdio> #include <cstring> #inc ...

  9. ysql+heartbeat+DRBD+LVS实现mysql高可用

    在企业应用中,mysql+heartbeat+DRBD+LVS是一套成熟的集群解决方案,通过heart+DRBD实现mysql的主 节点写操作的高可用性,而通过mysql+LVS实现数据库的主从复制和 ...

  10. Implement Stack using Queues 解答

    Question Implement the following operations of a stack using queues. push(x) -- Push element x onto ...