多重继承:(multiple inheritance, MI)使用多个基类的继承被称为多重继承。且描述的是有多个直接基类的类。与单继承一样,公有多重继承表示的也是is-a关系。
公有多重继承:必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生,示例如下:
// 使用关键字public限定每一个基类
class SingWaiter : public Waiter, public Singer
{
};
// 不使用关键字public限定基类
// Singer是一个私有基类
class SingWaiter : public Waiter, Singer
{
};
私有多重继承和保护多重继承:使用关键字private和关键字protected来限定基类,表示的是has-a关系。
下面着重介绍公有多重继承
多重继承的使用会产生一些新问题。其中有两个主要的问题需要引起关注:
通过多重继承示例了解多重继承,并了解多重继承的问题及解决问题的方式:
示例创建说明:
定义一个抽象基类Worker,并使用抽象基类派生出Waiter和Singer类。
使用多重继承(MI)从Waiter类和Singer类派生出SingingWaiter类。同时使用独立的派生来使基类Worker被继承。
下面是抽象基类Worker的声明:
// 抽象基类Worker
class Worker
{
private:
std::string fullname;
long id;
public:
Worker();
Worker(const std::string& s, long n);
virtual ~Worker() = 0;
virtual void set();
virtual void show() const;
};
以下示例是派生类Waiter与派生类Singer的声明:
// 派生类Waiter
class Waiter : public Worker
{
private:
int panache;
public:
Waiter();
Waiter(const std::string& s, long n, int p = 0);
Waiter(const Worker& wk, int p = 0);
void set();
void show() const;
};
// 派生类Singer
class Singer : public Worker
{
protected:
// 一个枚举使用符号常量alto、contralto等表示声音类型
enum
{
other,
alto,
contralto,
soprano,
bass,
baritone,
tenor,
};
enum
{
Vtypes = 7,
};
private:
// 静态数组pv存储指向相应C-风格字符串的指针
static char* pv[Vtypes];
int voice;
public:
Singer();
Singer(const std::string& s, long n, int v = other);
Singer(const Worker& wk, int v = other);
void set();
void show() const;
};
下面是抽象基类的实现,当然抽象基类不能实例化对象:
// 抽象基类Worker
Worker::Worker()
: fullname("no one")
, id(0L)
{
}
Worker::Worker(const std::string &s, long n)
: fullname(s)
, id(n)
{
}
Worker::~Worker()
{
}
void Worker::set()
{
std::cout << "Enter worker's name: ";
getline(std::cin, fullname);
std::cout << "Enter worker's ID: ";
std::cin >> id;
while (std::cin.get() != '\n'){
continue;
}
}
void Worker::show() const
{
std::cout << "Name: " << fullname << std::endl;
std::cout << "Employee ID: " << id << std::endl;
}
下面示例代码是派生类waiter和singer的实现:
// 派生类Waiter
Waiter::Waiter()
: Worker()
, panache(0)
{
}
Waiter::Waiter(const std::string &s, long n, int p)
: Worker(s, n)
, panache(p)
{
}
Waiter::Waiter(const Worker &wk, int p)
: Worker(wk)
, panache(p)
{
}
void Waiter::set()
{
Worker::set();
std::cout << "Enter waiter's panache rating: ";
std::cin >> panache;
while (std::cin.get() != '\n') {
continue;
}
}
void Waiter::show() const
{
std::cout << "Category: waiter\n";
Worker::show();
std::cout << "Panache rating: " << panache << "\n";
}
// 派生类Singer
char* Singer::pv[Vtypes] = {"other", "aito", "contralto", "soprano", "bass", "baritone", "tenor"};
Singer::Singer()
: Worker()
, voice(other)
{
}
Singer::Singer(const std::string &s, long n, int v)
: Worker(s, n)
, voice(v)
{
}
Singer::Singer(const Worker &wk, int v)
: Worker(wk)
, voice(v)
{
}
void Singer::set()
{
Worker::set();
std::cout << "Enter number of singer's vocal range:\n";
int i;
for (i = 0;i < Vtypes; ++i) {
std::cout << i << ": " << pv[i] << " ";
if (i % 4 == 3) {
std::cout << std::endl;
}
}
if (i % 4 != 0) {
std::cout << std::endl;
}
while (std::cin >> voice && (voice < 0 || voice >= Vtypes)) {
std::cout << "Please enter a value >= 0 and < " << Vtypes << std::endl;
}
while (std::cin.get() != '\n') {
continue;
}
}
void Singer::show() const
{
std::cout << "Category: singer\n";
Worker::show();
std::cout << "Vocal range: " << pv[voice] << "\n";
}
2.2.2章节的设计看起来可行,可以使用Waiter指针调用Waiter::Show()和Waiter::Set(),可以使用Singer指针调用Singer::Show()和Singer::Set()。然而假设再使用Waiter和Singer类派生出SingingWaiter类,将会产生如下问题:
根据2.2.3描述的再派生出一个派生类产生的问题,我们再深入理解,假设从Waiter类和Singer类公有派生出SingingWaiter类,如下代码所示:
// 再派生出的派生类SingingWaiter
class SingingWaiter : public Singer, public Waiter
{
// TODO: do something
};
因为Waiter类和Singer类都继承了一个抽象基类Worker,因此SingingWaiter类将包含两个Worker组件,这将引起问题。例如,通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性,如下示例代码:
SingingWaiter ed;
// 通常,这种赋值将基类指针设置为派生类对象中的基类对象的地址
Worker* pw = &ed;
由于ed对象中有两个Worker对象,有两个地址可供选择,所以应该使用类型转换来指定对象:
// 基类指针指向转换为Waiter*的地址
Worker *pw = (Waiter*)&ed;
// 基类指针指向转换为Singer*的地址
Worker *pw = (Singer*)&ed;
问题1:
上述情况,将使得使用基类指针来引用不同的对象(多态性)复杂化。包含两个Worker的对象拷贝还会导致其它问题。
真正的问题是:为什么需要Worker对象的两个拷贝?
处理1:
C++引入多重继承的同时,引入了一种新技术虚基类(virtual base class),使得多重继承成为可能。
虚基类: 虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
虚基类方式: 通过在类声明继承时使用virtual关键字,virtual与public的次序无关紧要,不分先后,习惯性写法是virtual在前,如下代码示例:
// Waiter声明为虚基类继承
class Waiter : virtual public Worker
{
// TODO: do something
};
// Singer声明为虚基类继承
class Singer : virtual public Worker
{
// TODO: do something
};
上述派生类声明后,就可以将由这两个派生类再派生出的派生类声明为如下形式:
// 1、现在SingingWaiter对象将只包含Worker对象的一个副本。
// 2、本质上讲,继承Waiter和Singer对象共享一个Worker对象,而不是各自引入自己的Worker对象副本
// 3、因为SingingWaiter现在只包含了一个Worker子对象,所以可以使用多态。
class SingingWaiter : public Waiter, public Singer
{
// TODO: do something
};
看到上面的虚基类使用方式定义,虚基类的声明及虚基类的使用后,可能会产生如下几个疑问?
疑问1: 为什么使用术语“虚”?
疑问2: 为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为多MI(多重继承)的准则呢?
疑问3: 是否存在麻烦呢?
下面根据上述的3个疑问,再来深入分析多重继承下的虚基类存在的意义:
疑问1解析:
疑问2解析:
疑问3解析:
// 基类A
class A
{
int a;
public:
A(int n = 0) : a(n) { }
// TODO: do something
// ...
};
// 派生类B
class B : public A
{
int b;
public:
B(int m = 0, int n = 0) : A(n), b(m) { }
// TODO: do something
// ...
};
// 派生类C
class C : public B
{
int c;
public:
C(int q = 0, int m = 0, int n = 0) : B(m, n), c(q) { }
// TODO: do something
// ...
};
使用非虚基类时: 上述示例代码解释说明,以便更能方便的理解使用非虚基类时的派生类信息传递给基类的传递过程,分析如下:
使用虚基类时: 则上述的信息自动传递将不起作用。如下的MI(多重继承)示例构造函数,并且对这段示例构造函数的说明放在示例代码之后,以便深入理解:
SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) {}
问题1:使用虚基类时,上述信息传递存在的问题:
自动传递信息时,将通过2条不同的途径(Waiter和Singer)将wk传递给Worker对象。
问题1解决:
// 1、本示例代码显式的调用构造函数Worker(const Worker&)
// 2、这种用法是合法的,对于虚基类必须这样做;对于非虚基类,则是非法的
SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk, p), Singer(wk, v) {}
问题1解决方式下的注意点:
如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
多重继承除了修改类构造函数规则外,MI(多重继承)通常还要求调整其它代码。
假设没有在SingingWaiter类中重新定义Show()方法,并试图使用SingingWaiter对象调用继承的Show()方法,如下使用SingingWaiter对象的示例代码:
SingingWaiter sw("Test L", 2345, 6, soprano);
sw.Show();
上述示例代码说明:
二义性问题解决方式:
SingingWaiter sw("Test L", 2345, 6, soprano);
// 使用的是Singer基类的Show()函数
sw.Singer::Show();
void SingingWaiter::Show()
{
Singer::Show();
}
对于单继承来说,让派生方法调用基类的方法是可以的;但是在多重继承中,派生类方法调用多个基类的方法,就会出现基类的基类方法被调用多次的情况。
基类的基类被调用多次的情况如何解决呢? 方式如下:
// 1、不友好的方式:递增方式
void Worker::Show() const
{
cout << "Name: " << fullname << "\n";
cout << "Employee ID: " << id << "\n";
}
void Waiter::Show() const
{
Worker::Show();
cout << "Panache rating: " << panache << "\n";
}
void Singer::Show() const
{
Worker::Show();
cout << "Vocal range: " << pv[voice] << "\n";
}
coid HeadWaiter::Show() const
{
Waiter::Show();
cout << "Presence rating: " << presence << "\n";
}
// 递增方式下,如果是这样调用就会出现调用Worker::Show()多次的情况
void SingingWaiter::Show() const
{
Waiter::Show();
Singer::Show();
}
// 2、友好的方式:模块化方式
void Worker::Data() const
{
cout << "Name: " << fullname << "\n";
cout << "Employee ID: " << id << "\n";
}
void Waiter::Data() const
{
cout << "Panache rating: " << panache << "\n";
}
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << "\n";
}
void SingingWaiter::Data() const
{
Waiter::Data();
Singer::Data();
}
// 1、模块化方式下,这样调用实现只调用一次Worker类对象的方法一次
// 2、Data()方法可只在类内部使用,作为协助公有接口的辅助方法。使Data()方法成为保护的,则只能在继承层次结构中的类中使用它,在其它地方不能使用
// 3、使Data()方法成为私有的将阻止Waiter中的代码使用Worker::Data(),这正是保护访问类的用武之地
void SingingWaiter::Show() const
{
Worker::Data();
Data();
}
// 举例如下:set()方法取得数据,以设置对象值,该方法也有类似的问题。如SingingWaiter::Set()方法应请求Worker信息一次,而不是两次。
// 1、可以使用前面提到的模块化方式解决
// 2、可以提供一个受保护的get()方法,该方法只请求一个类的信息
总结:在祖先相同时,使用MI(多重继承)必须引入虚基类,并修改构造函数初始化列表的规则。如果在编写这些类时没有考虑到MI(多重继承),则还可能需要重新编写它们。
改进多次后的最终形态示例代码如下所示,记录在此,方便后续根据前面的说明进行代码比对理解:
// .h头文件
// 说明:
// 1、该示例代码使用了多态属性,将各种类的地址赋给基类指针。
class WorkerMI
{
private:
std::string fullname;
long id;
protected:
virtual void data() const;
virtual void get();
public:
WorkerMI();
WorkerMI(const std::string& s, long n);
virtual ~WorkerMI() = 0;
virtual void set() = 0;
virtual void show() const = 0;
};
class WaiterMI : virtual public WorkerMI
{
private:
int panache;
protected:
void data() const;
void get();
public:
WaiterMI();
WaiterMI(const std::string& s, long n, int p = 0);
WaiterMI(const WorkerMI& wk, int p = 0);
void set();
void show() const;
};
class SingerMI : virtual public WorkerMI
{
protected:
enum
{
otherMI,
altoMI,
contraltoMI,
sopranoMI,
bassMI,
baritoneMI,
tenorMI,
};
enum
{
VtypesMI = 7,
};
void data() const;
void get();
private:
static char* pvmi[VtypesMI];
int voice;
public:
SingerMI();
SingerMI(const std::string& s, long n, int v = otherMI);
SingerMI(const WorkerMI& wk, int v = otherMI);
void set();
void show() const;
};
class SingingWaiterMI : public SingerMI, public WaiterMI
{
protected:
void data() const;
void get();
public:
SingingWaiterMI();
SingingWaiterMI(const std::string& s, long n, int p = 0, int v = otherMI);
SingingWaiterMI(const WorkerMI& wk, int p = 0, int v = otherMI);
SingingWaiterMI(const WaiterMI& wk, int v = otherMI);
SingingWaiterMI(const SingerMI& wk, int p = 0);
virtual ~SingingWaiterMI();
void set();
void show() const;
};
// .cpp源文件
WorkerMI::WorkerMI()
: fullname("no one")
, id(0L)
{
}
WorkerMI::WorkerMI(const std::string &s, long n)
: fullname(s)
, id(n)
{
}
WorkerMI::~WorkerMI()
{
}
void WorkerMI::data() const
{
std::cout << "Name: " << fullname << std::endl;
std::cout << "Employee ID: " << id << std::endl;
}
void WorkerMI::get()
{
getline(std::cin, fullname);
std::cout << "Enter worker's ID: ";
std::cin >> id;
while (std::cin.get() != '\n')
continue;
}
// WaiterMI
WaiterMI::WaiterMI()
: WorkerMI()
, panache(0)
{
}
WaiterMI::WaiterMI(const std::string &s, long n, int p)
: WorkerMI(s, n)
, panache(p)
{
}
WaiterMI::WaiterMI(const WorkerMI &wk, int p)
: WorkerMI(wk)
, panache(p)
{
}
void WaiterMI::data() const
{
std::cout << "Panache rating: " << panache << std::endl;
}
void WaiterMI::get()
{
std::cout << "Enter waiter's panache rating: ";
std::cin >> panache;
while (std::cin.get() != '\n')
continue;
}
void WaiterMI::set()
{
std::cout << "Enter waiter's name: ";
WorkerMI::get();
get();
}
void WaiterMI::show() const
{
std::cout << "Category: waiter\n";
WorkerMI::data();
data();
}
// SingerMI
// 派生类Singer
char* SingerMI::pvmi[VtypesMI] = {"other", "aito", "contralto", "soprano", "bass", "baritone", "tenor"};
SingerMI::SingerMI()
: WorkerMI()
, voice(otherMI)
{
}
SingerMI::SingerMI(const std::string &s, long n, int v)
: WorkerMI(s, n)
, voice(v)
{
}
SingerMI::SingerMI(const WorkerMI &wk, int v)
: WorkerMI(wk)
, voice(v)
{
}
void SingerMI::data() const
{
std::cout << "Vocal range: " << pvmi[voice] << std::endl;
}
void SingerMI::get()
{
std::cout << "Enter number for singer's vocal range: ";
int i;
for (i = 0;i < VtypesMI; ++i) {
std::cout << i << ": " << pvmi[i] << " ";
if (i % 4 == 3) {
std::cout << std::endl;
}
}
if (i % 4 != 0) {
std::cout << std::endl;
}
std::cin >> voice;
while (std::cin.get() != '\n') {
continue;
}
}
void SingerMI::set()
{
std::cout << "Enter singer's name: ";
WorkerMI::get();
get();
}
void SingerMI::show() const
{
std::cout << "Category: singer\n";
WorkerMI::data();
data();
}
// SingingWaiterMI
SingingWaiterMI::SingingWaiterMI()
{
}
SingingWaiterMI::SingingWaiterMI(const std::string &s, long n, int p, int v)
: WorkerMI(s, n)
, WaiterMI(s, n, p)
, SingerMI(s, n, v)
{
}
SingingWaiterMI::SingingWaiterMI(const WorkerMI &wk, int p, int v)
: WorkerMI(wk)
, WaiterMI(wk, p)
, SingerMI(wk, v)
{
}
SingingWaiterMI::SingingWaiterMI(const WaiterMI &wt, int v)
: WorkerMI(wt)
, WaiterMI(wt)
, SingerMI(wt, v)
{
}
SingingWaiterMI::SingingWaiterMI(const SingerMI &sg, int p)
: WorkerMI(sg)
, WaiterMI(sg, p)
, SingerMI(sg)
{
}
SingingWaiterMI::~SingingWaiterMI()
{
}
void SingingWaiterMI::data() const
{
SingerMI::data();
WaiterMI::data();
}
void SingingWaiterMI::get()
{
WaiterMI::get();
SingerMI::get();
}
void SingingWaiterMI::set()
{
std::cout << "Enter singer waiter's name: ";
WorkerMI::get();
get();
}
void SingingWaiterMI::show() const
{
std::cout << "Category: singing waiter\n";
WorkerMI::data();
data();
}
通过多种途径继承一个基类的派生类情况:
// 举例说明虚基类和非虚基类混合时的继承情况
// 1、假设B类时C类和D类的虚基类
// 2、假设B类同时是X类和Y类的非虚基类
// 3、类M是从C、D、X和Y派生而来的
// 4、这种情况下类M从虚派生祖先(C类和D类)那继承了一个B类子对象
// 5、这种情况下类M从非虚派生祖先(即X类和Y类)分别继承了一个B类子对象,即2个B类子对象
如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。
但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于其它所有名称,则使用它时,即使不使用限定符,也不会导致二义性。
问题1: 一个成员名如何优先于另一个成员名呢?
解决方式1: 派生类中的名称优先于直接或间接祖先类中的相同名称。如下示例代码:
class B
{
public:
short ();
// TODO: do something
// ...
};
// 1、类C中的q()函数定义优先于类B中的q()定义,因为类C是从类B派生而来的。
// 2、因此,类F中的方法可以使用q()来表示C::q()
class C : virtual public B
{
public:
long q();
ing omg();
// TODO: do something
// ...
};
class D : public C
{
// TODO: do something
// ...
};
class E : virtual public B
{
private:
int omg();
// TODO: do something
// ...
};
// 3、任何一个omg()定义都不优先于其他omg()定义,因为C和E都不是对方的基类。在F中使用非限定的omg()将导致二义性
// 4、虚二义性规则与访问规则无关,也就是说,即使E::omg()是私有的,不能在F类中直接访问,但是使用omg()仍将导致二义性
// 5即使C::q()是私有的,它将优先于D::q()。这种情况,可以在类F中调用B::q(),如果不限定q(),则将意味着要调用不可访问的C::q()
class F : public D, public E
{
// TODO: do something
// ...
};
1、不使用虚基类
(1) 不使用虚基类的MI(多重继承)不会引入新的规则。
(2) 如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。
(3) 举例,从GunSlinger和PokerPlayer派生而来的BadDude类中,将分别使用GunSlinger::draw()和PokerPlayer::draw()来区分从这两个类那里继承的draw()方法。否则,编译器将指出二义性。
2、使用虚基类
(1) 如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。通常情况下,多个基类实例都是问题。
(2) 下面是一个使用虚基类的MI(多重继承)。当派生类使用关键字virtual来指示派生时,基类就成为虚基类。
class marketing : public virtual reality
{
// TODO: do something
}
(3) 主要变化(同时也是使用虚基类的原因)是,从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其它要求:
MI(多重继承)会增加编程的复杂度。然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。