读书笔记 effective c++ Item 39 明智而谨慎的使用private继承
1. private 继承介绍
Item 32表明C++把public继承当作”is-a”关系来对待。考虑一个继承体系,一个类Student public 继承自类Person,如果一个函数的成功调用需要从Student到Person的隐式转换,这时候“is-a”关系就出现了。对于一部分实例,使用private继承来代替public继承也是有价值的事情:
class Person { ... };
class Student: private Person { ... }; // inheritance is now private void eat(const Person& p); // anyone can eat void study(const Student& s); // only students study Person p; // p is a Person Student s; // s is a Student eat(p); // fine, p is a Person eat(s); // error! a Student isn’t a Person
很清楚的是,private继承并不意味着“is-a”关系。那它意味着什么呢?
在我们看到结果之前,先让我们看一下private继承的行为。Private继承的第一条规则也是刚才从实际中看到的:与public继承相反,如果类之间的继承关系是privte继承,编译器不会将派生类对象(Student)转换成为基类对象(Person)。这也是为什么为对象s调用eat会失败。第二条规则是即使在基类中的成员是protected或者public的,从此基类中private继承而来的成员会变成派生类中的private成员。
这就是private继承所表现出来的行为。我们也从中看到了结论:private继承意味着“is-implemented-in-terms-of”。如果你让类D private继承自类B,你这么做是因为你想利用类B中的一些让你感兴趣的性质,而不是因为在类型B和类型D之前有任何概念上的关系。因此,private继承纯粹是实现上的技术。(这也是为什么你从private基类中继承而来的任何东西在你的类中都变为了private的:所有的都只是实现上的细节。)使用Item34中引入的术语,private继承意味着只是继承了实现;而接口应该被忽略掉。如果类D private继承自类B,就意味着D对象的实现依赖于类B对象,没有别的意思了。Private继承在软件实现层名才有意义,在软件设计层面是没有意义的。
2. 如何在private继承和组合之间做出选择
Private继承意味着“is-implemented-in-terms-of”的事实会让你感觉有一些不安,因为Item 38中指出组合(composition)也同样意味着“is-implemented-in-terms-of”。怎么在它们之间做出选择?答案是简单的:在任何可能的时候使用组合(composition),在必须使用private继承的时候才去使用它。什么时候必须使用?主要是当protected成员或者(和)虚函数被牵扯进来的时候,也有处在边界的情况,因为空间原因而不能使用private继承。我们过后再去担心它,毕竟它是处在边界的情况。
2.1 一个不能简单的使用public继承的例子
假设我们正在一个涉及到Widgets类的应用上工作,我们想更好的理解Widgets是如何被使用的。例如,我们不但想知道Widget成员函数的调用有多频繁,我们同样想知道函数调用随时间变化的频率变化情况。程序在不同的执行阶段会有不同的行为轮廓。举个例子,编译器对函数的使用会大大的不同于优化和代码生成时对函数的使用。
我们决定修改Widget类来对每个成员函数的调用次数进行追踪。在运行时,我们会定期来检查这项信息,可能也会伴随着检查每个Widget对象值和其它一些我们认为有用的数据。为了达到这个目的,我们会创建一个定时器于是我们可以知道什么时候去收集这些统计信息。
我们更乐意去重用代码而不是实现新代码,我们翻阅了工具集,很高兴的找到了如下的类:
class Timer {
public:
explicit Timer(int tickFrequency); virtual void onTick() const; // automatically called for each tick ... };
这正是我们要找的。我们可以为这个Timer对象配置任意的tick频率,在每个tick发生的时候,它会调用一个虚函数。我们可以重定义这个虚函数来检查Widget世界的当前状态。非常完美!
为了让Widget在Timer中重定义一个虚函数,Widget必须继承自Timer。但public继承是不合适的。因为Widget不是一个Timer。Widget客户不应该在一个Widget对象上调用onTick,因为onTick不是Widget的接口。并且允许这样的函数调用会使得客户很容易出现对Widget接口的误用,这很明显的违反了Item 18的忠告:使接口容易被正确使用不容易被误用。Public继承在这里不是有效选择。
2.2 使用private继承
所以我们在这里使用private继承:
class Widget: private Timer {
private: virtual void onTick() const; // look at Widget usage data, etc. ... }
凭借private继承的力量,Timer的public onTick函数在Widget中变为了private,我们将其放在private关键字下并对其进行了重新声明。
2.3 使用组合(compostion)以及两个优点
这是个很好的设计,但如果private继承不是必须的,它就没有任何价值。如果我们决定使用组合(compostion)来替代private继承。我们可以在Widget内部声明一个内嵌类,此类public继承Timer,在Timer中重新定义onTick,然后在Widget中声明一个此类型的对象。下面是这个方法的实现:
这个设计比private继承更加复杂,因为它同时涉及到(public)继承和组合(composition),同时引入了一个新类(WidgetTimer)。我用这个例子是提醒你如果有多种方法来处理一个设计问题,训练自己考虑多种方法是值得的(Item 35)。然而,我能想出两个原因来证明使用public继承加组合比private继承更好。
第一, 你可能想使用Widget作为其他类的基类,但是你可能想阻止派生类重新定义onTick。如果Widget继承自Timer,这是不可能的,即使继承是private继承。(回忆一下Item 35,即使虚函数是private的,派生类还是可能重新定义它)但是如果WidgetTImer在Widget中是private的,并且继承自Timer,Widget的派生类就没有对WidgetTimer的访问权,也就不能继承它或者重新定义它的虚函数。如果你使用java或者C#,并且发现C++没有阻止派生类重定义虚函数的能力(Java使用final methods,C#使用sealed),现在你有方法在C++中对此行为进行模拟了。
第二, 你可能想最小化Widget的编译依赖性。如果Widget继承自Timer,当Widget被编译的时候必须能够得到Timer的定义,所以定义Widget的文件必须#include Timer.h。从另外一个角度讲,如果将WidgetTimer移出Widget并且Widget只包含一个指向WidgetTimer的指针,在Widget中对WidgetTimer进行简单的声明就可以了,不需要#include与Timer相关的任何头文件。对于大型系统来说,这样的解耦是很重要的。(编译依赖的详细介绍看Item 31)
2.4 使用private继承比组合更加合理的例子
早些时候我指出来在派生类想要访问基类的protected部分或者想去重定义基类的虚函数的时候private继承才是有用的,但是类之间的关系是”is-implemented-in-terms-of”而不是“is-a”。然而,我同时指出有一种涉及到空间优化的边缘情况可以促使你更加喜欢private继承而不是composition(组合)。
这种边缘情况确实靠边缘:它只应用在没有数据的类中。这种类没有非静态数据成员;没有虚函数(因为虚函数的存在会为每个对象添加一个vptr指针,见Item 7);没有虚基类(因为这样的基类同样会引入间接费用,见Item40)。从概念上来说,这样的空类对象应该不使用空间,因为对象中没有数据需要保存。然而由于技术的原因,C++使得独立对象必须占用空间,所以如果你写下下面的代码:
class Empty {}; // has no data, so objects should
// use no memory class HoldsAnInt { // should need only space for an int private:
int x; Empty e; // should require no memory };
你会发现sizeof(HoldsAnInt)>sizeof(int):一个Empty数据成员也会占用空间。对于大多数编译器来说,sizeof(Empty)为1,因为C++法则处理大小为0的独立对象时会默认向” empty ”对象中插入一个char。然而,内存对齐的需求(见Item 50)可能导致编译器向HoldsASnInt这样的类中添加填充物,所以HoldsAnInt对象不会只多出来一个char的大小,实际上会增加足够的空间来容纳第二个int。(在我测试过的所有编译器中,上面描述的填充也确实发生了。)
但是可能你注意到了我非常小心的说明是“独立”(freestanding)对象占用的空间必须不能为0。这个限制不能被应用在派生类对象的基类部分中,因为他们不是“独立“的。如果你继承自Empty类而不是包含一个Empty类型的对象,
class HoldsAnInt: private Empty {
private:
int x;
};
你就会发现sizeof(HoldsAnInt)==sizeof(int)。这被称作EBO(empty base optimization),并且我测试过的编译器都通过了这个测试。如果你是一个库开发人员,如果其客户对空间十分关心,那么了解一下EBO是很值得的。并且你需要知道EBO一般只在单继承下才是可行的。管理C++对象布局的规则通常意味着EBO不能被应用在有多个基类的派生类中。
事实上,“empty“类不是真的空。虽然它们永远不会拥有非静态数据成员,它们通常会包含typedefs,enums,静态数据成员或者非虚函数。STL在技术上有很多包含有用成员(通常为typedefs)的空类,包括基类unary_function和binary_function,用户定义的函数对象会继承这些类。多亏了EBO的广泛使用,使得这些继承很少会增加派生类的大小。
2.5 结论
让我们回到基本议题。因为大多数类不是空的,所以EBO不是使用private继承的合法理由。进一步来说,大多数继承对应着”is-a”,这也是public继承的工作而不是private继承。组合和private继承都意味着“is-implemented-in-terms-of“,但是组合更容易理解,所以你应该在任何可能的情况下使用它。
当你处理两个类时,它们不是“is-a“的关系,一个类要么需要访问另外一个类的protected成员要么需要重新定义一个或多个它的虚函数,这时候private继承在大多数情况下会是合法的设计策略。即使在这种情况中,我们看到public继承和包含(containment)的混合使用通常情况下能够产生我们需要的行为,虽然增加了设计复杂性。明智而谨慎的使用private继承就意味着,在你考虑过所有的替代方法之后,在你的软件中它是表示两个类关系的最好方法,在这种情况下才去使用它。
3. 总结
- Private继承意味着“is-implemented-in-terms-of “。它通常比组合的使用要低一个层次,但是当派生类需要访问protected基类成员或者需要重新定义继承而来的虚函数时使用Private继承有意义的。
- 不像组合,private继承能够使用空基类优化。这对努力减少对象大小的库开发者来说很重要。
读书笔记 effective c++ Item 39 明智而谨慎的使用private继承的更多相关文章
- 读书笔记 effective C++ Item 40 明智而谨慎的使用多继承
1. 多继承的两个阵营 当我们谈论到多继承(MI)的时候,C++委员会被分为两个基本阵营.一个阵营相信如果单继承是好的C++性质,那么多继承肯定会更好.另外一个阵营则争辩道单继承诚然是好的,但多继承太 ...
- 读书笔记 effective c++ Item 22 将数据成员声明成private
我们首先看一下为什么数据成员不应该是public的,然后我们将会看到应用在public数据成员上的论证同样适用于protected成员.最后够得出结论:数据成员应该是private的. 1. 为什么数 ...
- 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库
1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...
- 读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型
1. 何为public继承的”is-a”关系 在C++面向对象准则中最重要的准则是:public继承意味着“is-a”.记住这个准则. 如果你实现一个类D(derived)public继承自类B(ba ...
- 读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来
1. 使用模板可能导致代码膨胀 使用模板是节省时间和避免代码重用的很好的方法.你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定 ...
- 读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定
Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些 ...
- 读书笔记 effective C++ Item 33 避免隐藏继承而来的名字
1. 普通作用域中的隐藏 名字实际上和继承没有关系.有关系的是作用域.我们都知道像下面的代码: int x; // global variable void someFunc() { double x ...
- 读书笔记 effective c++ Item 43 了解如何访问模板化基类中的名字
1. 问题的引入——派生类不会发现模板基类中的名字 假设我们需要写一个应用,使用它可以为不同的公司发送消息.消息可以以加密或者明文(未加密)的方式被发送.如果在编译阶段我们有足够的信息来确定哪个信息会 ...
- 读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用
1. 什么样的接口才是好的接口 C++中充斥着接口:函数接口,类接口,模板接口.每个接口都是客户同你的代码进行交互的一种方法.假设你正在面对的是一些“讲道理”的人员,这些客户尝试把工作做好,他们希望能 ...
随机推荐
- .NET Core在WindowsServer服务器部署及发布
VS使用WEB DEPLOY发布.NET Core程序 背景是这样的,公司有两台服务器,平时一台备用,另一台做为主生产机器.当有大量补丁或者安装什么东西需要重启的时候,交其中一台直接关掉IIS,然 ...
- swift 运算符快速学习(建议懂OC或者C语言的伙伴学习参考)
昨晚看了swift 的运算符的知识点,先大概说一下,这个点和 c 或者oc 的算运符知识点一样,都是最基础最基础的.其他的最基本的加减乘除就不多说了.注意的有几点点..先说求余数运算: 一 :求余数运 ...
- python - bilibili(二)出错的解决办法
在获取房间号之前我们先解决上篇文章遗留的bug,即输入的房间号不是数字和对应的房间号不存在而产生的问题. 输入的房间号不是数字: 在python中,你所输入的必定是字符串,虽然你输入的是数字,但是类型 ...
- weex官方demo weex-hackernews代码解读(1)
一.介绍 weex 是阿里出品的一个类似RN的框架,可以使用前端技术来开发移动应用,实现一份代码支持H5,IOS和Android.最新版本的weex已默认将vue.js作为前端框架,而weex-hac ...
- JavaScript中国象棋程序(2) - 校验棋子走法
"JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第2节. 这一系列共有9个部分: 0.JavaScript中国象 ...
- 纪中集训 Day 5
不知不觉已经day 5了啊 今天早上醒来,觉得要AK的节奏,结果就立flag了 - - 30分QAQ 其实第一题应该得想得到的,还有T2也能够解决的(话说后来看别人的代码写的好赞啊QAQ) 然后下午就 ...
- jQuery中绑定事件bind() on() live() one()的异同
jQuery中绑定事件的四种方法,他们可以同时绑定一个或多个事件 bind()-------------------------版本号小于3.0(在Jquery3.0中已经移除,相应unbind()也 ...
- UCOSII时间任务块
转:http://blog.csdn.net/wchp314/article/details/5416476 uCOS-II的任务控制块 标签: uCOS-II 2009-12-01 14:45 ...
- iOS开发——设计模式那点事
单例模式(Singleton) 概念:整个应用或系统只能有该类的一个实例 在iOS开发我们经常碰到只需要某类一个实例的情况,最常见的莫过于对硬件参数的访问类,比如UIAccelerometer.这个类 ...
- Spring-mvc介绍
Spring-mvc介绍 1.1市面上流行的框架 Struts2(比较多) Springmvc(比较多而且属于上升的趋势) Struts1(即将被淘汰) 其他 1.2 spring-mvc结构 1 ...