目录
菱形继承的内存分布
假设继承体系如左图,则 D 的对象内存布局如右图。
【结论】D的对象可以分成3块,每个直接基类(B和C)各占一块,按继承的顺序排放,最后是派生类(D)自己的成员变量。而B和C的数据块中都拷贝了一份间接基类A的数据。
代码验证:
class A{
public:
int d = -1;
int i = 1;
char c = 'a';
void f(){cout<<"A::f()"<<endl;}
};
class B: public A{
public:
int i = 2; // 覆盖基类
char c = 'b'; // 覆盖基类
void f(){cout<<"B::f()"<<endl;}
};
class C: public A{
public:
char c = 'c'; // 覆盖基类
int i2 = 3;
void f(){cout<<"C::f()"<<endl;}
};
class D: public B, public C{
public:
int j = 4;
char c = 'd'; // 覆盖基类
};
int main(){
D x;
cout<<&x<<" size:\t"<<sizeof(x)<<endl<<endl;
// A的所有成员都不能显式访问 error: 'A' is an ambiguous base of 'D'
// x.A::f(); cout<< x.A::d << x.A::c << x.A::i;
x.B::f();
cout<<"B::d "<<&x.B::d <<"\t"<<x.B::d<<endl
<<"B::i "<<&x.B::i <<"\t"<<x.B::i<<endl
<<"B::c "<<(void*)&x.B::c<<"\t"<<x.B::c<<endl<<endl; // ok
x.C::f();
cout<<"C::d "<<&x.C::d <<"\t"<<x.C::d<<endl
<<"C::i "<<&x.C::i <<"\t"<<x.C::i<<endl
<<"C::c "<<(void*)&x.C::c<<"\t"<<x.C::c<<endl
<<"C::i2 "<<&x.C::i2 <<"\t"<<x.C::i2<<endl<<endl; // ok
cout<<"D::i2 "<<&x.D::i2 <<"\t"<<x.D::i2<<endl
<<"D::j "<<&x.D::j <<"\t"<<x.D::j<<endl
<<"D::c "<<(void*)&x.D::c<<"\t"<<x.D::c<<endl<<endl; // ok
// D中定义(或覆盖)的成员、继承的唯一性成员可以直接访问,等价于使用 D::
cout<<"x.i2 "<<&x.i2 <<"\t"<<x.i2<<endl
<<"x.j "<<&x.j <<"\t"<<x.j<<endl
<<"x.c "<<(void*)&x.c <<"\t"<<x.c<<endl<<endl;
// 有二义性的不能访问
// cout<<x.d <<x.i; // ambiguous
// cout<<x.D::d <<x.D::i; // ambiguous
// x.f(); x.D::f(); // ambiguous
// 打印不能直接访问的地址
int* p = &x.B::d;
cout<<p+1<<"\t"<<*(p+1)<<endl
<<p+2<<"\t"<<*(char*)(p+2)<<endl;
p = &x.C::i;
cout<<p+1<<"\t"<<*(char*)(p+1)<<endl;
}
运行结果:
虚继承的内存分布
在上面的菱形继承中,如果直接访问间接基类A的成员,就会出现二义性调用的错误。因为B和C都有一份拷贝,所以必须使用作用域限定符,指定使用哪个直接基类的数据。而使用虚继承可以消除对间接基类A的重复拷贝,使得D的对象中只有一份拷贝。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
使用虚继承得到的派生类(B和C),它的对象中有隐藏的虚基类表指针(vbptr)指向虚基类表(vbtable),表中记录了虚基类表指针与虚基类之间的偏移量。而虚基类部分被放到对象内存的最后面。
虚基类表中存放了2个偏移量:
- 第1个是当前的vbptr 相对于虚函数表指针(vfptr)的偏移量(由于这里没有定义虚函数,也就没有虚表指针,所以偏移量为0);
- 第2个是当前的vbptr 相对于虚基类(A)部分的偏移量。
假设继承体系如左图,则 D 的对象内存布局如右图。
代码验证:
class A{
public:
int d = -1;
int i = 1;
char c = 'a';
void f(){cout<<"A::f()"<<endl;}
};
class B: virtual public A{
public:
int i = 2; // 覆盖基类
char c = 'b'; // 覆盖基类
};
class C: virtual public A{
public:
char c = 'c'; // 覆盖基类
int i2 = 3;
void f(){cout<<"C::f()"<<endl;}
};
class D: public B, public C{
public:
int j = 4;
};
int main(){
D x;
cout<<&x<<" size:\t"<<sizeof(x)<<endl<<endl;
x.B::f();
cout<<"B::d "<<&x.B::d <<"\t"<<x.B::d<<endl
<<"B::i "<<&x.B::i <<"\t"<<x.B::i<<endl
<<"B::c "<<(void*)&x.B::c<<"\t"<<x.B::c<<endl<<endl; // ok
x.C::f();
cout<<"C::d "<<&x.C::d <<"\t"<<x.C::d<<endl
<<"C::i "<<&x.C::i <<"\t"<<x.C::i<<endl
<<"C::c "<<(void*)&x.C::c<<"\t"<<x.C::c<<endl
<<"C::i2 "<<&x.C::i2 <<"\t"<<x.C::i2<<endl<<endl;
x.D::f();
cout<<"D::d "<<&x.D::d <<"\t"<<x.D::d<<endl
<<"D::i "<<&x.D::i <<"\t"<<x.D::i<<endl
//<<"D::c "<<(void*)&x.D::c<<"\t"<<x.D::c<<endl // ambiguous
<<"D::i2 "<<&x.D::i2 <<"\t"<<x.D::i2<<endl
<<"D::j "<<&x.D::j <<"\t"<<x.D::j<<endl<<endl;
x.A::f();
cout<<"A::d "<<&x.A::d <<"\t"<<x.A::d<<endl
<<"A::i "<<&x.A::i <<"\t"<<x.A::i<<endl
<<"A::c "<<(void*)&x.A::c<<"\t"<<x.A::c<<endl<<endl;
x.f();
cout<<"x.d "<<&x.d <<"\t"<<x.d<<endl
<<"x.i "<<&x.i <<"\t"<<x.i<<endl
//<<"x.c "<<(void*)&x.c <<"\t"<<x.c<<endl // ambiguous
<<"x.i2 "<<&x.i2 <<"\t"<<x.i2<<endl
<<"x.j "<<&x.j <<"\t"<<x.j<<endl<<endl;
}
运行结果:
【结论】
- 虚基类成员的可见性:因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。假设 B 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
- 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 B 的成员,此时不存在二义性。
- 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
- 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。
- 使用 x. 和 x.D:: 访问是等价的。
虚继承(有虚函数)的内存分布
关于带虚函数的类的内存分布,详见:图解C++虚函数表和类对象的内存分布
这里分析既有虚函数又有虚继承的类的内存分布。
假设继承体系如左图(所有函数均为虚函数),则Derive对象的内存布局如右图。
【结论】
- 每个基类(直接基类B1,B2、虚基类A)都有一个虚函数表指针(vfptr),而直接基类(B1,B2)又都有一个虚基类表指针(vbptr),虚函数表指针在虚基类表指针的前面。
- 派生类Derive新定义的虚函数保存在第一个虚函数表中。
- 每个类的虚函数表都只保存自己新定义的虚函数,继承或重写的虚函数保存在最早定义这个虚函数的类的虚函数表中。
- 由上一条可知,每个虚函数只出现在一个虚函数表中。
- 如果虚函数表中的某个函数被子类重写,则表中保存子类版本。
代码验证:
class A {
public:
virtual void f() { cout << "A::f" << endl; }
virtual void i() { cout << "A::i" << endl; }
virtual void j() { cout << "A::j" << endl; }
int a;
};
class B1 : virtual public A {
public:
virtual void f() { cout << "B1::f" << endl; }
virtual void h1() { cout << "B1::h1" << endl; }
int b1;
};
class B2 : virtual public A {
public:
virtual void i() { cout << "B2::i" << endl; }
virtual void h2() { cout << "B2::h2" << endl; }
int b2;
};
class Derive : public B1, public B2 {
public:
virtual void f() { cout << "Derive::f" << endl; }
virtual void h2() { cout << "Derive::h2" << endl; }
virtual void g() { cout << "Derive::g" << endl; }
int c;
};
【注】不同IDE下的内存分布可能不同。比如在CodeBlocks下Derive对象只有28字节,少了B1和B2的vbptr,可能把虚基类表放到了vfptr所指的虚函数表后面(即vfptr和vbptr合并成一个指针,虚基类表和虚函数表合并成一张表)。
菱形继承的构造/析构顺序
因为菱形继承的 D 是多继承,所以要遵循多继承的构造/析构顺序,即按继承顺序调用相应基类(B、C)的构造函数,而初始化B、C之前又要分别调用它们的基类(A)的构造函数,所以构造和析构顺序如下:
A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()
代码验证:
class A{
public:
A(){cout<<"A()"<<endl;}
~A(){cout<<"~A()"<<endl;}
};
class B: public A{
public:
B(){cout<<"B()"<<endl;}
~B(){cout<<"~B()"<<endl;}
};
class C: public A{
public:
C(){cout<<"C()"<<endl;}
~C(){cout<<"~C()"<<endl;}
};
class D: public B, public C{
public:
D(){cout<<"D()"<<endl;}
~D(){cout<<"~D()"<<endl;}
};
int main(){
D x;
cout<<"sizeof(D): "<<sizeof(D)<<endl;
}
运行结果如下。注意到D的对象大小为2字节,这是因为本例中的所有类都是空类,C++规定空类必须占1字节,所以这2个字节分别来自B、C。
虚继承的构造/析构顺序
先初始化虚基类(A),再按继承顺序初始化直接基类(B、C)。若有多个虚基类,按声明的顺序依次初始化。析构顺序正好相反。
因此本例的构造/析构顺序为:
A()
B()
C()
D()
~D()
~C()
~B()
~A()
代码验证:
class A{
public:
A(){cout<<"A()"<<endl;}
~A(){cout<<"~A()"<<endl;}
};
class B: virtual public A{
public:
B(){cout<<"B()"<<endl;}
~B(){cout<<"~B()"<<endl;}
};
class C: virtual public A{
public:
C(){cout<<"C()"<<endl;}
~C(){cout<<"~C()"<<endl;}
};
class D: public B, public C{
public:
D(){cout<<"D()"<<endl;}
~D(){cout<<"~D()"<<endl;}
};
int main(){
D x;
cout<<"sizeof(D): "<<sizeof(D)<<endl;
}
运行结果如下。注意到D的对象大小为8字节,这是因为它的内存中有2个指针(4字节),分别对应直接基类B和C。
【例2】一个更复杂的继承体系:
F的构造和析构顺序:
C()
E()
A()
B()
D()
F()
~F()
~D()
~B()
~A()
~E()
~C()