c++继承下的内存布局

单一继承:
 
使用如下的例子:
 
class
{
public :
     virtual void f(){}
     virtual void h(){}
     int a;

};

class B : public A
{
public :
     virtual void f(){}
     virtual void g(){}
     int b;
};
 
通过下面的函数调用可以看出对象的内存布局和虚函数表的结构
 
A a;
B b;
A *pa = &b;
pa - >h();
pa - >f();
// pa->g();
B *pb = &b;
pb - >g();
 
其中注释的那一行是编译错误的,因为不可以由基类指针去调用派生类的函数。
vs编译器存放b对象的格式,首先看一下debug下的图示:
 
      
 
可以看到B的对象存放基类A的subobject的时候,共用了一个虚函数指针,作为B对象的虚函数指针,指向自己的虚函数表,但是虚函数表里怎么存放自己和基类的虚函数的呢?通过调用各个虚函数并查看汇编代码,可以看出,虚函数表存放的方式是:
 
B::f() (本来是A::f(),但是B中重写了这个虚函数,所以替换成B的虚函数)
A::h() (没有重写,没有替换,按顺序存放)
B::g() (存放完A的所有虚函数,在放B的虚函数)
 
可以看出经由指向派生类对象的基类指针无法直接调用基类的虚函数,但是可以通过静态的方式调用,即:
pa - >A : :f();
 
小结:在单一继承下,派生类对象只存放了一个虚函数指针和一个虚函数表,将基类和派生类的所有虚函数按继承的顺序存放,基类重写的虚函数则替换基类的虚函数。
 
多重继承:
 
先看如下的例子:
 
class
{
public :
     virtual void f(){}
     virtual void h(){}
     int a;

};
class AA{
public :
     int aa;
     virtual void f(){}
};

class B : public A, public AA
{
public :
     virtual void f(){}
     virtual void g(){}
     int b;
};
 
我们可以使用如下代码反汇编来确定对象的布局:
 
A a;
B b;
A *pa = &b;
AA * paa = &b;
B *pb = &b;
int xx = pb - >b;
xx = pb - >a;
xx = paa - >aa;
pa - >h();
pa - >f();
paa - >f();
pb - >g();
pb - >f();
 
先看debug下的vs查看对象的值:
 
 
通过上面的图可以看出B与第一个基类A共用了一个虚指针,基类AA的subobject也有一个虚指针,总共有两个虚指针,即有两个虚函数表。
通过查看汇编代码可以确定B的对象的内存布局为:
 

 
当调换基类的继承顺序时,即class B: public AA, public A,这时候AA的subobject会在最上面,可以看出编译器按顺序堆放基类subobject。
小结:多重继承的时候,派生类的虚函数指针和第一个基类的虚函数指针共享一个,其他基类均包含一个自己的虚指针。
 
单一虚继承:
对于单一虚继承,先使用如下一个例子:
 
class
{
public :
     virtual void f(){}
     virtual void h(){}
     int a;

};
class B : virtual public A
{
public :
     virtual void f(){}
     int b;
};
 
这个例子中,派生类只重写了基类的虚函数,没有其他自己独有的虚函数,使用如下代码的反汇编查看内存对象布局:
 
A a;
B b;
A *pa = &b;
B *pb = &b;
int xx = pb - >b;
xx = pb - >a;
pa - >h();
pa - >f();
pb - >f();
 
得到内存布局如图:
 
   
 
再查看反汇编的时候,注意到,在使用指向派生类对象的派生类指针寻址基类的成员的时候经过了虚基类表这么一层去寻址基类的位置,虚基类表里面的8就是相对于 该指针的偏移而不是对象起始位置的基类偏移量,后面可以看出),于是内存中的B对象就存放了一个指向虚基类表的指针,但是并没有保存自己的虚函数指针,而是在需要调用虚函数的时候,寻址到A的subobject的虚函数指针,然后调用虚函数表的虚函数。
在我猜测,这个可能是编译器的优化,因为B没有自己独立的虚函数,只是重写了基类的虚函数,则就不产生B的独立虚函数表,和A的虚函数表优化在一起。
 
通过下面这个例子(B增加了自己独立的虚函数g),可以稍微肯定这样的猜测:
 
class
{
public :
     virtual void f(){}
     virtual void h(){}
     int a;

};
class B : virtual public A
{
public :
     virtual void f(){}
     virtual void g(){}
     int b;
};
 
采用如下的代码的反汇编结果进行查看内存的布局:
A a;
B b;
A *pa = &b;
B *pb = &b;
int xx = pb - >b;
xx = pb - >a;
pa - >h();
pa - >f();
pb - >f();
pb - >g();
 
内存布局如图:
 
 
B的对象中多出了一个B的虚函数指针指向B的虚函数表,而虚函数表里只存放了只在B中定义的虚函数,而没有“重写基类的”的虚函数。
在查看虚基类表的时候发现存放的值仍然是8,由此可以断定是相对于指向基类指针的地址的偏移量。
 
如下代码对多个虚基类的情况进行了鉴定,发现虚基类的偏移均存放在虚基类表中。
 
class
{
public :
     virtual void f(){}
     virtual void h(){}
     int a;

};
class AA{
public :
     int aa;
     virtual void f(){}
};

class B : virtual public A, virtual public AA
{
public :
     virtual void f(){}
//    virtual void g(){}
     int b;
};
 
图就不画了,只要在虚基类表中添加一项AA偏移量。
 
 
重复继承、虚拟继承:
对于虚拟继承解决重复继承的情况,用如下例子:
 
class A
{
public :
     virtual void f(){}
     virtual void h(){}
     int a;

};
class B : virtual public A
{
public :
     virtual void f(){}
//    virtual void g(){}
     int b;
};
class C : virtual public A{
public :
//    virtual void h(){}
     int c;
};
class D : public B, public C{
public :
     virtual void f(){}
     int d;
};
 
注意上面的例子的特点,派生类中均没有定义虚函数,都是重写基类的虚函数,根据sizeof(D) = 28的结果和编译器的优化,猜测在D对象的内存布局中,B的subobject和C的subobject均没有产生虚函数指针,也没有产生虚函数表,但他们各自都有一个虚基类指针和虚基类表,它的内存布局如下图:
 

 
如果某个派生类有自己的虚函数,则就会产生一个虚函数指针和虚函数表,就是这个意思,不画了,烦死了。
 
 
 
 
 
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值