僻的但是又是绝对必要的组成部份而存在着,并且其行为和模型均表现出和一般的继承体系之间的巨大的差异(包括访问性能上的差异),现在我们就来彻底的从语言、模型、性能和应用等多个方面对虚继承和虚基类进行研究。
首先还是先给出虚继承和虚基类的定义。
虚继承:在继承定义中包含了virtual关键字的继承关系;
虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:struct CSubClass : public virtual CBase {}; 其中CBase称之为CSubClass 的虚基类,而不是说CBase就是个虚基类,因为CBase还可以不不是虚继承体系中的基类。
当在多条继承路径上有一个公共的基类,在这些路径的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。
class x1:virtual public x
{
//... ...
};
class x2:virtual public x
{
//... ...
};
虚基类的初始化
虚基类(虚拟继承)的初始化与一般多继承的初始化在语法上是一样的,但构造函数的调用次序不同。
派生类的构造函数的调用次序有三个原则:
(1) 虚基类的构造函数在非虚基类之前调用;
(2) 若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
(3) 若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数。
C++的虚基类
在派生类继承基类时,加上一个virtual关键词则为虚拟基类继承,如:
class derive:virtual public base
{
};
虚基类主要解决在多重继承时,基类可能被多次继承,虚基类主要提供一个基类给派生类,如:
classB
{
};
class D1:public B
{
};
class D2:public B
{
};
class C:public D1, public D2
{
};
这里C在D1,D2上继承,但有两个基类,造成胡乱。因而使用虚基类,即:
classB
{
};
class D1:virtual public B
{
};
class D2:virtual public B
{
};
class C:public D1, public D2
{
};
在使用虚基类时要注意:
(1) 一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
(2) 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的子对象。
(3) 虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。
(4) 最派生类是指在继承结构中建立对象时所指定的类。
(5) 派生类的构造函数的成员初始化列表必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。
(6) 从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类的子对象只初始化一次。
(7) 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
虚基类的构造函数
为了初始化虚基类的子对象,派生类的构造函数要调用基类的构造函数。由于派生类的对象中只有一个虚基类子对象,那么就必须要保证虚基类的子对象只被初始化一次。也就是说,虚基类的构造函数只能被调用一次。
由于继承的层次可能会很深,C++规定,真正创建对象时的类称为是最派生类,虚基类子对象是由最派生类的构造函数,在其成员初始化列表中,通过直接调用虚基类的构造函数进行初始化的。如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的显式调用(除非虚基类有默认构造函数)。如果没有列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类子对象。
从虚基类直接或间接继承的派生类的成员初始化列表中必须列出对该虚基类构造函数的调用。但是只有真正用于创建对象的那个最派生类的构造函数才会真正调用虚基类的构造函数。而最派生类到虚基类的中间类初始化时,它们所列出的对虚基类的构造函数的调用在实际执行中被忽略。这样就保证对虚基类的子对象只初始化一次。
C++又规定:在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,则虚基类的构造函数先于非虚基类的构造函数被执行。
在需要使用虚基类的场合,由于每一个继承类的初始化列表中,必须包含初始化虚基类的语句。而这些初始化语句仅仅只在最底层子类(最派生类)中才实际调用,这样就会使得某些上层子类得到的虚基类子对象的状态不是自己所期望的,因为自己的初始化语句被跳过了。所以,一般建议不要在虚基类中包含任何数据成员,即不要有状态,只可以作为接口类来使用。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
#include <iostream>
using
namespace
std;
class
furniture
{
public
:
furniture(
int
nWeight )
{
m_nWeight = nWeight;
cout <<
"家具的构造"
<< endl;
}
int
m_nWeight;
};
//虚继承
//使派生类对象只有一份基类(furniture)的拷贝
//在菱形继承下,在下面的两个继承关系中,加上虚继承
class
Safa :
virtual
public
furniture
{
public
:
Safa(
int
nWeight ):furniture(nWeight)
{
cout <<
"沙发的构造"
<< endl;
}
void
sit()
{
}
};
class
Bed :
virtual
public
furniture
{
public
:
Bed(
int
nWeight ):furniture(nWeight)
{
cout <<
"床的构造"
<< endl;
}
void
sleep()
{
}
};
//构造顺序,先是虚基类
//再是非虚基类,按声明的顺序构造
class
SafaBed :
public
Bed,
public
Safa
{
public
:
//注意Safa和Bed的构造顺序,按照声明顺序,不是初始化列表顺序
SafaBed(
int
nWeight ): Safa(nWeight), Bed(nWeight), furniture(nWeight)
{
Bed::m_nWeight = nWeight;
cout <<
"沙发床的构造"
<< endl;
}
};
int
main(
int
argc,
char
* argv[])
{
SafaBed theSafaBed(100);
furniture* pFurniture = (Bed*)&theSafaBed;
theSafaBed.sit();
theSafaBed.sleep();
cout <<
sizeof
(theSafaBed) << endl;
getchar
();
return
0;
}
|
运行结果:
注意SafaBed的成员初始化列表中,需要显式初始化虚基类furniture。
如果把Bed和Safa类定义的时候的2个virtual去掉,那么也必须修改SafaBed的成员初始化列表,需要删除对furniture的初始化,否则编译器提示:对“furniture”的访问不明确。
运行结果为: