一、多重继承中,接口调用的歧义性
- 当一个类继承自两个基类时,两个基类包含有相同的名称(如函数、typedef等),那么调用时就会产生歧义性
演示案例
class BorrowableItem {
public:
void checkOut();
};
class ElectronicGadget {
private:
bool checkOut()const; //注意,此处的为private
};
//多重继承
class MP3Player :public BorrowableItem, public ElectronicGadget { };
int main()
{
MP3Player mp;
mp.checkOut(); //错误,歧义性
return 0;
}
- 上面的代码中,虽然ElectronicGadget中的checkOut()函数为private的,但是调用仍然会产生歧义性。因为在调用checkOut()之前,C++会解析代码,发现在两个基类中都存在,因此报错
- 正确的做法是:明确指出调用哪一个base class内的函数,例如:
MP3Player mp;
mp.BorrowableItem::checkOut(); //正确
mp.ElectronicGadget::checkOut();//错误,ElectronicGadget中的checkOut()为private
二、菱形继承与虚(virtual)继承
三、virtual继承的代价
- 虽然virtual继承有优点,但是还是会付出一定的代价,例如:
- virtual继承所创建的对象比non-virtual继承创建的对象体积大
- 访问virtual base class的成员变量时,也比访问non-virtual base class的成员变量速度慢
- virtual base的初始化责任是由继承体系中最底层的class负责的。因此:
- 派生类必须为virtual base进行初始化,不论它们距离有多远
- 当一个新的派生类加入继承体系中,它也必须承担起virtual base的初始化责任
四、多重继承演示案例
- 现在我们来重新塑模“人”的C++ Interface class(参阅条款31)
IPerson类
- 下面是一个抽象类:其中包含纯虚函数name()和birthDate()
class IPerson {
public:
virtual ~IPerson();
virtual std::string name()const = 0; //返回人的名称
virtual std::string birthDate()const = 0; //返回生日
};
- name()和birthDate()两个虚函数返回人物的名称和生日
- IPerson必须使用pointera或references指向于派生类来编写程序,因为抽象类无法实例化。下面创建一个factory functions(工厂函数,见条款31),在其中使用IPerson的派生类创建一个对象,然后返回这个对象(返回值类型为IPerson)。代码如下:
class DatabaseID {};
//参数为一个数据库ID对象
shared_ptr<IPerson> makePerson(DatabaseID personIdentifier)
{
//在其中使用IPerson的派生类创建一个对象
//然后返回该对象
}
DatabaseID askUserForDatabaseID()
{
//该函数返回一个DatabaseID对象
}
int main()
{
//创建一个DatabaseID对象
DatabaseID id(askUserForDatabaseID());
//使用makePerson函数创建一个IPerson对象
shared_ptr<IPerson> pp(makePerson(id));
return 0;
}
PersonInfo类
- 现在假设有一个和数据库相关的类,提供一些CPerson类(定义在下面)所需要的实质东西
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName()const;
virtual const char* theBirthDate()const;
private:
virtual const char* valueDelimOpen()const;
virtual const char* valueDelimClose()const;
};
- valueDelimOpen()、valueDelimClose():
- 功能:每个字段值的起点和结尾都以特殊字符串为界
- 缺省的头尾界限符号是方括号。例如Ring-tailed Lemur将被格式化为:[Ring-tailed Lemur]
- 每个人可能喜欢不同的界限符号,所以这两个virtual函数允许派生类自己定义不同的头尾界限符号。例如可能PersonInfo的派生类可能会重写这两个虚函数,代码如下:
//缺省的虚函数,派生类可以重写
const char* valueDelimOpen()const
{
return "[";
}
const char* valueDelimClose()const
{
return "]";
}
- theName()、theBirthDate():用来返回相关的数据库字段(名字、生日等)。下面以theName()为例:
const char* theName()const {
static char value[Max_Formatted_Field_Value_Length];
std::strcpy(value, valueDelimOpen());
//将名字添加进value
std::strcat(value, valueDelimClose());
return value;
}
CPerson类
- CPerson是最终的表示“人”的类,其继承于IPerson和PersonInfo
- 公有继承于IPerson:
- 因为IPerson的name()和birthDate()两个虚函数返回未经修饰的人物的名称和生日,并且IPerson为抽象类,因此CPerson以public继承于IPerson
- 私有继承于PersonInfo:
- PersonInfo已经提供了返回修饰的人名和生日的虚函数,因此CPerson可以利用PersonInfo来实现,这是一种is-implemented-in-terms-of(根据某物实现出)模式
- 在前几篇文章中,我们介绍了:复合与private继承都可以实现is-implemented-in-terms-of模式。但是由于PersonInfo中有虚函数,派生类可以重写其虚函数,因此我们建议使用private继承
- 所以最终CPerson私有继承于PersonInfo
- 最终的代码如下:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name()const = 0;
virtual std::string birthDate()const = 0;
};
class DatabaseID {};
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName()const;
virtual const char* theBirthDate()const;
private:
virtual const char* valueDelimOpen()const;
virtual const char* valueDelimClose()const;
};
class CPerson :public IPerson, private PersonInfo {
public:
explicit CPerson(DatabaseID pid) :PersonInfo(pid) {}
virtual std::string name()const = 0{
return PersonInfo::theName();
}
virtual std::string birthDate()const = 0 {
return PersonInfo::theBirthDate();
}
private:
virtual const char* valueDelimOpen()const;
virtual const char* valueDelimClose()const;
};
- 在CPerson中:
- 其重写了IPerson中的name()和birthDate(),在其中返回人的名字和生日
- 由于private继承于PersonInfo,并且PersonInfo中已经实现了从数据库中读取并格式化人的名字和生日的功能,因此在name()和birthDate()中分别调用PersonInfo的theName()、theBirthDate()即可
- 并且自己可以重写valueDelimOpen()、valueDelimClose()函数,来重写格式化人的名字和生日的格式
![](https://i-blog.csdnimg.cn/blog_migrate/39fde6d10690556ba89f46dafc258454.png)
- 当单一继承和多重继承可以实现相同的功能时,尽量选择单一继承
五、总结
- 多重继承比单一继承复杂。它可+能导致0新的歧义性,以及对virtual继承的需要
- virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具使用价值的情况
- 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两项组合