0%

C++编程规范

1. 命名约定

最重要的一致性规则是命名管理. 命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义: 类型, 变量, 函数, 常量, 宏, 等等, 甚至, 我们大脑中的模式匹配引擎非常依赖这些命名规则.

命名规则具有一定随意性, 但相比按个人喜好命名, 一致性更重要, 所以无论你认为它们是否重要, 规则总归是规则.

1.1. 通用命名规则

总述

函数命名, 变量命名, 文件命名要有描述性; 少用缩写.

说明

尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词.

1
2
3
4
5
6
7
8
9
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 表示模板参数.

模板参数的命名应当遵循对应的分类: 类型模板参数应当遵循类型命名的规则, 而非类型模板应当遵循变量命名的规则.

1.2. 文件命名

总述

文件名要全部小写, 可以包含下划线 (_) 或连字符 (-), 依照项目的约定. 如果没有约定, 那么 “_” 更好.

说明

可接受的文件命名示例:

  • my_useful_class.cc
  • my-useful-class.cc
  • myusefulclass.cc
  • myusefulclass_test.cc // _unittest_regtest 已弃用.

C++ 文件要以 .cpp 结尾, 头文件以 .h 结尾. 专门插入文本的文件则以 .inc 结尾.

不要使用已经存在于 /usr/include 下的文件名 (Yang.Y 注: 即编译器搜索系统头文件的路径), 如 db.h.

通常应尽量让文件名更加明确. http_server_logs.h 就比 logs.h 要好. 定义类时文件名一般成对出现, 如 foo_bar.hfoo_bar.cc, 对应于类 FooBar.

内联函数必须放在 .h 文件中. 如果内联函数比较短, 就直接放在 .h 中.

1.3. 类型命名

总述

类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.

说明

所有类型命名 —— 类, 结构体, 类型定义 (typedef), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线. 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;

// 枚举
enum UrlTableErrors { ...

1.4. 变量命名

总述

变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.

说明

普通变量命名

举例:

1
2
3
string table_name;  // 好 - 用下划线.

string tableName; // 差 - 混合大小写

类数据成员

不管是静态的还是非静态的, 类数据成员都可以和普通变量一样, 但要接下划线.

1
2
3
4
5
6
class TableInfo {
...
private:
string table_name_; // 好 - 后加下划线.
static Pool<TableInfo>* pool_; // 好.
};

结构体变量

不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类那样接下划线:

1
2
3
4
5
struct UrlTableProperties {
string name;
int num_entries;
static Pool<UrlTableProperties>* pool;
};

1.5. 常量命名

总述

声明为 constexprconst 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. 例如:

1
const int kDaysInAWeek = 7;

说明

所有具有静态存储类型的变量 (例如静态变量或全局变量, 参见 存储类型) 都应当以此方式命名. 对于其他存储类型的变量, 如自动变量等, 这条规则是可选的. 如果不采用这条规则, 就按照一般的变量命名规则.

1.6. 函数命名

总述

常规函数使用大小写混合, 首字母应小写, 如 myExcitingFunction(), myExcitingMethod(), setMyExcitingMemberVariable().

说明

一般来说, 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线. 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 startRpc() 而非 startRPC()).

1
2
3
addTableEntry()
deleteUrl()
openFileOrDie()

(同样的命名规则同时适用于类作用域与命名空间作用域的常量, 因为它们是作为 API 的一部分暴露对外的, 因此应当让它们看起来像是一个函数, 因为在这时, 它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节.)

1.7. 命名空间命名

总述

命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.

顶级命名空间的名称应当是项目名或者是该命名空间中的代码所属的团队的名字. 命名空间中的代码, 应当存放于和命名空间的名字匹配的文件夹或其子文件夹中.

注意不使用缩写作为名称的规则同样适用于命名空间. 命名空间中的代码极少需要涉及命名空间的名称, 因此没有必要在命名空间中使用缩写.

