C++编码规范
- 1 格式
- 1.1 缩进4个空格
- 1.2 花括号位置
- 1.3
if
,for
,while
等语句就算只有一行, 也强制使用花括号 - 1.4 习惯使用空格
- 2.1 文件命名
- 2.2 类型名称
- 2.3 变量
- 2.4 函数
- 2.5 命令空间
- 2.6 宏
- 2.7 枚举命名
- 2.8 纯 C 风格的接口
- 2.9 例外
- 3.1 #define 保护
- 3.2 #include 的顺序
- 3.3 尽可能减少头文件的依赖
- 3.4
#include
中的头文件的路径问题 - 4.1 全局变量
- 4.4 命名空间
- 4.5 文件作用域
- 4.6 头文件不要出现
using namespace ....
- 5.1 声明顺序
- 5.2 继承
- 6.1 函数参数顺序
- 7.1 注释语言
- 7.2 注释风格
- 7.3 文件注释
- 7.3 类注释
- 7.4 函数注释
- 7.5 变量注释
- 7.6 实现注释
- 7.7 标点, 拼写和语法
- 7.8
TODO
注释 - 7.9 弃用注释
- 8.1
const
的使用 - 8.2 不要注释废除的代码, 代码废除就直接删掉
1 格式
1.1 缩进4个空格
1.2 花括号位置
namespace
, struct
, class
, enum
, union
, 函数
等采用Allman
风格, 每个花括号单独占一行
namespace aa_bb
{
struct TestStruct
{
};
class TestClass
{
};
} // end of namespace aa_bb
if
, else
, for
, while
, switch-case
等采用K&R
风格, 在圆括号之后(圆括号与花括号之间加一个空格). 例子
for (auto i = 0; i < 100; i++) {
printf("%d\n", i);
}
1.3 if
, for
, while
等语句就算只有一行, 也强制使用花括号
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) {
goto fail;
}
1.4 习惯使用空格
(,
;
)之后加一个空格
(=
==
<
<=
>
>=
!=
)两边加空格
圆括号与花括号之间加一个空格
2 命名
尽可能使用描述性的命名, 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词.
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; // <错> 删减了若干字母.
2.1 文件命名
文件名要全部小写, 可以包含下划线 .
2.2 类型名称
采用大写的驼峰命名法):包括类, 结构体, 类型定义 (typedef
), 枚举, 类型模板参数等.
2.3 变量
一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 结构体的成员不用.
2.4 函数
采用小写的骆驼命名法.
2.5 命令空间
使用小写加下划线的形式.
2.6 宏
全部大写, 中间加下划线相连接.
2.7 枚举命名
尽量使用 c++11
风格 enum
, 例如:
enum class ColorType : uint8_t
{
Black,
While,
Red,
}
枚举里面的数值, 全部采用大写的骆驼命名法. 使用的时候, 就为 ColorType::Black
.
有些时候, 需要使用c++11
之前的enum
风格, 这种情况下, 每个枚举值, 都需要带上类型信息, 用下划线分割. 比如:
enum HttpResult
{
HttpResult_OK = 0,
HttpResult_Error = 1,
HttpResult_Cancel = 2,
}
2.8 纯 C 风格的接口
假如我们需要结构里面的内存布局精确可控, 有可能需要编写一些纯C风格的结构和接口. 这个时候, 接口前面应该带有模块或者结构的名字, 中间用下划线分割. 比如
struct HSVColor
{
float h;
float s;
float v;
};
struct RGBColor
{
float r;
float g;
float b;
}
RGBColor color_hsvToRgb(HSVColor hsv);
HSBColor color_rgbToHsv(RGBColor rgb);
这里, color 就是模块的名字. 这里的模块, 充当 C++ 中命名空间的作用.
struct Path
{
....
}
Path *Path_new();
void Path_destrory(Path *path);
void Path_moveTo(Path *path, float x, float y);
void Path_lineTo(Path *path, float x, float y);
这里, 接口中Path出现的是类的名字.
2.9 例外
我们需要自己写的库跟C++的标准库结合时可以采用跟C++标准库相类似的风格.
如与Qt结合的代码可以采用跟Qt类似的风格.
3 代码文件
3.1 #define 保护
所有的头文件, 都应该使用#define来防止头文件被重复包含. 命名的格式为:
<模块>_<文件名>_H
3.2 #include 的顺序
C++代码使用#include
来引入其它的模块的头文件. 尽可能, 按照模块的稳定性顺序来排列#include
的顺序. 按照稳定性从高到低排列. 分别是C标准库, C++标准库, 第三方库, 公司内部的库, 工程内部定义的头文件.
但有一个例外, 就是.cpp
中, 对应的.h文件放在第一位. 比如geo
模块中的, Point.h
跟 Point.cpp
文件, Point.cpp
中的包含.
#include "geo/Point.h"
#include <cmath>
这里, 将 #include "geo/Point.h"
, 放到第一位, 之后按照上述原则来排列#include顺序. 理由下一条规范来描述.
3.3 尽可能减少头文件的依赖
使用前置声明, 而不是直接#include
.
3.4 #include
中的头文件的路径问题
引用的外部库或者要输出的接口头文件:
路径的起始点, 为工程文件代码文件的include
根目录, 类似<Project_Root>/include
.
#include "hardware_interface/robot_hw.h"
#include <libcstone/log.h>
内部用的头文件:
工程内部使用的头文件可以根据情况, 自行指定INCLUDE
目录
4 作用域
作用域, 表示某段代码或者数据的生效范围. 作用域越大, 修改代码时候影响区域也就越大, 原则上, 作用域越小越好.
4.1 全局变量
禁止使用全局变量. 全局变量在项目的任何地方都可以访问.
4.4 命名空间
C++中, 尽量不要出现全局函数, 应该放入某个命名空间当中. 命名空间将全局的作用域细分, 可有效防止全局作用域的名字冲突.
4.5 文件作用域
假如, 某个函数, 或者类型, 只在某个.cpp
中使用, 请将函数或者类放入匿名命名空间. 来防止文件中的函数导出. 比如:
// fileA.cpp
namespace
{
void doSomething()
{
....
}
}
4.6 头文件不要出现 using namespace ....
5 类
5.1 声明顺序
类的成员函数或者成员变量, 按照使用的重要程度, 从高到低来排列.
具体规范:
- 按照
public
,protected,
private
的顺序分块. 那一块没有, 就直接忽略.
每一块中, 按照下面顺序排列:
typedef
,enum
,struct
,class
定义的嵌套类型;- 常量;
- 构造函数;
- 析构函数;
- 成员函数,含静态成员函数;
- 数据成员,含静态数据成员;
.cpp
文件中, 函数的实现尽可能跟声明次序一致.
5.2 继承
优先使用组合, 而不是继承.
继承主要用于两种场合: 实现继承, 子类继承了父类的实现代码. 接口继承, 子类仅仅继承父类的方法名称.
我们不提倡实现继承, 实现继承的代码分散在子类跟父亲当中, 理解起来变得很困难. 通常实现继承都可以采用组合来替代.
规则:
- 继承应该都是
public
; - 假如父类有虚函数, 父类的析构函数为
virtual
; - 假如子类覆写了父类的虚函数, 应该显式写上
override
;
6 函数
6.1 函数参数顺序
参数顺序, 按照传入参数, 传出参数, 的顺序排列. 不要使用可传入可传出的参数.
bool loadFile(const std::string &filePath, ErrorCode *code); // 对
bool loadFile(ErrorCode *code, const std::string &filePath); // 错
保持统一的顺序, 使得他人容易记忆.
7 注释
最好的代码应当本身就是文档. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字.
7.1 注释语言
注释可以用中文或者英文, 前提是必须让读者看得懂。例如算法中有很多专有名词和概念来自于英文论文, 那相关注释我们可以选用英文.又如软件逻辑中, 某些逻辑我们可以用中文描述.
7.2 注释风格
以 //
为主, /* */
为辅.
7.3 文件注释
在每一个文件开头加入版权公告.
文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释.
如果一个 .h
文件声明了多个概念, 则文件注释应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系. 一个一到两行的文件注释就足够了, 对于每个概念的详细文档应当放在各个概念中, 而不是文件注释中.
不要在 .h
和 .cpp
之间复制注释, 这样的注释偏离了注释的实际意义.
// Copyright 2020 Aubo Robotics
// License: All rights reserved by Aubo Robotics.
// Author: Lou Wei(louwei@aubo-robotics.cn)
// Maintainer:
//
// This file is ...
7.3 类注释
每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显.
// 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
和 .cpp
文件中), 此时, 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.
7.4 函数注释
函数声明
基本上每个函数声明处前都应当加上注释, 描述函数的功能和用途. 只有在函数的功能简单而明显时才能省略这些注释(例如, 简单的取值和设值函数). 注释使用叙述式 (“Opens the file”) 而非指令式 (“Open the file”); 注释只是为了描述函数, 而不是命令函数做什么. 通常, 注释不会描述函数如何工作. 那是函数定义部分的事情.
函数声明处注释的内容:
- 函数的输入输出;
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数;
- 函数是否分配了必须由调用者释放的空间;
- 参数是否可以为空指针;
- 是否存在函数使用上的性能隐患;
- 如果函数是可重入的, 其同步前提是什么?
举例如下:
// Returns an iterator for this table. It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginning of the table.
//
// This method is equivalent to:
// Iterator* iter = table->NewIterator();
// iter->Seek("");
// return iter;
// If you are going to immediately seek to another place in the
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator *GetIterator() const;
但也要避免罗罗嗦嗦, 或者对显而易见的内容进行说明. 下面的注释就没有必要加上 “否则返回 false”, 因为已经暗含其中了:
// Returns true if the table cannot hold any more entries.
bool IsTableFull();
注释函数重载时, 注释的重点应该是函数中被重载的部分, 而不是简单的重复被重载的函数的注释. 多数情况下, 函数重载不需要额外的文档, 因此也没有必要加上注释.
注释构造/析构函数时, 切记读代码的人知道构造/析构函数的功能, 所以 “销毁这一对象” 这样的注释是没有意义的. 你应当注明的是注明构造函数对参数做了什么 (例如, 是否取得指针所有权) 以及析构函数清理了什么. 如果都是些无关紧要的内容, 直接省掉注释. 析构函数前没有注释是很正常的.
函数定义
如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释. 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由. 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要.
不要 从 .h
文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上.
7.5 变量注释
类数据成员
每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果有非变量的参数(例如特殊值, 数据成员之间的关系, 生命周期等)不能够用类型与变量名明确表达, 则应当加上注释. 然而, 如果变量类型与变量名已经足以描述一个变量, 那么就不再需要加上注释.
特别地, 如果变量可以接受 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_;
7.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.
}
注意, 这里用了两段注释分别描述这段代码的作用, 和提示函数返回时错误已经被记入日志.
如果你需要连续进行多行注释, 可以使之对齐获得更好的可读性:
DoSomething(); // Comment here so the comments line up.
DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment.
{ // One space before comment when opening a new scope is allowed,
// thus the comment lines up with the following comments and code.
DoSomethingElse(); // Two spaces before line comments normally.
}
std::vector<string> list {
// Comments in braced lists describe the next element...
"First item",
// .. and should be aligned appropriately.
"Second item" };
DoSomething(); // For trailing block comments, one space is fine.
函数参数注释
如果函数参数的意义不明显, 考虑用下面的方式进行弥补:
- 如果参数是一个字面常量, 并且这一常量在多处函数调用中被使用, 用以推断它们一致, 你应当用一个常量名让这一约定变得更明显, 并且保证这一约定不会被打破.
- 考虑更改函数的签名, 让某个
bool
类型的参数变为enum
类型, 这样可以让这个参数的值表达其意义. - 如果某个函数有多个配置选项, 你可以考虑定义一个类或结构体以保存所有的选项, 并传入类或结构体的实例. 这样的方法有许多优点, 例如这样的选项可以在调用处用变量名引用, 这样就能清晰地表明其意义. 同时也减少了函数参数的数量, 使得函数调用更易读也易写. 除此之外, 以这样的方式, 如果你使用其他的选项, 就无需对调用点进行更改.
- 用具名变量代替大段而复杂的嵌套表达式.
- 万不得已时, 才考虑在调用点用注释阐明参数的意义.
比如:
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
CalculateProduct(values, options, /*completion_callback=*/nullptr);
7.7 标点, 拼写和语法
注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句. 大多数情况下, 完整的句子比句子片段可读性更高. 短一点的注释, 比如代码行尾注释, 可以随意点, 但依然要注意风格的一致性.
7.8 TODO
注释
对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO
注释.
TODO
注释要使用全大写的字符串 TODO
, 在随后的圆括号里写上你的名字, 邮件地址, bug ID, 或其它身份标识和与这一 TODO
相关的 issue. 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 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
如果加 TODO
是为了在 “将来某一天做某事”, 可以附上一个非常明确的时间 “Fix by November 2005”), 或者一个明确的事项 (“Remove this code when all clients can handle XML responses.”).
7.9 弃用注释
通过弃用注释(DEPRECATED
comments)以标记某接口点已弃用.
您可以写上包含全大写的 DEPRECATED
的注释, 以标记某接口为弃用状态. 注释可以放在接口声明前, 或者同一行.
在 DEPRECATED
一词后, 在括号中留下您的名字, 邮箱地址以及其他身份标识.
8 其它
8.1 const
的使用
尽可能的多使用const
.
C++中, const
是个很重要的关键字, 应用了const
之后, 就不可以随便改变变量的数值了, 不小心改变了编译器会报错, 就容易找到错误的地方. 比如:
- 想求圆的周长, 需要用到
Pi
,Pi
不会变的, 加const
,const double Pi = 3.1415926;
- 需要在函数中传引用, 只读, 不会变的, 前面加
const
; - 函数有个返回值, 返回值是个引用, 只读, 不会变的, 前面加
const
; - 类中有个private数据, 外界要以函数方式读取, 不会变的, 加
const
, 这个时候const
就是加在函数定义末尾.
const
的位置:
const int *name; // 对(这样写, 可读性更好)
int const *name; // 错