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

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

 

 

作者:magictong

调试环境:Windows7VS2005

 

概述

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

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

 

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

KVBase有一个虚函数Run()KAKVBase虚拟继承并且覆盖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对象内存分布情况:


0x00403238a对象的虚基类指针,注意不是虚表指针,我开始也以为是虚表指针,但是根据后面的分析,在这种情况下,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;

};

 

输出:


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


视图:


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


0x00403264是虚基类指针,存放着-40xfffffffc的补码,上面讨论过这种情况下表示当前类有自己的特有虚函数)和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;

}

 

运行一下:


 

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


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

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


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


KB类相关的两个地址,0x004033900x004033a8的地址内容跟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++对象模型》侯捷


样例代码下载

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值