读书笔记 effective C++ Item 40 明智而谨慎的使用多继承
1. 多继承的两个阵营
当我们谈论到多继承(MI)的时候,C++委员会被分为两个基本阵营。一个阵营相信如果单继承是好的C++性质,那么多继承肯定会更好。另外一个阵营则争辩道单继承诚然是好的,但多继承太麻烦,而不值得去使用它。在这个条款中,我的主要目标是让你明白多继承的这两个观点。
2. 从多个基类中继承的名字不能相同
第一件事情是你需要认识到使用MI进行设计时,从多个基类中可能会继承相同的名字(例如函数或者typedef等等)。这就会导致模棱两可的问题,例如:
class BorrowableItem { // something a library lets you borrow public: void checkOut(); // check the item out from the library ... }; class ElectronicGadget { private: bool checkOut() const; // perform self-test, return whether
... // test succeeds
};
class MP3Player: // note MI here public BorrowableItem, // (some libraries loan MP3 players) public ElectronicGadget { ... }; // class definition is unimportant MP3Player mp;
mp.checkOut(); // ambiguous! which checkOut?
注意在这个例子中,对checkout的调用是模棱两可的,即使只有两个函数中的一个是可访问的。(checkout在BorrowableItem中是public的而在ElectronicGadget中是private的)。这对应着C++规则中对重载函数是如何解决的:在寻找是否有一个函数可被访问之前,C++首先首先识别出函数调用的最佳匹配函数。找到最佳匹配函数之后才会检查函数的可访问性。在这种情况下,搜寻名字的时候checkOut是模棱两可的,所以既不能解决函数重载的问题也不能决定最佳匹配函数。ElectronicGadget::checkOut的可访问性根本不会被检查到。
为了解决这个模棱两可的问题,你必须指定调用哪个基类的函数:
mp.BorrowableItem::checkOut(); // ah, that checkOut
当然你也可以显示调用ElectronicGadget::checkOut,但是模棱两可的错误将会被下面的错误所替代——“你在尝试调用一个private成员函数”
3. 钻石型多继承
3.1 钻石型继承中数据成员的重复问题
多继承仅仅意味着从多个基类(多于一个)中继承,但是对于多继承来说在继承体系中发现更高层次的基类也并不是不常见。这就导致了我们常说的致命的“钻石型多继承”:
任何时候在一个继承体系中,如果基类和派生类之间的路径多于一条,你必须面对基类中的数据成员是否在每条路径上都要被复制的问题。例如,假设File类有一个数据成员,fileName。IOFile应该有它的几份拷贝?从一方面讲,它从每个基类中都继承了一份拷贝,所以表明IOFile应该会有两个fileName数据成员。从另外一方面讲,一个IOFIle只有一个文件名,所以从两个基类中继承的fileName部分不应该被重复。
3.2 C++如何处理钻石型继承
C++在这场辩论中没有任何立场。它很高兴的支持两种选择,默认的选择是执行重复。如果这不是你想要的,你必须将包含数据(也即是File)的类变为虚基类。为了达到这个目的,你会对所有继承自它的类使用虚继承。
标准C++库包含一个多继承体系,就像上面的继承一样,但是类模版的类不在其中,这些类的名字是basic_ios,basic_istream,basic_ostream和basic_iostream,它们分别替换了File,InputFile,OutputFile和IOFile。
3.3 虚继承耗费资源,不能随意使用
从正确行为的角度,public继承应该总是virtual的。如果你只考虑这一点,规则会很简单:在你使用public继承的任何时候都要使用virtual public继承。但正确性不是我们要唯一关注的,为了阻止对继承而来的字段进行重复,编译器会在背后耍一些花招,结果是使用虚继承的类创建出来的对象会比不使用虚继承的类创建出来的对象要大。访问虚基类中的数据成员比访问非虚基类中的数据成员要慢。详细情况随编译器的不同而不同,但是基本的观点很清楚:虚继承耗费资源。
资源耗费也体现在其他方面。管理虚基类初始化列表的规则比非虚基类更加复杂,更加不直观。初始化虚基类部分的责任由继承体系中最底层的派生类承担。这种规则就意味着:(1)继承自虚基类的类如果需要初始化,它们必须意识到虚基类的存在,无论这个虚基类离派生类有多远。(2)当一个派生类被添加到继承体系中的时候,它必须承担初始化虚基类的责任(无论是直接的还是间接的虚基类)。
我对于使用虚基类(也就是虚继承)的建议很简单。首先,不要使用虚基类,除非你需要它。默认情况下使用非虚基类。第二,如果你必须使用虚基类,尝试着不要在这些类中放置数据。这样你就不必为这些类的初始化(还有赋值)规则的古怪行为进行担心了。值得注意的是,Java和.NET中的接口(在许多方面相当于C++的虚基类)是不允许包含任何数据的。
4. 另外一种多继承的使用场景
让我们看一下下面的C++接口类(见Item 31),这个类为persons建模:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = ;
virtual std::string birthDate() const = ;
};
IPerson的客户必须依赖IPerson指针和引用来进行编程,因为抽象类不能被实例化。为了创建可以被IPerson对象操作的对象,IPerson的客户使用工厂函数(Item 31)来实例化派生自IPerson的具现类:
// factory function to create a Person object from a unique database ID;
// see Item 18 for why the return type isn’t a raw pointer
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier); // function to get a database ID from the user
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id)); // create an object
// supporting the
// IPerson interface ... // manipulate *pp via
// IPerson’s member
// functions
但是如何使用makePerson创建返回指针指向的对象呢?很清楚,必须有一些派生自IPerson具现类,使得makePerson能够对这些具现类进行实例化。
我们把这个类叫做CPerson。作为一个具现类,CPerson必须为继承自IPerson的纯虚函数提供一份实现。我们可以从头开始实现这个函数,但是利用现成的组件来对其进行实现更好,这些现成的组件实现了大部分或者全部的必要功能。例如,假设一个旧数据库指定的类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; // see
virtual const char * valueDelimClose() const; // below
...
};
你能够识别出来这是一个旧类,因为成员函数返回的是const
char*而不是string对象。并且如果鞋是合适的,为什么不穿上呢?这个类的成员函数的名字也表明了使用结果会非常棒。
你现在发现了PersonInfo的设计是用来帮助打印不同格式的数据库字段的,这些字段值的开始和结尾处都由特殊的字符串进行分隔。默认情况下,字段值的开始和结束分隔符是方括号,所以值”Ring-tailed
Lemur”会被格式化成下面的字符串:
[Ring-tailed Lemur]
认识到方括号并不能被PersonInfo的客户普遍接受,虚函数valueDelimOpen和valueDelimClose允许派生类指定它们自己的开始和结束分隔字符串。PersonInfo的成员函数的实现会调用这些虚函数来为返回值添加合适的分隔符。使用PersonInfo::theName作为例子,代码可能像下面这样:
const char * PersonInfo::valueDelimOpen() const
{ return "["; // default opening delimiter } const char * PersonInfo::valueDelimClose() const { return "]"; // default closing delimiter } const char * PersonInfo::theName() const { // reserve buffer for return value; because this is // static, it’s automatically initialized to all zeros static char value[Max_Formatted_Field_Value_Length]; // write opening delimiter std::strcpy(value, valueDelimOpen()); append to the string in value this object’s name field (being careful to avoid buffer overruns!) // write closing delimiter std::strcat(value, valueDelimClose()); return value; }
有人可能对PersonInfo::theName的过时设计发出疑问(特使是对固定大小的static缓存的使用,有时会造成越界或者线程问题,见Item 21),但是将这种问题放到一边,我们将焦点放在下面这件事情上:theName调用valueDelimOpen来为返回的string生成开始分隔符,然后生成名字本身,最后调用valueDelimClose。
因为valueDelimOpen和valueDelimClose是虚函数,由theName返回的值不仅依赖于PerosnInfo同时依赖于PersonInfo的派生类。
作为CPerson的实现者,这是个好消息,因为当对IPerson的文档进行精读时,你发现name和birthDate需要返回没有分隔符的值。也就是一个叫做”Homer”的人,对这个名字进行函数调用会返回“Homer”而不是“[Homer]”。
CPerson和PersonInfo之间的关系是PersonInfo恰好有一些函数使得CPerson的实现更加容易。它们的关系因此为“is-implemented-in-terms-of”,我们知道这种关系可以被表现为其它两种形式:通过组合(Item 38)和private继承(Item 39)。Item 39指出组合通常讲比Private继承要更好,但如果虚函数需要重定义,private继承就是必须的。在这种情况中,CPerson需要重新定义valueDelimOpen和valueDelimClose,所以使用组合在这里不能工作。最简单直接的解决方案是让CPerson private继承PersonInfo,虽然Item 39解释道如果多做一些工作,CPerson可以使用组合和继承的结合体来有效重定义PersonInfo的虚函数。在这里,我们使用private继承。
但是CPerson同样必须实现IPerson接口,这些接口为public继承所用。这也导致了合理的多继承应用:将一个接口的public接口和一个实现的private继承结合起来使用:
class IPerson { // this class specifies the public: // interface to be implemented virtual ~IPerson(); virtual std::string name() const = ; virtual std::string birthDate() const = ; }; class DatabaseID { ... }; // used below; details are
// unimportant class PersonInfo { // this class has functions public: // useful in implementing
explicit PersonInfo(DatabaseID pid); // the IPerson interface
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 { // note use of MI
public:
explicit CPerson(DatabaseID pid): PersonInfo(pid) {}
virtual std::string name() const // implementations
{ return PersonInfo::theName(); } // of the required
// IPerson member
virtual std::string birthDate() const // functions
{ return PersonInfo::theBirthDate(); }
private: // redefinitions of
const char * valueDelimOpen() const { return ""; } // inherited virtual
const char * valueDelimClose() const { return ""; } // delimiter }; // functions
用UML来表示:
这个例子表明了多继承是有用的和可被理解的。
5. 何时使用多继承
最后,多继承只是面向对象工具包中的另外一个工具。和单继承相比,使用和理解都更加复杂,所以如果你碰到一个和多继承设计基本相当的单继承设计,使用单继承设计基本上会是更好的选择。如果你只能想出多继承设计,你应该更加努力的思考——肯定有一些方法来让单继承正常的工作。同时,多继承有时是最干净的,最容易维护以及最合理的,它能使工作有效进行。如果你碰到这种情况,不要害怕使用它。确保合理而谨慎的使用private继承就可以了。
6. 总结
- 多继承比单继承更加复杂。它会引起新的模棱两可的问题,因此需要使用虚继承。
- 虚继承的使用会增大体积,降低速度,增加初始化和赋值的复杂度。在虚基类中没有数据的情况下使用多继承是最实际的。
- 多继承也有合理的使用场景。一种使用场景涉及到将对接口类的public继承和将对实现类的private继承相结合的情况。
读书笔记 effective C++ Item 40 明智而谨慎的使用多继承的更多相关文章
- 读书笔记 effective c++ Item 39 明智而谨慎的使用private继承
1. private 继承介绍 Item 32表明C++把public继承当作”is-a”关系来对待.考虑一个继承体系,一个类Student public 继承自类Person,如果一个函数的成功调用 ...
- 读书笔记 effective c++ Item 49 理解new-handler的行为
1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...
- 读书笔记 effective c++ Item 43 了解如何访问模板化基类中的名字
1. 问题的引入——派生类不会发现模板基类中的名字 假设我们需要写一个应用,使用它可以为不同的公司发送消息.消息可以以加密或者明文(未加密)的方式被发送.如果在编译阶段我们有足够的信息来确定哪个信息会 ...
- 读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用
1. 什么样的接口才是好的接口 C++中充斥着接口:函数接口,类接口,模板接口.每个接口都是客户同你的代码进行交互的一种方法.假设你正在面对的是一些“讲道理”的人员,这些客户尝试把工作做好,他们希望能 ...
- 读书笔记 effective c++ Item 29 为异常安全的代码而努力
异常安全在某种意义上来说就像怀孕...但是稍微想一想.在没有求婚之前我们不能真正的讨论生殖问题. 假设我们有一个表示GUI菜单的类,这个GUI菜单有背景图片.这个类将被使用在多线程环境中,所以需要mu ...
- 读书笔记 effective c++ Item 35 考虑虚函数的替代者
1. 突破思维——不要将思维限定在面向对象方法上 你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系.你的游戏处在农耕时代,人类很容易受伤或者说健康度降低.因此你决定为其提供一个成员函数, ...
- 读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值
从一开始就让我们简化这次的讨论.你有两类你能够继承的函数:虚函数和非虚函数.然而,重新定义一个非虚函数总是错误的(Item 36),所以我们可以安全的把这个条款的讨论限定在继承带默认参数值的虚函数上. ...
- 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库
1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...
- 读书笔记 effective c++ Item 1 将c++视为一个语言联邦
Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有“合适的”规则看上 ...
随机推荐
- SharePoint 2016 文档库的新功能简介
今天,重装了一下SharePoint 2016,想多了解了解,看到一些自己平时没注意的功能,或者新的功能,分享一下给大家. 1.界面上操作的变换,多了一排按钮,更像SharePoint Online了 ...
- JAVA程序测试之Swing编程
package swingtest; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt. ...
- php框架Yaf路由重写
php框架Yaf路由重写 通常为了友好的URL格式,会进行站点URL的重写,可以在webserver(Nginx)的配置中进行rewrite,也可在在程序端进行 以下使用Yaf框架进行URL的重写,进 ...
- Matlab立体标定mat转换成Opencv的CvMat
最近在做基于双目视觉的三维重建.比较opencv和matlab工具箱的立体标定结果精度时,发现貌似如果手工选取角点不那么离谱的话,matlab标定结果精度更高也更鲁棒.就想先用matlab标定好相机, ...
- 选择 GCD 还是 NSTimer
我们常常会延迟某件任务的执行,或者让某件任务周期性的执行.然后也会在某些时候需要取消掉之前延迟执行的任务. 延迟操作的方案一般有三种: 1.NSObject的方法: gcdTimer 2.使用NSTi ...
- wemall app中基于Java获取和保存图片的代码
wemall-mobile是基于WeMall的android app商城,只需要在原商城目录下上传接口文件即可完成服务端的配置,客户端可定制修改.分享其中关于 保存正在下载的图片URL集合和图片三种获 ...
- (0)写给Web初学者的教案-----Web基础
0,Web基础 一. What is the Web? Can It Eat? 很多同学可能都听说过一个名词叫做“Web”,这个词隐隐约约好像和我们上网相关.但是呢,又很难说的清楚.我们今天每位 ...
- 3404: [Usaco2009 Open]Cow Digit Game又见数字游戏
3404: [Usaco2009 Open]Cow Digit Game又见数字游戏 Time Limit: 3 Sec Memory Limit: 128 MBSubmit: 72 Solved ...
- php简单分页
<?php //详情联系QQ:1195989301 自己引用数据库类文件 $c = $db->get_one("select count(*) from `{$DT_PRE}br ...
- ST HW1 An Error
曾经使用ThinkPHP完成一个网上电子银行的项目,凡是涉及到金钱的,需要注意的问题很多,比如使用float类型,比如金钱的数额不能为负数等等.最初没有考虑到剩余金额不能为负数或者消费的金额不能为负, ...