EffectiveC++
简介
effectiveC++ 这本书是C++程序员的工作必备之书,讲述了在C++开发中常用的一些,以及经常注意的一些规则,遵循它且不要忽视它,我们就能写出很好的友善的C++代码。
项目链接
点击下载: github项目链接
一. 让自己习惯C++
条款01: 视C++为一个语言联邦
C++是一个多重范型编程语言(Multiparadigm programing language)
- 支持过程形式
- 面向对象形式
- 函数形式
- 泛型形式
- 元编程形式
我们理解其C++时应该视其为 一个相关语言组成的联邦(有4个次语言)
- C:对于C++问题的解法类似C的高级解法时,高效的解法就是去映射C语言的规范,不要掺杂过多C++其他此语言特性
- Object-Oriented C++: 这部分即 C With Class,C++的主流编程
- Template C++: 十分强大的模板编程范式
- STL 模板库: STL 对于容器迭代器算法函数对象等等的规约有极佳的紧密配合与协调,伙同STL进行开发,要遵循其规约
C++对于该4个次语言都有它自己的规约, 记住这4个次语言你就会发现C++容易了解的多
- 建议: C++高校编程守则视情况而变化,取决于你使用C++的哪一个部分。
条款02: 尽量以 const, enum,inline 替换 #define
请看以下代码:
#define ASPECT_RATIO 1.653
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
ASPECT_RATIO 从未被编译器看到,而是被预处理器展开, 可能出现的问题:
- 运用该宏定义的常量时出现了编译错误信息时,错误提示提到的是 1.653 而非常量名,排查错误难
- 普通的变量会被编译器看到,会记录到记号表中(symbol table) ,而宏定义的常量不会被记录,导致出现
常量值的目标码(object code),从而可能造成代码膨胀(代码膨胀会导致额外的换页行为,降低指令高速缓存装置的击中率,带来效率损失)。 - 函数宏虽不会有函数调用的栈方面的开销,但是缺点很明显,难读,容易出错
一. 以 const 替换 宏定义常量
二. 以enum hack 替换 宏定义常量
三. 以 inline 替换 宏定义函数
总结
- 对于单纯常量, 最好以 const 对象或 enums 替换 #defines
- 对于形似函数的宏, 最好改用 inline函数替换 #define
条款03: 尽可能使用 const
一. const 是C++中的对于变量语义约束(不可修改),编译器会强制实行这项约束,只要该值不可被改变(事实),就应该去进行约束
二. const 最具威力的是面对函数声明时的应用, const可以与函数返回值,各参数,函数自身(成员函数)产生关联
- 令函数返回一个常量值,可以降低因客户操作而造成的(意外错误)
- const 实施成员函数: 确认该成员函数可作用于 const 对象身上。
第一:使得 class接口更加容易被理解,(得知那些函数可以改动对象内容那些不行)
第二:使得操作 const 对象成为可能, (pass by reference-to-const 方式传递对象),我们有const成员函数处理const对象。
成员函数如果只是常量性不同,可以被重载
如何实施对对象进行const限制的措施:
- bitwise const阵营:不更改对象内的任何一个 bit (太过于强硬)
- bitwise constness阵营: 对对象的成员变量实施 const(编译器的做法)
- logical constness(重要): 一个const成员函数可以修改它所处理的对象的某些 bits,但请确保客户端侦测不出 (实现办法是利用C++的一个与const相关的摆动场: mutable)
在const 和 non-const 成员函数中避免重复(写出重复的代码)
总结:
- 将某些东西声明为const能帮助编译器去甄别错误用法
(作用域对象,函数参数,函数返回类型,成员函数本体) - 编译器强制实施 bitwise constness,但我们编写程序时应该使用 “概念上的常量性”
- 当const 和 non-const成员函数有着实质等价的实现时,令 non-const 版本调用const版本可避免代码重复
条款04: 确定对象被使用前已经被初始化
一. C++的对象成员变量的初始化发生在进入构造函数本体之前
如上代码会对成员先进行默认构造函数的调用,之后在进行赋值。
解决办法是: 使用 member initalization list 初始化列表替换赋值动作。 为了规范统一:将全部成员(无物也要使用初始化列表初始化)
注意:C++类对象的初始化次序: base class 总是早于其 derived class 被初始化, class 的成员变量总是以其声明次序被初始化
二. C++对于定义在不同编译单元内的non-local static 对象的初始化相对次序无明确定义。
解决办法就是使用 Singleton 使得 non-local static 搬到自己的专属函数中
C++保证, 函数内的 local static对象会在 “首次遇上该对象之定义上” 被初始化。
多线程环境下执行顺序的麻烦性: 尽量在单线程运行期按一定顺序初始化这些 static
总结:
- 为内置型对象进行手工初始化,因为 C++不保证初始化他们
- 构造函数最好使用成员初始值,而不要在构造函数本体内使用赋值操作, 且初始
列列出的成员变量,其排序次序应该和他们在 class 中声明次序相同 - 为免除“跨编译单元之初始化次序”问题, 请以 local static 对象替换 non-static 对象
二. 构造/析构/赋值运算
条款05: 了解C++默认调用哪些函数
一. 检阅一个 empty 类编译器为其做的事情
二: 编译器合成的函数做的事情
发现: operator= 与 copy构造函数 都是编译器合成, 内置类型的成员使用拷贝bit方式,非内置则调用 其定义的 operator= 与 copy构造函数从右侧操作数拷贝数据
三. 注意点:
- 默认拷贝赋值运算符/拷贝构造函数在成员含引用类型时不能被生成
- 当基类将拷贝赋值运算符/拷贝构造函数声明为 private或 delete, 也是不能被生成的。
总结:
编译器可以暗自为class 创建default构造函数,copy构造函数,copy assignment 操作符,以及析构函数。
条款06: 若不想使用编译器自动生成的函数,就该明确拒绝
一. 最简单的拒绝(copy构造函数与 copy赋值运算符)办法是 声明其为 private
- 为了解决在成员函数和友元函数仍然还是能调用,声明其而不定义其是个好办法(会报链接错误)
- 为了将链接错误提前到编译期,需要将其继承一个阻止 copy 的 base class(利用了继承了基类的拷贝操作为private的类,编译器自身将不会生成其 拷贝操作,使用时就会报错)
条款07: 为多态基类声明 virtual 析构函数
一:问题浮现: 销毁一个heap分配的基类指针(指向的是派生类)内存泄漏问题
原因: 通过GetTimeKeeper 返回的指针是一个基类指针,销毁基类指针则会取基类的部分(调用基类的析构函数)
官方: C++明白指出,当derived class对象经由一个base class指针被删除,而其base class带一个 non-virtual函数, 其结果就是未定义-实际执行下来发生的就是对象的 derived 成分没被销毁
解决: 给base class 设置一个 virtual 析构函数即可
二: 验证: 任何 class 带有virtual函数都几乎确定应该有一个 virtual 析构函数, 没有理由地把所有 class 的析构函数设置为 virtual的行为是错误的。
三: 利用析构函数实现抽象类, 适用于没有其余能定义pure virtual函数的类
总结:
-
polymorphic base classes 应该声明一个virtual析构函数, 如果 class 带有任何 virtual 函数,他就应该拥有一个 virtual 析构函数
-
Class 的设计目的如果不是当作 base classes 使用,就不应该声明 virtual 析构函数
条款08: 别让异常逃离析构函数
首先C++并不禁止析构函数抛出异常,但在析构函数中抛出异常很容易导致内存泄漏(程序过早结束)
一: 验证析构函数抛出异常的问题
二: 使用最佳策略解决该问题,避免析构函数传播异常
我们要对 “导致 close 抛出异常” 的情况做出反应
重新设计 DBConn接口,使客户对有机会对可能出现的问题作出反应
1: 管理类提供一个 close 函数,赋予客户一个机会处理因该操作而发生的异常。
2: 管理类设置标志位并在析构函数调用时检测其是否正常关闭,如果未关闭,则正常关闭
3: 第二步在析构函数种再次关闭失败后,我们将又退回 “强迫结束程序或吞下异常的套路”
总结:
-
析构函数绝不要吐出异常, 如果一个被析构函数调用的函数可能抛出异常,析构函数
应该捕捉任何异常, 然后吞下他们(不传播)或结束程序。 -
如果客户端需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个 普通函数(而非在析构函数种)执行该操作
条款09: 绝不在构造和析构过程中调用 virtual 函数
一: 证明在 base class 构造期间, virtual 函数不是 virtual 函数
原因1: 如果在构造base class时调用的是 derived class 的函数(会使用到derived class成员, 但此时成员都是未构造的,会出现问题)。
原因2: 在derived class对象的 base class构造期间,对象本身是base class而不是 derived class不止virtual函数会被编译器解析至 base class,运行期类型信息,也会把对象视为 base class 。
相对应析构函数执行到base class部分,编译器也会视当前对象为 base class
二:如何确保每一次有 Transaction继承体系上的对象被创建,就会有适当版本的 logTransaction被调用
由于你无法在 base class构造时期通过 virtual函数 调用到 derived class 的函数,
因此可以使用 非virtual 通过在 derived class构造函数传(必要参数)传递到 base class 的构造函数, 进而调用 base class 的通过必要参数而实行的普通函数
总结
- 在base class 构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class
(比起当前执行构造函数和析构函数那层),virtual本质上并没有用
条款10: 令 operator= 返回一个 reference to *this
这份协议可以说是为了实现连锁赋值而 创造的协议
总结:
连锁赋值几乎