类继承(五) 多重继承
多重继承(MI)指的是有多个直接基类的继承。和单继承一样,公有MI表示的也是 is-a 关系。例如我们可以从Waiter
和Singer
派生出SingingWaiter
:
class SingingWaiter :public Waiter,public Singer{...};
这里这个关键词public
必须每个基类都要用,如果不加修饰词,默认是私有继承。
在之前我提过使用私有MI和保护MI可以实现 has-a 关系。但是我们重点是公有MI。
MI可能会给程序员带来很多问题。主要的问题是:从不同的基类继承同名方法;从两个或者更多相关基类那里继承同一个类的多个实例。为解决这些问题,需要使用一些新规则和不同的语法。MI比单继承困难许多。很多C++用户强烈反对使用MI,有一些人甚至希望删除MI。
1. 一个例子
设计一个抽象基类
Worker
,然后从中派生出Waiter
和Singer
。然后我们将这两个类使用公有MI派生出SingingWaiter
。
如下是从Worker
中派生出Waiter
和Singer
的代码
//多重继承1.h
#ifndef WORKER0_H_
#define WORERK0_H_
#include<string>
using std::string;
class Worker
{
private:
string fullname;
long id;
public:
Worker():fullname("no one"),id(0l){}
Worker(const string & s,long i):fullname(s),id(i){}
virtual ~Worker(){};
virtual void Set()=0;
virtual void Show() const;
};
class Waiter:public Worker
{
private:
int panache;
public:
Waiter():Worker(),panache(0){}
Waiter(const string &s,long i,int p):Worker(s,i),panache(p){}
virtual void Set();
virtual void Show() const;
};
class Singer:public Worker
{
protected:
enum{other,alto,contralto,soprano,bass,baritone,tenor};
enum{Vtypes=7};
private:
static const char * pv[Vtypes];
int voice;
public:
Singer():Worker(),voice(other){}
Singer(const string &s,long i,int v):Worker(s,i),voice(v){}
virtual void Set();
virtual void Show() const;
};
#endif
//多重继承1.cpp
#include"多重继承1.h"
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
void Worker::Set()
{
cout<<"Enter worker's name: ";
std::getline(cin,fullname);
cout<<"Worker's id: ";
cin>>id;
cin.ignore();
}
void Worker::Show() const
{
cout<<"Name: "<<fullname<<endl;
cout<<"ID: "<<id<<endl;
}
void Waiter::Set()
{
Worker::Set();
cout<<"Enter the waiter's panache rating: ";
cin>>panache;
cin.ignore();
}
void Waiter::Show()const
{
cout<<"Category: waiter"<<endl;
Worker::Show();
cout<<"Panache rating: "<<panache<<endl;
}
const char* Singer::pv[Vtypes]={"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+1)%4==0)
cout<<endl;
}
if(Vtypes%4!=0)
cout<<endl;
while (!(cin>>voice) || (voice<0 || voice>=Vtypes))
{
cout<<"Please enter a value >=0 and < "<<Vtypes<<endl;
}
cin.ignore();
}
void Singer::Show() const
{
cout<<"Category: Singer"<<endl;
Worker::Show();
cout<<"Vocal range: "<<pv[voice]<<endl;
}
有几点注意一下:
- 对于抽象基类中的纯虚函数,我们可以给它定义,但是不能在类声明中定义,否则会报
pure-specifier on function-definition
错误。 - 获得一个类作用域中的常数我们一般使用
enum
成员,或者使用静态const
变量。 - 抽象基类不能生成对象,但是我们可以使用抽象基类指针或引用指向其派生类对象。
如下是测试代码:
//多重继承1mian.cpp
#include<iostream>
#include"多重继承1.h"
const int LIM=4;
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};
pw[2]->Set();
pw[3]->Set();
for(int i=0;i<LIM;i++)
{
pw[i]->Show();
std::cout<<std::endl;
}
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 多重继承1 .\多重继承1.cpp .\多重继承1main.cpp
PS D:\study\c++\path_to_c++> .\多重继承1.exe
Enter worker's name: Waldo Dropmaster
Worker's id: 442
Enter the waiter's panache rating: 3
Enter worker's name: Sylvie Sirenne
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
ID: 314
Panache rating: 5
Category: Singer
Name: Beverly Hills
ID: 522
Vocal range: soprano
Category: waiter
Name: Waldo Dropmaster
ID: 442
Panache rating: 3
Category: Singer
Name: Sylvie Sirenne
ID: 555
Vocal range: soprano
2. 虚基类
假设我们从Singer
和Waiter
公有派生出SingingWaiter
:
class SingingWaiter:public Singer,public Waiter{...}
但是我们现在出现了一个问题:由于Singer
和Waiter
都包含一个Worker
子对象,因此SingingWaiter
会包含两个Worker
组件。
- 这将会出现问题
SingingWaiter ed;
Worker *p=&ed;
上面这段代码会出现
ambiguous
的错误。为了改进上述错误,我们可以这样修改代码:
SingingWaiter ed;
Worker *p1=(Singer*)&ed;
Worker *p2=(Waiter*)&ed;
由于
ed
中存在两个Worker
组件,我们必须显式指出,你需要的组件是哪个。
- 可以前瞻性的看,为什么我们需要2个
Worker
组件,我们可不可以只包含一个Worker
组件?C++引进了一种新技术–虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。我们可以通过在类声明中使用关键词virtual
,将Worker
作为Singer
和Waiter
的虚基类(virtual
和public
的次序无所谓),然后从Singer
和Waiter
中公有派生出SingingWaiter
class Singer:public virtual Worker{...};
class Waiter:virtual public Worker{...};
class SingingWaiter:public Singer,public Waiter{...};
现在SingingWaiter
对象只包含Worker
对象的一个副本。本质上来说,继承的Singer
和Waiter
对象共享一个Worker
对象,而不是各自引用自己的Worker
对象副本。
![](https://i-blog.csdnimg.cn/blog_migrate/637eeaa58810a2421770e22214435046.jpeg)
2.1 新的构造函数的规则
- 我们先复习一下旧的构造函数规则
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中调用A的构造函数就是错误的。
- 为什么只能调用直接基类的构造函数?
构造函数的接口不能被继承,但是它的实现可以被继承。(也就是说,派生类相当于把基类的构造函数放到了私有部分),例如A是B的基类,B是C的基类。由于B没有继承A的构造函数接口,所以C继承B的时候,A的构造函数相当于在B的私有部分,既不能继承接口也不能继承实现,所以在C中无法调用A的构造函数。
插一句,
class H
{
public:
int dad();
};
class G :public H
{
void q();
};
class F:private G
{
...
};
上面这段代码中,在F中可以使用
H::dad()
,这是因为G公有继承了H的接口,而F继承了G的实现,从而继承了H::dad()
在F中可用,但是它不是接口,也就是说,G的对象,可以调用dad()
,而F的对象不能调用
dad()
。在F中不能使用G::q()
,这是因为,私有数据不会被继承。
- 新的构造函数规则
class A
{
int a;
public:
A(int n=0):a(n){}
};
class B:public virtual 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){}
};
但是,如果我将A改成虚基类,
class B:public virtual A
,那么我们就可以在C中调用A的构造函数。因为在C中,现在包含了两个直接子对象,一个是不包含A的B对象,一个是A对象。所以说,间接虚拟基A相当于成为了C的直接基类。进一步的,我们可以在C中使用A的方法,而且C也继承了A的接口。可以看一下,虚基类和非虚基类的那张图就明白了。
回到SingingWaiter
问题。
我们试着构建SingingWaiter
的构造函数。
我们的一种想法是:
SingingWaiter(const string &s,long i,int p=0,int v=other):Waiter(s,i,p),Singer(s,i,v){};
这种方法存在问题:
Waiter
和Singer
的构造函数都可以给Worker
传递参数,也就是说有两个途径传参给Worker
,就会产生冲突。为了避免这种冲突,在基类是虚的时,禁止通过中间类自动传递给基类。因此上面那句代码,s
和i
不会传递给Worker
对象,在这种情况下,编译器会自动调用Worker
的默认构造函数。
在上面我们说过,间接虚基相当于是直接基类,我们可以这样写:
SingingWaiter(const string &s,long i,int p=0,int v=other):Worker(s,i),Waiter(s,i,p),Singer(s,i,v){};
上面这种写法是合法的,对于
Worker
是虚基类是合法的,如果它不是虚基类则是非法的,因为它不是直接基类。
如果有间接虚基类,则除非只需要改虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
插一句,在设计一个抽象基类
Worker
,然后从中派生出Waiter
和Singer
时,由于Worker
是抽象基类,它不可能创造对象,所以我们在给Singert
编写构造函数的时候,我们没有写Singer(const Worker &w,int v);
这样的构造函数,但是我们现在要从Waiter
和Singer
派生出SingingWaiter
时,我们就需要编写这样的构造函数SingingWaiter(const Waiter & w,int v):Worker(w),Waiter(w),Singer(w,v){};
这里出现了我们需要调用Singer(w,v)
的构造函数,这就意味着在编写Singer
类时,Singer(const Worker &w,int v);
的构造函数是有必要的。
2.2 使用哪个方法
如果我们没有在SingingWaiter
中,重新定义Show()
方法,那么
SingingWaiter w("Elis",2005,6,3);
w.Show();
这段代码是错误的。如果是单继承,那么这个
Show()
默认是基类的接口,因为公有继承继承基类接口。而现在是公有MI,就会出现问题,我们知道SingingWaiter
有2个直接基类,1个间接虚基类,他们中都有Show()
接口,那么上面那段代码就会ambiguous
错误。
那么我们只好采用,作用域解析符来澄清:
SingingWaiter w("Elis",2005,6,3);
w.Singer::Show();
w.Waiter::Show();
w.Worker::Show();
当然了,我们需要在SingingWaiter
中,重新定义Show()
方法,但是问题在于我们无法直接访问,Worker
、Singer
、Waiter
中的私有数据,我们只能通过它们中的接口来访问数据,但是问题还是出现了
void SingingWaiter::Show() const
{
Singer::Show();
Waiter::Show();
}
上面这种方法将会显式名字和ID两次,不是完美的。
这就意味着,我们必须重新修改基类的接口,属实是麻烦。
总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。如果在编写这些类时没有考虑MI,则可能需要重新编写它们。
2.3 实现1
//多重继承2.h
#ifndef aa
#define aa
#include<string>
using std::string;
class Worker
{
private:
string fullname;
long id;
protected:
virtual void Data() const;
virtual void Get();
public:
Worker():fullname("no one"),id(0l){}
Worker(const string & s,long i):fullname(s),id(i){}
virtual ~Worker(){};
virtual void Set()=0;
virtual void Show()const=0;
};
class Waiter:public virtual Worker
{
private:
int panache;
protected:
virtual void Data() const;
virtual void Get();
public:
Waiter():Worker(),panache(0){}
Waiter(const string &s,long i,int p=0):Worker(s,i),panache(p){}
Waiter(const Worker &w,int p=0):Worker(w),panache(p){}
virtual void Set();
virtual void Show() const;
};
class Singer:public virtual Worker
{
protected:
enum{other,alto,contralto,soprano,bass,baritone,tenor};
enum{Vtypes=7};
virtual void Data() const;
virtual void Get();
private:
static const char * pv[Vtypes];
int voice;
public:
Singer():Worker(),voice(other){}
Singer(const string &s,long i,int v=other):Worker(s,i),voice(v){}
Singer(const Worker &w,int v=other):Worker(w),voice(v){}
virtual void Set();
virtual void Show() const;
};
class SingingWaiter:public Waiter,public Singer
{
protected:
virtual void Data() const;
virtual void Get();
public:
SingingWaiter():Worker(),Waiter(),Singer(){}
SingingWaiter(const string &s,long i,int p=0,int v=other):Worker(s,i),Waiter(s,i,p),Singer(s,i,v){};
SingingWaiter(const Waiter &w,int v=other):Worker(w),Waiter(w),Singer(w,v){}
SingingWaiter(const Singer &s,int p=0):Worker(s),Singer(s),Waiter(s,p){}
virtual void Set();
virtual void Show() const;
};
#endif
//多重继承2.cpp
#include"多重继承2.h"
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
//Work
void Worker::Data() const
{
cout<<"Name: "<<fullname<<endl;
cout<<"ID: "<<id<<endl;
}
void Worker::Get()
{
std::getline(cin,fullname);
cout<<"Enter worker's id: ";
cin>>id;
cin.ignore();
}
//Waiter
void Waiter::Data() const
{
cout<<"Panache rating: "<<panache<<endl;
}
void Waiter::Get()
{
cout<<"Enter the waiter's panache rating: ";
cin>>panache;
cin.ignore();
}
void Waiter::Show()const
{
cout<<"Category: waiter"<<endl;
Worker::Data();
Data();
}
void Waiter::Set()
{
cout<<"Enter waiter's name: ";
Worker::Get();
Get();
}
//Singer
const char* Singer::pv[Vtypes]={"other","alto","contralto","soprano","bass","baritone","tenor"};
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+1)%4==0)
cout<<endl;
}
if(Vtypes%4!=0)
cout<<endl;
while (!(cin>>voice) || (voice<0 || voice>=Vtypes))
{
cout<<"Please enter a value >=0 and < "<<Vtypes<<endl;
}
cin.ignore();
}
void Singer::Show() const
{
cout<<"Category: Singer"<<endl;
Worker::Data();
Data();
}
void Singer::Set()
{
cout<<"Enter singer's name: ";
Worker::Get();
Get();
}
//SingingWaiter
void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}
void SingingWaiter::Get()
{
Waiter::Get();
Singer::Get();
}
void SingingWaiter::Show() const
{
cout<<"Category: Singing waiter\n";
Worker::Data();
Data();
}
void SingingWaiter::Set()
{
cout<<"Enter singing waiter's name: ";
Worker::Get();
Get();
}
//多重继承3main.cpp
#include<iostream>
#include<cstring>
#include"多重继承2.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";
while (!(cin>>choice) || strchr("wstq",choice)==NULL)
{
cin.ignore();
cout<<"Please enter a w,s,t,q: ";
}
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.ignore();//这是因为cin>>choice完成后,缓冲区会有一个换行符,我们必须吸收掉,以免影响Set()
lolas[ct]->Set();
}
cout<<"\n Here is your staff:\n";
for(int i=0;i<ct;i++)
{
cout<<endl;
lolas[i]->Show();
}
for(int i=0;i<ct;i++)
delete lolas[i];
cout<<"Bye!\n";
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 多重继承2 .\多重继承2.cpp .\多重继承2main.cpp
PS D:\study\c++\path_to_c++> .\多重继承2.exe
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
w
Enter waiter's name: Wally Slipshod
Enter worker's id: 1040
Enter the waiter's panache rating: 4
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
s
Enter singer's name: Sinclair Parma
Enter worker's id: 1044
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: Natasha Gargalova
Enter worker's id: 1021
Enter the waiter's panache rating: 6
Enter number for singer's vocal range:
0: other 1: alto 2: contralto 3: soprano
4: bass 5: baritone 6: tenor
3
Enter the employee category:
w: waiter s: singer t: singing waiter q: quit
q
Here is your staff:
Category: waiter
Name: Wally Slipshod
ID: 1040
Panache rating: 4
Category: Singer
Name: Sinclair Parma
ID: 1044
Vocal range: baritone
Category: Singing waiter
Name: Natasha Gargalova
ID: 1021
Vocal range: soprano
Panache rating: 6
Bye!
2.4 实现2
既然无法直接访问基类的私有数据项,那我们就把基类的数据设置成
protected
的,那么派生类SingingWaiter
就可以直接访问基类的数据了。
//多重继承3.h
#ifndef aa
#define aa
#include<string>
using std::string;
class Worker
{
protected:
string fullname;
long id;
public:
Worker():fullname("no one"),id(0l){}
Worker(const string & s,long i):fullname(s),id(i){}
virtual ~Worker(){};
virtual void Set()=0;
virtual void Show()const=0;
};
class Waiter:public virtual Worker
{
protected:
int panache;
public:
Waiter():Worker(),panache(0){}
Waiter(const string &s,long i,int p=0):Worker(s,i),panache(p){}
Waiter(const Worker &w,int p=0):Worker(w),panache(p){}
virtual void Set();
virtual void Show() const;
};
class Singer:public virtual Worker
{
protected:
enum{other,alto,contralto,soprano,bass,baritone,tenor};
enum{Vtypes=7};
static const char * pv[Vtypes];
int voice;
public:
Singer():Worker(),voice(other){}
Singer(const string &s,long i,int v=other):Worker(s,i),voice(v){}
Singer(const Worker &w,int v=other):Worker(w),voice(v){}
virtual void Set();
virtual void Show() const;
};
class SingingWaiter:public Waiter,public Singer
{
public:
SingingWaiter():Worker(),Waiter(),Singer(){}
SingingWaiter(const string &s,long i,int p=0,int v=other):Worker(s,i),Waiter(s,i,p),Singer(s,i,v){};
SingingWaiter(const Waiter &w,int v=other):Worker(w),Waiter(w),Singer(w,v){}
SingingWaiter(const Singer &s,int p=0):Worker(s),Singer(s),Waiter(s,p){}
virtual void Set();
virtual void Show() const;
};
#endif
#include"多重继承3.h"
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
//Worker
void Worker::Set()
{
cout<<"Enter worker's name: ";
std::getline(cin,fullname);
cout<<"Worker's id: ";
cin>>id;
cin.ignore();
}
void Worker::Show() const
{
cout<<"Name: "<<fullname<<endl;
cout<<"ID: "<<id<<endl;
}
//Waiter
void Waiter::Set()
{
Worker::Set();
cout<<"Enter the waiter's panache rating: ";
cin>>panache;
cin.ignore();
}
void Waiter::Show()const
{
cout<<"Category: waiter"<<endl;
Worker::Show();
cout<<"Panache rating: "<<panache<<endl;
}
//Singer
const char* Singer::pv[Vtypes]={"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+1)%4==0)
cout<<endl;
}
if(Vtypes%4!=0)
cout<<endl;
while (!(cin>>voice) || (voice<0 || voice>=Vtypes))
{
cout<<"Please enter a value >=0 and < "<<Vtypes<<endl;
}
cin.ignore();
}
void Singer::Show() const
{
cout<<"Category: Singer"<<endl;
Worker::Show();
cout<<"Vocal range: "<<pv[voice]<<endl;
}
//SingingWaiter
void SingingWaiter::Set()
{
Singer::Set();
cout<<"Enter the waiter's panache rating: ";
cin>>panache;
cin.ignore();
}
void SingingWaiter::Show()const
{
cout<<"Category: Singing waiter"<<endl;
Worker::Show();
cout<<"Vocal range: "<<pv[voice]<<endl;
cout<<"Panache rating: "<<panache<<endl;
}
3.MI的其他问题
- 混合使用虚基类和非虚基类
例如,B是C和D的虚基类,同时B是X和Y的非虚基类,M是从C、D、X和Y派生而来的。那么M类从虚派生祖先(C和D)中共继承了一个B类子对象,并从每一个非虚基类祖先(X和Y)那分别继承了一个B类子对象。因此,它包含3个B类子对象。
- 虚基类和支配
使用非基类时,如果类从不同的类那里继承了两个或多个同名成员,那么使用这些成员的时候,如果没有使用类名进行限定,将会导致二义性。如果使用虚基类,则这样做不一定会导致二义性,因为这时,可能某个名称优先于其他所有名称,即便不使用限定符,也不会导致二义性。
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()
,因此在F类中,可以直接使用q()
表示C::q()
。另一方面,仍和一个omg()
都不优先于另一个omg()
,因为C和E都不是对方的基类。所以在F中使用omg()
会导致二义性。而且虚二义性规则和访问规则无关,也就是说,E::omg()
是私有的,不能在F中直接访问,但是使用omg()
仍将导致二义性。