C++注释风格建议

有个笑话,一位从不写注释的程序员在编写一段复杂的代码时,骄傲地认为这段代码只有自己和上帝知道它是干嘛的,等过了一段时间再回顾时,发现没有注释,感叹到这段代码现在只有上帝知道它是干嘛的。可见,注释是项目中不可或缺的部分,不仅是为了帮助团队其它开发人员快速理解代码,也是帮助自己快速恢复对代码功能的了解。所以,我们对注释不要过于吝啬,该书写时就书写。

虽然注释书写起来有点麻烦,但是为了提供程序本身额外的信息,如函数功能描述、类动能描述、算法描述、程序开发作者、开发时间、开发背景、第三方资料来源路径等,提高代码可读性和可维护性,我们为注释付出的麻烦是值得的。当然也要记住,注释固然重要,但最好的代码应当本身就是文档,要达到“代码即注释”的效果,有意义的类型名和变量名,要远胜于使用注释来解释含糊不清的名字。

1.注释风格(Comment Style)

C++注释有两种形式,使用C风格/**/或C++风格//都可以,项目中统一就好。但是毕竟是C++项目,建议还是使用C++风格的//。当然,//在对代码块注释时,不太方便友好,可以适当采用/**/来快速便捷地注释代码块。

2.文件注释(File Comments)

在每一个文件开头依次加入以下内容:
(1)版权公告(Copyright Statement),如Copyright 2018 Google Inc;
(2)许可版本(License Boilerplate),如果是开源项目加上合适的许可证版本,如Apache 2.0、BSD、LGPL、GPL;
(3)创建信息(Creation Info),标识文件创建作者与日期;
(4)文件内容(File Content),简要描述文件功能和内容。

如果想记录版本变更信息,可根据需要加入版本与最近修改信息。一个合适的文件注释示例如下:

//
//@Copyright:Copyright 2018 Google Inc
//@License:GPL
//@Birth:created by Dablelv on 2018-08-02
//@Content:开源JSON解析库
//@Version:1.1.1
//@Revision:last revised by lvlv on 2018-09-02
//

3.类注释(Class Comments)

每个类的定义都要附带一份注释,描述类的功能和用法,除非它的功能相当明显。类注释应当为读者理解如何使用与何时使用类提供足够的信息,同时应当提醒读者在正确使用此类时应当考虑的因素。如果该类的实例可被多线程访问,要特别注意文档说明多线程环境下相关的规则和常量使用。如果你想用一小段代码演示这个类的基本用法或通常用法,放在类注释里也非常合适。如果类的声明和定义分开了(例如分别放在了 .h 和 .cc 文件中),此时,描述类用法的注释应当和接口定义放在一起(.h文件中),描述类的操作和实现的注释应当和实现放在一起(.cc文件中)。

一个简单的注释示例如下:

// 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
{
    ...
};

4.函数注释(Function Comments)

函数声明处的注释描述函数功能,定义处的注释描述函数实现。

4.1函数声明

基本上每个函数声明处前都应当加上注释,描述函数的功能和用途。只有在函数的功能简单而明显时才能省略这些注,例如,简单的取值和设值函数。注释使用叙述式 (“Opens the file”) 而非指令式 (“Open the file”),注释只是为了描述函数,而不是命令函数做什么。通常,函数声明的注释不会描述函数如何工作,那是函数定义部分的事情。

函数声明处注释的一般内容:
(1)函数功能简介;
(2)函数参数说明;
(3)函数返回值说明;
(4)函数创建信息,包括创建背景、时间和作者等信息。

举例如下:

//
// @brief:获取容器迭代器
// @params:void
// @ret:返回容器迭代器
// @birth:created by Dablelv on 20180802
//
Iterator* getIterator() const;

但也要避免啰啰嗦嗦和对显而易见的内容进行说明,比如下面的注释就没有必要加上 “否则返回 false”,因为已经暗含其中了:

//@ret:returns true if the table cannot hold any more entries.
bool IsTableFull();

注释函数重载时,注释的重点应该是函数中被重载的部分,而不是简单的重复被重载的函数的注释。多数情况下,函数重载不需要额外的文档,因此也没有必要加上注释。

