C++虚拟多重继承对象模型讨论

作者:magictong

调试环境:Windows7VS2005

概述

记得刚开始写C++程序时,那还是大学时光,感觉这玩意比C强大多了,怎么就实现了多态,RTTI这些牛逼的玩意呢?当时没有深究,后来零零散散看过一些介绍的文章,也看了一些相关的书籍,总觉得说得不甚清楚。而这些问题的本质还是在于C++对象的内存模型问题,数据结构决定了你的算法嘛,在这里也是基本适用的。网上有很多讲C++对象模型的文章,但是大部分都是涉及基本继承,多重继承等等,而对于虚拟多重继承的情况则涉及不多,这篇文章则主要讲述这种情况下C++的对象模型情况,希望能够起到抛砖引玉的作用。

本文分三个步骤由浅入深的讨论这个问题。

一、先看一个最简单的例子

KVBase有一个虚函数Run(),KA从KVBase虚拟继承并且覆盖Run()函数。

源代码

#include"stdafx.h"

#include<iostream>

usingnamespacestd;

//先看一个简单的例子

classKVBase

{

public:

KVBase():m_nBase(1){}

virtualvoidRun()

{

cout <<"KVBase::Run()is called." <<endl;

}

private:

intm_nBase;

};

classKA :virtualpublic
KVBase

{

public:

KA():m_nb(2){}

virtualvoidRun()

{

cout <<"KA::Run()is called." <<endl;

}

private:

intm_nb;

};

int_tmain(intargc,_TCHAR*argv[])

{

KAa;

KVBase*pBase = &a;

cout<<"The Base Address:"<<hex
<<"0x" << &a <<endl;

cout<<"//=============>ObjectInfomation: " <<endl;

cout<<hex <<"0x" << (((int
*)&a)+0) <<": 0x" << *(((int *)&a)+0)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+1) <<": 0x" << *(((int *)&a)+1)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+2) <<": 0x" << *(((int *)&a)+2)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+3) <<": 0x" << *(((int *)&a)+3)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+4) <<": 0x" << *(((int *)&a)+4)<<endl;

pBase->Run();

return0;

}

输出:

参考一下调试的结果(第一个图是视图,直观反映成员情况但不是具体的内存对象模型,第二张图才是内存中的真正数据排布)。

a对象内存分布情况:

0x00403238是a对象的虚基类指针,注意不是虚表指针,我开始也以为是虚表指针,但是根据后面的分析,在这种情况下,a对象因为没有自己的独特虚函数,实际上它不需要专门的虚指针了,共用虚基类的就可以了(至少我认为它是这么设计的^_^)。它里面的两个成员第一个成员是目前只发现有两个可能的值0和-4,如果当前类没有独特的虚函数,则值是0,否则是-4,我把它简单称之为一个标志位,第二个成员则是一个当前位置到虚基类地址的偏移量,单位是字节(0x00ff30+0x0c=0x00ff3c)。

0x00403234则是虚基类的虚表指针,0x004015d0实际就是KA::Run的地址。

这种情况下,虚表结构图大概是这样的(虚基类在最后面):

二、继承类有自己特有的虚函数

稍微变动一下,给KA加一个自己的独特的虚函数RunKA(),分析方法同上,此时你会发现内存布局发生了一些小变化,最大的变化是KA类有了自己的虚表。

classKA :virtualpublic
KVBase

{

public:

KA():m_nb(2){}

virtualvoidRun()

{

cout <<"KA::Run()is called." <<endl;

}

virtualvoidRunKA()

{

cout <<"KA::RunKA()is called." <<endl;

}

private:

intm_nb;

};

输出:

其它部分不变,再次调试发现,内存是这样了:

视图:

0x00403258是KA类的虚表指针,里面的0x00401120正是KA::RunKA()的地址。

0x00403264是虚基类指针,存放着-4(0xfffffffc的补码,上面讨论过这种情况下表示当前类有自己的特有虚函数)和0x0c(偏移)。

0x403260是虚基类的虚表指针,里面存放这KA::Run()地址。

因此,这种情况下,虚表图大概是这样的(虚基类依然在最后面):

三、钻石型继承

好吧,分析一个复杂的钻石型继承情况之后收工,为了说明的完整性,我贴一下全部的代码。

#include"stdafx.h"

#include<iostream>

usingnamespacestd;