要避免嵌套的命名空间与常见的顶级命名空间发生名称冲突. 由于名称查找规则的存在, 命名空间之间的冲突完全有可能导致编译失败. 尤其是, 不要创建嵌套的 std 命名空间. 建议使用更独特的项目标识符 (websearch::index, websearch::index_util) 而非常见的极易发生冲突的名称 (比如 websearch::util).

对于 internal 命名空间, 要当心加入到同一 internal 命名空间的代码之间发生冲突 (由于内部维护人员通常来自同一团队, 因此常有可能导致冲突). 在这种情况下, 请使用文件名以使得内部名称独一无二 (例如对于 frobber.h, 使用 websearch::index::frobber_internal).

1.8. 枚举命名

总述

枚举的命名应当和 常量 一致: kEnumName 或是 ENUM_NAME.

说明

单独的枚举值应该优先采用 常量 的命名方式. 但 方式的命名也可以接受. 枚举名 UrlTableErrors (以及 AlternateUrlTableErrors) 是类型, 所以要用大小写混合的方式.

1
2
3
4
5
6
7
8
9
10
enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};

2009 年 1 月之前, 我们一直建议采用 的方式命名枚举值. 由于枚举值和宏之间的命名冲突, 直接导致了很多问题. 由此, 这里改为优先选择常量风格的命名方式. 新代码应该尽可能优先使用常量风格. 但是老代码没必要切换到常量风格, 除非宏风格确实会产生编译期问题.

1.9. 宏命名

总述

你并不打算 使用宏, 对吧? 如果你一定要用, 像这样命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN.

说明

参考 预处理宏; 通常 不应该 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线:

1
2
#define ROUND(x) ...
#define PI_ROUNDED 3.0

1.10. 命名规则的特例

总述

如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略.

bigopen(): 函数名, 参照 open() 的形式

1
uint`: `typedef

bigpos: structclass, 参照 pos 的形式

sparse_hash_map: STL 型实体; 参照 STL 命名约定

1
LONGLONG_MAX`: 常量, 如同 `INT_MAX

2. 格式

2.1. 函数声明与定义

总述

返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致.

说明

函数看上去像这样:

1
2
3
4
ReturnType ClassName::functionName(Type par_name1, Type par_name2) {
doSomething();
...
}

如果同一行文本太多, 放不下所有参数:

1
2
3
4
5
ReturnType ClassName::reallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
doSomething();
...
}

甚至连第一个参数都放不下:

1
2
3
4
5
6
7
ReturnType LongClassName::reallyReallyReallyLongFunctionName(
Type par_name1, // 4 space indent
Type par_name2,
Type par_name3) {
doSomething(); // 2 space indent
...
}

注意以下几点:

  • 使用好的参数名.
  • 只有在参数未被使用或者其用途非常明显时, 才能省略参数名.
  • 如果返回类型和函数名在一行放不下, 分行.
  • 如果返回类型与函数声明或定义分行了, 不要缩进.
  • 左圆括号总是和函数名在同一行.
  • 函数名和左圆括号间永远没有空格.
  • 圆括号与参数间没有空格.
  • 左大括号总在最后一个参数同一行的末尾处, 不另起新行.
  • 右大括号总是单独位于函数最后一行, 或者与左大括号同一行.
  • 右圆括号和左大括号间总是有一个空格.
  • 所有形参应尽可能对齐.
  • 缺省缩进为 2 个空格.
  • 换行后的参数保持 4 个空格的缩进.

未被使用的参数, 或者根据上下文很容易看出其用途的参数, 可以省略参数名:

1
2
3
4
5
6
7
class Foo {
public:
Foo(Foo&&);
Foo(const Foo&);
Foo& operator=(Foo&&);
Foo& operator=(const Foo&);
};

未被使用的参数如果其用途不明显的话, 在函数定义处将参数名注释起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Shape {
public:
virtual void rotate(double radians) = 0;
};

class Circle : public Shape {
public:
void rotate(double radians) override;
};

void Circle::rotate(double /*radians*/) {}
// 差 - 如果将来有人要实现, 很难猜出变量的作用.
void Circle::rotate(double) {}

