7 注释
注释对保证代码可读性至关重要。下面的规则描述了如何注释以及在哪儿注释。当然也要记住:注释固然很重要,但最好的代码应当本身就是文档。有意义的类型名和变量名,要远胜过要用注释解释的含糊不清的名字。
7.1 注释风格
规则:使用//或/* */,注释及注释风格应保持统一。
7.2 文件注释
规则:每一个文件开头应加入版权公告。
规则:文件注释描述了该文件的内容。如果一个文件只声明、实现或测试了一个对象,并且这个对象已经在它的声明处进行了详细的注释,就没必要再加上文件注释。除此之外的其他文件都需要文件注释。
7.2.1 法律公告和作者信息
规则:每个文件都应该包含许可证引用,请为项目选择合适的许可证版本 (比如:Apache 2.0,BSD,LGPL,GPL)。
规则:如果对原始作者的文件做了重大修改,请考虑删除原作者信息。
7.2.2 文件内容
规则:如果一个.h 文件声明了多个概念,则文件注释应当对文件的内容做一个大致的说明,同时说明各概念之间的联系。一到两行的文件注释就足够了,对于每个概念的详细文档应当放在各个概念中,而不是文件注释中。
规则:不要在 .h 和 .cc 之间复制注释,这样的注释偏离了注释的实际意义。
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 和 .cc 文件中),此时,描述类用法的注释应当和接口定义放在一起,描述类的操作和实现的注释应当和实现放在一起。
7.4 函数注释
函数声明处的注释描述函数功能,函数定义处的注释描述函数实现。
7.4.1 函数声明
规则:每个函数声明的前面都应加上注释来描述函数的功能和用途。函数的功能简单且明显时可省略注释。注释应使用叙述式而非指令式,因为它只是为了描述函数,而不是命令函数做什么。通常,注释不会描述函数如何工作,那是函数定义部分的事情。
函数声明处注释的内容:
- 函数的输入输出。
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数,是否会释放这些参数。
- 函数是否分配了必须由调用者释放的内存。
- 参数是否可以为空指针。
- 是否存在函数使用上的性能隐患。
- 如果函数是可重入的,其同步前提是什么?
举例如下:
//返回表的迭代器,完成后,调用者需要删除这个迭代器。
//一旦 GargantuanTable 对象删掉后,对其创建的迭代器就不能使用。
//
//迭代器应该在表的开始位置初始化
//
//这个方法等价于:
//Iterator* iter = table->NewIterator();
//iter->Seek("");
//return iter;
//如果准备立即在返回的迭代器中搜素其他位置,
//使用 NewIterator()更快,还能避免额外搜索
Iterator* GetIterator() const;
但也要避免罗罗嗦嗦,或者对显而易见的内容进行说明。下面的注释就没有必要加上“否则返回 false”,因为已经暗含其中了:
//如果表不能容纳更多记录就返回真
bool IsTableFull();
注释构造/析构函数时,切记读代码的人知道构造/析构函数的功能,所以 “销毁这一对象” 这样的注释是没有意义的。应当注明的是构造函数对参数做了什么 (例如,是否取得指针所有权) 以及析构函数清理了什么。如果都是些无关紧要的内容,直接省掉注释。析构函数前没有注释是很正常的。
7.4.2 函数定义
规则:如果函数的实现过程中用到了某些很巧妙的方式,则应在函数定义处应当加上解释性的注释。例如,所使用的编程技巧,实现的大致步骤,或解释如此实现的理由。举个例子,可以说明为什么函数的前半部分要加锁而后半部分不需要。
规则:不要从.h 文件或其它地方的函数声明处直接复制注释。简要重述函数功能是可以的,但注释重点要放在如何实现上。
7.5 变量注释
通常变量名应足以很好说明变量用途。某些情况下,也需要额外的注释说明。
7.5.1 类数据成员
规则:每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途。如果非变量的参数 (例如:特殊值、数据成员之间的关系或生命周期等) 不能够用类型与名称明确推理出其用途,则应当加上注释。然而,如果变量类型与变量名已经足以描述一个变量( int num_events_; ),那么就不再需要加上注释。
特别地,如果变量可以接受 nullptr 或-1 等警戒值,须加以说明。比如:
private:
//用于边界检查表访问,-1 表示还不知道这个表有多少条记录
int num_total_entries_;
7.5.2 全局变量
规则:全局变量要注释说明含义及用途,以及作为全局变量的原因。比如:
//在回归测试里面要运行的全部测试案例数目
const int kNumTestCases = 6;
7.6 实现注释
规则:代码中巧妙的、晦涩的、有趣的或重要的地方都应加以注释。
7.6.1 代码前注释
规则:巧妙或复杂的代码段前要加注释。比如:
//结果除以 2,考虑到加法运算对 x 的叠加
for (int i = 0; i < result->size(); i++) {
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
}
7.6.2 行注释
规则:比较隐晦的地方要在行尾加入注释,注释时应在行尾空两格。比如:
//如果内存足够,数据部分也可以内存映射
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes,mlock))
return; //错误已经记录
注意, 这里用了两段注释分别描述这段代码的作用,提示函数返回时错误已经被记入日志。
如果需要连续进行多行注释,可以使之对齐获得更好的可读性:
DoSomething();
//此处注释保持对齐
DoSomethingElseThatIsLonger(); //代码和注释之间两个空格
{ //新起一行时,注释前要有空格
//并且要和接下来的注释和代码对齐
DoSomethingElse(); //通常在每行注释前两个空格
}
std::vector<string> list{
//列表注释描述下一个元素...
"First item",
// ..并且应该适当对齐
"Second item"};
DoSomething(); /* 对块末尾的注释,留一个空格就好 */
7.6.3 函数参数注释
规则:如果函数参数的意义不明显,考虑用下面的方式进行弥补:
- 如果参数是一个字面常量,并且这一常量在多处函数调用中被使用,用以推断它们一致,应当用一个常量名让这一约定变得更明显,并且保证这一约定不会被打破。
- 考虑更改函数的签名,让某个 bool 类型的参数变为 enum 类型,这样可以让这个参数的值能够表达其意义。
- 如果某个函数有多个配置选项,可以考虑定义一个类或结构体以保存所有的选项,并传入类或结构体的实例。这样的方法有许多优点,例如这样的选项可以在调用处用变量名引用,这样就能清晰地表明其意义。同时也减少了函数参数的数量,使得函数调用更易读也易写。除此之外,以这样的方式,如果使用其他的选项,就无需对调用点进行更改。
- 用命名变量代替大段而复杂的嵌套表达式。
- 万不得已时,才考虑在调用点用注释阐明参数的意义。
比如下面的示例的对比:
//这些参数是什么含义?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);
和
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
CalculateProduct(values, options,
/*completion_callback=*/nullptr);
哪个更清晰一目了然。
7.6.4 不允许的行为
规则:不要描述显而易见的现象,永远不要用自然语言翻译作为代码注释。
规则:注释应当解释代码为什么要这么做和代码的目的,或者最好是让代码自文档化。
比较这样的注释:
// 寻找向量元素 <-- 差: 这太明显了!
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}
和这样的注释:
//处理"element"除非它已经被处理了
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}
自文档化的代码根本就不需要注释。上面例子中的注释对下面的代码来说就是毫无必要的:
if (!IsAlreadyProcessed(element)) {
Process(element);
}
7.7 标点, 拼写和语法
规则:注意标点,拼写和语法,写得好的注释比差的要易读得多。
注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句。大多数情况下,完整的句子比句子片段可读性更高。短一点的注释,比如代码行尾注释,可以随意点,但依然要注意风格的一致性。
7.8 TODO 注释
规则:对那些临时的,短期的解决方案,或已经够好但仍不完美的代码使用 TODO 注释。
规则:TODO 注释要使用全大写的字符串 TODO,在随后的圆括号里写上作者名字,邮件地址,bug ID,或其它身份标识和与这一 TODO 相关的 issue。主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 TODO 格式进行查找。
// TODO(kl@gmail。com):使用"*"连接运算符
// TODO(Zeke) change this to use relations。改为使用关系
// TODO(bug 12345):删除“最后访问者”特性
如果加 TODO 是为了在 “将来某一天做某事”,可以附上一个非常明确的时间 “Fix byNovember 2005”),或者一个明确的事项 (“Remove this code when all clients can handle XMLresponses.”)。
7.9 弃用注释
规则:通过弃用注释(DEPRECATED comments)以标记某接口点已弃用。
规则:可以写上包含全大写的 DEPRECATED 的注释,以标记某接口为弃用状态。注释可以放在接口声明前,或者同一行。
在 DEPRECATED 一词后,在括号中留下名字,邮箱地址以及其他身份标识。
弃用注释应当包涵简短而清晰的指引,以帮助其他人修复其调用点。在 C++ 中,可以将一个弃用函数改造成一个内联函数,这一函数将调用新的接口。