classKVBase

{

public:

KVBase():m_nBase(1){}

virtualvoidRun()

{

cout <<"KVBase::Run()is called." <<endl;

}

private:

intm_nBase;

};

classKA :virtualpublic
KVBase

{

public:

KA():m_na(2){}

virtualvoidRun()

{

cout <<"KA::Run()is called." <<endl;

}

virtualvoidRunKA()

{

cout <<"KA::RunKA()is called." <<endl;

}

private:

intm_na;

};

classKB :virtualpublic
KVBase

{

public:

KB():m_nb(3),m_nb2(0x1022){}

virtualvoidRun()

{

cout <<"KB::Run()is called." <<endl;

}

virtualvoidRunKB()

{

cout <<"KB::RunKB()is called." <<endl;

}

virtualvoidFuncKB()

{

cout <<"KB::FuncKB()is called." <<endl;

}

private:

intm_nb;

intm_nb2;

};

classDChild :publicKA,public
KB

{

public:

DChild():m_ndChild(4){}

virtualvoidRun()

{

cout <<"DChild::Run()is called." <<endl;

}

virtualvoidRunKA()

{

cout <<"DChild::RunKA()is called." <<endl;

}

virtualvoidRunKB()

{

cout <<"DChild::RunKB()is called." <<endl;

}

virtualvoidFuncDChild()

{

cout <<"DChild::FuncDChild()is called." <<endl;

}

private:

intm_ndChild;

};

int_tmain(intargc,_TCHAR*argv[])

{

DChilda;

KVBase*pBase = &a;

cout<<"The Base Address:"<<hex
<<"0x" << &a <<endl;

cout<<"//=============>ObjectInfomation: " <<endl;

cout<<hex <<"0x" << (((int
*)&a)+0) <<": 0x" << *(((int *)&a)+0)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+1) <<": 0x" << *(((int *)&a)+1)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+2) <<": 0x" << *(((int *)&a)+2)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+3) <<": 0x" << *(((int *)&a)+3)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+4) <<": 0x" << *(((int *)&a)+4)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+5) <<": 0x" << *(((int *)&a)+5)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+6) <<": 0x" << *(((int *)&a)+6)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+7) <<": 0x" << *(((int *)&a)+7)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+8) <<": 0x" << *(((int *)&a)+8)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+9) <<": 0x" << *(((int *)&a)+9)<<endl;

cout<<hex <<"0x" << (((int
*)&a)+10) <<": 0x" << *(((int *)&a)+10)<<endl;

pBase->Run();

return0;

}

运行一下:

现在开始变得有些有趣了,先看一下视图,大概了解一下整个布局。

然后一个个的看打印出来的那些地址都代表着什么。

0x00403384是KA的虚表指针,其中0x00401330是DChild::RunKA()地址,0x00401390是DChild::FuncDChild()地址,很明显,DChild类的虚函数是放在这个虚表里面的。

0x004033a0是KA的虚基类指针,标志位依然是-4,偏移则是0x20,算一下,0x0012FF1C+0x20果然是0x0012FF3C。

跟KB类相关的两个地址,0x00403390和0x004033a8的地址内容跟KA类是相似的(不过虚表就没有DChild的虚函数指针了),聪明的小伙伴可以自己去看下。

至于0x0040339c则依然是虚基类虚表指针,现在虚表里面是函数DChild::Run()地址。

直接上图,这种情况下,虚表结构图大概是这样的:

总结

其实C++的对象内存模型在不同的编译器下面是有差异的,有兴趣的同学可以在GCC之类的环境下测试一下,但是整体的设计思想其实都是大同小异的,我们也许没有必要把所有的细节都弄得极其清楚,但是学习这个思想才是最根本的,思考一下前人为什么要这么设计?!这么设计的好处是什么?!他们想解决什么问题?!

参考文献

[1] 虚函数表解析http://blog.csdn.net/haoel/article/details/1948051

[2] C++对象的内存布局http://blog.csdn.net/haoel/article/details/3081328

[3] 《深度探索C++对象模型》侯捷

样例代码下载