属性, 和展开为属性的宏, 写在函数声明或定义的最前面, 即返回类型之前:

1
MUST_USE_RESULT bool IsOK();

2.2. 指针和引用表达式

总述

句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.

说明

下面是指针和引用表达式的正确使用范例:

1
2
3
4
x = *p;
p = &x;
x = r.y;
x = r->y;

注意:

  • 在访问成员时, 句点或箭头前后没有空格.
  • 指针操作符 *& 后没有空格.

在声明指针变量或参数时, 星号与类型或变量名紧挨都可以:

1
2
3
4
5
6
7
8
9
10
// 好, 空格前置.
char *c;
const string &str;

// 好, 空格后置.
char* c;
const string& str;
int x, *y; // 不允许 - 在多重声明中不能使用 & 或 *
char * c; // 差 - * 两边都有空格
const string & str; // 差 - & 两边都有空格.

在单个文件内要保持风格一致, 所以, 如果是修改现有文件, 要遵照该文件的风格.

在此工程中,我们约定使用引用型参数作为输出或输入输出参数,常引用作为输入参数,例如

1
2
3
4
OperStatus mmp::IterativeClosestPoint::align(
const PointCloudRGB &cloud_src,
const PointCloudRGB &cloud_tgt,)
PointCloudRGB &cloud_result);

2.3. 函数返回值

总述

不要在 return 表达式里加上非必须的圆括号.

说明

只有在写 x = expr 要加上括号的时候才在 return expr; 里使用括号.

1
2
3
4
5
6
return result;                  // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
another_condition);
return (value); // 毕竟您从来不会写 var = (value);
return(result); // return 可不是函数!

2.4. 预处理指令

总述

预处理指令不要缩进, 从行首开始.

说明

即使预处理指令位于缩进代码块中, 指令也应从行首开始.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 好 - 指令从行首开始
if (lopsided_score) {
#if DISASTER_PENDING // 正确 - 从行首开始
DropEverything();
# if NOTIFY // 非必要 - # 后跟空格
NotifyClient();
# endif
#endif
BackToNormal();
}
// 差 - 指令缩进
if (lopsided_score) {
#if DISASTER_PENDING // 差 - "#if" 应该放在行开头
DropEverything();
#endif // 差 - "#endif" 不要缩进
BackToNormal();
}

2.5. 命名空间格式化

总述

命名空间内容不缩进.

说明

命名空间不要增加额外的缩进层次, 例如:

1
2
3
4
5
6
7
namespace {

void foo() { // 正确. 命名空间内没有额外的缩进.
...
}

} // namespace

不要在命名空间内缩进:

1
2
3
4
5
6
7
8
namespace {

// 错, 缩进多余了.
void foo() {
...
}

} // namespace

声明嵌套命名空间时, 每个命名空间都独立成行.

1
2
3
4
5
namespace foo {
namespace bar {

} // namespace bar
} // namespace foo

3. 类

3.1. 接口

总述

接口是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制).

定义

当一个类满足以下要求时, 称之为纯接口:

  • 只有纯虚函数 (“=0”) 和静态函数 (除了下文提到的析构函数).
  • 没有非静态数据成员.
  • 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 protected.
  • 如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.

接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数 (作为上述第 1 条规则的特例, 析构函数不能是纯虚函数). 具体细节可参考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 节.

优点

Interface 为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员. 这一点对于 多重继承 尤其重要. 另外, 对于 Java 程序员来说, 接口的概念已是深入人心.

缺点

Interface 后缀增加了类名长度, 为阅读和理解带来不便. 同时, 接口属性作为实现细节不应暴露给用户.

结论

只有在满足上述条件时, 类才以 Interface 结尾, 但反过来, 满足上述需要的类未必一定以 Interface 结尾.

3.2. 继承

总述

使用组合 (YuleFox 注: 这一点也是 GoF 在 <> 里反复强调的) 常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.

定义

当子类继承基类时, 子类包含了父基类所有数据及操作的定义. C++ 实践中, 继承主要用于两种场合: 实现继承, 子类继承父类的实现代码; 接口继承, 子类仅继承父类的方法名称.

