多重继承
MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系。例如,可以从Waiter类和Singer类派生出 SingingWaiter类:
class SingingWaiter : public Waiter, public Singer {...};
请注意。必须使用关键字public 来限定每个基类。这是因为,除非特别指出,否则编译器将认为是私有派生:
class SingingWaiter : public Waiter, Singer {...}; //Singer 是私有继承
MI可能会给程序员带来许多新问题。其中两个主要的问题是:从两个不同的基类继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例。为解决这些问题,需要使用一些新规则和不同的句法。因此,与使用单继承相比,使用MI更困难,也更容易出现问题。由于这个原因,许多C++用户强烈反对使用MI,一些人甚至希望删除MI。而喜欢MI的人则认为,对一些特殊的工程来说,MI很有用,甚至是必不可少的:也有一些人建议谨慎、适度地使用MI。
Worker
下面来看一个例子,并介绍有哪些问题以及如何解决它们。要使用MI,需要几个类。我们将定义一个抽象基类Worker,并使用它派生出 Waiter类和Singer类。然后,便可以使用MI从 Waiter类和Singer类派生出 SingingWaiter类(参见图14.3)。这里使用两个独立的派生来使基类(Worker)被继承,这将导致MI的大多数麻烦。首先声明Worker、Waiter和 Singer类,如程序清单14.7所示。
worker0.h
#ifndef WORKER0_H_
#define WORKER0_H_
#include <string>
class Worker
{
private:
std::string fullname;
long id;
public:
Worker() : fullname("no one"), id(0L) {}
Worker(const std::string & s, long n)
: fullname(s), id(n) {}
virtual ~Worker() = 0;
virtual void Set();
virtual void Show() const;
};
class Waiter : public Worker
{
private:
int panache;
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 : public Worker
{
protected:
enum {other, alto, contralto, soprano, bass, baritone, tenor};
enum {Vtypes = 7};
private:
static char *pv[Vtypes];
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;
};
#endif
worker0.cpp
#include "worker0.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
Worker::~Worker() {}
void Worker::Set()
{
cout << "Enter worker's name: ";
getline(cin, fullname);
cout << "Enter worker's ID: ";
cin >> id;
while (cin.get() != '\n')
continue;
}
void Worker::Show() const
{
cout << "Name: " << fullname << "\n";
cout << "Employee ID: " << id << "\n";
}
void Waiter::Set()
{
Worker::Set();
cout << "Enter waiter's panache rating: ";
cin >> panache;
while (cin.get() != '\n')
continue;
}
void Waiter::Show() const
{
cout << "Category: waiter\n";
Worker::Show();
cout << "Panache rating: " << panache << "\n";
}
char * Singer::pv[] = { "other", "alto", "contalto", "soprano", "bass", "baritone", "tenor" };
void Singer::Set()
{
Worker::Set();
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 << endl;
}
while (cin >> voice && (voice < 0 || voice >= Vtypes))
cout << "Please enter a value >= 0 and < " << Vtypes << endl;
while (cin.get() != '\n')
continue;
}
void Singer::Show() const
{
cout << "Category: singer\n";
Worker::Show();
cout << "Vocal ranges: " << pv[voice] << endl;
}
main.cpp
#include <iostream>
#include "worker0.h"
const int LIM = 4;
int main()
{
Waiter bob("Bob Apple", 314L, 5);
Singer bev("Beverly Hills", 522L, 3);
Waiter w_temp;
Singer s_temp;
Worker * pw[LIM] = { &bob, &bev, &w_temp, &s_temp };
int i;
for (i = 2; i < LIM; i++)
{
pw[i]->Set();
}
for (i = 0; i < LIM; i++)
{
pw[i]->Show();
std::cout << std::endl;
}
return 0;
}
这种设计看起来是可行的:使用Waiter 指针来调用Waiter::Show()和Waiter::Set();使用Singer指针来调用Singer::Show()和 Singer::Set()。然后,如果添加一个从 Singer 和 Waiter 类派生出的 SingingWaiter类后,将带来一些问题。具体地说,将出现以下几个问题:
有多少个Worker
假设首先从 Singer和 Waiter公有派生出 SingingWaiter:
class SingingWaiter : public Singer, public Waiter {...};
因为Singer 和 Waiter都继承了一个Worker组件,因此 SingingWaiter将包含两个Worker 组件。
正如预期的,这将引起问题。例如,通常可以将派生类对象的地址赋给基类指针,但是现在将出现二义性:
SiningWaiter ed;
Worker * pw = &ed; //二义性
通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象:
Worker * pw1 = (Waiter *) &ed;
Worker * pw2 = (Singer *) %ed;
这将使得使用基类指针来引用不同的对象(多态性)复杂化。
包含两个Worker对象拷贝还会导致其他的问题。不过,真正的问题是:为什么需要Worker对象的两个拷贝?唱歌的侍者和其他Worker对象一样,也应只包含一个姓名和一个 ID。当C++引入多重继承的同时,它引入了一种新技术——虚基类(virtual base class),使MI成为可能。
虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字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子对象,所以可以使用多态。
为什么使用术语"虚"
毕竟,在虚函数和虚基类之间并不存在明显的联系。C++用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。例如,如果新关键字与重要程序中的重要函数或变量的名称相同,这将非常麻烦。因此,C+对这种新特性也使用关键字virtual——有点像关键字重载。
为什么不抛弃将基类声明为虚拟的这种方式,而使虚拟行为成为MI的准则呢
第一,在一些情况下,可能需要基类的多个拷贝;
第二,将基类作为虚拟的要求程序完成额外的计算,为不需要的工具付出代价是不应当的;
第三,这样做有其缺点,将在下一段介绍。
是否存在麻烦
是的。为使虚基类能够工作,需要对C++规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码。例如,将SingingWaiter类添加到Worker集成层次中时,需要在Singer和 Waiter类中添加关键字virtual。
新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,惟一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:
class A
{
int a;
public:
A (int n = 0) {a = n;}
...
};
class B : public A
{
int b;
public:
B(int m = 0, int n = 0) : A(n) {b = m;}
...
};
class C : public B
{
int c;
public:
C(int q = 0, int m = 0, int n = 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) {} //有缺陷的
存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter和 Singer)将wk传递给Worker对象。为避免这种冲突,C++在基类是虚拟的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员panache和 voice,但wk参数中的信息将不会传递给子对象Waiter。不过,编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker 的默认构造函数。
如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该是这样:
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other)
: Worker(wk), Waiter(wk, p), Singer(wk, v) {}
上述代码将显式地调用构造函数worker ( const Worker &)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。
警告:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
哪种方法
除了修改类构造函数规则外,Ml通常还要求调整其他代码。假设要在SingingWaiter类中扩展Show()方法。因为SingingWaiter对象没有新的数据成员,所以可能会认为它只需使用继承的方法即可。这引出了第一个问题。假设没有在SingingWaiter类中重新定义Show()方法,并试图使用SingingWaiter对象调用继承的 Show()方法:
SingingWaiter newhire ("Elise Hawks", 2005, 6, soprano);
newhire.Show(); //有歧义的
对于单继承,如果没有重新定义Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个 Show()函数,这使得上述调用是二义性的。
警告:多重继承可能导致函数调用的二义性。例如,BadDude类可能从Gunslinger类和 PokerPlayer类那里继承两个完全不同的Draw()方法。
可以使用作用域解析操作符来澄清编程者的意图:
SingingWaiter newhire ("Elise Hawks", 2005, 6, soprano);
newhire.Singer::Show(); //使用Singer的
不过,更好的方法是在. SingingWaiter中重新定义Show(),并指出要使用哪个Show()。例如,如果希望SingingWaiter对象使用Singer 版本的 Show(),则可以这样做:
void SingingWaiter::Show()
{
Singer::Show();
}
对于单继承来说,让派生方法调用基类的方法是可以的。例如,假设HeadWaiter类是从Waiter类派生而来的,则可以使用下面的定义序列,其中每个派生类使用其基类显示信息,并添加自己的信息:
void Worker::Show() const
{
cout << "Name: " << fullname << endl;
cout << "Employee ID: " << id << endl;
}
void Waiter::Show() const
{
Worker::Show();
cout << "Panache rating: " << panache << "\n";
}
void HeadWaiter::Show() const
{
Waiter::Show();
cout << "Presence rating: " << presence << "\n";
}
不过,这种递增的方式对SingingWaiter范例无效。方法:
void SingingWaiter::Show()
{
Singer::Show();
}
将无效,因为它忽略了Waiter 组件。可以通过同时调用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 << "Name: " << fullname << endl;
cout << "Employee ID: " << id << endl;
}
void Waiter::Data() const
{
cout << "Panache rating: " << panache << endl;
}
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << endl;
}
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,则还可能需要重新编写它们。
workermi.h
#ifndef WORKERMI_H_
#define WORKERMI_H_
#include <string>
class Worker
{
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;
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:
static char *pv[Vtypes];
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;
};
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 // !WORKERMI_H
workermi.cpp
#include "workermi.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
//Worker
Worker::~Worker() {}
//protected
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
void Waiter::Set()
{
cout << "Enter waiter's name: ";
Worker::Get();
Get();
}
void Waiter::Show() const
{
cout << "Category: waiter\n";
Worker::Data();
Data();
}
//protected
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
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
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';
cin >> voice;
while (cin.get() != '/n')
continue;
}
//SingingWaiter
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();
}
main.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";
return 0;
}
混合使用虚基类和非虚基类
再来看下通过多种途径继承一个基类的派生类的情况。如果基类是虚基类,派生类将包含基类的一个子对象;如果基类不是虚基类,派生类将包含多个子对象。当虚基类和非虚基类混合时,情况将如何呢?
例如,假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类,而类M是从C、D、X和Y派生而来的。在这种情况下,类M从虚拟派生祖先(即类C和D)那里共继承了一个B类子对象,并从每一个非虚拟派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含三个B类子对象。当类通过多条虚拟途径和非虚拟途径继承某个特定的基类时,该类将包含–个表示所有的虚拟途径的基类子对象和分别表示各条非虚拟途径的多个基类子对象。
虚基类和支配
使用虚基类将改变C++解析二义性的方式。使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于(dominates )其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。
那么,一个成员名如何优先于另一个成员名呢?
派生类中的名称优先于直接或间接祖先类中的相同名称。例如,在下面的定义中:
class B
{
public:
short q();
...
};
class C : virtual public A
{
public:
long q();
int omb()
...
};
class D : public C
{
...
};
class E: virtual public B
{
private:
int omb()
...
};
class F : public D, public E
{
...
};
类C中的q()定义优先于类B中的q()定义,因为类C是从类B派生而来的。因此,F中的方法可以使用q()来表示C::q()。而任何一个omb()定义都不优先于其他omb()定义,因为C和E都不是对方的基类。所以,在F中使用非限定的omb()将导致二义性。
虚拟二义性规则与访问规则无关,也就是说,即使E::omb()是私有的,不能在F类中直接访问,但使用omb()仍将导致二义性。同样,即使C::q()是私有的,它也将优先于D::q()。在这种情况下,可以在类F中调用B::q(),但如果不限定q(),则将意味着要调用不可访问的C::q()。