《Effective C++》读书笔记

2018-12-16  本文已影响0人  奔向火星005

1 让自己习惯C++重点内容

条款2:尽量以const, enum, inline替换

  1. define的记号没有进入记录表,不利于调试追踪;
  2. 宏替换会出现目标码的多份拷贝,生成较大代码量;
  3. 无法利用#define创建一个class专属常量,#deine不能提供封装性;
  4. 对于static的成员变量,声明在头文件,定义在实现文件,并给它赋初值;
  5. 用宏有时会有“括号”问题,用inline函数替换宏完成计算;

条款3:尽量使用const

  1. 注意const对于指针而言,用在星号前还是星号后的区别;
  2. const用在迭代器上的效果;
  3. 将函数返回值定义为const,如operator*返回值,让用户定义类型与内置类型保持兼容;
  4. Const成员函数,可让接口容易理解,有利编译器排错;
  5. Const成员函数,让操作const对象成为可能;
  6. non-const operator[]返回类型应该是reference to char, 不是char, 否则如th[0] = ‘x’,无法通过编译(见左值和右值);
  7. 两个成员函数因常量性不同,可被重载(但要注意,两个成员函数的参数的常量性不同,是不能被重载的,但是!如果参数是引用,那常量性不同还是可以重载的)
  8. const成员函数中不能改变成员变量的值;
  9. Const成员函数返回引用时,所指内容有可能会被修改;
  10. 改善C++程序效率的一个根本办法是以pass by reference-const方式传递对象,注意有reference和没有reference的区别;
  11. 让non-const成员函数调用const成员函数避免重复;
  12. mutalbe对象可在const函数中被改变。
  13. 为什么处理的代码完全一样,还是会同时有const和non-const函数,个人认为这是为了操作const对象。

条款4:确定对象被使用前已先被初始化

  1. 永远在使用对象前先就给它初始化;
  2. 确保每一个构造函数都将对象的每一个成员初始化;
  3. 对象的成员变量的初始化动作发生在进入构造函数本体之前,采用初始化成员列表比赋值更有效率,为一致性,应把所有成员用初始化列表初始化;
  4. Const和references成员变量一定需要初始化,不能赋值;
  5. 成员变量总是以声明次序被初始化,最好总是以其声明次序为次序初始化;
  6. 定义于不同编译单元的non-local static对象的初始化相对次序无明确定义,可使用局部static对象(Singleton模式),但多线程不安全。

2 构造/析构/赋值运算

条款5:了解C++默默编写并调用哪些函数

  1. 如果你没有声明,编译器会自动为一个类声明default构造函数,copy构造函数,copy assignment操作符和析构函数;
  2. 唯有大概这些函数被需要(被调用),它们才会被编译器创建出来;
  3. 编译器产出的析构函数是non-virtual的;
  4. 一旦声明了一个构造函数,编译器就不再为它创建default构造函数;
  5. 编译器拒绝为“内含referece或const成员变量”的类生成copy assignment操作符;
  6. 如果base class把copy assignment操作符声明为private,那么编译器拒绝为derived类生成copy assignment操作符;

条款6:若不想编译器自动生成的函数,就该明确拒绝

  1. 若对象是独一无二的,就应该阻止其拷贝,通过把copy构造函数和copy assignment操作符声明为private,并故意不实现,或使用uncopyable类继承,可办到。

条款7:为多态基类声明virtual析构函数

  1. 当一个具有non-virtual析构函数的基类指针指向一个派生类时,用delete删除该基类指针时,调用的是基类的析构函数,因此会引发诡异的“局部删除”。
  2. 更广泛的说,当non-virtual函数被指针调用时,实际上是调用指针的类的成员函数,而virtual函数实际上是调用指针实际所指的对象的成员函数。
  3. 任何class只要带有virtual函数就几乎可以确定应该有一个virtual析构函数;
  4. 如果一个class不含virtual函数,通常表示它不意图用作一个base class。把不企图当做base类的class声明virtual是个馊主意,因为会增加内存。

条款8:别让异常逃离析构函数

  1. 当异常抛出时,会进行栈展开以匹配catch,在展开过程中会销毁局部对象,若在销毁过程中局部对象的析构函数再次抛出异常,会导致不明确行为。因此不应该在析构函数抛出异常;
  2. 如一个vector被销毁时,内含的一个对象在析构中抛出了异常,其他的内部对象也会被销毁(调用析构函数),若第二个对象的析构函数又抛出异常,则会导致不明确行为;
  3. 在析构函数中处理异常的两个方法(没有太大吸引力),一是调用abort终止程序;而是用catch“吞咽”异常;
  4. 可以给客户提供一个机会处理异常(详见书本例子)。