C++虚拟多重继承对象模型讨论的更多相关文章

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

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

  2. C++ 系列:C++ 对象模型

    1      何为C++对象模型 C++对象模型可以概括为以下2部分: 1.语言中直接支持面向对象程序设计的部分: 2.对于各种支持的底层实现机制 语言中直接支持面向对象程序设计的部分,如构造函数.析 ...

  3. C++对象模型(五):The Semantics of Data Data语义学

    本文是<Inside the C++ Object Model>第三章的读书笔记.主要讨论C++ data member的内存布局.这里的data member 包含了class有虚函数时 ...

  4. 【C++对象模型】第三章 Data语义学

    1. Data Member 的布局 同一个Access Section(private, public等)中,data member的顺序按照声明顺序排列,但是没有规定需要连续排序.同时编译器可能会 ...

  5. 第51课 C++对象模型分析(下)

    1. 单继承对象模型 (1)单一继承 [编程实验]继承对象模型初探 #include <iostream> using namespace std; class Demo { protec ...

  6. C++对象模型——函数的效能(第四章)

    4.3 函数的效能 在以下的这组測试中,在不同的编译器上计算两个3D点,当中用到一个nonmember friend function,一个member function,以及一个 virtual m ...

  7. C++ 对象的内存布局(上)

    C++ 对象的内存布局(上) 陈皓 http://blog.csdn.net/haoel 点击这里查看下篇>>> 前言 07年12月,我写了一篇<C++虚函数表解析>的文 ...

  8. C++ 对象的内存布局(上)

    本文转载自haoel博主的博客:陈皓专栏 [空谷幽兰,心如皓月] 原文地址:C++ 对象的内存布局(上) C++ 对象的内存布局(上) 陈皓 http://blog.csdn.net/haoel 点击 ...

  9. [算法]从Trie树(字典树)谈到后缀树

    我是好文章的搬运工,原文来自博客园,博主July_,地址:http://www.cnblogs.com/v-July-v/archive/2011/10/22/2316412.html 从Trie树( ...

随机推荐

  1. NLP系列(4)_朴素贝叶斯实战与进阶

    作者: 寒小阳 && 龙心尘 时间:2016年2月. 出处:http://blog.csdn.net/han_xiaoyang/article/details/50629608 htt ...

  2. 安卓高级5 zXing

    ZXing作者的github地址: https://github.com/zxing/zxing 这里为大家也提供一个封装好的最新的ZXing Lib: https://github.com/xuyi ...

  3. 远程通信(RPC,Webservice,RMI,JMS、EJB、JNDI的区别)对比

    总结这些概念都是易混淆,最基本概念定义复习和深入理解,同时也是架构师必备课程 RPC(Remote Procedure Call Protocol) RPC使用C/S方式,采用http协议,发送请求到 ...

  4. lldb po [$view recursiveDescription]; 打印视图层次

    备忘: lldb 打印视图层次: 对某一个view,比如operationBgView po [operationBgView recursiveDescription]; 

  5. Launcher3 HotSeat显示名称

    今天闲的无聊,研究了下launcher代码,看到Hotseat.java的时候,想起来以前有做过显示hotseat中应用名称,因为换了公司代码都没拿出来,就想在试着修改,看了很久发现无从下手,记得ho ...

  6. 如何获得Android手机的软件安装列表

    Android的PackageManager类用于检索目前安装在设备上的应用软件包的信息.你可以通过调用getpackagemanager()得到PackageManager类的一个实例.对查询和操作 ...

  7. 获取客户信息SQL

    /*取客户信息SQL*/ --客户信息 SELECT hou.name 业务实体, hca.account_number 客户编号, hp.party_name 客户名称, arp_addr_pkg. ...

  8. AsnycTask的内部的实现机制

    AsnycTask的内部的实现机制 写在前面 我们为什么要用AsnycTask. 在Android程序开始运行的时候会单独启动一个进程,默认情况下所有 这个程序操作都在这个进程中进行.一个Androi ...

  9. 游戏引擎cocos2d-android使用大全

    做手机游戏需要三个核心的类,即:SurfaceView,SurfaceHolder,Thread.帧数要在30帧左右是最好的. cocos2d游戏引擎 封装好的框架,可直接使用 cocos2d-and ...

  10. android 图片网络下载github开源框架之Universal-Image-Loader

    最近在做妙趣剪纸项目,剪纸应用项目链接.发扬传统文化,大家多多关注. 需要自己搭建服务器,我用的是新浪sae,简直秒杀京东云几条街,把图片放在网上下载,但是图片经常下载要遇到很多问题,包括oom等.所 ...