你所不知到的C++ 之 多重继承

1. C++中class与struct。

在C++里面,class与struct没有本质的区别,只是class的默认权限是private,而struct则是public。这个概念也揭示了一点:class和struct在内部存储结构上是一致的。所以我们可以利用这一点来探讨class的实现原理。我们可以将class转换成对应的struct对象,通过struct的简单性来展示class的内存存储结构。

2. 关于class的基本内存结构

class包括成员变量和成员函数。对于成员变量,其结构和struct的结构是一致的,即按照声明的顺序,安排每个成员的内存位置。对于成员函数,如果是非虚函数(包括普通函数和静态函数),他们实际上同其他函数没有区别。对于非静态非虚函数,默认隐藏了this参数。当编译时,编译器将函数地址直接编译进去。因此,这类函数没有动态能力。对于虚函数,其函数地址将存在类this指针关联的虚函数内(参见上一篇文章),在运行时,从虚函数表内取得地址后再调用。

这样一个不带虚函数的类的内存结构,等同与一个类似的struct,而带虚函数的类的内存结构,等同于一个带有虚函数指针的struct。我用伪代码可以清晰的表示出来:

class ClassA {
   int a;
   flaot b;
   void *c;
};
//等同于
struct StructA {
   int a;
   float b;
   void *c;
<span style="font-family:Arial, Helvetica, sans-serif;">};

//带虚函数的类</span><pre name="code" class="plain">offset A::a=0, offset B::a=8, offset B::b=12
b=0x7fffad613130, pa=0x7fffad613138

class ClassAWithVirtual { int a; float b; void* c; virtual ~ClassAWithVirtual();};//等同于struct StructAWithVirtual { VTable *vtable; //包括虚函数表的vtable int a; float b; void *c;};

 


3. class的单线继承

单线继承是很简单也很容易理解的一种继承方式。如果有class B继承自class A。那么,在class B的低地址部分,是class A的成员空间。这样class B可以直接转换为class A。

如果class A有虚函数,那么class B必须也有虚函数表。那么,如果class A没有虚函数,而class B却有虚函数,那么这个时候的内存分布应该是什么样呢?class B还能否直接转换为class A呢?

#include <stdio.h>
#include <stddef.h>

#define OFFSET(X,m) (offsetof(X, m))

class A {
public:
    int a;
};

class B : public A{
public:
    int b;
    virtual ~B() { }
};

int main() {
    printf("offset A::a=%d, offset B::a=%d, offset B::b=%d\n", OFFSET(A,a), OFFSET(B, a), OFFSET(B, b));

    B b;
    A *pa = &b;
    printf("b=%p, pa=%p\n", &b, pa);

    return 0;
}

在G++ 4.6 ubuntu 10.04下,输出的结果是

offset A::a=0, offset B::a=8, offset B::b=12
b=0x7fffad613130, pa=0x7fffad613138
很明显的可以看到,当B有虚函数时,B必须在内存的开始处添加一个8字节(64位系统)的虚函数表指针。这个时候,在class B内,A的成员变量不能从偏移为0的位置开始了。



3. 不带虚函数的C++的多重继承类的内存分布

一般情况下,如果一个类继承自两个类以上,那么,它的内存分布会像垒砖头那样一层一层的添加上去。比如

#include <stdio.h>
#include <stddef.h>


class A {
public:
    int a;
};


class B {
public:
    int b;
};


class C : public A, public B{
public:
    int c;
};


int main() {
    printf("Offset: C::a=%d, C::b=%d, C::c=%d\n", offsetof(C, a), offsetof(C, b), offsetof(C, c));
    C c;
    A *pa = &c;
    B *pb = &c;


    printf("c=%p, pa=%p, pb=%p\n", &c, pa, pb);
    
    return 0;
};


输出结果是

Offset: C::a=0, C::b=4, C::c=8
c=0x7fffc90bbab0, pa=0x7fffc90bbab0, pb=0x7fffc90bbab4
这个与预想的很相似,它的等价结构是

struct C {
   struct A a;
   struct B b;
   int c;
};
的确像砖头一样,先放A,在放B,最后C的成员放上去。


那么,再考虑一种情况,如果是这样一种继承方式:

                         A

                      /     \       

                    B      C

                     \      /

                       D

那么A的成员在D内部是一份还是两份?

#include <stdio.h>
#include <stddef.h>

class A {
public:
    int a;
};

class B : public A{
public:
    int b;
};

class C : public A {
public:
    int c;
};

class D : public B, public C {
public:
   int d;
};

int main() {
    printf("Offset: D::a=%d, D::b=%d, D::c=%d, D::d\n", offsetof(D, B::a), offsetof(D, b), offsetof(D, c), offsetof(D, d));
    D d;
    B *pb = &d;
    C *pc = &d;
    A *pa_b = (A*)pb;
    A *pa_c = (A*)pc;

    printf("d=%p, pa_b=%p, pa_c=%p, pb=%p, pc=%p, d.B::a=%p, d.C::a=%p\n", &d, pa_b, pa_c, pb, pc, &d.B::a, &d.C::a);
    
    return 0;
};
先看一下输出结果:

Offset: D::a=0, D::b=4, D::c=12, D::d
d=0x7fffad730f30, pa_b=0x7fffad730f30, pa_c=0x7fffad730f38, pb=0x7fffad730f30, pc=0x7fffad730f38, d.B::a=0x7fffad730f30, d.C::a=0x7fffad730f38
事实上,如果我直接写b.a是错误的,因为编译器不知到应该选择那个a。同样的,如果写A *pa = &d也是错误的。

结合输出结果,class D内部仍然等同于

struct D {
   struct B b;
   struct C c;
   int d;
};
而且存在两份A,这两份A分别包含在B和C内部。在使用时,必须正确指出是那个。
 

由此可见,不管继承层次有多深,C++总是按照这种垒砖头的方式叠加。如果有祖先类内部有重复包含,那么,C++也会重复包含相同的内容。

这也提醒我们,多重继承不能太复杂,否则就很难搞清楚其结构关系了。


4. 带虚函数的类的多重继承的内存分布

带虚函数的情况下,情况会变得非常复杂。首先,对于最简单的一种继承方式

                         A      B

                            \   /
                             C


我们需要分好几种情况来考虑:

   1、A B虚 C非虚

   2、A B 非虚,C虚
   3、A B 其中一个虚,C虚

   4、A B C 都虚

4.1 A B 虚 C非虚

如果只有A虚, 按照默认的规则,A的内存会被安排在偏移0处。这个时候,A的虚函数表也就是C的虚函数表。

如果只有B虚,因为B的内存会被安排在A之后,那么,B的虚函数表应该在B所在位置,C没有虚函数表。


4.2 A B 不虚,C虚

这种情况下,虚函数表应该在偏移0处,然后才是A和B的内存结构。我们来验证一下:




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值