条款9:绝不在构造和析构函数中调用virtual函数

  1. derived构造函数会先调用base构造函数,在base构造函数中被调用的virtual函数是base版本的;析构函数同理。

条款10:令operator=返回一个reference to *this

  1. 内置类型及标准库提供的类型,赋值操作符都会返回一个reference指向操作符的左侧实参,自定义的类无特殊理由也应该遵循此协议。

条款11:在operator=中处理“自我赋值”

  1. 由于指针或引用及继承体系,有时“自我赋值”行为并不明显;
  2. 在operator=的实现中,要注意自我赋值的情况,避免先将自身删除,另外要注意异常抛出的处理,可使用swap。

条款12:复制对象时勿忘其每一个成分

  1. 若我们没有声明copy构造函数和copy assignment操作符,编译器会自动生成:将被拷贝对象的所有成员变量都做一份拷贝;
  2. 如果你自己声明copying函数,当你的实现代码几乎必然出错时,编译器也不会做出警告;
  3. 当你为自己的类添加新的成员变量时,自己实现copying函数将不会自动添加相应代码;
  4. 若你自己撰写derived class的copying函数,则必须手动将实参赋给base class的copying函数,否则derived class中的base成分将不会被拷贝;
  5. 总而言之,当你编写一个copying函数,请确保(1)复制所有local成员变量 (2)调用所有base classes内的适当的copying函数;
  6. 令copying assignment操作符调用copy构造函数,或用copy构造函数调用copying assignment操作符都是不合理的,千万别尝试。

3 资源管理

条款13:以对象管理资源

  1. RAII:获得资源后立刻放进管理对象,管理对象运用析构函数确保资源被释放;
  2. 靠手动释放资源存在隐患,应使用auto_ptr管理资源,可自动调用析构函数释放资源;
  3. 若通过copy构造函数或copy assignment操作符复制auto_ptr,他们会变成null,而复制所得的指针将取得资源的唯一拥有权;
  4. 引用计数型指针shared_ptr可避免上述auto_ptr的缺点;
  5. Auto_ptr和shared_ptr不能使用于array上。

条款14:在资源管理类中小心copying行为

  1. auto_ptr和shared_ptr一般只适用于heap-based资源,对于非heap-based类资源,你应该自己编写资源管理类;
  2. 当你自己编写RAII对象时,应该要注意其copying问题,一般有如下选择:1.禁止copying行为,如lock;2.对底层资源使用“引用计数法”(shared_ptr);3.复制资源管理对象时一并复制底部资源;4.转移底部资源拥有权,如auto_ptr。
  3. Class的析构函数会自动调用其non-static成员变量的析构函数;
  4. Copying函数(包括copy构造函数和copy assignment操作符)有可能由编译器创造出来,除非编译器做了你想做的事,否则应该自己编写。

条款15:在资源管理类中提供对原始资源的访问

  1. 资源管理类是对抗资源泄露的堡垒,但有时要绕过资源管理类直接访问原始资源;
  2. Auto_ptr和shared_ptr提供get成员函数提供原始指针,也重载了operator->和operator*,它们允许隐式转换至底部原始指针;
  3. 有时需要提供RAII对象内的原始资源,可提供一个显示转换函数,也可提供隐式转换函数,但隐式转换容易出错;
  4. RAII class内返回原始资源的函数,与封装性矛盾,但RAII classes并不是为了封装某物而存在,它们的存在是为了确保一个特殊行为——资源释放。

条款16:成对使用new和delete时要采取相同形式

  1. 当new一个数组时,应使用delete[],否则结果是未定义的;
  2. 避免typedef一个数组,因为可能会对数组使用delete而不是delele[]。

条款17:以独立语句将newed对象置入智能指针

  1. shared_ptr构造函数是explicit构造函数,无法将原始指针隐式转换为shared_ptr;
  2. 应将“new对象”的操作和“构造shared_ptr智能指针”操作连在一起,否则若两个操作中间有代码并出现异常,则会引起内存泄露。(参考书中例子)

4 设计与声明

条款18:让接口容易被正确使用,不易被误用

  1. 可通过建立新类型,限制类型上的操作,束缚对象值,加const等方法来防止客户错误的使用接口;
  2. 尽量让接口保持一致性,与内置类型的行为兼容;
  3. 可让factory函数直接返回shared_ptr,可防止客户资源泄露问题;
  4. Shared_ptr智能指针可指定删除器;
  5. Shared_ptr可消除cross_DLL problem?

条款19:设计class犹如设计type

