类的继承
C++中类与类之间的继承有三种方式
其中静态基础和动态继承互斥, 虚继承可与静态继承或动态继承之一组合.
静态继承+单继承
无虚函数或虚表的类的继承为静态继承. 设有一个不含虚函数和虚表的类
struct A { ... };
另一个同样不含虚函数和虚表的类继承该类时就会发生静态继承
struct B : A { ... };
静态继承实质上是复制基类的内容到子类, 从而构建出新的类型. 除了共有的成员变量的偏移是一样的之外, 基类和子类之间无其他更多关联.
在其上调用类函数时, 用哪个类的指针, 就直接调用哪个类的函数, 无任何多态性质.
void* p = new B;
((B*) p)->f();
((A*) p)->f();
void* p = new A;
((B*) p)->f();
((A*) p)->f();
静态继承+多继承
C++允许多继承, 其实现方式为子类复制各个基类的内容到子类. 此时基类成员变量的偏移与子类中继承的成员变量的偏移将不一致, 若像单继承那样直接用基类指针指向子类对象并调用基类函数将由于成员变量偏移不一致, 导致基类函数访问到错误的子类对象中继承得到的基类成员变量.
struct A { int a; };
struct B { int b; int getA() { return b; } };
struct C : A, B {};
void* c = new C{ 1, 2 };
((C*) c)->getB();
((B*) c)->getB();
offsetof(C, b)
offsetof(B, b)
若要用基类指针指向多继承的子类对象, 则需要先让指针转为子类指针, 然后再用static_cast转为基类指针. 该变换可以在编译期完成.
((B*)(C*) c)->getB();
获取成员变量在类中的偏移, 有offsetof() 宏, 但标准库并未提供获取基类在子类中的偏移的工具, 可以写一个宏自行实现:
#define typeoffsetof(t, s) (((size_t)(s*)(t*)1) - 1)
typeoffsetof(C, A)
typeoffsetof(C, B)
注: 0地址是特殊值, 0地址转任何类型都只会得到0地址
另外静态继承发生菱形继承时, 菱形继承链底端的子类将复制多份基类的内容, 分别来自菱形继承链腰部的各个类. 子类可通过腰部基类::顶部基类成员 访问各个基类的内容
struct Z { int z; };
struct A : Z { int a; };
struct B : Z { int b; };
struct C : A, B {};
void* p = new C{ 1, 2, 3, 4 };
((C*) p)->A::z
((C*) p)->a
((C*) p)->B::z
((C*) p)->b
子类可以先转为腰部基类后再转为顶部基类来获取各个基类的内容.
((size_t)(Z*)(A*)(C*) 1) - 1
((size_t)(Z*)(B*)(C*) 1) - 1
动态继承
当类定义了虚函数后, 该类的对象即有了虚表, 从该类开始, 所有的子类对其父类的继承都为动态继承, 或者说动态继承有扩散的特性.
struct A { virtual void f() {}; }
如果子类不覆写(override)基类虚函数, 那么子类的虚表可直接复制自基类, 与基类完全一致(有虚继承亦可如此). 如果有覆写, 那么可以将基类的虚表复制一份到子类, 以此为模板, 用子类覆写的函数的地址覆盖基类的函数的地址.
类对象有了虚表, 即给对象打上了构造他的类的标记. 无论是用基类(仅限有虚表的基类), 还是用子类指针去调用对象的虚函数, 最终都会调用构造他的类的实现, 此即称为运行时多态.
虚继承
类继承时使用virtual的继承称为虚继承. 基类可有虚表亦可无虚表.
虚继承除了复制基类内容到子类外, 还添加了基类偏移指针(虚继承表)到子类, 子类访问基类内容时不直接取其偏移地址然后访问(这是类访问成员变量的一般方式), 而是通过基类偏移指针访问基类内容. 单论该类的对象来说, 由于增加了一层指针访问, 类中部分成员变量的访问变慢了(大约200ps).
struct Z { int z; };
struct A : Z {};
struct B : virtual Z {};
double base = timeIt([] {});
timeIt([b = new B]{ volatile int z = b->z; }) - base;
timeIt([a = new A]{ volatile int z = a->z; }) - base;
但发生菱形继承时, 菱形继承链底端的多继承子类对象可以只含有一个顶端基类的内容, 其内腰部基类对象可以共享一个顶端基类的内容.
struct Z { int z; };
struct A : virtual Z {};
struct B : virtual Z {};
struct C : A, B {};
void* p = new C;
p->z;
基类被虚继承后其所有子类共享一个基类对象, 但非虚继承的还是会单独复制一份基类的内容
struct Z { int z; };
struct A : virtual Z {};
struct B : virtual Z {};
struct C : Z {};
struct D : A, B, C {};
void* p = new D;
((D*) p)->A::z = 1;
((D*) p)->B::z = 2;
((D*) p)->C::z = 3;
((D*) p)->A::z == 2;
((D*) p)->B::z == 2;
((D*) p)->C::z == 3;
子类中如果有虚继承, 那么所有从C中来的对各成员用大括号初始化的语法(默认构造函数)将不可用, 需要显式写构造函数来初始化.
虚继承而来的成员变量不可取成员地址, 只可以取普通的地址
&D::A::z;
&D::B::z;
&D::C::z;
void* p = new D;
auto p1 = &((D*) p)->A::z;
auto p2 = &((D*) p)->B::z;
auto p3 = &((D*) p)->C::z;
各基类内容在内存布局上, 从地址低位开始, 是非虚继承而来的成员排在前列, 然后才是虚继承而来的成员
虚表 虚继承表
如果类中含虚函数, 那么该类及其所有子类都有虚表(虚函数表) 如果类中虚继承了一个基类, 那么该类及其所有子类都有虚继承表
接口
一般称呼一个没有成员变量只有虚函数的类为接口或接口类. 此类由于没有成员变量而没有多继承的问题, 也没有虚继承的必要. 一些面向对象语言(java等)中, 将C++的类进一步细化为一般类和接口两种, 并要求一般类不允许除接口之外的多继承. 没有静态继承, 一切函数都为虚函数.
而C++中为复用基类数据而使用的虚继承, 在这些语言中可改写为将基类做为成员变量来模拟多继承乃至菱形继承, 改继承链为引用链.
附录
计时函数实现
template<typename T>
double timeIt(T const& func) {
LARGE_INTEGER t1, t2, tc;
QueryPerformanceFrequency(&tc);
size_t sum = 0;
size_t cnt = 0;
while (sum < 1000000000 / 50) {
cnt++;
QueryPerformanceCounter(&t1);
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
func(); func(); func(); func(); func();
QueryPerformanceCounter(&t2);
sum += (t2.QuadPart - t1.QuadPart) * 1000000000 / 50 / tc.QuadPart;
}
return (double) sum / cnt;
}
|