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 ClassAWithVirtual { int a; float b; void* c; virtual ~ClassAWithVirtual();};//等同于struct StructAWithVirtual { VTable *vtable; //包括虚函数表的vtable int a; float b; void *c;};
单线继承是很简单也很容易理解的一种继承方式。如果有class B继承自class A。那么,在class B的低地址部分,是class A的成员空间。这样class B可以直接转换为class A。
如果class A有虚函数,那么class B必须也有虚函数表。那么,如果class A没有虚函数,而class B却有虚函数,那么这个时候的内存分布应该是什么样呢?class B还能否直接转换为class A呢?
在G++ 4.6 ubuntu 10.04下,输出的结果是
很明显的可以看到,当B有虚函数时,B必须在内存的开始处添加一个8字节(64位系统)的虚函数表指针。这个时候,在class B内,A的成员变量不能从偏移为0的位置开始了。
3. 不带虚函数的C++的多重继承类的内存分布
一般情况下,如果一个类继承自两个类以上,那么,它的内存分布会像垒砖头那样一层一层的添加上去。比如
输出结果是
这个与预想的很相似,它的等价结构是
的确像砖头一样,先放A,在放B,最后C的成员放上去。
那么,再考虑一种情况,如果是这样一种继承方式:
A
/ \
B C
\ /
D
那么A的成员在D内部是一份还是两份?
先看一下输出结果:
事实上,如果我直接写b.a是错误的,因为编译器不知到应该选择那个a。同样的,如果写A *pa = &d也是错误的。
结合输出结果,class 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的内存结构。我们来验证一下: