虚基类
上节介绍多重继承产生的二义性时讨论了,若通过不同路径继承了公共基类,则在派生类中会存在公共基类中可访问数据成员的多份拷贝,这种二义性问题需要虚基类来解决。
在多重继承中,若一个类声明为虚基类,则能保证一个派生类间接地多次继承该类时,派生类中只继承该基类的一份成员,避免了派生类中访问公共基类公有属性多份拷贝的二义性。
虚基类的定义形式是在派生类定义时基类名称前加virtual关键字,具体形式如下所示:
class 派生类名:virtual 继承方式 基类名
{
派生类成员
};
使用虚基类时,派生类构造函数的调用过程有点特别。对于普通基类来说,派生类的构造函数只负责调用其直接基类的构造函数,若直接基类还有它的更上层的基类,则依次调用各层基类的构造函数,对于虚基类的派生类来说,其构造函数不仅调用其直接基类的构造函数,还需要调用虚基类的构造函数。
接下来将Animal声明为虚基类,观察程序的变化,如例1所示。
例1
1 #include <iostream>
2 using namespace std;
3
4 class Animal //定义类Animal
5 {
6 public:
7 //定义类Animal的构造函数
8 Animal(int age):m_nAge(age) { cout << "Animal constructor!" << endl; }
9 protected:
10 int m_nAge; //成员m_nAge记录动物年龄
11 };
12
13 class Bird:virtual public Animal //定义类Bird继承自虚基类Animal
14 {
15 public:
16 //定义类Bird的构造函数
17 Bird(int age, int fh):Animal(age)
18 {
19 cout << "Bird constructor!" << endl;
20 m_nFlightAltitude = fh;
21 }
22 //定义获取鸟飞行高度的函数
23 int get_flightaltitude() { return m_nFlightAltitude; }
24 private:
25 int m_nFlightAltitude;
26 };
27
28 class Fish:virtual public Animal //定义类Fish继承自虚基类Animal
29 {
30 public:
31 Fish(int age, int speed):Animal(age) //定义类Fish的构造函数
32 {
33 cout << "Fish constructor!" << endl;
34 m_nSwimSpeed = speed;
35 }
36 //定义获取鱼游速的函数
37 int get_swimspeed() { return m_nSwimSpeed; }
38 private:
39 int m_nSwimSpeed;;
40 };
41
42 class WaterBird:public Bird, public Fish //定义水鸟类
43 {
1 public:
2 WaterBird(int b_age, int f_age, int fh, int speed):Bird(b_age, fh),
3 Fish(f_age, speed), Animal(b_age) //定义水鸟类带参数的构造函数
4 {
5 cout << "WaterBird constructor!" << endl;
6 }
7 //定义打印动物年龄的成员函数
8 void print_animalage() { cout << "age = " << m_nAge << endl; }
9 };
10
11 int main()
12 {
13 WaterBird waterbird(5, 6, 20, 30); //定义水鸟对象
14 cout << "waterbird flight altitude: " << waterbird.get_flightaltitude()
15 << ", swimming speed:" << waterbird.get_swimspeed() << endl;
16 waterbird.print_animalage();
17 system("pause");
18 return 0;
19 }
程序运行结果如图1所示。
图1 例1运行结果
例1中定义派生类Bird、Fish时将基类Animal声明为虚基类,这样通过多重继承定义派生类WaterBird时只保留了Animal中m_nAge成员的一份拷贝,数值为5。本例中类的层次结构及各类中成员描述如图2所示。
图2 各派生类中成员描述
值得一提的是,在例1中通过WaterBird类对象waterbird调用print_animalage()函数打印m_nAage值时,不产生二义性,为了得到同样的结果,也可将print_animalage()函数内容修改为如下代码:
cout << "age = " << Bird::m_nAge << endl;
或
cout << "age = " << Fish::m_nAge << endl;
由于在虚基类Animal中定义了带参数的构造函数,因此在其直接派生类Bird、Fish和间接派生类WaterBird的构造函数初始化表中,都要显式调用虚基类Animal的构造函数,如代码第17、31、46行。
观察例1的运行结果发现定义派生类WaterBird的对象waterbird时,虚基类Animal的构造函数只被调用一次。在介绍多重继承派生类构造函数时曾提到,派生类的构造函数会依次调用各级基类的构造函数,若按这样解释,WaterBird类继承自类Bird和类Fish,这两个类又继承自Animal,则创建派生类对象waterbird时,类Animal的构造函数会被多次调用,但事实并非如此。
对于虚基类的构造函数,C++编译器的调用方法是:由最后定义的派生类,即类的层次结构中最低层的派生类在定义对象时完成虚基类构造函数的调用,该派生类的其他基类对虚基类的构造函数的调用被忽略。
例1中,WaterBird是最低层的派生类,由它完成对虚基类Animal构造函数的调用,WaterBird的直接基类Bird、Fish对虚基类Animal构造函数的调用被忽略。
通过对虚基类的继承派生出新类,定义新类对象时,对象中数据成员的排列形式与普通非虚基类派生出的新类数据成员的排列形式不同,派生类对象中将增加一个隐藏的“虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类成员位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量。
将例3-16进行修改,将各类中的数据成员均声明为具有public访问属性,并在WaterBird类中添加公有成员int m_nWeight,打印所有数据成员的地址,观察其排列情况,如例2所示。
例2
1 #include <iostream>
2 using namespace std;
3
4 class Animal //定义类Animal
5 {
6 public:
7 //定义类Animal的构造函数
8 Animal(int age):m_nAge(age) { }
9 int m_nAge; //成员m_nAge记录动物年龄
10 };
11 class Bird:virtual public Animal //定义类Bird继承自虚基类Animal
12 {
13 public:
1 //定义类Bird的构造函数
2 Bird(int age, int fh):Animal(age), m_nFlightAltitude(fh){ }
3 int m_nFlightAltitude;
4 };
5 class Fish:virtual public Animal //定义类Fish继承自虚基类Animal
6 {
7 public:
8 //定义类Fish的构造函数
9 Fish(int age, int speed):Animal(age),m_nSwimSpeed(speed){ }
10
11 int m_nSwimSpeed;;
12 };
13 class WaterBird:public Bird, public Fish //定义水鸟类
14 {
15 public:
16 //定义水鸟类带参数的构造函数
17 WaterBird(int age, int fh, int speed, int weight):Bird(age, fh),
18 Fish(age, speed), Animal(age), m_nWeight(weight)
19 {
20 }
21 int m_nWeight;
22 };
23
24 int main()
25 {
26 WaterBird waterbird(5, 20, 30, 10); //定义水鸟对象
27
28 cout << "sizeof(waterbird) = " << sizeof(waterbird) << endl;
29 cout << "waterbird addr:" << &waterbird << endl;
30 cout << "Bird::m_nFlightAltitude addr:"
31 << &(waterbird.m_nFlightAltitude) << endl;
32 cout << "Fish::m_nSwimSpeed addr:" << &(waterbird.m_nSwimSpeed) << endl;
33 cout << "WaterBird::m_nWeight addr:" << &(waterbird.m_nWeight) << endl;
34 cout << "Animal::m_nAge addr:" << &(waterbird.m_nAge) << endl;
35
36 system("pause");
37 return 0;
38 }
程序运行结果如图3所示。
图3 例2运行结果
从例2的运行结果看出,派生类对象占用的字节数为24,数据成员的地址分布情况如图4所示。
图4 继承自虚基类的派生类中数据成员排列图
从图4可以看出,具有虚基类的继承结构中,对于最底层的派生类对象而言,其普通基类的数据成员按照定义时的顺序依次排列,接着是派生类自己的成员,最后是虚基类的成员,并且若通过虚基类派生出新类,新类对象中会出现“虚基类指针”成员,如Bird::vbptr和Fish::vbptr,“虚基类指针”排列在该类所有成员的最前部,指向一个偏移量表,比如Bird::vbptr指针指向的表中第2项描述了虚基类的成员m_nAge与虚基类指针之间的偏移量为20字节,,则从上图可以看出,虚基类Animal中的数据成员m_nAge的地址为0X003FFC88,它与Brid::vbptr指针所在单元地址0X003FF074刚好相差20,因此通过该指针能够访问到虚基类成员。