参考链接:Google C++ Style Guide
# 头文件
- 每个
.cpp
源文件都应该有一个相对应的.h
头文件 - 每个头文件都应该有头文件守卫,守卫格式统一为:
_<PROJECT>_<PATH>_<FILE>_H_
- 对于头文件中对于 inline 内联函数的定义,要求函数体在十行以内,且不包括循环、switch 语句。
- 头文件的 include 顺序:相关头文件、C 系统头文件、C++ 头文件、其他库头文件、项目头文件
(这里的 “相关头文件” 指的是与该源文件同名的头文件)
# 作用域
除了少数例外,将代码放置在命名空间中,以减少命名冲突
不要使用
using namespace xxx
来导入命名空间中的内容(会污染命名空间)1
2// Forbidden -- This pollutes the namespace.
using namespace foo;当源文件中的定义不需要被其他文件所引用时,通过未命名命名空间或 static 来赋予其内部链接性:
1
2
3namespace {
...
} // namespace用注释来结束命名空间
1
2
3
4
5
6
7
8
9
10
11
12// In the .h file
namespace mynamespace {
// All declarations are within the namespace scope.
// Notice the lack of indentation.
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
# 对于局部变量
将其放在尽可能小的范围内,并在声明中初始化变量。
定义局部变量时尽可能地靠近其第一次被使用的地方,便于读者找到定义并了解其初始值
1
2
3
4
5
6
7int jobs = NumJobs();
// More code...
f(jobs); // Bad -- declaration separate from use.
int jobs = NumJobs();
f(jobs); // Good -- declaration immediately (or closely) followed by use.局部变量应该在定义时初始化,而不是先定义后初始化:
1
2
3
4
5int i;
i = f(); // Bad -- initialization separate from declaration.
int i = f(); // Good -- declaration has initialization.
1 | std::vector<int> v; |
# 类
- 避免在类的构造函数中调用虚函数,详见虚函数与构造函数
- 避免隐式转换,尽可能地使用
explicit
关键字来修饰单一参数的函数(拷贝构造和移动构造除外) - 只对携带数据的被动对象(
passive objects
)使用struct
,其他一律使用class
。 - 尽可能地考虑组合,而不是继承。当使用继承时,优先公有继承
- 对于纯抽象类,其派生类的继承属于 “接口继承”,其他情况则都是 “实现继承”。
- 强烈不建议多实现继承
- 类的成员变量应该是私有的
其中,"passive objects" 指的是那些仅仅用于存储数据而不包含任何行为的对象。换句话说,它们是纯粹的数据结构,用于在程序中传递数据,而不负责执行任何操作或行为。通常,这些对象仅包含公共数据成员和对这些数据成员进行初始化、访问和设置的方法,而不包含任何其他的逻辑或操作。
这种对象通常用于数据传递和数据存储,而不涉及到具体的行为。它们被设计为尽可能简单和轻量,只提供了对数据的简单操作,例如读取和写入。因此,当文本中提到 "passive objects" 时,意思是将结构体(struct)仅用于这种数据容器的情况。
另外,在 C++ 中,“多实现继承” 指的是一个类同时从多个基类继承属性和方法,可能会导致 菱形继承 问题。
# 函数
- 对于函数的输出,优先使用返回值进行传递,而不是使用输出参数
- 对于函数的返回值,优先返回值(return by value),而不是返回引用(return by reference)
- 避免返回裸指针,除非它可以为
nullptr
- 对于函数的输入参数,应该采用值传递或者
const
的引用传递(pass by value, or pass by reference of const) - 对于函数的输出参数和输入输出参数,应该采用引用传递
- 对于函数参数的顺序,优先输入参数,然后是输出参数
- 函数体尽可能短小(40 行左右),一个函数就应该集中做某件事情
一个函数应该避免依赖某个引用参数在整个函数调用期间的存活(Avoid defining functions that require a reference parameter to outlive the call.)即,某个引用参数的存活与否,不应该影响该函数的正常运作,其不会因为某个引用参数在函数调用过程中不再存活而出错。
对于函数的返回值,存在两种返回语法,一种是普通的、常见的,返回类型出现在函数名之前;而另一种则是在函数名之前用 auto 进行占位,而真正的返回值则尾随函数的参数列表(这种语法我们称之为 Trailing Return Type Syntax
):
1 | auto foo(int x) -> int; |
对于 Trailing Return Type Syntax
,其于普通的返回形式不同之处在于,这种情况下返回类型存在于函数体的作用域中,则对于以下情况能够有较好的表现:
1 | //普通的“前置”返回 |
总得来说,在大多数情况下,我们优先考虑普通的 “前置” 返回,而当我们在处理涉及 lambda 表达式时,或者当将函数的返回类型后置能够带来更好的阅读体验时,我们采用 “后置” 的 Trailing Return Type Syntax
。
# 所有权与智能指针
- 首选使用
std::unique_ptr
来明确所有权转移 - 转移所有权比 “借用” 指针或引用更方便,它减少了用户之间对于内存对象的生存期的协同。
(所有权明确了谁改对内存的生存期负责,明确了谁该最终释放内存,而单纯地指针拷贝、借用则会混淆这一点)
如果动态分配是必要的,最好将所有权闲置 / 保留在分配它的代码中。此后,如果其他代码需要访问对象,考虑向其传递一个副本,或者传递一个指针或引用而不转移所有权。(我猜这样做的目的是,将所有权保留在原先分配它的内存的位置,让原 own 负责维护其生存期,进而避免所有权的混乱。)
# C++ 特性
# 类型转换
- 使用 C++ 风格的类型转换(
static_cast
、const_cast
和reinterpret_cast
)而不是 C 风格的类型转换 - 不要使用 C 风格的转换(例如
(int)x
) 除非转换的目标类型是 void - 当且仅当
T
是一个类类型时使用T(x)
这种转换形式 - 使用大括号初始化来转换算术类型(例如
int64_t{x}
) - 用
static_cast
进行类类型的向上、向下转换 - 用
const_cast
来移除 const 限定词 - 用
reinterpret_cast
来进行不安全的类型转换,当且仅当你知道你在干什么的时候使用
# 其他
- 优先使用前置形式的自增、自减运算符
++
、--
,(即++i
、--i
)除非你需要用到后置自增 / 自减的结果。 - 优先使用 using 语法而不是 typedef 语法进行别名
- 对于空指针,使用
nullptr
,对于空字符(串),使用\0
- 优先考虑使用 sizeof (变量名) 而不是 sizeof (类型名)
原因是 sizeof (变量名) 会随着类型的改变而进行适当的更新1
2
3
4MyStruct data;
memset(&data, 0, sizeof(data)); //good
memset(&data, 0, sizeof(MyStruct)); //bad
# const 限定
- 在 API 中,尽可能地(合理)使用 const,用于修饰类函数、函数传参
- 对于以值传递的函数参数,
加 const 修饰无意义,因为它不会影响到调用者传入的数据 - 对于 const 修饰符,使用前置的书写形式,即
const int* foo
# 无符号整型
- 尽可能地不要使用无符号整型(unsigned integer)诸如
uint32_t
,除非有一个合理有效的理由:用于表示位图 - 不要因为 “一个数字不能出现负值” 这样的原因而使用无符号整型
# 宏定义
- 避免定义宏,禁止使用宏定义 C++ API
- 对于常量,用 const 变量代替,对于缩写,用引用代替,总之,尽可能避免使用宏定义
# 类型推导
- 当且仅当类型推导使得代码变得更清晰和安全的情况下,使用类型推导
1
2
3
4
5
6
7
8
9
10std::unique_ptr<WidgetWithBellsAndWhistles> widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
absl::flat_hash_map<std::string, std::unique_ptr<WidgetWithBellsAndWhistles>>::const_iterator
it = my_map_.find(key);
std::array<int, 6> numbers = {4, 8, 15, 16, 23, 42};
//it's ok and better below:
auto widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
auto it = my_map_.find(key);
std::array numbers = {4, 8, 15, 16, 23, 42};
# 命名
- 减少使用缩写,命名的描述性(详细程度)应该与该命名在作用域的可见性成正比
(例如,在一个只有 5 行的代码中,变量名n
可以被接受,但如果是在一个大段代码的类中,则n
有点显得过于模糊了)
一些 “常见的” 缩写可以被接受,比如用 i
来表示迭代(iteration)次数,用 T 来表示模板参数(template parameter)
# 文件名的命名规范
全小写,单词之间以下划线( _
)或破折号( -
)进行连接,优先考虑下划线( _
)
1 | my_useful_class.cpp |
# 类型名的命名规范
以大写字母开头,每个单词首字母大写,不带任何下划线:
1 | // classes and structs |
这里我们所说的类型,包括了 classes、structs、type aliases、enums 和 type template parameters
# 变量名的命名规范
采用蛇形命名法,全小写,每个单词之间用下划线( _
)进行连接
特殊地,对于类内的类成员变量,在变量名的前面加一个前缀的下划线( _
)
(在谷歌命名规范中,它们采用后缀下划线,但我 prefer 前缀下划线)
1 | std::string table_name; |
# 常量名的命名规范
对于用 const 和 constexpr 描述的变量,或其值在程序的整个生存期保持固定不变的变量,采用 “全大写 + 蛇形命名” 的形式:
(在谷歌规范中,用一个小写的前缀 “k” 开头,然后其余每个单词首字母大写。个人不赞同不喜欢这种风格):
1 | constexpr uint32_t MAX_FRAME_WIDTH = 640; |
谷歌风格常量:
1 | const int kDaysInAWeek = 7; |
# 枚举类型的命名规范
枚举类型本质上也是一种常量,所以其命名规则同常量名,小写字母 “k”+ 每个单词首字母大写。
# 命名空间的命名规范
在谷歌规范中,其对命名空间的约定为:全小写,单词以下划线分隔。
但我不喜欢这种风格,更偏向于同类型名,以大写字母开头,每个单词首字母大写,不带任何下划线
# 注释
好的代码自我阐释。
函数定义时最好在函数前添加注释,用于形容函数的作用以及用法
对于函数的参数,如果是指针类型,可以备注其是否允许为空指针,以及如果是会怎样
对于类内成员,可以添加注释形容某个变量的默认值 / 初始化值的取值原因 or 含义:
1
2
3
4private:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;对于 TODO 类型的注释,其用于描述临时解决方案、短期解决方案、足够好但不够完美的方案。
TODO 类型的注释以全大写的 “TODO” 开头,其后跟随 bug ID、姓名、邮箱地址以及问题描述。
TODO 最好包含一个 “什么时候做某事” 的截止日期或具体的事件(时机)
# 格式
# 基本规范
- 对于每行代码的长度,尽量不超过 80 个字符(不要有单行过长的代码)
- 尽可能避免使用非 ASCII 的字符(包括但不限于中文字符(中文注释))
- 采用 4 个空格作为缩进,不使用 tabs,或者在编辑器中将 tab 设置为输出 4 个空格
(原文采用的是 2 个空格作为缩进,但我不喜欢) - 每行代码行末不应该尾随多余的空格,对于行末的注释,间隔两个空格之后开始,注释内容与
//
之间留一个空格1
int i = 0; // Two spaces before end-of-line comments.
# 对于函数定义
- 返回值与函数名处于同一行,函数参数尽可能也与函数名处于同一行。
- 左花括号处于一行的行末,与前面的内容之间存在一个空格。
- 如果参数过多,则可适当分行,第一个参数保留在与函数名同一行的位置,后续参数则与第一个参数对齐。
1
2
3
4ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
DoSomething();
...
}
由于这种换行方式,为了避免换行的函数参数与函数体内容混淆,则在这种情况下,把花括号另起一行:
1 | ReturnType ClassName::ReallyLongFunctionName(Type par_name1, |
# 对于函数调用
与函数定义的情况基本一致,如果函数传参过多,则换行,第一个参数与函数名同一行,其余参数换行后与第一个参数对齐:
1 | bool result = DoSomething(averyveryveryverylongargument1, |
# 对于分支与循环语句
- if、while 和 for 之后紧跟左圆括号,不留空格
- else、else if 前后各留一个空格
(这里与原文的 preference 略有偏差,我 prefer 在 if、while、for 和条件之间不希望有空格)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23if(condition) {
DoOneThing();
DoAnotherThing();
} else if (int a = f(); a != 3) { //分号之后要有空格隔开
DoAThirdThing(a);
} else {
DoNothing();
}
// Good - the same rules apply to loops.
while(condition) {
RepeatAThing();
}
// Good - the same rules apply to loops.
do {
RepeatAThing();
} while (condition);
// Good - the same rules apply to loops.
for(int i = 0; i < 10; ++i) { //分号之后要有空格隔开
RepeatAThing();
}
以下情况不被鼓励:
1 | if(condition) |
# 对于空循环
1 | while(condition) {} // Good - `{}` indicates no logic. |
以下情况不被鼓励:
1 | while(condition); // Bad - looks like part of `do-while` loop. |
# 对于指针和引用
采用 “尾随空格” 的形式,即变量类型紧跟星号 *
或引用符 &
,之前不含空格,其后再与变量名之间填充一个空格:
(特殊地,在 <>
表示的模板类或类型转换中,指针或引用不需要任何空格)
1 | char* c; |
另外,当定义指针和引用时,为了避免混淆和误导,不允许在单行定义多个变量:
1 | //以下情况被强烈禁止 |
# 对于条件
对于条件语句和循环语句中的条件表达式,如果存在多个条件,则可适当进行换行,每行以条件的逻辑运算符(如 &&
、 ||
)结尾,(大致上与函数定义的逻辑一致):
1 | if(this_one_thing > this_other_thing && |
# 对于预处理指令
预处理指令顶格书写,不允许缩进:
1 | // Good - directives at beginning of line |
# 对于类内的格式
采用 public、protected、private 的顺序进行代码书写,这三个关键字不缩进(谷歌规范则缩进一个空格,我不喜欢)
# 对于命名空间
其内容不应该 / 不需要因此而缩进一级:
1 | namespace { |