多重继承
MI 描述的是有多个直接基类的类。与单继承一样,公有 MI 表示的也是 is-a 关系。例如,可以从 Waiter 类和 Singer 类派生出 SingingWaiter 类:
class SingingWaiter : public Waiter, public Singer { ... };
请注意,必须使用关键字 public 来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生:
class SingerWaiter : public Waiter, Singer { ... }; // Singer is a private base
正如本章前面讨论的,私有 MI 和保护MI 可以表示 has-a 关系。Student 类的 student.h 实现就是一个这样的示例。下面将重点介绍公有 MI。
MI 可能会给程序员带来很多新问题。其中两个主要的问题是:从两个不同的基类继承同名的方法;从两个或更多相关基类那里继承同一个类的多个实例。为解决这些问题,需要使用一些新规则和不同的语法。因此,与使用单继承相比,使用 MI 更困难,也更容易出现问题。由于这个原因,很多 C++ 用户强烈反对使用 MI,一些人甚至希望删除 MI;而喜欢 MI 的人则认为,对一些特殊的工程来说,MI 很有用,甚至是必不可少的;也有一些人建议谨慎、适度地使用MI。
下面来看一个例子,并介绍有哪些问题以及如何解决它们。要使用MI,需要几个类。我们将定义一个抽象基类 Worker,并使用它派生出 Waiter 类和 Singer 类。然后,便可以使用 MI 从 Waiter 类 和 Singer 类派生出 SingingWaiter 类。这里使用两个独立的派生来使基类(Worker)被继承,这将导致 MI 的大多数麻烦。首先声明 Worker、Waiter 和 Singer 类,如下面程序所示:
// worker0.h -- working classes
#ifndef WORKER0_H_
#define WORKER0_H_
#include<string>
class Worker{ // an abstract base
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
上面的程序的类声明中包含一些表示声音类型的内部常量。一个枚举用符号常量alto、contralto 等表示声音类型,静态数组 pv 存储了指向相应 C-风格字符串的指针,下面的程序初始化了该数组,并提供了方法的定义。
// worker0.cpp -- working class methods
#include"14.7_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
const char * Singer::pv[] = {"other", "alto", "contralto",
"soprano", "base", "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 << "Catogory: singer\n";
Worker::Show();
cout << "Vocal range: " << pv[voice] << endl;
}
下面的程序是一个简短的程序,它使用一个多态指针数组对这些类进行了测试。
// worktest.cpp -- test worker class hierarchy
#include<iostream>
#include"14.7_worker0.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};
int i;
for(i=2; i<LIM; i++){
pw[i]->Set();
}
for(i=0; i<LIM; i++){
pw[i]->Show();
std::cout << std::endl;
}
return 0;
}
这种设计看起来是可行的:使用 Waiter 指针来调用 Waiter::Show() 和 Waiter::Set();使用 Singer 指针来调用 Singer::Show() 和 Singer::Set()。然后,如果添加一个从 Singer 和 Waiter 类派生出的 SingingWaiter 类后,将带来一些问题。具体地说,将出现以下问题。
- 有多少 Worker?
- 哪个方法?
有多少 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
这将使得使用基类指针来引用不同的对象(多态性)复杂化。
包含两个 Worker 对象拷贝还会导致其他的问题。然而,真正的问题是:为什么需要 Worker 对象的两个拷贝?SingingWaiter 和其他 Worker 对象一样,也应只包含一个姓名和一个ID。C++ 引入多重继承的同时。引入了一种新技术——虚基类(virtual base class),使 MI 成为可能。
-
虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字 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 对象副本。因为 SingingWaiter 现在只包含了一个 Worker 子对象,所以可以使用多态。
您可能会有这样的疑问:
- 为什么使用术语”虚“?
- 为什么不抛弃将基类声明为虚的这种方式,而使虚行为 成为多 MI 的准则呢?
- 是否存在麻烦呢?
首先,为什么使用术语虚?毕竟,在虚函数和虚基类之间并不存在明显的联系。C++ 用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。例如,如果新关键字与重要程序中的重要函数或变量的名称相同,这将非常麻烦。因此,C++ 对这种新特性也使用关键字 virtual——有点像关键字重载。
其次,为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为 MI 的准则呢?第一,在一些情况下,可能需要基类的多个拷贝;第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应当的;第三,这样做有其缺点,将在下一段介绍。
最后,是否存在麻烦?是的。为使虚基类能够工作,需要对 C++ 规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码。例如,将 SingingWaiter 类添加到 Worker 集成层次中时,需要在 Singer 和 Waiter 类中添加关键字 virtual。
-
新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:class A { int a; public: A(int n = 0) : a(n) { } ... }; class B : public A{ int b; 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 &)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。
哪个方法
除了修改类构造函数规则外,MI 通常还要求调整其他代码。假设要在 SingingWaiter 类中扩展 Show() 方法。因为SingingWaiter 对象没有新的数据成员,所以可能会认为它只需使用继承的方法即可。这引出了第一个问题。假设没有在 SingingWaiter 类中重新定义 Show() 方法,并试图使用 SingingWaiter 对象调用继承的 Show() 方法:
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Show(); // ambigous
对于单继承,如果没有重新定义 Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个 Show() 函数,这使得上述调用是二义性的。
警告:多重继承可能导致函数调用的二义性。例如,BadDude 类可能从 Gunslinger 类和 PokerPlayer 类那里继承两个完全不同的 Draw() 方法。
可以使用作用域解析运算符来澄清编程者的意图:
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Singer::Show(); // use Singer version
然而,更好的方法是在 SingerWaiter 中重新定义 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() cosnt {
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,则还可能需要重新编写它们。下面的程序列出了修改后的类声明。
// 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 const 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 Waiter & wt, int v = other)
: Worker(wt), Waiter(wt), Singer(wt,v) {}
SingingWaiter(const Singer & sg, int p = 0)
: Worker(sg), Waiter(sg, p), Singer(sg) {}
void Set();
void Show() const;
};
#endif
下面的程序给出实现:
// workermi.cpp -- working class methods with MI
#include "14.10_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] = {
"const", "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();
}
当然,好奇心要求我们测试这些类,下面的程序提供了测试代码。注意,该程序使用了多态属性,将各种类的地址赋给基类指针。另外,该程序还在下面的检测中使用了 C-风格字符串库函数 strchr();
while (strchr("wstq", choice) == NULL)
该函数返回参数 choice 指定的字符在字符串 “wstq” 中第一次出现的地址,如果没有这样的字符,则返回 NULL 指针。使用这种检测比使用 if语句将choice指定的字符同每个字符进行比较简单。
// workmi.cpp -- multiple inheritance
// compile with workermi.cpp
#include<iostream>
#include<cstring>
#include"14.10_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";
return 0;
}
下面介绍一些其他有关 MI 的问题。
-
混合使用虚基类和非虚基类
再来看一下通过多种途径继承一个基类的派生类的情况。如果基类是虚基类,派生类将包含基类的一个子对象;如果基类不是虚基类,派生类将包含多个子对象。当虚基类和非虚基类混合时,情况将如何呢?例如,假设类 B 被用作类 C 和 D 的虚基类,同时被用作类 X 和 Y 的非虚基类,而类 M 是从C、D、和Y派生而来的。在这种情况下,类M从虚派生祖先(即C和D)那里共继承了一个B类子对象,并从每一个非虚派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含三个B类子对象。当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。 -
虚基类和支配
使用虚基类将改变 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 : pubilic C, 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()。
MI 小结
首先复习一下不使用虚基类的 MI。这种形式的 MI 不会引入新的规则。然而,如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。即在从 GunSlinger 和 PokerPlayer 派生而来的 BadDude 类中,将分别使用 Gunslinger::draw() 和 PokerPlayer::draw() 来区分从这两个类那里继承的 draw() 方法。否则,编译器将指出二义性。
如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类,则该类从每种途径分别继承非虚基类的一个实例。在某些情况下,这可能正是所希望的,但通常情况下,多个基类实例都是问题。
接下来看一看使用虚基类的 MI。当派生类使用关键字 virtual 来指示派生时,基类就成为虚基类:
class marketing : public virtual reality { ... } ;;
主要变化(同时也是使用虚基类的原因)是,从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其他要求:
- 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
- 通过优先规则解决名称二义性。
正如您看到的,MI 会增加编程的复杂程度。然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。避免这种情况后,唯一需要注意的是,在必要时对继承的名称进行限定。