[关闭]
@taqikema 2020-02-06T08:03:19.000000Z 字数 10052 阅读 851

Google C++风格指南--学习笔记

Google C++风格 学习笔记


与上一篇华为 C语言规范的学习笔记类似,本篇Google C++风格指南的学习笔记也更多地是记录阅读过程中觉得比较重要的点和整体提纲。感兴趣的同学可以直接阅读原文,该文档已经有热心的人儿翻译成了中文,想学习的同学可以直接移步那里。
看完之后,最大的感觉就是四个字,言简意赅!讲的东西都是日常编程中会遇到的问题,并且用尽量简练的语言分析了某种做法的优缺点和结论。看起来很快,但收获却很多!

0. 扉页

0.1 译者前言

0.2 背景

本指南的另一个观点是 C++ 特性的臃肿. C++ 是一门包含大量高级特性的庞大语言. 某些情况下, 我们会限制甚至禁止使用某些特性. 这么做是为了保持代码清爽, 避免这些特性可能导致的各种问题. 指南中列举了这类特性, 并解释为什么这些特性被限制使用.

1. 头文件

1.1. Self-contained 头文件

1.用来存储特定文本内容的头文件可以用 inc结尾。

1.2. #define 保护

所有头文件都应该使用 #define 来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_

1.3. 前置声明

1.尽量避免前置声明那些定义在其他项目中的实体.
2.函数:总是使用 #include.
3.类模板:优先使用 #include

1.4. 内联函数

1.一个较为合理的经验准则是, 不要内联超过 10 行的函数. **谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
2.另一个实用的经验准则: **内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行)
.
3.虚函数和递归函数即使被声明为内联的,也不一定会被编译器内联。

1.5. #include 的路径及顺序

1.头文件 include顺序建议为:相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.
2.包含文件的名称使用 . 和 .. 虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理

2. 作用域

2.1. 命名空间

1.在命名空间的最后注释出命名空间的名字

2.2. 匿名命名空间和静态变量

1.在 .cc 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 static 。但是不要在 .h 文件中这么做。

2.3. 非成员函数、静态成员函数和全局函数

1.使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关.

2.4. 局部变量

1.将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
2.如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.

2.5. 静态和全局变量

1.禁止定义静态储存周期非POD变量(Plain Old Data,即 int, char 和 float, 以及 POD 类型的指针、数组和结构体),禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。
2.多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致的 bug

3. 类

3.1. 构造函数的职责

不要在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化

3.2. 隐式类型转换

不要定义隐式类型转换. 对于转换运算符和单参数构造函数, 请使用 explicit 关键字。

3.3. 可拷贝类型和可移动类型

1.如果你的类型需要, 就让它们支持拷贝 / 移动.
2.如果定义了拷贝/移动操作, 则要保证这些操作的默认实现(包括构造和赋值两个操作)是正确的. 记得时刻检查默认操作的正确性, 并且在文档中说明类是可拷贝的且/或可移动的.
3.由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现.
4.如果你的类不需要拷贝 / 移动操作, 请显式地通过在 public 域中使用 = delete 或其他手段禁用之.

3.4. 结构体 VS. 类

仅当只有数据成员时使用 struct, 其它一概使用 class

3.5. 继承

1.所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式。
2.不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 “是一个” (“is-a”, YuleFox 注: 其他 “has-a” 情况下请使用组合) 的情况下使用继承: 如果 Bar 的确 “是一种” Foo, Bar 才能继承 Foo
3.析构函数声明为 virtual. 如果你的类有虚函数, 则析构函数也应该为虚函数
4.对于重载的虚函数或虚析构函数, 使用 override, 或 (较不常用的) final 关键字显式地进行标记.

3.6. 多重继承

只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以 Interface 为后缀.

3.7. 接口

接口是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制).

3.8. 运算符重载

1.只有在意义明显, 不会出现奇怪的行为并且与对应的内建运算符的行为一致时才定义重载运算符.
2.只有对用户自己定义的类型重载运算符. 更准确地说, 将它们和它们所操作的类型定义在同一个头文件中, .cc 中和命名空间中.
3.如果你定义了一个运算符, 请将其相关且有意义的运算符都进行定义, 并且保证这些定义的语义是一致的. 例如, 如果你重载了 <, 那么请将所有的比较运算符都进行重载, 并且保证对于同一组参数, < 和 > 不会同时返回 true.
4.建议不要将不进行修改的二元运算符定义为成员函数.
5.不要重载 &&, ||, , 或一元运算符 &. 不要重载 operator"", 也就是说, 不要引入用户定义字面量.

