C++Primer Plus 第十四章代码重用:多重继承14.3,有多少个Worker

系列文章目录

提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:有多少个Worker


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档


14.3.1 有多少 Worker

假设首先从 Singer和Waiter 公有派生出 SingingWaiter:class SingingWaiter:public singer,public waiter(...);
因为 Singer 和 Waiter 都继承了一个 Worker 组件,因此 SingingWaiter将包含两个 Worker 组件(参见图 14.4)
正如预期的,这将引起问题。例如,通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:

SingingWaiter ed;
Worker *pw=&ed;//.ambiquous

通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但ed中包含两个 Worker 对象,有两个地址可供选择,所以应使用类型转换来指定对象:

Worker *pw1 =(Waiter *)&ed;// the worker in waiter
Worker *pw2=(Singer *)&ed;//the worker in singer

在这里插入图片描述

这将使得使用基类指针来引用不同的对象(多态性)复杂化。包含两个 Worker 对象拷贝还会导致其他的问题。然而,真正的问题是:为什么需要 Worker 对象的两个拷贝?唱歌的侍者和其他 Worker 对象一样,也应只包含一个姓名和一个ID。C++引入多重继承的同时,引入了一种新技术–虚基类(virtualbase 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{...};

现在,SingingWaiter 对象将只包含 Worker 对象的一个副本。从本质上说,继承的 Singer 和 Waiter 对象共享一个 Worker 对象,而不是各自引入自己的 Worker 对象副本(请参见图14.5)。因为 SingingWaiter现在只包含了一个 Worker 子对象,所以可以使用多态。
您可能会有这样的疑问:

  • 为什么使用术语“虚”?
  • 为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为多M的准则呢?
  • 是否存在麻烦呢?
    首先,为什么使用术语虚?毕竟,在虚函数和虚基类之间并不存在明显的联系。C++用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。例如,如果新关键字与重要程序中的重要函数或变量的名称相同,这将非常麻烦。因此,C++对这种新特性也使用关键字 virtual–有点像关键字重载。
    在这里插入图片描述
    其次,为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为M的准则呢?第一,在一些情况下,可能需要基类的多个拷贝;第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应当的:第三,这样做有其缺点,将在下一段介绍。最后,是否存在麻烦?是的。为使虚基类能够工作,需要对C++规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码。例如,将SingingWaiter 类添加到 Worker 集成层次中时,需要在 Singer 和 Waiter 类中添加关键字 virtual。

2.新的构造函数规则

使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:

class A
{
	int a;
	public :
	A(int n=0):a(n){}
};
class B:public A
{
	int b;
public :
	B(intm=0,intn=0):A(n)b(m)
}

:class C:public B
{
	int c;
public :
	C(intq=0,intm=0,intn=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和Simger)将wk传递给 Worker 对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员 panache 和 voice,但 wk参数中的信息将不会传递给子对象 Waiter。然而,编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker 的默认构造函数。如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该是这样:

SingingWaiter(const Worker &wk,intp=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()函数,这使得上述调用是二义性的。
警告:多重继承可能导致函数调用的二义性。例如,BadDude 类可能从 Gunslinger 类和 PokerPlayer 类那里继承两个完全不同的 Draw( )方法。
可以使用作用域解析运算符来澄清编程者的意图:

SingingWaiter newhire("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
"Name:"<< fullname << "\n";Cout <<
cout <<"Employee ID:"<< id << "\n";
void Waiter::Show()const
Worker::Show();
cout<<"Panache rating:"<<panache << "\n";
void HeadWaiter::Show()constWaiter::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 <s"Name:"sfullname <n":
cout <<"Employee ID:"<< id << "\n";
void Waiter::Data()const
cout<<"Panache rating:"<<panache <<"\n";
void Singer::Data()const
cout <<"Vocalrange:"<< 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

// 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:
    const 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.12 workmi.cpp

// 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

const 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.11 workermi.cpp

// 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];

    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();
    return 0; 
}

当然,好奇心要求我们测试这些类,程序清单14.12提供了测试代码。注意,该程序使用了多态属性,将各种类的地址赋给基类指针。另外,该程序还在下面的检测中使用了C-风格字符串库函数 strchr():while(strchr(“wstq”,choice)==NULL)
该函数返回参数 choice 指定的字符在字符串“wstq”中第一次出现的地址,如果没有这样的字符,则返回 NULL指针。使用这种检测比使用if语句将choice 指定的字符同每个字符进行比较简单。请将程序清单14.12与workermi.cpp 一起编译。


  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值