多继承派生类有多个基类或虚基类。
派生类继承所有基类的数据成员和成员函数。
派生类在继承多个基类时,不同的基类可以采用不同的派生控制。
基类之间的成员可能同名,基类与派生类的成员也可能同名。在出现同名时,如面向对象的作用域不能解析,应该使用作用域运算符来指明所要访问的类的成员。
单继承是多继承的一种特例,多继承派生类具有更强的类型表达能力。
多继承机制是C++语言所特有的(Java、C#、SmallTalk等没有)。其他面向对象语言需要描述多继承类的对象时,常常通过对象成员或委托代理实现多继承。
委托代理在多数情况下能够满足需要,但当对象成员和基类类型相同或存在共同的基类时,就可能对同一个物理对象重复进行初始化(可能是危险的和不需要的)。
通常需要重新设计类,使派生类不包含重复的基类或对象成员。
多继承派生类的定义:
class 派生类名 :<派生方式> 基类1, <派生方式> 基类2, … {
<类体>
};
存在派生类对象多次初始化同一(物理)基类对象问题。
class Window {
//...
public:
Window(int top, int left, int bottom, int right);
~Window();
};
class Hscrollbar {
//...
public:
HScrollbar(int top, int left, int bottom, int right);
~HScrollbar();
};
class Vscrollbar {
//...
public:
VScrollbar(int top, int left, int bottom, int right);
~VScrollbar();
};
class ScrollableWind : public Window {
HScrollbar hScrollBar; //委托hScrollBar代理水平滚动
VScrollbar vScrollBar; //委托vScrollBar代理垂直滚动
//...
public:
ScrollableWind(int top, int left, int bottom, int right);
~ScrollableWind();
};
ScrollableWind::ScrollableWind(int t, int l, int b, int r) :Window(t, l, b, r), hScrollbar(t, r + 1, b - 1, r), vScrollbar(b - 1, l - 1, b, r + 1) {
//...
};
//Window、hScrollBar和vScrollBar分别初始化显示端口,则派生类ScrollableWind的对象就会多次初始化显示端口,从而导致显示屏出现多次闪烁。
//用多继承方式定义派生类 ScrollableWind:
class ScrollableWind : public Window, public HScrollbar, public Vscrollbar {
//...
public:
ScrollableWind(int top, int left, int bottom, int right);
~ScrollableWind();
};
ScrollableWind::ScrollableWind(int t, int l, int b, int r) : Window(t, l, b, r), HScrollbar(t, r + 1, b - 1, r), VScrollbar(b - 1, l - 1, b, r + 1) {
//...
}
派生类对象多次初始化同一基类成员问题(多次闪烁):
假设类Window、HScrollbar、VScrollbar 都是从基类 Port 派生,即:
class Port { … };
class Window : public Port { … }; class HScrollbar : public Port { … };
class VScrollbar : public Port { … };
class ScrollableWind : public Window, public HScrollbar, public VScrollbar { … };
创建ScrollableWind对象时,Port的构造函数通过3条不同的路径,被调用了3次,从而将显示端口初始化3次。即1个子类有3个同名祖先类,而物理显示端口只有一个!
如何实现:创建ScrollableWind 对象时,显示端口Port仅被初始化1次?用virtual定义虚基类。
class Window : virtual public Port { … };
class HScrollbar : public virtual Port { … };
class VScrollbar : public virtual Port { … };
class ScrollableWind : public Window, public HScrollbar, public VScrollbar { … };
同一个类不能多次作为某个派生类的直接基类,但可多次作为其间接基类,从而引起存储空间的浪费和其他问题。此时,这些间接基类可定义为虚基类。
同一颗派生树中的同名虚基类,共享同一个存储空间;其构造函数和析构函数仅执行1次,且构造函数尽可能最早执行,而析构函数尽可能最晚执行。
如果虚基类与基类同名,则它们将分别拥有各自的存储空间,只有同名虚基类才共享存储空间,而同名基类则拥有各自的存储空间。
虚基类和基类同名必然会导致二义性访问,编译程序会对这种二义性访问提出警告。当出现这种情况时,建议:要么将基类说明为对象成员,要么将基类都说明为虚基类。可用作用域运算符限定要访问的成员。
#include
using namespace std;
struct A {
int a;
A(int x) { a = x; }
};
struct B : A { //等于struct B:public A
B(int x) :A(x) { }
};
struct C {
C() { }
};
struct D : virtual A, C {
D(int x) :A(x) { } //同样调用C( )
};
struct E : B, D {
E(int x) :A(x), B(x + 5), D(x + 10) { }
};
void main(void) {
E e(0);
// cout << "a=" << e.a; //二义性访问
cout << "a=" << e.B::a;
cout << "a=" << e.D::a;
}
为解决 e.a 产生的二义性,要么将E的基类B说明为对象成员,要么将B的基类A说明为虚基类。若将B的基类A说明为虚基类,则 e.a、e.B::a及 e.D::a 都表示虚基类A的成员 a 。
当派生类有多个基类或虚基类时,基类或虚基类的成员之间可能出现同名;
派生类和基类或虚基类的成员之间也可能出现同名。
出现上述同名问题时,必须通过面向对象的作用域解析,或者用作用域运算符::指定要访问的成员,否则就会引起二义性问题。
在考虑多继承派生类构造函数的执行顺序时,必须注意派生类可能有虚基类、基类、对象成员、const成员以及引用成员。当虚基类、基类和对象成员只有带参数的构造函数时,派生类必须定义自己的构造函数,而不能利用C++提供的缺省构造函数。类有非静态对象成员、const 成员时,也必须定义构造函数。
对于虚基类、基类和对象成员来说,如果它们没有定义自己的构造函数,则编译程序就会为它们提供缺省的无参构造函数。对于虚基类、基类和对象成员的无参构造函数,无论它们是自定义的还是由编译程序提供的,可被派生类构造函数按定义顺序自动地调用。
派生类对象的构造顺序描述:
#include
using namespace std;
struct A {
A() {
cout << 'A';
}
};
struct B {
B() {
cout << 'B';
}
};
struct C {
int a; int& b;
const int c;
C(char d) : c(d), b(a) { a = d; cout << d; }
};
struct D {
D() { cout << 'D'; }
};
struct E : A, virtual B, C, virtual D {
A x, y;
B z;
E() : z(), y(), C('C')
{
cout << 'E';
}
};
void main(void)
{
E e;
}
输出: BDACAABE
#include
using namespace std;
struct A {
A() {
cout << 'A';
}
};
struct B {
const A a;
B() {
cout << 'B';
}
};
struct C {
C() {
cout << 'C';
}
};
struct D {
D() {
cout << 'D';
}
};
struct E : A {
E() {
cout << 'E';
}
};
struct F : B, virtual C {
F() {
cout << 'F';
}
};
struct G : B {
G() {
cout << 'G';
}
};
struct H : virtual C, virtual D {
H() {
cout << 'H';
}
};
struct I : E, F, virtual G, H {
E e;
F f;
I() : f(), e(), F(), E() {
cout << 'I';
}
};
int main(void) {
I i;
return 0;
}
输出:CABGDAEABFHAECABFI
关于包含虚基类的多继承说明(vs2019):
在一个派生树中,同名的虚基类和非虚基类不能同时存在。
struct A { };
struct B : A {};
struct C : A {};
struct D : B {};
struct E : A, C {};
struct F : virtual A, C { }; //错:虚基类A和非虚基类A同时存在
struct G : virtual B, D { }; //错:虚基类B和非虚基类B同时存在
struct H : virtual B, C { }; //对
struct I : virtual B, E { }; //对
struct J : virtual C, E { }; //错:虚基类C和非虚基类C同时存在
派生类无虚基类的情况下:
class A {
int a;
public:
virtual void f1() {};
};
class B {
int b, c;
public:
virtual void f2() {};
};
class C {
int d;
public:
void f3() {};
};
class D : A, B, C {
int e;
public:
virtual void f4() {};
};
D的第1个基类A已建立了VFT 首址。
D共用该表首址所占用的存储单元。
- sizeof(D) = sizeof(A) + sizeof(B) + sizeof(C) + sizeof(e)
- sizeof(A) = sizeof(void *) + sizeof(a)
- sizeof(B) = sizeof(void *) + sizeof(b) + sizeof(c)
- sizeof(C) = sizeof(d)
派生类有虚基类的情况下,虚基类的存储空间建于派生类的尾部,且按虚基类的构造顺序建立:
派生类依次处理每个直接基类或虚基类,如果为直接基类,则为其建立存储空间,如果为直接虚基类则建立一个到虚基类的偏移。
如果派生类继承的第一个类为非虚基类,且该基类定义了虚函数地址表,则派生类就共享该表首址占用的存储单元。对于其他任何情形,派生类在处理完所有基类或虚基类后,根据派生类是否新定义了虚函数,确定是否为该表首址分配存储单元。
派生类依次处理自定义的数据成员,为每个数据成员建立相应的存储空间。
派生类根据虚基类偏移的建立顺序,依次为虚基类建立存储空间,同名虚基类仅在派生类存储空间内建立一次。
如果直接基类和虚基类又是派生类,则在派生类的存储空间内重复步骤1至5。如果数据成员又为派生类类型,则在数据成员的存储空间内重复步骤1至5。
#include
using namespace std;
struct A {
virtual void fa() {};
};
struct B {
int b;
void fb();
};
struct E : virtual A {
int x;
virtual void fe() { };
};
struct F : virtual A, virtual B {
int x;
void ff() { };
};
struct G : B, virtual A {
int x;
virtual void fg() { };
};