系列文章目录
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:有多少个Worker
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
14.3.1 有多少 Worker
假设首先从 Singer和Waiter 公有派生出 SingingWaiter:class SingingWaiter:public singer,public waiter(...);
因为 Singer 和 Waiter 都继承了一个 Worker 组件,因此 SingingWaiter将包含两个 Worker 组件(参见图 14.4)
正如预期的,这将引起问题。例如,通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:
SingingWaiter ed;
Worker *pw=&ed;//.ambiquous
通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但ed中包含两个 Worker 对象,有两个地址可供选择,所以应使用类型转换来指定对象:
Worker *pw1 =(Waiter *)&ed;// the worker in waiter
Worker *pw2=(Singer *)&ed;//the worker in singer
这将使得使用基类指针来引用不同的对象(多态性)复杂化。包含两个 Worker 对象拷贝还会导致其他的问题。然而,真正的问题是:为什么需要 Worker 对象的两个拷贝?唱歌的侍者和其他 Worker 对象一样,也应只包含一个姓名和一个ID。C++引入多重继承的同时,引入了一种新技术–虚基类(virtualbase class),使 MI成为可能。
1.虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字 virtual,可以使 Worker 被用作 Singer 和 Waiter 的虛基类(virtual和 public 的次序无关紧要):
class Singer:virtual public Worker{...};
class Waiter:public virtual Worker{...};
然后,可以将SingingWaiter类定义为:
class SingingWaiter:public Singer,public waiter{...};
现在,SingingWaiter 对象将只包含 Worker 对象的一个副本。从本质上说,继承的 Singer 和 Waiter 对象共享一个 Worker 对象,而不是各自引入自己的 Worker 对象副本(请参见图14.5)。因为 SingingWaiter现在只包含了一个 Worker 子对象,所以可以使用多态。
您可能会有这样的疑问:
- 为什么使用术语“虚”?
- 为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为多M的准则呢?
- 是否存在麻烦呢?
首先,为什么使用术语虚?毕竟,在虚函数和虚基类之间并不存在明显的联系。C++用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。例如,如果新关键字与重要程序中的重要函数或变量的名称相同,这将非常麻烦。因此,C++对这种新特性也使用关键字 virtual–有点像关键字重载。
其次,为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为M的准则呢?第一,在一些情况下,可能需要基类的多个拷贝;第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应当的:第三,这样做有其缺点,将在下一段介绍。最后,是否存在麻烦?是的。为使虚基类能够工作,需要对C++规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码。例如,将SingingWaiter 类添加到 Worker 集成层次中时,需要在 Singer 和 Waiter 类中添加关键字 virtual。
2.新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:
class A
{
int a;
public :
A(int n=0):a(n){}
};
class B:public A
{
int b;
public :
B(intm=0,intn=0):A(n),b(m)
}
:class C:public B
{
int c;
public :
C(intq=0,intm=0,intn=0):B(m,n),c(q){}
}
C类的构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。这里,C类的构造函数使用值 q,并将值m和n传递给B类的构造函数;而B类的构造函数使用值m,并将值n传递给 A类的构造函数。
如果 Worker 是虚基类,则这种信息自动传递将不起作用。例如,对于下面的 MI构造函数
SingingWaiter(const Worker &wk,int p=0,int v= Singer::other)
:Waiter(wk,p),Singer(wk,v){}//flawed
存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter和Simger)将wk传递给 Worker 对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员 panache 和 voice,但 wk参数中的信息将不会传递给子对象 Waiter。然而,编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker 的默认构造函数。如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该是这样:
SingingWaiter(const Worker &wk,intp=0,int v= Singer::other):Worker(wk),Waiter(wk,p),Singer(wk,v){}
上述代码将显式地调用构造函数 worker(const Worker&)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。
警告:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
14.3.2 哪个方法
除了修改类构造函数规则外,MI通常还要求调整其他代码。假设要在SingingWaiter 类中扩展 Show()方法。因为 SingingWaiter 对象没有新的数据成员,所以可能会认为它只需使用继承的方法即可。这引出了第一个问题。假设没有在 SingingWaiter 类中重新定义 Show()方法,并试图使用 SingingWaiter 对象调用继承的 Show()方法:
SingingWaiter newhire("Elise Hawks" 2005,6,soprano);newhire.show();//ambiguous
对于单继承,如果没有重新定义Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个 Show()函数,这使得上述调用是二义性的。
警告:多重继承可能导致函数调用的二义性。例如,BadDude 类可能从 Gunslinger 类和 PokerPlayer 类那里继承两个完全不同的 Draw( )方法。
可以使用作用域解析运算符来澄清编程者的意图:
SingingWaiter newhire("Elise Hawks",2005,6,soprano);newhire.Singer::Show();/use Singer version
然而,更好的方法是在 SingingWaiter 中重新定义Show(),并指出要使用哪个 Show()。例如,如果希望 SingingWaiter对象使用 Singer版本的 Show(),则可以这样做:
void SingingWaiter::Show()
Singer::Show();
对于单继承来说,让派生方法调用基类的方法是可以的。例如,假设HeadWaiter 类是从 Waiter 类派生而来的,则可以使用下面的定义序列,其中每个派生类使用其基类显示信息,并添加自己的信息:
void Worker::Show()const
"Name:"<< fullname << "\n";Cout <<
cout <<"Employee ID:"<< id << "\n";
void Waiter::Show()const
Worker::Show();
cout<<"Panache rating:"<<panache << "\n";
void HeadWaiter::Show()constWaiter::Show();cout<<"Presence rating:"<<presence <<"n";
然而,这种递增的方式对 SingingWaiter 示例无效。下面的方法将无效,因为它忽略了 Waiter 组件:
void SingingWaiter::Show()
Singer::Show();
可以通过同时调用 Waiter版本的 Show()来补救:void SingingWaiter::Show() Singer::Show(); Waiter::Show();
然而,这将显示姓名和ID两次,因为 Singer::Show()和 Waiter::Show()都调用了 Worker::Show().如果解决呢?一种办法是使用模块化方式,而不是递增方式,即提供一个只显示 Worker 组件的方法和-个只显示 Waiter 组件或 Singer 组件(而不是 Waiter 和 Worker组件)的方法。然后,在SingingWaiter::Show()方法中将组件组合起来。例如,可以这样做:
void Worker::Data()const
cout <s"Name:"sfullname <n":
cout <<"Employee ID:"<< id << "\n";
void Waiter::Data()const
cout<<"Panache rating:"<<panache <<"\n";
void Singer::Data()const
cout <<"Vocalrange:"<< pv[voice]<< "\n";
void Singingwaiter::Data()const
Singer::Data();Waiter::Data();
void SingingWaiter::show()const
cout <<"Category:singing waiter\n";
Worker::Data();
Data();
与此相似,其他 Show()方法可以组合适当的 Data( )组件。采用这种方式,对象仍可使用 Show()方法。而Data()方法只在类内部可用,作为协助公有接口的辅助方法。然而,使 Data()方法成为私有的将阻止 Waiter 中的代码使用 Worker::Data(),这正是保护访问类的用武之地。如果 Data()方法是保护的,则只能在继承层次结构中的类中使用它,在其他地方则不能使用。
另一种办法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。
Set()方法取得数据,以设置对象值,该方法也有类似的问题。例如,SingingWaiter:Set()应请求 Worker信息一次,而不是两次。对此,可以使用前面的解决方法。可以提供一个受保护的Get()方法,该方法只请求一个类的信息,然后将使用Get()方法作为构造块的Set()方法集合起来。
总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到 MI,则还可能需要重新编写它们。程序清单14.10列出了修改后的类声明,程序清单14.11列出实现。
程序清单 14.10 workermi.h
// workermi.h -- working classes with MI
#ifndef WORKERMI_H_
#define WORKERMI_H_
#include <string>
class Worker // an abstract base class
{
private:
std::string fullname;
long id;
protected:
virtual void Data() const;
virtual void Get();
public:
Worker() : fullname("no one"), id(0L) {}
Worker(const std::string & s, long n)
: fullname(s), id(n) {}
virtual ~Worker() = 0; // pure virtual function
virtual void Set() = 0;
virtual void Show() const = 0;
};
class Waiter : virtual public Worker
{
private:
int panache;
protected:
void Data() const;
void Get();
public:
Waiter() : Worker(), panache(0) {}
Waiter(const std::string & s, long n, int p = 0)
: Worker(s, n), panache(p) {}
Waiter(const Worker & wk, int p = 0)
: Worker(wk), panache(p) {}
void Set();
void Show() const;
};
class Singer : virtual public Worker
{
protected:
enum {other, alto, contralto, soprano,
bass, baritone, tenor};
enum {Vtypes = 7};
void Data() const;
void Get();
private:
const static char *pv[Vtypes]; // string equivs of voice types
int voice;
public:
Singer() : Worker(), voice(other) {}
Singer(const std::string & s, long n, int v = other)
: Worker(s, n), voice(v) {}
Singer(const Worker & wk, int v = other)
: Worker(wk), voice(v) {}
void Set();
void Show() const;
};
// multiple inheritance
class SingingWaiter : public Singer, public Waiter
{
protected:
void Data() const;
void Get();
public:
SingingWaiter() {}
SingingWaiter(const std::string & s, long n, int p = 0,
int v = other)
: Worker(s,n), Waiter(s, n, p), Singer(s, n, v) {}
SingingWaiter(const Worker & wk, int p = 0, int v = other)
: Worker(wk), Waiter(wk,p), Singer(wk,v) {}
SingingWaiter(const Waiter & wt, int v = other)
: Worker(wt),Waiter(wt), Singer(wt,v) {}
SingingWaiter(const Singer & wt, int p = 0)
: Worker(wt),Waiter(wt,p), Singer(wt) {}
void Set();
void Show() const;
};
#endif
程序清单14.12 workmi.cpp
// workermi.cpp -- working class methods with MI
#include "workermi.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// Worker methods
Worker::~Worker() { }
// protected methods
void Worker::Data() const
{
cout << "Name: " << fullname << endl;
cout << "Employee ID: " << id << endl;
}
void Worker::Get()
{
getline(cin, fullname);
cout << "Enter worker's ID: ";
cin >> id;
while (cin.get() != '\n')
continue;
}
// Waiter methods
void Waiter::Set()
{
cout << "Enter waiter's name: ";
Worker::Get();
Get();
}
void Waiter::Show() const
{
cout << "Category: waiter\n";
Worker::Data();
Data();
}
// protected methods
void Waiter::Data() const
{
cout << "Panache rating: " << panache << endl;
}
void Waiter::Get()
{
cout << "Enter waiter's panache rating: ";
cin >> panache;
while (cin.get() != '\n')
continue;
}
// Singer methods
const char * Singer::pv[Singer::Vtypes] = {"other", "alto", "contralto",
"soprano", "bass", "baritone", "tenor"};
void Singer::Set()
{
cout << "Enter singer's name: ";
Worker::Get();
Get();
}
void Singer::Show() const
{
cout << "Category: singer\n";
Worker::Data();
Data();
}
// protected methods
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << endl;
}
void Singer::Get()
{
cout << "Enter number for singer's vocal range:\n";
int i;
for (i = 0; i < Vtypes; i++)
{
cout << i << ": " << pv[i] << " ";
if ( i % 4 == 3)
cout << endl;
}
if (i % 4 != 0)
cout << '\n';
while (cin >> voice && (voice < 0 || voice >= Vtypes) )
cout << "Please enter a value >= 0 and < " << Vtypes << endl;
while (cin.get() != '\n')
continue;
}
// SingingWaiter methods
void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}
void SingingWaiter::Get()
{
Waiter::Get();
Singer::Get();
}
void SingingWaiter::Set()
{
cout << "Enter singing waiter's name: ";
Worker::Get();
Get();
}
void SingingWaiter::Show() const
{
cout << "Category: singing waiter\n";
Worker::Data();
Data();
}
程序清单 14.11 workermi.cpp
// workmi.cpp -- multiple inheritance
// compile with workermi.cpp
#include <iostream>
#include <cstring>
#include "workermi.h"
const int SIZE = 5;
int main()
{
using std::cin;
using std::cout;
using std::endl;
using std::strchr;
Worker * lolas[SIZE];
int ct;
for (ct = 0; ct < SIZE; ct++)
{
char choice;
cout << "Enter the employee category:\n"
<< "w: waiter s: singer "
<< "t: singing waiter q: quit\n";
cin >> choice;
while (strchr("wstq", choice) == NULL)
{
cout << "Please enter a w, s, t, or q: ";
cin >> choice;
}
if (choice == 'q')
break;
switch(choice)
{
case 'w': lolas[ct] = new Waiter;
break;
case 's': lolas[ct] = new Singer;
break;
case 't': lolas[ct] = new SingingWaiter;
break;
}
cin.get();
lolas[ct]->Set();
}
cout << "\nHere is your staff:\n";
int i;
for (i = 0; i < ct; i++)
{
cout << endl;
lolas[i]->Show();
}
for (i = 0; i < ct; i++)
delete lolas[i];
cout << "Bye.\n";
// cin.get();
// cin.get();
return 0;
}
当然,好奇心要求我们测试这些类,程序清单14.12提供了测试代码。注意,该程序使用了多态属性,将各种类的地址赋给基类指针。另外,该程序还在下面的检测中使用了C-风格字符串库函数 strchr():while(strchr(“wstq”,choice)==NULL)
该函数返回参数 choice 指定的字符在字符串“wstq”中第一次出现的地址,如果没有这样的字符,则返回 NULL指针。使用这种检测比使用if语句将choice 指定的字符同每个字符进行比较简单。请将程序清单14.12与workermi.cpp 一起编译。