注释构造/析构函数时,切记读代码的人知道构造/析构函数的功能,所以 “销毁这一对象” 这样的注释是没有意义的,你应当注明的是构造函数对参数做了什么以及析构函数清理了什么。如果都是些无关紧要的内容,无需注释。析构函数前没有注释是很正常的。

4.2函数定义

如果函数的实现过程中用到了很巧妙的方式,那么在函数定义处应当加上解释性的注释。例如,你所使用的编程技巧,实现的大致步骤,或解释如此实现的理由。举例如下:

// 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;
}

比较隐晦的地方要在行尾加入注释,在行尾空两格或一个Tab进行注释。比如:

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();  // Comment here so after two spaces
...

注意,不要 从 .h 文件或其他地方的函数声明处直接复制注释简要重述函数功能是可以的,但注释重点要放在如何实现上。

4.3函数调用

函数调用时,如果函数实参意义不明显,考虑用下面的方式进行弥补:
(1)如果参数是一个字面常量,并且这一常量在多处函数调用中被使用,你应当用一个统一的常量名来标识该常量;
(2)考虑更改函数的签名,让某个 bool 类型的参数变为 enum 类型,这样可以用参数名称表达其意义;
(3)如果某个函数有多个参数,你可以考虑定义一个类或结构体以保存所有参数,并传入类或结构体的实例。这样的方法有许多优点,一减少了函数参数数量,使得函数调用易读易写;二使用引用或指针传入构造类型对象,减少对象拷贝,提高运行效率;三如果新增参数,函数原型无需改变,之需要将新增的参数加入类或结构体中;
(4)传参时,用具名变量代替大段而复杂的嵌套表达式。比如:

void print(const string& accountCompanyName)
{
    cout<<accountCompanyName<<endl;
}

//版本1
print(((stUserID.dwType==stUserID_t::USER_TYPE_WX||stUserID.dwType==stUserID_t::USER_TYPE_QQ)?true:false)?"Tencent":"Others");

//版本2
//帐号所属公司名称
string accountCompanyName=((stUserID.dwType==stUserID_t::USER_TYPE_WX||stUserID.dwType==stUserID_t::USER_TYPE_QQ)?true:false)?"Tencent":"Others";
print(accountCompanyName);

很显然,版本2使用具名变量清晰地表达了实参的含义,而不是直接使用又臭又长的表达式作为实参传递给函数。

(5)万不得已时,才考虑在调用点用注释阐明参数的意义。比如下面的示例:

//版本1
// What are these arguments?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);

//版本2
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product = CalculateProduct(values, options, /*completion_callback=*/nullptr);

很显然,版本2对函数调用传入的实参解释得更加清晰明了。

5.变量注释(Variable Comments)

通常变量名本身足以很好说明变量用途,某些情况下,也需要额外的注释说明。

5.1类数据成员

每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途。如果有非变量的参数(例如特殊值, 数据成员之间的关系,生命周期等)不能够用类型与变量名明确表达,则应当加上注释。然而,如果变量类型与变量名已经足以描述一个变量,那么就不再需要加上注释。

特别地,如果变量可以接受 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_;

5.2全局变量

和数据成员一样,所有全局变量也要注释说明含义及用途,以及作为全局变量的原因。比如:

//The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;

6.标点、拼写和语法(Punctuation, Spelling and Grammar)

注释时,注意标点、拼写和语法。写的好的注释比差的要易读的多。注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句。大多数情况下,完整的句子比句子片段可读性更高。短一点的注释,比如代码行尾注释,可以随意点,但依然要注意风格的一致性。清晰易读的代码还是很重要的,正确的标点, 拼写和语法对此会有很大帮助。

7.TODO注释(TODO Comments)

如果项目中存在功能代码有待修改和编写地方,建议使用TODO注释进行简略说明。TODO注释的作用类似于书签,便于开发者快速找到需要继续开发的位置和有待完成的功能,起到提醒标记的作用。

