Effective C++条款40:继承与面向对象——明智而审慎地使用多重继承
作者:互联网
一、多重继承中,接口调用的歧义性
- 当一个类继承自两个基类时,两个基类包含有相同的名称(如函数、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)继承
- 关于这个知识点可以参阅:https://blog.csdn.net/qq_41453285/article/details/92699042
- 此处不再多介绍
三、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()函数,来重写格式化人的名字和生日的格式
- 当单一继承和多重继承可以实现相同的功能时,尽量选择单一继承
五、总结
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要
- virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具使用价值的情况
- 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两项组合
标签:const,Effective,继承,PersonInfo,40,virtual,class,IPerson 来源: https://blog.csdn.net/www_dong/article/details/113799398