3.9. 存取控制

3.10. 声明顺序

1.类定义一般应以 public: 开始, 后跟 protected:, 最后是 private:. 省略空部分.
2.在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它函数, 数据成员.

译者 (YuleFox) 笔记

1.编译器提供的默认构造函数不会对变量进行初始化, 如果定义了其他构造函数, 编译器不再提供, 需要编码者自行提供默认构造函数;
2.存取函数一般内联在头文件中。

4. 函数

4.1. 参数顺序

1.函数的参数顺序为: 输入参数在先, 后跟输出参数.
2.在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前.

4.2. 编写简短函数

2.倾向于编写简短, 凝练的函数。如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割。

4.3. 引用参数

1.函数参数列表中, 所有引用参数都必须是 const

4.4. 函数重载

1.使用函数重载时,尽量做到读者一看调用方式就知道进行了何种重载,而不是依靠 C++本身五花八门的重载规则。
2.如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append(). 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector 以便使用者可以用 列表初始化 指定参数.

4.5. 缺省参数

1.对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作. 如果在每个调用点缺省参数的值都有可能不同, 在这种情况下缺省函数也不允许使用

4.6. 函数返回类型后置语法

1.在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前. 只有在必需的时候 (如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法. 但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中, 而多数情况下不鼓励写这样复杂的模板代码.

5. 来自 Google 的奇技

5.1. 所有权与智能指针

1.动态分配出的对象最好有单一且固定的所有主, 并通过智能指针(shared\_ptruniqued\_ptr)传递所有权。

5.2. Cpplint

1.使用 cpplint.py 检查风格错误。
2. 在行尾加 // NOLINT, 或在上一行加 // NOLINTNEXTLINE, 可以忽略报错.

6. 其他 C++ 特性

6.1. 引用参数

所有按引用传递的参数必须加上 const.

6.2. 右值引用

只在定义移动构造函数与移动赋值操作时使用右值引用, 不要使用 std::forward 功能函数.你可能会使用 std::move 来表示将值从一个对象移动而不是复制到另一个对象.

6.3. 函数重载

6.4. 缺省参数

除以下几种情况外,不建议使用缺省参数:

6.5. 变长数组和 alloca()

不允许使用变长数组和 alloca(),改用更安全的分配器(allocator),就像 std::vectorstd::unique_ptr<T[]>

6.6. 友元

1.通常友元应该定义在同一文件内, 避免代码读者跑到其它文件查找使用该私有成员的类。
2.某些情况下, 将一个单元测试类声明成待测类的友元会很方便.

6.7. 异常

Goolge禁止使用异常,是因为 Google的现有代码中没有考虑异常,如果新代码中使用异常,那么与旧代码的整合工作将会变得十分巨大。但如果是一个崭新的工程,可以考虑使用异常,毕竟,使用异常利大于弊。比如,异常是处理构造函数失败的唯一途径。

6.8. 运行时类型识别

1.禁止使用 RTTI。
2.RTTI 在某些单元测试中非常有用. 比如进行工厂类测试时, 用来验证一个新建对象是否为期望的动态类型. RTTI 对于管理对象和派生对象的关系也很有用。
3.RTTI 有合理的用途但是容易被滥用, 因此在使用时请务必注意。

6.9. 类型转换

不要使用 C 风格类型转换. 而应该使用 C++ 风格.

6.10. 流

Google建议不要使用流, 除非是日志接口需要. 使用 printf 之类的代替。流最大的优势是在输出时不需要关心打印对象的类型. 这是一个亮点. 同时, 也是一个不足: 你很容易用错类型, 而编译器不会报警,从而导致错误。

6.11. 前置自增和自减

对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符

6.12. const 用法

const 变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障; 便于尽早发现错误. 因此, 我们强烈建议在任何可能的情况下使用 const:

6.13. constexpr 用法

constexpr 特性,方才实现了 C++ 在接口上打造真正常量机制的可能。好好用 constexpr 来定义真・常量以及支持常量的函数。避免复杂的函数定义,以使其能够与constexpr一起使用。 千万别痴心妄想地想靠 constexpr 来强制代码「内联」

6.14. 整型

1.C++ 没有指定整型的大小. 通常人们假定 short 是 16 位, int 是 32 位, long 是 32 位, long long 是 64 位.
2.C++ 内建整型中, 仅使用 int. 如果程序中需要不同大小的变量, 可以使用 <stdint.h> 中长度精确的整型, 如 int16_t.如果您的变量可能不小于 2^31 (2GiB), 就用 64 位变量比如 int64_t.
3.拿不准该用何种数据类型时,干脆用更大的类型。

6.15. 64 位下的可移植性

1.记住 sizeof(void *) != sizeof(int). 如果需要一个指针大小的整数要用 intptr_t
2.创建 64 位常量时使用 LLULL 作为后缀。

6.16. 预处理宏

使用宏时要非常谨慎!
值得庆幸的是, C++ 中, 宏不像在 C 中那么必不可少. 以往用宏展开性能关键的代码, 现在可以用内联函数替代. 用宏表示常量可被 const 变量代替. 用宏 “缩写” 长变量名可被引用代替。

6.17. 0, nullptr 和 NULL

1.整数用 0, 实数用 0.0, 指针用 nullptrNULL, 字符 (串) 用 '\0'.
2.整数用 0, 实数用 0.0, 这一点是毫无争议的.
3.对于指针 (地址值), 到底是用 0, NULL 还是 nullptr. C++11 项目用 nullptr; C++03 项目则用 NULL, 毕竟它看起来像指针。实际上,一些 C++ 编译器对 NULL 的定义比较特殊,可以输出有用的警告,特别是 sizeof(NULL) 就和 sizeof(0) 不一样。
4.字符 (串) 用 '\0', 不仅类型正确而且可读性好

6.18. sizeof

尽可能用 sizeof(varname) 代替 sizeof(type)

6.19. auto

auto 只能用在局部变量里用。别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化 auto 变量。

6.20. 列表初始化

用户自定义类型也可以定义接收 std::initializer_list<T> 的构造函数和赋值运算符,以自动列表初始化。

6.21. Lambda 表达式

适当使用 lambda 表达式,一般用在 STL 算法中较多。别用默认 lambda 捕获,所有捕获都要显式写出来。

6.22. 模板编程

模板编程有时候能够实现更简洁更易用的接口, 但是更多的时候却适得其反. 因此模板编程最好只用在少量的基础组件, 基础数据结构上, 因为模板带来的额外的维护成本会被大量的使用给分担掉。

6.23. Boost 库

只使用 Boost 中被认可的库.

6.24. C++11

适当用 C++11(前身是 C++0x)的库和语言扩展,在贵项目用 C++11 特性前三思可移植性,这主要是考虑代码的可维护性和编程人员的水平。

译者(acgtyrant)笔记

1.注意初始化 const 对象时,必须在初始化的同时值初始化。
2.用断言代替无符号整型类型,深有启发。

7. 命名约定

最重要的一致性规则是命名管理. 命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义: 类型, 变量, 函数, 常量, 宏等等。

7.1. 通用命名规则

尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词。

7.2. 文件命名

1.文件名要全部小写, 可以包含下划线 (_) 或连字符 (-), 依照项目的约定. 如果没有约定, 那么 “_” 更好.
2.C++ 文件要以 .cc 结尾, 头文件以 .h 结尾. 专门插入文本的文件则以 .inc 结尾。
3.通常应尽量让文件名更加明确. http_server_logs.h 就比 logs.h 要好. 定义类时文件名一般成对出现, 如 foo_bar.h 和 foo_bar.cc, 对应于类 FooBar。

7.3. 类型命名

1.类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.

7.4. 变量命名

1.变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.

7.5. 常量命名

声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合.

7.6. 函数命名

1.常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction()。
2.同样的命名规则同时适用于类作用域与命名空间作用域的常量, 因为它们是作为 API 的一部分暴露对外的, 因此应当让它们看起来像是一个函数。

7.7. 命名空间命名

命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.

7.8. 枚举命名

枚举的命名应当和 常量 或 宏 一致: kEnumName 或是 ENUM_NAME.

7.9. 宏命名

通常 不应该 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线

7.10. 命名规则的特例

8. 注释

注释固然很重要, 但最好的代码应当本身就是文档. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字.

8.1. 注释风格

///* */ 都可以; 但 // 更 常用. 要在如何注释及注释风格上确保统一.

8.2. 文件注释

文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释。

法律公告和作者信息

文件内容

不要在 .h 和 .cc 之间复制注释, 这样的注释偏离了注释的实际意义。

8.3. 类注释

如果类的声明和定义分开了(例如分别放在了 .h 和 .cc 文件中), 此时, 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.

8.4. 函数注释

函数声明

1.函数声明处注释的内容:

2.注释函数重载时, 注释的重点应该是函数中被重载的部分, 而不是简单的重复被重载的函数的注释. 多数情况下, 函数重载不需要额外的文档, 因此也没有必要加上注释.

函数定义

定义处的注释描述函数实现。

8.5. 变量注释

类数据成员

当变量可以接受 NULL 或 -1 等警戒值时, 须加以说明.

全局变量

8.6. 实现注释

代码前注释

行注释

比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释。

函数参数注释

函数参数意义不明显时,使用以下方式弥补:

不允许的行为

你所提供的注释应当解释代码 为什么 要这么做和代码的目的, 或者最好是让代码自文档化.

8.7. 标点, 拼写和语法

8.8. TODO 注释

8.9. 弃用注释

可以写上包含全大写的 DEPRECATED 的注释, 以标记某接口为弃用状态. 注释可以放在接口声明前, 或者同一行.

译者 (YuleFox) 笔记

9. 格式

9.1. 行长度

1.每一行代码字符数不超过 80.
2.如果无法在不伤害易读性的条件下进行断行, 那么注释行可以超过 80 个字符, 这样可以方便复制粘贴. 例如, 带有命令示例或 URL 的行可以超过 80 个字符.
3.包含长路径的 #include 语句可以超出80列.

9.2. 非 ASCII 字符

尽量不使用非 ASCII 字符, 如果使用的话, 参考 UTF-8 格式 (尤其是 UNIX/Linux 下, Windows 下可以考虑宽字符), 尽量不将字符串常量耦合到代码中, 比如独立出资源文件, 这不仅仅是风格问题了。

9.3. 空格还是制表位

UNIX/Linux 下无条件使用空格, MSVC 的话使用 Tab 也无可厚非。

9.4. 函数声明与定义

1.返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行。
2.只有在参数未被使用或者其用途非常明显时, 才能省略参数名。
3.未被使用的参数如果其用途不明显的话, 在函数定义处将参数名注释起来。

9.5. Lambda 表达式

引用捕获, 在变量名和 & 之间不留空格。

9.6. 函数调用

1.要么一行写完函数调用, 要么在圆括号里对参数分行, 要么参数另起一行且缩进四格. 如果没有其它顾虑的话, 尽可能精简行数, 比如把多个参数适当地放在同一行里。
2.如果某参数独立成行, 对可读性更有帮助的话, 那也可以如此做. 参数的格式处理应当以可读性而非其他作为最重要的原则.

9.7. 列表初始化格式

9.8. 条件语句

1.关键字和圆括号之间有空格,圆括号的条件之间没有空格。
2.有else分支时,不允许将条件语句与 if写在同一行。
3.某个 if-else 分支使用了大括号的话, 其它分支也必须使用。

9.9. 循环和开关选择语句

1.如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning). 如果 default 应该永远执行不到, 简单的加条 assert。
2.空循环体应使用 {} 或 continue, 而不是一个简单的分号.

