charCDocType;
cin>>CDocType;
switch(tolower(CDocType))
{
case'p':
DocLib[i]=newPaperbackBook;
break;
case'm':
DocLib[i]=newMagazine;
break;
case'h':
DocLib[i]=newHelpFile;
break;
case'c':
DocLib[i]=newComputerBasedTraining;
break;
default:
--i;
break;
}
}
for(i=0;i<10;++i)
DocLib[i]->PrintNameOf();
}
在前面例子的SWITCH语句中,创建了不同类型的对象。
这一点依赖于用户对CDocType对象所作出的说明。
然而这些类型都是从类Document中派生出来的,故可以隐含地转换为Document*。
结果是DocLib成为一个“相似链表”(heterogeneouslist)。
此链表所包含的是不同种类的对象,其中的所有对象并不是有相同的类型。
因为Document类有一个PrintNameOf函数。
因此它能够打印图书馆中每本书的名称,但对于Document类型来说有一些信息会省略掉了(如:
Book的总页数,HelpFile的字节数等)。
注意:
强制基类去实现一个如PrintNameOf的函数,通常不是一个很好的设计,本章后面的“虚拟函数”中提供了一个可替换的设计方法。
多重继承
C++的后期的一些版本为继承引入了“多重继承”模式。
在一个多重继承的图中,派生类可以有多个直接基类。
考虑图9.3。
9.3所示的图中,显示了一个CollectibleString类。
该类既像Collectible类(一种可包容聚集的类),又像String类。
对于派生类需要多个基类的属性的问题,多重继承是一种很好的解决办法。
因而也很容易派生出CollectibleCustomer和CollectibleWindow等等。
对于一个特定的程序如果每个类的属性并不是全部要求使用,则每个类可以单独使用或者同别的类联合在一起使用。
因此把图9.3所描绘的类层次作为基础,用户很容易组织出不可收集的字符串或可收集的非字符串。
对于使用单一继承,则没有这种便利性。
虚基类层次有一些类层次很庞大,但有很多东西很普遍。
这些普遍的代码在基类中实现了,然而在派生类中又实现了特殊的代码。
对于基类来说重要的是建立一种机制,通过这种机制派生类能够完成大量的函数机能。
这种机制通常是用虚函数来实现的。
有时,基类为这些函数提供了一个缺省的实现。
如在图9.2的Document类层次中,两个重要的函数是Identify和WhereIs。
当调用Identify函数时,返回一个正确的标识。
对于各种文档来说正确的是:
对于Book,调用如doc->Identify()的函数必须返回ISBN编号;而对于一个HelpFile返回产品名和版本号更合理一些。
同样,WhereIs函数对于一本书来说应该返回行和书架号,但对于HelpFile就应该返回它的磁盘位置,也许是一个目录和名称。
了解到所有的Identify和WhereIs的函数实现返回的是同种类型的信息,这一点很重要。
在这个例子中,恰好是一种描述性字符串。
这些函数可以作为虚拟函数来实现,然后用指向基类的指针来调用,对于实际代码的联结将在运行时决定,以选择正确的Identify和WhereIs函数。
类协议的实现
类可以实现为要强制使用某些协议。
这些类称为“抽象类”,因为不能为这种类类型创建对象。
它们仅仅是为了派生别的类而存在。
当一个类中含有纯虚拟函数或当他们继承了某些纯虚拟函数却又没有为它们提供一个实现时,该类称为抽象类。
纯虚拟函数是用纯说明符定义的虚拟函数。
如下:
virtualchar*Identify()=0;
基类Document把如下一些协议强加给派生类。
*为Identify函数提供一个合适的实现
*为WhereIs函数提供一个合适的实现
在设计Document类时,通过说明这种协议,类设计者可以确保如不提供Identify和WhereIs函数则不能实现非抽象类。
因而Document类含有如下说明:
classDocument
{
public:
...
//对派生类的要求,它们必须实现下面这些函数
virtualchar*Identify()=0;
virtualchar*WhereIs()=0;
...
};
基类
如前面讨论的,继承过程创建的新的派生类是由基类的成员加上由派生类新加的成员组成。
在多重继承中,可以构造层次图,其中同一基类可以是多个派生类的一部分。
图9.4显示了这种图。
在图9.4中以图的形象表达了CollectibleString和CollectibleSortable的组成。
然而,基类Collectible通过路径CollectibleSortable以及CollectibleString到达类CollectibleSortableString。
为了消除这种冗余,当这些类被继承时,可以说明为虚拟基类。
有关说明虚拟基类以及带有虚拟基类的对象是如何组成的,见本章后面的“虚拟基类”。
--------------------------------------------------------------------------------
多重基类
如同多重继承中所描述的,一个类可以从多个基类中派生出来。
在派生类由多个基类派生出来的多重继承模式中,基类是用基类表语法成份来说明的(见本章开始的“概述”中的语法)。
例如:
CollectionOfBook类是由类Collection和Book类派生的,可按如下进行说明:
classCollectionOfBook:
publicBook,publicCollection
{
//新成员
};
基类的说明顺序一般没有重要的意义,除非在某些情况下要调用构造函数和析构函数的时候。
在这些情况下,基类的说明顺序会对下面所列的有影响。
*由构造函数引起的初始化发生的顺序。
如果你的代码依赖于CollectionOfBook的Book部分要在Collection部分之前初始化,则此说明顺序将很重要。
初始化是按基类表中的说明顺序进行初始化的。
*激活析构函数以作清除工作的顺序。
同样,当类的其它部分正在被清除时,如果某些特别部分要保留,则该顺序也很重要。
析构函数的调用是按基类表说明顺序的反向进行调用的。
注意:
基类的说明顺序会影响类的存储器分布。
不要对基类成员在存储器中的顺序作出任何编程的决定。
在你说明基类表时,不能把同一类名称说明多次。
但是对于一个派生类而言,其非直接基类可以有多个相同的。
虚拟基类
因为一个类可以多次作为一个派生类的非直接基类。
C++提供了一个办法去优化这种基类的工作。
研究图9.5中的类层次,它显示了一个模拟的午餐线。
在图9.5中,Queue是CashierQueue和LunchQueue的基类。
但是当这两个类联合在一起形成LunchCashierQueue时,下面的问题就产生了:
新的类包含有两个Queue类型的子对象,一个来自于CachierQueue,另一个来自于LunchQueue。
图9.6给出了一个概念上的存储器分布(实际的内容公布可能会进行优化)。
注意,在LunchCashierQueue对象中,有两个Queue子对象。
下面的代码说明Queue为虚拟基类:
classQueue
{
//成员表
};
classCashierQueue:
virtualpublicQueue
{
//成员表
};
classLunchQueue:
virtualpublicQueue
{
//成员表
};
classLunchCashierQueue:
publicLunchQueue,publicCashierQueue
{
//成员表
};
关键字virtual确保了仅有一个Queue对象的拷贝(见图9.7)。
一个类对于给定的类型既可以有虚拟的组成部分,也可以有非虚拟的组成部分。
这种情况发生在图9.8所示的情况下。
在图9.8中,CachierQueue和LunchQueue用Queue作为虚拟基类。
但是TakeoutQueue仅说明Queue为基类,并不是虚拟基类。
因此LunchTakeoutCashierQueue有两个Queue子对象:
一个是从包括LunchCashierQueue的路径中继承而来的,另一个则是从包括TakeoutQueue的路径中而来,图9.9显示了这一点。
注意:
虚拟继承同非虚拟继承相比具有大小上的好处,然而它也引入了额外的运行开销。
如果一个派生类重载了一个从虚拟基类中继承的虚拟函数,而且该派生类以指向虚拟基类的指针调用这些构造函数和析构函数时,编译器会引入一个附加的隐含的“vtordisp”域到带有虚拟基类的类中。
/vd0编译器选项禁止了这个增加的隐含vtordisp构造/析构位置成员。
/vd1选项(缺省),使得在需要时可以解除禁止。
只有在你确信所有类的构造函数或析构函数都虚拟地调用了虚拟函数,vtordisp才可以关掉。
/vd编译器选项会影响全局编译模式。
使用vtordisp编译指示可以在基于类方式上打开或禁止vtordisp域:
#pragmavtordisp(off)
classGetReal:
virtualpublic{...};
#pragmavtordisp(on)
名称的二义性
多重继承使得从不同的路径继承成员名称成为可能。
沿着这些路径的成员名称并不必然是唯一的。
这些名称的冲突称为“二义性”。
任何引用类成员的表达式必须使用一个无二义性的引用。
下面的例子显示了二义性是如何发生的。
//说明两个基类A和B
classA
{
public:
unsigneda;
unsignedb();
};
classB
{
public:
unsigneda();//注意类A也有一个成员"a"和一个成员"b"
intb();
charc;
};
//定义从类A和类B中派生出的类C
classC:
publicA,publicB
{
};
按上面所给出的类说明,如下的代码就会引出二义性,因为不清楚是引用类A的b呢,还是引用类B的b:
C*pc=newC;
pc->b();
考虑一下上面的代码,因为名称a既是类A又是类B的成员,因而编译器并不能区分到底调用哪一个a所指明的函数。
访问一个成员,如果它能代表多个函数、对象、类型或枚举则会引起二义性。
编译器通过下面的顺序执行以检测出二义性:
1.如果访问的名称是有二义性的(如前述),则产生一条错误信息。
2.如果重载函数是无二义性的,它们就没有什么问题了(有关重载函数二义性的情况,见第12章“重载”中的“参量匹配”)。
3.如果访问的名称破坏了成员访问许可,则产生一条错误信息(有关信息详见第10章“成员访问控制”)。
在一个表达式产生了一个通过继承产生的二义性时,通过用类名称限制发生问题的名称即可人工解决二义性,要使前面的代码以无二义性地正确编译,要按如下使用代码:
C*pc=newC;
pc->B:
:
a();
注意:
在类C说明之后,在C的范围中引用B就会潜在地引起错误。
但是,直到在C的范围中实际使用了一个对B的无限定性的引用,才会产生错误。
二义性和虚拟基类
如果使用了虚拟基类、函数、对象、类型以及枚举可以通过多重继承的路径到达,但因为只有一个虚拟基类的实例,因而访问这些名称时,不会引起二义性。
图9.10显示了采用虚基类和非虚基类的对象的组成。
在图9.10中,访问任何类A的成员,通过非虚拟基类访问则会引起二义性;因为编译器没有任何信息以解释是使用同类B联系在一起的子对象,还是使用同类C联系在一起的子对象,然而当A说明为虚拟基类时,则对于访问哪一个子对象不存在问题了。
支配
通过继承图可能有多个名称(函数的、对象的、枚举的)可以达到。
这种情况视为非虚拟基类引起的二义性。
但虚拟基类也可以引起二义性,除非一个名称“支配”(dominate)了其它的名称。
一
个名称支配其它的名称发生在该名称定义在两个类中,其中一个是由另一个派生的,占支配地位的名称是派生类中的名称,在此名称被使用的时候,相反不会产生二义性,如下面的代码所示:
classA
{
public:
inta;
};
classB:
publicvirtualA
{
public:
inta();
};
classC:
publicvirtualA
{
...
};
classD:
publicB,publicC
{
public:
D(){a();}//不会产生二义性,B:
:
a()支配了A:
:
a
};
转换的二义性
显式地或隐含地对指向类类型的指针或引用的转换也可引起二义性。
图9.11显示了如下几点:
*说明了一个类型D的对象。
*把取地址运算符用于此对象效果。
注意,地址运算符总是支持对象的基类地址。
*显式地把取地址运算符得到的指针转换为指向基类类型A的指针。
注意,把对象的地址造型转换成类型A*,通常并未给编译器提供足够的信息以确定到底是选择哪一个A类型的子对象。
在此情况下,存在着两个A类型的子对象。
对于到A*的转换是有二义性的,因为没有办法分辨出哪一个A类型的子对象是正确的。
注意只要显式地说明你想要使用的是哪一个子对象,如下:
(A*)(B*)&d//使用B的子对象
(A*)(C*)&d//使用C的子对象
--------------------------------------------------------------------------------
虚拟函数
虚拟函数可以确保在一个对象中调用正确的函数,而不管用于调用函数的表达式。
假设一个基类含有一个说明为虚拟函数同时一个派生类定义了同名的函数。
派生类中的函数是由派生类中的对象调用的,甚至它可以用指向基类的指针和引用来调用。
下面的例子显示了一个基类提供了一个PrintBalance函数的实现:
classAccount
{
public:
Account(doubled);//构造函数
virtualdoubleGetBalance();//获得平衡
virtualvoidPrintBalance();//缺省实现
private:
double_balance;
};
//构造函数Account的实现
doubleAccount:
:
Account(doubled)
{
_balance=d;
}
//Account的GetBalance的实现
doubleAccount:
:
GetBalance()
{
return_balance;
}
//PrintBalance的缺省实现
voidAccount:
:
PrintBalance()
{
cerr<<"Error.Balancenotavailableforbasetype".
<}
两个派生类CheckingAccount和SavingsAccount按如下方式创建:
classCheckingAccount:
publicAccount
{
public:
void
PrintBalance();
};
//CheckingAccount的PrintBalance的实0现
voidCheckingAccount:
:
PrintBalance()
{
cout<<"Checkingaccountbalance:
"
<}
classSavingsAccount:
publicAccount
{
public:
voidPrintBalance();
};
//SavingsAccount中的PrintBalance的实
现voidSavingsAccout:
:
PrintBalance()
{
cout<<"Savingsaccountbalance:
"
<}
函数PrintBalance在派生类中是虚拟的,因为在基类Account中它是说明为虚拟的,要调用如PrintBalance的虚拟函数,可以使用如下的代码:
//创建类型CheckingAccount和SavingsAccount的对象
CheckingAccount*pChecking=newCheckingAccount(100.00);
SavingsAccount*pSavings=newSavingsAccount(1000.00);
//用指向Account的指针调用PrintBalance
Account*pAccount=pChecking;
pAccount->PrintBalance();
//使用指向Account的指针调用PrintBalance
pAccount=pSavings;
pAccount->PrintBalance();
在前面的代码中,除了pAccount所指的对象不同,调用PrintBalance的代码是相同的。
因为PrintBalance是虚拟的,将会调用为每个对象所定义的函数版本,在派生类CheckingAccount和SavingsAccount中的函数“覆盖”了基类中的同名函数。
如果一个类的说明中没有提供一个对PrintBalance的覆盖的实现,则将采用基类Account中的缺省实现。
派生类中的函数重载基类中的虚拟函数,仅在它们的类型完全相同时才如此。
派生类中的函数不