Effective C++

让自己习惯C++

1. 视 C++ 为一个语言联邦

  • C:包括区块 blocks,语句 statements,预处理 preprocessor,内置数据类型 build-in data types,数组 arrays,指针 pointers 等。
  • C++:包括类 classes,封装 encapsulation,继承 inheritance,多态 polymorphism,virtual 函数等。
  • Template C++:泛型编程 generic programming。
  • STL:标准模板库 standard template library。

2. 尽量使用 constenuminline 替换 #define

  • 对于常量,最好使用 const 或者 enum。(#define直接替换导致名称没有被编译器看到,#define不重视作用域)
  • 对于函数,最好使用 inline 函数替代#define函数宏(#define宏函数容易出问题)。

3. 尽可能使用 const

  • 令函数返回一个常量值,可以预防无意义的赋值动作
  • const成员函数:
  1. const对象只能访问const成员函数,而非const对象可以访问任意的成员函数
  2. const成员函数不能修改对象的数据成员,const对象的成员变量不可以修改(mutable修饰的数据成员除外)

  另外:  

  两个成员函数如果只是常量性不同,是可以被重载的

  当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复

4. 确定对象在使用前初始化

  • 为内置型对象进行手工初始化;内置类型以外,构造函数负责初始化责任
  • 构造函数最好使用成员初值列 ,而不使用赋值操作 ;最好总是以声明次序为其次序
  • 不同编译单元的non-local static对象初始化相对次序并无明确定义,以local对象替换得以免除问题

 

构造、析构、赋值运算

5.了解C++默默编写并调用了哪些函数

  • 如果自己不声明, 编译器就会暗自为class创建default构造函数copy构造函数copy assignment操作符,以及析构函数

  注:

  默认copy构造函数只是进行简单的bits拷贝
  base class如果把copy构造函数或copy assignment操作符设置为private,derived class将拒绝生成copy构造函数或copy assignment操作符

6.若不想使用编译器自动生成的函数,就该明确拒绝

  • 将相应的成员函数声明为 private,并不予实现即可
  • 或者使用delete关键字

7.为多态基类声明virtual析构函数

  • 为了避免在多态情况下,通过一个基类指针去delete一个子类对象时,由于析构函数不是虚函数而发生错误
  • 如果一个基类可能有多态子类,那么就该声明一个虚析构函数。
  • 如果一个类有任何虚函数,那么它就应该有虚析构函数。
  • 如果一个类不被用来做基类,或者不是为了多态,那么就不该声明虚析构函数。

8.别让异常逃离析构函数

  • 析构函数绝对不能抛出异常;如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作

  注:

  1. 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题
  2. 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题

9.绝不在构造和析构过程中调用virtual函数

  • 由于无法使用virtual函数从base classes向下调用,可以令derived classes将必要的构造信息向上传递至base class构造函数替换 

  注:base class构造期间virtual函数绝不会下降到derived classes阶层,原因有二

  1. base class构造或析构函数执行时derived class的成员变量尚未初始化,如果调用的virtual函数下降到derived class阶层,必定导致使用的成员变量未初始化
  2. 在derived class的base class构造期间,对象的类型是base class,不会成为一个derived class对象

10.令operator=(包括+=,-=等) 返回一个reference to *this

  • 为了实现“连锁赋值”等

11.在operator= 中处理“自我赋值”

  • 由于变量有别名的存在(多个指针或引用只想一个对象),所以可能出现自我赋值的情况。应对方法有三个:
  1. if(this == &rhs) return *this; 添加判断
  2. 先创建一个temp对象指向本对象,然后令本对象复制目标对象(创建一个副本),然后删除temp对象(原本对象)。ClassName *temp = this.data;data = new ClassName(*rhs.data);delete temp;
  3. 使用copy and swap技术。先创建一个temp对象指向本对象,然后令本对象复制目标对象,然后删除temp对象(原本对象)。ClassName temp(rhs);swap(temp);

12.复制对象时务忘其每一个成分

  • 复制所有的local成员变量以及所有base class成分
  • 不要尝试以一个copying函数实现另一个copying函数。应将共同机能放进第三个函数中并由它们共同调用

 

资源管理

13.以对象管理资源

  • 为了防止资源泄漏,请使用RAII对象,在构造函数里面获得资源,并在析构函数里面释放资源
  • 使用智能指针(如shared_ptr,unique_ptr)来管理资源类,避免你忘记 delete 资源类。

14.在资源管理类小心copy行为

  • 一般资源管理类复制时可以选择以下做法:
  1. 禁止复制(复制不合理)
  2. 引用计数法”(使用shared_ptr指定“删除器”阻止引用次数为0时的删除行为
  3. 复制底层资源(“深度拷贝”)
  4. 转移底部资源的拥有权(unique_ptr)

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

  • APIs往往要求访问原始资源,所以每一RAII class应该提供一个“取得其所管理的资源”的方法。
  • 原始资源的访问可能经由显式转换或隐式转换,一般而言显式转换比较安全,但隐式转换对客户比较方便。

16.成对使用new和delete要采用相同的格式

  • new和delete对应,new[ ]和delete[ ]对应

17.以独立的语句将newd对象置入智能指针

  • 在一个语句中编译器拥有重新排列操作的自由,如此一来可能被异常干扰,就很难察觉发生了资源泄露

 

设计与声明

18.让接口容易被正确使用,不易被误用

  • 好的接口很容易被正确使用,不容易被误用;努力达成这些性质
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容;“防治误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除用户的资源管理责任
  • shared_ptr支持定制型删除器,可预防DLL问题,可被用来自动解除互斥锁等等

19.设计class犹如设计type

  • 在设计class时,要考虑一系列的问题,包括
  1. 对象的创建和销毁(构造、析构)
  2. 对象的初始化与赋值(构造、赋值操作符)
  3. 复制操作(复制构造)
  4. 合法值(约束条件)
  5. 继承体系(注意虚函数)
  6. 支持的类型转换(显示转换、类型转换操作符)
  7. 成员函数和成员变量的可见范围(public/protected/private)
  8. 是否用模板就能实现?

20.宁以pass-by-refrence-to-const替换pass-by-value

  • 尽量以pass-by-reference-to-const替换pass-by-value,比较高效,并可避免切割问题
  • 对于内置类型,以及STL的迭代器和函数对象pass-by-value往往更高效

21.必须返回对象时,别妄想返回其reference或者pointer

  • 绝不要返回pointer或reference指向一个local stack对象(在函数退出前被销毁)
  • 不要返回pointer或reference指向一个heap对象(用户不知道如何delete)
  • 不要返回pointer或者reference指向local static对象而有可能需要多个这样的对象(同一行不能调用多次该函数,static只有一份)

22.将成员变量申明为private

  • 切记将成员变量申明为private,这可具有语法的一致性、更精确的访问控制、封装、提供class作者充分的实现弹性等优点
  • protected并不比public更有封装性

23.宁以non-member,non-friend函数替换member函数

  • 因为这种函数位于函数之外,不能访问类的private成员变量和函数,保证了封装性(没有增加可以看到内部数据的函数量)
  • 此外,这些函数只要位于同一个命名空间内,就可以被拆分为多个不同的头文件,客户可以按需引入头文件来获得这些函数,而类是无法拆分的(子类继承与此需求不同),因此这种做法有更好的扩充性

24.若所有参数都需要类型转换,请为此采用non-member函数

  • 只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者;this对象(隐喻参数)绝不是隐式类型转换的合格参与者
  • menber函数的反面是non-member函数,不是friend函数

  注:  

  举个例子,你想为一个有理数类实现乘法函数,支持与int类型的乘积,可以,因为传参int进去后会调用构造函数隐式转换为有理数类型,同时你想满足交换律,这时就会报错,因为int类型并没有一个函数用来支持你的有理数类做参数的乘法运算。解决方案是将该乘法运算函数作为一个非成员函数,传两个参数进去,这样不管你的int放在前面还是后面,都能作为参数被转换类型了。但是,非成员函数不代表就一定成为友元函数,能够通过public函数调用完成功能的,就不该设为友元函数,避免权力过大造成麻烦。

25.考虑写一个不抛出异常的swap函数

  • 当std::swap对自定义类型效率不高时(例如深拷贝),提供一个swap成员函数,并确定不会抛出异常
  • 如果提供一个member swap,也该提供一个non-member swap用来调用前者 (对class而言,需特化std::swap;对class template而言,添加一个重载模板到非std命名空间内)
  • 不可以添加新的东西到std内
  • 调用swap时应该针对std::swap使用using声明式,然后调用swap不带任何”命名空间修饰”

 

实现

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

  • 不只应该延后变量定义直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止,这样可增加程序的清晰度并改善程序效率

27.尽量少做转型动作

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast;试着发展无需转型的替代设计
  • 如果转型是必要的,试着将它隐藏于某个函数后
  • 宁可使用C++-style转型,不要使用旧式转型(新式转型很容易辨识出来,而分门别类)

  四种新式转型如下:

  1. static_cast:适用范围最广的,适用于很多隐式转换,基本数据类型的转换,基类指针与子类指针的相互转换,或者添加const属性,任何类型转换为void类型  
  2. dynamic_cast:主要用来执行“安全向下转型”,决定某对象是否归属继承体系中的某个类型。static_cast在下行转换时不安全,是因为即使转换失败,它也不返回NULL ,而dynamic_cast转换失败会返回NULL;对于上行转换,dynamic_cast和static_cast是一样的
  3. const_cast:通常用来将对象的常量性消除
  4. reinterpret_cast:在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针,不能将非32bit的实例转成指针。最普通的用途就是在函数指针类型之间进行转换,不可移植

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

  • 避免返回handles(包括references、指针、迭代器)指向对象内部(包括成员变量和不被公开的成员函数),否则会破坏封装性,使const成员函数的行为矛盾,以及发生“空悬虚吊号牌码”

29.为“异常安全”而努力是值得的

  • “异常安全函数”即使发生异常也不会有泄漏资源或允许任何数据结构败坏,区分为以下三种保证:
  1. 基本承诺:异常抛出,程序内的任何事物仍然保持在有效状态下
  2. 强烈保证:异常抛出,程序状态不改变,回复到调用函数之前的状态(往往能够以copy-and-swap实现出来)
  3. 不抛掷保证:绝不抛出异常(如内置类型)
  • 可能的话提供“nothrow保证”,当“强烈保证”不切实际时,就必须提供“基本保证”
  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者

30.透彻了解inline函数的里里外外

  • 将大多数inlining限制在小型、被频繁调用的函数身上
  • Template的具现化与inlining无关(Template放在头文件只是因为一般在编译器完成具现化动作)
  • inline只是给编译器的建议,大部分的编译器拒绝将太过复杂的函数inlining,隐喻方式是将函数定义于class定义式内
  • 构造函数和析构函数往往是inlining的糟糕候选人
  • 随着程序库的升级,inline函数需要重新编译,而non-inline函数只需重新连接

31.将文件的编译依存关系降到最低

  为了增加编译速度,应该减少类文件之间的相互依存性(include),但是类内又常常使用到其他类,不得不相互依存,解决方案是:将类的声明和定义分开(不同的头文件),声明相互依存,而定义不相依存,这样当定义需要变更时,编译时不需要再因为依赖而全部编译。
  • 依赖关系复杂导致的问题就是你修改了某个实现却需要编译很多文件,最好是 接口和实现分离。
  • 支持 “编译依存最小化” 的一般构想是:相依于声明式,不相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
  • 程序库头文件应该以 “完全且仅有声明式” 的形式存在。

 

继承与面对对象设计

32.确定你的public继承塑模出is-a模型

  • public继承意味着is-a。适用于base class身上的每一件事情也一定适用于derived class身上

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

  • derived classes 内的名称会遮掩base classes内的名称,应该避免。
  • 编译器对于各作用域有查找顺序,所以会造成名称遮掩,各作用域查顺序:
  1. local作用域
  2. derived class
  3. base class
  4. namespace
  5. globle作用域
  • 可以利用using声明式或者inline转交函数使遮掩函数重见天日

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

  • pure virtual函数使derived class只继承函数接口
  • impure virtual函数使derived class继承函数接口和缺省实现
  • non-virtual函数使derived class继承函数的接口和一份强制性实现

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

可以考虑一些 virtual 函数的替代方案,如:

  • 使用 non-virtual interface(NVI)手法,这是一种名为模版方法模式的设计模式,使用成员函数包裹虚函数。
  • 将虚函数替换为函数指针的成员变量
  • 将虚函数替换为 std::function 
  • 将继承体系内的虚函数替换为另一个继承体系内的虚函数(策略模式)

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

  注

  non-virtual函数是静态绑定的,virtual函数是动态绑定的

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

  • 如拒绝这样做,可能会在调用一个定义于derived class内的virtual函数时,使用base class指定的缺省参数值
  • 使用NVI手法(令public non-virtual函数调用private virtual函数)可以防止缺省参数值被重新定义

  注:
  为了运行期效率,c++坚持缺省参数值为静态绑定,防止运行期复杂的决定

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

  • 当复合发生于应用域内的对象之间,表现has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系
  • 复合的意义和public继承完全不同

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

  • private 继承意味着父类所有非私有成员在子类中都是 private 的。这样就帮我们复用父类代码且防止父类接口曝光
  • 但是私有继承意味着不再是 is-a 关系,而更像是 has-a 关系。我们总是可以通过复合的方式替代私有继承,并且更容易理解,所以无论什么时候,只要可以,我们还是应该选择复合。
  • 一种极端情况下,即我们有一个空白的父类,私有继承可以更小的占用空间。

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

  • 多继承比单继承复杂,而且多继承可能导致二义性,以及对 virtual 继承的需要。
  • 父类中存在数据的话,virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,应尽量不使用 virtual 继承。
  • 多继承适用的一种场景是:public 继承某个接口类并 private 继承某个协助实现的类。

 

模板与泛型编程

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

  • classe和template都支持接口和多态
  • 对class而言接口是显式的,由函数签名式构成;多态是通过virtual函数发生于运行期
  • 对template而言接口是隐式的,由有效表达式组成;多态是通过template具现化和函数重载解析发生于编译期

42.了解typename的双重意义

  • 声明template参数时,前缀关键字class和typename可以互换
  • 使用typename标识嵌套从属类型名称(如果编译器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型),但是不得在base class lists或member initialization list内作为base class修饰符

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

  • template特化版本可能不提供和一般性template相同的接口,所以从Object Oriented C++跨进Template C++时,继承就不像以前那般畅行无阻了
  • 为了令c++不进入templatized base classes观察的行为失效,可以:
  1. 在调用动作之前加上“this->”
  2. 使用using声明式(using baseclass::func;)
  3. 明白指出被调用的函数位于base class内(baseclass::func())

44.将参数无关代码抽离template

  • 非类型模板参数造成的代码膨胀,以函数参数或者class成员变量替换template参数
  • 类型模板参数造成的代码膨胀,可以让具有完全相同二进制表述的具现类型共享实现码

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

  • 请使用成员函数模版生成“可接受所有兼容类型”的函数
  • 即使声明了“泛化拷贝构造函数”和“泛化的赋值操作符”,仍然需要声明正常的拷贝构造函数和拷贝赋值操作符

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

  • 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”
  • 在一个class template内,template名称可被用来作为“template”的简略表达方式

  注:
  template实参推导过程中从不将隐式类型转换函数纳入考虑,而class template并不依赖template实参推导,在生成模板类时就可推导出函数而非函数模板

47.请使用traits classes表现类型信息

  •  Traits classes 使得“类型相关信息”在编译期可用。它们以 templates 和 “templates 特化”完成实现
  • 整合重载技术后。traits classes 有可能在编译期对类型执行 if…else 测试

48.认识模板元编程

  • 模板元编程可将工作由运行期移至编译期,因而得以实现早期错误侦测和更高的执行效率,可能导致较小的可执行文件,较短的运行期,较少的内存需求,可以解决不少问题

 

定制new和delete

49.了解new-handler的行为

  • 当operator new无法满足某一内存分配需求时,它会抛出异常;抛出异常之前,也可以先调用一个客户指定的错误处理函数(new-handler),调用set_new_handler可以指定该函数
  • Nothrow new(在无法分配足够内存时返回NULL)是一个颇为局限的工具,它只适用于内存分配,后继的构造函数调用还是可能抛出异常

50.了解new和delete的合理替换时机

  • 有许多理由需要写个自定的new和delete,包括检测错误、改善效能,以及收集使用上的统计数据等等

51.编写符合常规的new和delete

  • operator new应该内含一个无限循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”
  • operator delete应该在收到null指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”(如果大小错误,调用标准版本的operator new和delete)

52.写了placement new也要写相应的placement delete

  • new表达式先后调用operator new和default构造函数
  • 当你写一个placement operator new,请确定也写出了对应的placement operator delete.如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏(运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete,如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用)
  • 当你声明placement new和placement delete,请确定不要无意识地遮掩了它们的正常版本

 

杂项讨论

53.不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息,努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉(在你打发某个警告信息之前,请确定你了解它意图说出的精确意义)
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失

54.让自己熟悉包括TR1在内的标准程序库

  • TR1详细叙述了14个新组件,放在std命名空间内(std::tr1)包括:智能指针、tr1::function、tr1::bind、Hash tables(用来实现sets、multisets、maps和multi-maps)、正则表达式、Tuples(变量组)、tr1::array、tr1::mem_fn(语句构造上与成员函数指针一致)、tr1::reference_wrapper(使容器“犹如持有references”)、随机数生成工具、数学特殊函数、C99兼容扩充以及Type traits(一组traits classes)、tr1::result_of(推导函数调用的返回类型)

55.让自己熟悉Boost

 

参考

《Effective C++》

https://blog.csdn.net/a245705313/article/details/81783455

https://www.jianshu.com/p/4661bd7b7593

https://blog.csdn.net/afei__/article/details/83624720

 

posted @ 2020-07-10 16:03  Chen沉尘  阅读(376)  评论(0编辑  收藏  举报