本文主要是我在读《Effective C++》时的笔记,记录在此,以便以后能够重头再读这本书时,再看这些笔记会重新有所体会。

基础话题

2. 以const,enum,inline替换#define

即宁可以编译器替换预处理器。好处在于一些错误编译器是可以检查出来的,但是预处理器
却不会为我们做任何的检查,它只是简单的进行替换。

3. 尽量使用const

const变化多端,它可以用来修饰变量,也可以用来修饰成员函数。当它修饰变量的时候,表明这个变量是不可变的,即常量;而当它修饰成员函数的时候,则告诉编译器,在这个函数内部,不会更改任何的成员变量(即该函数可以作用于const对象)。一个容易搞错的情况是当const修饰指针的时候:当const修饰符出现在星号左边,表明被指对象是常量;而当const出现在右边,则表明指针是常量,指针不可更改,但它所指对象的值可以更改。

当我们看见某段代码中出现函数返回const非引用常量的时候,第一反应大都点可能感觉有点怪。但是,它有时候却是有效的,因为它可以避免用户错误地将该函数直接作为一个左值(可能我们本来是想写\=\=,结果却不小心写成了\=,这时候返回常量就会造成编译器报错)。

注意mutable的出现是为了解决什么问题。另外,对于C++的编译器来说,const只能保证bit-wise常量性,但对于实现者来说,我们应该保证概念上的常量性。

4. 关于构造、赋值、析构函数

编译器可以偷偷地为class创建默认构造函数,复制构造函数,copy
assignment操作符和析构函数。

而如果某一个类是多态基类,那么应该为它声明虚析构函数。

资源管理

13. 使用对象管理资源

