以下部分内容摘抄自《C++primer 中文版 4》
多重继承(multiple inheritance)是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性。简而言之,就是采用多重继承的派生类有至少两个父类。
多重继承的方式:
class Base1{
public:
Base1(int x) {cout << x << endl; }
};
class Base2{
public:
Base2(int x) {cout << x << endl; }
};
class Derived : public Base1, public Base2 {
};
至于C++中该不该使用多重继承,也一直是个备受争议的问题,《Effective C++》作者也没有正面上给出选择,而是说要“
明智而审慎地使用多重继承”(条款40)。
下面我们来看看多重继承中需要注意的问题,然后引入虚继承。
在多重继承下,派生类的对象会包含每个基类的基类子对象,派生类的构造函数可以在初始化列表中给出零个或者多个基类传递值,例如:
//a)
Derived(int i, int j, int k) : Base2(i), Base1(j)
{
cout << k << endl;
}
//b)
Derived(int k) : Base2(0)
{
cout << k << endl;
}
这里需要注意的是,
构造函数调用的次序不受构造函数初始化列表中出现的基类的影响(与基类继承列表有关)。
在上面a例的Derived类的构造函数的初始化列表中,虽然Base2写在Base1之前,但实际构造的次序是先调用Base1的构造函数(即使在b例子中,初始化列表中没有写Base1,也会隐式调用Base1的默认构造函数),再调用Base2的构造函数。另外,如果Base1还是由另外的Base派生而来,则在构造Base1之前需要先构造Base,这与单继承中一致。析构函数的次序与构造函数相反,不再赘述。
拷贝构造函数和赋值的调用顺序与构造函数是一致的,但是
如果派生类定义了自己的拷贝构造函数或者赋值操作符的话,它需要自己负责拷贝或者赋值所有的基类子部分,否则会编译错误。(与构造函数不同,构造函数中可以只调用部分基类的构造函数,其他的会自动调用默认的)
总之,在多重继承下,派生类的对象会包含每个基类的基类子对象,包括构造和初始化所有基类子对象,派生类的构造函数可以在初始化列表中给出零个或者多个基类传递值。多重继承中若没有显式调用某个基类的构造函数,则编译器会调用该基类的默认构造函数。而基类构造函数的调用次序只与基类继承列表有关。
class Base{
public:
Base(int x)
{
cout << x;
}
Base(const Base& c)
{
cout << "Base" << " ";
}
};
class Base1 : public Base {
public:
Base1(int i, int j = 0) : Base(j)
{
cout << i;
}
Base1(const Base1& c) : Base(c)
{
cout << "Base1" << " ";
}
};
class Base2: public Base {
public:
Base2(int i, int j = 0) : Base(j)
{
cout << i;
}
Base2(const Base2& c) : Base(c)
{
cout << "Base2" << " ";
}
};
class Derived : public Base1, public Base2 {
public:
Derived(int a, int b, int c, int d) : mem1(a), mem2(b), Base1(c), Base2(d)
{ // 先按照继承列表先构造Base1,然后构造Base2,然后是Derived类自身的两个成员,按照声明的顺序,先构造mem2,再构造mem1,最后输出b
cout << b;
}
Derived(const Derived& c) : Base1(c), Base2(c), mem1(c.mem1), mem2(c.mem2)
{ // 拷贝构造函数的顺序与构造函数类似,先Base1,再Base2,然后是mem2和mem1,最后是Derived自身(在调用Base1和Base2的时候会先调用Base,这与单继承一致)
// 注意:这里一定要把mem1和mem2的构造放在上面的列表中,否则会编译错误,如果定义了赋值运算符的话,可以在函数内通过赋值实现拷贝
cout << "Derived" << " ";
}
private:
Base2 mem2;
Base1 mem1;
};
int main()
{
Derived t(1, 2, 3, 4); // 030402012
cout << endl;
Derived s(t); // Base Base1 Base Base2 Base Base2 Base Base1 Derived
return 0;
}
上面的构造函数和拷贝构造函数都可以看出,对于上面这种菱形继承的关系,Base类保存了多份,这样会出现什么问题呢?
1. 如果Base类有一片缓冲区可供Base1读取,或者Base2写的话,你想通过定义Derived类来实现读写的共享,则这样写是达不到目的的,因为Derived会保存两部分相对独立的Base缓冲区。
2.
多重继承的一个需要非常谨慎的地方,如果Base或者Base1或者Base2中定义了相同的函数,那么采用Derived对象去调用的时候会出现二义性,一个解决二义性的办法就是调用函数的时候指定作用域(即我们想调用哪个类的这个函数。)
关于二义性,两个继承类的函数相同的二义性很明显,但是,下面几点依旧需要注意:
(1)即使两个继承的函数有不同的形参表,也会产生错误;
(2)即使函数在一个类中是私有的,而在另一个类中是共有或受保护的,也是错误的;
(3)如果Base中定义了一个函数,只有Base1继承了,而Base2没有继承,也是错误的;
另一个解决二义性的办法就是
虚继承(virtual inheritance),
虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。
我们先把上面的例子中Base1和Base2的继承方式稍微修改一下,改成虚继承,
那么,上面的例子会输出什么结果呢?
class Base1 : public virtual Base {
class Base2: public virtual Base {
class Derived : public Base1, public Base2 {
public:
Derived(int a, int b, int c, int d) : mem1(a), mem2(b), Base1(c), Base2(d), Base(a)
{ // 最先构造虚基类,然后按照继承列表先构造Base1,Base2,然后是Derived类自身的两个成员,按照声明的顺序,先构造mem2,再构造mem1,最后输出b
// output : 13402012
cout << b;
}
Derived(const Derived& c) : Base(c), Base1(c), Base2(c), mem1(c.mem1), mem2(c.mem2)
{ // 拷贝构造函数的顺序与构造函数类似,先是虚基类Base,然后Base1,再Base2,然后是mem2和mem1,最后是Derived自身
// output : Base Base1 Base2 Base Base2 Base Base1 Derived
cout << "Derived" << " ";
}
private:
Base2 mem2;
Base1 mem1;
};
上面的例子我们可以看到,不定义虚继承出现二义性是在Derived类中,而我们需要定义的虚继承居然不是说Derived相对Base1和Base2,而是Base1和Base2相对于Base。即
必须在提出虚派生的任意实际需要之前进行虚派生。
虚继承可以很好地解决二义性的问题,但是使用时也得小心,因为它有
特殊的初始化语义。在构造虚继承的对象时,构造次序是:先构造虚基类,然后按照声明次序调用非虚基类的构造函数。以C++primer中的一个例子(下图)为例,其
构造TeddyBear时,调用顺序:
ZooAnimal(), ToyAnimal(), Character(),
BookChatacter(),
Bear(), T
eddyBear()