优点

实现继承通过原封不动的复用基类代码减少了代码量. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误. 从编程角度而言, 接口继承是用来强制类输出特定的 API. 在类没有实现 API 中某个必须的方法时, 编译器同样会发现并报告错误.

缺点

对于实现继承, 由于子类的实现代码散布在父类和子类间之间, 要理解其实现变得更加困难. 子类不能重写父类的非虚函数, 当然也就不能修改其实现. 基类也可能定义了一些数据成员, 因此还必须区分基类的实际布局.

结论

所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式.

不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 “是一个” (“is-a”, YuleFox 注: 其他 “has-a” 情况下请使用组合) 的情况下使用继承: 如果 Bar 的确 “是一种” Foo, Bar 才能继承 Foo.

必要的话, 析构函数声明为 virtual. 如果你的类有虚函数, 则析构函数也应该为虚函数.

对于可能被子类访问的成员函数, 不要过度使用 protected 关键字. 注意, 数据成员都必须是 私有的.

对于重载的虚函数或虚析构函数, 使用 override, 或 (较不常用的) final 关键字显式地进行标记. 较早 (早于 C++11) 的代码可能会使用 virtual 关键字作为不得已的选项. 因此, 在声明重载时, 请使用 override, finalvirtual 的其中之一进行标记. 标记为 overridefinal 的析构函数如果不是对基类虚函数的重载的话, 编译会报错, 这有助于捕获常见的错误. 这些标记起到了文档的作用, 因为如果省略这些关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数.

4. 头文件

4.1. #include 的路径及顺序

Tip

使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.

项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 . (当前目录) 或 .. (上级目录). 例如, google-awesome-project/src/base/logging.h 应该按如下方式包含:

1
#include "base/logging.h"

又如, dir/foo.ccdir/foo_test.cc 的主要作用是实现或测试 dir2/foo2.h 的功能, foo.cc 中包含头文件的次序如下:

  1. dir2/foo2.h (优先位置, 详情如下)
  2. C 系统文件
  3. C++ 系统文件
  4. 其他库的 .h 文件
  5. 本项目内 .h 文件

这种优先的顺序排序保证当 dir2/foo2.h 遗漏某些必要的库时, dir/foo.ccdir/foo_test.cc 的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。

dir/foo.ccdir2/foo2.h 通常位于同一目录下 (如 base/basictypes_unittest.ccbase/basictypes.h), 但也可以放在不同目录下.

按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。

您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,前置声明 (forward declarations) 情况除外。比如您要用到 bar.h 中的某个符号, 哪怕您所包含的 foo.h 已经包含了 bar.h, 也照样得包含 bar.h, 除非 foo.h 有明确说明它会自动向您提供 bar.h 中的 symbol. 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 foo.cc 只包含 foo.h 就够了,不用再管后者所包含的其它内容。

举例来说, google-awesome-project/src/foo/internal/fooserver.cc 的包含次序如下:

1
2
3
4
5
6
7
8
9
10
11
12
> #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"
>

例外:

有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:

1
2
3
4
5
6
7
8
> #include "foo/public/fooserver.h"
>
> #include "base/port.h" // For LANG_CXX11.
>
> #ifdef LANG_CXX11
> #include <initializer_list>
> #endif // LANG_CXX11
>

4.2 #define 保护

所有头文件都应该使用 #define 来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H .

为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:

1
2
3
4
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

在C++ 11标准中,建议使用以下的形式来避免头文件重复包含:

1
#pragma once

1
_Pragma("once")

在本项目中,提倡使用第三种形式的保护. 因为第一种使用ifndef 的保护形式,编译器需要在文件末尾找到 endif 才能确定开发者的意图,对于多次需要 include 的或较长的头文件,这项工作常常需要花费很多时间;而第二种形式本身是宏指令,在后续工作中如果需要使用新的宏指令执行类似的任务时,可能会不得不将宏嵌套在一起从而进入死路.