实现
26.尽可能延后变量定义时的出现时间
你不只应该延后变量定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止
考虑一个经常出现的问题(如下):如果classes的一个赋值成本低于一组构造+析构成本,做法A大体而言比较高效。尤其当n很大的时候。否则做法B比较好。此外,做法A造成名称w的作用于比做法B大,有事对程序的可理解性和易维护性造成冲突
// 方法A
Widget w;
for (int i = 0 ; i < n; i++) {
w = xxx;
}
// 方法B
for (int i = 0 ; i < n ; i++) {
Widget w(xxx);
}
27. 尽量少做转型动作
如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有一个设计需要转型动作,试着发展无须转型的替代设计
如果转型是必要的,试着将它隐藏域某个函数背后。客户随后可以调用该函数,而不需将转型放进它们自己的代码
宁可使用C++-style转型,不要使用旧式转型。前者容易辨识出来,有分门别类的职掌
- 转型会破坏类型系统。所以干净通过编译,是指它不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作
- 四种新型转型(如果需要转型,请始终使用新式转型)
– const_cast:通常被用来将对象的常量性转除
– dynamic_cast:主要用来执行“安全向下转型”(基类转派生类),这是在运行期决定转型的操作,可能耗费重大的运行成本
– reinterpret_cast:执行低级转型,实际动作(及结果)可能取决于编译器,这儿就表示它不可移植
– static_cast:强迫隐式转换 - 单个对象可能拥有一个以上的地址(例如“以Base*指向它”时的地址和“以Derived*指向它”时的地址)。这是C++可能会发生的,而实际上,一旦使用多重继承,这种事几乎以致发生。这是因为,转型的时候会存在一个偏移量,这个量与对象的布局方式和它们的地址计算方式有关,而这两样东西有何编译器相关
- 因为dynamic_cast是运行时的操作,注意继承情况下,虚函数内的dynamic_cast,倘若虚函数存在嵌套调用,可能导致多次dynamic_cast,增加运行时间
- dynamic_cast通常运用场景:你想在一个你认定为派生类的对象上执行派生类的操作,但现在只有指向base的指针或引用。两个一般性做法可以避免这种情况:
– 使用容器并在其中存执直接指向派生类的指针
– 在base class内提供virtual函数做你想对各个派生类做的事,即使有些派生类用不上 - 个人理解:C++的转型会破坏类型系统,带来不必要的错误,C++需要尽量避免转型,实在无法避免,则用新的转型方法转型,它们自带异常检测
28.避免返回handles指向对象内部成分
遵循这条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低
- handle:reference、指针和迭代器统称handles(号码牌)
- 成员变量的封装性最多只等于“返回其reference的函数的访问级别”
- 如果const成员函数传出一个reference,后者所指数据和对象自身有关联,而它又被存储在对象外,那么这个函数的调用者可以修改那个数据
- 除了数据,私有或者protected的函数也属于内部信息,不要返回它们的handles
- 空悬的号码牌:返回了handles,但因为某些情况,所指对象不存在
个人理解:这一条主要是分析封装型的,意思是如果你的成员函数能够返回非const的指针或者reference指向私有函数,其实破坏了封装性;尽量不要这样,但不是绝对
29.为“异常安全”而努力使值得的
异常安全函数即使发生异常也不会泄露资源或者允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型
“强烈保证”往往能够以copy-and-swap实现,但“强烈保证”并非对所有函数都可实现
函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全保证”的最弱者
- 异常安全性是指,在异常被抛出时满足:不泄露任何资源;不允许数据败坏
- 对于泄露资源,采用智能指针解决
- 基本型(基本承诺):如果异常被抛出,程序内任何事物仍保持在有效
- 强烈保证:异常被抛出,程序状态不改变
- 不抛出异常:总能够完成原先承诺的功能
- int doSomething() throw(),注意,这句的意思不是不抛出异常,而是如果抛出,将是严重错误
- 不要为了表示某件事情发生而改变对象状态,除非它真的发生了
个人理解:写代码的时候,需要对函数考虑异常安全性,首先通过智能指针防止资源泄漏,其次根据情况选择所需的安全性级别,应该挑选“现实可实施”条件下的最强烈等级,但总体而言,我们的抉择往往落在基本保证和强烈保证之间
30.透彻了解“inlining”的里里外外
将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序速度提升机会最大化
不要只因为function Template出现在头文件就将它们声明为inline
- inlining好处:比宏好得多,可以调用它们而不需受函数调用带来的额外开销;编译器最优化机制通常被设计用来浓缩“不含函数调用”的代码,inline某个函数,或许编译器有能力对它的执行语境相关最优化
- inlining坏处:它的观念是“对此函数的每个调用”都用函数本体替代,在编译过程完成,将增加你的目标码大小;如果函数库内有一个inline函数,一旦改变,所有用到该函数的客户端都必须重新编译
- inline只是对编译器的申请,不是强制命令。申请可以隐喻提出也可以明确提出。隐喻方式是讲函数定义在class定义式内(也就是一般写头文件的时候,直接在头文件内写函数体就是隐喻)
- lnline函数是否inline取决于你的建置环境,主要取决于编译器。因为inline只是申请,但假如它们无法将你要求的函数inline,编译器一般会给你警告信息
- 不inline的情况:函数太过复杂;函数是virtual;通过函数指针进行调用的地方;构造函数和析构函数(即使是空的,但是C++内部会对这两个函数做基本的操作,本身就带有大量代码)
31.将文件间的编译依存关系降至最低
支持“编译依存性最小化”的一般构想:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes 和 Interface classes
程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用
- 一般写代码,头文件中包含其他的头文件,当其中一个头文件改变的时候,包含它的所有文件全部都要重新编译,也就是编译依存关系较强
- 所谓相依于声明式不是定义式,本质是让头文件尽可能自我满足。
- 简单的,意思是头文件里面,只是申明要用到的class,不要去include它,在定义的cpp文件里面include,然后,使用的时候尽量用reference和pointer去解决,因为定义这些handler的时候,只需要知道声明而不需要定义类型;其次,定义复杂class的时候,写3个文件,一个写依赖的class的声明式,另外是标准的h和cpp文件。这样,至少保证类间的功能外壳互不相关
- 然后是2种手段:
– handle class:将实现的类分成两个类,一个(handle class)只有若干功能外壳和实际实现这些功能的类的指针;一个是实现这些功能的类(Impl class),handle class的每个函数都调用对应的Impl class的功能函数。这样实现和声明就分开了,当修改了某个头文件,只有handle的实现类需要重新编译(只有它include)
– Interface class:其实就是工厂模式,实现好ABC,实现对应的派生类,实现工厂生成各种派生类并调用
– 缺陷:这两种手段都有不足。Handle class必须实现指针,每次调用函数还包含一次指针调用函数,增加访问的间接性;Interface class因为有abc,所以很多virtual函数,除了增加内存(虚表),还增加一个跳跃
个人理解:这种手段不可能完全使得编译依存变无。而且,当它们导致速度或大小差异过于重大以至于classes之间的耦合不成为关键时,就用具象类替换它们