TODO注释要使用全大写的字符串 TODO,在随后的圆括号里写上负责人的名字、邮件地址、bug ID或其它身份标识和与这一 TODO相关的问题,主要目的是让添加注释的人或负责人可根据规范的 TODO 格式进行查找。添加 TODO 注释一般都是写上自己的名字,但并不意味着一定要自己来修正,除非自己是该TODO所描述的问题的负责人。

简单示例:

// TODO([email protected]): 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.”)。

8.弃用注释(DEPRECATED Comments)

可以写上包含全大写的 DEPRECATED 的注释,以标记某接口为弃用状态。

弃用注释应当包涵简短而清晰的指引,以帮助其他人修复其调用点。在 C++ 中,你可以将一个弃用函数改造成一个内联函数,这一函数将调用新的接口。在 DEPRECATED 一词后,在括号中留下负责人的名字、邮箱地址以及其他身份标识。注释可以放在接口声明前,或者同一行。简单示例如下:

//DEPRECATED(dablelv):new interface changed to bool IsTableFull(const Table& t)
bool IsTableFull();

9.注释注意事项

(1)多行注释不要嵌套
在使用C风格注释符/**/进行块注释时,不能在注释内部再次使用/**/,否则会引发编译错误。示例如下:

/*
    This is a false demo
    /*
        This is nested code
    */
    This becomes a normal statement
*/

上面的注释中,第一个/*会跟第一个*/匹配,导致“This becomes a normal statement”被编译器当做一个正常的语句来编译,从而导致编译错误。

(2)程序中不能没有注释
良好的编程习惯和规范能够帮助编码者尽可能的做到“代码即注释”,但往往由于项目的庞大和程序功能的高复杂性,代码结构和功能会变得异常的复杂,为了便于程序员之间的交流合作,提高程序的可读性和可维护性,代码注释必不可少。

(3)切勿过度使用注释
注释在项目开发中虽然必不可能,但过犹不及,注释并不是越多越好,切勿给显而易见的代码功能添加注释,画蛇添足,添加不需要的注释。比如下面的注释就没有必要。

// Find the element in the vector.
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) 
{
    Process(element);
}

多余的注释没有存在的价值,不可过度使用注释还有一个更重要的原因,注释同样需要维护,如果程序员在变更代码功能后,忽略了对注释的及时维护,那么会严重降低代码的可读性。比如

a=b;    //assign b to a

//代码变更后未变更注释
c=b;    //assign b to a 

上面多余的注释在变更代码后未及时的修改,会给阅读者造成困扰。程序员的第一反应并不会怀疑注释是错误的,而是会分析注释的“真正意图”。或许c是a的一个引用,于是c=b;就完成了b对a的赋值,然而事实并非如此。

因此,注释存在的意义是给代码阅读者提供有价值的信息,而不是盲目添加变成代码的累赘。实际上,开发者应该尽可能地让代码如果本身具有可读性,能够自成文档,达到代码即注释的效果,而让注释成为“锦上添花”的作用。如果程序本身设计存在问题,且不遵守编码规范,那么指望注释来提高程序的可读性只能是天方夜谭。

10.小结

注释是较为人性化约定,每一个程序员都应该养成注释的习惯。
(1)关于注释风格,很多 C++ 的coders 更喜欢行注释,C coders或许对块注释依然情有独钟,或者在文件头大段大段的注释时使用块注释;
(2)注释要言简意赅,不要拖沓冗余,不必要的注释我们是拒绝的;
(3)对于Chinese coders来说,用英文注释还是用中文注释是一个问题,但不管怎样,注释是为了让别人看懂,而不是炫耀母语或外语的水平;
(4)注释不要太乱,适当的缩进才会让人乐意看,但也没有必要规定注释从第几列开始 ;
(5)TODO很不错,有时候,注释确实是为了标记一些未完成的或完成的不尽如人意的地方,这样一搜索,就知道还有哪些活要干,日志都省了。

注释时建议留下大名,不仅可以彰显个人成就,也是在出现问题时,快速找到对应的负责人,做一个信任自己代码和富有责任感的coder吧。


参考文献

[1]Google C++编程风格指南
[2]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008:280-282[11.6正确使用注释]

猜你喜欢

转载自blog.csdn.net/K346K346/article/details/81354538