《effective c++》读书笔记

来源:互联网 发布:illustrator软件下载 编辑:程序博客网 时间:2024/06/05 14:22

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

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

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

条款3:尽量使用const

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

原创粉丝点击