005《Google C++ Style Guide》读书笔记
1. 背景 (Background)
1.1. 动机 (Motivation)
① 本文档旨在为 Google 的 C++ 代码提供详细的风格指南。通过遵循这些规则,我们可以使我们的代码更易于维护、阅读和协作。
② C++ 是一种功能强大但复杂的语言。其强大的功能可能导致代码可读性差,从而导致难以发现的错误和高昂的维护成本。本指南旨在通过详细描述如何以一致的方式使用 C++ 来避免这些陷阱。
③ 本指南基于 Google 多年来使用 C++ 的经验,并参考了广泛接受的 C++ 社区最佳实践。我们相信,遵循这些规则将有助于我们编写出高质量、可扩展且高效的 C++ 代码。
1.2. 范围 (Scope)
① 本指南主要关注 Google 内部使用的 C++ 代码。虽然其中的许多规则也适用于其他 C++ 项目,但某些规则可能是 Google 特有的。
② 本指南涵盖了 C++ 语言的各个方面,包括头文件、作用域、类、函数、命名约定、注释、格式化以及其他 C++ 特性。
③ 本指南是一个活文档,会随着 C++ 语言的发展和我们经验的积累而不断更新和完善。
1.3. 如何使用本指南 (How to Use This Guide)
① 本指南旨在作为编写新 C++ 代码的参考。对于现有的代码,除非有明确的理由,否则不需要强制进行大规模的风格调整。然而,在修改现有代码时,建议尽量遵循本指南中的规则。
② 如果你对本指南中的任何规则有疑问或不同意,请与你的团队成员讨论并达成共识。在某些情况下,为了代码的清晰性和可维护性,可以允许例外(参见“规则的例外”章节)。
③ 自动化工具(例如,Clang Format)可以帮助强制执行本指南中的许多格式规则。建议在你的开发工作流程中集成这些工具。
1.4. 致谢 (Acknowledgements)
① 本指南是许多 Google 工程师多年来辛勤工作的成果。我们感谢所有为本指南的创建和维护做出贡献的人。
② 本指南也借鉴了 C++ 社区的许多优秀资源,包括书籍、文章和公开的风格指南。
1.5. 风格指南的目标 (Goals of the Style Guide)
1.5.1. 代码清晰且一致 (Code Should Be Clear and Consistent)
① 代码应该易于阅读和理解。一致的风格有助于减少认知负担,使开发人员能够更快地理解代码的意图。
② 一致性还降低了引入 bug 的可能性,因为当代码看起来都相似时,更容易发现不规则之处。
1.5.2. 代码应该易于维护 (Code Should Be Maintainable)
① 代码在整个生命周期内都会被修改。遵循一致的风格可以使其他人(包括未来的你)更容易理解和修改代码,而不会引入新的 bug。
② 清晰的代码结构和良好的注释对于代码的可维护性至关重要。
1.5.3. 代码应该经过充分测试 (Code Should Be Well-Tested)
① 风格指南本身并不能保证代码的正确性,但它可以提高代码的可测试性。清晰、模块化的代码更容易进行单元测试。
② 遵循某些规则(例如,避免全局变量)可以减少代码中的隐藏依赖关系,从而使测试更加可靠。
1.5.4. 允许合理的折衷 (Reasonable Compromises Are Acceptable)
① 虽然本指南提供了许多具体的规则,但我们认识到在某些情况下,严格遵守所有规则可能并不总是最佳选择。
② 关键在于做出明智的决策,并在代码的清晰性、一致性和可维护性之间找到合理的平衡。如果某个规则在特定情况下会降低代码的可读性或可维护性,那么可以考虑偏离该规则,但需要清晰地记录原因。
1.5.5. 风格指南是不断演进的 (The Style Guide Is a Living Document)
① C++ 语言在不断发展,我们的经验也在不断积累。本风格指南会随着时间的推移而更新和完善。
② 我们鼓励大家积极参与到风格指南的讨论和改进中来。如果你认为某个规则应该修改或添加新的规则,请提出你的建议。
2. 头文件 (Header Files)
2.1. 自包含头文件 (Self-contained Headers)
2.1.1. 规则 (Rule)
① 头文件应该自包含并且以 .h
结尾。
2.1.2. 说明 (Explanation)
所有头文件都应该能够被单独包含,而不需要包含任何其他文件。为了实现这个目的,一个头文件需要:
① 包含(#include
)它所需要的任何其他头文件。
② 前向声明(forward declare)来自其他头文件的任何符号。
很少有不满足这个规则的例外情况,最主要的例外是当一个头文件被设计成只能被另一个头文件直接包含时。即使在这种情况下,也强烈建议主要的头文件自包含,因为它能提高可读性和可维护性。
自包含头文件的主要优点是它们使得依赖关系的管理更加容易。如果一个头文件不是自包含的,那么它可能会意外地依赖于其他头文件被包含的顺序。这会导致构建错误,并且很难理解和调试。
2.1.3. 如何修复 (How to Fix)
要使一个头文件自包含,你需要确保它包含了它所依赖的所有头文件。你可以通过以下步骤来做到这一点:
① 从头文件中删除所有不必要的 #include
语句。
② 尝试单独包含头文件。如果出现任何编译错误,那么你需要添加缺少的 #include
语句或者进行前向声明。
2.2. #define
保护 (The #define
Guard)
2.2.1. 规则 (Rule)
① 所有头文件都应该使用 #define
保护来防止多重包含。
2.2.2. 说明 (Explanation)
#define
保护(也称为预处理器保护或包含保护)是一种防止头文件在同一个编译单元中被多次包含的机制。当一个头文件被多次包含时,可能会导致编译错误,并且会增加编译时间。
#define
保护的工作原理是使用预处理器宏来标记一个头文件是否已经被包含过。当预处理器遇到一个已经被标记为已包含的头文件时,它会跳过该头文件的内容。
#define
保护的标准格式如下:
1
#ifndef <PROJECT>_<PATH>_<FILE>_H_
2
#define <PROJECT>_<PATH>_<FILE>_H_
3
4
// 头文件内容
5
6
#endif // <PROJECT>_<PATH>_<FILE>_H_
<PROJECT>
应该是项目的名称(或一个简短的缩写)。<PATH>
应该是头文件相对于项目根目录的路径。<FILE>
应该是头文件的名称(不包括 .h
扩展名)。所有这些部分都应该转换为大写,并且用下划线分隔。
例如,对于项目 foo
中位于 bar/baz.h
的头文件,#define
保护应该是:
1
#ifndef FOO_BAR_BAZ_H_
2
#define FOO_BAR_BAZ_H_
3
4
// 头文件内容
5
6
#endif // FOO_BAR_BAZ_H_
2.2.3. 如何修复 (How to Fix)
要为一个头文件添加 #define
保护,你需要按照上述标准格式添加 #ifndef
、#define
和 #endif
语句。确保宏名称是唯一的,并且遵循命名约定。
2.3. 前向声明 (Forward Declarations)
2.3.1. 规则 (Rule)
① 尽可能使用前向声明来代替包含头文件。
2.3.2. 说明 (Explanation)
前向声明是一种在不包含完整类、函数或模板定义的情况下声明它们的方法。你可以通过简单地声明它们的名称来实现这一点:
1
class Foo;
2
namespace bar {
3
class Baz;
4
}
当你只需要使用一个类或结构的指针或引用时,或者当你声明一个返回或接受一个类或结构的函数时,可以使用前向声明。
使用前向声明而不是包含头文件的主要优点是它可以减少编译时间。当包含一个头文件时,编译器需要解析和编译该头文件中的所有代码,以及它所依赖的所有其他头文件。这可能会花费大量时间,特别是对于大型项目。
前向声明还可以帮助减少头文件之间的依赖关系,这可以提高代码的可维护性。
2.3.3. 何时不使用前向声明 (When Not to Use Forward Declarations)
在某些情况下,不能使用前向声明,而必须包含头文件:
① 当你需要访问一个类或结构的成员(例如,调用方法或访问成员变量)时。
② 当你需要继承一个类或结构时。
③ 当你需要使用模板类的特定实例时。
2.3.4. 如何修复 (How to Fix)
要使用前向声明而不是包含头文件,你需要找到所有只需要使用指针或引用的地方,并将 #include
语句替换为相应的前向声明。
2.4. 内联函数 (Inline Functions)
2.4.1. 规则 (Rule)
① 只有当函数非常小并且对性能至关重要时,才应该在头文件中定义内联函数。
2.4.2. 说明 (Explanation)
在头文件中定义的函数默认是内联的。内联函数是指编译器尝试在调用点直接替换函数体的函数。这可以消除函数调用的开销,从而提高性能。
然而,过度使用内联函数会导致代码膨胀,这会增加可执行文件的大小并降低缓存局部性。因此,只有当函数非常小并且对性能至关重要时,才应该在头文件中定义内联函数。
一个好的经验法则是,如果一个函数超过 10 行代码,则不应该将其定义在头文件中。对于更长的函数,应该将其定义在源文件中。
2.4.3. 模板 (Templates)
模板通常必须定义在头文件中,因为编译器需要在使用模板时知道其完整定义。
2.4.4. 如何修复 (How to Fix)
如果你在一个头文件中定义了一个不应该内联的函数,你应该将该函数的定义移动到一个源文件中。你仍然可以在头文件中保留函数的声明。
2.5. 标准库头文件 (Standard Library Header Files)
2.5.1. 规则 (Rule)
① 尽可能包含标准库的头文件,而不是 C 风格的标准库头文件。
2.5.2. 说明 (Explanation)
C++ 标准库提供了许多有用的类和函数,它们被声明在各种头文件中。C 风格的标准库也提供了类似的功能,但它们的头文件通常具有不同的命名约定(例如,<stdio.h>
而不是 <cstdio>
)。
应该尽可能包含 C++ 风格的标准库头文件,因为它们提供了更好的类型安全性和命名空间管理。例如,<cstdio>
将函数声明在 std
命名空间中,而 <stdio.h>
将它们声明在全局命名空间中。
2.5.3. 如何修复 (How to Fix)
将所有 C 风格的标准库头文件(例如,<stdio.h>
、<stdlib.h>
、<string.h>
、<math.h>
等)替换为相应的 C++ 风格的头文件(例如,<cstdio>
、<cstdlib>
、<cstring>
、<cmath>
等)。
2.6. 作用域 (Scope)
2.6.1. 规则 (Rule)
① 不要在头文件中放置具有全局作用域的变量或非 inline
函数定义。
2.6.2. 说明 (Explanation)
在头文件中定义具有全局作用域的变量或非 inline
函数会导致链接错误。当一个头文件被多个源文件包含时,全局变量或非 inline
函数的定义会在每个包含该头文件的编译单元中都存在一份。这会导致链接器在链接这些编译单元时遇到多个相同符号的定义,从而产生链接错误。
全局常量通常可以安全地定义在头文件中,因为它们在链接时通常会被折叠(collapsed)。然而,为了避免潜在的问题,最好还是将它们的定义放在源文件中,并在头文件中使用 extern
声明。
2.6.3. 如何修复 (How to Fix)
将所有具有全局作用域的变量和非 inline
函数定义从头文件中移动到源文件中。在头文件中,你可以使用 extern
关键字声明全局变量。
2.7. 包含的顺序 (Include Ordering)
2.7.1. 规则 (Rule)
① 按照以下顺序包含头文件:
① 项目自己的头文件。
② 第三方库或框架的头文件。
③ C++ 标准库的头文件。
④ C 风格的标准库头文件。
2.7.2. 说明 (Explanation)
包含头文件的顺序可能会影响编译时间和链接。按照推荐的顺序包含头文件可以帮助减少这些问题。
将项目自己的头文件放在最前面可以确保它们优先于其他头文件被编译。这有助于在早期发现项目内部的依赖问题。
将第三方库或框架的头文件放在其次,因为它们通常比标准库的头文件更复杂,并且可能依赖于其他头文件。
将 C++ 标准库的头文件放在再次,因为它们是语言的标准部分,并且通常具有良好的兼容性。
最后,将 C 风格的标准库头文件放在最后,因为它们通常是最小的,并且应该尽可能少地使用。
2.7.3. 如何修复 (How to Fix)
检查你的头文件,并确保它们按照推荐的顺序包含。如果顺序不正确,请重新排列 #include
语句。
2.8. 避免使用前置斜杠 (Avoid Using Forward Slashes)
2.8.1. 规则 (Rule)
① 在包含本地头文件时,使用双引号 ""
而不是尖括号 <>
。
2.8.2. 说明 (Explanation)
尖括号 <>
用于包含系统头文件和第三方库的头文件。编译器会在系统包含路径中搜索这些头文件。
双引号 ""
用于包含项目自己的头文件。编译器会首先在当前目录中搜索这些头文件,然后在系统包含路径中搜索。
使用正确的引号可以帮助编译器更快地找到头文件,并且可以避免命名冲突。
2.8.3. 如何修复 (How to Fix)
检查你的 #include
语句,并确保你使用双引号来包含本地头文件。
3. 作用域 (Scoping)
3.1. 命名空间 (Namespaces)
3.1.1. 规则 (Rule)
① 所有的 C++ 代码都应该放在命名空间中,除了 main()
函数、那些将作为库被广泛使用的代码以及某些必须放在全局命名空间的样板代码,例如标志定义。
3.1.2. 说明 (Explanation)
命名空间提供了一种避免命名冲突的方法。通过将你的代码放在一个命名空间中,你可以避免与其他库或项目中的代码发生命名冲突。
3.1.3. 匿名命名空间和静态 (Unnamed Namespaces and Static)
① 当在 .cc
文件中定义不需要在其他地方引用的文件作用域的符号时,使用匿名命名空间。不要使用 static
。
3.1.4. 说明 (Explanation)
在 .cc
文件中,匿名命名空间提供了更明确且更少出错的方式来表示符号的文件作用域。static
关键字的含义取决于上下文,并且在命名空间中可能令人困惑。匿名命名空间可以更清晰地表达意图。
1
// foo.cc
2
namespace { // 匿名命名空间
3
int helper_function() { ... }
4
} // namespace
3.1.5. 嵌套命名空间 (Nested Namespaces)
① 鼓励使用嵌套命名空间。
3.1.6. 说明 (Explanation)
对于大型项目,嵌套命名空间可以更好地组织代码并减少命名冲突的可能性。你应该按照项目的目录结构或其他逻辑结构来组织你的命名空间。
1
namespace google {
2
namespace protobuf {
3
...
4
} // namespace protobuf
5
} // namespace google
3.1.7. 不要在头文件中使用 using
声明 (Do Not Use using
Statements in Header Files)
① 在头文件中,不要使用 using
声明来引入命名空间中的所有名称。
3.1.8. 说明 (Explanation)
当你在头文件中使用 using namespace foo;
时,你将命名空间 foo
中的所有名称都引入到了包含该头文件的每个源文件中。这可能会导致命名冲突,并且使得很难理解代码中使用的名称来自哪里。
在头文件中,你应该总是使用完整的命名空间限定符(例如,foo::bar
)或者使用单个 using
声明来引入特定的名称。
1
// 好的做法:
2
namespace foo {
3
class Bar;
4
}
5
class Baz {
6
public:
7
void DoSomething(foo::Bar* bar);
8
};
9
10
// 也可以:
11
namespace foo {
12
class Bar;
13
}
14
using foo::Bar;
15
class Baz {
16
public:
17
void DoSomething(Bar* bar);
18
};
19
20
// 不好的做法 (在头文件中):
21
namespace foo {
22
class Bar;
23
}
24
using namespace foo;
25
class Baz {
26
public:
27
void DoSomething(Bar* bar);
28
};
3.1.9. 不要在命名空间中声明标准库中的实体 (Do Not Declare Standard Library Entities in Namespaces)
① 不要尝试在任何命名空间中声明标准库中的实体,即使是标准库命名空间。这样做会导致未定义的行为。
1
namespace std {
2
template <typename T>
3
void swap(T& a, T& b) { ... } // 不要这样做
4
} // namespace std
3.2. 局部变量 (Local Variables)
3.2.1. 规则 (Rule)
① 将函数的局部变量保持在尽可能小的作用域内。
3.2.2. 说明 (Explanation)
将局部变量的作用域限制在它们被使用的代码块内可以提高代码的可读性和可维护性。它还可以减少意外使用未初始化或不再需要的变量的可能性。
3.2.3. 在循环中声明变量 (Declare Variables Inside Loops)
① 如果一个变量只在循环中使用,那么应该在循环内部声明它。
1
for (int i = 0; i < count; ++i) {
2
// 使用 i
3
}
3.2.4. 在条件语句中声明变量 (Declare Variables Inside Conditional Statements)
① 如果一个变量只在 if
语句中使用,那么应该在 if
语句内部声明它。
1
if (condition) {
2
int result = CalculateResult();
3
// 使用 result
4
}
3.3. 全局变量 (Global Variables)
3.3.1. 规则 (Rule)
① 避免使用全局变量。
3.3.2. 说明 (Explanation)
全局变量可能会导致代码难以理解和维护。它们会引入隐藏的依赖关系,并且使得很难跟踪变量的值是如何被修改的。全局变量还会使得代码难以进行单元测试。
如果需要全局数据,考虑使用单例模式或将数据作为类的静态成员。
3.3.3. 常量作为例外 (Constants as an Exception)
① 命名空间的常量定义可以接受。
3.3.4. 说明 (Explanation)
命名空间中的常量(例如,const int kMaxConnections = 10;
)通常是安全的,因为它们的值不会改变。然而,即使是常量也应该谨慎使用,并且只有在真正需要全局可见性时才应该使用。
3.4. 静态成员变量 (Static Member Variables)
3.4.1. 规则 (Rule)
① 类中的静态成员变量应该定义在 .cc
文件中,而不是在头文件中。
3.4.2. 说明 (Explanation)
与全局变量类似,在头文件中定义静态成员变量会导致链接错误。当一个头文件被多个源文件包含时,静态成员变量的定义会在每个包含该头文件的编译单元中都存在一份。
你应该在类的头文件中声明静态成员变量,然后在其中一个 .cc
文件中定义它。
1
// my_class.h
2
class MyClass {
3
public:
4
static int counter;
5
};
6
7
// my_class.cc
8
int MyClass::counter = 0;
3.5. 线程局部存储 (Thread-Local Storage)
3.5.1. 规则 (Rule)
① 合理且谨慎地使用线程局部存储。
3.5.2. 说明 (Explanation)
线程局部存储(Thread-Local Storage, TLS)是一种为每个线程提供变量独立副本的机制。这对于编写线程安全的代码非常有用,因为每个线程都可以访问其自己的数据副本,而无需担心与其他线程的冲突。
然而,过度使用 TLS 可能会导致性能问题和难以调试的代码。你应该只在真正需要为每个线程提供独立数据副本时才使用 TLS。
在 C++11 中,可以使用 thread_local
存储类说明符来声明线程局部变量。
1
thread_local int counter = 0;
在使用 TLS 时,请务必考虑其生命周期和初始化。
3.6. 避免隐藏其他作用域的变量 (Avoid Hiding Other Scoped Variables)
3.6.1. 规则 (Rule)
① 不要声明与外层作用域中已存在的变量同名的局部变量。
3.6.2. 说明 (Explanation)
隐藏外层作用域中的变量会导致代码难以理解和调试。当你在内层作用域中声明一个与外层作用域中已存在的变量同名的变量时,内层作用域中的变量会“隐藏”外层作用域中的变量。这意味着在内层作用域中,你将无法访问外层作用域中的变量。
这可能会导致意外的行为和难以发现的错误。你应该总是为你的局部变量选择唯一的名称,以避免隐藏其他作用域中的变量。
1
int global_variable = 0;
2
3
void MyFunction() {
4
int global_variable = 10; // 避免这样做,它隐藏了全局变量
5
// 在这里,global_variable 的值是 10,而不是 0
6
}
4. 类 (Classes)
4.1. 构造函数 (Constructor)
4.1.1. 规则 (Rule)
① 不要在构造函数中调用虚函数,也不要在对象还没有完全构造完成时使用 dynamic_cast
。
4.1.2. 说明 (Explanation)
在构造函数的执行期间,对象的类型是不断变化的。基类的构造函数首先执行,然后是派生类的构造函数。在基类构造函数执行时,对象被认为是基类的实例。因此,调用虚函数会解析到基类的实现,即使在派生类中该函数已被覆盖。同样,在对象完全构造之前使用 dynamic_cast
可能会导致不可预测的行为或 nullptr
返回。
4.2. 隐式类型转换 (Implicit Type Conversions)
4.2.1. 规则 (Rule)
① 不要定义隐式类型转换。对于类类型,应该使用显式的转换方法。
4.2.2. 说明 (Explanation)
隐式类型转换会使代码难以理解和调试,因为它们可能会在不期望的情况下发生。为了避免这种情况,C++ 提供了 explicit
关键字。如果一个构造函数被声明为 explicit
,那么它只能被用于显式地构造对象,而不能用于隐式类型转换。
1
class Foo {
2
public:
3
explicit Foo(int x);
4
};
5
6
void Bar(Foo f);
7
8
Bar(42); // 错误:不能从 int 隐式转换为 Foo
9
Bar(Foo(42)); // 正确
对于提供类型转换的类,应该提供显式的方法(例如,AsInt()
、AsString()
等)。
4.2.3. 例外 (Exception)
在极少数情况下,如果类型转换是显而易见的且不会引起歧义,则可以允许使用隐式类型转换。例如,单参数的拷贝构造函数通常可以不使用 explicit
。
4.3. 可拷贝性与可移动性 (Copyability and Moveability)
4.3.1. 规则 (Rule)
① 如果你的类需要拷贝或移动的行为,请显式地声明拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。
4.3.2. 说明 (Explanation)
C++ 允许你通过定义特殊的成员函数来控制对象的拷贝和移动方式。如果你不显式地定义这些函数,编译器可能会为你生成默认的版本。然而,默认版本可能并不总是正确的,特别是当你的类管理资源(例如,内存、文件句柄)时。
如果你希望禁止拷贝或移动,你应该将相应的函数声明为 = delete
。
1
class Uncopyable {
2
public:
3
Uncopyable(const Uncopyable&) = delete;
4
Uncopyable& operator=(const Uncopyable&) = delete;
5
};
如果你定义了任何拷贝/移动操作,你应该同时定义所有五个特殊成员函数(拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数)。这条规则被称为“五法则”(Rule of Five)。在 C++11 之后,如果你的类只包含可以安全移动的成员,你可以使用 = default
来显式地请求编译器生成默认的移动操作。在这种情况下,如果需要,你仍然需要显式地处理拷贝操作。这条规则被称为“零法则”(Rule of Zero)或者“五法则的演变”(Evolution of the Rule of Five),提倡优先使用 RAII(Resource Acquisition Is Initialization)来管理资源,从而避免显式地编写拷贝/移动操作。
4.3.3. 何时需要自定义拷贝/移动 (When to Provide Custom Copying/Moving)
你需要自定义拷贝/移动操作的情况包括:
① 你的类拥有需要深拷贝的指针成员。
② 你的类管理需要在拷贝或移动时进行特殊处理的资源。
③ 你想要记录拷贝或移动操作。
④ 你想要禁止拷贝或移动。
4.4. 结构体 vs. 类 (Structs vs. Classes)
4.4.1. 规则 (Rule)
① 使用 struct
来表示仅包含数据的对象。对于其他对象,使用 class
。
4.4.2. 说明 (Explanation)
在 C++ 中,struct
和 class
几乎是相同的。唯一的区别是默认的访问修饰符:struct
的成员默认是 public
的,而 class
的成员默认是 private
的。
按照 Google C++ 风格指南,如果一个对象主要用于保存数据,并且没有重要的行为(方法),那么应该使用 struct
。所有其他类型的对象都应该使用 class
。这有助于区分简单的数据结构和更复杂的对象。
1
// 好的做法:
2
struct Point {
3
int x;
4
int y;
5
};
6
7
// 好的做法:
8
class Shape {
9
public:
10
virtual void Draw() = 0;
11
};
4.5. 继承 (Inheritance)
4.5.1. 规则 (Rule)
① 只有当子类真正是父类的一种特殊类型时,才使用公有继承(public inheritance
)。所有其他的继承都应该是私有的(private inheritance
)或保护的(protected inheritance
)。
4.5.2. 说明 (Explanation)
公有继承表示“是一个”(is-a)关系。这意味着子类的对象可以被当作父类的对象来使用。例如,Dog
是 Animal
的一种,所以 Dog
可以公有继承自 Animal
。
私有继承表示“有一个”(has-a)关系或者“用...来实现”(is-implemented-in-terms-of)。这意味着子类使用父类的实现细节,但子类的对象不能被当作父类的对象来使用。
保护继承类似于私有继承,但它允许子类的子类访问父类的保护成员。
应该谨慎使用继承,因为它会增加代码的复杂性。优先考虑使用组合(composition)来代替继承。
4.5.3. 多重继承 (Multiple Inheritance)
① 强烈建议不要使用多重继承。
4.5.4. 说明 (Explanation)
多重继承会引入复杂性,例如菱形继承问题,并且可能导致代码难以理解和维护。在大多数情况下,可以使用其他技术(例如,组合、接口)来代替多重继承。
如果必须使用多重继承,请确保仔细考虑其设计和潜在的复杂性。
4.6. 接口 (Interfaces)
4.6.1. 规则 (Rule)
① 当一个类只包含纯虚函数时,可以将其定义为接口。
4.6.2. 说明 (Explanation)
接口定义了一组方法,任何实现该接口的类都必须提供这些方法的具体实现。在 C++ 中,可以使用包含纯虚函数的抽象类来定义接口。纯虚函数是指在基类中声明但没有定义的虚函数,使用 = 0
来标记。
1
class Drawable {
2
public:
3
virtual ~Drawable() {}
4
virtual void Draw() = 0;
5
};
接口可以用于实现多态性,并允许不同的类以统一的方式进行交互。
4.7. 操作符重载 (Operator Overloading)
4.7.1. 规则 (Rule)
① 只有当操作符的含义是明确且与内置类型的用法一致时,才重载操作符。
4.7.2. 说明 (Explanation)
操作符重载允许你为自定义类型定义操作符的行为。虽然这可以使代码更具表现力,但滥用操作符重载会使代码难以理解。
应该只重载那些含义明确且与内置类型用法一致的操作符。例如,对于表示向量或矩阵的类,可以重载加法、减法和乘法操作符。然而,不应该重载那些含义不明确或可能引起混淆的操作符。
4.7.3. 避免重载逗号操作符 (Avoid Overloading the Comma Operator)
① 不要重载逗号操作符。
4.7.4. 说明 (Explanation)
逗号操作符在 C++ 中具有特殊的含义(按顺序计算表达式并返回最后一个表达式的值)。重载逗号操作符可能会导致非常令人困惑的代码。
4.8. 访问控制 (Access Control)
4.8.1. 规则 (Rule)
① 使用 private
来修饰数据成员。使用 protected
很少。使用 public
来修饰接口方法。
4.8.2. 说明 (Explanation)
应该尽可能地限制类成员的访问权限,以提高封装性并减少意外修改数据的可能性。
private
成员只能在类的内部访问。protected
成员可以在类的内部以及派生类中访问。public
成员可以在任何地方访问。
通常,数据成员应该是 private
的,并通过 public
的访问器(getter)和修改器(setter)方法来访问。protected
成员应该谨慎使用,因为它们会破坏封装性,并使得修改基类的实现变得更加困难。public
成员应该只用于定义类的接口。
4.8.3. 友元 (Friends)
① 只有在必要时才使用友元类和友元函数。
4.8.4. 说明 (Explanation)
友元机制允许一个类或函数访问另一个类的 private
和 protected
成员。虽然友元有时是必要的(例如,用于实现某些操作符重载或在两个密切相关的类之间共享数据),但过度使用友元会破坏封装性,并使得代码难以理解和维护。
4.9. 虚函数 (Virtual Functions)
4.9.1. 规则 (Rule)
① 只有当需要运行时多态时才使用虚函数。
4.9.2. 说明 (Explanation)
虚函数允许在运行时根据对象的实际类型来调用正确的函数实现。这对于实现继承和多态非常有用。
如果一个函数在基类中被声明为 virtual
,那么它可以在派生类中被覆盖。当通过基类指针或引用调用该函数时,会调用与对象的实际类型相对应的实现。
如果不需要运行时多态,则不应该使用虚函数,因为它们会增加一些开销。
4.9.3. 纯虚函数 (Pure Virtual Functions)
① 使用纯虚函数来定义接口。
4.9.4. 说明 (Explanation)
纯虚函数是在基类中声明但没有定义的虚函数,使用 = 0
来标记。包含至少一个纯虚函数的类是抽象类,不能被实例化。派生类必须提供纯虚函数的具体实现才能被实例化。
纯虚函数常用于定义接口。
4.9.5. 虚析构函数 (Virtual Destructors)
① 如果一个类有可能作为基类被继承,并且该类拥有任何资源(例如,通过 new
分配的内存),那么它应该声明一个虚析构函数。
4.9.6. 说明 (Explanation)
当通过基类指针删除一个派生类对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类拥有的资源没有被正确释放,从而导致内存泄漏或其他问题。
通过将基类的析构函数声明为虚函数,可以确保在删除派生类对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数,从而保证所有资源都被正确释放。
如果一个类不打算作为基类被继承,或者它不拥有任何资源,那么不需要声明虚析构函数。
4.10. 友元 (Friends)
4.10.1. 规则 (Rule)
① 只有在必要时才使用友元类和友元函数。
4.10.2. 说明 (Explanation)
友元机制允许一个类或函数访问另一个类的 private
和 protected
成员。虽然友元有时是必要的(例如,用于实现某些操作符重载或在两个密切相关的类之间共享数据),但过度使用友元会破坏封装性,并使得代码难以理解和维护。
尽可能地使用公有的成员函数来访问和修改对象的状态,而不是使用友元。
4.11. 空类 (Empty Classes)
4.11.1. 规则 (Rule)
① 空类应该保持简洁,不包含任何非必要的成员。
4.11.2. 说明 (Explanation)
有时需要定义一个空类,例如作为类型标记或用于模板编程。这样的类应该保持简洁,只包含必要的构造函数和析构函数(如果需要)。避免在空类中添加不必要的数据成员或方法。
5. 函数 (Functions)
5.1. 函数的长度 (Function Length)
5.1.1. 规则 (Rule)
① 我们倾向于简短、功能单一的函数。
5.1.2. 说明 (Explanation)
长函数有时难以理解、测试和维护。如果一个函数超过 40 行,可以考虑在不显著增加代码复杂性的情况下将其拆分为更小的函数。
当然,并不存在绝对的行数限制,有时较长的函数也是合适的。关键在于函数应该具有明确的目的,并且其实现应该清晰易懂。
5.2. 函数的签名 (Function Signatures)
5.2.1. 规则 (Rule)
① 函数签名应该清晰明了,并且能够准确地表达函数的输入和输出。
5.2.2. 说明 (Explanation)
函数的签名包括函数名、参数列表和返回类型。一个好的函数签名应该能够让调用者很容易理解如何使用该函数以及它会返回什么。
5.2.3. 输出参数 (Output Parameters)
① 优先使用返回值而不是输出参数。
5.2.4. 说明 (Explanation)
虽然可以使用指针或引用作为输出参数来返回多个值,但返回值通常更清晰且更不容易出错。返回值可以明确地表示函数的计算结果。
1
// 好的做法:
2
std::optional<int> FindValue(const std::vector<int>& data, int key);
3
4
// 不好的做法:
5
bool FindValue(const std::vector<int>& data, int key, int* result);
只有在以下情况下,输出参数才是合理的:
① 函数需要返回多个逻辑上不相关的输出。
② 由于性能原因,需要避免返回值的大型对象。在这种情况下,考虑使用移动语义。
③ 函数的目的是修改通过引用传递的参数。
5.2.5. const
的使用 (Use of const
)
① 尽可能将不修改输入参数的函数参数声明为 const
。
5.2.6. 说明 (Explanation)
使用 const
可以提高代码的可读性,并帮助编译器进行优化。它还可以防止函数意外地修改不应该被修改的参数。
5.2.7. 引用参数 (Reference Arguments)
① 对于输入参数,优先使用 const
引用,而不是值传递。这可以避免不必要的拷贝,特别是对于大型对象。
② 对于输出参数,使用非 const
指针或引用。
5.2.8. 函数重载 (Function Overloading)
① 只有当函数的功能相似且参数类型不同时,才使用函数重载。
5.2.9. 说明 (Explanation)
函数重载可以提高代码的灵活性,但过度使用会使代码难以理解。应该只在重载函数的行为在逻辑上是一致的情况下才使用重载。避免使用仅通过返回值类型不同的重载。
5.3. 函数的实现 (Function Implementation)
5.3.1. 规则 (Rule)
① 函数的实现应该清晰、简洁且易于理解。
5.3.2. 说明 (Explanation)
一个好的函数实现应该遵循以下原则:
① 单一职责原则 (Single Responsibility Principle):每个函数应该只做一件事情,并且做好这件事。
② 避免副作用 (Avoid Side Effects):函数应该尽可能地避免修改全局状态或其输入参数。如果需要修改状态,应该在函数签名中明确说明。
③ 清晰的控制流 (Clear Control Flow):使用清晰的控制流结构(例如,if
、for
、while
)来组织代码。避免使用复杂的嵌套或 goto
语句。
④ 适当的注释 (Appropriate Comments):对于复杂或不明显的部分,添加注释来解释代码的意图。
5.3.3. 避免代码重复 (Avoid Code Duplication)
① 如果在多个地方有相似的代码,考虑将其提取到一个单独的函数中。
5.3.4. 早期返回 (Early Returns)
① 在适当的时候使用早期返回来简化函数的逻辑。
1
// 好的做法:
2
bool IsValid(int value) {
3
if (value < 0) {
4
return false;
5
}
6
// ... 更多的检查
7
return true;
8
}
9
10
// 不好的做法:
11
bool IsValid(int value) {
12
bool valid = true;
13
if (value < 0) {
14
valid = false;
15
} else {
16
// ... 更多的检查
17
if (...) {
18
valid = false;
19
}
20
}
21
return valid;
22
}
5.4. 内联函数 (Inline Functions)
5.4.1. 规则 (Rule)
① 只有当函数非常小并且对性能至关重要时,才应该使用内联函数。
5.4.2. 说明 (Explanation)
内联函数是指编译器尝试在调用点直接替换函数体的函数。这可以消除函数调用的开销,从而提高性能。
然而,过度使用内联函数会导致代码膨胀,这会增加可执行文件的大小并降低缓存局部性。因此,只有当函数非常小并且对性能至关重要时,才应该使用内联函数。
通常,如果一个函数超过 10 行代码,则不应该将其声明为 inline
。对于更长的函数,编译器通常会忽略 inline
关键字。
5.4.3. 何时使用内联 (When to Use Inline)
适合使用内联函数的场景包括:
① 简单的访问器(getter)和修改器(setter)方法。
② 非常小的工具函数。
③ 在性能关键的代码路径中被频繁调用的函数。
5.5. 函数指针 (Function Pointers)
5.5.1. 规则 (Rule)
① 合理且谨慎地使用函数指针。
5.5.2. 说明 (Explanation)
函数指针可以用于实现回调函数、策略模式等设计模式。它们提供了一种灵活的方式来选择在运行时执行哪个函数。
然而,过度使用函数指针会使代码难以理解和调试。在使用函数指针时,应该确保其类型安全,并提供清晰的文档说明其用途。
在 C++11 中,std::function
提供了一种更通用和类型安全的函数包装器,可以用于代替原始的函数指针。
5.6. 默认参数 (Default Arguments)
5.6.1. 规则 (Rule)
① 只有当默认值是显而易见的且不会引起歧义时,才使用默认参数。
5.6.2. 说明 (Explanation)
默认参数可以使函数的调用更加方便,因为调用者可以省略具有默认值的参数。
然而,如果默认值不明显或者存在多个具有默认参数的函数重载时,可能会导致代码难以理解。
应该谨慎使用默认参数,并确保其默认值是合理的且易于理解的。默认参数只能在函数声明中指定,而不能在函数定义中指定。
5.6.3. 不要使用指向可变对象的指针作为默认参数 (Do Not Use Pointers to Mutable Objects as Default Arguments)
① 不要使用指向可变对象的指针作为默认参数。
5.6.4. 说明 (Explanation)
指向可变对象的指针作为默认参数会导致在多次函数调用之间共享同一个对象,这可能会导致意外的行为。
1
// 不好的做法:
2
void Foo(int value, std::vector<int>* buffer = new std::vector<int>);
在每次调用 Foo
时,如果省略了 buffer
参数,都会使用同一个 std::vector<int>
对象。
5.7. 函数返回类型 (Function Return Types)
5.7.1. 规则 (Rule)
① 始终显式地指定函数的返回类型。
5.7.2. 说明 (Explanation)
在 C++ 中,如果函数没有返回值,应该将其返回类型声明为 void
。省略返回类型是 C 语言的遗留特性,在 C++ 中应该避免。显式地指定返回类型可以提高代码的可读性,并帮助编译器进行类型检查。
5.7.3. 返回值优化 (Return Value Optimization)
① 了解返回值优化(Return Value Optimization, RVO)和具名返回值优化(Named Return Value Optimization, NRVO),并在适当的时候利用它们来提高性能。
5.7.4. 说明 (Explanation)
返回值优化是一种编译器优化技术,可以避免在函数返回对象时进行不必要的拷贝。当函数返回一个本地创建的对象时,编译器可以直接在调用者的栈帧中构造该对象,从而消除拷贝操作。
具名返回值优化是返回值优化的一种特殊情况,当函数返回一个具名的局部变量时,编译器也可以进行优化。
了解这些优化可以帮助你编写更高效的代码。
5.8. constexpr
和 consteval
函数 (constexpr and consteval Functions)
5.8.1. 规则 (Rule)
① 尽可能使用 constexpr
来声明可以在编译时计算值的函数。
② 使用 consteval
来声明必须在编译时计算值的函数。
5.8.2. 说明 (Explanation)
constexpr
函数是指可以在编译时求值的函数。如果一个 constexpr
函数的所有参数都是字面值常量,那么编译器可以在编译时计算出函数的结果,并将其作为常量使用。这可以提高程序的性能。
consteval
函数是 C++20 中引入的新特性。它强制函数必须在编译时求值。如果 consteval
函数不能在编译时求值(例如,因为它的参数不是常量),那么程序将无法编译。
使用 constexpr
和 consteval
可以编写更高效、更安全的代码。
1
constexpr int Square(int n) {
2
return n * n;
3
}
4
5
consteval int Factorial(int n) {
6
return (n <= 1) ? 1 : n * Factorial(n - 1);
7
}
8
9
int main() {
10
constexpr int result1 = Square(5); // 编译时计算
11
int value = 3;
12
int result2 = Square(value); // 运行时计算
13
14
consteval int result3 = Factorial(5); // 必须在编译时计算
15
16
return 0;
17
}
5.9. 函数注解 (Function Annotations)
5.9.1. 规则 (Rule)
① 使用注解来提供关于函数的额外信息,例如前置条件、后置条件和不变量。
5.9.2. 说明 (Explanation)
注解是一种为代码添加元数据的方式。可以使用特定的语法或工具来处理这些注解,以进行静态分析、文档生成或其他目的。
例如,可以使用注解来指定函数在调用之前必须满足的条件(前置条件),以及函数在返回之后保证成立的条件(后置条件)。
虽然 C++ 本身没有内置的注解语法,但可以使用注释或其他约定来表示这些信息。一些静态分析工具也支持特定的注解格式。
6. Google 特有的奇技淫巧 (Google-Specific Magic)
6.1. 所有权与智能指针 (Ownership and Smart Pointers)
6.1.1. 规则 (Rule)
① 如果需要动态分配的对象的所有权可以明确地转移,则使用 std::unique_ptr
。
② 默认情况下,使用 std::shared_ptr
来表示共享所有权。只有在有明确的理由(例如,性能)时,才考虑使用原始指针,并且必须非常小心地管理其生命周期。
6.1.2. 说明 (Explanation)
在 Google 的 C++ 代码库中,我们广泛使用智能指针来管理动态分配的对象的生命周期。智能指针通过在不再需要对象时自动释放其内存,从而帮助避免内存泄漏。
std::unique_ptr
:表示对对象的独占所有权。当std::unique_ptr
被销毁或通过移动操作转移所有权时,它所管理的对象也会被删除。应该在需要明确所有权转移的情况下使用std::unique_ptr
。std::shared_ptr
:表示对对象的共享所有权。多个std::shared_ptr
可以指向同一个对象,并且只有当所有指向该对象的std::shared_ptr
都被销毁时,该对象才会被删除。默认情况下,应该使用std::shared_ptr
来管理共享所有权的场景。原始指针 (
T*
):应该谨慎使用原始指针来管理动态分配的内存。如果必须使用原始指针,则必须非常小心地确保在不再需要对象时将其删除,以避免内存泄漏。在现代 C++ 中,通常可以通过使用智能指针来避免手动内存管理。
6.1.3. 何时不使用智能指针 (When Not to Use Smart Pointers)
① 指向静态分配的对象(例如,全局变量或栈上的局部变量)的指针不应该使用智能指针管理。这些对象的生命周期由其作用域管理。
② 在某些性能关键的代码中,智能指针的开销可能是一个问题。在这种情况下,可以考虑使用原始指针,但必须非常小心地管理其生命周期,并进行充分的测试以确保没有内存泄漏。
③ 当与不理解智能指针的外部代码(例如,某些 C 库)交互时,可能需要使用原始指针。在这种情况下,务必清晰地记录所有权模型。
6.2. absl::Status
6.2.1. 规则 (Rule)
① 使用 absl::Status
来表示可能失败的操作的结果。
6.2.2. 说明 (Explanation)
absl::Status
是 Google 开源的 Abseil 库中的一个类,用于表示操作的结果,包括成功或失败,以及失败的原因。它比简单的布尔返回值或错误码提供了更丰富的信息。
absl::Status
可以包含一个 absl::StatusCode
,用于指示失败的类型(例如,absl::StatusCode::kNotFound
、absl::StatusCode::kInvalidArgument
),以及一个可选的消息,用于提供关于错误的更多上下文。
1
absl::StatusOr<Foo> GetFoo(int id) {
2
if (id < 0) {
3
return absl::InvalidArgumentError("ID cannot be negative.");
4
}
5
// ... 获取 Foo 的逻辑
6
if (/* 获取失败 */) {
7
return absl::NotFoundError("Foo with ID " + std::to_string(id) + " not found.");
8
}
9
return Foo();
10
}
11
12
void ProcessFoo(int id) {
13
absl::StatusOr<Foo> foo_result = GetFoo(id);
14
if (!foo_result.ok()) {
15
LOG(ERROR) << "Failed to get Foo: " << foo_result.status().ToString();
16
return;
17
}
18
Foo foo = foo_result.value();
19
// ... 处理 foo
20
}
6.2.3. absl::StatusOr<T>
① 对于返回类型为 T
的操作,如果该操作可能失败,则返回 absl::StatusOr<T>
。
6.2.4. 说明 (Explanation)
absl::StatusOr<T>
是一个模板类,用于表示可能返回类型为 T
的值或失败状态的操作结果。它包含一个 absl::Status
对象和一个类型为 T
的值。可以使用 .ok()
方法检查操作是否成功,使用 .status()
方法获取状态,使用 .value()
方法获取成功时的返回值。
6.3. 日志 (Logging)
6.3.1. 规则 (Rule)
① 使用 Google 的日志宏(例如,LOG(INFO)
、LOG(ERROR)
)进行日志记录。
6.3.2. 说明 (Explanation)
Google 提供了一套标准的日志宏,用于在代码中生成日志消息。这些宏提供了不同的日志级别(例如,INFO
、WARNING
、ERROR
、FATAL
),并可以将日志消息输出到不同的目标(例如,控制台、文件)。
使用标准的日志宏可以确保日志消息的格式一致,并且可以方便地配置和管理日志输出。
1
LOG(INFO) << "Starting the process...";
2
if (error_occurred) {
3
LOG(ERROR) << "An error occurred: " << error_message;
4
}
6.4. 标志 (Flags)
6.4.1. 规则 (Rule)
① 使用 Google 的标志库来定义和管理程序的命令行标志。
6.4.2. 说明 (Explanation)
Google 的标志库提供了一种标准化的方式来定义和解析程序的命令行标志。它允许用户在命令行中指定程序的配置选项,而无需修改代码。
使用标志库可以使程序的配置更加灵活和方便。
1
#include <gflags/gflags.h>
2
3
DEFINE_string(input_file, "default.txt", "Path to the input file.");
4
DEFINE_int32(num_iterations, 10, "Number of iterations to run.");
5
6
int main(int argc, char* argv[]) {
7
gflags::ParseCommandLineFlags(&argc, &argv, true);
8
LOG(INFO) << "Input file: " << FLAGS_input_file;
9
LOG(INFO) << "Number of iterations: " << FLAGS_num_iterations;
10
// ... 使用标志的逻辑
11
return 0;
12
}
6.5. 其他 Google 特有的工具 (Other Google-Specific Tools)
6.5.1. 说明 (Explanation)
除了上述内容之外,Google 的 C++ 代码库还使用了许多其他内部工具和库。这些工具通常是为了解决 Google 内部特定的问题而开发的,并且可能没有在 Abseil 库中开源。
如果你在 Google 的 C++ 项目中工作,你应该熟悉这些内部工具,并遵循相关的风格和使用指南。
7. 其他 C++ 特性 (Other C++ Features)
7.1. Rvalue 引用 (Rvalue References)
7.1.1. 规则 (Rule)
① 只有在定义移动感知(move-aware)的函数或者需要完美转发(perfect forwarding)时,才使用 Rvalue 引用。
7.1.2. 说明 (Explanation)
Rvalue 引用(&&
)是 C++11 中引入的一个特性,用于识别临时对象(rvalues)。它们使得可以实现移动语义,从而避免不必要的拷贝操作,提高性能。
移动语义 (Move Semantics):允许将资源(例如,动态分配的内存)的所有权从一个对象转移到另一个对象,而不是进行深拷贝。这对于大型对象或资源密集型对象尤其有用。
完美转发 (Perfect Forwarding):允许将函数参数以其原始类型(包括左值和右值属性)转发给另一个函数。这在编写泛型代码时非常有用。
应该谨慎使用 Rvalue 引用,并且只有在真正需要利用移动语义或完美转发时才使用。不正确地使用 Rvalue 引用可能会导致意外的行为。
1
// 移动构造函数
2
class MyClass {
3
public:
4
MyClass(MyClass&& other) noexcept;
5
// ...
6
};
7
8
// 完美转发
9
template <typename F, typename... Args>
10
auto wrapper(F&& f, Args&&... args) -> decltype(std::forward<F>(f)(std::forward<Args>(args)...)) {
11
// ... 其他逻辑
12
return std::forward<F>(f)(std::forward<Args>(args)...);
13
}
7.2. 友元 (Friends)
7.2.1. 规则 (Rule)
① 只有在必要时才使用友元类和友元函数。
7.2.2. 说明 (Explanation)
友元机制允许一个类或函数访问另一个类的 private
和 protected
成员。虽然友元有时是必要的(例如,用于实现某些操作符重载或在两个密切相关的类之间共享数据),但过度使用友元会破坏封装性,并使得代码难以理解和维护。
尽可能地使用公有的成员函数来访问和修改对象的状态,而不是使用友元。
7.3. 异常 (Exceptions)
7.3.1. 规则 (Rule)
① 我们不使用 C++ 异常。
7.3.2. 说明 (Explanation)
在 Google 的 C++ 代码中,我们通常不使用异常作为控制流机制。这主要是出于历史原因,并且考虑到异常在大型代码库中的管理和维护可能带来的复杂性。
对于错误处理,我们更倾向于使用返回值(例如,absl::Status
或 std::optional
)来指示操作是否成功,并在必要时提供错误信息。
如果你需要在 Google 的 C++ 项目中使用异常,请务必遵循项目或团队的特定指导原则。
7.4. 运行时类型识别 (Run-Time Type Information, RTTI)
7.4.1. 规则 (Rule)
① 避免使用运行时类型识别(RTTI)。
7.4.2. 说明 (Explanation)
RTTI 允许在运行时检查对象的类型。C++ 通过 dynamic_cast
和 typeid
运算符提供 RTTI。
在 Google 的 C++ 代码中,我们通常避免使用 RTTI,因为它可能会导致代码更加脆弱和难以维护。过度依赖类型信息可能会表明设计上存在问题。
如果需要在运行时处理不同类型的对象,可以考虑使用虚函数、模板或访问者模式等替代方案。
7.5. 类型转换 (Casting)
7.5.1. 规则 (Rule)
① 使用 C++ 风格的类型转换,例如 static_cast
、dynamic_cast
、const_cast
和 reinterpret_cast
。不要使用 C 风格的类型转换。
7.5.2. 说明 (Explanation)
C++ 提供了四种命名的类型转换运算符,它们比 C 风格的类型转换更安全且更易于理解:
static_cast
:用于执行静态类型转换,例如基本类型之间的转换或具有继承关系的类型之间的转换。它在编译时进行类型检查。dynamic_cast
:用于执行安全的向下转型(downcasting),即从基类指针或引用转换为派生类指针或引用。它在运行时进行类型检查,如果转换不合法,则返回nullptr
(对于指针)或抛出异常(对于引用)。应该避免使用dynamic_cast
,因为它依赖于 RTTI。const_cast
:用于添加或移除变量的const
或volatile
限定符。应该谨慎使用const_cast
,因为它可能会导致未定义的行为。reinterpret_cast
:用于执行低级的类型转换,例如将一个指针转换为另一种类型的指针。应该非常谨慎地使用reinterpret_cast
,因为它可能会破坏类型安全。
C 风格的类型转换(例如,(int)value
或 (int*)ptr
)不提供类型安全检查,并且难以在代码中搜索。因此,应该始终使用 C++ 风格的类型转换。
7.6. 流 (Streams)
7.6.1. 规则 (Rule)
① 只有在与用户交互或者需要格式化输出时才使用流。对于其他输入/输出操作,优先使用 printf
和相关的 C 风格函数。
7.6.2. 说明 (Explanation)
C++ 的流库(例如,std::cout
、std::cin
、std::fstream
)提供了类型安全的输入/输出操作。然而,与 printf
和 scanf
等 C 风格的函数相比,它们在某些情况下可能会有性能开销。
在 Google 的 C++ 代码中,我们通常只在与用户交互(例如,命令行工具)或者需要进行复杂的格式化输出时才使用流。对于其他类型的输入/输出操作(例如,读写文件、网络通信),我们更倾向于使用 printf
和相关的 C 风格函数,因为它们通常更高效。
如果你选择使用流,请确保遵循流的正确使用方式,并注意可能存在的性能影响。
7.7. 前置自增/自减运算符 (Preincrement and Predecrement Operators)
7.7.1. 规则 (Rule)
① 对于迭代器和其他对象,优先使用前置自增(++i
)和前置自减(--i
)运算符,而不是后置自增(i++
)和后置自减(i--
)运算符。
7.7.2. 说明 (Explanation)
前置自增/自减运算符通常比后置自增/自减运算符更高效。后置运算符需要先保存原始值,然后再进行自增/自减操作,并返回原始值的副本。对于迭代器和其他复杂对象,这可能会带来额外的开销。
对于内置类型(例如,int
、float
),前置和后置运算符的性能差异通常可以忽略不计。然而,为了保持一致性,建议始终使用前置运算符,除非后置运算符的语义是必需的。
1
// 好的做法:
2
for (int i = 0; i < n; ++i) {
3
// ...
4
}
5
6
std::vector<int>::iterator it = v.begin();
7
while (it != v.end()) {
8
++it;
9
}
10
11
// 不好的做法 (通常):
12
for (int i = 0; i < n; i++) {
13
// ...
14
}
15
16
std::vector<int>::iterator it = v.begin();
17
while (it != v.end()) {
18
it++;
19
}
7.8. const
的使用 (Use of const
)
7.8.1. 规则 (Rule)
① 尽可能地使用 const
。
7.8.2. 说明 (Explanation)
const
是 C++ 中一个非常有用的关键字,用于表示变量、参数或成员函数是只读的,不能被修改。使用 const
可以带来许多好处:
- 提高代码的可读性:明确哪些值是不应该被修改的。
- 帮助编译器进行优化:允许编译器进行更多的优化,因为它可以假设
const
值不会改变。 - 提高代码的安全性:防止意外地修改不应该被修改的值。
应该尽可能地将变量声明为 const
,除非你需要修改它们的值。对于不修改对象状态的成员函数,应该将其声明为 const
。对于不修改输入参数的函数参数,应该将其声明为 const
。
7.9. using
别名 (using Aliases)
7.9.1. 规则 (Rule)
① 优先使用 using
别名(C++11 引入)而不是 typedef
来定义类型别名。
7.9.2. 说明 (Explanation)
using
别名提供了一种更清晰和更易于理解的方式来定义类型别名,特别是对于模板类型别名。
1
// 好的做法:
2
using StringVector = std::vector<std::string>;
3
template <typename T>
4
using MapStringTo = std::map<std::string, T>;
5
6
// 较旧的做法:
7
typedef std::vector<std::string> StringVector;
8
template <typename T>
9
typedef std::map<std::string, T> MapStringTo; // 语法略显笨拙
using
别名的语法更自然,并且在处理模板时更加直观。
7.10. lambda 表达式 (Lambda Expressions)
7.10.1. 规则 (Rule)
① 合理且谨慎地使用 lambda 表达式。
7.10.2. 说明 (Explanation)
lambda 表达式(C++11 引入)提供了一种定义匿名函数对象(closures)的简洁方式。它们可以用于简化代码,特别是当需要将一个小的函数作为参数传递给另一个函数(例如,用于算法或回调)时。
然而,过度使用 lambda 表达式或者使用过于复杂的 lambda 表达式可能会使代码难以理解。应该只在 lambda 表达式能够提高代码的清晰度和简洁性的情况下才使用它们。
1
std::vector<int> numbers = {1, 2, 3, 4, 5};
2
std::for_each(numbers.begin(), numbers.end(), [](int n){
3
std::cout << n * 2 << " ";
4
});
5
std::cout << std::endl;
7.11. 模板元编程 (Template Metaprogramming, TMP)
7.11.1. 规则 (Rule)
① 避免使用复杂的模板元编程。
7.11.2. 说明 (Explanation)
模板元编程是一种在编译时执行计算的技术。虽然 TMP 可以用于实现一些高级的特性,例如静态类型检查和代码优化,但它通常会导致代码非常难以理解和调试。
在 Google 的 C++ 代码中,我们通常避免使用复杂的 TMP。如果需要进行编译时计算,可以考虑使用 constexpr
或 consteval
函数。
7.12. Boost 库 (Boost Library)
7.12.1. 规则 (Rule)
① 我们不强制使用 Boost 库。
7.12.2. 说明 (Explanation)
Boost 是一个广泛使用的 C++ 库集合,提供了许多有用的工具和功能。虽然 Boost 库的某些部分已经被吸收到 C++ 标准中(例如,智能指针),但在 Google 的 C++ 代码库中并没有强制使用 Boost。
是否使用 Boost 库取决于具体的项目和团队。如果决定使用 Boost,应该遵循项目的相关指导原则,并确保所使用的 Boost 库是经过批准的。通常情况下,优先使用标准库中提供的功能。
8. 包容性语言 (Inclusive Language)
8.1. 规则 (Rule)
① 在 C++ 代码和文档中,使用尊重和包容的语言。
8.2. 说明 (Explanation)
我们致力于创建一个包容和欢迎所有人的社区。因此,我们要求在我们的 C++ 代码和文档中使用尊重和包容的语言。这包括避免使用可能被认为是冒犯性、排斥性或歧视性的词语。
8.2.1. 应避免的术语示例 (Examples of Terms to Avoid)
以下是一些应该避免使用的术语示例,以及推荐的替代方案:
① “Whitelist” 和 “Blacklist” (白名单和黑名单):这些术语与种族有关,应该避免使用。推荐的替代方案包括:
▮▮▮▮ⓐ “Allowlist” 和 “Denylist” (允许列表和拒绝列表)
▮▮▮▮ⓑ “Acceptlist” 和 “Blocklist” (接受列表和阻止列表)
② “Master” 和 “Slave” (主和从):这些术语与奴隶制有关,应该避免使用。推荐的替代方案包括:
▮▮▮▮ⓐ “Primary” 和 “Secondary” (主和次)
▮▮▮▮ⓑ “Leader” 和 “Follower” (领导者和跟随者)
▮▮▮▮ⓒ “Controller” 和 “Replica” (控制器和副本)
③ 性别化的代词 (Gendered Pronouns):尽可能使用性别中立的语言。如果需要指代特定的人,并且知道他们的代词,则可以使用他们的代词。否则,可以使用性别中立的代词(例如,“they/them”),或者重写句子以避免使用代词。
8.2.2. 其他指导原则 (Other Guidelines)
① 尊重他人的身份:使用他人自我认同的名称和代词。
② 避免概括性陈述:避免使用基于性别、种族、宗教或其他受保护特征的概括性陈述。
③ 注意隐性偏见:努力意识到并避免在语言中体现隐性偏见。
④ 使用易于理解的语言:避免使用可能使某些人感到被排斥的行话或技术术语。
⑤ 保持专业:即使在非正式的交流中,也要保持专业的态度和语言。
8.3. 如何修复 (How to Fix)
① 审查现有代码和文档:检查你的代码和文档中是否存在任何不包容的语言。
② 使用查找和替换工具:可以使用文本编辑器的查找和替换功能来查找并替换需要更改的术语。
③ 寻求反馈:如果你不确定某个术语是否具有包容性,请向你的同事或团队寻求反馈。
④ 学习和成长:包容性语言是一个不断发展的领域。持续学习并适应新的最佳实践。
通过遵循这些指导原则,我们可以帮助创建一个更加包容和欢迎所有人的 C++ 社区。
9. 命名约定 (Naming)
9.1. 通用命名规则 (General Naming Rules)
9.1.1. 规则 (Rule)
① 尽可能使用描述性的名称。不要使用过于简洁或神秘的缩写。
② 变量名通常应该是名词或名词短语。
③ 函数名通常应该是动词或动词短语。
④ 布尔变量名应该以 is
、has
或类似的谓语开头。
⑤ 枚举类型名应该使用驼峰命名法,枚举值应该使用全部大写字母,单词之间用下划线分隔。
⑥ 常量名应该以 k
开头,并使用驼峰命名法。
⑦ 模板参数名应该简洁明了。
9.1.2. 说明 (Explanation)
清晰且一致的命名约定可以提高代码的可读性和可维护性。好的名称应该能够准确地表达所代表的实体是什么或者做什么。
避免缩写:除非缩写是广为人知的(例如,
URL
、ID
),否则应该避免使用缩写,因为它们可能会使代码难以理解。保持一致性:在整个代码库中保持命名约定的一致性。
使用正确的语言:对于特定领域的概念,使用该领域常用的术语。
9.2. 文件命名 (File Names)
9.2.1. 规则 (Rule)
① C++ 源文件应该以 .cc
结尾,头文件应该以 .h
结尾。
② 通常,文件名应该小写,并且可以包含下划线 (_
) 以提高可读性。
③ 非常相似的文件名应该避免。
9.2.2. 说明 (Explanation)
一致的文件命名约定有助于组织代码库并快速找到所需的文件。
大小写:虽然有些系统对文件名的大小写不敏感,但为了避免混淆,建议始终使用小写文件名。
下划线:可以使用下划线分隔文件名中的单词,以提高可读性(例如,
my_class.cc
)。避免相似名称:避免使用仅在大小写或少量字符上不同的文件名,这可能会导致构建错误或混淆。
9.3. 类型命名 (Type Names)
9.3.1. 规则 (Rule)
① 类型名称(类、结构体、枚举、类型别名等)应该使用驼峰命名法(CamelCase),首字母大写。
9.3.2. 说明 (Explanation)
驼峰命名法通过将每个单词的首字母大写并将它们连接在一起来形成名称(例如,MyClass
、HTTPRequest
)。
1
// 好的做法:
2
class MyClass { ... };
3
struct MyStruct { ... };
4
enum class MyEnum { VALUE1, VALUE2 };
5
using MyAlias = std::vector<int>;
6
7
// 不好的做法:
8
class myclass { ... };
9
struct mystruct { ... };
10
enum my_enum { VALUE_ONE, VALUE_TWO };
11
typedef std::vector<int> my_alias;
9.4. 变量命名 (Variable Names)
9.4.1. 规则 (Rule)
① 变量名应该使用小写字母,单词之间用下划线分隔(snake_case)。
② 类的成员变量应该以尾部下划线结尾。
③ 全局变量应该以 g_
开头。
9.4.2. 说明 (Explanation)
一致的变量命名约定可以帮助区分不同作用域和类型的变量。
1
// 好的做法:
2
int my_variable;
3
std::string user_name;
4
5
class MyClass {
6
private:
7
int member_variable_;
8
};
9
10
int g_global_variable;
11
12
// 不好的做法:
13
int myVariable;
14
std::string userName;
15
16
class MyClass {
17
private:
18
int mMemberVariable;
19
};
20
21
int globalVariable;
局部变量:使用小写字母和下划线。
成员变量:与局部变量类似,但以尾部下划线结尾。这可以明确区分成员变量和局部变量。
全局变量:以
g_
开头,以清楚地表明它们是全局的。应该尽量避免使用全局变量。
9.5. 常量命名 (Constant Names)
9.5.1. 规则 (Rule)
① 常量名应该以 k
开头,并使用驼峰命名法。
9.5.2. 说明 (Explanation)
使用 k
前缀可以明确地表明一个变量是常量,其值在初始化后不会改变。
1
// 好的做法:
2
const int kMaxConnections = 10;
3
const std::string kDefaultUserName = "guest";
4
5
// 不好的做法:
6
const int MAX_CONNECTIONS = 10;
7
const std::string DEFAULT_USER_NAME = "guest";
8
static const int sMaxConnections = 10; // 不推荐使用 static const
9.6. 函数命名 (Function Names)
9.6.1. 规则 (Rule)
① 函数名应该使用驼峰命名法,首字母大写。
② 函数名通常应该是动词或动词短语。
9.6.2. 说明 (Explanation)
清晰的函数命名可以帮助理解函数的功能。
1
// 好的做法:
2
void CalculateTotalAmount();
3
std::string GetUserName(int user_id);
4
bool IsUserLoggedIn();
5
6
// 不好的做法:
7
void calculate_total_amount();
8
std::string getusername(int userid);
9
bool isloggedin;
访问器函数 (Accessor Functions):通常以
Get
开头(例如,GetCount()
、GetName()
)。修改器函数 (Mutator Functions):可以使用
Set
前缀,但有时也可以直接使用动词(例如,SetName(const std::string& name)
、AddElement(int element)
)。布尔值返回函数:通常以
Is
或Has
开头(例如,IsValid()
、HasPermission()
)。
9.7. 命名空间命名 (Namespace Names)
9.7.1. 规则 (Rule)
① 命名空间名称应该全部小写。
② 顶层命名空间名称通常应该是项目的名称或一个简短的缩写。
③ 嵌套命名空间名称应该描述其包含的内容。
9.7.2. 说明 (Explanation)
一致的命名空间命名有助于组织代码并避免命名冲突。
1
// 好的做法:
2
namespace myproject {
3
namespace internal {
4
// ...
5
} // namespace internal
6
} // namespace myproject
7
8
// 不好的做法:
9
namespace MyProject {
10
namespace Internal {
11
// ...
12
} // namespace Internal
13
} // namespace MyProject
9.8. 枚举命名 (Enum Names)
9.8.1. 规则 (Rule)
① 枚举类型名称应该使用驼峰命名法,首字母大写。
② 枚举值应该使用全部大写字母,单词之间用下划线分隔。
9.8.2. 说明 (Explanation)
清晰的枚举命名可以提高代码的可读性。
1
// 好的做法:
2
enum class Color {
3
kRed,
4
kGreen,
5
kBlue,
6
};
7
8
// 不好的做法:
9
enum color {
10
RED,
11
GREEN,
12
BLUE
13
};
14
15
enum class ErrorCode {
16
kNoError,
17
kFileNotFoundError,
18
kAccessDeniedError,
19
};
9.9. 宏命名 (Macro Names)
9.9.1. 规则 (Rule)
① 通常不应该使用宏。如果必须使用,宏名称应该全部大写字母,单词之间用下划线分隔。
9.9.2. 说明 (Explanation)
宏可能会导致难以调试和理解的代码。尽可能使用 const
常量、内联函数、模板或枚举来代替宏。
如果必须使用宏(例如,在与 C 风格的 API 交互时),请确保宏名称清晰明了,并且遵循全部大写字母和下划线的约定。
1
// 好的做法 (如果必须使用宏):
2
#define MAX_VALUE 100
3
#define LOG_ERROR(message) LOG(ERROR) << (message)
4
5
// 不好的做法:
6
#define maxValue 100
7
#define logError(message) LOG(ERROR) << (message)
9.10. 例外 (Exceptions)
9.10.1. 说明 (Explanation)
在某些特殊情况下,可以偏离上述命名约定。例如,在与现有代码库或第三方库交互时,可能需要遵循其特定的命名约定。在这种情况下,应该在代码中清晰地记录这些例外情况。
10. 注释 (Comments)
10.1. 注释风格 (Comment Style)
10.1.1. 规则 (Rule)
① 使用 //
或 /* ... */
风格的注释都可以。然而,通常更倾向于使用 //
进行行注释。
② 对于文件、类和函数,使用 Doxygen 风格的注释块。
10.1.2. 说明 (Explanation)
注释是代码的重要组成部分,它们可以帮助理解代码的目的、功能和实现方式。
行注释 (
//
):适用于简短的解释或代码行的注释。块注释 (
/* ... */
):适用于多行注释或需要更详细解释的情况。在头文件中,对于文件、类和函数的文档注释,推荐使用/* ... */
风格,并遵循 Doxygen 的格式。在.cc
文件中,也可以使用/* ... */
进行块注释,但通常更倾向于使用多个//
行注释。Doxygen 注释:Doxygen 是一种流行的文档生成工具,可以从源代码中的特殊格式注释生成文档。对于公开的头文件,应该使用 Doxygen 风格的注释来记录文件、类、结构体、枚举、函数等。
10.2. 文件注释 (File Comments)
10.2.1. 规则 (Rule)
① 每个源文件都应该以版权声明开始。
② 许可证信息(例如,Apache 2.0)应该在版权声明之后。
③ 文件注释应该描述文件的内容。
10.2.2. 说明 (Explanation)
文件注释提供了关于文件中包含的代码的高级概述。
1
/*
2
* Copyright 2023 The SomeProject Authors.
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
7
*
8
* http://www.apache.org/licenses/LICENSE-2.0
9
*
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
15
*/
16
17
// 描述文件的内容,例如:
18
// 这个文件包含了 Foo 类的实现。
10.3. 类注释 (Class Comments)
10.3.1. 规则 (Rule)
① 每个类的定义都应该包含一个描述该类用途和功能的注释。
② 如果类有任何重要的约定或限制,也应该在类注释中说明。
10.3.2. 说明 (Explanation)
类注释帮助用户理解类的设计和使用方式。
1
/*
2
* 表示一个形状在二维平面上的点。
3
*
4
* 可以通过其 x 和 y 坐标来访问点。
5
*/
6
class Point {
7
public:
8
// ...
9
};
10.4. 函数注释 (Function Comments)
10.4.1. 规则 (Rule)
① 每个公开的函数声明都应该包含一个描述该函数功能、参数和返回值的注释。
② 对于非显而易见的私有函数,也应该添加注释说明其用途。
10.4.2. 说明 (Explanation)
函数注释是理解函数接口的关键。应该清晰地说明函数的输入、输出以及任何可能抛出的异常(虽然 Google C++ 风格指南不鼓励使用异常,但如果使用,则需要说明)。
1
/*
2
* 计算两个整数的和。
3
*
4
* @param a 第一个整数。
5
* @param b 第二个整数。
6
* @return 两个整数的和。
7
*/
8
int Add(int a, int b);
对于更复杂的函数,可能需要更详细的注释,包括:
- 前置条件 (Preconditions):函数调用之前必须满足的条件。
- 后置条件 (Postconditions):函数返回之后保证成立的条件。
- 线程安全性 (Thread Safety):关于函数在多线程环境中的行为。
10.5. 变量注释 (Variable Comments)
10.5.1. 规则 (Rule)
① 通常,变量名本身应该足够清晰地表达其用途。对于含义不明显的变量,或者全局变量和成员变量,应该添加注释。
10.5.2. 说明 (Explanation)
局部变量的注释通常是不必要的,因为它们的作用域很小,并且通常在上下文中很容易理解。然而,对于具有更广作用域的变量,注释可以帮助理解其目的。
1
int counter; // 用于跟踪已处理的元素数量。
2
3
class MyClass {
4
private:
5
std::string user_name_; // 存储用户的名称。
6
};
对于全局变量,由于其潜在的影响范围很广,务必添加清晰的注释说明其用途和生命周期。
10.6. 实现注释 (Implementation Comments)
10.6.1. 规则 (Rule)
① 对于代码中不明显或复杂的部分,应该添加注释来解释其实现细节和背后的逻辑。
② 注释应该解释“为什么”这样做,而不是简单地重复代码“做了什么”。
10.6.2. 说明 (Explanation)
实现注释可以帮助其他开发人员(以及未来的你)理解代码的工作方式。
1
// 我们需要检查 i < size - 1,以避免访问超出数组边界的元素。
2
if (i < size - 1) {
3
// ...
4
}
5
6
// 使用二分查找是因为数据已经排序。
7
int index = std::lower_bound(data.begin(), data.end(), target) - data.begin();
10.6.3. 注意事项 (Things to Avoid)
① 不要过度注释:对于显而易见的代码,不需要添加注释。过多的注释反而会使代码难以阅读。
② 注释应该与代码保持同步:当代码被修改时,务必更新相关的注释。过时的注释比没有注释更糟糕。
③ 避免使用块注释来注释掉代码:应该使用预处理器指令(例如,#if 0
和 #endif
)来禁用代码块。
11. 格式 (Formatting)
11.1. 行长度 (Line Length)
11.1.1. 规则 (Rule)
① 每行代码的长度限制为 80 个字符。
11.1.2. 说明 (Explanation)
虽然现在的显示器越来越大,但保持代码行的长度在 80 个字符以内仍然是一个好习惯。这有助于提高代码的可读性,并且在不同的编辑器和查看器中都能良好显示。
11.1.3. 如何处理长行 (How to Handle Long Lines)
① 如果一行代码超过了 80 个字符的限制,应该将其拆分为多行。拆分时应该遵循一致的风格,并尽量保持代码的可读性。
② 拆分长字符串字面量时,可以考虑使用 C++11 的原始字符串字面量或者将字符串拆分为多个较小的字符串字面量并使用字符串连接。
1
// 好的做法:
2
// 拆分函数调用参数
3
ReturnType functionName(int argument1, int argument2,
4
int argument3, int argument4);
5
6
// 拆分长字符串字面量
7
const char* long_string =
8
"This is a very long string literal that spans multiple lines "
9
"but is still treated as a single string.";
10
11
// 使用原始字符串字面量 (C++11)
12
const char* long_string_raw = R"(This is a very long string literal
13
that spans multiple lines.)";
14
15
// 不好的做法:
16
// 过长的行
17
ReturnType reallyLongFunctionName(int argument1, int argument2, int argument3, int argument4);
11.2. 非空格字符与空格 (Non-space Characters vs. Spaces)
11.2.1. 规则 (Rule)
① 只使用空格进行缩进。不要在代码中使用制表符。
② 在一行的末尾不要有多余的空格。
11.2.2. 说明 (Explanation)
使用空格进行缩进可以确保代码在不同的编辑器和平台上看起来一致。制表符的宽度在不同的环境中可能会有所不同,导致代码格式混乱。
删除行尾多余的空格可以避免不必要的版本控制差异。
11.2.3. 空格的使用规则 (Rules for Using Spaces)
① 圆括号:在圆括号内部的左右两侧都不加空格。
1
// 好的做法:
2
functionCall(arg1, arg2);
3
if (condition) {
4
// ...
5
}
6
7
// 不好的做法:
8
functionCall( arg1, arg2 );
9
if ( condition ) {
10
// ...
11
}
② 花括号:
▮▮▮▮ⓐ 对于非空的代码块,左花括号前有一个空格。
▮▮▮▮ⓑ 右花括号通常单独占一行,除非它后面跟着 else
、else if
或逗号。
1
// 好的做法:
2
if (condition) {
3
DoSomething();
4
} else {
5
// ...
6
}
7
8
// 不好的做法:
9
if (condition){
10
DoSomething();
11
}
12
else {
13
// ...
14
}
③ 操作符:大多数二元操作符的两侧应该各有一个空格。对于一元操作符(例如,++
、--
、&
、*
),通常不加空格。
1
// 好的做法:
2
int x = a + b;
3
x++;
4
y = *ptr;
5
6
// 不好的做法:
7
int x= a + b;
8
x ++;
9
y = * ptr;
④ 逗号:逗号后面应该有一个空格。
1
// 好的做法:
2
functionCall(arg1, arg2, arg3);
3
4
// 不好的做法:
5
functionCall(arg1,arg2,arg3);
⑤ 分号:分号前面通常不加空格。
1
// 好的做法:
2
for (int i = 0; i < count; ++i) {
3
// ...
4
}
5
6
// 不好的做法:
7
for (int i = 0 ; i < count ; ++i) {
8
// ...
9
}
⑥ 模板和类型转换:尖括号 <
和 >
内部的左右两侧都不加空格。
1
// 好的做法:
2
std::vector<int> numbers;
3
static_cast<char>(value);
4
5
// 不好的做法:
6
std::vector< int > numbers;
7
static_cast < char > (value);
11.3. 垂直留白 (Vertical Whitespace)
11.3.1. 规则 (Rule)
① 使用空行在逻辑上相关的代码段之间创建视觉分隔。
② 不要在不必要的地方使用多个空行。
11.3.2. 说明 (Explanation)
适当的垂直留白可以提高代码的可读性,使代码结构更清晰。
11.3.3. 何时使用空行 (When to Use Blank Lines)
① 在函数定义之间。
② 在类的不同逻辑部分之间(例如,public
、protected
、private
成员)。
③ 在函数内部,将代码划分为逻辑上的段落。
1
#include <iostream>
2
#include <vector>
3
4
class Foo {
5
public:
6
Foo();
7
~Foo();
8
9
void Bar();
10
11
private:
12
int count_;
13
std::vector<int> data_;
14
};
15
16
Foo::Foo() : count_(0) {
17
// ...
18
}
19
20
Foo::~Foo() {}
21
22
void Foo::Bar() {
23
// 一段逻辑
24
for (int i = 0; i < 10; ++i) {
25
data_.push_back(i);
26
}
27
28
// 另一段逻辑
29
int sum = 0;
30
for (int val : data_) {
31
sum += val;
32
}
33
std::cout << "Sum: " << sum << std::endl;
34
}
11.4. 函数声明与定义 (Function Declarations and Definitions)
11.4.1. 规则 (Rule)
① 返回类型应该与函数名在同一行。
② 参数列表应该在圆括号内,即使没有参数。
③ 如果函数定义很短,可以放在一行。
④ 如果函数定义很长,或者为了提高可读性,可以将左花括号放在新的一行。
11.4.2. 说明 (Explanation)
一致的函数声明和定义格式可以提高代码的可读性。
1
// 好的做法:
2
int Add(int a, int b);
3
4
int Add(int a, int b) {
5
return a + b;
6
}
7
8
void DoSomething() {
9
// ...
10
}
11
12
// 非常短的函数可以放在一行:
13
int GetCount() const { return count_; }
14
15
// 不好的做法:
16
int
17
Add(int a, int b);
18
19
int Add
20
(int a, int b) {
21
return a + b;
22
}
23
24
void DoSomething(){
25
// ...
26
}
11.5. if
语句 (if Statements)
11.5.1. 规则 (Rule)
① if
关键字和左圆括号之间应该有一个空格。
② 圆括号内部不应该有空格。
③ 左花括号通常放在 if
语句的同一行。
④ 即使 if
语句块只包含一行代码,也应该使用花括号。
⑤ else
关键字应该与前一个 if
或 else
语句的右花括号在同一行。
1
// 好的做法:
2
if (condition) {
3
DoSomething();
4
} else {
5
DoSomethingElse();
6
}
7
8
if (very_long_variable_name >= kThreshold) {
9
// ...
10
}
11
12
// 不好的做法:
13
if(condition){
14
DoSomething();
15
}
16
else{
17
DoSomethingElse();
18
}
19
20
if ( condition ) {
21
// ...
22
}
23
24
if (condition)
25
DoSomething(); // 避免单行 if 语句不带花括号
11.6. 循环语句 (Loop Statements)
11.6.1. 规则 (Rule)
① for
和 while
关键字和左圆括号之间应该有一个空格。
② 圆括号内部不应该有空格。
③ 左花括号通常放在循环语句的同一行。
④ 即使循环体只包含一行代码,也应该使用花括号。
1
// 好的做法:
2
for (int i = 0; i < count; ++i) {
3
// ...
4
}
5
6
while (condition) {
7
// ...
8
}
9
10
// 不好的做法:
11
for(int i = 0; i < count; ++i){
12
// ...
13
}
14
15
while(condition){
16
// ...
17
}
18
19
for ( int i = 0 ; i < count ; ++i ) {
20
// ...
21
}
22
23
while ( condition ) {
24
// ...
25
}
26
27
for (int i = 0; i < count; ++i)
28
DoSomething(i); // 避免单行循环不带花括号
11.7. switch
语句 (switch Statements)
11.7.1. 规则 (Rule)
① switch
关键字和左圆括号之间应该有一个空格。
② 圆括号内部不应该有空格。
③ case
标签应该与 switch
关键字在同一缩进级别。
④ break
语句通常应该放在每个 case
块的末尾。如果故意省略 break
,应该添加注释说明。
⑤ default
标签应该总是存在。
1
// 好的做法:
2
switch (variable) {
3
case 0: {
4
DoSomething();
5
break;
6
}
7
case 1: {
8
DoSomethingElse();
9
break;
10
}
11
default: {
12
// ...
13
break;
14
}
15
}
16
17
// 不好的做法:
18
switch(variable){
19
case 0:{
20
DoSomething();
21
break;
22
}
23
case 1:{
24
DoSomethingElse();
25
break;
26
}
27
default:{
28
// ...
29
break;
30
}
31
}
32
33
switch ( variable ) {
34
case 0:
35
DoSomething();
36
break;
37
case 1:
38
DoSomethingElse();
39
break;
40
default:
41
// ...
42
break;
43
}
11.8. 表达式 (Expressions)
11.8.1. 规则 (Rule)
① 在二元操作符的两侧应该各有一个空格。
② 对于复杂表达式,可以使用额外的空格来提高可读性。
11.8.2. 说明 (Explanation)
适当的空格可以使表达式更容易阅读和理解。
1
// 好的做法:
2
int x = a + b * c;
3
if (x > 0 && y < 10) {
4
// ...
5
}
6
7
// 可以使用额外的空格提高可读性:
8
int complicated_value = a * b + c / d - e;
9
int another_value = (a * b) + (c / d) - e;
10
11
// 不好的做法:
12
int x=a+b*c;
13
if(x>0&&y<10){
14
// ...
15
}
16
17
int complicated_value=a*b+c/d-e;
11.9. 其他格式规则 (Other Formatting Rules)
11.9.1. 指针和引用 (Pointers and References)
① 在声明指针或引用时,星号 *
或与号 &
应该紧靠类型名或变量名,但要保持风格一致。Google C++ 风格指南倾向于紧靠变量名。
1
// 倾向于这种风格:
2
int *ptr;
3
int &ref = value;
4
5
// 也可以接受,但不太常用:
6
int* ptr;
7
int& ref = value;
11.9.2. 预处理器指令 (Preprocessor Directives)
① 预处理器指令(例如,#include
、#define
)应该总是从行的开头开始,不要有任何缩进。
1
// 好的做法:
2
#include <iostream>
3
4
#define MAX_SIZE 100
5
6
// 不好的做法:
7
#include <iostream>
8
9
#define MAX_SIZE 100
11.9.3. 类成员的顺序 (Order of Class Members)
① 在类中,通常按照以下顺序声明成员:public
、protected
、private
。
② 在每个访问级别内部,通常按照以下顺序声明成员:
▮▮▮▮ⓐ 类型别名和枚举
▮▮▮▮ⓑ 静态常量
▮▮▮▮ⓒ 静态变量
▮▮▮▮ⓓ 实例常量
▮▮▮▮ⓔ 构造函数
▮▮▮▮ⓕ 析构函数
▮▮▮▮ⓖ 成员函数(包括静态成员函数)
▮▮▮▮ⓗ 成员变量(包括静态成员变量)
③ 应该使用空行在不同的访问级别之间进行分隔。
11.9.4. 命名空间格式 (Namespace Formatting)
① 命名空间的左花括号通常放在新的一行。
② 命名空间的右花括号应该在注释中包含命名空间的名称。
1
// 好的做法:
2
namespace mynamespace {
3
4
// ...
5
6
} // namespace mynamespace
7
8
// 不好的做法:
9
namespace mynamespace {
10
11
// ...
12
}
13
14
namespace mynamespace {
15
// ...
16
};
11.10. 总结 (Summary)
保持代码格式的一致性对于提高代码的可读性和可维护性至关重要。遵循这些格式规则可以帮助确保你的代码风格清晰且易于理解。在 Google,我们通常使用自动化工具(例如,Clang Format)来强制执行这些格式规则。
12. 规则的例外 (Exceptions to the Rules)
12.1. 规则 (Rule)
① 尽管本指南提供了许多强制性的规则,但在某些情况下,为了代码的清晰性和可维护性,可以允许例外。
12.2. 说明 (Explanation)
本指南旨在为大多数 C++ 代码提供一套通用的最佳实践。然而,在某些特定的情况下,严格遵守这些规则可能会导致代码变得更加复杂、难以阅读或维护。因此,我们承认在某些情况下可以存在例外。
12.2.1. 允许例外的场景 (Scenarios Where Exceptions May Be Allowed)
① 与现有代码保持一致:当修改或扩展已有的代码库时,有时为了保持风格的一致性,可能需要遵循现有代码的风格,即使它与本指南中的某些规则有所不同。然而,对于新的代码,应该尽可能遵循本指南。
② 第三方代码:当与第三方库或框架的代码交互时,可能需要遵循这些代码的风格约定,即使它们与本指南有所不同。
③ 特定于平台的代码:某些特定于平台或操作系统的代码可能需要使用不同的约定或特性。
④ 性能关键的代码:在极少数情况下,为了达到最佳的性能,可能需要牺牲某些风格规则。然而,这种情况应该非常罕见,并且需要充分的理由和仔细的权衡。
⑤ 历史原因:某些旧的代码可能由于历史原因而没有遵循本指南。在没有充分理由进行大规模重构的情况下,可以接受这些不一致之处。
12.2.2. 如何处理例外 (How to Handle Exceptions)
① 清晰地记录:如果决定偏离本指南中的某个规则,应该在代码中清晰地记录原因。可以使用注释来说明为什么选择不遵循该规则。
② 保持一致性:在同一个代码单元(例如,一个文件或一个类)中,对于同一类型的例外情况,应该保持一致性。
③ 谨慎使用:例外情况应该谨慎使用,并且只有在真正有必要时才应该偏离规则。不要将例外作为不遵循指南的借口。
④ 团队共识:对于较大的例外情况或不明确的情况,应该与团队成员讨论并达成共识。
12.3. 示例 (Examples)
① 使用 printf
而不是流:在某些性能关键的代码路径中,或者在与 C 风格的 API 交互时,可以使用 printf
而不是 C++ 的流,即使本指南通常推荐使用流进行用户交互。在这种情况下,应该添加注释说明原因。
② 较长的行:在某些情况下,为了提高可读性,可以允许代码行稍微超过 80 个字符的限制,例如当一行包含一个很长的 URL 或一个非常长的宏定义时。
③ 不同的命名约定:当与一个使用不同命名约定的第三方库交互时,可以暂时采用该库的命名约定,以保持代码的一致性。
12.4. 总结 (Summary)
本指南提供了一套有用的规则,可以帮助编写一致且可维护的 C++ 代码。然而,理解在某些情况下可以存在例外是很重要的。当决定偏离规则时,应该仔细考虑,清晰地记录原因,并与团队成员进行沟通。最终的目标是编写出清晰、可读且易于维护的代码。