读书笔记 Effective C++(3)

26、尽可能延后变量定义式的出现时间

这样做可以增加程序的清晰度并改善程序效率。如果程序异常提前退出而变量定义在前,则该变量不会被使用但是还是付出了构造和析构的成本,因此将变量定义延后直到使用变量之前,甚至延后到能给予初值之前,这样可以避免无意义的default构造函数。

27、尽量少做转型动作

尽量避免转型,特别是避免dynamic_cast,其运行速度很慢,如果某个设计需要转型动作,试着找一些替代做法,如virtual函数,如果转型是必要的,将其隐藏于某个函数里。
使用C++新式转型不要用旧式转型,前者很容易识别出来。

28、避免返回handles指向对象内部成分

避免返回handles(指针,引用,迭代器)指向对象内部,一是提升封装性,如果指向private成员,则用户可以修改这些成员,private就没用了,二是帮助const成员函数的行为像一个const,并降低空悬的可能性。

29、为异常安全努力是值得的

异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构被破坏,这样的函数分为3种可能,基本型(若异常被抛出,程序内任何事物仍然处于有效状态下,有可能发生改变)、强烈型(要么全变,要么不变)、不抛异常型。

强烈保证往往能够以copy and swap实现出来,但不一定对所有函数都有实现意义,例如copy的成本太高。

一个函数提供的异常安全性最高是它所调用的各个函数的安全性的最弱的一个。

30、透彻了解inlining的里里外外

  • inline将每一处调用函数的地方都用函数本体替换,提升运行速度但会导致代码膨胀,因此inline函数适合体积小、调用频繁的函数
  • 在类内部定义的函数默认是inline的
  • inline只是一种申请,实际上到底实施inline还要取决于编译器的决定
  • 因为inline是编译时决定,而virtual是运行时决定,所以对一个virtual函数inline没有意义。
  • 如果需要对inline的函数取地址,编译器往往会生成一个no-inline的函数,当用函数指针调用这个函数时,使用的是非inline的版本。
  • inline函数往往不利于调试,无法设置断点

31、将文件间的编译依存关系降至最低

支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式,两个手段是Handle class(pimpl)和Interface class(抽象类)。

程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用。

32、确定你的public继承塑模出is-a关系

public继承意味着is-a,对基类适用的所有操作对派生类也适用,因为每个派生类对象也是一个基类对象。

33、避免遮掩继承而来的名称

派生类内的名称会遮掩基类内的名称,可以适用using声明来让基类的名称可见,或者使用转交函数,转交函数显式调用基类的函数。

34、区分接口继承和实现继承

接口继承和实现继承不同,在public继承下,派生类总是继承基类的接口。

纯虚函数只具体指定接口继承,派生类必须自己实现。

虚函数指定接口继承和缺省实现继承,派生类可以选择实现,可以用纯虚函数来实现,基类的纯虚函数定义缺省行为,在派生类的虚函数实现中显式调用基类的纯虚函数。

非虚函数实现接口继承和强制性实现继承,派生类不应该重新定义。

35、考虑virtual函数以外的其他选择

虚函数的替代方案,第一个是NVI(Non-Virtual Interface)手法,是一种Template Method设计模式,定义一个非虚的public函数调用private的虚函数,这个非虚函数成为外覆器,这么做的好处是在虚函数调用前后,还可以做其他的处理,如互斥锁上锁,制造日志等,只用virtual函数无法做到这些。

第二个是所谓的Strategy模式,用函数指针代替虚函数,创建对象时传递相应的函数指针,这样的好处是对于同一类型可以有不同的表现方法,传递的函数不同表现则不同,有更大的弹性,若对象的表现在运行期会改变,也可以提供一个setMethod函数方便地进行改变,但是这样的函数若需要访问类的私有成员就不得不声明为友元,降低了类的封装性。
还可以用function来实现Strategy模式,function可以保存的是所有可以调用的对象,包括函数指针,仿函数,成员函数等,而且也允许兼容,例如返回类型或者参数类型可以隐式转换成所需的类型,这样的函数也可以放进function里面。
一种古典的Strategy模式是将体系内的virtual函数替换为另一个继承体系中的virtual函数。

36、绝不重新定义继承而来的non-virtual函数

由于静态绑定的原因,当用基类指针调用non-virtual函数时永远是调用基类的,因此不要重新定义继承来的non-virtual函数。

37、绝不重新定义继承而来的缺省参数值

缺省参数值是静态绑定,而虚函数是动态绑定,当缺省参数不同时,基类指针只会调用基类的缺省参数不会用派生类的。

38、通过复合塑模出has-a或者“根据某物实现出”

public继承是is-a,而在类里定义另一个类的对象则是has-a或者is-implemented-in-terms-of a(由a实现出)。

39、明智而审慎地使用private继承

private继承是一种is-implemented-in-terms-of,private继承的派生类对象不会被转为基类对象,类似于复合,根据某物实现出,尽可能使用复合而不是private继承,当需要访问protected成员和virtual函数时,使用private继承。

private继承可以利用empty base最优化:

1
2
3
4
5
6
class Empty{};
class HoldInt{
private:
int x;
Empty e;
}

上面这个复合的例子,空的类的size并不是0,编译器往往会放入一个char,size往往是1,因此HoldInt的size加上对齐达到了8,而使用private继承就不会有这种问题:

1
2
3
4
5
class Empty{};
class HoldInt : private Empty{
private:
int x;
}

这时HoldInt的size就为一个int的大小。

40、明智而审慎地使用多重继承

多重继承比单继承复杂,可能导致二义性,即两个基类中有同名函数,不知道该调用哪一个,此外如果一个基类在继承路径上出现多次还可能需要虚继承。

虚继承会增加大小、速度、初始化复杂度等成本,如果虚基类不带任何数据是最具有实用性的情况。

多重继承的一个用途是,其中一个以public继承某个interface,另一个以private继承某个实现。

分享到