背景介绍
C++ 是 Google 大部分开源项目的主要编程语言. 正如每个 C++ 程序员都知道的, C++ 有很多强大的特性, 但这种强大不可避免的导致它走向复杂,使代码更容易产生 bug, 难以阅读和维护.
本指南的目的是通过详细阐述 C++ 注意事项来驾驭其复杂性. 这些规则在保证代码易于管理的同时, 也能高效使用 C++ 的语言特性.
风格, 亦被称作可读性, 也就是指导 C++ 编程的约定. 使用术语 “风格” 有些用词不当, 因为这些习惯远不止源代码文件格式化这么简单.
使代码易于管理的方法之一是加强代码一致性. 让任何程序员都可以快速读懂你的代码这点非常重要. 保持统一编程风格并遵守约定意味着可以很容易根据 “模式匹配” 规则来推断各种标识符的含义. 创建通用, 必需的习惯用语和模式可以使代码更容易理解. 在一些情况下可能有充分的理由改变某些编程风格, 但我们还是应该遵循一致性原则,尽量不这么做.
本指南的另一个观点是 C++ 特性的臃肿. C++ 是一门包含大量高级特性的庞大语言. 某些情况下, 我们会限制甚至禁止使用某些特性. 这么做是为了保持代码清爽, 避免这些特性可能导致的各种问题. 指南中列举了这类特性, 并解释为什么这些特性被限制使用.
Google 主导的开源项目均符合本指南的规定.
注意: 本指南并非 C++ 教程, 我们假定读者已经对 C++ 非常熟悉.
本文章参考了 Google 开源项目风格指南 (中文版). 并节选部分了对作者有用的部分。
1.头文件
1.1头文件一致性
每一个CPP应该对应一个H文件,不应该出现一个H对应多个CPP,或者多个H对应一个CPP文件。
1.2 #define 保护
所有头文件都应该使用 #define
来防止头文件被多重包含。
为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo
中的头文件 foo/src/bar/baz.h
可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
1.3 前置声明
尽可能地避免使用前置声明。使用 #include
包含需要的头文件即可。
所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义.
-
函数:总是使用
#include
. -
类模板:优先使用
#include
.
1.4 内联函数
只有当函数只有 10 行甚至更少时才将其定义为内联函数.
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch
语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch
语句从不被执行).
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数. 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
在linux系统和Window系统中,不同编译器会导致同一代码的编译错误。
1.5 #include
的路径及顺序
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.
项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 .
(当前目录) 或 ..
(上级目录). 例如,
#include "foo/public/fooserver.h" // 优先位置
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
错误的示例如下:
#include "../../foo/public/bar.h"
2. 变量
2.1 命名空间及其使用
-
不要在命名空间
std
内声明任何东西, 包括标准库的类前置声明. 在std
命名空间声明实体是未定义的行为, 会导致如不可移植. 声明标准库下的实体, 需要包含对应的头文件. -
不应该使用 using 指示 引入整个命名空间的标识符号。
// 禁止 —— 污染命名空间 using namespace foo; test() // 最好用 foo::test()
2.2 局部变量
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:
糟糕的声明:
int i;
i = f(); // 坏——初始化和声明分离
比较好的
int j = g(); // 好——初始化时声明
糟糕的声明:
vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
比较好的
vector<int> v = {1, 2}; // 好——v 一开始就初始化
糟糕的
// 低效的实现
for (int i = 0; i < 1000000; ++i)
{
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
比较好的
Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i)
{
f.DoSomething(i);
}
3. 命名法则
最重要的一致性规则是命名管理. 命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义: 类型, 变量, 函数, 常量, 宏, 等等, 甚至. 我们大脑中的模式匹配引擎非常依赖这些命名规则.
命名规则具有一定随意性, 但相比按个人喜好命名, 一致性更重要, 所以无论你认为它们是否重要, 规则总归是规则.
3.1. 通用命名规则
总述
函数命名, 变量命名, 文件命名要有描述性; 少用缩写.
说明
尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词.
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
表示模板参数.
模板参数的命名应当遵循对应的分类: 类型模板参数应当遵循 类型命名 的规则, 而非类型模板应当遵循 变量命名 的规则.
3.2. 文件命名
总述
文件名要全部小写, 可以包含下划线 (_
) 或连字符 (-
), 依照项目的约定. 如果没有约定, 那么 “_
” 更好.
说明
可接受的文件命名示例:
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc
//_unittest
和_regtest
已弃用.
C++ 文件要以 .cc
结尾, 头文件以 .h
结尾. 专门插入文本的文件则以 .inc
结尾, 参见 头文件自足.
不要使用已经存在于 /usr/include
下的文件名 (Yang.Y 注: 即编译器搜索系统头文件的路径), 如 db.h
.
通常应尽量让文件名更加明确. http_server_logs.h
就比 logs.h
要好. 定义类时文件名一般成对出现, 如 foo_bar.h
和 foo_bar.cc
, 对应于类 FooBar
.
内联函数必须放在 .h
文件中. 如果内联函数比较短, 就直接放在 .h
中.
3.3. 类型命名
总述
类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass
, MyExcitingEnum
.
说明
所有类型命名 —— 类, 结构体, 类型定义 (typedef
), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线. 例如:
// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;
// 枚举
enum UrlTableErrors { ...
3.4. 变量命名
总述
变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: 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;
};
结构体与类的使用讨论, 参考 结构体 vs. 类.
3.5. 常量命名
总述
声明为 constexpr
或 const
的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. 例如:
const int kDaysInAWeek = 7;
说明
所有具有静态存储类型的变量 (例如静态变量或全局变量, 参见 存储类型) 都应当以此方式命名. 对于其他存储类型的变量, 如自动变量等, 这条规则是可选的. 如果不采用这条规则, 就按照一般的变量命名规则.
3.6. 函数命名
总述
常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction()
, MyExcitingMethod()
, my_exciting_member_variable()
, set_my_exciting_member_variable()
.
说明
一般来说, 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线. 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 StartRpc()
而非 StartRPC()
).
AddTableEntry()
DeleteUrl()
OpenFileOrDie()
(同样的命名规则同时适用于类作用域与命名空间作用域的常量, 因为它们是作为 API 的一部分暴露对外的, 因此应当让它们看起来像是一个函数, 因为在这时, 它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节.)
取值和设值函数的命名与变量一致. 一般来说它们的名称与实际的成员变量对应, 但并不强制要求. 例如 int count()
与 void set_count(int count)
.
3.7. 命名空间命名
总述
命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.
顶级命名空间的名称应当是项目名或者是该命名空间中的代码所属的团队的名字. 命名空间中的代码, 应当存放于和命名空间的名字匹配的文件夹或其子文件夹中.
注意 不使用缩写作为名称 的规则同样适用于命名空间. 命名空间中的代码极少需要涉及命名空间的名称, 因此没有必要在命名空间中使用缩写.
要避免嵌套的命名空间与常见的顶级命名空间发生名称冲突. 由于名称查找规则的存在, 命名空间之间的冲突完全有可能导致编译失败. 尤其是, 不要创建嵌套的 std
命名空间. 建议使用更独特的项目标识符 (websearch::index
, websearch::index_util
) 而非常见的极易发生冲突的名称 (比如 websearch::util
).
对于 internal
命名空间, 要当心加入到同一 internal
命名空间的代码之间发生冲突 (由于内部维护人员通常来自同一团队, 因此常有可能导致冲突). 在这种情况下, 请使用文件名以使得内部名称独一无二 (例如对于 frobber.h
, 使用 websearch::index::frobber_internal
).
3.8. 枚举命名
总述
枚举的命名应当和 常量 或 宏 一致: kEnumName
或是 ENUM_NAME
.
说明
单独的枚举值应该优先采用 常量 的命名方式. 但 宏 方式的命名也可以接受. 枚举名 UrlTableErrors
(以及 AlternateUrlTableErrors
) 是类型, 所以要用大小写混合的方式.
enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};
2009 年 1 月之前, 我们一直建议采用 宏 的方式命名枚举值. 由于枚举值和宏之间的命名冲突, 直接导致了很多问题. 由此, 这里改为优先选择常量风格的命名方式. 新代码应该尽可能优先使用常量风格. 但是老代码没必要切换到常量风格, 除非宏风格确实会产生编译期问题.
3.9. 宏命名
总述
你并不打算 使用宏, 对吧? 如果你一定要用, 像这样命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN
.
说明
参考 预处理宏; 通常 不应该 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线:
#define ROUND(x) ...
#define PI_ROUNDED 3.0
3.10. 命名规则的特例
总述
如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略.
bigopen()
: 函数名, 参照 open()
的形式
uint`: `typedef
bigpos
: struct
或 class
, 参照 pos
的形式
sparse_hash_map
: STL 型实体; 参照 STL 命名约定
LONGLONG_MAX`: 常量, 如同 `INT_MAX