多重继承(MI)描述的是有多个直接基类的类。与单继承一样,共有MI表示的也是is-a关系。例如,可以从Awiter类和Singer类派生出SingingWaiter类:
class SingingWaiter : public Waiter, public Singer {...};
MI可能会给程序员带来很多新问题。其中两个主要的问题是:从两个不同的基类继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例。
在下面的例子中,我们将定义一个抽象基类Worker,并使用它派生出Waiter类和Singer类。然后,使用MI从Waiter类和Singer类派生出SingingWaiter类。
程序清单14.7 worker0.h

// 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() = ; // pure virtual destructor
virtual void Set();
virtual void Show() const;
}; class Waiter : public Worker
{
private:
int panache;
public:
Waiter() : Worker(), panache() {}
Waiter(const std::string & s, long n, int p = )
: Worker(s, n), panache(p) {}
Waiter(const Worker & wk, int p = )
: Worker(wk), panache(p) {}
void Set();
void Show() const;
}; class Singer : public Worker
{
protected:
enum {other, alto, contralto, soprano,
bass, baritone, tenor};
enum {VTypes = };
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 // WORKER0_H_

程序清单14.7的类声明中包含一些表示声音类型的内部变量。一个枚举类型符号常量alto、contralto等表示声音类型,静态数组pv存储了指向相应C-风格字符串的指针,程序清单14.8初始化了该数组,并提供了方法的定义。
程序清单14.8 worker0.cpp

// 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 = ; i < VTypes; i ++)
{
cout << i << ": " << pv[i] << " ";
if (i % == )
cout << endl;
}
if (i % != )
cout << endl;
while (cin >> voice && (voice < || 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

// worktest.cpp -- test worker class hierarchy
#include <iostream>
#include "worker0.h"
const int LIM = ;
int main()
{
Waiter bob("Bob Apple", 314L, );
Singer bev("Beverly Hills", 522L, );
Waiter w_temp;
Singer s_temp; Worker * pw[LIM] = {&bob, &bev, &w_temp, &s_temp}; int i;
for (i = ; i < LIM; i ++)
pw[i]->Set();
for (i = ; i < LIM; i ++)
{
pw[i]->Show();
std::cout << std::endl;
} return ;
}

效果:

Enter worker's name: Waldo Dropmaster
Enter worker's ID: 442
Enter waiter's panache rating: 3
Enter worker's name: Sylvis Sirenne
Enter worker's ID: 555
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
3
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: Waldo Dropmaster
Employee ID: 442
Panache rating: 3 Category: singer
Name: Sylvis Sirenne
Employee ID: 555
Vocal range: soprano

这种设计看起来是可行的:使用Waiter指针来调用Waiter::Show()和Waiter::Set();使用Singer指针来调用Singer::Show()和Singer::Set()。然后,如果添加一个从Singer和Waiter类派生出的SingingWaiter类后,将带来一些问题。具体地说,将出现以下问题。
* 有多少Worker?
* 哪个方法?
14.3.1 有多少Worker
假设首先从Singer和Waiter共有派生出SingingWaiter:
class SingingWaiter: public Singer, public Waiter {...};
因为Singer和Waiter都继承了一个Worker组件,因此SingingWaiter将包含两个Worker组件,这将引起问题。例如,通常可以将派生类对象的地址赋给基类指针,单现在将出现二义性:
SingingWaiter ed;
Worker * pw = &ed;    // ambiguous
通常,这种赋值把基类指针设置为派生类对象中的基类对象的地址。但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象:
Worker * pw1 = (Waiter *) &ed;    // the Worker in Waiter
Worker * pw2 = (Singer *) &ed;    // the Worker in Singer
C++引入多重继承的同时引入了虚基类(virtual base 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 {...};
现在,SingerWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本。因为SingingWaiter现在值包含了一个Worker子对像,所以可以使用多态。
2.新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即使基类构造函数。但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:
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) {} // 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 &)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。
注:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
14.3.2 哪个方法
除了修改类构造函数规则外,MI通常还要求调整其他代码。假设要在SingingWaiter类中扩展Show()方法。因为SingingWaiter对象没有新的数据称源,所以可能会认为它只需使用继承的方法即可。这引出了第一个问题。假设没有在SingingWaiter类中重新定义Show()方法,并试图使用SingingWaiter对象调用继承的Show()方法:
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Show();    // ambiguous
对于单继承,如果没有重新定义Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个Show()函数,这使得上述调用是二义性的。
(1)可以使用作用域解析运算符来澄清编程者的意图:
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Singer::Show();    // use Singer version
(2)更好的方法是在SingerWaiter中重新定义Show(),并指出要使用哪个Show()。例如,如果希望SingingWaiter对象使用Singer版本的Show(),则可以这样做:
void SingingWaiter::Show()
{
    Singer::Show();
}
(3)另一种办法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地空置对数据的访问。

其他一些有关MI的问题:
1.混合使用虚基类和非虚基类
……
2.虚基类和支配
……

14.3.3 MI小结
使用虚基类的MI:当派生类使用关键字irtual来指示派生时,基类就称为虚基类:
class marketing : public virtual reality { ... };
主要变化(同时也是使用虚基类的原因)是:从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其他要求:
* 有简介虚基类的派生类包含直接调用简介基类构造函数的构造函数,这对于简介非虚基类来说是非法的;
* 通过有限规则解决名称二义性。

在必要时对继承的名称进行限定。

《C++ Primer Plus》14.3 多重继承 学习笔记的更多相关文章

  1. C++ 11 从C++ primer第五版的学习笔记

    1. auto (page107) auto 推断会忽略const   const int ci = i, & cr = ci; auto b = ci; // b is an int (to ...

  2. C Primer Plus(第五版)学习笔记-可变宏:...和__VA_ARGS__

    一 .__VA_ARGS__ P454 所讲printf()这些输出函数的参数是可变的,在调试程序时,可能希望定义参数为可变的输出函数, 那么可变参数宏会是一个选择,例如: #define DEBUG ...

  3. 1.14(java学习笔记)数组

    假如我们需要用到1000个相同类型的数据,肯定不可能创建1000个变量, 这样既不方便,也不直观,也不便于我们使用.这时就需要用到数组. 一.数组的声明与使用 public class Array { ...

  4. 【Python】2.14&2.15学习笔记 运算符与表达式

    太爽了,今天可以尽情熬夜了,明天不上课,可以学一整天\(Python\) 运算符 \(+,-,*,%\)就不说了,说几个和\(c\)不太一样的 除法 print( 5/3 ) 输出了\(1.66666 ...

  5. C++学习笔记(3)

    本学习笔记是C++ primer plus(第六版)学习笔记.是C++学习笔记(2)的后续.复习C++基础知识的可以瞄瞄. 转载请注明出处http://www.cnblogs.com/zrtqsk/p ...

  6. C++学习笔记(2)

    本学习笔记是C++ primer plus(第六版)学习笔记.是C++学习笔记(1)的后续.复习C++基础知识的可以瞄瞄. 转载请注明出处http://www.cnblogs.com/zrtqsk/p ...

  7. C++学习笔记(1)

    本学习笔记是C++ primer plus(第六版)学习笔记.复习C++基础知识的可以瞄瞄. 转载请注明出处http://www.cnblogs.com/zrtqsk/p/3874148.html,谢 ...

  8. C++学习基础十六-- 函数学习笔记

    C++ Primer 第七章-函数学习笔记 一步一个脚印.循序渐进的学习. 一.参数传递 每次调用函数时,都会重新创建函数所有的形参,此时所传递的实参将会初始化对应的形参. 「如果形参是非引用类型,则 ...

  9. Linux命令学习笔记目录

    Linux命令学习笔记目录 最近正在使用,linux,顺便将用到的命令整理了一下. 一. 文件目录操作命令: 0.linux命令学习笔记(0):man 命令 1.linux命令学习笔记(1):ls命令 ...

随机推荐

  1. 从c++看js的那些概念

    感觉c++逐渐没落了,js正风生水起,各种概念层出不穷. reactjs,flux ,angularjs cmd,amd模块化设计. 初看起来好像很高端的样子,后来发现也不过是如此. c++毕竟经历了 ...

  2. VMware 14 Pro 安装 CentOS 7

    今年准备好好学习一下.NET CORE了,那也是得学习Linux.然后又得学习更多,咬着牙干吧... 1.Vmware虚拟机安装 在windows平台,首先咱们得先安装Vmware虚拟机,步骤省略,一 ...

  3. FastCGI 进程管理器(FPM)

    FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的. 它的功能包括: 支持平滑停止/启动的高级进程管理功能: 可以工作于不同的 uid/ ...

  4. 【这特么是个坑。。。】iOS 10.3下解决Charles抓包ssl证书信任问题

    针对近期iOS 10.3以上的系统charles抓https信任问题 前言 最近iPhone系统更新到ios 10.3后,在公司里用Charles抓包竟然出现了一些问题,https的请求都会失败,提示 ...

  5. chrome 常用快捷键(可以摆脱鼠标哦)

      Ctrl+N                                打开新窗口. Ctrl+T                                打开新标签页. Ctrl+Sh ...

  6. python + opencv: kalman 跟踪

    之前博文中讲解过kalman滤波的原理和应用,这里用一个跟踪鼠标的例程来演示怎么在opencv里用自带的kalman函数进行目标跟踪,文章的内容对做图像跟踪有借鉴意义.文章主要是网络资源进行整理和简单 ...

  7. Runtime是什么?

    在看 RPC 的概念模型与实现解析 的时候,看到图片上有Runtime,又想到见过很多Runtime之类的东西,所以就想弄明白这到底是个什么东西. (因为是程序名,所以根本没想到代码的“编译-运行”~ ...

  8. Ubuntu 12.04 root用户登录设置

    Ubuntu 12.04默认是不允许root登录的,在登录窗口只能看到普通用户和访客登录.以普通身份登陆Ubuntu后我们需要做一些修改,普通用户登录后,修改系统配置文件需要切换到超级用户模式,在终端 ...

  9. 模式识别之线性判别---naive bayes朴素贝叶斯代码实现

    http://blog.csdn.net/xceman1997/article/details/7955349 http://www.cnblogs.com/yuyang-DataAnalysis/a ...

  10. Spring部署报错:Could not open ServletContext resource [/db.properties]

    在使用Spring MVC过程中,部署项目报错,报错信息如下: 八月 15, 2016 5:02:04 下午 org.apache.catalina.core.StandardContext list ...