《Effective C++》读书笔记
2018-12-16 本文已影响0人
奔向火星005
1 让自己习惯C++重点内容
条款2:尽量以const, enum, inline替换
- define的记号没有进入记录表,不利于调试追踪;
- 宏替换会出现目标码的多份拷贝,生成较大代码量;
- 无法利用#define创建一个class专属常量,#deine不能提供封装性;
- 对于static的成员变量,声明在头文件,定义在实现文件,并给它赋初值;
- 用宏有时会有“括号”问题,用inline函数替换宏完成计算;
条款3:尽量使用const
- 注意const对于指针而言,用在星号前还是星号后的区别;
- const用在迭代器上的效果;
- 将函数返回值定义为const,如operator*返回值,让用户定义类型与内置类型保持兼容;
- Const成员函数,可让接口容易理解,有利编译器排错;
- Const成员函数,让操作const对象成为可能;
- non-const operator[]返回类型应该是reference to char, 不是char, 否则如th[0] = ‘x’,无法通过编译(见左值和右值);
- 两个成员函数因常量性不同,可被重载(但要注意,两个成员函数的参数的常量性不同,是不能被重载的,但是!如果参数是引用,那常量性不同还是可以重载的)
- const成员函数中不能改变成员变量的值;
- Const成员函数返回引用时,所指内容有可能会被修改;
- 改善C++程序效率的一个根本办法是以pass by reference-const方式传递对象,注意有reference和没有reference的区别;
- 让non-const成员函数调用const成员函数避免重复;
- mutalbe对象可在const函数中被改变。
- 为什么处理的代码完全一样,还是会同时有const和non-const函数,个人认为这是为了操作const对象。
条款4:确定对象被使用前已先被初始化
- 永远在使用对象前先就给它初始化;
- 确保每一个构造函数都将对象的每一个成员初始化;
- 对象的成员变量的初始化动作发生在进入构造函数本体之前,采用初始化成员列表比赋值更有效率,为一致性,应把所有成员用初始化列表初始化;
- Const和references成员变量一定需要初始化,不能赋值;
- 成员变量总是以声明次序被初始化,最好总是以其声明次序为次序初始化;
- 定义于不同编译单元的non-local static对象的初始化相对次序无明确定义,可使用局部static对象(Singleton模式),但多线程不安全。
2 构造/析构/赋值运算
条款5:了解C++默默编写并调用哪些函数
- 如果你没有声明,编译器会自动为一个类声明default构造函数,copy构造函数,copy assignment操作符和析构函数;
- 唯有大概这些函数被需要(被调用),它们才会被编译器创建出来;
- 编译器产出的析构函数是non-virtual的;
- 一旦声明了一个构造函数,编译器就不再为它创建default构造函数;
- 编译器拒绝为“内含referece或const成员变量”的类生成copy assignment操作符;
- 如果base class把copy assignment操作符声明为private,那么编译器拒绝为derived类生成copy assignment操作符;
条款6:若不想编译器自动生成的函数,就该明确拒绝
- 若对象是独一无二的,就应该阻止其拷贝,通过把copy构造函数和copy assignment操作符声明为private,并故意不实现,或使用uncopyable类继承,可办到。
条款7:为多态基类声明virtual析构函数
- 当一个具有non-virtual析构函数的基类指针指向一个派生类时,用delete删除该基类指针时,调用的是基类的析构函数,因此会引发诡异的“局部删除”。
- 更广泛的说,当non-virtual函数被指针调用时,实际上是调用指针的类的成员函数,而virtual函数实际上是调用指针实际所指的对象的成员函数。
- 任何class只要带有virtual函数就几乎可以确定应该有一个virtual析构函数;
- 如果一个class不含virtual函数,通常表示它不意图用作一个base class。把不企图当做base类的class声明virtual是个馊主意,因为会增加内存。
条款8:别让异常逃离析构函数
- 当异常抛出时,会进行栈展开以匹配catch,在展开过程中会销毁局部对象,若在销毁过程中局部对象的析构函数再次抛出异常,会导致不明确行为。因此不应该在析构函数抛出异常;
- 如一个vector被销毁时,内含的一个对象在析构中抛出了异常,其他的内部对象也会被销毁(调用析构函数),若第二个对象的析构函数又抛出异常,则会导致不明确行为;
- 在析构函数中处理异常的两个方法(没有太大吸引力),一是调用abort终止程序;而是用catch“吞咽”异常;
- 可以给客户提供一个机会处理异常(详见书本例子)。
条款9:绝不在构造和析构函数中调用virtual函数
- derived构造函数会先调用base构造函数,在base构造函数中被调用的virtual函数是base版本的;析构函数同理。
条款10:令operator=返回一个reference to *this
- 内置类型及标准库提供的类型,赋值操作符都会返回一个reference指向操作符的左侧实参,自定义的类无特殊理由也应该遵循此协议。
条款11:在operator=中处理“自我赋值”
- 由于指针或引用及继承体系,有时“自我赋值”行为并不明显;
- 在operator=的实现中,要注意自我赋值的情况,避免先将自身删除,另外要注意异常抛出的处理,可使用swap。
条款12:复制对象时勿忘其每一个成分
- 若我们没有声明copy构造函数和copy assignment操作符,编译器会自动生成:将被拷贝对象的所有成员变量都做一份拷贝;
- 如果你自己声明copying函数,当你的实现代码几乎必然出错时,编译器也不会做出警告;
- 当你为自己的类添加新的成员变量时,自己实现copying函数将不会自动添加相应代码;
- 若你自己撰写derived class的copying函数,则必须手动将实参赋给base class的copying函数,否则derived class中的base成分将不会被拷贝;
- 总而言之,当你编写一个copying函数,请确保(1)复制所有local成员变量 (2)调用所有base classes内的适当的copying函数;
- 令copying assignment操作符调用copy构造函数,或用copy构造函数调用copying assignment操作符都是不合理的,千万别尝试。
3 资源管理
条款13:以对象管理资源
- RAII:获得资源后立刻放进管理对象,管理对象运用析构函数确保资源被释放;
- 靠手动释放资源存在隐患,应使用auto_ptr管理资源,可自动调用析构函数释放资源;
- 若通过copy构造函数或copy assignment操作符复制auto_ptr,他们会变成null,而复制所得的指针将取得资源的唯一拥有权;
- 引用计数型指针shared_ptr可避免上述auto_ptr的缺点;
- Auto_ptr和shared_ptr不能使用于array上。
条款14:在资源管理类中小心copying行为
- auto_ptr和shared_ptr一般只适用于heap-based资源,对于非heap-based类资源,你应该自己编写资源管理类;
- 当你自己编写RAII对象时,应该要注意其copying问题,一般有如下选择:1.禁止copying行为,如lock;2.对底层资源使用“引用计数法”(shared_ptr);3.复制资源管理对象时一并复制底部资源;4.转移底部资源拥有权,如auto_ptr。
- Class的析构函数会自动调用其non-static成员变量的析构函数;
- Copying函数(包括copy构造函数和copy assignment操作符)有可能由编译器创造出来,除非编译器做了你想做的事,否则应该自己编写。
条款15:在资源管理类中提供对原始资源的访问
- 资源管理类是对抗资源泄露的堡垒,但有时要绕过资源管理类直接访问原始资源;
- Auto_ptr和shared_ptr提供get成员函数提供原始指针,也重载了operator->和operator*,它们允许隐式转换至底部原始指针;
- 有时需要提供RAII对象内的原始资源,可提供一个显示转换函数,也可提供隐式转换函数,但隐式转换容易出错;
- RAII class内返回原始资源的函数,与封装性矛盾,但RAII classes并不是为了封装某物而存在,它们的存在是为了确保一个特殊行为——资源释放。
条款16:成对使用new和delete时要采取相同形式
- 当new一个数组时,应使用delete[],否则结果是未定义的;
- 避免typedef一个数组,因为可能会对数组使用delete而不是delele[]。
条款17:以独立语句将newed对象置入智能指针
- shared_ptr构造函数是explicit构造函数,无法将原始指针隐式转换为shared_ptr;
- 应将“new对象”的操作和“构造shared_ptr智能指针”操作连在一起,否则若两个操作中间有代码并出现异常,则会引起内存泄露。(参考书中例子)
4 设计与声明
条款18:让接口容易被正确使用,不易被误用
- 可通过建立新类型,限制类型上的操作,束缚对象值,加const等方法来防止客户错误的使用接口;
- 尽量让接口保持一致性,与内置类型的行为兼容;
- 可让factory函数直接返回shared_ptr,可防止客户资源泄露问题;
- Shared_ptr智能指针可指定删除器;
- Shared_ptr可消除cross_DLL problem?
条款19:设计class犹如设计type
...
条款20:宁以pass-by-reference-to-const替换pass-by-value
- pass-by-value会耗费一次或多次构造函数和析构函数时间,有时代价是非常昂贵的;
- pass-by-reference-to-const没有任何构造函数和析构函数被调用,效率要高得多;
- 当一个derived class对象以pass-by-value方式传递给bass class对象时,只会调用bass class构造函数,因此引起“对象切割”问题,而以pass-by-reference-to-const方式则可以解决;
- 对于内置类型,STL的迭代器和函数对象,使用pass-by-value更合适;
- 即使是“小型用户自定义类型”,也不推荐使用pass-by-value。
条款21:必须返回对象时,别妄想返回其reference
- 当函数返回时,local对象即被销毁,因此函数返回local对象的reference或指针都是严重错误;
- 函数返回在Heap中new的对象的reference或指针,会存在资源泄露问题;
- 函数返回reference或指针指向一个定义与函数内部的static对象,首先会因此多线程安全问题,另外还有更深层的瑕疵;
- 一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。
条款22:将成员变量声明为private
- 如果成员变量不是public,客户唯一访问对象的办法是访问成员函数,即public接口的每样东西都是函数,这是从语法一致性角度。
- 使用函数可以让你对成员变量的处理有更精确的控制,你可以实现“不准访问”,“只读访问”,“读写访问”,甚至“唯写访问”。
- 最重要的是封装性,将成员函数隐藏在函数接口背后,可以为“所有可能的实现”提供弹性,只有成员函数可以影响客户代码,若不隐藏成员变量,会破坏大量的客户代码;
- 成员变量的封装性与“成员变量改变时所破坏的代码数量”成反比,protect和public一样不能提供封装性。
条款23:宁以non-member,non-friend替换member函数
- 作为一种粗糙的测量,越多函数能访问class内的成员数据,数据的封装性越低;
- 能够访问private成员变量的函数只有class内的member函数和friend函数,而non-menber,non-friend函数并不增加“能够访问class内之private成分”的函数数量,因此non-menber,non-friend函数能提供更好的封装性;
- 在C++中,一般的做法是让non-member函数位于class所在的同一个namespace中;
- Namespace可跨越多个文件,利用namespace和头文件,可将一个class的多个“便利函数”分离,并有较低的编译相依关系。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
- 令class支持隐式转换通常是个糟糕的主意,但也有例外,例如将整数转换为有理数class;
- 有关operator*的例子,具体请看书。
条款25:考虑写出一个不抛异常的swap函数
(难度较大,可参看书本)
- 如果swap的缺省实现码对你的class或class template提供可接受的效率,就不要做额外的事(太难写了);
- 如果缺省实现版效率不足,则试着提供一个public swap成员函数;
- 成员版swap绝不可抛出异常,因为swap的一个最好应用是帮助classes提供强烈的异常安全性保障;
- 这一约束只施行于成员版!不可施行于非成员版,因为swap缺省版本是以copy构造函数和copy assignment操作符为基础,而这两者都允许抛出异常。
5 实现
条款26:尽可能延后变量定义式的出现时间
- 当你定义一个变量而其类型带有构造函数和析构函数,那么当程序控制流到达这个定义式时,你便得承受构造成本,当变量离开作用域时,你便得承受析构成本;
- 尽量延后变量定义式时间,可避免没必要的构造和析构;
- “通过default构造函数构造出一个对象然后对它赋值”比“直接在构造时指定初值”效率差。
- 分析然后确定定义在循环内还是循环外。
条款27:尽量少做转型操作
尽量少用就是了...
条款28:避免返回handle指向对象内部成员
- class的函数(public)不应返回内部成员变量或函数(private)的reference、指针和迭代器等,因为这样外部调用者可以通过这些handle修改对象的内部成员数据,降低其封装性,也会导致“虽然调用const成员函数却造成对象状态被修改”。
- “返回一个handle代表对象内部成分”还会导致当对象被销毁后,handle成为“虚吊”的危险。
条款29:为“异常安全”而努力是值得的
- 当异常抛出时,要注意保证:(1)不泄露任何资源(如锁);(2)不允许数据败坏;
- 声明throw()并不能保证绝不会抛出异常,那些性质由函数的实现决定;
- 应以智能指针管理对象内部的指针成员;
- 一个异常安全的策略是“copy and swap”;
条款30:透彻了解inlining的里里外外
- 使用inline函数虽然可以避免函数调用所带来的开销,但也很可能会增加你的目标码,引起代码膨胀;
- Inline函数通常一定置于头文件,因为大多数建置环境在编译过程中进行inlining。
- Templates通常也被置于头文件内,但它的具现化与inlining无关?
- Inline只是个申请,编译器可以加以忽略;
- 当程序要取某个inline函数的地址时,编译器必须为此函数生成一个outline本体,这意味着对一个函数调用有可能被inlined,也可能不被inlined;
- 即使你未使用函数指针,inline函数还是有可能未被inlined,如构造函数和析构函数;
- 即使无任何代码的构造函数,其也不太可能是inlined,因为编译器至少会在内调用其他成员和base class两者的构造函数。析构函数亦如此;
- Inline函数无法随着程序库的升级而升级,若f是程序库的一个inline函数,当程序库改变f时,客户端程序必须重新编译。而对于non-inline函数,客户端只需重新连接即可;
- 大部分调试器对inline函数束手无策;
- 应将inlining限制在小型,被频繁调用的函数上。
条款31:将文件间的依存关系降至最低
- 编译器必须在编译期间知道对象的大小,编译器获得这项信息的唯一方法是查询class的定义式;
- 使用pimpl(Pointer to implementation)设计,即在main class内只含一个指针成员,指向实现类,这样,main class与其实现类中的成员的实现细目分离,那些classes的任何实现的改变都不需要使用main class的客户端重新编译;
- 分离的关键在于以“声明的依存性”替换“定义的依存性”,有两个策略:1.如果使用object reference或object pointer可以完成任务,就不要使用object。2.如果能够,尽量以class声明式替换class定义式;
- 声明函数时,只需参数的声明式而无需定义式,只有当函数被调用时(函数定义式),参数的定义式才要曝光;
- 为声明式和定义式提供不同的头文件。如Data类,“Datafwd.h”只声明,“Data.h”定义具体Data类结构,“Data.cpp”做具体实现;
- 也可以用Interface class解除接口与实现之间的耦合,从而降低文件间的编译依存性;
- 用handle class和interface class都会增加额外的空间和时间成本,但不应因此而放弃他们;
- 不论handle class和interface class,一旦脱离inline函数,都无法有太大作为?
6 继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系
- “public继承”意味is-a。适用于base class身上的每一件事情也一定适用于derived class身上,因为每一个derived class对象也一定是base class对象。
条款33:避免遮掩继承而来的名称
- derived class 内的名称会遮掩base class内的名称。在public继承下从来没有人希望如此;
- 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。
条款34:区分接口继承与实现继承
- 声明一个pure virtual函数是为了derived class只继承函数接口;
- 声明impure virtual函数是为了让derived class继承函数的接口和缺省的实现;
- 声明non-virtual函数是为了令derived class继承函数的接口和一份强制性实现。
条款35:考虑virtual函数以外的选择
- 使用NVI(non-virtual interface)方法,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,可替换virtual函数,一个优点是可以在virtual函数调用前后做一些工作;
- Derived class可以重新定义virtual函数,即使它是private;
- 将virtual函数替换为“函数指针成员变量”,这是Strategy模式的一种分解表现形式;
- Strategy模式可提供更大的弹性,但也有弱点,因为当non-member成员函数访问class的non-public成分时,必须弱化封装性;
- 可由tr1::function完成Strategy模式。
条款36:绝不重新定义继承而来的non-virtual函数
- non-virtual函数是静态绑定的,例如,当pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B定义的版本,即使pB指向一个类型为“B派生之class”对象;
- Virtual函数是动态绑定的,通过pB调用的virtual函数是pB实际所指向的对象的版本,这就是所谓多态性;
- 条款7是此条款的一个特例。
条款37:绝不重新定义继承而来的缺省参数值
- 清楚理解什么是“静态绑定”和“动态绑定”;
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数确是动态绑定。
条款38:通过复合塑模出has-a或“根据某物实现出”
- “public继承”带有“is-a”,而复合意味着“has-a”或“is- implemented-in-term-of”。
条款39:明智而审慎地使用private继承
- 如果class之间的继承关系是private,则编译器不会自动将一个derived class对象转换为base class对象,其二,由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原来是protected或public属性;
- Private继承意味着implemented-in-terms-of,用条款34的术语,private继承意味着只有实现部分被继承,接口部分应略去;
- 尽量使用复合而非private,首先,复合可以“阻止derived class重新定义virtual函数”,第二,复合更有利于降低编译依存性;
- Private继承只用于“当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数”,或者某些“激进情况”。