首页 > 代码库 > C++继承模型

C++继承模型

在C++继承模型中,一个派生类对象表现出来的东西,是其自己的成员加上其基类成员的总和。但这些成员如何摆放,标准并未强制规定。一般而言,先摆放基类成员,内存向高地址增长。

下面从四个部分讨论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.