一、多继承
1、概念
当一个派生类具有多个基类时,这种派生方法称为多基派生或多继承。
class z:private x, public y {
//…
};
2、二义性
class X { //思考题:
public:
int f();
};
class Y {
public:
int f();
int g();
};
class Z∶public X, public Y {
public:
int g();
int h();
};
main()
{
Z obj;
obj.f(); // 不知调用的是 X、Y 里哪一个f()
}
使用成员名限定可以消除二义性,例如:
obj.X∷f(); //调用类X的f()
obj.Y∷f(); //调用类Y的f()
3、构造/析构顺序
派生类名(参数总表) : 基类名1(参数表1), 基类名2(参数表2), …, 基类名n(参数表)
{
派生类新增成员的初始化语句
}
多继承构造函数的执行顺序与单继承构造函数的执行顺序相同:
(1)先执行基类的构造函数(按继承从左到右顺序);
(2)再执行对象成员的构造函数;
(3)最后执行派生类构造函数。
析构函数的执行顺序则刚好与构造函数的执行顺序相反。
class Hard {
protected:
int a;
public:
Hard(int a) { this->a = a; } //基类Hard构造函数
};
class Soft {
protected:
int b;
public:
Soft(int b) { this->b = b; } //基类Soft的构造函数
};
class System : public Hard, public Soft { //派生类,多继承
public:
int c;
string str;
System(int a, int b, int c, string s) : Hard(a), Soft(b), str(s)
{
this->c = c;
}
};
(1)Hard中a初始化;(2)Soft中b初始化;(3)然后str初始化,在初始化表中找到str项,于是str初始化成功;(4)this->c相当于是给c第一次赋值,是后初始化。
二、虚基类 (虚继承)
1、共同祖先二义性
如果一个派生类是从多个基类派生出来的,而这些基类又有一个共同的基类,则存在来自同一个基类的多份数据的拷贝,则在这个派生类中访问这个共同的基类中的成员时,可能会产生二义性。
class B {
protected: int a;
public:
B(){ a=5; cout<<"B a="<<a<<endl;}
};
class B1:public B {
public:
B1(){ a=a+10; cout<<"B1 a="<<a<<endl;}
};
class B2:public B {
public:
B2(){ a=a+20; cout<<"B2 a="<<a<<endl;}
};
class D:public B1,public B2 {
public:
D(){ cout << "D a=" << a << endl; } 不知道调的是哪一个a,来自B1还是B2
};
int main() {
D obj;
return 0;
}
应该使用作用域修饰符
class D : public B1, public B2 {
public:
D() {
cout << “B1 a=" << B1::a << endl; 来自B1
cout << “B2 a=" << B2::a << endl; 来自B2
}
};
运行结果为
B a=5
B1 a=15
B a=5
B2 a=25
B1 a=15
B2 a=25
2、虚基类
为了解决这种二义性,使从不同的路径继承的基类的成员在内存中只拥有一个拷贝, C++引入了虚基类的概念。
如果将公共基类说明为虚基类。那么,对同一个虚基类的构造函数只调用一次,这样从不同的路径继承的虚基类的成员在内存中就只拥有一个拷贝。从而解决了以上的二义性问题。
3、构造顺序
对同一个虚基类的构造函数只调用一次,且是在第一次出现时调用。
class B {
protected:
int a;
public:
B(){ a=5; cout<<"B a="<<a<<endl;}
};
class B1 : public virtual B { 虚继承
public:
B1(){ a=a+10; cout<<"B1 a="<<a<<endl;}
};
class B2 : virtual public B { 虚继承
public:
B2(){ a=a+20; cout<<"B2 a="<<a<<endl;}
};
class D : public B1, public B2 { 继承
public:
D(){ cout<<"D a="<< a<<endl;}
};
main()
{
D obj;
return 0;
}
运行结果为:
base a=5
base1 a=15
base2 a=35
derived a=35
若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数;
对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下;
对于非虚基类,构造函数的执行顺序仍是先左后右,自上而下。
4、底层实现原理 —— 虚基类指针、虚基类表
虚继承用于解决多继承条件下的公共祖先问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。