下面从四个部分讨论C++继承模型:
- 单一继承不含虚函数
- 单一继承并含虚函数
- 多重继承
- 虚拟继承
1、单一继承不含虚函数
这种继承关系很简单,基类子对象包含在了派生类对象中,在内存中连续存放。但有一点需要注意,把类分解成多层可能会造成空间的膨胀。例如:
#include <iostream> #include <vector> using namespace std; class Foo { public: int val; char bit1, bit2, bit3; }; class A { public: int val; char bit1; }; class B : public A { public: char bit2; }; class C : public B { public: char bit3; }; int main() { cout << "size Foo = " << sizeof(Foo) << endl; cout << "size C = " << sizeof(C) << endl; system("pause"); return 0; }
运行结果:
两个类中包含同样的成员,空间却差了一倍,这是由于基类需要边界对齐的缘故。C++语言保证,出现在派生类中的基类子对象有其完整原样性,这是关键所在。为什么要使用这样牺牲空间的布局?原因是在对象之间拷贝时,只对子对象进行成员拷贝而不影响派生类中的成员。
2、单一继承并含虚函数
基类中有虚函数,那么编译器会给基类生成一个virtual function table和一个vptr,派生类会继承此vptr,但不会指向相同的virtual function table,而是指向自己的virtual function table。毕竟派生类一般都会重写从基类继承的虚函数。关于vptr的摆放位置,要视编译器而定。我手头的VS2013就把vptr放在了对象的开头处。
下面做个实验:
#include <iostream> #include <vector> using namespace std; class Foo { public: int x; }; class Bar : public Foo { public: int y; virtual void func() {} }; int main() { Bar bar; cout << &bar << endl; cout << &bar.x << endl; cout << &bar.y << endl; system("pause"); return 0; }
运行结果:
Foo类没有虚函数,也就没有vptr。而派生类Bar有虚函数,编译器把它的vptr插在了类的开头处,先于基类成员摆放。
3、多重继承
当出现多重继承时,指针或引用之间的转换就不会显得那么“自然”了,需要借助编译器来完成许多细节工作。
比如有如下代码:
#include <iostream> #include <vector> using namespace std; class A { public: int x; }; class B { public: int y; }; class C : public A, public B { public: int z; }; int main() { C c; A *pa = &c; B *pb = &c; cout << &c << endl; cout << pa << endl; cout << pb << endl; system("pause"); return 0; }
运行结果:
指针pa直接指向对象c的开头,所以两者的地址相同,这没有什么问题。但是,pb不是指向对象c开头,而是要向后移动,指向类B的子对象,编译器需要做如下转换:
// 伪代码 pb = (B *)(((char *)&c) + sizeof(A))
也就是说需要加上一个偏移量,指向对应的子对象,这就是“不自然”的解释。在多重继承情况下,存取各个基类中的数据成员,不需要额外的开销,数据成员的偏移量在编译时期完成。
4、虚拟继承
由于虚拟基类是共享的,所以在各个派生类中必须要由编译器添加某种信息,用来保存共享的虚拟基类的地址。关于如何添加,各个编译器厂家的实现都有所不同,而且在未来也会有更新,这里就不具体说明了。但有一点需要注意,派生类通过对象存取虚拟基类的数据成员时,编译器会对它做优化,使数据成员的地址是在编译时期就可以确定的。
参考:
《深度探索C++对象模型》 P99-P123.