9.10. 指针和引用表达式

句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.

9.11. 布尔表达式

9.12. 函数返回值

不要在 return 表达式里加上非必须的圆括号.

9.13. 变量及数组初始化

注意(){}在初始化时的区别:

  1. vector<int> v(100, 1); // 内容为 100 个 1 的向量.
  2. vector<int> v{100, 1}; // 内容为 100 和 1 的向量.

9.14. 预处理指令

预处理指令不要缩进, 从行首开始.

9.15. 类格式

注意事项:

9.16. 构造函数初始值列表

9.17. 命名空间格式化

命名空间内容不缩进.

9.19. 水平留白

永远不要在行尾添加没意义的留白.

9.19. 垂直留白

函数体内开头或结尾的空行可读性微乎其微.
在多重 if-else 块里加空行或许有点可读性

10. 规则特例

10.1. 现有不合规范的代码

当你修改使用其他风格的代码时, 为了与代码原有风格保持一致可以不使用本指南约定。

10.2. Windows 代码

1.不要使用匈牙利命名法 (比如把整型变量命名成 iNum). 使用 Google 命名约定, 包括对源文件使用 .cc 扩展名。
2.尽量使用原有的 C++ 类型, 例如使用 const TCHAR * 而不是 LPCTSTR。
3.使用 Microsoft Visual C++ 进行编译时, 将警告级别设置为 3 或更高, 并将所有警告(warnings)当作错误(errors)处理。
4.不要使用 #pragma once; 而应该使用 Google 的头文件保护规则. 头文件保护的路径应该相对于项目根目录。

11. 结束语

新代码应该要尽量与旧代码的风格保持一致!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注