Effective C++ 学习笔记 1:让自己习惯 C++
- Effective C++ 学习笔记 1:让自己习惯 C++
- Effective C++ 学习笔记 2:构造、析构、赋值运算
- Effective C++ 学习笔记 3:资源管理
- Effective C++ 学习笔记 4:设计与声明
- Effective C++ 学习笔记 5:实现
- Effective C++ 学习笔记 6:继承与面向对象设计
- Effective C++ 学习笔记 7:模板与泛型编程
- Effective C++ 学习笔记 8:定制 new 和 delete
- Effective C++ 学习笔记 9:杂项讨论
条款 01:让自己习惯 C++
Accustoming Yourself to C++.
C++是语言联邦,它综合了多种编程语言的特点,是多重范型编程语言(注意是范型,不是泛型),支持过程形式(procedural),面向对象形式(object-oriented),函数形式(functionnal),泛型形式(generic),元编程形式(meta programming)。
将 C++认为是多种次语言的结合,次语言有四种:
- C 语言基础。C++最早出现时,从 C 中派生出来的那些特性。
- Object-Oriented C++。也就是 C with classes,类,继承,多态,虚函数这些概念。
- Template C++。C++的泛型编程,类和对象泛型化。
- STL:最重要的模板库,提供容器、迭代器、算法和函数对象等。
在次语言之间切换工程,需要认真遵守当前次语言的规范。
总结
C++高效编程守则视状况而变化,取决于你使用 C++的哪一部分。
条款 02:尽量以const, enum, inline 替换 #define
** Prefer consts, enums, and inlines to #defines.**
话题 1:用 const 和 enum 代替 #define 常量
#define 定义之下的标记不会经由编译器处理,记住这个道理,在编译器之前被预处理器处理了,所以 #define 引入的问题,编译器很难查出来。使用 const 代替 #define 来定义常量,编译器可以帮助检查如类型错误这一类问题。
const 作用于指针,分为作用到指针指向对象的不变性和指针本身的不变性。
以下代码定义了一个类内的类静态常量成员(类专属常量成员):
1 | class Game { |
如果是类静态整形常量成员,C++ 要求如果你要取 NumTurns 的地址,或者编译器要查看,那必须为其提供一个定义式,放到实现文件中:
1 | const int Game::NumTurns; // 这是定义,因为声明时已指定常量值,这里不能再指定一次 |
现代编译器允许对类静态整形常量成员在声明时指定常量值。
对于非整数的其他类静态常量成员,无法在声明中指定常量值,这时可通过定义指定常量(前者在声明时指定常量,是为了能在后续的其他声明时用,比如 scores 数组长度)。
书中还给出了补偿做法。如果编译器不允许在声明时指定常量值,可采用 enum 方式:
1 | class Game { |
enum 无法取地址,所以其更像 #define 而不是 const,如果你希望能约束对 NumTurns 的取地址操作,可采用 enum hack 的方式。
enum hack 是模板元编程的基础技术。
话题 2:用 inline 取代 #define 宏
我们已经知道 #define 宏来作为函数功能会导致一些难以调试的 bug。
为了避免这些 bug,我们需要做保护性操作,比如对每个参数都加括号,但依然对一些情况无法避免。例子如下:
1 |
|
如注释说明。这种问题很难调试,因为要记住,#define 不会输入编译器。
话题 3:#define 不等同于预处理操作
不要认为因为 #define 有各种问题,就否定预处理操作。除了 #define 以外,还有很多预处理指令。
#include在文件包含中必不可少;#ifdef和#ifndef在编译控制中无法被替代;#pragma控制编译特性也会被用到;
总结
- 对于单纯常量,最好用 const 对象或 enum s 替换
#defines。 - 对于
#define宏,最好用 inline 函数替换。
条款 03:尽可能使用 const
Use const whenever possible
话题 1:const 修饰指针
众所周知,const 修饰指针分为修饰指针本身(指针只能指向固定的地址,但指向位置的值可变)和修饰指针指向的对象(指针指向的位置的值固定,但指针可指向不同地址),或者两者。
对于后者,有些人习惯将 const 写在类型前,有些人习惯写在 * 前,这都是对的。
1 | void f1(const Widget * pw); |
使用迭代器时,默认迭代器 iterator 在实现上等同于 T *,对其进行 const 修饰,等同于修饰指针本身为 const,若希望实现 const 修饰迭代器指向位置的值为固定值,使用 const_iterator:
1 | const std::vector<int>::iterator iter = vec.begin(); // 等同于 T * const |
不过,还是建议使用 auto 类型来接受迭代器对象。
话题 2:const 修饰函数
const 修饰返回值时,可以有效防止错误的自定义类型赋值操作:
1 | class Rational { ... }; |
上例中,如果operator* 没有将返回值修饰为const,则后续的赋值操作编译器不会报错,但其本身也没意义,还可能隐含有 bug 难以排查。
内置类型不存在这个问题,无法给内置类型运算结果赋值。
话题 3:const 修饰成员函数(重要)
可针对同一种功能接口(成员函数)区分两种不同返回类型(non-const 和 const),来分别处理 non-const 对象和 const 对象。
另外,重要哲学思辨:const 修饰成员函数本身有两种解释:bitwise constness(又称 physical constness)和 logical constness。
bitwise constness 认为,成员函数只有在不更改任何成员变量(static 除外)时,才是 const 的。
logical constness 说,你有漏洞,const 的成员函数中,通过指针更改了指针所指物,如果所指物本身属于类,而指针本身不会在 const 的成员函数中被修改,那么编译器是不会报错的,比如:
1 | class CTB { |
但实际操作中,operator[] 还是会修改类成员变量:
1 | const CTB cctb("Hello"); // 常量对象 |
logical constness 认为,const 修饰成员函数,可以修改他所处理的对象内的数据,但只有当客户端感知不到的情况下才可以。
编译器按 bitwise constness 检查代码,但程序员应该做到 logical constness。
另外,引入 mutable 关键字,用来释放掉非静态成员变量的 bitwise constness 约束:
1 | class CTB { |
话题 4:const_cast 应用
const_cast 用于做与 const 相关的类型转换,移除 const 或添加 const。
当类中 non-const 和 const 版本的相同功能成员函数的内部功能完全一致的情况下,为了避免重复 copy-paste,可采用 const_cast 来协助。
请查看如下示例代码:
1 | class TB { |
注意,反过来,在 const 成员函数中使用这种方法调用 non-const 成员函数,是不合理的。
总结
- 将某些东西声明为 const 可以帮助编译器检查错误。 const 可用于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制执行 bitwise constness,但编程时应当按照 logical constness 完成。
- 当 const 和 non-const 成员函数有完全等价的实现时,可使 non-const 版本调用 const 版本来避免代码重复。
条款 04:确定对象被使用前已先被初始化
Make sure that objects are initialized before they’re used.
C++ 在一些情况下,不会给用户未指定的对象赋初值,这种时候,对象的值是未定义的任意值。
解决方法就是,确保都被初始化,如果是非基本类型对象,确保在构造函数中对对象的所有数据对象初始化。
话题 1:不要混淆赋值和初始化
看下边的例子:
1 | class ABE { |
如注释述,这种方式是赋值,虽然也能达到效果,但 C++规定,对象的成员变量的初始化动作要发生在进入构造函数本体之前,所以实际上编译器会在进入构造函数之前,先给所有数据成员做初始化,然后进去后再做赋值操作。
应当使用初始化列表来完成初始化(而不是在构造函数中赋值)。初始化列表这么用:
1 | ABE::ABE(const std::string& name) |
这么做效率高,因为不需要先调 ABE 的 defualt 构造函数再调用 std::string 的 copy 赋值操作,而是直接调用 std::string 的 copy 操作。内置类型没有影响,但考虑到格式统一,也放到初始化列表中为宜。
但注意,如果没有手动对内置类型对象做初始化,可能编译器也不会帮你做。
建议所有数据成员都写在 default 构造函数的初始化列表中,包括内置类型。这样另外一个好处是,const 和 reference 的数据成员可以被初始化了,而赋值不行,既然初始化列表都能做,干嘛还在构造函数内赋值呢。
初始化列表中的次序与其被初始化的顺序无关,初始化顺序取决于数据成员的声明顺序,但为了便于理解,建议顺序统一。这种场景比如先初始化 array 的长度,再初始化 array,反过来就会出错。
话题 2:non-local static 对象
static 对象的生存期从其构造出开始,到程序结束为止。
位于函数中的 static 对象称为 local static 对象,否则称为 non-local static 对象。
C++对定义在不同的编译单元(不同的源文件内)中的 non-local static 对象的初始化次序是未定义的。
所以存在的问题是:如果多个编译单元内同时有non-local static 对象,而且他们有依赖,将怎么保证正确性?
答案是没办法做到。
建议是,把这些 non-local static 对象放到函数里边,变成 local static 对象,然后通过调用函数传引用的方式来使用这些对象。编译器可以保证在调用函数且遇到 local static 对象时将其初始化。
但是,在多线程应用时,依然会存在问题,解决办法是,在多线程应用的单线程运行时(启动多线程之前),将这些函数都调用一下,让编译器创建其内部的所有 local static 对象。
总结
- 为内置类型对象进行手动初始化,C++不保证为他们初始化。
- 构造函数最好用成员初始化列表,而不要在内部使用赋值操作。初始化的排列次序与初始化列表中顺序无关,和声明次序一致。
- 为避免跨编译单元的初始化次序问题,将这些 non-local static 对象变成 local static 对象。
封面图片来自:Photo by Adam Bignell on Unsplash
本文同步发布在 CSDN 账号下:Effective C++ 学习笔记 第一章:让自己习惯 C++



















