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中可以提高代码可维护性,提高代码可靠性的相关约定。 185. 本规范优先适于用C++17版本。 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="r2-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="r2-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="r3-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 someCondition) { 421 DoSomething(); 422 ... 423} 424 425int result = reallyReallyLongVariableName1 + // Good 426 reallyReallyLongVariableName2; 427``` 428表达式换行后,注意保持合理对齐,或者4空格缩进。参考下面例子 429 430```cpp 431int sum = longVariableName1 + longVariableName2 + longVariableName3 + 432 longVariableName4 + longVariableName5 + longVariableName6; // Good: 4空格缩进 433 434int sum = longVariableName1 + longVariableName2 + longVariableName3 + 435 longVariableName4 + longVariableName5 + longVariableName6; // 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### <a name="r3-13-2"></a>规则3.13.2 避免使用宏 510宏会忽略作用域,类型系统以及各种规则,容易引发问题。应尽量避免使用宏定义,如果必须使用宏,要保证证宏名的唯一性。 511在C++中,有许多方式来避免使用宏: 512- 用const或enum定义易于理解的常量 513- 用namespace避免名字冲突 514- 用inline函数避免函数调用的开销 515- 用template函数来处理多种类型 516 517在文件头保护宏、条件编译、日志记录等必要场景中可以使用宏。 518 519### <a name="r3-13-3"></a>规则3.13.3 禁止使用宏来表示常量 520宏是简单的文本替换,在预处理阶段完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名; 宏没有类型检查,不安全; 宏没有作用域。 521 522### <a name="r3-13-4"></a>规则3.13.4 禁止使用函数式宏 523宏义函数式宏前,应考虑能否用函数替代。对于可替代场景,建议用函数替代宏。 524函数式宏的缺点如下: 525- 函数式宏缺乏类型检查,不如函数调用检查严格 526- 宏展开时宏参数不求值,可能会产生非预期结果 527- 宏没有独立的作用域 528- 宏的技巧性太强,例如#的用法和无处不在的括号,影响可读性 529- 在特定场景中必须用编译器对宏的扩展语法,如GCC的statement expression,影响可移植性 530- 宏在预编译阶段展开后,在期后编译、链接和调试时都不可见;而且包含多行的宏会展开为一行。函数式宏难以调试、难以打断点,不利于定位问题 531- 对于包含大量语句的宏,在每个调用点都要展开。如果调用点很多,会造成代码空间的膨胀 532 533函数没有宏的上述缺点。但是,函数相比宏,最大的劣势是执行效率不高(增加函数调用的开销和编译器优化的难度)。 534为此,可以在必要时使用内联函数。内联函数跟宏类似,也是在调用点展开。不同之处在于内联函数是在编译时展开。 535 536内联函数兼具函数和宏的优点: 537- 内联函数执行严格的类型检查 538- 内联函数的参数求值只会进行一次 539- 内联函数就地展开,没有函数调用的开销 540- 内联函数比函数优化得更好 541 542对于性能要求高的产品代码,可以考虑用内联函数代替函数。 543 544例外: 545在日志记录场景中,需要通过函数式宏保持调用点的文件名(__FILE__)、行号(__LINE__)等信息。 546 547## <a name="c3-14"></a> 空格和空行 548### <a name="r3-14-1"></a>规则3.14.1 水平空格应该突出关键字和重要信息,避免不必要的留白 549水平空格应该突出关键字和重要信息,每行代码尾部不要加空格。总体规则如下: 550 551- if, switch, case, do, while, for等关键字之后加空格; 552- 小括号内部的两侧,不要加空格; 553- 大括号内部两侧有无空格,左右必须保持一致; 554- 一元操作符(& * + ‐ ~ !)之后不要加空格; 555- 二元操作符(= + ‐ < > * / % | & ^ <= >= == != )左右两侧加空格 556- 三目运算符(? :)符号两侧均需要空格 557- 前置和后置的自增、自减(++ --)和变量之间不加空格 558- 结构体成员操作符(. ->)前后不加空格 559- 逗号(,)前面不加空格,后面增加空格 560- 对于模板和类型转换(<>)和类型之间不要添加空格 561- 域操作符(::)前后不要添加空格 562- 冒号(:)前后根据情况来判断是否要添加空格 563 564常规情况: 565```cpp 566void Foo(int b) { // Good:大括号前应该留空格 567 568int i = 0; // Good:变量初始化时,=前后应该有空格,分号前面不要留空格 569 570int buf[BUF_SIZE] = {0}; // Good:大括号内两侧都无空格 571``` 572 573函数定义和函数调用: 574```cpp 575int result = Foo(arg1,arg2); 576 ^ // Bad: 逗号后面需要增加空格 577 578int result = Foo( arg1, arg2 ); 579 ^ ^ // Bad: 函数参数列表的左括号后面不应该有空格,右括号前面不应该有空格 580``` 581 582指针和取地址 583```cpp 584x = *p; // Good:*操作符和指针p之间不加空格 585p = &x; // Good:&操作符和变量x之间不加空格 586x = r.y; // Good:通过.访问成员变量时不加空格 587x = r->y; // Good:通过->访问成员变量时不加空格 588``` 589 590操作符: 591```cpp 592x = 0; // Good:赋值操作的=前后都要加空格 593x = -5; // Good:负数的符号和数值之前不要加空格 594++x; // Good:前置和后置的++/--和变量之间不要加空格 595x--; 596 597if (x && !y) // Good:布尔操作符前后要加上空格,!操作和变量之间不要空格 598v = w * x + y / z; // Good:二元操作符前后要加空格 599v = w * (x + z); // Good:括号内的表达式前后不需要加空格 600 601int a = (x < y) ? x : y; // Good: 三目运算符, ?和:前后需要添加空格 602``` 603 604循环和条件语句: 605```cpp 606if (condition) { // Good:if关键字和括号之间加空格,括号内条件语句前后不加空格 607 ... 608} else { // Good:else关键字和大括号之间加空格 609 ... 610} 611 612while (condition) {} // Good:while关键字和括号之间加空格,括号内条件语句前后不加空格 613 614for (int i = 0; i < someRange; ++i) { // Good:for关键字和括号之间加空格,分号之后加空格 615 ... 616} 617 618switch (condition) { // Good: switch 关键字后面有1空格 619 case 0: // Good:case语句条件和冒号之间不加空格 620 ... 621 break; 622 ... 623 default: 624 ... 625 break; 626} 627``` 628 629模板和转换 630```cpp 631// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有. 632vector<string> x; 633y = static_cast<char*>(x); 634 635// 在类型与指针操作符之间留空格也可以, 但要保持一致. 636vector<char *> x; 637``` 638 639域操作符 640```cpp 641std::cout; // Good: 命名空间访问,不要留空格 642 643int MyClass::GetValue() const {} // Good: 对于成员函数定义,不要留空格 644``` 645 646冒号 647```cpp 648// 添加空格的场景 649 650// Good: 类的派生需要留有空格 651class Sub : public Base { 652 653}; 654 655// 构造函数初始化列表需要留有空格 656MyClass::MyClass(int var) : someVar_(var) 657{ 658 DoSomething(); 659} 660 661// 位域表示也留有空格 662struct XX { 663 char a : 4; 664 char b : 5; 665 char c : 4; 666}; 667``` 668 669```cpp 670// 不添加空格的场景 671 672// Good: 对于public:, private:这种类访问权限的冒号不用添加空格 673class MyClass { 674public: 675 MyClass(int var); 676private: 677 int someVar_; 678}; 679 680// 对于switch-case的case和default后面的冒号不用添加空格 681switch (value) 682{ 683 case 1: 684 DoSomething(); 685 break; 686 default: 687 break; 688} 689``` 690 691注意:当前的集成开发环境(IDE)可以设置删除行尾的空格,请正确配置。 692 693### <a name="a3-14-1"></a>建议3.14.1 合理安排空行,保持代码紧凑 694 695减少不必要的空行,可以显示更多的代码,方便代码阅读。下面有一些建议遵守的规则: 696- 根据上下内容的相关程度,合理安排空行; 697- 函数内部、类型定义内部、宏内部、初始化表达式内部,不使用连续空行 698- 不使用连续 **3** 个空行,或更多 699- 大括号内的代码块行首之前和行尾之后不要加空行,但namespace的大括号内不作要求。 700 701```cpp 702int Foo() 703{ 704 ... 705} 706 707 708 709int Bar() // Bad:最多使用连续2个空行。 710{ 711 ... 712} 713 714 715if (...) { 716 // Bad:大括号内的代码块行首不要加入空行 717 ... 718 // Bad:大括号内的代码块行尾不要加入空行 719} 720 721int Foo(...) 722{ 723 // Bad:函数体内行首不要加空行 724 ... 725} 726``` 727 728## <a name="c3-15"></a> 类 729### <a name="r3-15-1"></a>规则3.15.1 类访问控制块的声明依次序是 public:, protected:, private:,缩进和 class 关键字对齐 730```cpp 731class MyClass : public BaseClass { 732public: // 注意没有缩进 733 MyClass(); // 标准的4空格缩进 734 explicit MyClass(int var); 735 ~MyClass() {} 736 737 void SomeFunction(); 738 void SomeFunctionThatDoesNothing() 739 { 740 } 741 742 void SetVar(int var) { someVar_ = var; } 743 int GetVar() const { return someVar_; } 744 745private: 746 bool SomeInternalFunction(); 747 748 int someVar_; 749 int someOtherVar_; 750}; 751``` 752 753在各个部分中,建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它成员函数, 数据成员。 754 755 756### <a name="r3-15-2"></a>规则3.15.2 构造函数初始化列表放在同一行或按四格缩进并排多行 757```cpp 758// 如果所有变量能放在同一行: 759MyClass::MyClass(int var) : someVar_(var) 760{ 761 DoSomething(); 762} 763 764// 如果不能放在同一行, 765// 必须置于冒号后, 并缩进4个空格 766MyClass::MyClass(int var) 767 : someVar_(var), someOtherVar_(var + 1) // Good: 逗号后面留有空格 768{ 769 DoSomething(); 770} 771 772// 如果初始化列表需要置于多行, 需要逐行对齐 773MyClass::MyClass(int var) 774 : someVar_(var), // 缩进4个空格 775 someOtherVar_(var + 1) 776{ 777 DoSomething(); 778} 779``` 780 781# <a name="c4"></a>4 注释 782一般的,尽量通过清晰的架构逻辑,好的符号命名来提高代码可读性;需要的时候,才辅以注释说明。 783注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,**按需注释**。 784 785注释内容要简洁、明了、无二义性,信息全面且不冗余。 786 787**注释跟代码一样重要。** 788写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。 789修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。 790 791使用英文进行注释。 792 793## <a name="c3-1"></a>注释风格 794 795在 C++ 代码中,使用 `/*` `*/`和 `//` 都是可以的。 796按注释的目的和位置,注释可分为不同的类型,如文件头注释、函数头注释、代码注释等等; 797同一类型的注释应该保持统一的风格。 798 799注意:本文示例代码中,大量使用 '//' 后置注释只是为了更精确的描述问题,并不代表这种注释风格更好。 800 801## <a name="c4-2"></a> 文件头注释 802### <a name="r3-1"></a>规则3.1 文件头注释必须包含版权许可 803 804/* 805 * Copyright (c) 2020 XXX 806 * Licensed under the Apache License, Version 2.0 (the "License"); 807 * you may not use this file except in compliance with the License. 808 * You may obtain a copy of the License at 809 * 810 * http://www.apache.org/licenses/LICENSE-2.0 811 * 812 * Unless required by applicable law or agreed to in writing, software 813 * distributed under the License is distributed on an "AS IS" BASIS, 814 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 815 * See the License for the specific language governing permissions and 816 * limitations under the License. 817 */ 818 819## <a name="c4-3"></a> 函数头注释 820### <a name="r4-3-1"></a>规则4.3.1 公有(public)函数必须编写函数头注释 821公有函数属于类对外提供的接口,调用者需要了解函数的功能、参数的取值范围、返回的结果、注意事项等信息才能正常使用。 822特别是参数的取值范围、返回的结果、注意事项等都无法做到自注示,需要编写函数头注释辅助说明。 823 824### <a name="r4-3-2"></a>规则4.3.2 禁止空有格式的函数头注释 825并不是所有的函数都需要函数头注释; 826函数签名无法表达的信息,加函数头注释辅助说明; 827 828函数头注释统一放在函数声明或定义上方,使用如下风格之一: 829使用`//`写函数头 830 831```cpp 832// 单行函数头 833int Func1(void); 834 835// 多行函数头 836// 第二行 837int Func2(void); 838``` 839 840使用`/* */`写函数头 841```cpp 842/* 单行函数头 */ 843int Func1(void); 844 845/* 846 * 另一种单行函数头 847 */ 848int Func2(void); 849 850/* 851 * 多行函数头 852 * 第二行 853 */ 854int Func3(void); 855``` 856函数尽量通过函数名自注释,按需写函数头注释。 857不要写无用、信息冗余的函数头;不要写空有格式的函数头。 858 859函数头注释内容可选,但不限于:功能说明、返回值,性能约束、用法、内存约定、算法实现、可重入的要求等等。 860模块对外头文件中的函数接口声明,其函数头注释,应当将重要、有用的信息表达清楚。 861 862例: 863 864```cpp 865/* 866 * 返回实际写入的字节数,-1表示写入失败 867 * 注意,内存 buf 由调用者负责释放 868 */ 869int WriteString(const char *buf, int len); 870``` 871 872坏的例子: 873```cpp 874/* 875 * 函数名:WriteString 876 * 功能:写入字符串 877 * 参数: 878 * 返回值: 879 */ 880int WriteString(const char *buf, int len); 881``` 882上面例子中的问题: 883 884- 参数、返回值,空有格式没内容 885- 函数名信息冗余 886- 关键的 buf 由谁释放没有说清楚 887 888## <a name="c4-4"></a> 代码注释 889### <a name="r4-4-1"></a>规则4.4.1 代码注释放于对应代码的上方或右边 890### <a name="r4-4-2"></a>规则4.4.2 注释符与注释内容间要有1空格;右置注释与前面代码至少1空格 891代码上方的注释,应该保持对应代码一样的缩进。 892选择并统一使用如下风格之一: 893使用`//` 894```cpp 895 896// 这是单行注释 897DoSomething(); 898 899// 这是多行注释 900// 第二行 901DoSomething(); 902``` 903 904使用`/*' '*/` 905```cpp 906/* 这是单行注释 */ 907DoSomething(); 908 909/* 910 * 另一种方式的多行注释 911 * 第二行 912 */ 913DoSomething(); 914``` 915代码右边的注释,与代码之间,至少留1空格,建议不超过4空格。 916通常使用扩展后的 TAB 键即可实现 1-4 空格的缩进。 917 918选择并统一使用如下风格之一: 919 920```cpp 921int foo = 100; // 放右边的注释 922int bar = 200; /* 放右边的注释 */ 923``` 924右置格式在适当的时候,上下对齐会更美观。 925对齐后的注释,离左边代码最近的那一行,保证1-4空格的间隔。 926例: 927 928```cpp 929const int A_CONST = 100; /* 相关的同类注释,可以考虑上下对齐 */ 930const int ANOTHER_CONST = 200; /* 上下对齐时,与左侧代码保持间隔 */ 931``` 932当右置的注释超过行宽时,请考虑将注释置于代码上方。 933 934### <a name="r4-4-3"></a>规则4.4.3 不用的代码段直接删除,不要注释掉 935被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入易被忽略的缺陷。 936正确的做法是,不需要的代码直接删除掉。若再需要时,考虑移植或重写这段代码。 937 938这里说的注释掉代码,包括用 /* */ 和 //,还包括 #if 0, #ifdef NEVER_DEFINED 等等。 939 940# <a name="c5"></a>5 头文件 941## <a name="c5-1"></a> 头文件职责 942头文件是模块或文件的对外接口,头文件的设计体现了大部分的系统设计。 943头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。对于cpp文件中内部才需要使用的函数、宏、枚举、结构定义等不要放在头文件中。 944头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。 945 946### <a name="a5-1-1"></a>建议5.1.1 每一个.cpp文件应有一个对应的.h文件,用于声明需要对外公开的类与接口 947通常情况下,每个.cpp文件都有一个相应的.h,用于放置对外提供的函数声明、宏定义、类型定义等。 948如果一个.cpp文件不需要对外公布任何接口,则其就不应当存在。 949例外:__程序的入口(如main函数所在的文件),单元测试代码,动态库代码。__ 950 951示例: 952```cpp 953// Foo.h 954 955#ifndef FOO_H 956#define FOO_H 957 958class Foo { 959public: 960 Foo(); 961 void Fun(); 962 963private: 964 int value_; 965}; 966 967#endif 968``` 969 970```cpp 971// Foo.cpp 972#include "Foo.h" 973 974namespace { // Good: 对内函数的声明放在.cpp文件的头部,并声明为匿名namespace或者static限制其作用域 975 void Bar() 976 { 977 } 978} 979 980... 981 982void Foo::Fun() 983{ 984 Bar(); 985} 986``` 987 988## <a name="c5-2"></a> 头文件依赖 989### <a name="r5-2-1"></a>规则5.2.1 禁止头文件循环依赖 990头文件循环依赖,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h, 导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。 991而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。 992 993头文件循环依赖直接体现了架构设计上的不合理,可通过优化架构去避免。 994 995 996### <a name="r5-2-4"></a>规则5.2.2 头文件必须编写`#define`保护,防止重复包含 997为防止头文件被重复包含,所有头文件都应当使用 #define 保护;不要使用 #pragma once 998 999定义包含保护符时,应该遵守如下规则: 10001)保护符使用唯一名称; 10012)不要在受保护部分的前后放置代码或者注释,文件头注释除外。 1002 1003示例:假定timer模块的timer.h,其目录为timer/include/timer.h,应按如下方式保护: 1004 1005```cpp 1006#ifndef TIMER_INCLUDE_TIMER_H 1007#define TIMER_INCLUDE_TIMER_H 1008... 1009#endif 1010``` 1011 1012### <a name="r5-2-5"></a>规则5.2.3 禁止通过声明的方式引用外部函数接口、变量 1013只能通过包含头文件的方式使用其他模块或文件提供的接口。 1014通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。 1015同时这种隐式依赖,容易导致架构腐化。 1016 1017不符合规范的案例: 1018 1019// a.cpp内容 1020```cpp 1021extern int Fun(); // Bad: 通过extern的方式使用外部函数 1022 1023void Bar() 1024{ 1025 int i = Fun(); 1026 ... 1027} 1028``` 1029 1030// b.cpp内容 1031```cpp 1032int Fun() 1033{ 1034 // Do something 1035} 1036``` 1037应该改为: 1038 1039// a.cpp内容 1040```cpp 1041#include "b.h" // Good: 通过包含头文件的方式使用其他.cpp提供的接口 1042 1043void Bar() 1044{ 1045 int i = Fun(); 1046 ... 1047} 1048``` 1049 1050// b.h内容 1051```cpp 1052int Fun(); 1053``` 1054 1055// b.cpp内容 1056```cpp 1057int Fun() 1058{ 1059 // Do something 1060} 1061``` 1062例外,有些场景需要引用其内部函数,但并不想侵入代码时,可以 extern 声明方式引用。 1063如: 1064针对某一内部函数进行单元测试时,可以通过 extern 声明来引用被测函数; 1065当需要对某一函数进行打桩、打补丁处理时,允许 extern 声明该函数。 1066 1067### <a name="r5-2-6"></a>规则5.2.4 禁止在extern "C"中包含头文件 1068在 extern "C" 中包含头文件,有可能会导致 extern "C" 嵌套,部分编译器对 extern "C" 嵌套层次有限制,嵌套层次太多会编译错误。 1069 1070在C,C++混合编程的情况下,在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改。 1071 1072示例,存在a.h和b.h两个头文件: 1073 1074// a.h内容 1075```cpp 1076... 1077#ifdef __cplusplus 1078void Foo(int); 1079#define A(value) Foo(value) 1080#else 1081void A(int) 1082#endif 1083``` 1084// b.h内容 1085```cpp 1086... 1087#ifdef __cplusplus 1088extern "C" { 1089#endif 1090 1091#include "a.h" 1092void B(); 1093 1094#ifdef __cplusplus 1095} 1096#endif 1097``` 1098 1099使用C++预处理器展开b.h,将会得到 1100```cpp 1101extern "C" { 1102 void Foo(int); 1103 void B(); 1104} 1105``` 1106 1107按照 a.h 作者的本意,函数 Foo 是一个 C++ 自由函数,其链接规范为 "C++"。 1108但在 b.h 中,由于 `#include "a.h"` 被放到了 `extern "C"` 的内部,函数 Foo 的链接规范被不正确地更改了。 1109 1110例外: 1111如果在 C++ 编译环境中,想引用纯C的头文件,这些C头文件并没有` extern "C"` 修饰。非侵入式的做法是,在 `extern "C"` 中去包含C头文件。 1112 1113### <a name="a5-2-1"></a>建议5.2.1尽量避免使用前置声明,而是通过`#include`来包含头文件 1114前置声明(forward declaration)通常指类、模板的纯粹声明,没伴随着其定义。 1115 1116- 优点: 1117 1. 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。 1118 2. 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。 1119- 缺点: 1120 1. 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。 1121 2. 前置声明可能会被库的后续更改所破坏。前置声明模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。 1122 3. 前置声明来自命名空间` std::` 的 symbol 时,其行为未定义(在C++11标准规范中明确说明)。 1123 4. 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。 1124 5. 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。 1125 6. 很难判断什么时候该用前置声明,什么时候该用`#include`,某些场景下面前置声明和`#include`互换以后会导致意想不到的结果。 1126 1127所以我们尽可能避免使用前置声明,而是使用#include头文件来保证依赖关系。 1128 1129# <a name="c6"></a>6 作用域 1130 1131## <a name="c6-1"></a> 命名空间 1132 1133### <a name="a6-1-1"></a>建议6.1.1 对于cpp文件中不需要导出的变量,常量或者函数,请使用匿名namespace封装或者用static修饰 1134在C++ 2003标准规范中,使用static修饰文件作用域的变量,函数等被标记为deprecated特性,所以更推荐使用匿名namespace。 1135 1136主要原因如下: 11371. static在C++中已经赋予了太多的含义,静态函数成员变量,静态成员函数,静态全局变量,静态函数局部变量,每一种都有特殊的处理。 11382. static只能保证变量,常量和函数的文件作用域,但是namespace还可以封装类型等。 11393. 统一namespace来处理C++的作用域,而不需要同时使用static和namespace来管理。 11404. static修饰的函数不能用来实例化模板,而匿名namespace可以。 1141 1142但是不要在 .h 中使用中使用匿名namespace或者static。 1143 1144```cpp 1145// Foo.cpp 1146 1147namespace { 1148 const int MAX_COUNT = 20; 1149 void InternalFun() {}; 1150} 1151 1152void Foo::Fun() 1153{ 1154 int i = MAX_COUNT; 1155 1156 InternalFun(); 1157} 1158 1159``` 1160 1161### <a name="r6-1-1"></a>规则6.1.1 不要在头文件中或者#include之前使用using导入命名空间 1162说明:使用using导入命名空间会影响后续代码,易造成符号冲突,所以不要在头文件以及源文件中的#include之前使用using导入命名空间。 1163示例: 1164 1165```cpp 1166// 头文件a.h 1167namespace NamespaceA { 1168 int Fun(int); 1169} 1170``` 1171 1172```cpp 1173// 头文件b.h 1174namespace NamespaceB { 1175 int Fun(int); 1176} 1177 1178using namespace NamespaceB; 1179 1180void G() 1181{ 1182 Fun(1); 1183} 1184``` 1185 1186```cpp 1187// 源代码a.cpp 1188#include "a.h" 1189using namespace NamespaceA; 1190#include "b.h" 1191 1192void main() 1193{ 1194 G(); // using namespace NamespaceA在#include “b.h”之前,引发歧义:NamespaceA::Fun,NamespaceB::Fun调用不明确 1195} 1196``` 1197 1198对于在头文件中使用using导入单个符号或定义别名,允许在模块自定义名字空间中使用,但禁止在全局名字空间中使用。 1199```cpp 1200// foo.h 1201 1202#include <fancy/string> 1203using fancy::string; // Bad,禁止向全局名字空间导入符号 1204 1205namespace Foo { 1206 using fancy::string; // Good,可以在模块自定义名字空间中导入符号 1207 using MyVector = fancy::vector<int>; // Good,C++11可在自定义名字空间中定义别名 1208} 1209``` 1210 1211 1212## <a name="c6-2"></a> 全局函数和静态成员函数 1213 1214### <a name="a6-2-1"></a>建议6.2.1 优先使用命名空间来管理全局函数,如果和某个class有直接关系的,可以使用静态成员函数 1215说明:非成员函数放在名字空间内可避免污染全局作用域, 也不要用类+静态成员方法来简单管理全局函数。 如果某个全局函数和某个类有紧密联系, 那么可以作为类的静态成员函数。 1216 1217如果你需要定义一些全局函数,给某个cpp文件使用,那么请使用匿名namespace来管理。 1218```cpp 1219namespace MyNamespace { 1220 int Add(int a, int b); 1221} 1222 1223class File { 1224public: 1225 static File CreateTempFile(const std::string& fileName); 1226}; 1227``` 1228 1229## <a name="c6-3"></a> 全局常量和静态成员常量 1230 1231### <a name="a6-3-1"></a>建议6.3.1 优先使用命名空间来管理全局常量,如果和某个class有直接关系的,可以使用静态成员常量 1232说明:全局常量放在命名空间内可避免污染全局作用域, 也不要用类+静态成员常量来简单管理全局常量。 如果某个全局常量和某个类有紧密联系, 那么可以作为类的静态成员常量。 1233 1234如果你需要定义一些全局常量,只给某个cpp文件使用,那么请使用匿名namespace来管理。 1235```cpp 1236namespace MyNamespace { 1237 const int MAX_SIZE = 100; 1238} 1239 1240class File { 1241public: 1242 static const std::string SEPARATOR; 1243}; 1244``` 1245 1246## <a name="c6-4"></a> 全局变量 1247 1248### <a name="a6-4-1"></a>建议6.4.1 尽量避免使用全局变量,考虑使用单例模式 1249说明:全局变量是可以修改和读取的,那么这样会导致业务代码和这个全局变量产生数据耦合。 1250```cpp 1251int g_counter = 0; 1252 1253// a.cpp 1254g_counter++; 1255 1256// b.cpp 1257g_counter++; 1258 1259// c.cpp 1260cout << g_counter << endl; 1261``` 1262 1263使用单实例模式 1264```cpp 1265class Counter { 1266public: 1267 static Counter& GetInstance() 1268 { 1269 static Counter counter; 1270 return counter; 1271 } // 单实例实现简单举例 1272 1273 void Increase() 1274 { 1275 value_++; 1276 } 1277 1278 void Print() const 1279 { 1280 std::cout << value_ << std::endl; 1281 } 1282 1283private: 1284 Counter() : value_(0) {} 1285 1286private: 1287 int value_; 1288}; 1289 1290// a.cpp 1291Counter::GetInstance().Increase(); 1292 1293// b.cpp 1294Counter::GetInstance().Increase(); 1295 1296// c.cpp 1297Counter::GetInstance().Print(); 1298``` 1299 1300实现单例模式以后,实现了全局唯一一个实例,和全局变量同样的效果,并且单实例提供了更好的封装性。 1301 1302例外:有的时候全局变量的作用域仅仅是模块内部,这样进程空间里面就会有多个全局变量实例,每个模块持有一份,这种场景下是无法使用单例模式解决的。 1303 1304# <a name="c7"></a>7 类 1305 1306## <a name="c7-1"></a> 构造,拷贝构造,赋值和析构函数 1307构造,拷贝,移动和析构函数提供了对象的生命周期管理方法: 1308- 构造函数(constructor): `X()` 1309- 拷贝构造函数(copy constructor):`X(const X&)` 1310- 拷贝赋值操作符(copy assignment):`operator=(const X&)` 1311- 移动构造函数(move constructor):`X(X&&)` *C++11以后提供* 1312- 移动赋值操作符(move assignment):`operator=(X&&)` *C++11以后提供* 1313- 析构函数(destructor):`~X()` 1314 1315### <a name="r7-1-1"></a>规则7.1.1 类的成员变量必须显式初始化 1316说明:如果类有成员变量,没有定义构造函数,又没有定义默认构造函数,编译器将自动生成一个构造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。 1317 1318例外: 1319- 如果类的成员变量具有默认构造函数,那么可以不需要显式初始化。 1320 1321示例:如下代码没有构造函数,私有数据成员无法初始化: 1322```cpp 1323class Message { 1324public: 1325 void ProcessOutMsg() 1326 { 1327 //… 1328 } 1329 1330private: 1331 unsigned int msgID_; 1332 unsigned int msgLength_; 1333 unsigned char* msgBuffer_; 1334 std::string someIdentifier_; 1335}; 1336 1337Message message; // message成员变量没有初始化 1338message.ProcessOutMsg(); // 后续使用存在隐患 1339 1340// 因此,有必要定义默认构造函数,如下: 1341class Message { 1342public: 1343 Message() : msgID_(0), msgLength_(0), msgBuffer_(nullptr) 1344 { 1345 } 1346 1347 void ProcessOutMsg() 1348 { 1349 // … 1350 } 1351 1352private: 1353 unsigned int msgID_; 1354 unsigned int msgLength_; 1355 unsigned char* msgBuffer_; 1356 std::string someIdentifier_; // 具有默认构造函数,不需要显式初始化 1357}; 1358``` 1359 1360### <a name="a7-1-1"></a>建议7.1.1 成员变量优先使用声明时初始化(C++11)和构造函数初始化列表初始化 1361说明:C++11的声明时初始化可以一目了然的看出成员初始值,应当优先使用。如果成员初始化值和构造函数相关,或者不支持C++11,则应当优先使用构造函数初始化列表来初始化成员。相比起在构造函数体中对成员赋值,初始化列表的代码更简洁,执行性能更好,而且可以对const成员和引用成员初始化。 1362 1363```cpp 1364class Message { 1365public: 1366 Message() : msgLength_(0) // Good,优先使用初始化列表 1367 { 1368 msgBuffer_ = nullptr; // Bad,不推荐在构造函数中赋值 1369 } 1370 1371private: 1372 unsigned int msgID_{0}; // Good,C++11中使用 1373 unsigned int msgLength_; 1374 unsigned char* msgBuffer_; 1375}; 1376``` 1377 1378### <a name="r7-1-2"></a>规则7.1.2 为避免隐式转换,将单参数构造函数声明为explicit 1379说明:单参数构造函数如果没有用explicit声明,则会成为隐式转换函数。 1380示例: 1381 1382```cpp 1383class Foo { 1384public: 1385 explicit Foo(const string& name): name_(name) 1386 { 1387 } 1388private: 1389 string name_; 1390}; 1391 1392 1393void ProcessFoo(const Foo& foo){} 1394 1395int main(void) 1396{ 1397 std::string test = "test"; 1398 ProcessFoo(test); // 编译不通过 1399 return 0; 1400} 1401``` 1402 1403上面的代码编译不通过,因为`ProcessFoo`需要的参数是Foo类型,传入的string类型不匹配。 1404 1405如果将Foo构造函数的explicit关键字移除,那么调用`ProcessFoo`传入的string就会触发隐式转换,生成一个临时的Foo对象。往往这种隐式转换是让人迷惑的,并且容易隐藏Bug,得到了一个不期望的类型转换。所以对于单参数的构造函数是要求explicit声明。 1406 1407### <a name="r7-1-3"></a>规则7.1.3 如果不需要拷贝构造函数、赋值操作符 / 移动构造函数、赋值操作符,请明确禁止 1408说明:如果用户不定义,编译器默认会生成拷贝构造函数和拷贝赋值操作符, 移动构造和移动赋值操作符(移动语义的函数C++11以后才有)。 1409如果我们不要使用拷贝构造函数,或者赋值操作符,请明确拒绝: 1410 14111. 将拷贝构造函数或者赋值操作符设置为private,并且不实现: 1412```cpp 1413class Foo { 1414private: 1415 Foo(const Foo&); 1416 Foo& operator=(const Foo&); 1417}; 1418``` 14192. 使用C++11提供的delete, 请参见后面现代C++的相关章节。 1420 1421 14223. 推荐继承NoCopyable、NoMovable,禁止使用DISALLOW_COPY_AND_MOVE,DISALLOW_COPY,DISALLOW_MOVE等宏。 1423```cpp 1424class Foo : public NoCopyable, public NoMovable { 1425}; 1426``` 1427NoCopyable和NoMovable的实现: 1428```cpp 1429class NoCopyable { 1430public: 1431 NoCopyable() = default; 1432 NoCopyable(const NoCopyable&) = delete; 1433 NoCopyable& operator = (NoCopyable&) = delete; 1434}; 1435 1436class NoMovable { 1437public: 1438 NoMovable() = default; 1439 NoMovable(NoMovable&&) noexcept = delete; 1440 NoMovable& operator = (NoMovable&&) noexcept = delete; 1441}; 1442``` 1443 1444### <a name="r7-1-4"></a>规则7.1.4 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止 1445拷贝构造函数和拷贝赋值操作符都是具有拷贝语义的,应该同时出现或者禁止。 1446 1447```cpp 1448// 同时出现 1449class Foo { 1450public: 1451 ... 1452 Foo(const Foo&); 1453 Foo& operator=(const Foo&); 1454 ... 1455}; 1456 1457// 同时default, C++11支持 1458class Foo { 1459public: 1460 Foo(const Foo&) = default; 1461 Foo& operator=(const Foo&) = default; 1462}; 1463 1464// 同时禁止, C++11可以使用delete 1465class Foo { 1466private: 1467 Foo(const Foo&); 1468 Foo& operator=(const Foo&); 1469}; 1470``` 1471 1472### <a name="r7-1-5"></a>规则7.1.5 移动构造和移动赋值操作符应该是成对出现或者禁止 1473在C++11中增加了move操作,如果需要某个类支持移动操作,那么需要实现移动构造和移动赋值操作符。 1474 1475移动构造函数和移动赋值操作符都是具有移动语义的,应该同时出现或者禁止。 1476```cpp 1477// 同时出现 1478class Foo { 1479public: 1480 ... 1481 Foo(Foo&&); 1482 Foo& operator=(Foo&&); 1483 ... 1484}; 1485 1486// 同时default, C++11支持 1487class Foo { 1488public: 1489 Foo(Foo&&) = default; 1490 Foo& operator=(Foo&&) = default; 1491}; 1492 1493// 同时禁止, 使用C++11的delete 1494class Foo { 1495public: 1496 Foo(Foo&&) = delete; 1497 Foo& operator=(Foo&&) = delete; 1498}; 1499``` 1500 1501### <a name="r7-1-6"></a>规则7.1.6 禁止在构造函数和析构函数中调用虚函数 1502说明:在构造函数和析构函数中调用当前对象的虚函数,会导致未实现多态的行为。 1503在C++中,一个基类一次只构造一个完整的对象。 1504 1505示例:类Base是基类,Sub是派生类 1506```cpp 1507class Base { 1508public: 1509 Base(); 1510 virtual void Log() = 0; // 不同的派生类调用不同的日志文件 1511}; 1512 1513Base::Base() // 基类构造函数 1514{ 1515 Log(); // 调用虚函数Log 1516} 1517 1518class Sub : public Base { 1519public: 1520 virtual void Log(); 1521}; 1522``` 1523 1524当执行如下语句: 1525`Sub sub;` 1526会先执行Sub的构造函数,但首先调用Base的构造函数,由于Base的构造函数调用虚函数Log,此时Log还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为。 1527同样的道理也适用于析构函数。 1528 1529### <a name="r7-1-7"></a>规则7.1.7 多态基类中的拷贝构造函数、拷贝赋值操作符、移动构造函数、移动赋值操作符必须为非public函数或者为delete函数 1530如果报一个派生类对象直接赋值给基类对象,会发生切片,只拷贝或者移动了基类部分,损害了多态行为。 1531【反例】 1532如下代码中,基类没有定义拷贝构造函数或拷贝赋值操作符,编译器会自动生成这两个特殊成员函数, 1533如果派生类对象赋值给基类对象时就发生切片。可以将此例中的拷贝构造函数和拷贝赋值操作符声明为delete,编译器可检查出此类赋值行为。 1534```cpp 1535class Base { 1536public: 1537 Base() = default; 1538 virtual ~Base() = default; 1539 ... 1540 virtual void Fun() { std::cout << "Base" << std::endl;} 1541}; 1542 1543class Derived : public Base { 1544 ... 1545 void Fun() override { std::cout << "Derived" << std::endl; } 1546}; 1547 1548void Foo(const Base &base) 1549{ 1550 Base other = base; // 不符合:发生切片 1551 other.Fun(); // 调用的时Base类的Fun函数 1552} 1553``` 1554```cpp 1555Derived d; 1556Foo(d); // 传入的是派生类对象 1557``` 15581. 将拷贝构造函数或者赋值操作符设置为private,并且不实现: 1559 1560## <a name="c7-2"></a> 继承 1561 1562### <a name="r7-2-1"></a>规则7.2.1 基类的析构函数应该声明为virtual,不准备被继承的类需要声明为final 1563说明:只有基类析构函数是virtual,通过多态调用的时候才能保证派生类的析构函数被调用。 1564 1565示例:基类的析构函数没有声明为virtual导致了内存泄漏。 1566```cpp 1567class Base { 1568public: 1569 virtual std::string getVersion() = 0; 1570 1571 ~Base() 1572 { 1573 std::cout << "~Base" << std::endl; 1574 } 1575}; 1576``` 1577 1578```cpp 1579class Sub : public Base { 1580public: 1581 Sub() : numbers_(nullptr) 1582 { 1583 } 1584 1585 ~Sub() 1586 { 1587 delete[] numbers_; 1588 std::cout << "~Sub" << std::endl; 1589 } 1590 1591 int Init() 1592 { 1593 const size_t numberCount = 100; 1594 numbers_ = new (std::nothrow) int[numberCount]; 1595 if (numbers_ == nullptr) { 1596 return -1; 1597 } 1598 1599 ... 1600 } 1601 1602 std::string getVersion() 1603 { 1604 return std::string("hello!"); 1605 } 1606private: 1607 int* numbers_; 1608}; 1609``` 1610 1611```cpp 1612int main(int argc, char* args[]) 1613{ 1614 Base* b = new Sub(); 1615 1616 delete b; 1617 return 0; 1618} 1619``` 1620由于基类Base的析构函数没有声明为virtual,当对象被销毁时,只会调用基类的析构函数,不会调用派生类Sub的析构函数,导致内存泄漏。 1621例外: 1622NoCopyable、NoMovable这种没有任何行为,仅仅用来做标识符的类,可以不定义虚析构也不定义final。 1623 1624### <a name="r7-2-2"></a>规则7.2.2 禁止虚函数使用缺省参数值 1625说明:在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。为了避免虚函数重载时,因参数声明不一致给使用者带来的困惑和由此导致的问题,规定所有虚函数均不允许声明缺省参数值。 1626示例:虚函数display缺省参数值text是由编译时刻决定的,而非运行时刻,没有达到多态的目的: 1627```cpp 1628class Base { 1629public: 1630 virtual void Display(const std::string& text = "Base!") 1631 { 1632 std::cout << text << std::endl; 1633 } 1634 1635 virtual ~Base(){} 1636}; 1637 1638class Sub : public Base { 1639public: 1640 virtual void Display(const std::string& text = "Sub!") 1641 { 1642 std::cout << text << std::endl; 1643 } 1644 1645 virtual ~Sub(){} 1646}; 1647 1648int main() 1649{ 1650 Base* base = new Sub(); 1651 Sub* sub = new Sub(); 1652 1653 ... 1654 1655 base->Display(); // 程序输出结果: Base! 而期望输出:Sub! 1656 sub->Display(); // 程序输出结果: Sub! 1657 1658 delete base; 1659 delete sub; 1660 return 0; 1661}; 1662``` 1663 1664### <a name="r7-2-3"></a>规则7.2.3 禁止重新定义继承而来的非虚函数 1665说明:因为非虚函数无法实现动态绑定,只有虚函数才能实现动态绑定:只要操作基类的指针,即可获得正确的结果。 1666 1667示例: 1668```cpp 1669class Base { 1670public: 1671 void Fun(); 1672}; 1673 1674class Sub : public Base { 1675public: 1676 void Fun(); 1677}; 1678 1679Sub* sub = new Sub(); 1680Base* base = sub; 1681 1682sub->Fun(); // 调用子类的Fun 1683base->Fun(); // 调用父类的Fun 1684//... 1685 1686``` 1687 1688## <a name="c7-3"></a> 多重继承 1689在实际开发过程中使用多重继承的场景是比较少的,因为多重继承使用过程中有下面的典型问题: 16901. 菱形继承所带来的数据重复,以及名字二义性。因此,C++引入了virtual继承来解决这类问题; 16912. 即便不是菱形继承,多个父类之间的名字也可能存在冲突,从而导致的二义性; 16923. 如果子类需要扩展或改写多个父类的方法时,造成子类的职责不明,语义混乱; 16934. 相对于委托,继承是一种白盒复用,即子类可以访问父类的protected成员, 这会导致更强的耦合。而多重继承,由于耦合了多个父类,相对于单根继承,这会产生更强的耦合关系。 1694 1695多重继承具有下面的优点: 1696多重继承提供了一种更简单的组合来实现多种接口或者类的组装与复用。 1697 1698所以,对于多重继承的只有下面几种情况下面才允许使用多重继承。 1699 1700### <a name="a7-3-1"></a>建议7.3.1 使用多重继承来实现接口分离与多角色组合 1701如果某个类需要实现多重接口,可以通过多重继承把多个分离的接口组合起来,类似 scala 语言的 traits 混入。 1702 1703```cpp 1704class Role1 {}; 1705class Role2 {}; 1706class Role3 {}; 1707 1708class Object1 : public Role1, public Role2 { 1709 // ... 1710}; 1711 1712class Object2 : public Role2, public Role3 { 1713 // ... 1714}; 1715 1716``` 1717 1718在C++标准库中也有类似的实现样例: 1719```cpp 1720class basic_istream {}; 1721class basic_ostream {}; 1722 1723class basic_iostream : public basic_istream, public basic_ostream { 1724 1725}; 1726``` 1727 1728## <a name="c7-4"></a> 重载 1729 1730重载操作符要有充分理由,而且不要改变操作符原有语义,例如不要使用 ‘+’ 操作符来做减运算。 1731操作符重载令代码更加直观,但也有一些不足: 1732- 混淆直觉,误以为该操作和内建类型一样是高性能的,忽略了性能降低的可能; 1733- 问题定位时不够直观,按函数名查找比按操作符显然更方便。 1734- 重载操作符如果行为定义不直观(例如将‘+’ 操作符来做减运算),会让代码产生混淆。 1735- 赋值操作符的重载引入的隐式转换会隐藏很深的bug。可以定义类似Equals()、CopyFrom()等函数来替代=,==操作符。 1736 1737 1738 1739# <a name="c8"></a> 8 函数 1740## <a name="c8-1"></a>函数设计 1741### <a name="r8-1-1"></a>规则8.1.1 避免函数过长,函数不超过50行(非空非注释) 1742函数应该可以一屏显示完 (50行以内),只做一件事情,而且把它做好。 1743 1744过长的函数往往意味着函数功能不单一,过于复杂,或过分呈现细节,未进行进一步抽象。 1745 1746例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。 1747 1748即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的bug。 1749建议将其拆分为更加简短并易于管理的若干函数,以便于他人阅读和修改代码。 1750 1751## <a name="c8-2"></a>内联函数 1752 1753### <a name="a8-2-1"></a>建议8.2.1 内联函数不超过10行(非空非注释) 1754**说明**:内联函数具有一般函数的特性,它与一般函数不同之处只在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。 1755 1756内联函数只适合于只有 1~10 行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,也没有必要用内联函数实现,一般的编译器会放弃内联方式,而采用普通的方式调用函数。 1757 1758如果内联函数包含复杂的控制结构,如循环、分支(switch)、try-catch 等语句,一般编译器将该函数视同普通函数。 1759**虚函数、递归函数不能被用来做内联函数**。 1760 1761## <a name="c8-3"></a> 函数参数 1762 1763### <a name="a8-3-1"></a>建议8.3.1 函数参数使用引用取代指针 1764 1765**说明**:引用比指针更安全,因为它一定非空,且一定不会再指向其他目标;引用不需要检查非法的NULL指针。 1766 1767如果是基于老平台开发的产品,则优先顺从原有平台的处理方式。 1768选择 const 避免参数被修改,让代码阅读者清晰地知道该参数不被修改,可大大增强代码可读性。 1769 1770例外:当传入参数为编译期长度未知的数组时,可以使用指针而不是引用。 1771 1772### <a name="a8-3-2"></a>建议8.3.2 使用强类型参数,避免使用void* 1773尽管不同的语言对待强类型和弱类型有自己的观点,但是一般认为c/c++是强类型语言,既然我们使用的语言是强类型的,就应该保持这样的风格。 1774好处是尽量让编译器在编译阶段就检查出类型不匹配的问题。 1775 1776使用强类型便于编译器帮我们发现错误,如下代码中注意函数 FooListAddNode 的使用: 1777```cpp 1778struct FooNode { 1779 struct List link; 1780 int foo; 1781}; 1782 1783struct BarNode { 1784 struct List link; 1785 int bar; 1786} 1787 1788void FooListAddNode(void *node) // Bad: 这里用 void * 类型传递参数 1789{ 1790 FooNode *foo = (FooNode *)node; 1791 ListAppend(&g_FooList, &foo->link); 1792} 1793 1794void MakeTheList() 1795{ 1796 FooNode *foo = nullptr; 1797 BarNode *bar = nullptr; 1798 ... 1799 1800 FooListAddNode(bar); // Wrong: 这里本意是想传递参数 foo,但错传了 bar,却没有报错 1801} 1802``` 1803 18041. 可以使用模板函数来实现参数类型的变化。 18052. 可以使用基类指针来实现多态。 1806 1807### <a name="a8-3-3"></a>建议8.3.3 函数的参数个数不超过5个 1808函数的参数过多,会使得该函数易于受外部变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。 1809 1810如果超过可以考虑: 1811- 看能否拆分函数 1812- 看能否将相关参数合在一起,定义结构体 1813 1814# <a name="c9"></a> 9 C++其他特性 1815 1816## <a name="c9-1"></a> 常量与初始化 1817 1818不变的值更易于理解、跟踪和分析,所以应该尽可能地使用常量代替变量,定义值的时候,应该把const作为默认的选项。 1819 1820### <a name="r9-1-1"></a>规则9.1.1 不允许使用宏来表示常量 1821 1822**说明**:宏是简单的文本替换,在预处理阶段时完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名;宏没有类型检查,不安全;宏没有作用域。 1823 1824```cpp 1825#define MAX_MSISDN_LEN 20 // 不好 1826 1827// C++请使用const常量 1828const int MAX_MSISDN_LEN = 20; // 好 1829 1830// 对于C++11以上版本,可以使用constexpr 1831constexpr int MAX_MSISDN_LEN = 20; 1832``` 1833 1834### <a name="a9-1-1"></a>建议9.1.1 一组相关的整型常量应定义为枚举 1835 1836**说明**:枚举比`#define`或`const int`更安全。编译器会检查参数值是否位于枚举取值范围内,避免错误发生。 1837 1838```cpp 1839// 好的例子: 1840enum Week { 1841 SUNDAY, 1842 MONDAY, 1843 TUESDAY, 1844 WEDNESDAY, 1845 THURSDAY, 1846 FRIDAY, 1847 SATURDAY 1848}; 1849 1850enum Color { 1851 RED, 1852 BLACK, 1853 BLUE 1854}; 1855 1856void ColorizeCalendar(Week today, Color color); 1857 1858ColorizeCalendar(BLUE, SUNDAY); // 编译报错,参数类型错误 1859 1860// 不好的例子: 1861const int SUNDAY = 0; 1862const int MONDAY = 1; 1863 1864const int BLACK = 0; 1865const int BLUE = 1; 1866 1867bool ColorizeCalendar(int today, int color); 1868ColorizeCalendar(BLUE, SUNDAY); // 不会报错 1869``` 1870 1871当枚举值需要对应到具体数值时,须在声明时显式赋值。否则不需要显式赋值,以避免重复赋值,降低维护(增加、删除成员)工作量。 1872 1873```cpp 1874// 好的例子:S协议里定义的设备ID值,用于标识设备类型 1875enum DeviceType { 1876 DEV_UNKNOWN = -1, 1877 DEV_DSMP = 0, 1878 DEV_ISMG = 1, 1879 DEV_WAPPORTAL = 2 1880}; 1881``` 1882 1883程序内部使用,仅用于分类的情况,不应该进行显式的赋值。 1884 1885```cpp 1886// 好的例子:程序中用来标识会话状态的枚举定义 1887enum SessionState { 1888 INIT, 1889 CLOSED, 1890 WAITING_FOR_RESPONSE 1891}; 1892``` 1893 1894应当尽量避免枚举值重复,如必须重复也要用已定义的枚举来修饰 1895 1896```cpp 1897enum RTCPType { 1898 RTCP_SR = 200, 1899 RTCP_MIN_TYPE = RTCP_SR, 1900 RTCP_RR = 201, 1901 RTCP_SDES = 202, 1902 RTCP_BYE = 203, 1903 RTCP_APP = 204, 1904 RTCP_RTPFB = 205, 1905 RTCP_PSFB = 206, 1906 RTCP_XR = 207, 1907 RTCP_RSI = 208, 1908 RTCP_PUBPORTS = 209, 1909 RTCP_MAX_TYPE = RTCP_PUBPORTS 1910}; 1911``` 1912 1913### <a name="r9-1-2"></a>规则9.1.2 不允许使用魔鬼数字 1914所谓魔鬼数字即看不懂、难以理解的数字。 1915 1916魔鬼数字并非一个非黑即白的概念,看不懂也有程度,需要自行判断。 1917例如数字 12,在不同的上下文中情况是不一样的: 1918type = 12; 就看不懂,但 `monthsCount = yearsCount * 12`; 就能看懂。 1919数字 0 有时候也是魔鬼数字,比如 `status = 0`; 并不能表达是什么状态。 1920 1921解决途径: 1922对于局部使用的数字,可以增加注释说明 1923对于多处使用的数字,必须定义 const 常量,并通过符号命名自注释。 1924 1925禁止出现下列情况: 1926没有通过符号来解释数字含义,如` const int ZERO = 0` 1927符号命名限制了其取值,如 `const int XX_TIMER_INTERVAL_300MS = 300`,直接使用`XX_TIMER_INTERVAL_MS`来表示该常量是定时器的时间间隔。 1928 1929### <a name="r9-1-3"></a>规则9.1.3 常量应该保证单一职责 1930 1931**说明**:一个常量只用来表示一个特定功能,即一个常量不能有多种用途。 1932 1933```cpp 1934// 好的例子:协议A和协议B,手机号(MSISDN)的长度都是20。 1935const unsigned int A_MAX_MSISDN_LEN = 20; 1936const unsigned int B_MAX_MSISDN_LEN = 20; 1937 1938// 或者使用不同的名字空间: 1939namespace Namespace1 { 1940 const unsigned int MAX_MSISDN_LEN = 20; 1941} 1942 1943namespace Namespace2 { 1944 const unsigned int MAX_MSISDN_LEN = 20; 1945} 1946``` 1947 1948### <a name="r9-1-4"></a>规则9.1.4 禁止用memcpy_s、memset_s初始化非POD对象 1949 1950**说明**:`POD`全称是`Plain Old Data`,是C++ 98标准(ISO/IEC 14882, first edition, 1998-09-01)中引入的一个概念,`POD`类型主要包括`int`, `char`, `float`,`double`,`enumeration`,`void`,指针等原始类型以及聚合类型,不能使用封装和面向对象特性(如用户定义的构造/赋值/析构函数、基类、虚函数等)。 1951 1952由于非POD类型比如非聚合类型的class对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥用内存拷贝可能会导致严重的问题。 1953 1954即使对聚合类型的class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡`memcpy_s`、`memset_s`操作。 1955 1956对于POD类型的详细说明请参见附录。 1957 1958### <a name="a9-1-2"></a>建议9.1.2 变量使用时才声明并初始化 1959 1960**说明**:变量在使用前未赋初值,是常见的低级编程错误。使用前才声明变量并同时初始化,非常方便地避免了此类低级错误。 1961 1962在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题: 1963* 程序难以理解和维护:变量的定义与使用分离。 1964* 变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的空值(比如零)来初始化,这通常是一种浪费,如果变量在被赋于有效值以前使用,还会导致错误。 1965 1966遵循变量作用域最小化原则与就近声明原则, 使得代码更容易阅读,方便了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。 1967 1968```cpp 1969// 不好的例子:声明与初始化分离 1970string name; // 声明时未初始化:调用缺省构造函数 1971name = "zhangsan"; // 再次调用赋值操作符函数;声明与定义在不同的地方,理解相对困难 1972 1973// 好的例子:声明与初始化一体,理解相对容易 1974string name("zhangsan"); // 调用构造函数 1975``` 1976 1977 1978## <a name="c9-2"></a> 表达式 1979### <a name="r9-2-1"></a>规则9.2.1 含有变量自增或自减运算的表达式中禁止再次引用该变量 1980含有变量自增或自减运算的表达式中,如果再引用该变量,其结果在C++标准中未明确定义。各个编译器或者同一个编译器不同版本实现可能会不一致。 1981为了更好的可移植性,不应该对标准未定义的运算次序做任何假设。 1982 1983注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。 1984 1985示例: 1986```cpp 1987x = b[i] + i++; // Bad: b[i]运算跟 i++,先后顺序并不明确。 1988``` 1989正确的写法是将自增或自减运算单独放一行: 1990```cpp 1991x = b[i] + i; 1992i++; // Good: 单独一行 1993``` 1994 1995函数参数 1996```cpp 1997Func(i++, i); // Bad: 传递第2个参数时,不确定自增运算有没有发生 1998``` 1999 2000正确的写法 2001```cpp 2002i++; // Good: 单独一行 2003x = Func(i, i); 2004``` 2005 2006### <a name="r9-2-2"></a>规则9.2.2 switch语句要有default分支 2007大部分情况下,switch语句中要有default分支,保证在遗漏case标签处理时能够有一个缺省的处理行为。 2008 2009特例: 2010如果switch条件变量是枚举类型,并且 case 分支覆盖了所有取值,则加上default分支处理有些多余。 2011现代编译器都具备检查是否在switch语句中遗漏了某些枚举值的case分支的能力,会有相应的warning提示。 2012 2013```cpp 2014enum Color { 2015 RED = 0, 2016 BLUE 2017}; 2018 2019// 因为switch条件变量是枚举值,这里可以不用加default处理分支 2020switch (color) { 2021 case RED: 2022 DoRedThing(); 2023 break; 2024 case BLUE: 2025 DoBlueThing(); 2026 ... 2027 break; 2028} 2029``` 2030 2031### <a name="a9-2-1"></a>建议9.2.1 表达式的比较,应当遵循左侧倾向于变化、右侧倾向于不变的原则 2032当变量与常量比较时,如果常量放左边,如 if (MAX == v) 不符合阅读习惯,而 if (MAX > v) 更是难于理解。 2033应当按人的正常阅读、表达习惯,将常量放右边。写成如下方式: 2034```cpp 2035if (value == MAX) { 2036 2037} 2038 2039if (value < MAX) { 2040 2041} 2042``` 2043也有特殊情况,如:`if (MIN < value && value < MAX)` 用来描述区间时,前半段是常量在左的。 2044 2045不用担心将 '==' 误写成 '=',因为` if (value = MAX)` 会有编译告警,其他静态检查工具也会报错。让工具去解决笔误问题,代码要符合可读性第一。 2046 2047### <a name="a9-2-2"></a>建议9.2.2 使用括号明确操作符的优先级 2048使用括号明确操作符的优先级,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。下面是如何使用括号的建议。 2049 2050- 二元及以上操作符, 如果涉及多种操作符,则应该使用括号 2051```cpp 2052x = a + b + c; /* 操作符相同,可以不加括号 */ 2053x = Foo(a + b, c); /* 逗号两边的表达式,不需要括号 */ 2054x = 1 << (2 + 3); /* 操作符不同,需要括号 */ 2055x = a + (b / 5); /* 操作符不同,需要括号 */ 2056x = (a == b) ? a : (a – b); /* 操作符不同,需要括号 */ 2057``` 2058 2059 2060## <a name="c9-3"></a> 类型转换 2061 2062避免使用类型分支来定制行为:类型分支来定制行为容易出错,是企图用C++编写C代码的明显标志。这是一种很不灵活的技术,要添加新类型时,如果忘记修改所有分支,编译器也不会告知。使用模板和虚函数,让类型自己而不是调用它们的代码来决定行为。 2063 2064建议避免类型转换,我们在代码的类型设计上应该考虑到每种数据的数据类型是什么,而不是应该过度使用类型转换来解决问题。在设计某个基本类型的时候,请考虑: 2065- 是无符号还是有符号的 2066- 是适合float还是double 2067- 是使用int8,int16,int32还是int64,确定整形的长度 2068 2069但是我们无法禁止使用类型转换,因为C++语言是一门面向机器编程的语言,涉及到指针地址,并且我们会与各种第三方或者底层API交互,他们的类型设计不一定是合理的,在这个适配的过程中很容易出现类型转换。 2070 2071例外:在调用某个函数的时候,如果我们不想处理函数结果,首先要考虑这个是否是你的最好的选择。如果确实不想处理函数的返回值,那么可以使用(void)转换来解决。 2072 2073### <a name="r9-3-1"></a>规则9.3.1 如果确定要使用类型转换,请使用由C++提供的类型转换,而不是C风格的类型转换 2074 2075**说明**: 2076 2077C++提供的类型转换操作比C风格更有针对性,更易读,也更加安全,C++提供的转换有: 2078- 类型转换: 20791. `dynamic_cast`:主要用于继承体系下行转换,`dynamic_cast`具有类型检查的功能,请做好基类和派生类的设计,避免使用dynamic_cast来进行转换。 20802. `static_cast`:和C风格转换相似可做值的强制转换,或上行转换(把派生类的指针或引用转换成基类的指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。如果是纯粹的算数转换,那么请使用后面的大括号转换方式。 20813. `reinterpret_cast`:用于转换不相关的类型。`reinterpret_cast`强制编译器将某个类型对象的内存重新解释成另一种类型,这是一种不安全的转换,建议尽可能少用`reinterpret_cast`。 20824. `const_cast`:用于移除对象的`const`属性,使对象变得可修改,这样会破坏数据的不变性,建议尽可能少用。 2083 2084- 算数转换: (C++11开始支持) 2085 对于那种算数转换,并且类型信息没有丢失的,比如float到double, int32到int64的转换,推荐使用大括号的初始方式。 2086```cpp 2087 double d{ someFloat }; 2088 int64_t i{ someInt32 }; 2089``` 2090 2091### <a name="a9-3-1"></a>建议9.3.1 避免使用`dynamic_cast` 20921. `dynamic_cast`依赖于C++的RTTI, 让程序员在运行时识别C++类对象的类型。 20932. `dynamic_cast`的出现一般说明我们的基类和派生类设计出现了问题,派生类破坏了基类的契约,不得不通过`dynamic_cast`转换到子类进行特殊处理,这个时候更希望来改善类的设计,而不是通过`dynamic_cast`来解决问题。 2094 2095### <a name="a9-3-2"></a>建议9.3.2 避免使用`reinterpret_cast` 2096 2097**说明**:`reinterpret_cast`用于转换不相关类型。尝试用`reinterpret_cast`将一种类型强制转换另一种类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。 2098 2099### <a name="a9-3-3"></a>建议9.3.3 避免使用`const_cast` 2100 2101**说明**:`const_cast`用于移除对象的`const`和`volatile`性质。 2102 2103使用const_cast转换后的指针或者引用来修改const对象,行为是未定义的。 2104 2105```cpp 2106// 不好的例子 2107const int i = 1024; 2108int* p = const_cast<int*>(&i); 2109*p = 2048; // 未定义行为 2110``` 2111 2112```cpp 2113// 不好的例子 2114class Foo { 2115public: 2116 Foo() : i(3) {} 2117 2118 void Fun(int v) 2119 { 2120 i = v; 2121 } 2122 2123private: 2124 int i; 2125}; 2126 2127int main(void) 2128{ 2129 const Foo f; 2130 Foo* p = const_cast<Foo*>(&f); 2131 p->Fun(8); // 未定义行为 2132} 2133 2134``` 2135 2136 2137## <a name="c9-4"></a>资源分配和释放 2138 2139### <a name="r9-4-1"></a>规则9.4.1 单个对象释放使用delete,数组对象释放使用delete [] 2140说明:单个对象删除使用delete, 数组对象删除使用delete [],原因: 2141 2142- 调用new所包含的动作:从系统中申请一块内存,并调用此类型的构造函数。 2143- 调用new[n]所包含的动作:申请可容纳n个对象的内存,并且对每一个对象调用其构造函数。 2144- 调用delete所包含的动作:先调用相应的析构函数,再将内存归还系统。 2145- 调用delete[]所包含的动作:对每一个对象调用析构函数,再释放所有内存 2146 2147如果new和delete的格式不匹配,结果是未知的。对于非class类型, new和delete不会调用构造与析构函数。 2148 2149错误写法: 2150```cpp 2151const int MAX_ARRAY_SIZE = 100; 2152int* numberArray = new int[MAX_ARRAY_SIZE]; 2153... 2154delete numberArray; 2155numberArray = nullptr; 2156``` 2157 2158正确写法: 2159```cpp 2160const int MAX_ARRAY_SIZE = 100; 2161int* numberArray = new int[MAX_ARRAY_SIZE]; 2162... 2163delete[] numberArray; 2164numberArray = nullptr; 2165``` 2166 2167### <a name="a9-4-1"></a>建议9.4.1 使用 RAII 特性来帮助追踪动态分配 2168 2169说明:RAII是“资源获取就是初始化”的缩语(Resource Acquisition Is Initialization),是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 2170 2171RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。这种做法有两大好处: 2172- 我们不需要显式地释放资源。 2173- 对象所需的资源在其生命期内始终保持有效。这样,就不必检查资源有效性的问题,可以简化逻辑、提高效率。 2174 2175 2176示例:使用RAII不需要显式地释放互斥资源。 2177 2178```cpp 2179class LockGuard { 2180public: 2181 LockGuard(const LockType& lockType): lock_(lockType) 2182 { 2183 lock_.Acquire(); 2184 } 2185 2186 ~LockGuard() 2187 { 2188 lock_.Release(); 2189 } 2190 2191private: 2192 LockType lock_; 2193}; 2194 2195 2196bool Update() 2197{ 2198 LockGuard lockGuard(mutex); 2199 if (...) { 2200 return false; 2201 } else { 2202 // 操作数据 2203 } 2204 2205 return true; 2206} 2207``` 2208 2209## <a name="c9-5"></a>标准库 2210 2211STL标准模板库在不同产品使用程度不同,这里列出一些基本规则和建议,供各团队参考。 2212 2213### <a name="r9-5-1"></a>规则9.5.1 不要保存std::string的c_str()返回的指针 2214 2215说明:在C++标准中并未规定string::c_str()指针持久有效,因此特定STL实现完全可以在调用string::c_str()时返回一个临时存储区并很快释放。所以为了保证程序的可移植性,不要保存string::c_str()的结果,而是在每次需要时直接调用。 2216 2217示例: 2218 2219```cpp 2220void Fun1() 2221{ 2222 std::string name = "demo"; 2223 const char* text = name.c_str(); // 表达式结束以后,name的生命周期还在,指针有效 2224 2225 // 如果中间调用了string的非const成员函数,导致string被修改,比如operator[], begin()等 2226 // 可能会导致text的内容不可用,或者不是原来的字符串 2227 name = "test"; 2228 name[1] = '2'; 2229 2230 // 后续使用text指针,其字符串内容不再是"demo" 2231} 2232 2233void Fun2() 2234{ 2235 std::string name = "demo"; 2236 std::string test = "test"; 2237 const char* text = (name + test).c_str(); // 表达式结束以后,+号产生的临时对象被销毁,指针无效 2238 2239 // 后续使用text指针,其已不再指向合法内存空间 2240} 2241``` 2242例外:在少数对性能要求非常高的代码中,为了适配已有的只接受const char*类型入参的函数,可以临时保存string::c_str()返回的指针。但是必须严格保证string对象的生命周期长于所保存指针的生命周期,并且保证在所保存指针的生命周期内,string对象不会被修改。 2243 2244 2245### <a name="a9-5-1"></a>建议9.5.1 使用std::string代替char* 2246 2247说明:使用string代替`char*`有很多优势,比如: 22481. 不用考虑结尾的’\0’; 22492. 可以直接使用+, =, ==等运算符以及其它字符串操作函数; 22503. 不需要考虑内存分配操作,避免了显式的new/delete,以及由此导致的错误; 2251 2252需要注意的是某些stl实现中string是基于写时复制策略的,这会带来2个问题,一是某些版本的写时复制策略没有实现线程安全,在多线程环境下会引起程序崩溃;二是当与动态链接库相互传递基于写时复制策略的string时,由于引用计数在动态链接库被卸载时无法减少可能导致悬挂指针。因此,慎重选择一个可靠的stl实现对于保证程序稳定是很重要的。 2253 2254例外: 2255当调用系统或者其它第三方库的API时,针对已经定义好的接口,只能使用`char*`。但是在调用接口之前都可以使用string,在调用接口时使用string::c_str()获得字符指针。 2256当在栈上分配字符数组当作缓冲区使用时,可以直接定义字符数组,不要使用string,也没有必要使用类似`vector<char>`等容器。 2257 2258### <a name="r9-5-2"></a>规则9.5.2 禁止使用auto_ptr 2259说明:在stl库中的std::auto_ptr具有一个隐式的所有权转移行为,如下代码: 2260```cpp 2261auto_ptr<T> p1(new T); 2262auto_ptr<T> p2 = p1; 2263``` 2264当执行完第2行语句后,p1已经不再指向第1行中分配的对象,而是变为nullptr。正因为如此,auto_ptr不能被置于各种标准容器中。 2265转移所有权的行为通常不是期望的结果。对于必须转移所有权的场景,也不应该使用隐式转移的方式。这往往需要程序员对使用auto_ptr的代码保持额外的谨慎,否则出现对空指针的访问。 2266使用auto_ptr常见的有两种场景,一是作为智能指针传递到产生auto_ptr的函数外部,二是使用auto_ptr作为RAII管理类,在超出auto_ptr的生命周期时自动释放资源。 2267对于第1种场景,可以使用std::shared_ptr来代替。 2268对于第2种场景,可以使用C++11标准中的std::unique_ptr来代替。其中std::unique_ptr是std::auto_ptr的代替品,支持显式的所有权转移。 2269 2270例外: 2271在C++11标准得到普遍使用之前,在一定需要对所有权进行转移的场景下,可以使用std::auto_ptr,但是建议对std::auto_ptr进行封装,并禁用封装类的拷贝构造函数和赋值运算符,以使该封装类无法用于标准容器。 2272 2273 2274### <a name="a9-5-2"></a>建议9.5.2 使用新的标准头文件 2275 2276说明: 2277使用C++的标准头文件时,请使用`<cstdlib>`这样的,而不是`<stdlib.h>`这种的。 2278 2279## <a name="c9-6"></a> const的用法 2280在声明的变量或参数前加上关键字 const 用于指明变量值不可被篡改 (如 `const int foo` ). 为类中的函数加上 const 限定符表明该函数不会修改类成员变量的状态 (如 `class Foo { int Bar(char c) const; };`)。 const 变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障, 便于尽早发现错误。因此, 我们强烈建议在任何可能的情况下使用 const。 2281有时候,使用C++11的constexpr来定义真正的常量可能更好。 2282 2283### <a name="r9-6-1"></a>规则9.6.1 对于指针和引用类型的形参,如果是不需要修改的,请使用const 2284不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。 2285```cpp 2286class Foo; 2287 2288void PrintFoo(const Foo& foo); 2289``` 2290 2291### <a name="r9-6-2"></a>规则9.6.2 对于不会修改成员变量的成员函数请使用const修饰 2292尽可能将成员函数声明为 const。 访问函数应该总是 const。只要不修改数据成员的成员函数,都声明为const。 2293对于虚函数,应当从设计意图上考虑继承链上的所有类是否需要在此虚函数中修改数据成员,而不是仅关注单个类的实现。 2294```cpp 2295class Foo { 2296public: 2297 2298 // ... 2299 2300 int PrintValue() const // const修饰成员函数,不会修改成员变量 2301 { 2302 std::cout << value_ << std::endl; 2303 } 2304 2305 int GetValue() const // const修饰成员函数,不会修改成员变量 2306 { 2307 return value_; 2308 } 2309 2310private: 2311 int value_; 2312}; 2313``` 2314 2315### <a name="a9-6-1"></a>建议9.6.1 初始化后不会再修改的成员变量定义为const 2316 2317```cpp 2318class Foo { 2319public: 2320 Foo(int length) : dataLength_(length) {} 2321private: 2322 const int dataLength_; 2323}; 2324``` 2325 2326## <a name="c9-7"></a> 异常 2327 2328### <a name="a9-7-1"></a>建议9.7.1 C++11中,如果函数不会抛出异常,声明为`noexcept` 2329**理由** 23301. 如果函数不会抛出异常,声明为`noexcept`可以让编译器最大程度的优化函数,如减少执行路径,提高错误退出的效率。 23312. `vector`等STL容器,为了保证接口的健壮性,如果保存元素的`move运算符`没有声明为`noexcept`,则在容器扩张搬移元素时不会使用`move机制`,而使用`copy机制`,带来性能损失的风险。如果一个函数不能抛出异常,或者一个程序并没有截获某个函数所抛出的异常并进行处理,那么这个函数可以用新的`noexcept`关键字对其进行修饰,表示这个函数不会抛出异常或者抛出的异常不会被截获并处理。例如: 2332 2333```cpp 2334extern "C" double sqrt(double) noexcept; // 永远不会抛出异常 2335 2336// 即使可能抛出异常,也可以使用 noexcept 2337// 这里不准备处理内存耗尽的异常,简单地将函数声明为noexcept 2338std::vector<int> MyComputation(const std::vector<int>& v) noexcept 2339{ 2340 std::vector<int> res = v; // 可能会抛出异常 2341 // do something 2342 return res; 2343} 2344``` 2345 2346**示例** 2347 2348```cpp 2349RetType Function(Type params) noexcept; // 最大的优化 2350RetType Function(Type params); // 更少的优化 2351 2352// std::vector 的 move 操作需要声明 noexcept 2353class Foo1 { 2354public: 2355 Foo1(Foo1&& other); // no noexcept 2356}; 2357 2358std::vector<Foo1> a1; 2359a1.push_back(Foo1()); 2360a1.push_back(Foo1()); // 触发容器扩张,搬移已有元素时调用copy constructor 2361 2362class Foo2 { 2363public: 2364 Foo2(Foo2&& other) noexcept; 2365}; 2366 2367std::vector<Foo2> a2; 2368a2.push_back(Foo2()); 2369a2.push_back(Foo2()); // 触发容器扩张,搬移已有元素时调用move constructor 2370``` 2371 2372**注意** 2373默认构造函数、析构函数、`swap`函数,`move操作符`都不应该抛出异常。 2374 2375## <a name="c9-8"></a> 模板与泛型编程 2376 2377### <a name="a9-8-1"></a>规则9.8.1 禁止在OpenHarmony项目中进行泛型编程 2378泛型编程和面向对象编程的思想、理念以及技巧完全不同,OpenHarmony项目主流使用面向对象的思想。 2379 2380C++提供了强大的泛型编程的机制,能够实现非常灵活简洁的类型安全的接口,实现类型不同但是行为相同的代码复用。 2381 2382但是C++泛型编程存在以下缺点: 2383 23841. 对泛型编程不很熟练的人,常常会将面向对象的逻辑写成模板、将不依赖模板参数的成员写在模板中等等导致逻辑混乱代码膨胀诸多问题。 23852. 模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug 和维护起来都很麻烦。 23863. 模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解。 23874. 模板如果使用不当,会导致运行时代码过度膨胀。 23885. 模板代码难以修改和重构。模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用。 2389 2390所以,OpenHarmony大部分部件禁止模板编程,仅有 __少数部件__ 可以使用泛型编程,并且开发的模板要有详细的注释。 2391例外: 23921. stl适配层可以使用模板 2393 2394## <a name="c9-9"></a> 宏 2395在C++语言中,我们强烈建议尽可能少使用复杂的宏 2396- 对于常量定义,请按照前面章节所述,使用const或者枚举; 2397- 对于宏函数,尽可能简单,并且遵循下面的原则,并且优先使用内联函数,模板函数等进行替换。 2398 2399```cpp 2400// 不推荐使用宏函数 2401#define SQUARE(a, b) ((a) * (b)) 2402 2403// 请使用模板函数,内联函数等来替换。 2404template<typename T> T Square(T a, T b) { return a * b; } 2405``` 2406 2407如果需要使用宏,请参考C语言规范的相关章节。 2408**例外**:一些通用且成熟的应用,如:对 new, delete 的封装处理,可以保留对宏的使用。 2409 2410# <a name="c10"></a> 10 现代C++特性 2411 2412随着 ISO 在2011年发布 C++11 语言标准,以及2017年3月发布 C++17 ,现代C++(C++11/14/17等)增加了大量提高编程效率、代码质量的新语言特性和标准库。 2413本章节描述了一些可以帮助团队更有效率的使用现代C++,规避语言陷阱的指导意见。 2414 2415## <a name="c10-1"></a> 代码简洁性和安全性提升 2416### <a name="a10-1-1"></a>建议10.1.1 合理使用`auto` 2417**理由** 2418 2419* `auto`可以避免编写冗长、重复的类型名,也可以保证定义变量时初始化。 2420* `auto`类型推导规则复杂,需要仔细理解。 2421* 如果能够使代码更清晰,继续使用明确的类型,且只在局部变量使用`auto`。 2422 2423**示例** 2424 2425```cpp 2426// 避免冗长的类型名 2427std::map<string, int>::iterator iter = m.find(val); 2428auto iter = m.find(val); 2429 2430// 避免重复类型名 2431class Foo {...}; 2432Foo* p = new Foo; 2433auto p = new Foo; 2434 2435// 保证初始化 2436int x; // 编译正确,没有初始化 2437auto x; // 编译失败,必须初始化 2438``` 2439 2440auto 的类型推导可能导致困惑: 2441 2442```cpp 2443auto a = 3; // int 2444const auto ca = a; // const int 2445const auto& ra = a; // const int& 2446auto aa = ca; // int, 忽略 const 和 reference 2447auto ila1 = { 10 }; // std::initializer_list<int> 2448auto ila2{ 10 }; // std::initializer_list<int> 2449 2450auto&& ura1 = x; // int& 2451auto&& ura2 = ca; // const int& 2452auto&& ura3 = 10; // int&& 2453 2454const int b[10]; 2455auto arr1 = b; // const int* 2456auto& arr2 = b; // const int(&)[10] 2457``` 2458 2459如果没有注意 `auto` 类型推导时忽略引用,可能引入难以发现的性能问题: 2460 2461```cpp 2462std::vector<std::string> v; 2463auto s1 = v[0]; // auto 推导为 std::string,拷贝 v[0] 2464``` 2465 2466如果使用`auto`定义接口,如头文件中的常量,可能因为开发人员修改了值,而导致类型发生变化。 2467 2468### <a name="r10-1-1"></a>规则10.1.1 在重写虚函数时请使用`override`或`final`关键字 2469**理由** 2470`override`和`final`关键字都能保证函数是虚函数,且重写了基类的虚函数。如果子类函数与基类函数原型不一致,则产生编译告警。`final`还保证虚函数不会再被子类重写。 2471 2472使用`override`或`final`关键字后,如果修改了基类虚函数原型,但忘记修改子类重写的虚函数,在编译期就可以发现。也可以避免有多个子类时,重写虚函数的修改遗漏。 2473 2474**示例** 2475 2476```cpp 2477class Base { 2478public: 2479 virtual void Foo(); 2480 virtual void Foo(int var); 2481 void Bar(); 2482}; 2483 2484class Derived : public Base { 2485public: 2486 void Foo() const override; // 编译失败: Derived::Foo 和 Base::Foo 原型不一致,不是重写 2487 void Foo() override; // 正确: Derived::Foo 重写 Base::Foo 2488 void Foo(int var) final; // 正确: Derived::Foo(int) 重写 Base::Foo(int),且Derived的派生类不能再重写此函数 2489 void Bar() override; // 编译失败: Base::Bar 不是虚函数 2490}; 2491``` 2492 2493**总结** 24941. 基类首次定义虚函数,使用`virtual`关键字 24952. 子类重写基类虚函数(包括析构函数),使用`override`或`final`关键字(但不要两者一起使用),并且不使用`virtual`关键字 24963. 非虚函数,`virtual`、`override`和`final`都不使用 2497 2498### <a name="r10-1-2"></a>规则10.1.2 使用`delete`关键字删除函数 2499**理由** 2500相比于将类成员函数声明为`private`但不实现,`delete`关键字更明确,且适用范围更广。 2501 2502**示例** 2503 2504```cpp 2505class Foo { 2506private: 2507 // 只看头文件不知道拷贝构造是否被删除 2508 Foo(const Foo&); 2509}; 2510 2511class Foo { 2512public: 2513 // 明确删除拷贝赋值函数 2514 Foo& operator=(const Foo&) = delete; 2515}; 2516``` 2517 2518`delete`关键字还支持删除非成员函数 2519 2520```cpp 2521template<typename T> 2522void Process(T value); 2523 2524template<> 2525void Process<void>(void) = delete; 2526``` 2527 2528### <a name="r10-1-3"></a>规则10.1.3 使用`nullptr`,而不是`NULL`或`0` 2529**理由** 2530长期以来,C++没有一个代表空指针的关键字,这是一件很尴尬的事: 2531 2532```cpp 2533#define NULL ((void *)0) 2534 2535char* str = NULL; // 错误: void* 不能自动转换为 char* 2536 2537void(C::*pmf)() = &C::Func; 2538if (pmf == NULL) {} // 错误: void* 不能自动转换为指向成员函数的指针 2539``` 2540 2541如果把`NULL`被定义为`0`或`0L`。可以解决上面的问题。 2542 2543或者在需要空指针的地方直接使用`0`。但这引入另一个问题,代码不清晰,特别是使用`auto`自动推导: 2544 2545```cpp 2546auto result = Find(id); 2547if (result == 0) { // Find() 返回的是 指针 还是 整数? 2548 // do something 2549} 2550``` 2551 2552`0`字面上是`int`类型(`0L`是`long`),所以`NULL`和`0`都不是指针类型。 2553当重载指针和整数类型的函数时,传递`NULL`或`0`都调用到整数类型重载的函数: 2554 2555```cpp 2556void F(int); 2557void F(int*); 2558 2559F(0); // 调用 F(int),而非 F(int*) 2560F(NULL); // 调用 F(int),而非 F(int*) 2561``` 2562 2563另外,`sizeof(NULL) == sizeof(void*)`并不一定总是成立的,这也是一个潜在的风险。 2564 2565总结: 直接使用`0`或`0L`,代码不清晰,且无法做到类型安全;使用`NULL`无法做到类型安全。这些都是潜在的风险。 2566 2567`nullptr`的优势不仅仅是在字面上代表了空指针,使代码清晰,而且它不再是一个整数类型。 2568 2569`nullptr`是`std::nullptr_t`类型,而`std::nullptr_t`可以隐式的转换为所有的原始指针类型,这使得`nullptr`可以表现成指向任意类型的空指针。 2570 2571```cpp 2572void F(int); 2573void F(int*); 2574F(nullptr); // 调用 F(int*) 2575 2576auto result = Find(id); 2577if (result == nullptr) { // Find() 返回的是 指针 2578 // do something 2579} 2580``` 2581 2582### <a name="r10-1-4"></a>规则10.1.4 使用`using`而非`typedef` 2583在`C++11`之前,可以通过`typedef`定义类型的别名。没人愿意多次重复`std::map<uint32_t, std::vector<int>>`这样的代码。 2584 2585```cpp 2586typedef std::map<uint32_t, std::vector<int>> SomeType; 2587``` 2588 2589类型的别名实际是对类型的封装。而通过封装,可以让代码更清晰,同时在很大程度上避免类型变化带来的散弹式修改。 2590在`C++11`之后,提供`using`,实现`声明别名(alias declarations)`: 2591 2592```cpp 2593using SomeType = std::map<uint32_t, std::vector<int>>; 2594``` 2595 2596对比两者的格式: 2597 2598```cpp 2599typedef Type Alias; // Type 在前,还是 Alias 在前 2600using Alias = Type; // 符合'赋值'的用法,容易理解,不易出错 2601``` 2602 2603如果觉得这点还不足以切换到`using`,我们接着看看`模板别名(alias template)`: 2604 2605```cpp 2606// 定义模板的别名,一行代码 2607template<class T> 2608using MyAllocatorVector = std::vector<T, MyAllocator<T>>; 2609 2610MyAllocatorVector<int> data; // 使用 using 定义的别名 2611 2612template<class T> 2613class MyClass { 2614private: 2615 MyAllocatorVector<int> data_; // 模板类中使用 using 定义的别名 2616}; 2617``` 2618 2619而`typedef`不支持带模板参数的别名,只能"曲线救国": 2620 2621```cpp 2622// 通过模板包装 typedef,需要实现一个模板类 2623template<class T> 2624struct MyAllocatorVector { 2625 typedef std::vector<T, MyAllocator<T>> type; 2626}; 2627 2628MyAllocatorVector<int>::type data; // 使用 typedef 定义的别名,多写 ::type 2629 2630template<class T> 2631class MyClass { 2632private: 2633 typename MyAllocatorVector<int>::type data_; // 模板类中使用,除了 ::type,还需要加上 typename 2634}; 2635``` 2636 2637### <a name="r10-1-5"></a>规则10.1.5 禁止使用std::move操作const对象 2638从字面上看,`std::move`的意思是要移动一个对象。而const对象是不允许修改的,自然也无法移动。因此用`std::move`操作const对象会给代码阅读者带来困惑。 2639在实际功能上,`std::move`会把对象转换成右值引用类型;对于const对象,会将其转换成const的右值引用。由于极少有类型会定义以const右值引用为参数的移动构造函数和移动赋值操作符,因此代码实际功能往往退化成了对象拷贝而不是对象移动,带来了性能上的损失。 2640 2641**错误示例:** 2642```cpp 2643std::string g_string; 2644std::vector<std::string> g_stringList; 2645 2646void func() 2647{ 2648 const std::string myString = "String content"; 2649 g_string = std::move(myString); // bad:并没有移动myString,而是进行了复制 2650 const std::string anotherString = "Another string content"; 2651 g_stringList.push_back(std::move(anotherString)); // bad:并没有移动anotherString,而是进行了复制 2652} 2653``` 2654 2655## <a name="c10-2"></a> 智能指针 2656### <a name="r10-2-1"></a>规则10.2.1 单例、类的成员等所有权不会被多方持有的优先使用原始指针而不是智能指针 2657**理由** 2658智能指针会自动释放对象资源避免资源泄露,但会带额外的资源开销。如:智能指针自动生成的类、构造和析构的开销、内存占用多等。 2659 2660单例、类的成员等对象的所有权不会被多方持有的情况,仅在类析构中释放资源即可。不应该使用智能指针增加额外的开销。 2661 2662**示例** 2663 2664```cpp 2665class Foo; 2666class Base { 2667public: 2668 Base() {} 2669 virtual ~Base() 2670 { 2671 delete foo_; 2672 } 2673private: 2674 Foo* foo_ = nullptr; 2675}; 2676``` 2677 2678**例外** 26791. 返回创建的对象时,需要指针销毁函数的可以使用智能指针。 2680```cpp 2681class User; 2682class Foo { 2683public: 2684 std::unique_ptr<User, void(User *)> CreateUniqueUser() // 可使用unique_ptr保证对象的创建和释放在同一runtime 2685 { 2686 sptr<User> ipcUser = iface_cast<User>(remoter); 2687 return std::unique_ptr<User, void(User *)>(::new User(ipcUser), [](User *user) { 2688 user->Close(); 2689 ::delete user; 2690 }); 2691 } 2692 2693 std::shared_ptr<User> CreateSharedUser() // 可使用shared_ptr保证对象的创建和释放在同一runtime中 2694 { 2695 sptr<User> ipcUser = iface_cast<User>(remoter); 2696 return std::shared_ptr<User>(ipcUser.GetRefPtr(), [ipcUser](User *user) mutable { 2697 ipcUser = nullptr; 2698 }); 2699 } 2700}; 2701``` 27022. 返回创建的对象且对象需要被多方引用时,可以使用shared_ptr。 2703 2704### <a name="r10-2-2"></a>规则10.2.2 使用`std::make_unique`而不是`new`创建`unique_ptr` 2705**理由** 27061. `make_unique`提供了更简洁的创建方式 27072. 保证了复杂表达式的异常安全 2708 2709**示例** 2710 2711```cpp 2712// 不好:两次出现 MyClass,重复导致不一致风险 2713std::unique_ptr<MyClass> ptr(new MyClass(0, 1)); 2714// 好:只出现一次 MyClass,不存在不一致的可能 2715auto ptr = std::make_unique<MyClass>(0, 1); 2716``` 2717 2718重复出现类型可能导致非常严重的问题,且很难发现: 2719 2720```cpp 2721// 编译正确,但new和delete不配套 2722std::unique_ptr<uint8_t> ptr(new uint8_t[10]); 2723std::unique_ptr<uint8_t[]> ptr(new uint8_t); 2724// 非异常安全: 编译器可能按如下顺序计算参数: 2725// 1. 分配 Foo 的内存, 2726// 2. 构造 Foo, 2727// 3. 调用 Bar, 2728// 4. 构造 unique_ptr<Foo>. 2729// 如果 Bar 抛出异常, Foo 不会被销毁,产生内存泄露。 2730F(unique_ptr<Foo>(new Foo()), Bar()); 2731 2732// 异常安全: 调用函数不会被打断. 2733F(make_unique<Foo>(), Bar()); 2734``` 2735 2736**例外** 2737`std::make_unique`不支持自定义`deleter`。 2738在需要自定义`deleter`的场景,建议在自己的命名空间实现定制版本的`make_unique`。 2739使用`new`创建自定义`deleter`的`unique_ptr`是最后的选择。 2740 2741### <a name="r10-2-4"></a>规则10.2.4 使用`std::make_shared`而不是`new`创建`shared_ptr` 2742**理由** 2743使用`std::make_shared`除了类似`std::make_unique`一致性等原因外,还有性能的因素。 2744`std::shared_ptr`管理两个实体: 2745* 控制块(存储引用计数,`deleter`等) 2746* 管理对象 2747 2748`std::make_shared`创建`std::shared_ptr`,会一次性在堆上分配足够容纳控制块和管理对象的内存。而使用`std::shared_ptr<MyClass>(new MyClass)`创建`std::shared_ptr`,除了`new MyClass`会触发一次堆分配外,`std::shard_ptr`的构造函数还会触发第二次堆分配,产生额外的开销。 2749 2750**例外** 2751类似`std::make_unique`,`std::make_shared`不支持定制`deleter` 2752 2753## <a name="c10-3"></a> Lambda 2754### <a name="a10-3-1"></a>建议10.3.1 当函数不能工作时选择使用`lambda`(捕获局部变量,或编写局部函数) 2755**理由** 2756函数无法捕获局部变量或在局部范围内声明;如果需要这些东西,尽可能选择`lambda`,而不是手写的`functor`。 2757另一方面,`lambda`和`functor`不会重载;如果需要重载,则使用函数。 2758如果`lambda`和函数都可以的场景,则优先使用函数;尽可能使用最简单的工具。 2759 2760**示例** 2761 2762```cpp 2763// 编写一个只接受 int 或 string 的函数 2764// -- 重载是自然的选择 2765void F(int); 2766void F(const string&); 2767 2768// 需要捕获局部状态,或出现在语句或表达式范围 2769// -- lambda 是自然的选择 2770vector<Work> v = LotsOfWork(); 2771for (int taskNum = 0; taskNum < max; ++taskNum) { 2772 pool.Run([=, &v] {...}); 2773} 2774pool.Join(); 2775``` 2776 2777### <a name="r10-3-2"></a>规则10.3.1 非局部范围使用`lambdas`,避免使用按引用捕获 2778**理由** 2779非局部范围使用`lambdas`包括返回值,存储在堆上,或者传递给其它线程。局部的指针和引用不应该在它们的范围外存在。`lambdas`按引用捕获就是把局部对象的引用存储起来。如果这会导致超过局部变量生命周期的引用存在,则不应该按引用捕获。 2780 2781**示例** 2782 2783```cpp 2784// 不好 2785void Foo() 2786{ 2787 int local = 42; 2788 // 按引用捕获 local. 2789 // 当函数返回后,local 不再存在, 2790 // 因此 Process() 的行为未定义! 2791 threadPool.QueueWork([&]{ Process(local); }); 2792} 2793 2794// 好 2795void Foo() 2796{ 2797 int local = 42; 2798 // 按值捕获 local。 2799 // 因为拷贝,Process() 调用过程中,local 总是有效的 2800 threadPool.QueueWork([=]{ Process(local); }); 2801} 2802``` 2803 2804### <a name="a10-3-2"></a>建议10.3.2 如果捕获`this`,则显式捕获所有变量 2805**理由** 2806在成员函数中的`[=]`看起来是按值捕获。但因为是隐式的按值获取了`this`指针,并能够操作所有成员变量,数据成员实际是按引用捕获的,一般情况下建议避免。如果的确需要这样做,明确写出对`this`的捕获。 2807 2808**示例** 2809 2810```cpp 2811class MyClass { 2812public: 2813 void Foo() 2814 { 2815 int i = 0; 2816 2817 auto Lambda = [=]() { Use(i, data_); }; // 不好: 看起来像是拷贝/按值捕获,成员变量实际上是按引用捕获 2818 2819 data_ = 42; 2820 Lambda(); // 调用 use(42); 2821 data_ = 43; 2822 Lambda(); // 调用 use(43); 2823 2824 auto Lambda2 = [i, this]() { Use(i, data_); }; // 好,显式指定按值捕获,最明确,最少的混淆 2825 } 2826 2827private: 2828 int data_ = 0; 2829}; 2830``` 2831 2832### <a name="a10-3-3"></a>建议10.3.3 避免使用默认捕获模式 2833**理由** 2834lambda表达式提供了两种默认捕获模式:按引用(&)和按值(=)。 2835默认按引用捕获会隐式的捕获所有局部变量的引用,容易导致访问悬空引用。相比之下,显式的写出需要捕获的变量可以更容易的检查对象生命周期,减小犯错可能。 2836默认按值捕获会隐式的捕获this指针,且难以看出lambda函数所依赖的变量是哪些。如果存在静态变量,还会让阅读者误以为lambda拷贝了一份静态变量。 2837因此,通常应当明确写出lambda需要捕获的变量,而不是使用默认捕获模式。 2838 2839**错误示例** 2840```cpp 2841auto func() 2842{ 2843 int addend = 5; 2844 static int baseValue = 3; 2845 2846 return [=]() { // 实际上只复制了addend 2847 ++baseValue; // 修改会影响静态变量的值 2848 return baseValue + addend; 2849 }; 2850} 2851``` 2852 2853**正确示例** 2854```cpp 2855auto func() 2856{ 2857 int addend = 5; 2858 static int baseValue = 3; 2859 2860 return [addend, baseValue = baseValue]() mutable { // 使用C++14的捕获初始化拷贝一份变量 2861 ++baseValue; // 修改自己的拷贝,不会影响静态变量的值 2862 return baseValue + addend; 2863 }; 2864} 2865``` 2866 2867参考:《Effective Modern C++》:Item 31: Avoid default capture modes. 2868 2869## <a name="c10-4"></a> 接口 2870### <a name="a10-4-1"></a>建议10.4.1 不涉及所有权的场景,使用`T*`或`T&`作为参数,而不是智能指针 2871**理由** 28721. 只在需要明确所有权机制时,才通过智能指针转移或共享所有权. 28732. 通过智能指针传递,限制了函数调用者必须使用智能指针(如调用者希望传递`this`)。 28743. 传递共享所有权的智能指针存在运行时的开销。 2875 2876**示例** 2877 2878```cpp 2879// 接受任何 int* 2880void F(int*); 2881 2882// 只能接受希望转移所有权的 int 2883void G(unique_ptr<int>); 2884 2885// 只能接受希望共享所有权的 int 2886void G(shared_ptr<int>); 2887 2888// 不改变所有权,但需要特定所有权的调用者 2889void H(const unique_ptr<int>&); 2890 2891// 接受任何 int 2892void H(int&); 2893 2894// 不好 2895void F(shared_ptr<Widget>& w) 2896{ 2897 // ... 2898 Use(*w); // 只使用 w -- 完全不涉及生命周期管理 2899 // ... 2900}; 2901``` 2902 2903