intmain()
{Ccl;//定义C类对象cl……}
在类A和类B中虽然没有定义数据成员a和成员函数display,但是它们分别从类N继承了数据成员a和成员函数display,这样在类A和类B中同时存在着两个同名的数据成员a和成员函数display。
它们是N类成员的拷贝。
类A和类B中的数据成员a代表两个不同的存储单元,可以分别存放不同的数据。
在程序中可以通过类A和类B的构造函数去调用基类N的构造函数,分别对类A和类B的数据成员a初始化。
图19和图20表示了派生类C中成员的情况。
图19 图20
怎样才能访问类A中从基类N继承下来的成员呢?
显然不能用
c1.a=3;c1.display();或c1.N:
:
a=3;c1.N:
:
display();
因为这样依然无法区别是类A中从基类N继承下来的成员,还是类B中从基类N继承下来的成员。
应当通过类N的直接派生类名来指出要访问的是类N的哪一个派牛类中的基类成员。
如
c1.A:
:
a+3;c1.A:
:
display();//要访问的是类N的派生类A中的基类成员
解决方案:
四、虚基类
1、虚基类的作用
从上面的介绍可知:
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。
在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避免产生二义性,使其惟一地标识一个成员,如c1.A:
:
display()。
在一个类中保留间接共同基类的多份同名成员,虽然有时是有必要的,可以在不同的数据成员中分别存放不同的数据,也可以通过构造函数分别对它们进行初始化。
但在大多数情况下,这种现象是人们不希望出现的。
因为保留多份数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员时的困难,容易出错。
而且在实际上,并不需要有多份拷贝。
C++提供虚基类(virtualbaseclass)的方法,使得在继承间接共同基类时只保留一份成员。
假设类D是类B和类C公用派生类,而类B和类C又是类A的派生类,如图2l所示。
设类A有数据成员data和成员函数fun,见图5.22(a)。
派生类B和C分别从类A继承了data和fun,此外类B还增加了自己的数据成员datab,类C增加了数据成员data_c。
如图5.22(b)所示。
如果不用虚基类,根据前面学过的知识,在类D中保留了类A成员data的两份拷贝。
在图5.22(c)中表示为intB:
:
data和intC:
:
data。
同样有两个同名的成员函数,表示为voidB:
:
fun()和voidC:
:
fun()。
类B中增加的成员data_b和类C中增加的成员datac不同名,不必用类名限定。
此外,类D还增加了自己的数据成员data_d和成员函数fun_d。
图21 图22
现在,将类A声明为虚基类,方法如下:
classA //声明基类A
{…};
classB:
virtualpublicA//声明类B是类A的公用派生类,A是B的虚基类
{…};
classC:
virtualpublicA//声明类C是类A的公用派生类,A是C的虚基类
{…};
注意:
虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。
因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
声明虚基类的一般形式为:
class派生类名:
virtual继承方式基类名
即在声明派生类时,将关键字virtual加到相应的继承方式前面。
经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次。
在派生类B和C中作了上面的虚基类声明后,派生类D中的成员如图5.23所示。
需要注意:
为了保证虚基类在派生类中只继才一次,应当在该基类的所有直接派生类中声明为虚基类。
否则仍然会出现对基类的多次继承。
如果像图5.24所示的那样,在派生类B和C中将类A声明为虚基类,而在派生类D中没有将类A声明为虚基类,则在派生类E中,虽然从类B和c路径派生的部分只保留一份基类成员,但从类D路径派生的部分还保留一份基类成员。
图23 图24
2、虚基类的初始化
如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。
例如:
classA //定义基类A
{ A(inti){}//基类构造函数,有一个参数
...};
classB:
virtualpublicA//A作为B的虚基类
{ B(intn):
A(n){} //B类构造函数,在初始化表中对虚基类初始化
…};
classC:
virtualpublicA//A作为C的虚基类
{ C(intn):
A(n)){} //C类构造函数,在初始化表中对虚基类初始化
…};
classD:
publicB,publicC//类D的构造函数,在初始化表中对所有基类初始化
{ D(intn):
A(n),B(n),C(n){}
…};
注意:
1、在定义类D的构造函数时,与以往使用的方法有所不同。
规定:
在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
2、C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类c)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
3、虚基类的简单应用举例
例9在例8的基础上,在Teacher类和Student类之上增加一个共同的基类Person,如图25所示。
作为人员的一些基本数据都放在Person中,在Teacher类和Student类中再增加一些必要的数据。
#include
#include
usingnamespacestd;
1、定义公共基类Person
classPerson
{ public:
Person(char*nam,chars,inta)//构造函数
{ strcpy(name,nam);sex=s;age=a;}
protected:
//保护成员
charname[20];
charsex;
intage;};
2、定义类Teacher
classTeacher:
virtualpublicPerson//声明Person为公用继承的虚基类
{ public:
Teacher(char*nam,chars,inta,char*t):
Person(nam,s,a)//构造函数
{strcpy(title,t);}
protected:
//保护成员
chartitle[10];};//职称
3、定义类Student
classStudent:
virtualpublicPerson//声明Person为公用继承的虚基类
{ public:
Student(char*nam,chars,inta,floatsco):
//构造函数
Person(nam,s,a),score(sco){} //初始化表
protected:
//保护成员
floatscore; };//成绩
4、定义多重继承的派生类Graduate
classGraduate:
publicTeacher,publicStudent//声明Teacher和Student类为公用继承直接基类
{ public:
Graduate(char*nam,chars,inta,char*t,floatsco,floatw):
//构造函数
Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){}//初始化表
voidshow()//输出研究生的有关数据
{ cout<<"name:
"< cout<<"age:
"< cout<<"sex:
"< cout<<"score:
"< cout<<"title:
"<
cout<<"wages:
"< private:
floatwage;};//工资
intmain()
{ Graduategrad1("Wang-li",'f',24,"assistant",89.5,1234.5);
grad1.show();
return0;}
运行结果为
name:
Wang-li
age:
24
Sex:
T
score:
89.5
title:
assistant
wages:
1234.5
说明:
1、Person类是表示一般人员属性的公用类,其中包括人员的基本数据,现在只包含了3个数据成员:
name(姓名)、sex(性别)、age(年龄)。
Teacher和Student类是Person的公用派生类,在Teacher类中增加title(职称),在Student类中增加score(成绩)。
Graduate(研究生)是Teacher类和Student类的派生类,在Graduate类中增加wage(津贴)。
一个研究生应当包含以上全部数据。
为简化程序,除了最后的派生类Graduate外,在其他类中均不包含成员函数。
2、清注意各类的构造函数的写法。
在Person类中定义了包含3个形参的构造函数,用它对数据成员name、sex和age进行初始化。
在Teacher和Student类的构造函数中,按规定要在初始化表中包含对基类的初始化,尽管对虚基类来说,编译系统不会由此调用基类的构造函数,但仍然应当按照派生类构造函数的统一格式书写。
在最后派生类Graduate的构造函数中,既包括对虚基类构造函数的调用,也包括对其直接基类的初始化。
3、在Graduate类中,只保留一份基类的成员,因此可以用Graduate类中的show函数引用Graduate类对象中的公共基类Person的数据成员name,sex,age的值,不需要加基类名和域运算符(:
:
),不会产生二义性。
可以看到:
使用多重继承时要十分小心,经常会出现二义性问题。
前面介绍的例子是简单的,如果派生的层次再多一些,多重继承更复杂一些,程序设计人员很容易陷入迷魂阵,程序的编写、调试和维护工作都会变得更加困难。
因此,许多专业人员认为:
不要提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承。
也是由于这个原因,有些面向对象的程序设计语言(如Java,Smalltalk)并不支持多重继承。
五、作业:
1、P196:
8、9题。
2、实验6。