...

条款20:宁以pass-by-reference-to-const替换pass-by-value

  1. pass-by-value会耗费一次或多次构造函数和析构函数时间,有时代价是非常昂贵的;
  2. pass-by-reference-to-const没有任何构造函数和析构函数被调用,效率要高得多;
  3. 当一个derived class对象以pass-by-value方式传递给bass class对象时,只会调用bass class构造函数,因此引起“对象切割”问题,而以pass-by-reference-to-const方式则可以解决;
  4. 对于内置类型,STL的迭代器和函数对象,使用pass-by-value更合适;
  5. 即使是“小型用户自定义类型”,也不推荐使用pass-by-value。

条款21:必须返回对象时,别妄想返回其reference

  1. 当函数返回时,local对象即被销毁,因此函数返回local对象的reference或指针都是严重错误;
  2. 函数返回在Heap中new的对象的reference或指针,会存在资源泄露问题;
  3. 函数返回reference或指针指向一个定义与函数内部的static对象,首先会因此多线程安全问题,另外还有更深层的瑕疵;
  4. 一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。

条款22:将成员变量声明为private

  1. 如果成员变量不是public,客户唯一访问对象的办法是访问成员函数,即public接口的每样东西都是函数,这是从语法一致性角度。
  2. 使用函数可以让你对成员变量的处理有更精确的控制,你可以实现“不准访问”,“只读访问”,“读写访问”,甚至“唯写访问”。
  3. 最重要的是封装性,将成员函数隐藏在函数接口背后,可以为“所有可能的实现”提供弹性,只有成员函数可以影响客户代码,若不隐藏成员变量,会破坏大量的客户代码;
  4. 成员变量的封装性与“成员变量改变时所破坏的代码数量”成反比,protect和public一样不能提供封装性。

条款23:宁以non-member,non-friend替换member函数

  1. 作为一种粗糙的测量,越多函数能访问class内的成员数据,数据的封装性越低;
  2. 能够访问private成员变量的函数只有class内的member函数和friend函数,而non-menber,non-friend函数并不增加“能够访问class内之private成分”的函数数量,因此non-menber,non-friend函数能提供更好的封装性;
  3. 在C++中,一般的做法是让non-member函数位于class所在的同一个namespace中;
  4. Namespace可跨越多个文件,利用namespace和头文件,可将一个class的多个“便利函数”分离,并有较低的编译相依关系。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

  1. 令class支持隐式转换通常是个糟糕的主意,但也有例外,例如将整数转换为有理数class;
  2. 有关operator*的例子,具体请看书。

条款25:考虑写出一个不抛异常的swap函数

(难度较大,可参看书本)

  1. 如果swap的缺省实现码对你的class或class template提供可接受的效率,就不要做额外的事(太难写了);
  2. 如果缺省实现版效率不足,则试着提供一个public swap成员函数;
  3. 成员版swap绝不可抛出异常,因为swap的一个最好应用是帮助classes提供强烈的异常安全性保障;
  4. 这一约束只施行于成员版!不可施行于非成员版,因为swap缺省版本是以copy构造函数和copy assignment操作符为基础,而这两者都允许抛出异常。

5 实现

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

  1. 当你定义一个变量而其类型带有构造函数和析构函数,那么当程序控制流到达这个定义式时,你便得承受构造成本,当变量离开作用域时,你便得承受析构成本;
  2. 尽量延后变量定义式时间,可避免没必要的构造和析构;
  3. “通过default构造函数构造出一个对象然后对它赋值”比“直接在构造时指定初值”效率差。
  4. 分析然后确定定义在循环内还是循环外。

条款27:尽量少做转型操作

尽量少用就是了...

条款28:避免返回handle指向对象内部成员

  1. class的函数(public)不应返回内部成员变量或函数(private)的reference、指针和迭代器等,因为这样外部调用者可以通过这些handle修改对象的内部成员数据,降低其封装性,也会导致“虽然调用const成员函数却造成对象状态被修改”。
  2. “返回一个handle代表对象内部成分”还会导致当对象被销毁后,handle成为“虚吊”的危险。

条款29:为“异常安全”而努力是值得的

  1. 当异常抛出时,要注意保证:(1)不泄露任何资源(如锁);(2)不允许数据败坏;
  2. 声明throw()并不能保证绝不会抛出异常,那些性质由函数的实现决定;
  3. 应以智能指针管理对象内部的指针成员;
  4. 一个异常安全的策略是“copy and swap”;

