作者:Citrus 写于:2019.10.15 参考 Google C++ 编码规范
1.头文件
通常每一个 .cpp 文件都有一个对应的 .h 文件。也有一些常见例外,如单元测试代码和只包含 main() 函数的 .cpp 文件。
正确使用头文件可令代码在可读性、文件大小和性能上大为改观。以下规则将引导你规避使用头文件时的各种陷阱。
1.1. Self-contained头文件
Tip:所有头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 .h 结尾。
至于用来插入文本的文件,说到底它们并不是头文件,应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法。
所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。
**1.2. #define保护**
**Tip:所有头文件都应该使用**
**define 来防止同一个头文件被多重包含,**
**命名格式当是:<PROJECT>_<PATH>_<FILE>_H_ 。**
为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径。例如,项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_
**1.3. 前置声明**
**Tip:尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。**
「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义。是不完全类型。
注意:
需要在头文件中使用其他文件中定义的类,且只是使用声明而不是定义时,应该用前置声明。
尽量避免前置声明那些定义在其他项目中的实体。(区别本项目)
函数:总是使用 #include。
类模板:优先使用 #include。
**1.4. 内联函数**
**Tip:只有当函数只有 10 行甚至更少时才将其定义为内联函数。**
当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。合理使用内联函数可以令目标代码更高效。
注意:
一个较合理的经验准则是,不要内联超过 10 行的函数。谨慎对待析构函数,析构函数往往比其表面看起来要更长,因为有隐含的成员和基类析构函数被调用!
有些函数即使声明为内联的也不一定会被编译器内联,这点很重要,比如虚函数和递归函数就不会被正常内联。
**1.5. #include 的路径及顺序**
**Tip:使用标准的头文件包含顺序可增强可读性,避免隐藏依赖。**
项目内头文件应按照项目源代码目录树结构排列,避免使用 UNIX 特殊的快捷目录“ . ”(当前目录)或“..”(上级目录)。例如,google_project/src/base/logging.h 应该按如下方式包含:
#include "base/logging.h"
顺序:
1. 相关头文件(本类的声明文件)
2. C 系统文件
3. C++ 系统文件
4. 其他库的 .h 文件
5. 本项目内 .h 文件
注意:
平台特定(system-specific)代码需要条件编译(conditional includes),这部分放到其它includes最后面。
2. 作用域
**2.1. 命名空间**
**Tip: 鼓励在 .cpp 文件内对不需要在其他地方引用的标识符使用匿名命名空间或 static 声明。**
**使用具名的命名空间时,其名称可基于项目名或相对路径。**
**禁止使用 using 指示(using-directive)。禁止使用内联命名空间(inline namespace)。**
注意:
命名空间命名规则可依据项目名或相对路径,在命名空间最后注释出命名空间的名字(包括匿名)。
不要在命名空间std内声明任何东西,包括标准库的类前置声明(会导致不可移植等问题)。
禁止使用using指示引入整个命名空间的标识符号(会污染命名空间)。
不要在头文件中使用 命名空间别名 除非显式标记内部命名空间使用。
禁止用内联命名空间(inline namespace xxx)。
不要在 .h 文件中使用匿名命名空间。
**2.2. 非成员函数、静态成员函数 和 全局函数**
**Tip:使用静态成员函数或命名空间内的非成员函数,尽量不使用裸的全局函数。**
**将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关。**
注意:
可以把与同类的实例脱钩的函数定义为静态成员函数或非成员函数。
非成员函数不应依赖于外部变量,应尽量置于某个命名空间内。
相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间。
**2.3. 局部变量**
**Tip:将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化。**
注意:
属于 if,while 和 for 语句的变量(变量是一个对象时例外)应当在这些语句中正常地声明,这样可以限定这些变量的作用域。
**2.4. 静态变量和全局变量**
**Tip:禁止定义非POD的静态生成周期变量,禁止使用含有副作用的函数初始化POD全局变量,因为编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致难以发现的bug和代码的不可移植。**
静态生存周期的对象:包括了全局变量,静态变量,静态类成员变量和函数静态变量。
POD(原生数据类型Plain Old Data):包括 int,char 和 float,以及 POD 类型的指针、数组和结构体。
注意:
静态生存周期的对象都必须是原生数据类型。
3.类
类是 C++ 中代码的基本单元。本节列举了在写一个类时的主要注意事项。
**3.1. 构造函数**
**Tip:不要在构造函数中调用虚函数,也不要在无法报出错误时进行可能失败的初始化。**
注意:
简单的初始化用类成员初始化(成员变量声明时初始化)完成。
构造函数一般进行一些无意义的初始化,如果对象需要进行有意义的初始化,则考虑使用明确的Init()方法或使用工厂模式。
一个成员变量既在声明时初始化又在构造函数中初始化时,构造函数中的值会覆盖掉声明中的值。
**3.2. 隐式类型转换**
**Tip:不要定义隐式类型转换。对于转换运算符和单参数构造函数,请使用 explicit 关键字。**
注意:
在类型定义中,类型转换运算符和单参数构造函数都应当用 explicit 进行标记。
一个例外就是,拷贝和移动构造函数不应当被标记为 explicit,因为它们并不执行类型转换。
**3.3. 可拷贝类型和可移动类型**
**Tip:如果你的类型需要,就让类支持 拷贝/移动。否则,就把隐式产生的拷贝和移动函数禁用。**
注意:
如果对于你的用户来说这个拷贝操作不是一眼就能看出来,那就不要把类型设置为可拷贝。
如果让类型可拷贝,一定要同时给出拷贝构造函数和赋值操作的定义,反之亦然。
如果让类型可拷贝,同时移动操作的效率高于拷贝操作,那么就把移动的两个操作(移动构造函数和赋值操作)也给出定义。
如果类型不可拷贝,但是移动操作的正确性对用户显然可见,那么把这个类型设置为只可移动并定义移动的两个操作。
由于存在对象切割的风险,不要为任何有可能有派生类的对象提供赋值操作或者拷贝/移动构造函数(当然也不要继承有这样的成员函数的类)。如果你的基类需要可复制属性,请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现。
如果你的类不需要 拷贝/移动 操作,请显式地通过在 public 域中使用 = delete 或其他手段禁用之。
// MyClass is neither copyable nor movable. MyClass(const MyClass&) = delete; MyClass& operator=(const MyClass&) = delete;
**3.4. 结构体 VS. 类**
**Tip:仅当只有数据成员时使用 struct,其它一概使用 class。**
注意:
struct 用来定义包含数据的被动式对象,也可以包含相关的常量,但除了存取数据成员之外,没有别的函数功能。并且存取功能是通过直接访问位域,而非函数调用。除了构造函数,析构函数,Initialize(),Reset(),Validate() 等类似的用于设定数据成员的函数外,不能提供其它功能的函数。
如果需要更多的函数功能 class 更适合。如果拿不准,就用 class。
类和结构体的成员变量有着不同的命名规则。
**3.5. 继承**
**Tip:使用组合常常比使用继承更合理。如果使用继承的话,定义为 public 继承。**
当子类继承基类时,子类包含了父基类所有数据及操作的定义。C++ 实践中,继承主要用于两种场合:实现继承,子类继承父类的实现代码;接口继承,子类仅继承父类的方法名称。
注意:
所有继承必须是public的。如果你想使用私有继承,你应该替换成把基类的实例作为成员对象的方式。
不要过度使用实现继承,组合常常更合适一些。尽量做到只在“是一个”的情况下使用继承。
必要的话,析构函数声明为virtual。如果你的类有虚函数,则析构函数也应该为虚函数。
**3.6. 多重继承**
**Tip:真正需要用到多重实现继承的情况少之又少。只在以下情况我们才允许多重继承:最多只有一个基类是非抽象类;其它基类都是以 Interface 为后缀的纯接口类。**
多重继承允许子类拥有多个基类。要将作为纯接口 的基类和具有 实现 的基类区别开来。
注意:
只有当所有父类除第一个外都是纯接口类时,才允许使用多重继承。为确保它们是纯接口,这些类必须以 Interface 为后缀。
**3.7. 接口**
**Tip:接口是指满足特定条件的类,这些类以 Interface 为后缀命名(不强制)。**
接口类不能被直接实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(且析构函数不能是纯虚函数)。
纯接口条件:
1. 只有纯虚函数“=0”和静态函数(除了下文提到的析构函数)。
2. 没有非静态数据成员。
3. 没有定义任何构造函数。如果有,也不能带有参数,并且必须为 protected。
4. 如果它是一个子类,也只能从满足上述条件并以 Interface 为后缀的类继承。
注意:
只有满足上述条件,类才以 Interface 结尾,但反过来,满足上述条件的类未必一定以 Interface 结尾。
**3.8. 运算符重载**
**Tip:除少数特定环境外,不要重载运算符。也不要定义新的字面量。**
C++ 允许用户通过使用 operator 关键字对内建运算符进行重载定义,只要其中一个参数是用户定义的类型。operator关键字还允许用户使用 operator"" 定义新的字面运算符,并且定义类型转换函数。例如 operator bool()。
注意:
一般不要重载运算符,只有在意义明显,不会出现奇怪的行为并且与对应的内建运算符的行为一致时才定义重载运算符。
**3.9. 存取控制**
**Tip:将所有数据成员声明为 private,并根据需要提供相应的存取函数。**
**3.10. 声明顺序**
**Tip:在类中使用特定的声明顺序:public、protected、private,省略空部分。**
**成员函数在成员变量前。**
每个区段内声明顺序:
1. typedefs和枚举
2. 常量
3. 构造函数
4. 析构函数
5. 成员函数(含静态成员函数)
6. 成员变量(含静态成员变量)
4. 函数
**4.1. 参数顺序**
**Tip:函数的参数顺序为:输入参数在先,后跟输出参数。**
注意:
输入参数通常是值参或 const 引用。
输出参数则一般为非 const 指针。
**4.2. 引用参数**
**Tip:所有按引用传递的参数必须加上 const。**
void Foo(const string &in, string *out); //或 void Foo(const string &in, string *out); //或 void Foo(const string &in, string *out);
**4.3. 函数重载**
**Tip:若要使用函数重载,则必须能让读者一看调用点就明白,而不用花心思猜测调用的重载函数到底是哪一种。这一规则也适用于构造函数。**
class MyClass { public: void Analyze(const string &text); void Analyze(const char *text, size_t textlen); };
**4.4. 缺省参数**
**Tip:尽量少使用缺省函数参数,少数极端情况除外。可改用函数重载。**
注意:
只允许在非虚函数中使用缺省参数,且必须保证缺省参数的值始终一致。
缺省参数与函数重载 遵循同样的规则。
5. 其他C++ 特性
**5.1. 右值引用**
**Tip:只在定义移动构造函数与移动赋值操作时使用右值引用。不要使用 std::forward。**
**5.2. 变长数组和 alloca( )**
**Tip:我们不允许使用变长数组和 alloca()。**
注意:
变长数组和 alloca() 不是标准 C++ 的组成部分。
它们根据数据大小动态分配堆栈内存,会引起难以发现的内存越界bug导致程序挂掉。
可用更安全的分配器(allocator),就像 std::vector 或 std::unique_ptr<T[]>。
**5.3. 友元**
**Tip:允许合理的使用友元类及友元函数.**
**5.4. 异常**
**Tip:Google建议不使用 C++ 异常。**
**5.5. 运行时类型识别**
**Tip:禁止使用 RTTI。**
RTTI(运行时类型信息Runtime Type Information)允许程序员在运行时识别 C++ 类对象的类型。它通过使用 typeid 或者 dynamic_cast 完成。
int i = 10; char ch = 'a'; A *pA1 = new A(); A a2; cout<<typeid(i).name()<<endl; // int cout<<typeid(ch).name()<<endl; // char cout<<typeid(pA1).name()<<endl; // class A * cout<<typeid(a2).name()<<endl; // class A
**5.6. 类型转换**
**Tip:使用 C++ 的类型转换,如 static_cast<>()。不要使用 int y = (int)x 或 int y =int(x) 等转换方式。**
C++ 采用了有别于 C 的类型转换机制,对转换操作进行归类。
char a = 'a'; int b = static_cast<char>(a);
注意:
用 static_cast 替代 C 风格的值转换,或某个类指针需要明确的向上转换为父类指针时。
用 const_cast 去掉 const 限定符。
用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换。
**5.7. 流**
**Tip:只在记录日志时使用流。**
注意:
不要使用流,使用 printf 之类的代替,除非是日志接口需要。
使用流还有很多利弊,但代码一致性胜过一切。尽量不要在代码中使用流。
**5.8. 前置自增(自减)**
**Tip:对于迭代器和其他模板对象使用前缀形式的自增(自减)运算符。**
注意:
对简单数值(非对象)两种都无所谓;对迭代器和模板类型,使用前置自增(自减)。
不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高。因为后置自增(自减)需要对表达式的值 i 进行一次拷贝。
**5.9. const 用法**
**Tip:在任何可能的情况下都使用 const。此外有时改用 C++11 推出的 constexpr 更好。**
注意:
在声明的变量或参数前加const 表明变量值不可被篡改。
在类中的函数加 const表明该函数不会修改类成员变量的状态。
在C++11中使用constexpr定义真正的常量,或实现常量初始化。
**5.10. 整型**
**Tip:C++ 内建整型中,仅使用 int。如果程序中需要不同大小的变量,可以使用 <stdint.h> 中长度精确的整型。**
C++ 没有指定整型的大小。通常假定short是16位,int是32位,long是32位,long long是64位。
注意:
<stdint.h>定义了int16_t,uint32_t,int64_t等整型,在确保整型大小时可使用它们代替short,unsigned long long等。
如果您的变量可能不小于2^31(2GiB)就用64位变量比如int64_t。
此外要留意,哪怕您的值并不会超出int所能够表示的范围,在计算过程中也可能会溢出。所以拿不准时,干脆用更大的类型。
**5.11. 预处理宏**
**Tip:使用宏时要非常谨慎,尽量以内联函数,枚举和常量代替。**
宏意味着你和编译器看到的代码是不同的。这可能会导致异常行为,因为宏具有全局作用域。
注意:
以下用法可避免使用宏带来的问题(如要用宏可遵守):
1. 不要在 .h 文件中定义宏。
2. 在马上要使用时才进行 #define,使用后要立即 #undef。
3. 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称。
4. 不要试图使用展开后会导致 C++ 构造不稳定的宏,不然也至少要附上文档说明其行为。
5. 不要用 ## 处理函数,类和变量的名字。
**5.12. 0,nullptr 和 NULL**
**Tip:整数用 0,实数用 0.0,指针用 nullptr 或 NULL,字符(串)用 ' '。**
**5.13. sizeof**
**Tip:尽可能用sizeof(varname) 代替sizeof(type)。**
注意:
推荐使用
sizeof(varname) 是因为当代码中变量类型改变时会自动更新。
sizeof(type) 处理不涉及任何变量的代码,如果处理来自外部或内部的数据格式,用变量就不合适了。
**5.14. auto**
**Tip:用 auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方。**
注意:
auto只能用在局部变量里用。别用在文件作用域变量、命名空间作用域变量和类数据成员里。永远别列表初始化auto变量。
**5.15. 列表初始化**
**Tip:任何对象类型都可以被列表初始化。**
struct Point { int x; int y; }; Point p = {1, 2};
**5.16. Lambda 表达式**
**Tip:适当使用 lambda 表达式。别用默认 lambda 捕获,所有捕获都要显式写出来。**
Lambda 表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传
注意:
匿名函数始终要简短,如果函数体超过了五行,那么还不如起名或改用函数。
禁用默认捕获,捕获都要显式写出来。比如 [=](int x) {return x + n;},就应该写成 [n](int x){return x + n;} 才对,这样读者也好一眼看出 n 是被捕获的值。
6. 命名约定
**6.1. 通用命名规则**
**Tip:函数命名、变量命名、文件命名都要有描述性,少用缩写。**
注意:
一些特定的广为人知的缩写是允许的,例如用 i 表示迭代变量和用 T 表示模板参数。
**6.2. 文件命名**
**Tip:文件名全部小写,可以包含下划线“_”或连字符“-”。依照项目的约定,如果没有约定用“_”更好。**
**6.3. 类型命名**
**Tip:类型名称的每个单词首字母均大写,且不包含下划线。**
注意:
所有类型命名:类、结构体、类型定义(typedef)、枚举、类型模板参数,均使用相同约定。
// 类和结构体 class UrlTable { ... class UrlTableTester { ... struct UrlTableProperties { ... // 类型定义 typedef hash_map<UrlTableProperties *, string> PropertiesMap; // using 别名 using PropertiesMap = hash_map<UrlTableProperties *, string>; // 枚举 enum UrlTableErrors { ...
**6.4. 变量命名**
**Tip:变量(包括函数参数)和数据成员名一律小写,单词之间用下划线连接。**
注意:
不管是静态还是非静态,类成员变量用“m_”开头,结构体成员不需要,全局变量用“g_”开头。
**6.5. 常量命名**
**Tip:声明为 constexpr 或 const 的变量,或在程序运行期间其值始终保持不变的,命名时字母全大写,单词之间用下划线连接。**
**6.6. 函数命名**
**Tip:常规函数名除第一个字母小写外其他单词首字母大写,且不包含下划线。**
注意:
取值和设置函数要求与变量名匹配。
对于首字母缩写的单词,更倾向于将它们视作一个普通的单词,只对该单词首字母大写。
**6.7. 命名空间命名**
**Tip:命名空间以小写字母命名。最高级命名空间的名字取决于项目名称。**
注意:
避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突。
不要使用缩写作为命名空间名称。
**6.8. 枚举命名**
**Tip:枚举的命名应当和常量或宏一致:ENUM_NAME。**
注意:
枚举名是类型,所以要用大小写混合的方式。
enum AlternateUrlTableErrors { OK = 0, OUT_OF_MEMORY = 1, MALFORMED_INPUT = 2, };
**6.9. 宏命名**
**Tip:命名像枚举命名一样全部大写,使用下划线分隔。**
7. 注释
使用 // 或 /* */,统一就好。
**7.1. 文件注释**
**Tip:在每一个文件开头加入版权公告。**
内容顺序:
1. 版权
2. 许可证
3. 作者
4. 文件内容简短说明
**7.2. 类注释**
**Tip:每个类的定义都要附带一份注释,描述类的功能和用法,除非它的功能相当明显。**
注意:
如果类的声明和定义分开了,那么描述类用法的注释应当和接口定义放在一起,描述类的操作和实现的注释应当和实现放在一起。
**7.3. 函数注释**
**Tip:函数声明处的注释描述函数功能;定义处的注释描述函数实现。**
注释内容:
1. 函数的输入输出。
2. 对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数。
3. 函数是否分配了必须由调用者释放的空间。
4. 参数是否可以为空指针。
5. 是否存在函数使用上的性能隐患。
6. 如果函数是可重入的,其同步前提是什么?
**7.4. 变量注释**
**Tip:通常变量名本身足以很好说明变量用途。某些情况下,也需要额外说明含义及用途。**
**7.5. 实现注释**
**Tip:对于代码实现中巧妙的、晦涩的、有趣的、重要的地方可适当加以注释。**
**7.6. TODO注释**
**Tip:对那些临时的,短期的解决方案,或已经够好但仍不完美的代码使用 TODO 注释。**
注意:
TODO 注释要使用全大写的字符串 TODO, 在随后的圆括号里写上你的名字或其它身份标识。
// TODO(kl@gmail.com): Use a "*" here for concatenation operator. // TODO(Zeke) change this to use relations. // TODO(bug 12345): remove the "Last visitors" feature
**7.7. 弃用注释**
**Tip:通过弃用注释(DEPRECATED comments)以标记某接口点已弃用。**
**附图:下图分别是 头文件、实现文件 的格式**