• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# C++语言编程规范
2
3## <a name="c0-1"></a>目的
4规则并不是完美的,通过禁止在特定情况下有用的特性,可能会对代码实现造成影响。但是我们制定规则的目的“为了大多数程序员可以得到更多的好处”, 如果在团队运作中认为某个规则无法遵循,希望可以共同改进该规则。
5参考该规范之前,希望您具有相应的C++语言基础能力,而不是通过该文档来学习C++语言。
61. 了解C++语言的ISO标准;
72. 熟知C++语言的基本语言特性,包括C++ 03/11/14/17相关特性;
83. 了解C++语言的标准库;
9
10## <a name="c0-2"></a>总体原则
11代码需要在保证功能正确的前提下,满足**可读、可维护、安全、可靠、可测试、高效、可移植**的特征要求。
12
13## <a name="c0-2"></a> 重点关注
141. 约定C++语言的编程风格,比如命名,排版等。
152. C++语言的模块化设计,如何设计头文件,类,接口和函数。
163. C++语言相关特性的优秀实践,比如常量,类型转换,资源管理,模板等。
174. 现代C++语言的优秀实践,包括C++11/14/17中可以提高代码可维护性,提高代码可靠性的相关约定。
18
19
20## <a name="c0-3"></a> 约定
21**规则**:编程时必须遵守的约定(must)
22
23**建议**:编程时应该遵守的约定(should)
24
25本规范适用通用C++标准, 如果没有特定的标准版本,适用所有的版本(C++03/11/14/17)。
26
27## <a name="c0-4"></a> 例外
28无论是'规则'还是'建议',都必须理解该条目这么规定的原因,并努力遵守。
29但是,有些规则和建议可能会有例外。
30
31在不违背总体原则,经过充分考虑,有充足的理由的前提下,可以适当违背规范中约定。
32例外破坏了代码的一致性,请尽量避免。'规则'的例外应该是极少的。
33
34下列情况,应风格一致性原则优先:
35**修改外部开源代码、第三方代码时,应该遵守开源代码、第三方代码已有规范,保持风格统一。**
36
37# <a name="c2"></a>2 命名
38## <a name="c2-1"></a>通用命名
39__驼峰风格(CamelCase)__
40大小写字母混用,单词连在一起,不同单词间通过单词首字母大写来分开。
41按连接后的首字母是否大写,又分: 大驼峰(UpperCamelCase)和小驼峰(lowerCamelCase)
42
43
44| 类型                                       | 命名风格      |
45| ---------------------------------------- | --------- |
46| 类类型,结构体类型,枚举类型,联合体类型等类型定义, 作用域名称         | 大驼峰       |
47| 函数(包括全局函数,作用域函数,成员函数)                    | 大驼峰       |
48| 全局变量(包括全局和命名空间域下的变量,类静态变量),局部变量,函数参数,类、结构体和联合体中的成员变量 | 小驼峰       |
49| 宏,常量(const),枚举值,goto 标签                  | 全大写,下划线分割 |
50
51注意:
52上表中__常量__是指全局作用域、namespace域、类的静态成员域下,以 const或constexpr 修饰的基本数据类型、枚举、字符串类型的变量,不包括数组和其他类型变量。
53上表中__变量__是指除常量定义以外的其他变量,均使用小驼峰风格。
54
55## <a name="c2-2"></a> 文件命名
56### <a name="a2-2-1"></a>建议2.2.1 C++文件以.cpp结尾,头文件以.h结尾
57我们推荐使用.h作为头文件的后缀,这样头文件可以直接兼容C和C++。
58我们推荐使用.cpp作为实现文件的后缀,这样可以直接区分C++代码,而不是C代码。
59
60目前业界还有一些其他的后缀的表示方法:
61
62- 头文件:  .hh, .hpp, .hxx
63- cpp文件:.cc, .cxx, .c
64
65如果当前项目组使用了某种特定的后缀,那么可以继续使用,但是请保持风格统一。
66但是对于本文档,我们默认使用.h和.cpp作为后缀。
67
68
69### <a name="a2-2-2"></a>建议2.2.2 C++文件名和类名保持一致
70C++的头文件和cpp文件名和类名保持一致,使用下划线小写风格。
71
72如果有一个类叫DatabaseConnection,那么对应的文件名:
73- database_connection.h
74- database_connection.cpp
75
76结构体,命名空间,枚举等定义的文件名类似。
77
78## <a name="c2-3"></a>  函数命名
79函数命名统一使用大驼峰风格,一般采用动词或者动宾结构。
80```cpp
81class List {
82public:
83	void AddElement(const Element& element);
84	Element GetElement(const unsigned int index) const;
85	bool IsEmpty() const;
86};
87
88namespace Utils {
89    void DeleteUser();
90}
91```
92
93## <a name="c2-4"></a> 类型命名
94
95类型命名采用大驼峰命名风格。
96所有类型命名——类、结构体、联合体、类型定义(typedef)、枚举——使用相同约定,例如:
97```cpp
98// classes, structs and unions
99class UrlTable { ...
100class UrlTableTester { ...
101struct UrlTableProperties { ...
102union Packet { ...
103
104// typedefs
105typedef std::map<std::string, UrlTableProperties*> PropertiesMap;
106
107// enums
108enum UrlTableErrors { ...
109```
110
111对于命名空间的命名,建议使用大驼峰:
112```cpp
113// namespace
114namespace OsUtils {
115
116namespace FileUtils {
117
118}
119
120}
121```
122
123
124### <a name="a2-4-1"></a>建议2.4.1 避免滥用 typedef或者#define 对基本类型起别名
125除有明确的必要性,否则不要用 typedef/#define 对基本数据类型进行重定义。
126优先使用`<cstdint>`头文件中的基本类型:
127
128| 有符号类型    | 无符号类型     | 描述               |
129| -------- | --------- | ---------------- |
130| int8_t   | uint8_t   | 宽度恰为8的有/无符号整数类型  |
131| int16_t  | uint16_t  | 宽度恰为16的有/无符号整数类型 |
132| int32_t  | uint32_t  | 宽度恰为32的有/无符号整数类型 |
133| int64_t  | uint64_t  | 宽度恰为64的有/无符号整数类型 |
134| intptr_t | uintptr_t | 足以保存指针的有/无符号整数类型 |
135
136
137## <a name="c2-5"></a> 变量命名
138通用变量命名采用小驼峰,包括全局变量,函数形参,局部变量,成员变量。
139```cpp
140std::string tableName;  // Good: 推荐此风格
141std::string tablename;  // Bad: 禁止此风格
142std::string path;       // Good: 只有一个单词时,小驼峰为全小写
143```
144
145### <a name="r2-5-1"></a>规则2.5.1 全局变量应增加 'g_' 前缀,静态变量命名不需要加特殊前缀
146全局变量是应当尽量少使用的,使用时应特别注意,所以加上前缀用于视觉上的突出,促使开发人员对这些变量的使用更加小心。
147- 全局静态变量命名与全局变量相同。
148- 函数内的静态变量命名与普通局部变量相同。
149- 类的静态成员变量和普通成员变量相同。
150
151```cpp
152int g_activeConnectCount;
153
154void Func()
155{
156    static int packetCount = 0;
157    ...
158}
159```
160
161### <a name="r2-5-2"></a>规则2.5.2 类的成员变量命名以小驼峰加后下划线组成
162
163```cpp
164class Foo {
165private:
166    std::string fileName_;   // 添加_后缀,类似于K&R命名风格
167};
168```
169对于struct/union的成员变量,仍采用小驼峰不加后缀的命名方式,与局部变量命名风格一致。
170
171## <a name="c2-6"></a> 宏、常量、枚举命名
172宏、枚举值采用全大写,下划线连接的格式。
173全局作用域内,有名和匿名namespace内的 const 常量,类的静态成员常量,全大写,下划线连接;函数局部 const 常量和类的普通const成员变量,使用小驼峰命名风格。
174
175```cpp
176#define MAX(a, b)   (((a) < (b)) ? (b) : (a)) // 仅对宏命名举例,并不推荐用宏实现此类功能
177
178enum TintColor {    // 注意,枚举类型名用大驼峰,其下面的取值是全大写,下划线相连
179    RED,
180    DARK_RED,
181    GREEN,
182    LIGHT_GREEN
183};
184
185int Func(...)
186{
187    const unsigned int bufferSize = 100;    // 函数局部常量
188    char *p = new char[bufferSize];
189    ...
190}
191
192namespace Utils {
193	const unsigned int DEFAULT_FILE_SIZE_KB = 200;        // 全局常量
194}
195
196```
197
198# <a name="c3"></a>3 格式
199
200## <a name="c3-1"></a>行宽
201
202### <a name="a3-1-1"></a>建议3.1.1 行宽不超过 120 个字符
203建议每行字符数不要超过 120 个。如果超过120个字符,请选择合理的方式进行换行。
204
205例外:
206- 如果一行注释包含了超过120 个字符的命令或URL,则可以保持一行,以方便复制、粘贴和通过grep查找;
207- 包含长路径的 #include 语句可以超出120 个字符,但是也需要尽量避免;
208- 编译预处理中的error信息可以超出一行。
209预处理的 error 信息在一行便于阅读和理解,即使超过 120 个字符。
210```cpp
211#ifndef XXX_YYY_ZZZ
212#error Header aaaa/bbbb/cccc/abc.h must only be included after xxxx/yyyy/zzzz/xyz.h, because xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
213#endif
214```
215
216## <a name="c3-2"></a>缩进
217
218### <a name="r3-2-1"></a>规则3.2.1 使用空格进行缩进,每次缩进4个空格
219只允许使用空格(space)进行缩进,每次缩进为 4 个空格。不允许使用Tab符进行缩进。
220当前几乎所有的集成开发环境(IDE)都支持配置将Tab符自动扩展为4空格输入;请配置你的IDE支持使用空格进行缩进。
221
222## <a name="c3-3"></a>大括号
223### <a name="r3-3-1"></a>规则3.3.1 使用 K&R 缩进风格
224__K&R风格__
225换行时,函数(不包括lambda表达式)左大括号另起一行放行首,并独占一行;其他左大括号跟随语句放行末。
226右大括号独占一行,除非后面跟着同一语句的剩余部分,如 do 语句中的 while,或者 if 语句的 else/else if,或者逗号、分号。
227
228如:
229```cpp
230struct MyType {     // 跟随语句放行末,前置1空格
231    ...
232};
233
234int Foo(int a)
235{                   // 函数左大括号独占一行,放行首
236    if (...) {
237        ...
238    } else {
239        ...
240    }
241}
242```
243推荐这种风格的理由:
244
245- 代码更紧凑;
246- 相比另起一行,放行末使代码阅读节奏感上更连续;
247- 符合后来语言的习惯,符合业界主流习惯;
248- 现代集成开发环境(IDE)都具有代码缩进对齐显示的辅助功能,大括号放在行尾并不会对缩进和范围产生理解上的影响。
249
250
251对于空函数体,可以将大括号放在同一行:
252```cpp
253class MyClass {
254public:
255    MyClass() : value_(0) {}
256
257private:
258    int value_;
259};
260```
261
262## <a name="c3-4"></a> 函数声明和定义
263
264### <a name="r3-4-1"></a>规则3.4.1 函数声明和定义的返回类型和函数名在同一行;函数参数列表超出行宽时要换行并合理对齐
265在声明和定义函数的时候,函数的返回值类型应该和函数名在同一行;如果行宽度允许,函数参数也应该放在一行;否则,函数参数应该换行,并进行合理对齐。
266参数列表的左圆括号总是和函数名在同一行,不要单独一行;右圆括号总是跟随最后一个参数。
267
268换行举例:
269```cpp
270ReturnType FunctionName(ArgType paramName1, ArgType paramName2)   // Good:全在同一行
271{
272    ...
273}
274
275ReturnType VeryVeryVeryLongFunctionName(ArgType paramName1,     // 行宽不满足所有参数,进行换行
276                                        ArgType paramName2,     // Good:和上一行参数对齐
277                                        ArgType paramName3)
278{
279    ...
280}
281
282ReturnType LongFunctionName(ArgType paramName1, ArgType paramName2, // 行宽限制,进行换行
283    ArgType paramName3, ArgType paramName4, ArgType paramName5)     // Good: 换行后 4 空格缩进
284{
285    ...
286}
287
288ReturnType ReallyReallyReallyReallyLongFunctionName(            // 行宽不满足第1个参数,直接换行
289    ArgType paramName1, ArgType paramName2, ArgType paramName3) // Good: 换行后 4 空格缩进
290{
291    ...
292}
293```
294
295## <a name="c3-5"></a>函数调用
296### <a name="r3-5-1"></a>规则3.5.1 函数调用入参列表应放在一行,超出行宽换行时,保持参数进行合理对齐
297函数调用时,函数参数列表放在一行。参数列表如果超过行宽,需要换行并进行合理的参数对齐。
298左圆括号总是跟函数名,右圆括号总是跟最后一个参数。
299
300换行举例:
301```cpp
302ReturnType result = FunctionName(paramName1, paramName2);   // Good:函数参数放在一行
303
304ReturnType result = FunctionName(paramName1,
305                                 paramName2,                // Good:保持与上方参数对齐
306                                 paramName3);
307
308ReturnType result = FunctionName(paramName1, paramName2,
309    paramName3, paramName4, paramName5);                    // Good:参数换行,4 空格缩进
310
311ReturnType result = VeryVeryVeryLongFunctionName(           // 行宽不满足第1个参数,直接换行
312    paramName1, paramName2, paramName3);                    // 换行后,4 空格缩进
313```
314
315如果函数调用的参数存在内在关联性,按照可理解性优先于格式排版要求,对参数进行合理分组换行。
316```cpp
317// Good:每行的参数代表一组相关性较强的数据结构,放在一行便于理解
318int result = DealWithStructureLikeParams(left.x, left.y,     // 表示一组相关参数
319                                         right.x, right.y);  // 表示另外一组相关参数
320```
321
322## <a name="c3-6"></a> if语句
323
324### <a name="r3-6-1"></a>规则3.6.1 if语句必须要使用大括号
325我们要求if语句都需要使用大括号,即便只有一条语句。
326
327理由:
328- 代码逻辑直观,易读;
329- 在已有条件语句代码上增加新代码时不容易出错;
330- 对于在if语句中使用函数式宏时,有大括号保护不易出错(如果宏定义时遗漏了大括号)。
331
332```cpp
333if (objectIsNotExist) {         // Good:单行条件语句也加大括号
334    return CreateNewObject();
335}
336```
337### <a name="r3-6-2"></a>规则3.6.2 禁止 if/else/else if 写在同一行
338条件语句中,若有多个分支,应该写在不同行。
339
340如下是正确的写法:
341
342```cpp
343if (someConditions) {
344    DoSomething();
345    ...
346} else {  // Good: else 与 if 在不同行
347    ...
348}
349```
350
351下面是不符合规范的案例:
352
353```cpp
354if (someConditions) { ... } else { ... } // Bad: else 与 if 在同一行
355```
356
357## <a name="c3-7"></a> 循环语句
358### <a name="r3-7-1"></a>规则3.7.1 循环语句必须使用大括号
359和条件表达式类似,我们要求for/while循环语句必须加上大括号,即便循环体是空的,或循环语句只有一条。
360```cpp
361for (int i = 0; i < someRange; i++) {   // Good: 使用了大括号
362    DoSomething();
363}
364```
365```cpp
366while (condition) { }   // Good:循环体是空,使用大括号
367```
368```cpp
369while (condition) {
370    continue;           // Good:continue 表示空逻辑,使用大括号
371}
372```
373
374坏的例子:
375```cpp
376for (int i = 0; i < someRange; i++)
377    DoSomething();      // Bad: 应该加上括号
378```
379```cpp
380while (condition);      // Bad:使用分号容易让人误解是while语句中的一部分
381```
382
383## <a name="c3-8"></a> switch语句
384### <a name="r3-8-1"></a>规则3.8.1 switch 语句的 case/default 要缩进一层
385switch 语句的缩进风格如下:
386```cpp
387switch (var) {
388    case 0:             // Good: 缩进
389        DoSomething1(); // Good: 缩进
390        break;
391    case 1: {           // Good: 带大括号格式
392        DoSomething2();
393        break;
394    }
395    default:
396        break;
397}
398```
399
400```cpp
401switch (var) {
402case 0:                 // Bad: case 未缩进
403    DoSomething();
404    break;
405default:                // Bad: default 未缩进
406    break;
407}
408```
409
410## <a name="c3-9"></a> 表达式
411
412### <a name="a3-9-1"></a>建议3.9.1 表达式换行要保持换行的一致性,运算符放行末
413较长的表达式,不满足行宽要求的时候,需要在适当的地方换行。一般在较低优先级运算符或连接符后面截断,运算符或连接符放在行末。
414运算符、连接符放在行末,表示“未结束,后续还有”。
415例:
416
417// 假设下面第一行已经不满足行宽要求
418```cpp
419if ((currentValue > threshold) &&  // Good:换行后,逻辑操作符放在行尾
420    someConditionsion) {
421    DoSomething();
422    ...
423}
424
425int result = reallyReallyLongVariableName1 +    // Good
426             reallyReallyLongVariableName2;
427```
428表达式换行后,注意保持合理对齐,或者4空格缩进。参考下面例子
429
430```cpp
431int sum = longVaribleName1 + longVaribleName2 + longVaribleName3 +
432    longVaribleName4 + longVaribleName5 + longVaribleName6;         // Good: 4空格缩进
433
434int sum = longVaribleName1 + longVaribleName2 + longVaribleName3 +
435          longVaribleName4 + longVaribleName5 + longVaribleName6;   // Good: 保持对齐
436```
437## <a name="c3-10"></a> 变量赋值
438
439### <a name="r3-10-1"></a>规则3.10.1 多个变量定义和赋值语句不允许写在一行
440每行只有一个变量初始化的语句,更容易阅读和理解。
441
442```cpp
443int maxCount = 10;
444bool isCompleted = false;
445```
446
447下面是不符合规范的示例:
448
449```cpp
450int maxCount = 10; bool isCompleted = false; // Bad:多个变量初始化需要分开放在多行,每行一个变量初始化
451int x, y = 0;  // Bad:多个变量定义需要分行,每行一个
452
453int pointX;
454int pointY;
455...
456pointX = 1; pointY = 2;  // Bad:多个变量赋值语句放同一行
457```
458例外:for 循环头、if 初始化语句(C++17)、结构化绑定语句(C++17)中可以声明和初始化多个变量。这些语句中的多个变量声明有较强关联,如果强行分成多行会带来作用域不一致,声明和初始化割裂等问题。
459
460## <a name="c3-11"></a> 初始化
461初始化包括结构体、联合体、及数组的初始化
462
463### <a name="r3-11-1"></a>规则3.11.1 初始化换行时要有缩进,并进行合理对齐
464结构体或数组初始化时,如果换行应保持4空格缩进。
465从可读性角度出发,选择换行点和对齐位置。
466
467```cpp
468const int rank[] = {
469    16, 16, 16, 16, 32, 32, 32, 32,
470    64, 64, 64, 64, 32, 32, 32, 32
471};
472```
473
474## <a name="c3-12"></a> 指针与引用
475### <a name="a3-12-1"></a>建议3.12.1  指针类型"`*`"跟随变量名或者类型,不要两边都留有或者都没有空格
476指针命名: `*`靠左靠右都可以,但是不要两边都有或者都没有空格。
477```cpp
478int* p = nullptr;  // Good
479int *p = nullptr;  // Good
480
481int*p = nullptr;   // Bad
482int * p = nullptr; // Bad
483```
484
485例外:当变量被 const 修饰时,"`*`" 无法跟随变量,此时也不要跟随类型。
486```cpp
487const char * const VERSION = "V100";
488```
489
490### <a name="a3-12-2"></a>建议3.12.2  引用类型"`&`"跟随变量名或者类型,不要两边都留有或者都没有空格
491引用命名:`&`靠左靠右都可以,但是不要两边都有或者都没有空格。
492```cpp
493int i = 8;
494
495int& p = i;     // Good
496int &p = i;     // Good
497int*& rp = pi;  // Good,指针的引用,*& 一起跟随类型
498int *&rp = pi;  // Good,指针的引用,*& 一起跟随变量名
499int* &rp = pi;  // Good,指针的引用,* 跟随类型,& 跟随变量名
500
501int & p = i;    // Bad
502int&p = i;      // Bad
503```
504
505## <a name="c3-13"></a> 编译预处理
506### <a name="r3-13-1"></a>规则3.13.1 编译预处理的"#"统一放在行首,嵌套编译预处理语句时,"#"可以进行缩进
507编译预处理的"#"统一放在行首,即使编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。
508
509
510## <a name="c3-14"></a> 空格和空行
511### <a name="r3-14-1"></a>规则3.14.1 水平空格应该突出关键字和重要信息,避免不必要的留白
512水平空格应该突出关键字和重要信息,每行代码尾部不要加空格。总体规则如下:
513
514- if, switch, case, do, while, for等关键字之后加空格;
515- 小括号内部的两侧,不要加空格;
516- 大括号内部两侧有无空格,左右必须保持一致;
517- 一元操作符(& * + ‐ ~ !)之后不要加空格;
518- 二元操作符(= + ‐ < > * / % | & ^ <= >= == != )左右两侧加空格
519- 三目运算符(? :)符号两侧均需要空格
520- 前置和后置的自增、自减(++ --)和变量之间不加空格
521- 结构体成员操作符(. ->)前后不加空格
522- 逗号(,)前面不加空格,后面增加空格
523- 对于模板和类型转换(<>)和类型之间不要添加空格
524- 域操作符(::)前后不要添加空格
525- 冒号(:)前后根据情况来判断是否要添加空格
526
527常规情况:
528```cpp
529void Foo(int b) {  // Good:大括号前应该留空格
530
531int i = 0;  // Good:变量初始化时,=前后应该有空格,分号前面不要留空格
532
533int buf[BUF_SIZE] = {0};    // Good:大括号内两侧都无空格
534```
535
536函数定义和函数调用:
537```cpp
538int result = Foo(arg1,arg2);
539                    ^    // Bad: 逗号后面需要增加空格
540
541int result = Foo( arg1, arg2 );
542                 ^          ^  // Bad: 函数参数列表的左括号后面不应该有空格,右括号前面不应该有空格
543```
544
545指针和取地址
546```cpp
547x = *p;     // Good:*操作符和指针p之间不加空格
548p = &x;     // Good:&操作符和变量x之间不加空格
549x = r.y;    // Good:通过.访问成员变量时不加空格
550x = r->y;   // Good:通过->访问成员变量时不加空格
551```
552
553操作符:
554```cpp
555x = 0;   // Good:赋值操作的=前后都要加空格
556x = -5;  // Good:负数的符号和数值之前不要加空格
557++x;     // Good:前置和后置的++/--和变量之间不要加空格
558x--;
559
560if (x && !y)  // Good:布尔操作符前后要加上空格,!操作和变量之间不要空格
561v = w * x + y / z;  // Good:二元操作符前后要加空格
562v = w * (x + z);    // Good:括号内的表达式前后不需要加空格
563
564int a = (x < y) ? x : y;  // Good: 三目运算符, ?和:前后需要添加空格
565```
566
567循环和条件语句:
568```cpp
569if (condition) {  // Good:if关键字和括号之间加空格,括号内条件语句前后不加空格
570    ...
571} else {           // Good:else关键字和大括号之间加空格
572    ...
573}
574
575while (condition) {}   // Good:while关键字和括号之间加空格,括号内条件语句前后不加空格
576
577for (int i = 0; i < someRange; ++i) {  // Good:for关键字和括号之间加空格,分号之后加空格
578    ...
579}
580
581switch (condition) {  // Good: switch 关键字后面有1空格
582    case 0:     // Good:case语句条件和冒号之间不加空格
583        ...
584        break;
585    ...
586    default:
587        ...
588        break;
589}
590```
591
592模板和转换
593```cpp
594// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
595vector<string> x;
596y = static_cast<char*>(x);
597
598// 在类型与指针操作符之间留空格也可以, 但要保持一致.
599vector<char *> x;
600```
601
602域操作符
603```cpp
604std::cout;    // Good: 命名空间访问,不要留空格
605
606int MyClass::GetValue() const {}  // Good: 对于成员函数定义,不要留空格
607```
608
609冒号
610```cpp
611// 添加空格的场景
612
613// Good: 类的派生需要留有空格
614class Sub : public Base {
615
616};
617
618// 构造函数初始化列表需要留有空格
619MyClass::MyClass(int var) : someVar_(var)
620{
621    DoSomething();
622}
623
624// 位域表示也留有空格
625struct XX {
626    char a : 4;
627    char b : 5;
628    char c : 4;
629};
630```
631
632```cpp
633// 不添加空格的场景
634
635// Good: 对于public:, private:这种类访问权限的冒号不用添加空格
636class MyClass {
637public:
638    MyClass(int var);
639private:
640    int someVar_;
641};
642
643// 对于switch-case的case和default后面的冒号不用添加空格
644switch (value)
645{
646    case 1:
647        DoSomething();
648        break;
649    default:
650        break;
651}
652```
653
654注意:当前的集成开发环境(IDE)可以设置删除行尾的空格,请正确配置。
655
656### <a name="a3-14-1"></a>建议3.14.1 合理安排空行,保持代码紧凑
657
658减少不必要的空行,可以显示更多的代码,方便代码阅读。下面有一些建议遵守的规则:
659- 根据上下内容的相关程度,合理安排空行;
660- 函数内部、类型定义内部、宏内部、初始化表达式内部,不使用连续空行
661- 不使用连续 **3** 个空行,或更多
662- 大括号内的代码块行首之前和行尾之后不要加空行,但namespace的大括号内不作要求。
663
664```cpp
665int Foo()
666{
667    ...
668}
669
670
671
672int Bar()  // Bad:最多使用连续2个空行。
673{
674    ...
675}
676
677
678if (...) {
679        // Bad:大括号内的代码块行首不要加入空行
680    ...
681        // Bad:大括号内的代码块行尾不要加入空行
682}
683
684int Foo(...)
685{
686        // Bad:函数体内行首不要加空行
687    ...
688}
689```
690
691## <a name="c3-15"></a> 类
692### <a name="r3-15-1"></a>规则3.15.1 类访问控制块的声明依次序是 public:, protected:, private:,缩进和 class 关键字对齐
693```cpp
694class MyClass : public BaseClass {
695public:      // 注意没有缩进
696    MyClass();  // 标准的4空格缩进
697    explicit MyClass(int var);
698    ~MyClass() {}
699
700    void SomeFunction();
701    void SomeFunctionThatDoesNothing()
702    {
703    }
704
705    void SetVar(int var) { someVar_ = var; }
706    int GetVar() const { return someVar_; }
707
708private:
709    bool SomeInternalFunction();
710
711    int someVar_;
712    int someOtherVar_;
713};
714```
715
716在各个部分中,建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它成员函数, 数据成员。
717
718
719### <a name="r3-15-2"></a>规则3.15.2 构造函数初始化列表放在同一行或按四格缩进并排多行
720```cpp
721// 如果所有变量能放在同一行:
722MyClass::MyClass(int var) : someVar_(var)
723{
724    DoSomething();
725}
726
727// 如果不能放在同一行,
728// 必须置于冒号后, 并缩进4个空格
729MyClass::MyClass(int var)
730    : someVar_(var), someOtherVar_(var + 1)  // Good: 逗号后面留有空格
731{
732    DoSomething();
733}
734
735// 如果初始化列表需要置于多行, 需要逐行对齐
736MyClass::MyClass(int var)
737    : someVar_(var),             // 缩进4个空格
738      someOtherVar_(var + 1)
739{
740    DoSomething();
741}
742```
743
744# <a name="c4"></a>4 注释
745一般的,尽量通过清晰的架构逻辑,好的符号命名来提高代码可读性;需要的时候,才辅以注释说明。
746注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,**按需注释**。
747
748注释内容要简洁、明了、无二义性,信息全面且不冗余。
749
750**注释跟代码一样重要。**
751写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。
752修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。
753
754使用英文进行注释。
755
756## <a name="c3-1"></a>注释风格
757
758在 C++ 代码中,使用 `/*` `*/`和 `//` 都是可以的。
759按注释的目的和位置,注释可分为不同的类型,如文件头注释、函数头注释、代码注释等等;
760同一类型的注释应该保持统一的风格。
761
762注意:本文示例代码中,大量使用 '//' 后置注释只是为了更精确的描述问题,并不代表这种注释风格更好。
763
764## <a name="c4-2"></a>  文件头注释
765### <a name="r3-1"></a>规则3.1 文件头注释必须包含版权许可
766
767/*
768 * Copyright (c) 2020 XXX
769 * Licensed under the Apache License, Version 2.0 (the "License");
770 * you may not use this file except in compliance with the License.
771 * You may obtain a copy of the License at
772 *
773 *     http://www.apache.org/licenses/LICENSE-2.0
774 *
775 * Unless required by applicable law or agreed to in writing, software
776 * distributed under the License is distributed on an "AS IS" BASIS,
777 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
778 * See the License for the specific language governing permissions and
779 * limitations under the License.
780 */
781
782## <a name="c4-3"></a> 函数头注释
783### <a name="r4-3-1"></a>规则4.3.1 禁止空有格式的函数头注释
784并不是所有的函数都需要函数头注释;
785函数签名无法表达的信息,加函数头注释辅助说明;
786
787函数头注释统一放在函数声明或定义上方,使用如下风格之一:
788使用`//`写函数头
789
790```cpp
791// 单行函数头
792int Func1(void);
793
794// 多行函数头
795// 第二行
796int Func2(void);
797```
798
799使用`/*   */`写函数头
800```cpp
801/* 单行函数头 */
802int Func1(void);
803
804/*
805 * 另一种单行函数头
806 */
807int Func2(void);
808
809/*
810 * 多行函数头
811 * 第二行
812 */
813int Func3(void);
814```
815函数尽量通过函数名自注释,按需写函数头注释。
816不要写无用、信息冗余的函数头;不要写空有格式的函数头。
817
818函数头注释内容可选,但不限于:功能说明、返回值,性能约束、用法、内存约定、算法实现、可重入的要求等等。
819模块对外头文件中的函数接口声明,其函数头注释,应当将重要、有用的信息表达清楚。
820
821例:
822
823```cpp
824/*
825 * 返回实际写入的字节数,-1表示写入失败
826 * 注意,内存 buf 由调用者负责释放
827 */
828int WriteString(const char *buf, int len);
829```
830
831坏的例子:
832```cpp
833/*
834 * 函数名:WriteString
835 * 功能:写入字符串
836 * 参数:
837 * 返回值:
838 */
839int WriteString(const char *buf, int len);
840```
841上面例子中的问题:
842
843- 参数、返回值,空有格式没内容
844- 函数名信息冗余
845- 关键的 buf 由谁释放没有说清楚
846
847## <a name="c4-4"></a> 代码注释
848### <a name="r4-4-1"></a>规则4.4.1 代码注释放于对应代码的上方或右边
849### <a name="r4-4-2"></a>规则4.4.2 注释符与注释内容间要有1空格;右置注释与前面代码至少1空格
850代码上方的注释,应该保持对应代码一样的缩进。
851选择并统一使用如下风格之一:
852使用`//`
853```cpp
854
855// 这是单行注释
856DoSomething();
857
858// 这是多行注释
859// 第二行
860DoSomething();
861```
862
863使用`/*' '*/`
864```cpp
865/* 这是单行注释 */
866DoSomething();
867
868/*
869 * 另一种方式的多行注释
870 * 第二行
871 */
872DoSomething();
873```
874代码右边的注释,与代码之间,至少留1空格,建议不超过4空格。
875通常使用扩展后的 TAB 键即可实现 1-4 空格的缩进。
876
877选择并统一使用如下风格之一:
878
879```cpp
880int foo = 100;  // 放右边的注释
881int bar = 200;  /* 放右边的注释 */
882```
883右置格式在适当的时候,上下对齐会更美观。
884对齐后的注释,离左边代码最近的那一行,保证1-4空格的间隔。
885例:
886
887```cpp
888const int A_CONST = 100;         /* 相关的同类注释,可以考虑上下对齐 */
889const int ANOTHER_CONST = 200;   /* 上下对齐时,与左侧代码保持间隔 */
890```
891当右置的注释超过行宽时,请考虑将注释置于代码上方。
892
893### <a name="r4-4-3"></a>规则4.4.3 不用的代码段直接删除,不要注释掉
894被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入易被忽略的缺陷。
895正确的做法是,不需要的代码直接删除掉。若再需要时,考虑移植或重写这段代码。
896
897这里说的注释掉代码,包括用 /* */ 和 //,还包括 #if 0, #ifdef NEVER_DEFINED 等等。
898
899# <a name="c5"></a>5 头文件
900## <a name="c5-1"></a> 头文件职责
901头文件是模块或文件的对外接口,头文件的设计体现了大部分的系统设计。
902头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。对于cpp文件中内部才需要使用的函数、宏、枚举、结构定义等不要放在头文件中。
903头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。
904
905### <a name="a5-1-1"></a>建议5.1.1 每一个.cpp文件应有一个对应的.h文件,用于声明需要对外公开的类与接口
906通常情况下,每个.cpp文件都有一个相应的.h,用于放置对外提供的函数声明、宏定义、类型定义等。
907如果一个.cpp文件不需要对外公布任何接口,则其就不应当存在。
908例外:__程序的入口(如main函数所在的文件),单元测试代码,动态库代码。__
909
910示例:
911```cpp
912// Foo.h
913
914#ifndef FOO_H
915#define FOO_H
916
917class Foo {
918public:
919    Foo();
920    void Fun();
921
922private:
923    int value_;
924};
925
926#endif
927```
928
929```cpp
930// Foo.cpp
931#include "Foo.h"
932
933namespace { // Good: 对内函数的声明放在.cpp文件的头部,并声明为匿名namespace或者static限制其作用域
934    void Bar()
935    {
936    }
937}
938
939...
940
941void Foo::Fun()
942{
943    Bar();
944}
945```
946
947## <a name="c5-2"></a> 头文件依赖
948### <a name="r5-2-1"></a>规则5.2.1 禁止头文件循环依赖
949头文件循环依赖,指 a.h 包含 b.hb.h 包含 c.hc.h 包含 a.h, 导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。
950而如果是单向依赖,如a.h包含b.hb.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。
951
952头文件循环依赖直接体现了架构设计上的不合理,可通过优化架构去避免。
953
954
955### <a name="r5-2-4"></a>规则5.2.2 头文件必须编写`#define`保护,防止重复包含
956为防止头文件被重复包含,所有头文件都应当使用 #define 保护;不要使用 #pragma once
957
958定义包含保护符时,应该遵守如下规则:
9591)保护符使用唯一名称;
9602)不要在受保护部分的前后放置代码或者注释,文件头注释除外。
961
962示例:假定timer模块的timer.h,其目录为timer/include/timer.h,应按如下方式保护:
963
964```cpp
965#ifndef TIMER_INCLUDE_TIMER_H
966#define TIMER_INCLUDE_TIMER_H
967...
968#endif
969```
970
971### <a name="r5-2-5"></a>规则5.2.3 禁止通过声明的方式引用外部函数接口、变量
972只能通过包含头文件的方式使用其他模块或文件提供的接口。
973通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。
974同时这种隐式依赖,容易导致架构腐化。
975
976不符合规范的案例:
977
978// a.cpp内容
979```cpp
980extern int Fun();   // Bad: 通过extern的方式使用外部函数
981
982void Bar()
983{
984    int i = Fun();
985    ...
986}
987```
988
989// b.cpp内容
990```cpp
991int Fun()
992{
993    // Do something
994}
995```
996应该改为:
997
998// a.cpp内容
999```cpp
1000#include "b.h"   // Good: 通过包含头文件的方式使用其他.cpp提供的接口
1001
1002void Bar()
1003{
1004    int i = Fun();
1005    ...
1006}
1007```
1008
1009// b.h内容
1010```cpp
1011int Fun();
1012```
1013
1014// b.cpp内容
1015```cpp
1016int Fun()
1017{
1018    // Do something
1019}
1020```
1021例外,有些场景需要引用其内部函数,但并不想侵入代码时,可以 extern 声明方式引用。
1022如:
1023针对某一内部函数进行单元测试时,可以通过 extern 声明来引用被测函数;
1024当需要对某一函数进行打桩、打补丁处理时,允许 extern 声明该函数。
1025
1026### <a name="r5-2-6"></a>规则5.2.4 禁止在extern "C"中包含头文件
1027在 extern "C" 中包含头文件,有可能会导致 extern "C" 嵌套,部分编译器对 extern "C" 嵌套层次有限制,嵌套层次太多会编译错误。
1028
1029在C,C++混合编程的情况下,在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改。
1030
1031示例,存在a.hb.h两个头文件:
1032
1033// a.h内容
1034```cpp
1035...
1036#ifdef __cplusplus
1037void Foo(int);
1038#define A(value) Foo(value)
1039#else
1040void A(int)
1041#endif
1042```
1043// b.h内容
1044```cpp
1045...
1046#ifdef __cplusplus
1047extern "C" {
1048#endif
1049
1050#include "a.h"
1051void B();
1052
1053#ifdef __cplusplus
1054}
1055#endif
1056```
1057
1058使用C++预处理器展开b.h,将会得到
1059```cpp
1060extern "C" {
1061    void Foo(int);
1062    void B();
1063}
1064```
1065
1066按照 a.h 作者的本意,函数 Foo 是一个 C++ 自由函数,其链接规范为 "C++"。
1067但在 b.h 中,由于 `#include "a.h"` 被放到了 `extern "C"` 的内部,函数 Foo 的链接规范被不正确地更改了。
1068
1069例外:
1070如果在 C++ 编译环境中,想引用纯C的头文件,这些C头文件并没有` extern "C"` 修饰。非侵入式的做法是,在 `extern "C"` 中去包含C头文件。
1071
1072### <a name="a5-2-1"></a>建议5.2.1尽量避免使用前置声明,而是通过`#include`来包含头文件
1073前置声明(forward declaration)通常指类、模板的纯粹声明,没伴随着其定义。
1074
1075- 优点:
1076  1. 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
1077  2. 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
1078- 缺点:
1079  1. 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
1080  2. 前置声明可能会被库的后续更改所破坏。前置声明模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
1081  3. 前置声明来自命名空间` std::` 的 symbol 时,其行为未定义(在C++11标准规范中明确说明)。
1082  4. 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。
1083  5. 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。
1084  6. 很难判断什么时候该用前置声明,什么时候该用`#include`,某些场景下面前置声明和`#include`互换以后会导致意想不到的结果。
1085
1086所以我们尽可能避免使用前置声明,而是使用#include头文件来保证依赖关系。
1087
1088# <a name="c6"></a>6 作用域
1089
1090## <a name="c6-1"></a> 命名空间
1091
1092### <a name="a6-1-1"></a>建议6.1.1 对于cpp文件中不需要导出的变量,常量或者函数,请使用匿名namespace封装或者用static修饰
1093在C++ 2003标准规范中,使用static修饰文件作用域的变量,函数等被标记为deprecated特性,所以更推荐使用匿名namespace。
1094
1095主要原因如下:
10961. static在C++中已经赋予了太多的含义,静态函数成员变量,静态成员函数,静态全局变量,静态函数局部变量,每一种都有特殊的处理。
10972. static只能保证变量,常量和函数的文件作用域,但是namespace还可以封装类型等。
10983. 统一namespace来处理C++的作用域,而不需要同时使用static和namespace来管理。
10994. static修饰的函数不能用来实例化模板,而匿名namespace可以。
1100
1101但是不要在 .h 中使用中使用匿名namespace或者static。
1102
1103```cpp
1104// Foo.cpp
1105
1106namespace {
1107    const int MAX_COUNT = 20;
1108    void InternalFun() {};
1109}
1110
1111void Foo::Fun()
1112{
1113    int i = MAX_COUNT;
1114
1115    InternalFun();
1116}
1117
1118```
1119
1120### <a name="r6-1-1"></a>规则6.1.1 不要在头文件中或者#include之前使用using导入命名空间
1121说明:使用using导入命名空间会影响后续代码,易造成符号冲突,所以不要在头文件以及源文件中的#include之前使用using导入命名空间。
1122示例:
1123
1124```cpp
1125// 头文件a.h
1126namespace NamespaceA {
1127    int Fun(int);
1128}
1129```
1130
1131```cpp
1132// 头文件b.h
1133namespace NamespaceB {
1134    int Fun(int);
1135}
1136
1137using namespace NamespaceB;
1138
1139void G()
1140{
1141    Fun(1);
1142}
1143```
1144
1145```cpp
1146// 源代码a.cpp
1147#include "a.h"
1148using namespace NamespaceA;
1149#include "b.h"
1150
1151void main()
1152{
1153    G(); // using namespace NamespaceA在#include “b.h”之前,引发歧义:NamespaceA::Fun,NamespaceB::Fun调用不明确
1154}
1155```
1156
1157对于在头文件中使用using导入单个符号或定义别名,允许在模块自定义名字空间中使用,但禁止在全局名字空间中使用。
1158```cpp
1159// foo.h
1160
1161#include <fancy/string>
1162using fancy::string;  // Bad,禁止向全局名字空间导入符号
1163
1164namespace Foo {
1165    using fancy::string;  // Good,可以在模块自定义名字空间中导入符号
1166    using MyVector = fancy::vector<int>;  // Good,C++11可在自定义名字空间中定义别名
1167}
1168```
1169
1170
1171## <a name="c6-2"></a> 全局函数和静态成员函数
1172
1173### <a name="a6-2-1"></a>建议6.2.1 优先使用命名空间来管理全局函数,如果和某个class有直接关系的,可以使用静态成员函数
1174说明:非成员函数放在名字空间内可避免污染全局作用域, 也不要用类+静态成员方法来简单管理全局函数。 如果某个全局函数和某个类有紧密联系, 那么可以作为类的静态成员函数。
1175
1176如果你需要定义一些全局函数,给某个cpp文件使用,那么请使用匿名namespace来管理。
1177```cpp
1178namespace MyNamespace {
1179    int Add(int a, int b);
1180}
1181
1182class File {
1183public:
1184    static File CreateTempFile(const std::string& fileName);
1185};
1186```
1187
1188## <a name="c6-3"></a> 全局常量和静态成员常量
1189
1190### <a name="a6-3-1"></a>建议6.3.1 优先使用命名空间来管理全局常量,如果和某个class有直接关系的,可以使用静态成员常量
1191说明:全局常量放在命名空间内可避免污染全局作用域, 也不要用类+静态成员常量来简单管理全局常量。 如果某个全局常量和某个类有紧密联系, 那么可以作为类的静态成员常量。
1192
1193如果你需要定义一些全局常量,只给某个cpp文件使用,那么请使用匿名namespace来管理。
1194```cpp
1195namespace MyNamespace {
1196    const int MAX_SIZE = 100;
1197}
1198
1199class File {
1200public:
1201    static const std::string SEPARATOR;
1202};
1203```
1204
1205## <a name="c6-4"></a> 全局变量
1206
1207### <a name="a6-4-1"></a>建议6.4.1 尽量避免使用全局变量,考虑使用单例模式
1208说明:全局变量是可以修改和读取的,那么这样会导致业务代码和这个全局变量产生数据耦合。
1209```cpp
1210int g_counter = 0;
1211
1212// a.cpp
1213g_counter++;
1214
1215// b.cpp
1216g_counter++;
1217
1218// c.cpp
1219cout << g_counter << endl;
1220```
1221
1222使用单实例模式
1223```cpp
1224class Counter {
1225public:
1226    static Counter& GetInstance()
1227    {
1228        static Counter counter;
1229        return counter;
1230    }  // 单实例实现简单举例
1231
1232    void Increase()
1233    {
1234        value_++;
1235    }
1236
1237    void Print() const
1238    {
1239        std::cout << value_ << std::endl;
1240    }
1241
1242private:
1243    Counter() : value_(0) {}
1244
1245private:
1246    int value_;
1247};
1248
1249// a.cpp
1250Counter::GetInstance().Increase();
1251
1252// b.cpp
1253Counter::GetInstance().Increase();
1254
1255// c.cpp
1256Counter::GetInstance().Print();
1257```
1258
1259实现单例模式以后,实现了全局唯一一个实例,和全局变量同样的效果,并且单实例提供了更好的封装性。
1260
1261例外:有的时候全局变量的作用域仅仅是模块内部,这样进程空间里面就会有多个全局变量实例,每个模块持有一份,这种场景下是无法使用单例模式解决的。
1262
1263# <a name="c7"></a>7 类
1264
1265## <a name="c7-1"></a> 构造,拷贝构造,赋值和析构函数
1266构造,拷贝,移动和析构函数提供了对象的生命周期管理方法:
1267- 构造函数(constructor): `X()`
1268- 拷贝构造函数(copy constructor):`X(const X&)`
1269- 拷贝赋值操作符(copy assignment):`operator=(const X&)`
1270- 移动构造函数(move constructor):`X(X&&)`         *C++11以后提供*
1271- 移动赋值操作符(move assignment):`operator=(X&&)`       *C++11以后提供*
1272- 析构函数(destructor):`~X()`
1273
1274### <a name="r7-1-1"></a>规则7.1.1 类的成员变量必须显式初始化
1275说明:如果类有成员变量,没有定义构造函数,又没有定义默认构造函数,编译器将自动生成一个构造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。
1276
1277例外:
1278- 如果类的成员变量具有默认构造函数,那么可以不需要显式初始化。
1279
1280示例:如下代码没有构造函数,私有数据成员无法初始化:
1281```cpp
1282class Message {
1283public:
1284    void ProcessOutMsg()
1285    {
1286        //…
1287    }
1288
1289private:
1290    unsigned int msgID_;
1291    unsigned int msgLength_;
1292    unsigned char* msgBuffer_;
1293    std::string someIdentifier_;
1294};
1295
1296Message message;   // message成员变量没有初始化
1297message.ProcessOutMsg();   // 后续使用存在隐患
1298
1299// 因此,有必要定义默认构造函数,如下:
1300class Message {
1301public:
1302    Message() : msgID_(0), msgLength_(0), msgBuffer_(nullptr)
1303    {
1304    }
1305
1306    void ProcessOutMsg()
1307    {
1308        // …
1309    }
1310
1311private:
1312    unsigned int msgID_;
1313    unsigned int msgLength_;
1314    unsigned char* msgBuffer_;
1315    std::string someIdentifier_; // 具有默认构造函数,不需要显式初始化
1316};
1317```
1318
1319### <a name="a7-1-1"></a>建议7.1.1 成员变量优先使用声明时初始化(C++11)和构造函数初始化列表初始化
1320说明:C++11的声明时初始化可以一目了然的看出成员初始值,应当优先使用。如果成员初始化值和构造函数相关,或者不支持C++11,则应当优先使用构造函数初始化列表来初始化成员。相比起在构造函数体中对成员赋值,初始化列表的代码更简洁,执行性能更好,而且可以对const成员和引用成员初始化。
1321
1322```cpp
1323class Message {
1324public:
1325    Message() : msgLength_(0)  // Good,优先使用初始化列表
1326    {
1327        msgBuffer_ = nullptr;  // Bad,不推荐在构造函数中赋值
1328    }
1329
1330private:
1331    unsigned int msgID_{0};  // Good,C++11中使用
1332    unsigned int msgLength_;
1333    unsigned char* msgBuffer_;
1334};
1335```
1336
1337### <a name="r7-1-2"></a>规则7.1.2 为避免隐式转换,将单参数构造函数声明为explicit
1338说明:单参数构造函数如果没有用explicit声明,则会成为隐式转换函数。
1339示例:
1340
1341```cpp
1342class Foo {
1343public:
1344    explicit Foo(const string& name): name_(name)
1345    {
1346    }
1347private:
1348    string name_;
1349};
1350
1351
1352void ProcessFoo(const Foo& foo){}
1353
1354int main(void)
1355{
1356    std::string test = "test";
1357    ProcessFoo(test);  // 编译不通过
1358    return 0;
1359}
1360```
1361
1362上面的代码编译不通过,因为`ProcessFoo`需要的参数是Foo类型,传入的string类型不匹配。
1363
1364如果将Foo构造函数的explicit关键字移除,那么调用`ProcessFoo`传入的string就会触发隐式转换,生成一个临时的Foo对象。往往这种隐式转换是让人迷惑的,并且容易隐藏Bug,得到了一个不期望的类型转换。所以对于单参数的构造函数是要求explicit声明。
1365
1366### <a name="r7-1-3"></a>规则7.1.3 如果不需要拷贝构造函数、赋值操作符 / 移动构造函数、赋值操作符,请明确禁止
1367说明:如果用户不定义,编译器默认会生成拷贝构造函数和拷贝赋值操作符, 移动构造和移动赋值操作符(移动语义的函数C++11以后才有)。
1368如果我们不要使用拷贝构造函数,或者赋值操作符,请明确拒绝:
1369
13701. 将拷贝构造函数或者赋值操作符设置为private,并且不实现:
1371```cpp
1372class Foo {
1373private:
1374    Foo(const Foo&);
1375    Foo& operator=(const Foo&);
1376};
1377```
13782. 使用C++11提供的delete, 请参见后面现代C++的相关章节。
1379
1380### <a name="r7-1-4"></a>规则7.1.4 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止
1381拷贝构造函数和拷贝赋值操作符都是具有拷贝语义的,应该同时出现或者禁止。
1382
1383```cpp
1384// 同时出现
1385class Foo {
1386public:
1387    ...
1388    Foo(const Foo&);
1389    Foo& operator=(const Foo&);
1390    ...
1391};
1392
1393// 同时default, C++11支持
1394class Foo {
1395public:
1396    Foo(const Foo&) = default;
1397    Foo& operator=(const Foo&) = default;
1398};
1399
1400// 同时禁止, C++11可以使用delete
1401class Foo {
1402private:
1403    Foo(const Foo&);
1404    Foo& operator=(const Foo&);
1405};
1406```
1407
1408### <a name="r7-1-5"></a>规则7.1.5 移动构造和移动赋值操作符应该是成对出现或者禁止
1409在C++11中增加了move操作,如果需要某个类支持移动操作,那么需要实现移动构造和移动赋值操作符。
1410
1411移动构造函数和移动赋值操作符都是具有移动语义的,应该同时出现或者禁止。
1412```cpp
1413// 同时出现
1414class Foo {
1415public:
1416    ...
1417    Foo(Foo&&);
1418    Foo& operator=(Foo&&);
1419    ...
1420};
1421
1422// 同时default, C++11支持
1423class Foo {
1424public:
1425    Foo(Foo&&) = default;
1426    Foo& operator=(Foo&&) = default;
1427};
1428
1429// 同时禁止, 使用C++11的delete
1430class Foo {
1431public:
1432    Foo(Foo&&) = delete;
1433    Foo& operator=(Foo&&) = delete;
1434};
1435```
1436
1437### <a name="r7-1-6"></a>规则7.1.6 禁止在构造函数和析构函数中调用虚函数
1438说明:在构造函数和析构函数中调用当前对象的虚函数,会导致未实现多态的行为。
1439在C++中,一个基类一次只构造一个完整的对象。
1440
1441示例:类Base是基类,Sub是派生类
1442```cpp
1443class Base {
1444public:
1445    Base();
1446    virtual void Log() = 0;    // 不同的派生类调用不同的日志文件
1447};
1448
1449Base::Base()         // 基类构造函数
1450{
1451    Log();           // 调用虚函数Log
1452}
1453
1454class Sub : public Base {
1455public:
1456    virtual void Log();
1457};
1458```
1459
1460当执行如下语句:
1461`Sub sub;`
1462会先执行Sub的构造函数,但首先调用Base的构造函数,由于Base的构造函数调用虚函数Log,此时Log还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为。
1463同样的道理也适用于析构函数。
1464
1465
1466## <a name="c7-2"></a> 继承
1467
1468### <a name="r7-2-1"></a>规则7.2.1 基类的析构函数应该声明为virtual
1469说明:只有基类析构函数是virtual,通过多态调用的时候才能保证派生类的析构函数被调用。
1470
1471示例:基类的析构函数没有声明为virtual导致了内存泄漏。
1472```cpp
1473class Base {
1474public:
1475    virtual std::string getVersion() = 0;
1476
1477    ~Base()
1478    {
1479        std::cout << "~Base" << std::endl;
1480    }
1481};
1482```
1483
1484```cpp
1485class Sub : public Base {
1486public:
1487    Sub() : numbers_(nullptr)
1488    {
1489    }
1490
1491    ~Sub()
1492    {
1493        delete[] numbers_;
1494        std::cout << "~Sub" << std::endl;
1495    }
1496
1497    int Init()
1498    {
1499        const size_t numberCount = 100;
1500        numbers_ = new (std::nothrow) int[numberCount];
1501        if (numbers_ == nullptr) {
1502            return -1;
1503        }
1504
1505        ...
1506    }
1507
1508    std::string getVersion()
1509    {
1510        return std::string("hello!");
1511    }
1512private:
1513    int* numbers_;
1514};
1515```
1516
1517```cpp
1518int main(int argc, char* args[])
1519{
1520    Base* b = new Sub();
1521
1522    delete b;
1523    return 0;
1524}
1525```
1526由于基类Base的析构函数没有声明为virtual,当对象被销毁时,只会调用基类的析构函数,不会调用派生类Sub的析构函数,导致内存泄漏。
1527
1528
1529### <a name="r7-2-2"></a>规则7.2.2 禁止虚函数使用缺省参数值
1530说明:在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。为了避免虚函数重载时,因参数声明不一致给使用者带来的困惑和由此导致的问题,规定所有虚函数均不允许声明缺省参数值。
1531示例:虚函数display缺省参数值text是由编译时刻决定的,而非运行时刻,没有达到多态的目的:
1532```cpp
1533class Base {
1534public:
1535    virtual void Display(const std::string& text = "Base!")
1536    {
1537        std::cout << text << std::endl;
1538    }
1539
1540    virtual ~Base(){}
1541};
1542
1543class Sub : public Base {
1544public:
1545    virtual void Display(const std::string& text  = "Sub!")
1546    {
1547        std::cout << text << std::endl;
1548    }
1549
1550    virtual ~Sub(){}
1551};
1552
1553int main()
1554{
1555    Base* base = new Sub();
1556    Sub* sub = new Sub();
1557
1558    ...
1559
1560    base->Display();  // 程序输出结果: Base! 而期望输出:Sub!
1561    sub->Display();   // 程序输出结果: Sub!
1562
1563    delete base;
1564    delete sub;
1565    return 0;
1566};
1567```
1568
1569### <a name="r7-2-3"></a>规则7.2.3 禁止重新定义继承而来的非虚函数
1570说明:因为非虚函数无法实现动态绑定,只有虚函数才能实现动态绑定:只要操作基类的指针,即可获得正确的结果。
1571
1572示例:
1573```cpp
1574class Base {
1575public:
1576    void Fun();
1577};
1578
1579class Sub : public Base {
1580public:
1581    void Fun();
1582};
1583
1584Sub* sub = new Sub();
1585Base* base = sub;
1586
1587sub->Fun();    // 调用子类的Fun
1588base->Fun();   // 调用父类的Fun
1589//...
1590
1591```
1592
1593## <a name="c7-3"></a> 多重继承
1594在实际开发过程中使用多重继承的场景是比较少的,因为多重继承使用过程中有下面的典型问题:
15951. 菱形继承所带来的数据重复,以及名字二义性。因此,C++引入了virtual继承来解决这类问题;
15962. 即便不是菱形继承,多个父类之间的名字也可能存在冲突,从而导致的二义性;
15973. 如果子类需要扩展或改写多个父类的方法时,造成子类的职责不明,语义混乱;
15984. 相对于委托,继承是一种白盒复用,即子类可以访问父类的protected成员, 这会导致更强的耦合。而多重继承,由于耦合了多个父类,相对于单根继承,这会产生更强的耦合关系。
1599
1600多重继承具有下面的优点:
1601多重继承提供了一种更简单的组合来实现多种接口或者类的组装与复用。
1602
1603所以,对于多重继承的只有下面几种情况下面才允许使用多重继承。
1604
1605### <a name="a7-3-1"></a>建议7.3.1 使用多重继承来实现接口分离与多角色组合
1606如果某个类需要实现多重接口,可以通过多重继承把多个分离的接口组合起来,类似 scala 语言的 traits 混入。
1607
1608```cpp
1609class Role1 {};
1610class Role2 {};
1611class Role3 {};
1612
1613class Object1 : public Role1, public Role2 {
1614    // ...
1615};
1616
1617class Object2 : public Role2, public Role3 {
1618    // ...
1619};
1620
1621```
1622
1623在C++标准库中也有类似的实现样例:
1624```cpp
1625class basic_istream {};
1626class basic_ostream {};
1627
1628class basic_iostream : public basic_istream, public basic_ostream {
1629
1630};
1631```
1632
1633## <a name="c7-4"></a> 重载
1634
1635重载操作符要有充分理由,而且不要改变操作符原有语义,例如不要使用 ‘+’ 操作符来做减运算。
1636操作符重载令代码更加直观,但也有一些不足:
1637- 混淆直觉,误以为该操作和内建类型一样是高性能的,忽略了性能降低的可能;
1638- 问题定位时不够直观,按函数名查找比按操作符显然更方便。
1639- 重载操作符如果行为定义不直观(例如将‘+’ 操作符来做减运算),会让代码产生混淆。
1640- 赋值操作符的重载引入的隐式转换会隐藏很深的bug。可以定义类似Equals()、CopyFrom()等函数来替代=,==操作符。
1641
1642
1643
1644# <a name="c8"></a> 8 函数
1645## <a name="c8-1"></a>函数设计
1646### <a name="r8-1-1"></a>规则8.1.1 避免函数过长,函数不超过50行(非空非注释)
1647函数应该可以一屏显示完 (50行以内),只做一件事情,而且把它做好。
1648
1649过长的函数往往意味着函数功能不单一,过于复杂,或过分呈现细节,未进行进一步抽象。
1650
1651例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。
1652
1653即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的bug。
1654建议将其拆分为更加简短并易于管理的若干函数,以便于他人阅读和修改代码。
1655
1656## <a name="c8-2"></a>内联函数
1657
1658###  <a name="a8-2-1"></a>建议8.2.1 内联函数不超过10行(非空非注释)
1659**说明**:内联函数具有一般函数的特性,它与一般函数不同之处只在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。
1660
1661内联函数只适合于只有 1~10 行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,也没有必要用内联函数实现,一般的编译器会放弃内联方式,而采用普通的方式调用函数。
1662
1663如果内联函数包含复杂的控制结构,如循环、分支(switch)、try-catch 等语句,一般编译器将该函数视同普通函数。
1664**虚函数、递归函数不能被用来做内联函数**。
1665
1666## <a name="c8-3"></a> 函数参数
1667
1668### <a name="a8-3-1"></a>建议8.3.1 函数参数使用引用取代指针
1669
1670**说明**:引用比指针更安全,因为它一定非空,且一定不会再指向其他目标;引用不需要检查非法的NULL指针。
1671
1672如果是基于老平台开发的产品,则优先顺从原有平台的处理方式。
1673选择 const 避免参数被修改,让代码阅读者清晰地知道该参数不被修改,可大大增强代码可读性。
1674
1675例外:当传入参数为编译期长度未知的数组时,可以使用指针而不是引用。
1676
1677### <a name="a8-3-2"></a>建议8.3.2 使用强类型参数,避免使用void*
1678尽管不同的语言对待强类型和弱类型有自己的观点,但是一般认为c/c++是强类型语言,既然我们使用的语言是强类型的,就应该保持这样的风格。
1679好处是尽量让编译器在编译阶段就检查出类型不匹配的问题。
1680
1681使用强类型便于编译器帮我们发现错误,如下代码中注意函数 FooListAddNode 的使用:
1682```cpp
1683struct FooNode {
1684    struct List link;
1685    int foo;
1686};
1687
1688struct BarNode {
1689    struct List link;
1690    int bar;
1691}
1692
1693void FooListAddNode(void *node) // Bad: 这里用 void * 类型传递参数
1694{
1695    FooNode *foo = (FooNode *)node;
1696    ListAppend(&g_FooList, &foo->link);
1697}
1698
1699void MakeTheList()
1700{
1701    FooNode *foo = nullptr;
1702    BarNode *bar = nullptr;
1703    ...
1704
1705    FooListAddNode(bar);        // Wrong: 这里本意是想传递参数 foo,但错传了 bar,却没有报错
1706}
1707```
1708
17091. 可以使用模板函数来实现参数类型的变化。
17102. 可以使用基类指针来实现多态。
1711
1712### <a name="a8-3-3"></a>建议8.3.3 函数的参数个数不超过5个
1713函数的参数过多,会使得该函数易于受外部变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。
1714
1715如果超过可以考虑:
1716- 看能否拆分函数
1717- 看能否将相关参数合在一起,定义结构体
1718
1719# <a name="c9"></a> 9 C++其他特性
1720
1721## <a name="c9-1"></a> 常量与初始化
1722
1723不变的值更易于理解、跟踪和分析,所以应该尽可能地使用常量代替变量,定义值的时候,应该把const作为默认的选项。
1724
1725### <a name="r9-1-1"></a>规则9.1.1 不允许使用宏来表示常量
1726
1727**说明**:宏是简单的文本替换,在预处理阶段时完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名;宏没有类型检查,不安全;宏没有作用域。
1728
1729```cpp
1730#define MAX_MSISDN_LEN 20    // 不好
1731
1732// C++请使用const常量
1733const int MAX_MSISDN_LEN = 20; // 好
1734
1735// 对于C++11以上版本,可以使用constexpr
1736constexpr int MAX_MSISDN_LEN = 20;
1737```
1738
1739###  <a name="a9-1-1"></a>建议9.1.1 一组相关的整型常量应定义为枚举
1740
1741**说明**:枚举比`#define`或`const int`更安全。编译器会检查参数值是否位于枚举取值范围内,避免错误发生。
1742
1743```cpp
1744// 好的例子:
1745enum Week {
1746    SUNDAY,
1747    MONDAY,
1748    TUESDAY,
1749    WEDNESDAY,
1750    THURSDAY,
1751    FRIDAY,
1752    SATURDAY
1753};
1754
1755enum Color {
1756    RED,
1757    BLACK,
1758    BLUE
1759};
1760
1761void ColorizeCalendar(Week today, Color color);
1762
1763ColorizeCalendar(BLUE, SUNDAY); // 编译报错,参数类型错误
1764
1765// 不好的例子:
1766const int SUNDAY = 0;
1767const int MONDAY = 1;
1768
1769const int BLACK  = 0;
1770const int BLUE   = 1;
1771
1772bool ColorizeCalendar(int today, int color);
1773ColorizeCalendar(BLUE, SUNDAY); // 不会报错
1774```
1775
1776当枚举值需要对应到具体数值时,须在声明时显式赋值。否则不需要显式赋值,以避免重复赋值,降低维护(增加、删除成员)工作量。
1777
1778```cpp
1779// 好的例子:S协议里定义的设备ID值,用于标识设备类型
1780enum DeviceType {
1781    DEV_UNKNOWN = -1,
1782    DEV_DSMP = 0,
1783    DEV_ISMG = 1,
1784    DEV_WAPPORTAL = 2
1785};
1786```
1787
1788程序内部使用,仅用于分类的情况,不应该进行显式的赋值。
1789
1790```cpp
1791// 好的例子:程序中用来标识会话状态的枚举定义
1792enum SessionState {
1793    INIT,
1794    CLOSED,
1795    WAITING_FOR_RESPONSE
1796};
1797```
1798
1799应当尽量避免枚举值重复,如必须重复也要用已定义的枚举来修饰
1800
1801```cpp
1802enum RTCPType {
1803    RTCP_SR = 200,
1804    RTCP_MIN_TYPE = RTCP_SR,
1805    RTCP_RR    = 201,
1806    RTCP_SDES  = 202,
1807    RTCP_BYE   = 203,
1808    RTCP_APP   = 204,
1809    RTCP_RTPFB = 205,
1810    RTCP_PSFB  = 206,
1811    RTCP_XR  = 207,
1812    RTCP_RSI = 208,
1813    RTCP_PUBPORTS = 209,
1814    RTCP_MAX_TYPE = RTCP_PUBPORTS
1815};
1816```
1817
1818### <a name="r9-1-2"></a>规则9.1.2 不允许使用魔鬼数字
1819所谓魔鬼数字即看不懂、难以理解的数字。
1820
1821魔鬼数字并非一个非黑即白的概念,看不懂也有程度,需要自行判断。
1822例如数字 12,在不同的上下文中情况是不一样的:
1823type = 12; 就看不懂,但 `monthsCount = yearsCount * 12`; 就能看懂。
1824数字 0 有时候也是魔鬼数字,比如 `status = 0`; 并不能表达是什么状态。
1825
1826解决途径:
1827对于局部使用的数字,可以增加注释说明
1828对于多处使用的数字,必须定义 const 常量,并通过符号命名自注释。
1829
1830禁止出现下列情况:
1831没有通过符号来解释数字含义,如` const int ZERO = 0`
1832符号命名限制了其取值,如 `const int XX_TIMER_INTERVAL_300MS = 300`,直接使用`XX_TIMER_INTERVAL_MS`来表示该常量是定时器的时间间隔。
1833
1834### <a name="r9-1-3"></a>规则9.1.3 常量应该保证单一职责
1835
1836**说明**:一个常量只用来表示一个特定功能,即一个常量不能有多种用途。
1837
1838```cpp
1839// 好的例子:协议A和协议B,手机号(MSISDN)的长度都是20。
1840const unsigned int A_MAX_MSISDN_LEN = 20;
1841const unsigned int B_MAX_MSISDN_LEN = 20;
1842
1843// 或者使用不同的名字空间:
1844namespace Namespace1 {
1845    const unsigned int MAX_MSISDN_LEN = 20;
1846}
1847
1848namespace Namespace2 {
1849    const unsigned int MAX_MSISDN_LEN = 20;
1850}
1851```
1852
1853### <a name="r9-1-4"></a>规则9.1.4 禁止用memcpy_s、memset_s初始化非POD对象
1854
1855**说明**:`POD`全称是`Plain Old Data`,是C++ 98标准(ISO/IEC 14882, first edition, 1998-09-01)中引入的一个概念,`POD`类型主要包括`int`, `char`, `float`,`double`,`enumeration`,`void`,指针等原始类型以及聚合类型,不能使用封装和面向对象特性(如用户定义的构造/赋值/析构函数、基类、虚函数等)。
1856
1857由于非POD类型比如非聚合类型的class对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥用内存拷贝可能会导致严重的问题。
1858
1859即使对聚合类型的class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡`memcpy_s`、`memset_s`操作。
1860
1861对于POD类型的详细说明请参见附录。
1862
1863### <a name="a9-1-2"></a>建议9.1.2 变量使用时才声明并初始化
1864
1865**说明**:变量在使用前未赋初值,是常见的低级编程错误。使用前才声明变量并同时初始化,非常方便地避免了此类低级错误。
1866
1867在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题:
1868* 程序难以理解和维护:变量的定义与使用分离。
1869* 变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的空值(比如零)来初始化,这通常是一种浪费,如果变量在被赋于有效值以前使用,还会导致错误。
1870
1871遵循变量作用域最小化原则与就近声明原则, 使得代码更容易阅读,方便了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。
1872
1873```cpp
1874// 不好的例子:声明与初始化分离
1875string name;        // 声明时未初始化:调用缺省构造函数
1876name = "zhangsan";  // 再次调用赋值操作符函数;声明与定义在不同的地方,理解相对困难
1877
1878// 好的例子:声明与初始化一体,理解相对容易
1879string name("zhangsan");  // 调用构造函数
1880```
1881
1882
1883## <a name="c9-2"></a> 表达式
1884### <a name="r9-2-1"></a>规则9.2.1 含有变量自增或自减运算的表达式中禁止再次引用该变量
1885含有变量自增或自减运算的表达式中,如果再引用该变量,其结果在C++标准中未明确定义。各个编译器或者同一个编译器不同版本实现可能会不一致。
1886为了更好的可移植性,不应该对标准未定义的运算次序做任何假设。
1887
1888注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。
1889
1890示例:
1891```cpp
1892x = b[i] + i++; // Bad: b[i]运算跟 i++,先后顺序并不明确。
1893```
1894正确的写法是将自增或自减运算单独放一行:
1895```cpp
1896x = b[i] + i;
1897i++;            // Good: 单独一行
1898```
1899
1900函数参数
1901```cpp
1902Func(i++, i);   // Bad: 传递第2个参数时,不确定自增运算有没有发生
1903```
1904
1905正确的写法
1906```cpp
1907i++;            // Good: 单独一行
1908x = Func(i, i);
1909```
1910
1911### <a name="r9-2-2"></a>规则9.2.2 switch语句要有default分支
1912大部分情况下,switch语句中要有default分支,保证在遗漏case标签处理时能够有一个缺省的处理行为。
1913
1914特例:
1915如果switch条件变量是枚举类型,并且 case 分支覆盖了所有取值,则加上default分支处理有些多余。
1916现代编译器都具备检查是否在switch语句中遗漏了某些枚举值的case分支的能力,会有相应的warning提示。
1917
1918```cpp
1919enum Color {
1920    RED = 0,
1921    BLUE
1922};
1923
1924// 因为switch条件变量是枚举值,这里可以不用加default处理分支
1925switch (color) {
1926    case RED:
1927        DoRedThing();
1928        break;
1929    case BLUE:
1930        DoBlueThing();
1931        ...
1932        break;
1933}
1934```
1935
1936### <a name="a9-2-1"></a>建议9.2.1 表达式的比较,应当遵循左侧倾向于变化、右侧倾向于不变的原则
1937当变量与常量比较时,如果常量放左边,如 if (MAX == v) 不符合阅读习惯,而 if (MAX > v) 更是难于理解。
1938应当按人的正常阅读、表达习惯,将常量放右边。写成如下方式:
1939```cpp
1940if (value == MAX) {
1941
1942}
1943
1944if (value < MAX) {
1945
1946}
1947```
1948也有特殊情况,如:`if (MIN < value && value < MAX)` 用来描述区间时,前半段是常量在左的。
1949
1950不用担心将 '==' 误写成 '=',因为` if (value = MAX)` 会有编译告警,其他静态检查工具也会报错。让工具去解决笔误问题,代码要符合可读性第一。
1951
1952### <a name="a9-2-2"></a>建议9.2.2 使用括号明确操作符的优先级
1953使用括号明确操作符的优先级,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。下面是如何使用括号的建议。
1954
1955- 二元及以上操作符, 如果涉及多种操作符,则应该使用括号
1956```cpp
1957x = a + b + c;         /* 操作符相同,可以不加括号 */
1958x = Foo(a + b, c);     /* 逗号两边的表达式,不需要括号 */
1959x = 1 << (2 + 3);      /* 操作符不同,需要括号 */
1960x = a + (b / 5);       /* 操作符不同,需要括号 */
1961x = (a == b) ? a : (a – b);    /* 操作符不同,需要括号 */
1962```
1963
1964
1965## <a name="c9-3"></a> 类型转换
1966
1967避免使用类型分支来定制行为:类型分支来定制行为容易出错,是企图用C++编写C代码的明显标志。这是一种很不灵活的技术,要添加新类型时,如果忘记修改所有分支,编译器也不会告知。使用模板和虚函数,让类型自己而不是调用它们的代码来决定行为。
1968
1969建议避免类型转换,我们在代码的类型设计上应该考虑到每种数据的数据类型是什么,而不是应该过度使用类型转换来解决问题。在设计某个基本类型的时候,请考虑:
1970- 是无符号还是有符号的
1971- 是适合float还是double
1972- 是使用int8,int16,int32还是int64,确定整形的长度
1973
1974但是我们无法禁止使用类型转换,因为C++语言是一门面向机器编程的语言,涉及到指针地址,并且我们会与各种第三方或者底层API交互,他们的类型设计不一定是合理的,在这个适配的过程中很容易出现类型转换。
1975
1976例外:在调用某个函数的时候,如果我们不想处理函数结果,首先要考虑这个是否是你的最好的选择。如果确实不想处理函数的返回值,那么可以使用(void)转换来解决。
1977
1978### <a name="r9-3-1"></a>规则9.3.1 如果确定要使用类型转换,请使用由C++提供的类型转换,而不是C风格的类型转换
1979
1980**说明**:
1981
1982C++提供的类型转换操作比C风格更有针对性,更易读,也更加安全,C++提供的转换有:
1983- 类型转换:
19841. `dynamic_cast`:主要用于继承体系下行转换,`dynamic_cast`具有类型检查的功能,请做好基类和派生类的设计,避免使用dynamic_cast来进行转换。
19852. `static_cast`:和C风格转换相似可做值的强制转换,或上行转换(把派生类的指针或引用转换成基类的指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。如果是纯粹的算数转换,那么请使用后面的大括号转换方式。
19863. `reinterpret_cast`:用于转换不相关的类型。`reinterpret_cast`强制编译器将某个类型对象的内存重新解释成另一种类型,这是一种不安全的转换,建议尽可能少用`reinterpret_cast`。
19874. `const_cast`:用于移除对象的`const`属性,使对象变得可修改,这样会破坏数据的不变性,建议尽可能少用。
1988
1989- 算数转换: (C++11开始支持)
1990  对于那种算数转换,并且类型信息没有丢失的,比如float到double, int32到int64的转换,推荐使用大括号的初始方式。
1991```cpp
1992  double d{ someFloat };
1993  int64_t i{ someInt32 };
1994```
1995
1996### <a name="a9-3-1"></a>建议9.3.1 避免使用`dynamic_cast`
19971. `dynamic_cast`依赖于C++的RTTI, 让程序员在运行时识别C++类对象的类型。
19982. `dynamic_cast`的出现一般说明我们的基类和派生类设计出现了问题,派生类破坏了基类的契约,不得不通过`dynamic_cast`转换到子类进行特殊处理,这个时候更希望来改善类的设计,而不是通过`dynamic_cast`来解决问题。
1999
2000### <a name="a9-3-2"></a>建议9.3.2 避免使用`reinterpret_cast`
2001
2002**说明**:`reinterpret_cast`用于转换不相关类型。尝试用`reinterpret_cast`将一种类型强制转换另一种类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。
2003
2004### <a name="a9-3-3"></a>建议9.3.3 避免使用`const_cast`
2005
2006**说明**:`const_cast`用于移除对象的`const`和`volatile`性质。
2007
2008使用const_cast转换后的指针或者引用来修改const对象,行为是未定义的。
2009
2010```cpp
2011// 不好的例子
2012const int i = 1024;
2013int* p = const_cast<int*>(&i);
2014*p = 2048;      // 未定义行为
2015```
2016
2017```cpp
2018// 不好的例子
2019class Foo {
2020public:
2021    Foo() : i(3) {}
2022
2023    void Fun(int v)
2024    {
2025        i = v;
2026    }
2027
2028private:
2029    int i;
2030};
2031
2032int main(void)
2033{
2034    const Foo f;
2035    Foo* p = const_cast<Foo*>(&f);
2036    p->Fun(8);  // 未定义行为
2037}
2038
2039```
2040
2041
2042## <a name="c9-4"></a>资源分配和释放
2043
2044### <a name="r9-4-1"></a>规则9.4.1 单个对象释放使用delete,数组对象释放使用delete []
2045说明:单个对象删除使用delete, 数组对象删除使用delete [],原因:
2046
2047- 调用new所包含的动作:从系统中申请一块内存,并调用此类型的构造函数。
2048- 调用new[n]所包含的动作:申请可容纳n个对象的内存,并且对每一个对象调用其构造函数。
2049- 调用delete所包含的动作:先调用相应的析构函数,再将内存归还系统。
2050- 调用delete[]所包含的动作:对每一个对象调用析构函数,再释放所有内存
2051
2052如果new和delete的格式不匹配,结果是未知的。对于非class类型, new和delete不会调用构造与析构函数。
2053
2054错误写法:
2055```cpp
2056const int MAX_ARRAY_SIZE = 100;
2057int* numberArray = new int[MAX_ARRAY_SIZE];
2058...
2059delete numberArray;
2060numberArray = nullptr;
2061```
2062
2063正确写法:
2064```cpp
2065const int MAX_ARRAY_SIZE = 100;
2066int* numberArray = new int[MAX_ARRAY_SIZE];
2067...
2068delete[] numberArray;
2069numberArray = nullptr;
2070```
2071
2072### <a name="a9-4-1"></a>建议9.4.1 使用 RAII 特性来帮助追踪动态分配
2073
2074说明:RAII是“资源获取就是初始化”的缩语(Resource Acquisition Is Initialization),是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
2075
2076RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。这种做法有两大好处:
2077- 我们不需要显式地释放资源。
2078- 对象所需的资源在其生命期内始终保持有效。这样,就不必检查资源有效性的问题,可以简化逻辑、提高效率。
2079
2080
2081示例:使用RAII不需要显式地释放互斥资源。
2082
2083```cpp
2084class LockGuard {
2085public:
2086    LockGuard(const LockType& lockType): lock_(lockType)
2087    {
2088        lock_.Aquire();
2089    }
2090
2091    ~LockGuard()
2092    {
2093        lock_.Relase();
2094    }
2095
2096private:
2097    LockType lock_;
2098};
2099
2100
2101bool Update()
2102{
2103    LockGuard lockGuard(mutex);
2104    if (...) {
2105        return false;
2106    } else {
2107        // 操作数据
2108    }
2109
2110    return true;
2111}
2112```
2113
2114## <a name="c9-5"></a>标准库
2115
2116STL标准模板库在不同产品使用程度不同,这里列出一些基本规则和建议,供各团队参考。
2117
2118### <a name="r9-5-1"></a>规则9.5.1 不要保存std::string的c_str()返回的指针
2119
2120说明:在C++标准中并未规定string::c_str()指针持久有效,因此特定STL实现完全可以在调用string::c_str()时返回一个临时存储区并很快释放。所以为了保证程序的可移植性,不要保存string::c_str()的结果,而是在每次需要时直接调用。
2121
2122示例:
2123
2124```cpp
2125void Fun1()
2126{
2127    std::string name = "demo";
2128    const char* text = name.c_str();  // 表达式结束以后,name的生命周期还在,指针有效
2129
2130    // 如果中间调用了string的非const成员函数,导致string被修改,比如operator[], begin()等
2131    // 可能会导致text的内容不可用,或者不是原来的字符串
2132    name = "test";
2133    name[1] = '2';
2134
2135    // 后续使用text指针,其字符串内容不再是"demo"
2136}
2137
2138void Fun2()
2139{
2140    std::string name = "demo";
2141    std::string test = "test";
2142    const char* text = (name + test).c_str(); // 表达式结束以后,+号产生的临时对象被销毁,指针无效
2143
2144    // 后续使用text指针,其已不再指向合法内存空间
2145}
2146```
2147例外:在少数对性能要求非常高的代码中,为了适配已有的只接受const char*类型入参的函数,可以临时保存string::c_str()返回的指针。但是必须严格保证string对象的生命周期长于所保存指针的生命周期,并且保证在所保存指针的生命周期内,string对象不会被修改。
2148
2149
2150### <a name="a9-5-1"></a>建议9.5.1 使用std::string代替char*
2151
2152说明:使用string代替`char*`有很多优势,比如:
21531. 不用考虑结尾的’\0’;
21542. 可以直接使用+, =, ==等运算符以及其它字符串操作函数;
21553. 不需要考虑内存分配操作,避免了显式的new/delete,以及由此导致的错误;
2156
2157需要注意的是某些stl实现中string是基于写时复制策略的,这会带来2个问题,一是某些版本的写时复制策略没有实现线程安全,在多线程环境下会引起程序崩溃;二是当与动态链接库相互传递基于写时复制策略的string时,由于引用计数在动态链接库被卸载时无法减少可能导致悬挂指针。因此,慎重选择一个可靠的stl实现对于保证程序稳定是很重要的。
2158
2159例外:
2160当调用系统或者其它第三方库的API时,针对已经定义好的接口,只能使用`char*`。但是在调用接口之前都可以使用string,在调用接口时使用string::c_str()获得字符指针。
2161当在栈上分配字符数组当作缓冲区使用时,可以直接定义字符数组,不要使用string,也没有必要使用类似`vector<char>`等容器。
2162
2163### <a name="r9-5-2"></a>规则9.5.2 禁止使用auto_ptr
2164说明:在stl库中的std::auto_ptr具有一个隐式的所有权转移行为,如下代码:
2165```cpp
2166auto_ptr<T> p1(new T);
2167auto_ptr<T> p2 = p1;
2168```
2169当执行完第2行语句后,p1已经不再指向第1行中分配的对象,而是变为nullptr。正因为如此,auto_ptr不能被置于各种标准容器中。
2170转移所有权的行为通常不是期望的结果。对于必须转移所有权的场景,也不应该使用隐式转移的方式。这往往需要程序员对使用auto_ptr的代码保持额外的谨慎,否则出现对空指针的访问。
2171使用auto_ptr常见的有两种场景,一是作为智能指针传递到产生auto_ptr的函数外部,二是使用auto_ptr作为RAII管理类,在超出auto_ptr的生命周期时自动释放资源。
2172对于第1种场景,可以使用std::shared_ptr来代替。
2173对于第2种场景,可以使用C++11标准中的std::unique_ptr来代替。其中std::unique_ptr是std::auto_ptr的代替品,支持显式的所有权转移。
2174
2175例外:
2176在C++11标准得到普遍使用之前,在一定需要对所有权进行转移的场景下,可以使用std::auto_ptr,但是建议对std::auto_ptr进行封装,并禁用封装类的拷贝构造函数和赋值运算符,以使该封装类无法用于标准容器。
2177
2178
2179### <a name="a9-5-2"></a>建议9.5.2 使用新的标准头文件
2180
2181说明:
2182使用C++的标准头文件时,请使用`<cstdlib>`这样的,而不是`<stdlib.h>`这种的。
2183
2184## <a name="c9-6"></a> const的用法
2185在声明的变量或参数前加上关键字 const 用于指明变量值不可被篡改 (如 `const int foo` ). 为类中的函数加上 const 限定符表明该函数不会修改类成员变量的状态 (如 `class Foo { int Bar(char c) const; };`)。 const 变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障, 便于尽早发现错误。因此, 我们强烈建议在任何可能的情况下使用 const。
2186有时候,使用C++11的constexpr来定义真正的常量可能更好。
2187
2188### <a name="r9-6-1"></a>规则9.6.1 对于指针和引用类型的形参,如果是不需要修改的,请使用const
2189不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。
2190```cpp
2191class Foo;
2192
2193void PrintFoo(const Foo& foo);
2194```
2195
2196### <a name="r9-6-2"></a>规则9.6.2 对于不会修改成员变量的成员函数请使用const修饰
2197尽可能将成员函数声明为 const。 访问函数应该总是 const。只要不修改数据成员的成员函数,都声明为const。
2198对于虚函数,应当从设计意图上考虑继承链上的所有类是否需要在此虚函数中修改数据成员,而不是仅关注单个类的实现。
2199```cpp
2200class Foo {
2201public:
2202
2203    // ...
2204
2205    int PrintValue() const // const修饰成员函数,不会修改成员变量
2206    {
2207        std::cout << value_ << std::endl;
2208    }
2209
2210    int GetValue() const  // const修饰成员函数,不会修改成员变量
2211    {
2212        return value_;
2213    }
2214
2215private:
2216    int value_;
2217};
2218```
2219
2220### <a name="a9-6-1"></a>建议9.6.1 初始化后不会再修改的成员变量定义为const
2221
2222```cpp
2223class Foo {
2224public:
2225    Foo(int length) : dataLength_(length) {}
2226private:
2227    const int dataLength_;
2228};
2229```
2230
2231## <a name="c9-7"></a> 异常
2232
2233### <a name="a9-7-1"></a>建议9.7.1 C++11中,如果函数不会抛出异常,声明为`noexcept`
2234**理由**
22351. 如果函数不会抛出异常,声明为`noexcept`可以让编译器最大程度的优化函数,如减少执行路径,提高错误退出的效率。
22362. `vector`等STL容器,为了保证接口的健壮性,如果保存元素的`move运算符`没有声明为`noexcept`,则在容器扩张搬移元素时不会使用`move机制`,而使用`copy机制`,带来性能损失的风险。如果一个函数不能抛出异常,或者一个程序并没有截获某个函数所抛出的异常并进行处理,那么这个函数可以用新的`noexcept`关键字对其进行修饰,表示这个函数不会抛出异常或者抛出的异常不会被截获并处理。例如:
2237
2238```cpp
2239extern "C" double sqrt(double) noexcept;  // 永远不会抛出异常
2240
2241// 即使可能抛出异常,也可以使用 noexcept
2242// 这里不准备处理内存耗尽的异常,简单地将函数声明为noexcept
2243std::vector<int> MyComputation(const std::vector<int>& v) noexcept
2244{
2245    std::vector<int> res = v;    // 可能会抛出异常
2246    // do something
2247    return res;
2248}
2249```
2250
2251**示例**
2252
2253```cpp
2254RetType Function(Type params) noexcept;   // 最大的优化
2255RetType Function(Type params);            // 更少的优化
2256
2257// std::vector 的 move 操作需要声明 noexcept
2258class Foo1 {
2259public:
2260    Foo1(Foo1&& other);  // no noexcept
2261};
2262
2263std::vector<Foo1> a1;
2264a1.push_back(Foo1());
2265a1.push_back(Foo1());  // 触发容器扩张,搬移已有元素时调用copy constructor
2266
2267class Foo2 {
2268public:
2269    Foo2(Foo2&& other) noexcept;
2270};
2271
2272std::vector<Foo2> a2;
2273a2.push_back(Foo2());
2274a2.push_back(Foo2());  // 触发容器扩张,搬移已有元素时调用move constructor
2275```
2276
2277**注意**
2278默认构造函数、析构函数、`swap`函数,`move操作符`都不应该抛出异常。
2279
2280## <a name="c9-8"></a> 模板
2281
2282模板能够实现非常灵活简洁的类型安全的接口,实现类型不同但是行为相同的代码复用。
2283
2284模板编程的缺点:
2285
22861. 模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug 和维护起来都很麻烦。
22872. 模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解。
22883. 模板如果使用不当,会导致运行时代码过度膨胀。
22894. 模板代码难以修改和重构。模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用。
2290
2291所以, 建议__模板编程最好只用在少量的基础组件,基础数据结构上面__。并且使用模板编程的时候尽可能把__复杂度最小化__,尽量__不要让模板对外暴露__。最好只在实现里面使用模板, 然后给用户暴露的接口里面并不使用模板, 这样能提高你的接口的可读性。 并且你应该在这些使用模板的代码上写尽可能详细的注释。
2292
2293
2294## <a name="c9-9"></a> 宏
2295在C++语言中,我们强烈建议尽可能少使用复杂的宏
2296- 对于常量定义,请按照前面章节所述,使用const或者枚举;
2297- 对于宏函数,尽可能简单,并且遵循下面的原则,并且优先使用内联函数,模板函数等进行替换。
2298
2299```cpp
2300// 不推荐使用宏函数
2301#define SQUARE(a, b) ((a) * (b))
2302
2303// 请使用模板函数,内联函数等来替换。
2304template<typename T> T Square(T a, T b) { return a * b; }
2305```
2306
2307如果需要使用宏,请参考C语言规范的相关章节。
2308**例外**:一些通用且成熟的应用,如:对 new, delete 的封装处理,可以保留对宏的使用。
2309
2310# <a name="c10"></a> 10 现代C++特性
2311
2312随着 ISO 在2011年发布 C++11 语言标准,以及2017年3月发布 C++17 ,现代C++(C++11/14/17等)增加了大量提高编程效率、代码质量的新语言特性和标准库。
2313本章节描述了一些可以帮助团队更有效率的使用现代C++,规避语言陷阱的指导意见。
2314
2315## <a name="c10-1"></a> 代码简洁性和安全性提升
2316### <a name="a10-1-1"></a>建议10.1.1 合理使用`auto`
2317**理由**
2318
2319* `auto`可以避免编写冗长、重复的类型名,也可以保证定义变量时初始化。
2320* `auto`类型推导规则复杂,需要仔细理解。
2321* 如果能够使代码更清晰,继续使用明确的类型,且只在局部变量使用`auto`。
2322
2323**示例**
2324
2325```cpp
2326// 避免冗长的类型名
2327std::map<string, int>::iterator iter = m.find(val);
2328auto iter = m.find(val);
2329
2330// 避免重复类型名
2331class Foo {...};
2332Foo* p = new Foo;
2333auto p = new Foo;
2334
2335// 保证初始化
2336int x;    // 编译正确,没有初始化
2337auto x;   // 编译失败,必须初始化
2338```
2339
2340auto 的类型推导可能导致困惑:
2341
2342```cpp
2343auto a = 3;           // int
2344const auto ca = a;    // const int
2345const auto& ra = a;   // const int&
2346auto aa = ca;         // int, 忽略 const 和 reference
2347auto ila1 = { 10 };   // std::initializer_list<int>
2348auto ila2{ 10 };      // std::initializer_list<int>
2349
2350auto&& ura1 = x;      // int&
2351auto&& ura2 = ca;     // const int&
2352auto&& ura3 = 10;     // int&&
2353
2354const int b[10];
2355auto arr1 = b;        // const int*
2356auto& arr2 = b;       // const int(&)[10]
2357```
2358
2359如果没有注意 `auto` 类型推导时忽略引用,可能引入难以发现的性能问题:
2360
2361```cpp
2362std::vector<std::string> v;
2363auto s1 = v[0];  // auto 推导为 std::string,拷贝 v[0]
2364```
2365
2366如果使用`auto`定义接口,如头文件中的常量,可能因为开发人员修改了值,而导致类型发生变化。
2367
2368### <a name="r10-1-1"></a>规则10.1.1 在重写虚函数时请使用`override`或`final`关键字
2369**理由**
2370`override`和`final`关键字都能保证函数是虚函数,且重写了基类的虚函数。如果子类函数与基类函数原型不一致,则产生编译告警。`final`还保证虚函数不会再被子类重写。
2371
2372使用`override`或`final`关键字后,如果修改了基类虚函数原型,但忘记修改子类重写的虚函数,在编译期就可以发现。也可以避免有多个子类时,重写虚函数的修改遗漏。
2373
2374**示例**
2375
2376```cpp
2377class Base {
2378public:
2379    virtual void Foo();
2380    virtual void Foo(int var);
2381    void Bar();
2382};
2383
2384class Derived : public Base {
2385public:
2386    void Foo() const override; // 编译失败: Derived::Foo 和 Base::Foo 原型不一致,不是重写
2387    void Foo() override;       // 正确: Derived::Foo 重写 Base::Foo
2388    void Foo(int var) final;   // 正确: Derived::Foo(int) 重写 Base::Foo(int),且Derived的派生类不能再重写此函数
2389    void Bar() override;       // 编译失败: Base::Bar 不是虚函数
2390};
2391```
2392
2393**总结**
23941. 基类首次定义虚函数,使用`virtual`关键字
23952. 子类重写基类虚函数(包括析构函数),使用`override`或`final`关键字(但不要两者一起使用),并且不使用`virtual`关键字
23963. 非虚函数,`virtual`、`override`和`final`都不使用
2397
2398### <a name="r10-1-2"></a>规则10.1.2 使用`delete`关键字删除函数
2399**理由**
2400相比于将类成员函数声明为`private`但不实现,`delete`关键字更明确,且适用范围更广。
2401
2402**示例**
2403
2404```cpp
2405class Foo {
2406private:
2407    // 只看头文件不知道拷贝构造是否被删除
2408    Foo(const Foo&);
2409};
2410
2411class Foo {
2412public:
2413    // 明确删除拷贝赋值函数
2414    Foo& operator=(const Foo&) = delete;
2415};
2416```
2417
2418`delete`关键字还支持删除非成员函数
2419
2420```cpp
2421template<typename T>
2422void Process(T value);
2423
2424template<>
2425void Process<void>(void) = delete;
2426```
2427
2428### <a name="r10-1-3"></a>规则10.1.3 使用`nullptr`,而不是`NULL`或`0`
2429**理由**
2430长期以来,C++没有一个代表空指针的关键字,这是一件很尴尬的事:
2431
2432```cpp
2433#define NULL ((void *)0)
2434
2435char* str = NULL;   // 错误: void* 不能自动转换为 char*
2436
2437void(C::*pmf)() = &C::Func;
2438if (pmf == NULL) {} // 错误: void* 不能自动转换为指向成员函数的指针
2439```
2440
2441如果把`NULL`被定义为`0`或`0L`。可以解决上面的问题。
2442
2443或者在需要空指针的地方直接使用`0`。但这引入另一个问题,代码不清晰,特别是使用`auto`自动推导:
2444
2445```cpp
2446auto result = Find(id);
2447if (result == 0) {  // Find() 返回的是 指针 还是 整数?
2448    // do something
2449}
2450```
2451
2452`0`字面上是`int`类型(`0L`是`long`),所以`NULL`和`0`都不是指针类型。
2453当重载指针和整数类型的函数时,传递`NULL`或`0`都调用到整数类型重载的函数:
2454
2455```cpp
2456void F(int);
2457void F(int*);
2458
2459F(0);      // 调用 F(int),而非 F(int*)
2460F(NULL);   // 调用 F(int),而非 F(int*)
2461```
2462
2463另外,`sizeof(NULL) == sizeof(void*)`并不一定总是成立的,这也是一个潜在的风险。
2464
2465总结: 直接使用`0`或`0L`,代码不清晰,且无法做到类型安全;使用`NULL`无法做到类型安全。这些都是潜在的风险。
2466
2467`nullptr`的优势不仅仅是在字面上代表了空指针,使代码清晰,而且它不再是一个整数类型。
2468
2469`nullptr`是`std::nullptr_t`类型,而`std::nullptr_t`可以隐式的转换为所有的原始指针类型,这使得`nullptr`可以表现成指向任意类型的空指针。
2470
2471```cpp
2472void F(int);
2473void F(int*);
2474F(nullptr);   // 调用 F(int*)
2475
2476auto result = Find(id);
2477if (result == nullptr) {  // Find() 返回的是 指针
2478    // do something
2479}
2480```
2481
2482### <a name="r10-1-4"></a>规则10.1.4 使用`using`而非`typedef`
2483在`C++11`之前,可以通过`typedef`定义类型的别名。没人愿意多次重复`std::map<uint32_t, std::vector<int>>`这样的代码。
2484
2485```cpp
2486typedef std::map<uint32_t, std::vector<int>> SomeType;
2487```
2488
2489类型的别名实际是对类型的封装。而通过封装,可以让代码更清晰,同时在很大程度上避免类型变化带来的散弹式修改。
2490在`C++11`之后,提供`using`,实现`声明别名(alias declarations)`:
2491
2492```cpp
2493using SomeType = std::map<uint32_t, std::vector<int>>;
2494```
2495
2496对比两者的格式:
2497
2498```cpp
2499typedef Type Alias;   // Type 在前,还是 Alias 在前
2500using Alias = Type;   // 符合'赋值'的用法,容易理解,不易出错
2501```
2502
2503如果觉得这点还不足以切换到`using`,我们接着看看`模板别名(alias template)`:
2504
2505```cpp
2506// 定义模板的别名,一行代码
2507template<class T>
2508using MyAllocatorVector = std::vector<T, MyAllocator<T>>;
2509
2510MyAllocatorVector<int> data;       // 使用 using 定义的别名
2511
2512template<class T>
2513class MyClass {
2514private:
2515    MyAllocatorVector<int> data_;   // 模板类中使用 using 定义的别名
2516};
2517```
2518
2519而`typedef`不支持带模板参数的别名,只能"曲线救国":
2520
2521```cpp
2522// 通过模板包装 typedef,需要实现一个模板类
2523template<class T>
2524struct MyAllocatorVector {
2525    typedef std::vector<T, MyAllocator<T>> type;
2526};
2527
2528MyAllocatorVector<int>::type data;  // 使用 typedef 定义的别名,多写 ::type
2529
2530template<class T>
2531class MyClass {
2532private:
2533    typename MyAllocatorVector<int>::type data_;  // 模板类中使用,除了 ::type,还需要加上 typename
2534};
2535```
2536
2537### <a name="r10-1-5"></a>规则10.1.5 禁止使用std::move操作const对象
2538从字面上看,`std::move`的意思是要移动一个对象。而const对象是不允许修改的,自然也无法移动。因此用`std::move`操作const对象会给代码阅读者带来困惑。
2539在实际功能上,`std::move`会把对象转换成右值引用类型;对于const对象,会将其转换成const的右值引用。由于极少有类型会定义以const右值引用为参数的移动构造函数和移动赋值操作符,因此代码实际功能往往退化成了对象拷贝而不是对象移动,带来了性能上的损失。
2540
2541**错误示例:**
2542```cpp
2543std::string g_string;
2544std::vector<std::string> g_stringList;
2545
2546void func()
2547{
2548    const std::string myString = "String content";
2549    g_string = std::move(myString); // bad:并没有移动myString,而是进行了复制
2550    const std::string anotherString = "Another string content";
2551    g_stringList.push_back(std::move(anotherString));    // bad:并没有移动anotherString,而是进行了复制
2552}
2553```
2554
2555## <a name="c10-2"></a> 智能指针
2556### <a name="r10-2-1"></a>规则10.2.1 优先使用智能指针而不是原始指针管理资源
2557**理由**
2558避免资源泄露。
2559
2560**示例**
2561
2562```cpp
2563void Use(int i)
2564{
2565    auto p = new int {7};               // 不好: 通过 new 初始化局部指针
2566    auto q = std::make_unique<int>(9);  // 好: 保证释放内存
2567    if (i > 0) {
2568        return;                         // 可能 return,导致内存泄露
2569    }
2570    delete p;                           // 太晚了
2571}
2572```
2573
2574**例外**
2575在性能敏感、兼容性等场景可以使用原始指针。
2576
2577### <a name="r10-2-2"></a>规则10.2.2 优先使用`unique_ptr`而不是`shared_ptr`
2578**理由**
25791. `shared_ptr`引用计数的原子操作存在可测量的开销,大量使用`shared_ptr`影响性能。
25802. 共享所有权在某些情况(如循环依赖)可能导致对象永远得不到释放。
25813. 相比于谨慎设计所有权,共享所有权是一种诱人的替代方案,但它可能使系统变得混乱。
2582
2583### <a name="r10-2-3"></a>规则10.2.3 使用`std::make_unique`而不是`new`创建`unique_ptr`
2584**理由**
25851. `make_unique`提供了更简洁的创建方式
25862. 保证了复杂表达式的异常安全
2587
2588**示例**
2589
2590```cpp
2591// 不好:两次出现 MyClass,重复导致不一致风险
2592std::unique_ptr<MyClass> ptr(new MyClass(0, 1));
2593// 好:只出现一次 MyClass,不存在不一致的可能
2594auto ptr = std::make_unique<MyClass>(0, 1);
2595```
2596
2597重复出现类型可能导致非常严重的问题,且很难发现:
2598
2599```cpp
2600// 编译正确,但new和delete不配套
2601std::unique_ptr<uint8_t> ptr(new uint8_t[10]);
2602std::unique_ptr<uint8_t[]> ptr(new uint8_t);
2603// 非异常安全: 编译器可能按如下顺序计算参数:
2604// 1. 分配 Foo 的内存,
2605// 2. 构造 Foo,
2606// 3. 调用 Bar,
2607// 4. 构造 unique_ptr<Foo>.
2608// 如果 Bar 抛出异常, Foo 不会被销毁,产生内存泄露。
2609F(unique_ptr<Foo>(new Foo()), Bar());
2610
2611// 异常安全: 调用函数不会被打断.
2612F(make_unique<Foo>(), Bar());
2613```
2614
2615**例外**
2616`std::make_unique`不支持自定义`deleter`。
2617在需要自定义`deleter`的场景,建议在自己的命名空间实现定制版本的`make_unique`。
2618使用`new`创建自定义`deleter`的`unique_ptr`是最后的选择。
2619
2620### <a name="r10-2-4"></a>规则10.2.4 使用`std::make_shared`而不是`new`创建`shared_ptr`
2621**理由**
2622使用`std::make_shared`除了类似`std::make_unique`一致性等原因外,还有性能的因素。
2623`std::shared_ptr`管理两个实体:
2624* 控制块(存储引用计数,`deleter`等)
2625* 管理对象
2626
2627`std::make_shared`创建`std::shared_ptr`,会一次性在堆上分配足够容纳控制块和管理对象的内存。而使用`std::shared_ptr<MyClass>(new MyClass)`创建`std::shared_ptr`,除了`new MyClass`会触发一次堆分配外,`std::shard_ptr`的构造函数还会触发第二次堆分配,产生额外的开销。
2628
2629**例外**
2630类似`std::make_unique`,`std::make_shared`不支持定制`deleter`
2631
2632## <a name="c10-3"></a> Lambda
2633### <a name="a10-3-1"></a>建议10.3.1 当函数不能工作时选择使用`lambda`(捕获局部变量,或编写局部函数)
2634**理由**
2635函数无法捕获局部变量或在局部范围内声明;如果需要这些东西,尽可能选择`lambda`,而不是手写的`functor`。
2636另一方面,`lambda`和`functor`不会重载;如果需要重载,则使用函数。
2637如果`lambda`和函数都可以的场景,则优先使用函数;尽可能使用最简单的工具。
2638
2639**示例**
2640
2641```cpp
2642// 编写一个只接受 int 或 string 的函数
2643// -- 重载是自然的选择
2644void F(int);
2645void F(const string&);
2646
2647// 需要捕获局部状态,或出现在语句或表达式范围
2648// -- lambda 是自然的选择
2649vector<Work> v = LotsOfWork();
2650for (int taskNum = 0; taskNum < max; ++taskNum) {
2651    pool.Run([=, &v] {...});
2652}
2653pool.Join();
2654```
2655
2656### <a name="r10-3-2"></a>规则10.3.1 非局部范围使用`lambdas`,避免使用按引用捕获
2657**理由**
2658非局部范围使用`lambdas`包括返回值,存储在堆上,或者传递给其它线程。局部的指针和引用不应该在它们的范围外存在。`lambdas`按引用捕获就是把局部对象的引用存储起来。如果这会导致超过局部变量生命周期的引用存在,则不应该按引用捕获。
2659
2660**示例**
2661
2662```cpp
2663// 不好
2664void Foo()
2665{
2666    int local = 42;
2667    // 按引用捕获 local.
2668    // 当函数返回后,local 不再存在,
2669    // 因此 Process() 的行为未定义!
2670    threadPool.QueueWork([&]{ Process(local); });
2671}
2672
2673// 好
2674void Foo()
2675{
2676    int local = 42;
2677    // 按值捕获 local。
2678    // 因为拷贝,Process() 调用过程中,local 总是有效的
2679    threadPool.QueueWork([=]{ Process(local); });
2680}
2681```
2682
2683### <a name="a10-3-2"></a>建议10.3.2 如果捕获`this`,则显式捕获所有变量
2684**理由**
2685在成员函数中的`[=]`看起来是按值捕获。但因为是隐式的按值获取了`this`指针,并能够操作所有成员变量,数据成员实际是按引用捕获的,一般情况下建议避免。如果的确需要这样做,明确写出对`this`的捕获。
2686
2687**示例**
2688
2689```cpp
2690class MyClass {
2691public:
2692    void Foo()
2693    {
2694        int i = 0;
2695
2696        auto Lambda = [=]() { Use(i, data_); };   // 不好: 看起来像是拷贝/按值捕获,成员变量实际上是按引用捕获
2697
2698        data_ = 42;
2699        Lambda(); // 调用 use(42);
2700        data_ = 43;
2701        Lambda(); // 调用 use(43);
2702
2703        auto Lambda2 = [i, this]() { Use(i, data_); }; // 好,显式指定按值捕获,最明确,最少的混淆
2704    }
2705
2706private:
2707    int data_ = 0;
2708};
2709```
2710
2711### <a name="a10-3-3"></a>建议10.3.3 避免使用默认捕获模式
2712**理由**
2713lambda表达式提供了两种默认捕获模式:按引用(&)和按值(=)。
2714默认按引用捕获会隐式的捕获所有局部变量的引用,容易导致访问悬空引用。相比之下,显式的写出需要捕获的变量可以更容易的检查对象生命周期,减小犯错可能。
2715默认按值捕获会隐式的捕获this指针,且难以看出lambda函数所依赖的变量是哪些。如果存在静态变量,还会让阅读者误以为lambda拷贝了一份静态变量。
2716因此,通常应当明确写出lambda需要捕获的变量,而不是使用默认捕获模式。
2717
2718**错误示例**
2719```cpp
2720auto func()
2721{
2722    int addend = 5;
2723    static int baseValue = 3;
2724
2725    return [=]() {  // 实际上只复制了addend
2726        ++baseValue;    // 修改会影响静态变量的值
2727        return baseValue + addend;
2728    };
2729}
2730```
2731
2732**正确示例**
2733```cpp
2734auto func()
2735{
2736    int addend = 5;
2737    static int baseValue = 3;
2738
2739    return [addend, baseValue = baseValue]() mutable {  // 使用C++14的捕获初始化拷贝一份变量
2740        ++baseValue;    // 修改自己的拷贝,不会影响静态变量的值
2741        return baseValue + addend;
2742    };
2743}
2744```
2745
2746参考:《Effective Modern C++》:Item 31: Avoid default capture modes.
2747
2748## <a name="c10-4"></a> 接口
2749### <a name="a10-4-1"></a>建议10.4.1 不涉及所有权的场景,使用`T*`或`T&`作为参数,而不是智能指针
2750**理由**
27511. 只在需要明确所有权机制时,才通过智能指针转移或共享所有权.
27522. 通过智能指针传递,限制了函数调用者必须使用智能指针(如调用者希望传递`this`)。
27533. 传递共享所有权的智能指针存在运行时的开销。
2754
2755**示例**
2756
2757```cpp
2758// 接受任何 int*
2759void F(int*);
2760
2761// 只能接受希望转移所有权的 int
2762void G(unique_ptr<int>);
2763
2764// 只能接受希望共享所有权的 int
2765void G(shared_ptr<int>);
2766
2767// 不改变所有权,但需要特定所有权的调用者
2768void H(const unique_ptr<int>&);
2769
2770// 接受任何 int
2771void H(int&);
2772
2773// 不好
2774void F(shared_ptr<Widget>& w)
2775{
2776    // ...
2777    Use(*w); // 只使用 w -- 完全不涉及生命周期管理
2778    // ...
2779};
2780```
2781
2782