从汇编看c++中含有虚基类对象的析构
c++中,当继承结构中含有虚基类时,在构造对象时编译器会通过将一个标志位置1(表示调用虚基类构造函数),或者置0(表示不调用虚基类构造函数)来防止重复构造虚基类子对象。如下图菱形结构所示:

当构造类Bottom对象时,Bottom构造函数里面的c++伪码如下(单考虑标志位,不考虑其他):
//Bottom构造函数伪码
flag = ;//标志位
if (flag) {
调用虚基类Top的构造函数
}
flag = ;//标志位清零
调用Left的构造函数
flag = ;//标志位清零
调用Right的构造函数
其他操作 //Left构造函数伪码
if (flag) {
调用虚基类Top的构造函数
}
其他操作 //right构造函数伪码:
if (flag) {
调用虚基类Top的构造函数
}
其他操作
编译器通过这种方式,保证虚基类的构造函数不会重复调用,那么析构的时候,是不是也是通过这种标志位的方式呢?下面来看c++源码:
class Top {
private:
    int _top;
public:
    Top(int top = ) : _top(top) {}
    virtual ~Top() {}
    virtual int get1() {
        return ;
    }
};
class Left : virtual public Top {
private:
    int _left;
public:
    Left(int left = ) : _left(left) {}
    virtual ~Left() {}
    virtual int get2() {
        return ;
    }
};
class Right : virtual public Top {
private:
    int _right;
public:
    Right(int right = ) : _right(right) {}
    virtual ~Right() {}
    virtual int get3() {
        return ;
    }
};
class Bottom : public Left, public Right {
private:
    int _bottom;
public:
    Bottom(int bottom = ) : _bottom(bottom) {}
    virtual ~Bottom() {}
    virtual int get4() {
        return ;
    }
};
int main() {
    Bottom b;
}
上面类之间的继承关系是一个菱形继承,下面就来看一下析构的时候汇编码。
析构的时候,并不直接调用Bottom的析构函数,而是先调用的析构代理函数,其部分相关的汇编码如下:
lea ecx,[b];将对象b的首地址给寄存器ecx
012814BD call Bottom::`vbase destructor' (1281019h);调用析构代理函数
析构代理函数函数的汇编码如下(只列出相关部分)
mov dword ptr [ebp-],ecx ;寄存器ecx里面存放对象b首地址(this指针,即对象b首地址),将ecx的值给ebp-8所代表的的内存
01281C73 mov ecx,dword ptr [this];将this指针给寄存器ecx
01281C76 add ecx,1Ch;ecx里面的值加28byte,调整this指针,所指位置如图1所示
01281C79 call Bottom::~Bottom (1281055h) ;调用类Bottom的析构函数
01281C7E mov ecx,dword ptr [this];this指针给寄存器ecx
01281C81 add ecx,1Ch;ecx里面的 值加28byte,调整this指针,所指位置如图4所示
01281C84 call Top::~Top (1281096h) ;调用虚基类Top的析构函数
在析构代理函数里面先调用了Bottom的析构函数,然后调用虚基类Top的析构函数
Bottom的析构函数汇编码如下(只列出相关部分):
mov dword ptr [ebp-14h],ecx ;寄存器ecx里面存有this指针,所指位置如上图1,将其值存到ebp-14h所代表的内存中
00C13C52 mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(this指针)给寄存器eax
00C13C55 mov dword ptr [eax-1Ch],offset Bottom::`vftable' (0C16758h);虚表首地址给向上偏移this指针28byte处内存,即对象b首地址处(设置第一处虚表)
00C13C5C mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即对象b的首地址)给寄存器eax
00C13C5F mov dword ptr [eax-10h],offset Bottom::`vftable' (0C1674Ch);虚表首地址给向上偏移this指针16byte处内存,即父类Right子对象首地址(设置第二处虚表)
00C13C66 mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即this指针)给寄存器eax
00C13C69 mov ecx,dword ptr [eax-18h];将向上偏移this指针24byte处内存内容(即vbtable首地址)给寄存器ecx
00C13C6C mov edx,dword ptr [ecx+];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top子对象首地址的偏移量)给寄存器edx
00C13C6F mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即对象b的首地址)给寄存器eax
00C13C72 mov dword ptr [eax+edx-18h],offset Bottom::`vftable' (0C16740h);eax为this指针,edx为刚获得的偏移量 eax+edx-18h
;这里将虚表首地址给该内存(设置第三处虚表)
00C13C7A mov dword ptr [ebp-4],0
00C13C81 mov ecx,dword ptr [ebp-14h] ;将ebp-14h所代表的内存里面的值(即对象b的首地址)给寄存器ecx
00C13C84 sub ecx,4 ;ecx里面的值减4,调整this指针,此时this指针所指位置如图2
00C13C87 call Right::~Right (0C11091h);调用父类Right子对象的析构函数
00C13C8C mov dword ptr [ebp-4],0FFFFFFFFh
00C13C93 mov ecx,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即this指针)给寄存器ecx
00C13C96 sub ecx,10h ;ecx里面的值减16,调正this指针,this指针所指位置如图3
00C13C99 call Left::~Left (0C110DCh);调用父类Left子对象的析构函数 01281C20 mov dword ptr [ebp-8],ecx;ecx里面存放this指针,所指位置如图2,将其存放到 ebp-8所代表的内存
01281C23 mov eax,dword ptr [this];将this指针给寄存器eax
01281C26 mov dword ptr [eax-0Ch],offset Right::`vftable' (1286874h);将虚表首地址给向上偏移this指针12byte处内存,设置第一处虚表
01281C2D mov eax,dword ptr [this];将this指针给寄存器eax
01281C30 mov ecx,dword ptr [eax-] ;将向上偏移this指针8byte处内存内容(即vbtable首地址)给寄存器ecx
01281C33 mov edx,dword ptr [ecx+];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top首地址的偏移量),给寄存器edx
01281C36 mov eax,dword ptr [this] ;将this指针给寄存器eax
01281C39 mov dword ptr [eax+edx-],offset Right::`vftable' (1286814h) ;eax是this指针,edx是偏移量,因此eax+edx-8即使虚基类Top子对象的首地址
;这里将虚表首地址给该内存,设置第二处虚表
   
      
图1 图2 图3
在Bottom的析构函数里面,首先调用了Right的析构函数,然后调用了Left的析构函数。并且在调用这些函数之前,设置好了相关的虚表。
下面是Right析构函数的汇编码(只列出相关部分):
01281C20 mov dword ptr [ebp-8],ecx;ecx里面存放this指针,所指位置如图2,将其存放到 ebp-8所代表的内存
01281C23 mov eax,dword ptr [this];将this指针给寄存器eax
01281C26 mov dword ptr [eax-0Ch],offset Right::`vftable' (1286874h);将虚表首地址给向上偏移this指针12byte处内存,设置第一处虚表
01281C2D mov eax,dword ptr [this];将this指针给寄存器eax
01281C30 mov ecx,dword ptr [eax-8] ;将向上偏移this指针8byte处内存内容(即vbtable首地址)给寄存器ecx
01281C33 mov edx,dword ptr [ecx+4];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top首地址的偏移量),给寄存器edx
01281C36 mov eax,dword ptr [this] ;将this指针给寄存器eax
01281C39 mov dword ptr [eax+edx-8],offset Right::`vftable' (1286814h) ;eax是this指针,edx是偏移量,因此eax+edx-8即使虚基类Top子对象的首地址
;这里将虚表首地址给该内存,设置第二处虚表
在Right函数中并没有调用虚基类Top的析构函数,函数只是一开始的时候,设置好了相关的虚表。
下面是Left函数析构函数的汇编码(值列出相关部分):
mov dword ptr [ebp-],ecx;ecx里面存放的this指针,所指位置如图3,将其存放到 ebp-8所代表的内存里面
mov eax,dword ptr [this];将this指针给寄存器eax
mov dword ptr [eax-0Ch],offset Left::`vftable' (1286794h);将虚表首地址给向上偏移this指针12byte处内存,设置第一处虚表
0128188D mov eax,dword ptr [this] ;将this指针给寄存器eax
01281890 mov ecx,dword ptr [eax-8] ;将向上偏移this指针8byte处内存内容(即vbtable首地址)给寄存器ecx
01281893 mov edx,dword ptr [ecx+4];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top首地址的偏移量),给寄存器edx
01281896 mov eax,dword ptr [this];将this指针给寄存器eax
01281899 mov dword ptr [eax+edx-8],offset Left::`vftable' (1286788h);eax是this指针,edx是偏移量,因此eax+edx-8即使虚基类Top子对象的首地址
;这里将虚表首地址给该内存,设置第二处虚表
在Left函数里面也没有调用虚基类Top的析构函数,函数只是一开始的时候,设置好了相关的虚表
最后是虚基类Top的析构函数(只列出相关部分):
012816D0 mov dword ptr [ebp-],ecx;寄存器ecx里面存有this指针,所指位置如图1所示 ,将this指针的值存入ebp-8所代表的内存
012816D3 mov eax,dword ptr [this];将this指针给寄存器eax
012816D6 mov dword ptr [eax],offset Top::`vftable' (128677Ch);将虚表首地址给this指针所指向的内存
通过上面的汇编码,可以发现,菱形结构中的析构函数并没有使用构造函数中的标记来防止重复析构,而是将虚基类Top的析构函数放到最后调用。在调用Left和Right的析构函数时,根本不调用虚基类Top的析构函数。虚基类Top的析构含仅仅由析构代理函数调用。
从汇编看c++中含有虚基类对象的析构的更多相关文章
- C++派生类中如何初始化基类对象(五段代码)
		
今天收到盛大的面试,问我一个问题,关于派生类中如何初始化基类对象,我在想派生类对于构造函数不都是先构造基类对象,然后在构造子类对象,但是如果我们在成员初始化列表先初始化派生类的私有成员,在函数内去调用 ...
 - C++中 引入虚基类的作用
		
当某类的部分或全部直接基类是从另一个基类共同派生而来时,这直接基类中,从上一级基类继承来的成员就拥有相同的名称,派生类的对象的这些同名成员在内存中同时拥有多个拷贝,同一个函数名有多个映射.可以使用作用 ...
 - C++调用动态库中的虚基类成员函数时总是进错函数
		
原创文章,转载请注明作者与本文原始URL. 问题描述:最近遇到这样一个问题,在调用C++的一个成员函数时,总是进错函数.在调用 pMsg->GetMsgContent() 的时候,总是进入到 p ...
 - 从汇编看c++中的虚拟继承及内存布局(二)
		
下面是c++源码: class Top {//虚基类 public: int i; Top(int ii) { i = ii; } virtual int getTop() { cout <&l ...
 - C++中虚基类在派生类中的内存布局
		
今天重温C++的知识,当看到虚基类这点的时候,那时候也没有太过追究,就是知道虚基类是消除了类继承之间的二义性问题而已,可是很是好奇,它是怎么消除的,内存布局是怎么分配的呢?于是就深入研究了一下,具体的 ...
 - 详解C++中基类与派生类的转换以及虚基类
		
很详细!转载链接 C++基类与派生类的转换在公用继承.私有继承和保护继承中,只有公用继承能较好地保留基类的特征,它保留了除构造函数和析构函数以外的基类所有成员,基类的公用或保护成员的访问权限在派生类中 ...
 - C++ 虚基类的定义、功能、规定
		
原文声明:http://blog.sina.com.cn/s/blog_93b45b0f01011pkz.html 虚继承和虚基类的定义是非常的简单的,同时也是非常容易判断一个继承是否是虚继承的,虽然 ...
 - C++学习20  虚基类详解
		
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如非常经典的菱形继承层次.如下图所示: 类A派生出类B和类C,类D继承自类B和类 ...
 - RTTI、虚函数和虚基类的实现方式、开销分析及使用指导(虚函数的开销很小,就2次操作而已)
		
白杨 http://baiy.cn “在正确的场合使用恰当的特性” 对称职的C++程序员来说是一个基本标准.想要做到这点,首先要了解语言中每个特性的实现方式及其开销.本文主要讨论相对于传统 C 而言, ...
 
随机推荐
- 编写优秀jQuery插件的10个技巧
			
前言:在开发过很多 jQuery 插件以后,我慢慢的摸索出了一套开发jQuery插件比较标准的结构和模式.这样我就可以 copy & paste 大部分的代码结构,只要专注最主要的逻辑代码就行 ...
 - python爬虫下载youtube单个视频
			
__author__ = 'Sentinel'import requestsimport reimport jsonimport sysimport shutilimport urlparse &qu ...
 - editor.md实现Markdown编辑器
			
editor.md实现Markdown编辑器 Markdown和Editor.md简介 Markdwon编辑器在技术工作者圈子中已经越来越流行,简单的语法,统一的格式,强大的扩展功能,最重要的是:你可 ...
 - AngularJS 基础教程二:
			
5.过滤器 过滤器的主要功能是格式化数据 可以使用Angular提供的过滤器,也可以自定义过滤器 Angular过滤器: currency(货币).date(日期).filter(子串匹配).json ...
 - js拖拽进度条
			
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm4.aspx. ...
 - Java 网络编程---分布式文件协同编辑器设计与实现
			
目录: 第一部分:Java网络编程知识 (一)简单的Http请求 一般浏览网页时,使用的时Ip地址,而IP(Internet Protocol,互联网协议)目前主要是IPv4和IPv6. IP地址是一 ...
 - MVC 自定义错误处理
			
1. Application_Error namespace Libaray.Web{ public class MvcApplication : System.Web.HttpApplication ...
 - Powershell环境变量
			
Powershell环境变量 9 12月, 2011 在 Powershell tagged 变量by Mooser Lee 本文索引 [隐藏] 1读取特殊的环境变量 2查找环境变量 3创建新的环境 ...
 - Unix/Linux环境C编程入门教程(40) 初识文件操作
			
 1.函数介绍 close(关闭文件) 相关函数 open,fcntl,shutdown,unlink,fclose 表头文件 #include<unistd.h> 定义函数 int ...
 - JS帮你计算属相
			
背景:一个人出生在2014年的正月初一,他的生肖到底是属蛇还是属马呢?这就要确定那一天才是一年的开始.是春节还是立春?每年的春节是正月初一,但是生肖必须是从立春日开始计算.春节是1912年孙中 ...