多重继承(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. POI生成EXCEL,公式不自动执行的有关问题

    POI生成EXCEL,公式不自动执行的问题 场景:POI读取Excel模板. 当使用POI操作Excel时,发现由POI生成的公式能够在打开Excel是被执行, 而事先手工写入Excel模板文件的公式 ...

  2. 轻松学习之Linux教程二 一览纵山小:Linux操作系统具体解释

    本系列文章由@uid=hpw" style="padding:0px; margin:0px; color:rgb(255,0,0); text-decoration:none&q ...

  3. 也许,这样理解HTTPS更容易(今天看到的, 对https总结最好的一篇)

    摘要:本文尝试一步步还原HTTPS的设计过程,以理解为什么HTTPS最终会是这副模样.但是这并不代表HTTPS的真实设计过程.在阅读本文时,你可以尝试放下已有的对HTTPS的理解,这样更利于" ...

  4. 网络配置vlan

    1. # This file describes the network interfaces available on your system # and how to activate them. ...

  5. 上手并过渡到PHP7(2)——必须传递int, string, bool参数?没问题

    Type hints, Type safe 泊学实操视频 泊学原文链接PHP 7中最引人注目的新特性之一,无疑是Scalar type hints.我们可以在函数参数和返回值中使用scalar typ ...

  6. Android Studion的Monitor中显示No Debuggable Application的解决方法

    在使用Android Studion的时候,突然android Monitor中无法下拉显示调试项目,只是一直提示No Debuggable Application,然后上网搜索的解决办法: 第一种方 ...

  7. java基础复习二——面向对象一

    面向对象三大特性:封装,继承,多态 类:对象的蓝图,生成对象的模板,是对一类事物的描述,是抽象的概念上的定义 对象:是实际存在的该类事物的每个个体,也称为实例 类之间三种关系:依赖关系(uses-a) ...

  8. SecureCRT工具

    技巧收集: 文本文件内容 复制该行内容yy,p粘贴 2+yy复制两行 dd 删除该行 文件内容搜索 非编辑状态/+查找内容 查找指定行 :+行号

  9. Omnigraffle快捷键

    cmd+shift+. 和 cmd+shift+,  放大缩小 按住z点击鼠标是放大, z+Option是缩小 按住command双指推移 缩放 按住command,旋转物体 按住 option缩放 ...

  10. 用 #include “filename.h” 格式来引用非标准库的头文件

    用 #include “filename.h” 格式来引用非标准库的头文件(编译器将 从用户的工作目录开始搜索) #include <iostream> /* run this progr ...