5. 实现(Implementations)
条款26:尽可能延后变量定义式的出现时间(Postponevariable definations as long as possible)
本条款主要讲:如果你定义了一个变量且该类型带一个构造函数或析构函数,当程序到达该变量时,你要承受构造成本,而离开作用域时,你要承受析构成本。为了减少这个成本,最好尽可能延后变量定义式的出现时间。同时对于循环中的变量定义来说,参考书中关于定义方式的博弈,尽量采用在循环内部定义。
“尽可能延后”不只是应该延后变量的定义,直到非得使用它的前一刻,甚至应该尝试延后这份定义直到能够给它初始实参为止。这样不仅能够避免构造(析构)非必要对象,还可以避免无意义的default构造行为。
条款27:尽量少做转型(Minimizecasting)
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。本条款介绍了C++的四种转型方式,并建议程序员尽量少做转型。
C风格的转型动作看起来像这样:
(T)expression //将expression转型为T
函数风格的转型动作看起来像这样:
T(expression) //将expression转型为T
C++提供了四种新式转型(C++style casts):
(1)const_cast<T>(expression): 移除变量的const属性;
(2)reinterpret_cast<T>(expression):执行低级转换,处理互不相关类型的转换。例如从int到pointerto int;
(3)static_cast<T>(expression):用来强迫隐式转换,完成相关类型之间的转换。例如,同一个类层次结构中一个指针类型到另一个指针类型、int到double、non-const对象转换为const对象等;
(4)dynamic_cast<T>(expression): 主要用来执行“安全向下转型”(safedowncasting)。通常是你想在一个你认定为derivedclass对象身上执行derivedclass操作函数,但你手上却只有一个“指向base”的pointer或reference,只能靠dynamic_cast来处理对象。此外,dynamic_cast也可以用来执行向上强制和交叉强制。
对于这几种类型转换,给出的建议是:
(1)如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast,可以用“使用类型安全容器”或“将virtual函数往继承体系上方移动”的方法替代dynamic_cast;
(2)宁可使用C++style转型,不要使用旧式转型(即C语言风格的转型(T)expression和T(expression))。前者容易识别出来,而且也比较有分门别类的指掌。
条款28:避免返回handles指向对象内部成分(Avoidreturning”handles” to object internals)
不要在类的成员函数中返回handles(包括references、指针、迭代器)指向对象内部成员变量,因为这样可以增加封装性(避免private内部数据被修改),帮助const成员函数的行为像个const,防止空悬、虚吊(dangling)对象(即:handle比其所指对象更长寿)。
有一个概念就是const函数最好返回const对象,这点倒是比较有用。
const Point& upperLeft( )const { return pData->ulhc; }
条款29:为“异常安全“努力是值得的(Strivefor exception-safe code)
提供了防止异常的几个方法:“以对象管理资源”,可以阻止资源泄漏;copyand swap策略:对打算修改的对象(原件)做一个副本,在副本上做修改,修改完成后,将副本和原对象在一个不抛出异常的操作中置换。
异常安全函数提供以下三个保证之一(意思是,已经是异常安全,满足上面两个条件,在这个基础上再谈问题)
1、基本承诺:异常被抛出,程序内仍然保持有效状态(说白了就是满足上面两个条件)
2、强烈保证:异常回到以前的状态(要么成功,要么回到解放前)
3、不抛掷异常
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者
条款30:透彻了解inlining的里里外外(Understandthe ins and outs of inlining)
Inline函数可免除函数调用成本,提高程序执行效率,但它也会带来负面影响:(1)增大目标代码的大小。即使使用虚拟内存,inline造成的代码膨胀会导致额外的换页行为,降低cache命中率,降低程序执行速度。(2)inline 函数无法随着程序库的升级而升级。换句话说如果f 是程序库内的一个inline 函数,客户将“f 函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f 的客户端程序都必须重新编译。然而,如果f是non-inline函数,只需要重新连接就好了。
Inline函数无法设立断电,因此在调试版程序中禁止发生inline。这使得我们在决定哪些函数应该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inline施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。
1、将大多数inlining 限制在小型、被频繁调用的函数身上才是最明智的选择(根据80-20经验准则,80%的执行时间花在20%的代码上。所以,我们的目标是找到那20%的代码,将它inline并竭力优化)。
2、不要只因为functiontemplate出现在头文件,就将它们声明为inline
条款31: 将文件间的编译依存关系降至最低(Minimizecompilation dependencies between files)
将“编译依存最小化”的基本思想是:相依于声明式,而不是定义式。主要手段是:Handleclasses和Interfaceclasses。
(1) Handleclass。main class内含一个指针成员,指向其实现类。这般设计常被称为pimplidiom (pimpl 是”pointerto implementation” 的缩写,这种class称为“Handleclass”。例如下面的代码:
#include <memory> //为了std::tr1::shared_ptr而含入
class Personimpl; //Person实现类的前置声明
class Person
public:
...
private;
std::tr1::shared_ptr<Personimpl> PImpl; //指针,指向实现物
};
(2)Interfaceclasses,就是抽象基类。
· 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handleclassed和Interfaceclasses。
· 程序库头文件应该以“完全且仅有声明式”(fulland declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。