条款30:透彻了解inlining的里里外外

  1. 使用inline函数虽然可以避免函数调用所带来的开销,但也很可能会增加你的目标码,引起代码膨胀;
  2. Inline函数通常一定置于头文件,因为大多数建置环境在编译过程中进行inlining。
  3. Templates通常也被置于头文件内,但它的具现化与inlining无关?
  4. Inline只是个申请,编译器可以加以忽略;
  5. 当程序要取某个inline函数的地址时,编译器必须为此函数生成一个outline本体,这意味着对一个函数调用有可能被inlined,也可能不被inlined;
  6. 即使你未使用函数指针,inline函数还是有可能未被inlined,如构造函数和析构函数;
  7. 即使无任何代码的构造函数,其也不太可能是inlined,因为编译器至少会在内调用其他成员和base class两者的构造函数。析构函数亦如此;
  8. Inline函数无法随着程序库的升级而升级,若f是程序库的一个inline函数,当程序库改变f时,客户端程序必须重新编译。而对于non-inline函数,客户端只需重新连接即可;
  9. 大部分调试器对inline函数束手无策;
  10. 应将inlining限制在小型,被频繁调用的函数上。

条款31:将文件间的依存关系降至最低

  1. 编译器必须在编译期间知道对象的大小,编译器获得这项信息的唯一方法是查询class的定义式;
  2. 使用pimpl(Pointer to implementation)设计,即在main class内只含一个指针成员,指向实现类,这样,main class与其实现类中的成员的实现细目分离,那些classes的任何实现的改变都不需要使用main class的客户端重新编译;
  3. 分离的关键在于以“声明的依存性”替换“定义的依存性”,有两个策略:1.如果使用object reference或object pointer可以完成任务,就不要使用object。2.如果能够,尽量以class声明式替换class定义式;
  4. 声明函数时,只需参数的声明式而无需定义式,只有当函数被调用时(函数定义式),参数的定义式才要曝光;
  5. 为声明式和定义式提供不同的头文件。如Data类,“Datafwd.h”只声明,“Data.h”定义具体Data类结构,“Data.cpp”做具体实现;
  6. 也可以用Interface class解除接口与实现之间的耦合,从而降低文件间的编译依存性;
  7. 用handle class和interface class都会增加额外的空间和时间成本,但不应因此而放弃他们;
  8. 不论handle class和interface class,一旦脱离inline函数,都无法有太大作为?

6 继承与面向对象设计

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

  1. “public继承”意味is-a。适用于base class身上的每一件事情也一定适用于derived class身上,因为每一个derived class对象也一定是base class对象。

条款33:避免遮掩继承而来的名称

  1. derived class 内的名称会遮掩base class内的名称。在public继承下从来没有人希望如此;
  2. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

条款34:区分接口继承与实现继承

  1. 声明一个pure virtual函数是为了derived class只继承函数接口;
  2. 声明impure virtual函数是为了让derived class继承函数的接口和缺省的实现;
  3. 声明non-virtual函数是为了令derived class继承函数的接口和一份强制性实现。

条款35:考虑virtual函数以外的选择

  1. 使用NVI(non-virtual interface)方法,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,可替换virtual函数,一个优点是可以在virtual函数调用前后做一些工作;
  2. Derived class可以重新定义virtual函数,即使它是private;
  3. 将virtual函数替换为“函数指针成员变量”,这是Strategy模式的一种分解表现形式;
  4. Strategy模式可提供更大的弹性,但也有弱点,因为当non-member成员函数访问class的non-public成分时,必须弱化封装性;
  5. 可由tr1::function完成Strategy模式。

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

  1. non-virtual函数是静态绑定的,例如,当pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B定义的版本,即使pB指向一个类型为“B派生之class”对象;
  2. Virtual函数是动态绑定的,通过pB调用的virtual函数是pB实际所指向的对象的版本,这就是所谓多态性;
  3. 条款7是此条款的一个特例。

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

  1. 清楚理解什么是“静态绑定”和“动态绑定”;
  2. 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数确是动态绑定。

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

  1. “public继承”带有“is-a”,而复合意味着“has-a”或“is- implemented-in-term-of”。

条款39:明智而审慎地使用private继承

  1. 如果class之间的继承关系是private,则编译器不会自动将一个derived class对象转换为base class对象,其二,由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原来是protected或public属性;
  2. Private继承意味着implemented-in-terms-of,用条款34的术语,private继承意味着只有实现部分被继承,接口部分应略去;
  3. 尽量使用复合而非private,首先,复合可以“阻止derived class重新定义virtual函数”,第二,复合更有利于降低编译依存性;
  4. Private继承只用于“当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数”,或者某些“激进情况”。
上一篇下一篇

猜你喜欢

热点阅读