命名约定
通用命名规则
-
函数命名, 变量命名, 文件命名要有描述性; 少用缩写
- 尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词
int price_count_reader; // 无缩写 int num_errors; // "num" 是一个常见的写法 int num_dns_connections; // 人人都知道 "DNS" 是什么
int n; // 毫无意义. int nerr; // 含糊不清的缩写. int n_comp_conns; // 含糊不清的缩写. int wgc_connections; // 只有贵团队知道是什么意思. int pc_reader; // "pc" 有太多可能的解释了. int cstmr_id; // 删减了若干字母.
- 尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词
-
一些特定的广为人知的缩写是允许的, 例如用 i 表示迭代变量和用 T 表示模板参数
文件命名
- 文件名要全部小写, 可以包含下划线
_
或连字符-
, 依照项目的约定. 如果没有约定, 那么_
更好.
类型命名
- 类型名称的每个单词首字母均大写, 不包含下划线
_
: MyExcitingClass, MyExcitingEnum// 类和结构体 class UrlTable { ... class UrlTableTester { ... struct UrlTableProperties { ... // 类型定义 typedef hash_map<UrlTableProperties *, string> PropertiesMap; // using 别名 using PropertiesMap = hash_map<UrlTableProperties *, string>; // 枚举 enum UrlTableErrors { ...
变量命名
-
变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线
_
连接. 类的成员变量以下划线_
结尾, 但结构体的就不用,如: a_local_variable, a_struct_data_member, a_class_data_member_.-
普通变量命名
string table_name; // 好 - 用下划线. string tablename; // 好 - 全小写. string tableName; // 差 - 混合大小写
-
类数据成员
- 不管是静态的还是非静态的, 类数据成员都可以和普通变量一样, 但要接下划线.
class TableInfo { ... private: string table_name_; // 好 - 后加下划线. string tablename_; // 好. static Pool<TableInfo>* pool_; // 好. };
- 不管是静态的还是非静态的, 类数据成员都可以和普通变量一样, 但要接下划线.
-
-
结构体变量
- 不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类那样接下划线
struct UrlTableProperties { string name; int num_entries; static Pool<UrlTableProperties>* pool; };
- 不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类那样接下划线
常量命名
- 声明为
constexpr
或const
的变量, 或在程序运行期间其值始终保持不变的, 命名时以k
开头, 大小写混合. 所有具有静态存储类型的变量 (例如静态变量或全局变量) 都应当以此方式命名.- 例如:
const int kDaysInAWeek = 7;
- 例如:
宏命名
- 全部大写,使用下划线
_
函数命名
- 常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配(不强制要求)
AddTableEntry() DeleteUrl() OpenFileOrDie()
命名空间命名
枚举命名
- 与常量或宏一致
// 两种写法都可以 enum UrlTableErrors { kOK = 0, kErrorOutOfMemory, kErrorMalformedInput, }; enum AlternateUrlTableErrors { OK = 0, OUT_OF_MEMORY = 1, MALFORMED_INPUT = 2, };
格式
循环和开关选择语句
- 如果有不满足
case
条件的枚举值,switch
应该总是包含一个default
匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning). 如果 default 应该永远执行不到, 简单的加条 assertswitch (var) { case 0: { // 2 空格缩进 ... // 4 空格缩进 break; } case 1: { ... break; } default: { assert(false); } }
- 空循环体应使用
{}
或continue
, 而不是一个简单的分号;
for (int i = 0; i < kSomeNumber; ++i) {} // 可 - 空循环体. while (condition) continue; // 可 - contunue 表明没有逻辑.
条件语句
if (condition) { // 圆括号里没有空格.
... // 2 空格缩进.
} else if (...) { // else 与 if 的右括号同一行.
...
} else {
...
}
-
所有情况下
if
和(
间都有个空格.)
和{
之间也要有个空格if (condition) { // 好 - IF 和 { 都与空格紧邻
-
如果能增强可读性, 简短的条件语句允许写在同一行
-
通常, 单行语句不需要使用大括号
{}
-
如果语句中某个 if-else 分支使用了大括号的话, 其它分支也必须使用(即
if
有else
也要有)
命名空间格式化
- 命名空间内不要增加额外的缩进
namespace { void foo() { // 正确. 命名空间内没有额外的缩进. ... } } // namespace ``
水平留白
-
通用
- 列表初始化中大括号内的空格是可选的
int x[] = { 0 }; int x[] = {0};
- 继承与初始化列表中的冒号前后恒有空格
class Foo : public Bar { public: // 对于单行函数的实现, 在大括号内加上空格 // 然后是函数实现 Foo(int b) : Bar(), baz_(b) {} // 大括号里面是空的话, 不加空格. };
- 对于单行函数的实现, 在大括号内加上空格
void Reset() { baz_ = 0; } // 用空格把大括号与实现分开.
- 添加冗余的留白会给其他人编辑时造成额外负担. 因此, 行尾不要留空格
- 列表初始化中大括号内的空格是可选的
-
循环和条件语句
switch (i) { case 1: // switch case 的冒号前无空格. ... case 2: break; // 如果冒号有代码, 加个空格. }
-
操作符
- 其它二元操作符也前后恒有空格
v = w * x + y / z;
- 对于表达式的子式可以不加空格
v = w*x + y/z;
- 其它二元操作符也前后恒有空格
-
模板和转换
- 尖括号不与空格紧邻,
<
前没有空格,>
和(
之间也没有vector<string> x; y = static_cast<char*>(x);
- 在类型与指针操作符之间留空格也可以, 但要保持一致
vector<char *> x;
- 尖括号不与空格紧邻,
垂直留白
-
垂直留白越少越好
-
两个函数定义之间的空行不要超过 2 行
-
函数体首尾不要留空行, 函数体中也不要随意添加空行(可读性微乎其微)
函数声明与定义
-
返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行
-
如果同一行文本太多, 放不下所有参数
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { DoSomething(); ... }
-
甚至连第一个参数都放不下(参数另起一行且缩进四格)
ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 4 space indent Type par_name2, Type par_name3) { DoSomething(); // 2 space indent ... }
-
未被使用的参数如果其用途不明显的话, 在函数定义处将参数名注释起来
void Circle::Rotate(double /*radians*/) {}
-
注意!
- 只有在参数未被使用或者其用途非常明显时, 才能省略参数名
- 如果返回类型与函数声明或定义分行了, 不要缩进
(
总是和函数名在同一行- 函数名和
(
间永远没有空格 - 圆括号与参数间没有空格
{
总在最后一个参数同一行的末尾处, 不另起新行}
总是单独位于函数最后一行, 或者与{
同一行)
和{
间总是有一个空格- 所有形参应尽可能对齐
- 缺省缩进为 2 个空格
- 换行后的参数保持 4 个空格的缩进
行长度
-
每一行代码字符数不超过80
- 如果无法在不伤害易读性的条件下进行断行, 那么注释行可以超过 80 个字符.例如, 带有命令示例或 URL 的行可以超过 80 个字符.
- 头文件保护可以无视该原则
-
包含长路径的 #include 语句可以超出80列
非 ASCII 字符
- 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码
空格还是制表位
- 只使用空格,不要在代码中使用制表符,每次缩进两个空格**(Google风格)**
Lambda 表达式
- 若用引用捕获, 在变量名和 & 之间不留空格
auto add_to_x = [&x](int n) { x += n; };
函数调用
- 类似于函数声明
列表初始化格式
-
格式化时将将名字视作函数调用名,
{}
视作函数调用的括号. 如果没有名字, 就视作名字长度为零- 不得不断行
SomeFunction( {"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字. some_other_function_parameter);
- 稍微短的字符串
SomeOtherType{"Slightly shorter string", // 稍短的字符串. some, other, values}};
- 不得不断行
指针和引用表达式
- 在声明指针变量或参数时, 星号与类型或变量名紧挨都可以
// 好, 空格前置. char *c; const string &str; // 好, 空格后置. char* c; const string& str;
- 在多重声明时不能使用
&
或*
,建议分开声明int x, *y; // 不允许 - 在多重声明中不能使用 & 或 *
布尔表达式
- 断行时逻辑与
&&
操作符总位于行尾if (this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another && last_one) { ... }
函数返回值
- 不要在
return
表达式里加上非必须的圆括号return (value); // 毕竟您从来不会写 var = (value); return(result); // return 可不是函数!
// 可以用圆括号把复杂表达式圈起来, 改善可读性. return (some_long_condition && another_condition);
变量及数组初始化
-
列表初始化不允许整型类型的四舍五入, 这可以用来避免一些类型上的编程失误(大括号初始化禁止内建型别之间进行隐式窄化型别转换)
int pi(3.14); // 好 - pi == 3. int pi{3.14}; // 编译错误: 缩窄转换
-
非空列表初始化就会优先调用
std::initializer_list
构造函数 -
空列表
{}
初始化原则上会调用默认构造函数
预处理指令
- 预处理指令不要缩进, 从行首开始
- 非必要 -
#
后跟空格
类格式
class MyClass : public OtherClass {
public: // 注意有一个空格的缩进
MyClass(); // 标准的两空格缩进
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
void SomeFunctionThatDoesNothing() {
}
void set_some_var(int var) { some_var_ = var; }
int some_var() const { return some_var_; }
private:
bool SomeInternalFunction();
int some_var_;
int some_other_var_;
};
-
注意
- 所有基类名应在 80 列限制下尽量与子类名放在同一行
- 关键词
public:
,protected:
,private:
要缩进 1 个空格 - 除第一个关键词 (一般是
public
) 外, 其他关键词前要空一行. 如果类比较小的话也可以不空
构造函数初始值列表
- 构造函数初始化列表放在同一行或按四格缩进并排多行.
// 如果所有变量能放在同一行: MyClass::MyClass(int var) : some_var_(var) { DoSomething(); } // 如果不能放在同一行, // 必须置于冒号后, 并缩进 4 个空格 MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) { DoSomething(); } // 如果初始化列表需要置于多行, 将每一个成员放在单独的一行 // 并逐行对齐 MyClass::MyClass(int var) : some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up DoSomething(); } // 右大括号 } 可以和左大括号 { 放在同一行 // 如果这样做合适的话 MyClass::MyClass(int var) : some_var_(var) {}
头文件
#define保护
-
所有头文件都应该使用#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_
- 为保证唯一性, 头文件的命名应该基于所在项目"源代码树"的全路径。如项目foo中的头文件foo/src/bar/baz.h可以按此方式保护
前置声明
-
定义:指类、函数和模板的存粹声明,没伴随着其定义
-
尽可能地避免使用前置声明那些定义在其他项目中的实体,使用#include包含需要的头文件即可
内联函数
-
相关内联函数的性能差异,可参考[Effective C++ 条款2]
-
只有当函数只有10行甚至更少时才将其定义为内联函数
-
内联那些包含循环或switch语句的函数常常得不偿失
-
有些函数即使声明为内联的也不一定会被编译器内联(只是个请求)
-
类内部的函数一般会自动内联,所以某函一旦不需要内联就应该放到对应的.cc文件里,这样可以保持头文件的类相当精炼,也很好地贯彻了声明与定义分离的原则
#include的路径及顺序
-
项目内头文件应按照项目源代码目录树结构排列
-
包含头文件的次序
- cpp对应的头文件
- C系统文件
- C++系统文件
- 其他库的.h文件
- 本项目内的.h文件
注释
注释风格
//
或/* */
都可以; 但//
更 常用. 要在如何注释及注释风格上确保统一.
文件注释
-
在每一个文件开头加入版权公告
-
文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释.
-
法律公告和作者信息
- 每个文件都应该包含许可证引用. 为项目选择合适的许可证版本
- 如果你对原始作者的文件做了重大修改, 请考虑删除原作者信息
-
文件内容
- 如果一个 .h 文件声明了多个概念, 则文件注释应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系. 一个一到两行的文件注释就足够了, 对于每个概念的详细文档应当放在各个概念中, 而不是文件注释中.
类注释
-
每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显
// Iterates over the contents of a GargantuanTable. // Example: // GargantuanTableIterator* iter = table->NewIterator(); // for (iter->Seek("foo"); !iter->done(); iter->Next()) { // process(iter->key(), iter->value()); // } // delete iter; class GargantuanTableIterator { ... };
-
类注释应当为读者理解如何使用与何时使用类提供足够的信息, 同时应当提醒读者在正确使用此类时应当考虑的因素. 如果类有任何同步前提, 请用文档说明. 如果该类的实例可被多线程访问, 要特别注意文档说明多线程环境下相关的规则和常量使用.
-
如果你想用一小段代码演示这个类的基本用法或通常用法, 放在类注释里也非常合适
-
如果类的声明和定义分开了(例如分别放在了 .h 和 .c 文件中), 此时, 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.
函数注释
-
函数声明处的注释描述函数功能; 定义处的注释描述函数实现.
-
函数声明处注释的内容
- 函数的输入输出.
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
- 函数是否分配了必须由调用者释放的空间.
- 参数是否可以为空指针.
- 是否存在函数使用上的性能隐患.
- 如果函数是可重入的, 其同步前提是什么?
-
函数重载时
- 注释的重点应该是函数中被重载的部分
-
函数定义
-
如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释.
- 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由. 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要
-
不要 从 .h 文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上
-
变量注释
-
类数据成员
- 每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途
- 如果有非变量的参数(例如特殊值, 数据成员之间的关系, 生命周期等)不能够用类型与变量名明确表达, 则应当加上注释
- 如果变量类型与变量名已经足以描述一个变量, 那么就不再需要加上注释.
- 特别地, 如果变量可以接受 NULL 或 -1 等警戒值, 须加以说明
private: // Used to bounds-check table accesses. -1 means // that we don't yet know how many entries the table has. int num_total_entries_;
-
全局变量
- 注释说明含义及用途, 以及作为全局变量的原因
// The total number of tests cases that we run through in this regression test. const int kNumTestCases = 6;
- 注释说明含义及用途, 以及作为全局变量的原因
实现注释
-
代码前注释
- 巧妙或复杂的代码段前要加注释
// Divide result by two, taking into account that x // contains the carry from the add. for (int i = 0; i < result->size(); i++) { x = (x << 8) + (*result)[i]; (*result)[i] = x >> 1; x &= 1; }
- 巧妙或复杂的代码段前要加注释
-
行注释
- 比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释即
//
// If we have enough memory, mmap the data portion too. mmap_budget = max<int64>(0, mmap_budget - index_->length()); if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock)) return; // Error already logged.
- 如果你需要连续进行多行注释, 可以使之“对齐"获得更好的可读性
- 比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释即
标点, 拼写和语法
-
注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句
-
完整的句子比句子片段可读性更高
-
短一点的注释, 比如代码行尾注释, 可以随意点, 但依然要注意风格的一致性
TODO 注释
-
标记下次我们需要做的工作
-
TODO
注释要使用全大写的字符串TODO
, 在随后的圆括号里写上你的名字, 邮件地址, bug ID, 或其它身份标识和与这一TODO
相关的 issue -
主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的
TODO
格式进行查找. 添加TODO
注释并不意味着你要自己来修正
作用域
命名空间
-
禁止使用内联命名空间
-
cpp中的匿名命名空间可避免命名冲突, 限定作用域, 避免直接使用 using 关键字污染命名空间
-
作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率
局部变量
-
如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.
// 低效的实现 for (int i = 0; i < 1000000; ++i) { Foo f; // 构造函数和析构函数分别调用 1000000 次! f.DoSomething(i); }
- 在循环作用域外面声明这类变量要高效的多
Foo f; for (int i = 0; i < 1000000; ++i) { f.DoSomething(i); }
- 在循环作用域外面声明这类变量要高效的多
-
尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元
-
多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致的 bug
函数
参数顺序
-
输入参数是值参或
const
引用, 输出参数为指针 -
函数的参数顺序为: 输入参数在先, 后跟输出参数
int func(int a , int *p) { *p = a +10; return 0; }
编写简短函数
- 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割
引用参数
- 所有按引用传递的参数必须加上
const
void Foo(const string &in, string *out);
缺省参数
- 只允许在非虚函数中使用缺省参数, 对于虚函数, 不允许使用缺省参数,且必须保证缺省参数的值始终一致
函数返回类型后置语法
- 只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
类
构造函数
-
不要在构造函数中调用虚函数
-
不要在无法报错时进行可能失败的初始化
隐式类型转换
-
不要定义隐式类型转换
-
对于转换运算符和单参数构造函数, 请使用
explicit
关键字class Foo { explicit Foo(int x, double y); // 禁止发生隐式转换 ... }; void Func(Foo f);
-
拷贝和移动构造函数不应当被标记为
explicit
,因为它们并不执行类型转换
可拷贝类型和可移动类型
- 如果你的类不需要拷贝 / 移动操作, 请显式地通过在
public
域中使用= delete
或其他手段禁用之. [Effective C++ 条款6]// MyClass is neither copyable nor movable. MyClass(const MyClass&) = delete; MyClass& operator=(const MyClass&) = delete;
结构体
-
仅当只有数据成员时使用
struct
, 其它一概使用class
-
为了和 STL 保持一致, 对于仿函数等特性可以不用 class 而是使用 struct
继承
-
以下约定俗成可参看Effective C++
-
使用组合,GOF在设计模式中强调的,常常比使用继承更合理
-
"is a"使用
public
继承 -
"has a"使用组合
-
"is implement in items of"使用
private
或者组合 -
有虚成员函数或者多态时使用虚析构函数
-
对于重载的虚函数或虚析构函数, 使用
override
, 或 (较不常用的)final
关键字显式地进行标记, 用于判定该函数是否是虚函数 -
final
关键字:禁止类被继承,禁止虚函数被重写 -
override
关键字:声明在子类中重写的虚函数
多重继承
- 只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承
接口
-
满足特定条件的类
-
纯接口
- 只有纯虚函数 (“=0”) 和静态函数 (除了下文提到的析构函数).
- 没有非静态数据成员.
- 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为
protected
. - 如果它是一个子类, 也只能从满足上述条件并以
Interface
为后缀的类继承.
-
-
接口类不能被直接实例化, 因为它声明了纯虚函数,为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数
运算符重载
- 除少数特定环境外, 不要重载运算符. 也不要创建用户定义字面量
存取控制
- 将所有数据成员声明为
private
, 除非是static const
类型成员 (遵循 常量命名规则). 处于技术上的原因, 在使用 Google Test 时我们允许测试固件类中的数据成员为 protected
声明顺序
-
类定义一般应以
public:
开始, 后跟protected:
, 最后是private:
. 省略空部分. -
在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序:
- 类型 (包括 typedef, using 和嵌套的结构体与类)
- 常量(如enum)
- 工厂函数(虚函数)
- 构造函数
- 赋值运算符
- 析构函数
- 其它函数
- 数据成员