1. 多重继承
MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系。
例如,可以从 Waiter类和 Singer类派生出 SingingWaiter类
class SingingWaiter : public Waiter, public Singer {
};
请注意,必须使用关键字 public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生:
class SingingWaiter : public Waiter, Singer {
}; // Singer is a private base
正如本章前面讨论的,私有MI和保护MI可以表示has-a关系。 Student类的 studenti.h实现就是这样的示例。
下面将重点介绍公有MI。
MI可能会给程序员带来很多新问题。其中两个主要的问题是:
- 从两个不同的基类继承同名方法:
- 从两个或更多相关基类那里继承同一个类的多个实例。
为解决这些问题,需要使用一些新规则和不同的语法,因此,与使用单继承相比,使用MI更困难,也更容易出现问题。由于这个原因,很多C+用户强烈反对使用MI,一些人甚至希望删除MI:而喜欢MI的人则认为,对一些特殊的工程来说,MI很有用,甚至是必不可少的;也有一些人建议谨慎、适度地使用MI。
下面来看一个例子,并介绍有哪些问题以及如何解决它们。要使用MI,需要几个类。
我们将定义抽象基类 Worker.,并使用它派生出 Waiter 类和 Singer 类。然后,便可以使用MI从 Waiter类和Singer类派生出 SingingWaiter类(参见图14.3)。
这里使用两个独立的派生来使基类( Worker)被继承,这将导致Ml的大多数麻烦。
首先声明 Worker、 Waiter和 Singer类,如程序清单14.7所示。
程序清单14.7 worker0.h
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// worker0.h -- working classes
#ifndef WORKER0_H_
#define WORKER0_H_
#include <string>
class Worker // an abstract base class
{
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; // pure virtual destructor
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]; // 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;
};
#endif
程序清单14.7的类声明中包含一些表示声音类型的内部常量。一个枚举用符号常量alto、 contralto等表示声音类型,静态数组pv存储了指向相应C-风格字符串的指针,程序清单14.8初始化了该数组,并提供了方法的定义.
程序清单14.8 worker0.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// worker0.cpp -- working class methods
#include "worker0.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// Worker methods
// must implement virtual destructor, even if pure
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";
}
// Waiter methods
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";
}
// Singer methods
char * Singer::pv[] = {
"other", "alto", "contralto",
"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 range: " << pv[voice] << endl;
}
程序清单14.9是一个简短的程序,它使用一个多态指针数组对这些类进行了测试。
程序清单14.9 worktest.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// worktest.cpp -- test worker class hierarchy
#include <iostream>
#include "worker0.h"
const int LIM = 4;
using std::endl;
using std::cout;
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};
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
int i;
for (i = 2; i < LIM; i++)
pw[i]->Set();
for (i = 0; i < LIM; i++)
{
pw[i]->Show();
std::cout << std::endl;
}
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
// std::cin.get();
return 0;
}
程序的运行结果:
meng-yue@ubuntu:~/MengYue/c++/code_reuse/03$ ./worktest
---------------开始--->公众号:梦悦foundation---------------
Enter worker's name: meng
Enter worker's ID: 1234
Enter waiter's panache rating: 2
Enter worker's name: yue
Enter worker's ID: 4321
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
2
Category: waiter
Name: Bob Apple
Employee ID: 314
Panache rating: 5
Category: singer
Name: Beverly Hills
Employee ID: 522
Vocal range: soprano
Category: waiter
Name: meng
Employee ID: 1234
Panache rating: 2
Category: singer
Name: yue
Employee ID: 4321
Vocal range: contralto
---------------结束--->公众号:梦悦foundation---------------
meng-yue@ubuntu:~/MengYue/c++/code_reuse/03$
这种设计看起来是可行的:使用Waiter指针来调用 Waiter::Show()和 Waiter::Set(); 使用 Singer指针来调用 Singer::Show()和 Singer::Set()。
然后,如果添加一个从 Singer 和 Waiter类派生出的 SingingWaiter类后,将带来一些问题。具体地说,将出现以下问题.
- 有多少 Worker?
- 哪个方法?
2. 有多少 Worker
假设首先从 Singer和 Waiter公有派生出 SingingWaiter:
class SingingWaiter: public Singer, public Waiter {
};
因为 Singer和Waiter都继承了一个 Worker组件,因此 SingingWaiter 将包含两个 Worker 组件(参见图14.4)。
正如预期的,这将引起问题。例如,通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:
SingingWaiter ed;
Worker *pw = &ed; //ambiguous
通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但ed中包含两个 Worker对象, 有两个地址可供选择,所以应使用类型转换来指定对象:
Worker *pw1 = (Waiter *) &ed; //the Worker in Waiter
Worker *pw2 = (Singer *) &ed; // the Worker in Singer
这将使得使用基类指针来引用不同的对象(多态性)复杂化。
包含两个 Worker对象拷贝还会导致其他的问题。然而,真正的问题是:为什么需要 Worker对象的两个拷贝?唱歌的侍者和其他 Worker对象一样,也应只包含一个姓名和一个ID。
C++引入多重继承的同时,引入了一种新技术一一虚基类( virtual base class),使MI成为可能
3. 虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
例如,通过在类声明中使用关键字 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子对象,所以可以使用多态
您可能会有这样的疑问:
- 为什么使用术语“虚”?
- 为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为多MI的准则呢?
- 是否存在麻烦呢?
首先,为什么使用术语虚?毕竟,在虚函数和虚基类之间并不存在明显的联系。C++用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。例如,如果新关键字与重要程序中的重要函数或变量的名称相同,这将非常麻烦。因此,C++对这种新特性也使用关键字 virtual ——有点像关键字重载。
其次,为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为MI的准则呢?
- 第一,在一些情况下,可能需要基类的多个拷贝;
- 第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应当的;
- 第三,这样做有其缺点,将在下一段介绍。
最后,是否存在麻烦? 是的。
为使虚基类能够工作,需要对C+规则进行调整,必须以不同的方式编写一些代码。另外,使用虛基类还可能需要修改已有的代码。例如,将 SingingWaiter类添加到 Worker集成层次中时,需要在 Singer和 Waiter类中添加关键字 virtual
4. 新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。
对于非虚基类,唯一可以出现在初始化列表中的构造函数是基类构造函数。
但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:
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(1) {
}
};
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和 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&)。请注意,这种用法是合法的,对于虚基类,
必须这样做;但对于非虚基类,则是非法的。
警告:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
5. 哪个方法
除了修改类构造函数规则外,MI通常还要求调整其他代码。假设要在 SingingWaiter类中扩展Show()方法。
因为 SingingWaiter对象没有新的数据成员,所以可能会认为它只需使用继承的方法即可。这引出了第一个问题。假设没有在 SingingWaiter类中重新定义Show()方法,并试图使用 SingingWaiter对象调用继承的Show()方法
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Show(); // ambiguous
对于单继承,如果没有重新定义Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖
先都有一个Show()函数,这使得上述调用是二义性的。
警告:多重继承可能导致函数调用的二义性。例如, BadDude类可能从 Gunslinger类和 PokerPlayer类那里继承两个完全不同的Draw()方法。
可以使用作用域解析运算符来澄清编程者的意图
SingingWaiternewhire("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
{
cout << "Name: " << fullname << "\n";
cout << " Employee ID: " << id << "\n";
}
void Waiter::Show() const
{
Worker::Show();
cout << "Panache rating: "<< panache < "\n";
}
void Headwaiter::Show() const
{
Waiter::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 << "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
{
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
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// 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:
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.11 workermi.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// 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
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.12提供了测试代码。注意,该程序使用了多态属性,
将各种类的地址赋给基类指针。另外,该程序还在下面的检测中使用了C-风格字符串库函数 strchr();
while (strchr("wstq",choice) == NULL)
该函数返回参数 choice指定的字符在字符串“wsq”中第一次出现的地址,如果没有这样的字符,则返回NULL指针。使用这种检测比使用if语句将 choice指定的字符同每个字符进行比较简单.
程序清单14.12 workmi.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// 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];
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
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();
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
return 0;
}
程序运行结果:
meng-yue@ubuntu:~/MengYue/c++/code_reuse/03$ ./workmi
---------------开始--->公众号:梦悦foundation---------------
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
w
Enter waiter's name: meng
Enter worker's ID: 1234
Enter waiter's panache rating: 4
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
s
Enter singer's name: yue
Enter worker's ID: 4321
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
5
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
t
Enter singing waiter's name: foundation
Enter worker's ID: 6666
Enter waiter's panache rating: 5
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
6
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
q
Here is your staff:
Category: waiter
Name: meng
Employee ID: 1234
Panache rating: 4
Category: singer
Name: yue
Employee ID: 4321
Vocal range: baritone
Category: singing waiter
Name: foundation
Employee ID: 6666
Vocal range: tenor
Panache rating: 5
Bye.
---------------结束--->公众号:梦悦foundation---------------
meng-yue@ubuntu:~/MengYue/c++/code_reuse/03$
下面介绍其他一些有关MI的问题。
6. 混合使用虚基类和非虚基类
再来看一下通过多种途径继承一个基类的派生类的情况。
如果基类是虛基类,派生类将包含基类的一个子对象:
如果基类不是虚基类,派生类将包含多个子对象。
当虚基类和非虚基类混合时,情况将如何呢?
例如,假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类,而类M是从C、D、X和Y派生而来的。
在这种情况下,类M从虚派生祖先(即类C和D)那里共继承了一个B类子对象,并从每一个非虚派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含三个B类子对象。
当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
7. 虚基类和支配
使用虚基类将改变C+解析二义性的方式。
使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性.
但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于( dominates)其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。
那么,一个成员名如何优先于另一个成员名呢?派生类中的名称优先于直接或间接祖先类中的相同名称。例如,在下面的定义中:
class B
{
public:
short q();
};
class C : virtual public B
{
public:
long q();
int omg();
};
class D : public C
{
};
class E : virtual public B
{
private:
int omg();
};
class F : public D, public E
{
};
类C中的q()定义优先于类B中的q()定义,因为类C是从类B派生而来的。因此,F中的方法可以 使用q()来表示C::q()。
另一方面,任何一个omg()定义都不优先于其他omg()定义,因为C和E都不是
对方的基类。所以,在F中使用非限定的omg()将导致二义性。
虚二义性规则与访问规则无关,也就是说,即使E::omg()是私有的,不能在F类中直接访问,但使用omg()仍将导致二义性。同样,即使C::q()是私有的,它也将优先于D::q()。在这种情况下,可以在类F中调用B::q(),但如果不限定q(),则将意味着要调用不可访问的C::q()。
8. MI小结
首先复习一下不使用虚基类的MI。这种形式的MI不会引入新的规则。然而,如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。
即在从 GunSlinger和 PokerPlayer派生而来的 BadDude类中,将分别使用 Gunslinger::draw()和 Pokerplayer::draw()来区分从这两个类那里继承的draw()方法。否则,编译器将指出二义性。
如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。在某些情况下,这可能正是所希望的,但通常情况下,多个基类实例都是问题。
接下来看一看使用虚基类的MI。当派生类使用关键字 virtual来指示派生时,基类就成为虚基类:
class marketing : public virtual reality {
};
主要变化(同时也是使用虚基类的原因)是,从虚基类的一个或多个实例派生而来的类将只继承了个基类对象。为实现这种特性,必须满足其他要求:
-
有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的.
-
通过优先规则解决名称二义性
正如您看到的,MI会增加编程的复杂程度。然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。避免这种情况后,唯一需要注意的是,在必要时对继承的名称进行限定.