7.3 多继承
7.3.1 多继承的概念
前面讲述了单继承中派生类和基类之间的关系,这一节讨论多继承问题。多继承是指派生类具有多个基类,派生类与每个基类之间的关系仍可看做是一个单继承。多继承下派生类的定义格式如下:
class 派生类名 : 继承方式1 基类名1, 继承方式2 基类名2 {
派生类体
};
其中,继承方式1
、继承方式2
等是public
、private
和protected
之一。多继承中派生类与多个基类之间的关系如图7-2所示。
例如:
class A {
// 基类A的成员
};
class B {
// 基类B的成员
};
class C : public A, public B {
// 派生类C的成员
};
其中,派生类C具有两个基类(类A和类B),因此,C是多继承的派生类。按多继承的规定,派生类C的成员包含了基类A中的成员和基类B中的成员以及该类本身的成员。
7.3.2 多继承的构造函数和析构函数
在多继承的情况下,派生类的构造函数格式如下:
派生类名(总参数表) : 基类名1(参数表1), 基类名2(参数表2), ... {
派生类构造函数体
}
其中,总参数表中各个参数包含了其后的各个分参数表。多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的构造函数参数个数必须包含完成所有基类初始化所需的参数个数。
派生类构造函数的执行顺序是先执行所有基类的构造函数,再执行派生类本身的构造函数。处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的各基类顺序,与派生类构造函数中所定义的成员初始化列表的各项顺序无关。
多继承的派生类的析构函数中也隐含着所有基类的析构函数。在执行多继承派生类的析构函数时,其顺序与其构造函数的执行顺序相反。
[例7.8] 分析下列程序的输出结果
#include <iostream>
using namespace std;
class B1 {
public:
B1(int i) {
b1 = i;
cout << "constructor B1." << i << endl;
}
void print() {
cout << b1 << endl;
}
private:
int b1;
};
class B2 {
public:
B2(int i) {
b2 = i;
cout << "constructor B2." << i << endl;
}
void print() {
cout << b2 << endl;
}
private:
int b2;
};
class B3 {
public:
B3(int i) {
b3 = i;
cout << "constructor B3." << i << endl;
}
int getB3() {
return b3;
}
private:
int b3;
};
class A : public B2, public B1 {
public:
A(int i, int j, int k, int l) : B1(i), B2(j), bb(k) {
a = l;
cout << "constructor A." << l << endl;
}
void print() {
B1::print();
B2::print();
cout << a << "," << bb.getB3() << endl;
}
private:
int a;
B3 bb;
};
int main() {
A aa(1, 2, 3, 4);
aa.print();
return 0;
}
执行该程序输出如下结果:
constructor B2.2
constructor B1.1
constructor B3.3
constructor A.4
1
2
4,3
说明:
-
先分析派生类A构造函数的构成,类A的构造函数定义如下:
A(int i, int j, int k, int l) : B1(i), B2(j), bb(k) { a = l; cout << "constructor A." << l << endl; }
该构造函数的总参数表中有4个参数,它们分别是基类B1、基类B2和子对象bb的构造函数参数。该构造函数也可以写成如下形式:
A(int i, int j, int k, int l) : B1(i), B2(j), bb(k), a(l) { cout << "constructor A." << l << endl; }
-
分析派生类构造函数的执行顺序。在构造函数的成员初始化列表中,两个基类顺序是B1在前,B2在后。而定义派生类A时的两个基类顺序是B2在前,B1在后。输出结果中,可以看出:先执行B2的构造函数,后执行B1的构造函数。因此,执行基类构造函数的顺序取决于定义派生类时基类的顺序。可见,派生类构造函数的成员初始化列表中基类构造函数顺序可以任意地排列。
-
作用域运算符:在该程序中用于解决作用域冲突的问题。在派生类A中的print()函数的定义中,使用了
B1::print();
和B2::print();
语句分别指明调用哪个类中的print()函数。应学会这种用法。
请读者修改该程序进一步验证派生类A的析构函数的执行顺序。
7.3.3 多继承的二义性问题
一般来说,在派生类中对基类成员的访问应该是唯一的。但是,由于在多继承情况下,可能出现对基类中某个成员的访问不唯一的情况,这称为多继承的二义性问题。
在多继承情况下,通常有两种情况可能出现二义性。下面分别讨论在这两种情况下如何避免二义性。
1. 派生类的多个基类中调用其同名成员时可能出现二义性
在例7.8中已经出现过这一问题。回忆一下例7.8,派生类A的两个基类B1和B2中都有一个成员函数print()。如果在派生类A中访问print()函数,那么到底是哪个基类的呢?这时会出现二义性。但是在例7.8中解决了这个问题,其办法是通过作用域运算符::
进行了限定。如果不加以限定,则会出现二义性问题。
下面再举一个简单的例子,对二义性问题进行深入讨论。例如:
class A {
public:
void f() {}
};
class B {
public:
void f() {}
void g() {}
};
class C : public A, public B {
public:
void g() {}
void h() {
f(); // 这里会出现二义性
}
};
如果定义一个类C的对象:
C c1;
c1.f(); // 具有二义性:是访问类A中的f(),还是访问类B中的f()?
可以用前面已用过的成员名限定法来消除二义性。例如:
c1.A::f();
c1.B::f();
最好的解决办法是在类C中定义一个同名成员f()
,类C中的f()
再根据需要来决定调用A::f()
还是B::f()
,还是两者皆调用,这样,c1.f()
将调用C::f()
。
class C : public A, public B {
public:
void f() {
A::f();
B::f();
}
void g() {}
void h() {
A::f();
B::f();
}
};
另外,在前例的类B中有一个成员函数g()
,类C中也有一个成员函数g()
。这时,
c1.g(); // 不存在二义性。它是指C::g(),而不是指B::g()。
因为这两个g()
函数,一个出现在基类B,一个出现在派生类C,规定派生类的成员将覆盖基类中的同名成员。因此,上例中类C中的g()
覆盖类B中的g()
,不存在二义性。
在多继承中,为了清楚地表示各类之间的关系,常采用一种称为DAG(有向无环图)的图示表示法。上例中,类A、类B和类C如图7-3所示。
图7-3中表明类A和类B是类C的两个基类。
2. 派生类有共同基类时访问公共基类成员可能出现二义性
当一个派生类从多个基类派生,而这些基类又有一个公共的基类,在对该基类中说明的成员进行访问时,可能会出现二义性。例如:
class A {
public:
int a;
};
class B1 : public A {
private:
int b1;
};
class B2 : public A {
private:
int b2;
};
class C : public B1, public B2 {
public:
int f();
private:
int c;
};
使用DAG图示表示如图7-4所示。
已知:
C c1;
下面的两个访问都有二义性:
c1.a;
c1.A::a;
而下面的两个访问是正确的:
c1.B1::a;
c1.B2::a;
对类C的成员函数f()
作如下定义可以消除二义性:
int C::f() {
return B1::a + B2::a;
}
消除二义性的最好方法还是通过适当的类名限定,明确提出是哪个类的某个成员,这样就不会出现二义性了。
下面的程序是用来验证上述分析的。
[例7.9] 分析下列程序的输出结果
#include <iostream>
using namespace std;
class A {
public:
A(int i) : a(i) {
cout << "con.A " << a << endl;
}
void print() {
cout << a << endl;
}
~A() {
cout << "des.A\n";
}
private:
int a;
};
class B1 : public A {
public:
B1(int i, int j) : A(i), b1(j) {
cout << "con.B1 " << b1 << endl;
}
void print() {
A::print();
cout << b1 << endl;
}
~B1() {
cout << "des.B1\n";
}
private:
int b1;
};
class B2 : public A {
public:
B2(int i, int j) : A(i), b2(j) {
cout << "con.B2 " << b2 << endl;
}
void print() {
A::print();
cout << b2 << endl;
}
~B2() {
cout << "des.B2\n";
}
private:
int b2;
};
class C : public B1, public B2 {
public:
C(int i, int j, int k, int l, int m) : B1(i, j), B2(k, l), c(m) {
cout << "con.C " << c << endl;
}
void print() {
B1::print();
B2::print();
cout << c << endl;
}
~C() {
cout << "des.C\n";
}
private:
int c;
};
int main() {
C c1(1, 2, 3, 4, 5);
c1.print();
return 0;
}
执行该程序输出如下结果:
con.A 1
con.B1 2
con.A 3
con.B2 4
con.C 5
1
2
3
4
5
des.C
des.B2
des.A
des.B1
des.A
说明:
该程序消除了二义性。在程序中创建类C的对象时,类A的构造函数被调用两次,一次是类B1调用的,另一次是类B2调用的,以此来初始化类C对象中包含的两个类A的成员。
由于二义性的原因,一个类不可以从同一个类中直接继承多次。例如:
class A : public B, public B {};
这是错误的。