对于C/C++,它不支持垃圾回收,因此要实现自动资源的管理,只能依赖于智能指针,它利
用析构函数自动调用释放机制。

  • 由于auto_ptr假设没有一个以上的auto_ptr同时指向受管理的同一资源,因此导致带引用计数的智能指针出现(如shared_ptr)。
  • RAII(Resouce Acquisition Is Initialization
  • 尽量使用智能指针来管理内存分配资源。

14. 注意资源管理类的复制行为

有几种处理RAII类的复制构造函数的方式:

  1. 禁止复制
  2. 引用计数(shared_ptr允许自定义删除器,而不一定是释放内存资源)
  3. 复制底部资源
  4. 转移拥有权(如auto_ptr就是一旦出现复制,原来的智能指针就指向空)

15. 对于资源管理类,提供获得原始指针的接口

有些API可能接受裸指针作为输入参数,这时候需要提供一个这样的接口。可以使用一个类
似于get函数直接提取出裸数据;或者用隐式转换函数(不安全,见书中例子)

class Font {
public:
   ...
   //FontHandle is f's type
   operator FontHandle() const {return f;}
   ...
};

:接口应该容易被正确地使用,总体来说,显式比较安全,隐式比较方便

16. 正确地成对使用new和delete

new了一个数组,那就用delete [],而且要配对使用。

17. 以独立语句将new对象放入智能指针

先看一个例子:

//declaration
void func(std::tr1::shared_ptr<Widget> pw, int priority);

func(shared_ptr<Widget>(new Widget), priority());

后面调用函数的时候,第二个参数是一个返回int的函数,而第一个参数是直接把初始化
智能指针和new出新数据放到了一起,这时调用func之前,编译器需要执行new Widget
调用priority,调用shared_ptr构造函数,但具体priority是在什么时候被调用,不
得而知,只是new一定等于构造函数。有可能先new数据了,然后调用priority,这时
一旦调用出现差错,导致构造函数没有被调用,内存就发生了泄露。

所以最好就是先单独地把shared_ptr<Widget>(new Widget)写出来作为单独的一个变量,
然后把这个变量作为函数的输入参数。

设计与声明

18. 使接口易于使用不易误用

如果代码通过了编译,其作为就该是预期的。主要是要考虑严密,既然提供接口,就该把所
有的问题都考虑在其中。

  • 接口一致性以及与内置类型行为兼容
  • 建立新类型,限制类型上操作,束缚对象值以及消除客户的资源管理责任

19. 把类当作一个类型来设计

设计新类时的一些要考虑的问题:

  1. 新type对象的创建与销毁(构造函数与析构函数的设计)
  2. 对象初始化与对象赋值差别(分别对应构造函数与赋值操作符的重载)
  3. 新type的对象如果被以值传递(passed by value),意味着copy构造函数需要定义,即便是编译器自动生成
  4. 新type的合法值(有的类型变量取值只能是特定的某些数值集,特别注意三大函数的错误检查)
  5. 继承行为是否要被考虑(除了新type是继承自其它类,还有新type是否以后会被其它类继承,这时考虑析构函数是不是要写成virtual)
  6. 与某些类型的相互转换,是不是需要提供隐式的转换或explicit转换(见条款15)
  7. 操作符与函数的考虑
  8. 保护级别(protected等的考虑)
  9. 未声明接口(不懂
  10. 是否有必要把这个类型写成一个模板类

20. 尽量传入常量引用作为参数

有多个理由支持这样做,一方面效率可以得到提高(回避构造函数与析构函数),另一方面
可以支持多态,避免切割问题; 对于内置的类型,传值效率更高一些,因为在C++编译器
的底层,引用是用指针来实现的,这也适用于STL迭代器和函数对象

21. 返回值的时候谨慎返回引用

返回对象的时候,不能返回reference。因为一方面函数内部创建的元素一旦脱离函数就会
被自动销毁;如果是在堆上创建元素,返回出来的元素将导致内存泄露,因为其指针无法被
外界所用。

22. 成员变量应当声明为私有

这一点需要仔细思考。封装很重要。private提供了封装,而其它的(protected和 public
)则不提供封装

23. 尽量用非成员非友元函数替代成员函数

还是封装性的原因,非成员非友元函数可以提供更强的封装特性。将所有便利函数放在多个
头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数。只需要添加
其它的非友元非成员函数到此命名空间中。

24. 当类型转换应用到所有参数时,考虑非成员函数

注意这里说的是如果函数的所有参数都进行类型转换。如果我们定义了一个有理数类(
Rational),让它支持整数到有理数类的隐式转换是有必要的。但是如果把有理数相乘声
明为成员函数,那么2*Rational(4, 2)就是不合理了,最好是声明为非成员函数,这样使
用的时候就很自由和随意了。还有一个需要考虑的问题就是要不要把乘法声明为友元函数呢
?如果已经很直接地提供了get元素的接口,是没有必要的。

25. 考虑支持不抛出异常的swap函数

  1. public swap成员函数
  2. 在class或template所在命名空间提供一个非成员swap函数并令它调用swap成员函数
  3. 特化std::swap,并令它调用swap成员函数

swap成员函数绝不能抛出异常,因为swap的一个最好的应用就是帮助classes提供强烈的异
常安全性保障。见Item 29

注意一下Koenig lookup法则

  • std::swap对类型效率不高,提供一个swap成员函数,并确保它不抛出异常
  • 提供成员函数swap外,还须提供一个非成员swap函数来调用它,如果这个类不是模板类,就把非成员swap函数写成std::swap的特化版本
  • 调用swap时应该使用using std::swap声明式,然后直接调用swap而不加任何命名修饰
  • 如果类是模板,可以进行std templates的全特化,但不要在std内加入某些对std全新的东西

实现

这一章比较难,需要细看,可以参考《深入C++对象模型》

26. 延后变量定义出现的时间

效率上可以避免不必要的构造函数的创建;另外增加程序清晰度。考虑定义的同时初始化变
量与定义后赋值初始化变量两个的区别,如果是在一个循环体内,而且赋值操作比较耗时的
话,那不如把变量定义放在循环内部。

27. 少做类型转换

C风格的转型有两种:

(T) expression
T   (expression)

C++风格转型操作:

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

28. 避免直接返回指向对象内部的handle

这里的handle指的就是引用,指针,迭代器等。不管是返回引用还是返回指针,也不管
是不是加const,返回了内部对象就会有一定的问题,所以尽可能不要把内部元素显露出来
。但有时候也不得不暴露出来(如vector的[]重载)。

遵守上述规则可以增加封装性,帮助const成员函数的行为像个const,并将悬空handle的可
能性降到最低。

29. 为异常安全而努力

copy and swap策略:为打算修改的对象作一份副本,然后在副本上做一切必要的修改
。等到一切成功后,再将修改过的副本与原对象在一个不抛出异常的操作中转换。

30. 关于内联函数

inline函数背后的观念是:对每一个调用此函数的地方都以函数本体替换之,即这可能增加
目标代码的大小。所以inlining限制在了小型被频繁调用的函数上。

将函数定义于一个class内也表明这个函数是一个inline函数。通常这样的函数是成员函数
或友员函数。

虽然inline和template都需要在文件头内定义,但并不意味着函数模板一定是inline的。
inline需要定义在文件头的原因就在于编译器需要在编译期确定函数的主体然后插入到调用
的位置中。因此:

  • 所有对virtual函数的调用会使得inline失效。
  • 如果通过函数指针来调用inline函数,也将使得inline失效。
  • inline函数与构造函数,析构函数(考虑构造函数为inline,则其成员和基类扩展出来的代码也将全部内联,可能会造成程序的膨胀。)
  • inline函数是无法调试的,因为它相当于一个不存在的函数。

31. 最小化编译依赖

  • 编译依存性最小化,基于此构想的两个手段是“Handle classes”和“Interface classes”
  • 程序库头文件应该以“完全且仅有声明式”的形式存在。

关于pimpl:主要是把一个类分成两个classes,一个只提供接口,另一个负责实现接口。这样一方面任何的实现都无须重新编译,另一方面客户也无法看到实现的细节。

  1. 如果使用引用或指针可以完成任务,就不要直接使用object
  2. 如果可以,尽量以class声明式替换class定义式
  3. 注意一下interface class

继承与面向对象设计

32. public继承表现的是is-a特性

classes之间的关系包括:is-ahas-ais-implemented-terms-of

public继承说明了基类的属性子类也全都有,即子类is基类,反之则不行。举个例子,正方
形是矩形,于是就有两个类(Square),(Rectangle),有一个函数接受的参数是对于
Rectangle的引用,只修改Rectangle的宽度,这时把正方形对象传入函数是没有错误的,但
显然将函数应用后的正方形已经失去了正方形的属性。

33. 避免遮盖继承名称

派生类内名称都会遮掩基类内的名称,因为public继承是一个 is-a 关系,所以最好不要这
样做。如果被遮掩了名称,为了希望这些名称被derived class使用,可以使用using声明(
public继承)或转交函数(private继承)

有时候并不想继承基类所有函数,但在public继承下,这绝对不可能发生,因为它违反了
public继承所暗示的“base和子类是is-a的关系”。

34. 接口继承与实现继承的区别

  1. 接口继承与实现继承不同,在public继承之下derived classes总是继承base class的接口
  2. 纯虚函数只具体指定接口继承
  3. 非纯虚函数具体指定接口继承与缺省实现继承
  4. 非虚函数具体指定接口继承与强制性的实现继承

35. 虚函数以外的选择

C++在设计模式中的应用。使用virtual函数+non-virtual函数实现Template Method
NVI手法);使用函数指针或functor亦或tr1::function实现Strategy Pattern

36. 绝不重新定义非虚函数

如条款34所述,非虚函数表明的是接口与实现的双重继承,子类不应该重新定义这样一个非
虚函数。想象如下代码:

Derived obj;
Base* pB = &obj;
pB->nonVirtualFun();
Derived* pD = &obj;
pB->nonVirtualFun();

它们居然会产生不一样的结果,即调用不同的函数。

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

根据上一个条款,只考虑继承虚函数的情况。最好都不要出现带缺省参数的虚函数。因为虚
函数表现的是运行期的动态绑定,而缺省参数则是编译期的静态绑定。可以把带缺省参数的
函数写成一个调用虚函数非虚函数。

38. 组合展现的是“has-a”而public继承则是“is-a”

39. 明智而谨慎地使用私有继承

public继承表明两个类之间的关系是“is-a”的关系,但私有继承则不是这样的, 它意味
着实现的继承,却不包含任何的接口继承,因此对于一个接受基类引用参数的函数来说,当
传入参数为私有继承的子类对象时,会出现编译错误。(注:组合的意义类似于继承对象的
实现,这会与私有继承有点类似。事实上,我们应该尽量使用组合而少用私有继承。)

不是说私有继承一无是处,它可以造成“empty base”最优化^1

  • “让接口易被正确使用,不易被误用”,注意private继承将会把基类中的所有都变为private,哪怕它在基类中protected或public,所以最好把重新定义的继承的虚函数写在private域中。
  • 解决一个设计的方案可能不止一个,尽量多思考哪种方法更优。
  • private继承还可以解决空间最优化问题。(只适用于所处理的class不含任何数据)

当面对并不存在is-a关系的两个类时,其中一个需要访问另一个protected成员或重新定义
其一个或多个虚函数,private继承极有可能尤为正统设计策略。

40. 小心使用多重继承

没看明白

模板与泛型编程

41. 隐式接口和编译期多态

模板提供了隐式接口与编译期多态的特性。所以可以说,类与模板都支持接口与多态,只不
过发生的时机不同。PS:让错误在编译期就确定出来,是不是说编译期多态是更聪明的方法
呢?

43. 处理模板化基类内的名称

将面向对象与泛型编程(即Object Oriented C++和Template C++)混合到一起,需要特别
注意。子类如果继承基类后需要用到基类中已经存在的成员函数,不能直接用成员函数的名
称来调用,需要显式地指出调用了哪个函数(如用this->Fun(),或者用using声明式,还
可以Base<T>::Fun(...))。原因就在于子类假设具现化的基类不一定有这个函数(注,
模板类全特化是可以完全改写基类的成员函数的),而一旦模板参数是某个物化类型,继承
自该全特化后的基类就没有Fun成员函数可供调用。

具体见例子。

44. 与参数无关的代码抽离模板参数

注意,模板虽好,但多个模板参数一旦被具现化后,编译器就会在二进制码中将所有具现后
的类全部展开,造成二进制码的膨胀。所以尽量把可能造成模板膨胀的参数移除。因非类型
的模板参数(如int N)造成的膨胀是可以被消除移到函数参数或类的成员列表中的,事
实上非类型模板参数是造成模板类膨胀的很大一个原因。因类型的模板参数造成的膨胀是可
以被降低的,方法是让带有完全相同的二进制表述的具现类型共享实现码。(关于这一
点,暂时还没懂:

45. 运用成员函数模板接受所有兼容类型

46. 需要类型转换时为模板定义非成员函数

在模板的实参推导过程中,从不将隐式类型转换函数纳入考虑的范围之内。一般而言,解决
的方法是定义一个友元函数,并将其定义在类的内部(inline函数)以解决某些链接的错误

47. 使用萃取类表现类型信息

使用trait类进行类型的萃取,使得程序可以在编译期内决定使用不同版本的实现。以STL的
advance函数为例,不同类别的iterator实现方式不一样,但模板参数只有IterT,我
们需要从IterT中萃取出对应iterator的类型然后用对应的函数作用。

48. 模板元编程

元编程虽然把解决问题的时间移到了编译期,使得程序一旦编译结束也就完成了计算,但却
使得编译时间大大地延长了。

关于模板元编程,可以参考《C++模板元编程》。关于使用模板实现设计模板,可以参
《C++设计新思维》

定制newdelete

关于这一部分,主要是讲定制newdelete的一些问题。对于new和new-handler,当
operator new抛出异常时,将调用new-handler。

杂项讨论

53. 重视编译器警告

编译器警告的内容有时候会是bug的所在

54. 了解C++0x

熟悉C++11中的STL内容,同时也注意一下STL中的shared_ptr只适用于非环形数据结构。STL中的function,bind,随机数生成等组件有待熟悉