Effective C++快速复习
习惯 C++
01 视 C++ 为一个语言联邦:C、Object-Oriented C++、Template C++、STL
02 尽量以 const, enum, inline 替换 #define:其实是尽量以编译器替换预处理器比较好,因为 #define 只是简单的字符串匹配替换,编译器无法看到,不利于 debug,也容易出问题
03 尽可能使用 const:Rust 语言设计就是 const 作为默认语义,可以帮助编译器侦测出错误用法
04 确定对象被使用前已经被初始化:内置类型对象进行手动初始化,C++不保证初始化他们;尽量使用初始化列表来初始化
构造,析构,赋值
05 了解 C++ 默认编写了哪些函数:默认构造函数、默认拷贝构造函数、默认拷贝赋值操作符、默认移动赋值操作符、析构函数
06 可以明确拒绝编译器自动生成的函数:例如将相应函数声明为 private 且不予实现,例如 Boost 里边有个 uncopyable 类
07 为多态基类编写 virtual 函数:带有多态性质的基类应该声明 virtual 析构函数,或者说一个类只要带有 virtual 函数,就应该有 virtual 析构函数;
08 别让异常逃离析构函数:如果析构函数调用的函数可能抛出异常,那么析构函数就应该捕捉任何一场,不传播或者直接 abort ;如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么类应当提供一个普通函数,instead of 析构函数,执行该操作
09 绝不在构造和析构过程中调用 virtual 函数:你以为你调用的是子类的 virtual 函数,实际上可能是父类的 virtual 函数。在基类构造和析构期间调用的 virtual 函数不可下降至派生类。
*10 令 operator= 返回一个 this 引用:为了实现连续赋值
11 在 operator= 中处理自我赋值:比较来源对象和目标对象的地址是否相同;包括如果操作一个大的对象里有若干个对象,也要分别考虑是否有自我赋值。
12 复制对象时勿忘其每一个成分:拷贝赋值操作符要覆盖所有成员变量以及基类的成分(例如直接调用基类的拷贝赋值操作符);但是不要在拷贝赋值操作符里调用拷贝构造函数,而是要把二者的共同点提取出来作为一个新的函数
资源管理
13 以对象管理资源:使用 RAII 和智能指针的技术防止资源泄露
14 资源管理类中小心 coping 行为:常见的 RAII class copying 行为是:抑制复制、使用引用计数法、一同复制底部资源、转移底部资源的所有权
15 在资源管理类中提供对原始资源的访问:因为 APIs 往往要求访问原始资源(raw resources);可以采用显式转换和隐式转换的方式,前者比较安全,后者方便用户使用
16 成对使用 new 和 delete 时要采取相同形式:就是在有没有带数组 [] 符号上,new 和 delete 必须要统一
17 以独立语句将 new 得到的对象放入智能指针:意思是智能指针的以一个原始指针作为参数的构造函数是 explicit 的,所以要主动将 new 得到的对象指针放入智能指针里。
设计与声明
18 让接口容易被正确使用、不易被误用:接口一致、与内置类型行为兼容;消除客户的资源管理责任
19 设计 class 犹如设计 type:考虑新的类型应当如何被创建和销毁、初始化和赋值有着怎样的差别、值传递和引用传递有什么差别、是否需要配合某个继承体系、是否需要类型转换、如何使用、是否真的需要一个新的 class
20 宁以 pass-by-reference-to-cont 替换 pass-by-value:前者更高效,并且在继承关系存在时还可以防止子类部分被切割掉;但对于内置类型、STL迭代器和函数对象,后者更加合适。
21 必须返回对象时,不要返回其 reference:local stack 对象在函数结束时就会被销毁,如果是 heap-allocated 对象也应该返回 pointer 而不是 reference
22 将成员变量声明为 private:protedted 并不必 public 更具有封装性;将成员变量全部声明为 private,可赋予客户访问数据的一致性、进行更细粒度的访问控制、允许约束条件得到保证,给类的编写者更加充分的实现弹性
23 宁以 non-member、non-friend 替换 member 函数:这样可以提高封装性、包裹弹性和机能扩充性,可以将这类 non- 函数与类放在同一个命名空间下。(可以这样理解:增加这些 non- 函数只会在原有的封装性下做事情,而增加 member 函数却有可能会暴露出新的访问权限)
24 若所有参数皆需类型转化,请为此采用 non-member 函数:令 class 支持隐式类型转换往往是个坏主意;例如设计一个新的数值类型,它的算数运算想要兼容原有的内置类型,就需要将这些 operator+ 等函数写成 non-member;
25 考虑写一个不抛出异常的 swap 函数:自己提供的 swap 成员函数效率更高;定义完一个 member swap,也应当提供一个 non-member swap(即特化 std::swap()),来方便使用;为用户定义的类型进行 std templates 全特化是好的,但是不要往 std 命名空间里加入全新的东西。
实现
26 尽可能延后变量定义出现的时间:要用了再来定义,而不要像一些老的代码一样在开头定义好所有需要用到的变量;这样程序更加清晰,而且效率也可能得到提升。
27 尽量少做 casting 动作:即使要类型转换,也用 C++ 的新式类型转换(static_cast、dynamic_cast、const_cast、reinterprete_cast),这样在代码中容易被识别,也方便编译器诊断出错误;注重效率的代码中尽量不要用 dynamic_cast;注意 reinterprete_cast 不可移植;
28 避免返回 handles 指向对象内部成分:handles 指 references、指针、迭代器等,这样可以增加封装性
29 注意异常安全:注意不泄露任何资源、不允许数据破坏
30 透彻了解 inlining 的里里外外:inline 更适合小型的、被频繁调用的函数上,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
31 将文件键的编译依存关系降至最低:能使用 object reference 或者 pointer 弯沉任务的,就不要使用 object;尽量用 class 声明式替换定义式;为声明和定义提供不同的源文件,头文件应当完全仅有声明式。
继承与面型对象设计
32 public 继承应当塑模出 is-a 关系:is-a 关系是说,子类是一种基类,比如苹果是一种水果
33 避免遮掩继承而来的名称:可以用 using 声明或者 forwarding functions 的技巧来使用父类中被遮掩的名称
34 区分接口继承和实现继承:成员函数的接口总是会被继承;
- 声明 pure virtual 函数是为了让派生类只继承函数接口、而必须要自己实现这个函数;
- 声明 impure virtual 函数的目的,是让派生类继承该函数接口,同时提供一个默认实现版本
- 声明 non-virtual 函数是为了让派生类直接继承函数接口,并且提供强制性实现
35 考虑 virtual 函数以外的其他选择:例如可以用 non-vritual interface 实现 Template Method 模式、Function Pointer 或 std::function 实现 Stategy 模式、
36 绝不重新定义继承而来的 non-virtual 函数:正如 34 中所讲,这是为了提供一份强制性的实现。
37 绝不重新定义继承而来的缺省参数值:缺省参数值是静态绑定,如果派生类继承了一个带有缺省参数值的函数,那么一定不要改缺省参数值(可以直接不写)
38 通过复合塑模出 has-a 或 is-implemented-by:复合是指 composition
39 审慎地使用 private 继承:private 继承也是 is-implemented-by 的意思,比复合的级别更低,可以实现 EBO;
40 审慎地使用多重继承:多重继承可能会带来歧义,而且 virtual 继承会带来 overhead,当 virtual base class 不带任何数据,将是最具实用价值的情况。当然多重继承也有正当用途,例如 public 继承某个 interface class 的同时 private 继承某个协助实现的 class
模板与泛型编程
41 了解隐式接口和编译器多态:对于 template 而言,接口是隐式的,基于有效表达式,多态则是通过template 具现化和函数重载解析发生于编译期。
42 了解 typename 的双重意义:既可以用在模板参数声明时,和 class 一样的用法,又可以用在表示由模板参数得到的派生类型上,标识嵌套从属类型名称,告诉编译器这是个类型。
43 学习处理模板化基类内的名称:在 derived class template 内通过 this-> 来引用 base class template 内的成员名称
44 将与参数无关的代码抽离 templates:因非类型模板参数而造成的代码膨胀,往往可以通过以函数参数或者 class 成员变量替换 template 参数来消除;
45 运用成员函数模板接受所有兼容类型:如果声明 member templates 用于泛化拷贝构造动作或泛化赋值操作,还是需要声明正常的拷贝构造函数和赋值操作符
46 需要类型转换时请为模板定义非成员函数:当我们编写一个 class template,而它所提供的与此template相关的函数支持所有参数的隐式类型转换时,请将哪些函数定义为 class template 内部的 friend 函数(例如前面提到的定义一个新的数值类型)
47 使用 traits classes 表现类型信息:通过萃取其可以是类型相关信息在编译器可用,参考 STL 的 iterator 实现
48 认识模板元编程:是一种编程范式,可将工作从运行期移往编译器,更早实现错误侦测,具有更高的执行效率
定制 new 和 delete
49 了解 new-handler 的行为:
-
需要让更多内存可以被使用
-
当前 new-handler 无法获取更多内存时,可以安装别的 new-handler 替换自己
-
卸除 new-handler
-
抛出 (派生自)bad_alloc 的异常
-
不返回:通常调用 abort 或 exit
50 了解替换编译器的 new 和 delete 的合理时机:往往是为了获得非传统的行为
- 用来检测运用上的错误:例如 new 成功而 delete 失败造成的内存泄漏
- 强化效能:编译器提供的 new 和 delete 不适用于长时间执行的程序
- 收集统计数据:用于调试、收集 heap 使用信息
51 编写 new 和 delete 时需固守常规:
- operator new 内应当含有一个无穷循环,并且不断尝试分配内存;如果无法满足内存需求,就应当调用 new-handler;能够处理 0B 的申请;
- operator delete 在接受 null 指针时不要做任何事情;
52 写了 palcement new 也要写 placement delete:
杂项讨论
53 不要轻忽编译器的警告:0 error 不是最终目标
54/55 熟悉 TR1、Boost 等标准程序库 :虽然很多东西现在 C++11 以后都有了