3. 注释规范

注释这件事,说实话,很多团队都不重视。我见过太多项目,代码跑得飞起,但三个月后连原作者自己都看不懂写了什么。嗯,我自己也干过这种事。

今天我们就聊聊注释规范。不是那种「写注释就是好习惯」的空话,而是实实在在的落地规则。

3.1 文件头注释:给每个文件一个「身份证」

每个源文件、头文件,都应该有一个文件头注释。它就像文件的身份证,告诉别人这个文件是干什么的、谁写的、什么时候改过。

我个人习惯用这样的格式:

/**
 * @file    network_manager.cpp
 * @brief   HTTP网络请求管理器,封装libcurl实现
 * @author  张三 (zhangsan@example.com)
 * @date    2024-01-15
 * @version 1.2.0
 *
 * 本模块负责所有HTTP/HTTPS请求的发送与响应处理。
 * 支持GET/POST/PUT/DELETE四种方法,自动管理连接池。
 *
 * @note    依赖libcurl >= 7.68.0
 * @warning 非线程安全,外部需加锁调用
 */

这里有几个要点:

  • @file:文件名,不要省略路径
  • @brief:一句话说清楚功能
  • @author:写真实姓名+邮箱,别写昵称
  • @date:创建日期,格式统一用YYYY-MM-DD
  • @version:语义化版本号
  • @note / @warning:重要提示,比如依赖、线程安全等
注意:文件头注释不是摆设。每次修改文件时,记得更新版本号和日期。我见过一个项目,文件头写的「2020年创建」,实际代码已经改得面目全非了。

3.2 函数注释:接口的「使用说明书」

函数注释是团队协作中最容易出问题的地方。写少了别人看不懂,写多了又没人看。

我的原则是:公开接口必须写,内部函数看情况。

来看一个例子:

/**
 * @brief   发送HTTP POST请求
 * @param   url         目标URL,必须完整包含协议头
 * @param   body        请求体数据,JSON格式字符串
 * @param   timeout_ms  超时时间,单位毫秒,0表示使用默认值
 * @param   headers     自定义HTTP头,可为空
 * @return  HttpResponse 包含状态码、响应体和耗时
 * @retval  status_code == 200 表示成功
 * @retval  status_code == 0   表示网络错误
 *
 * @code
 *   auto resp = HttpPost("https://api.example.com/data",
 *                        R"({"key":"value"})",
 *                        5000);
 *   if (resp.status_code == 200) {
 *       ProcessData(resp.body);
 *   }
 * @endcode
 *
 * @exception std::runtime_error 当URL格式非法时抛出
 * @note     本函数会阻塞调用线程,建议在子线程中使用
 */

关键点:

  • @param:参数名+说明,必要时写取值范围
  • @return:返回值含义,别只写「返回结果」这种废话
  • @retval:特定返回值的含义,方便调用方判断
  • @exception:可能抛出的异常,让调用方知道要catch什么
  • @code:给一个简短的使用示例,比写十行说明都管用
小技巧:函数注释里加一个@code示例,能减少80%的沟通成本。我在团队里推行这个习惯后,新人上手速度快了一倍。

3.3 类注释:设计意图的「说明书」

类注释比函数注释更重要。因为类往往承载了设计模式、生命周期、线程模型等复杂信息。

/**
 * @class   ConnectionPool
 * @brief   数据库连接池,支持自动扩容和回收
 *
 * 采用懒加载策略,首次调用GetConnection()时创建初始连接。
 * 最大连接数由 max_connections 控制,超过时请求会阻塞等待。
 * 连接空闲超过 idle_timeout 秒后自动关闭。
 *
 * 线程安全:内部使用std::mutex保护所有公共接口。
 *
 * 使用示例:
 * @code
 *   ConnectionPool pool("localhost", 3306, "root", "pass", 10);
 *   auto conn = pool.GetConnection();
 *   conn->Execute("SELECT * FROM users");
 *   pool.ReleaseConnection(std::move(conn));
 * @endcode
 *
 * @see     DatabaseConnector 底层连接实现
 * @since   1.0.0
 */

类注释应该包含:

  • 设计意图:为什么这么设计?解决了什么问题?
  • 生命周期:对象怎么创建、怎么销毁?
  • 线程模型:是否线程安全?哪些接口可以并发调用?
  • 使用示例:一个完整的典型用法
  • @see:关联的其他类或模块
避坑指南:我曾经接手过一个项目,类注释写得天花乱坠,但实际代码跟注释完全对不上。后来花了整整两周才理清真实逻辑。所以记住:注释必须和代码同步更新,否则不如不写。

3.4 代码行注释:少即是多

行注释是最容易被滥用的。我见过有人给每一行都加注释,结果代码反而更难读了。

我的原则是:解释「为什么」,而不是「是什么」。

// 错误示范:解释「是什么」
int count = 0;  // 定义一个整数变量count,初始值为0

// 正确示范:解释「为什么」
int retry_count = 0;  // 最多重试3次,因为下游服务偶尔会超时

什么时候该写行注释?

  • 复杂算法:解释关键步骤的意图
  • 特殊处理:为什么这里要加一个看似多余的判断?
  • 性能优化:为什么用这种写法而不是更直观的方式?
  • 已知缺陷:这里有个bug但暂时不改,说明原因
// 这里用位运算代替除法,因为性能测试显示快了30%
int result = (value >> 1) + (value >> 3);

// 注意:这个分支理论上不会执行,但保留作为防御性编程
if (ptr == nullptr) {
    LogWarning("Unexpected null pointer in process_data");
    return;
}

3.5 TODO 与 FIXME 标记:代码里的「待办清单」

TODO和FIXME是团队协作中非常实用的标记。但很多人用得乱七八糟,最后变成了一堆没人管的「技术债务」。

我建议统一格式:

// TODO(your_name): 2024-06-15 这里需要支持批量删除,当前只支持单条
// FIXME(your_name): 2024-06-10 边界条件处理有bug,当size==0时会死循环

格式说明:

标记 含义 使用场景
TODO 待实现的功能或改进 功能未完成、需要优化、需要重构
FIXME 已知的bug或缺陷 代码有错误、边界条件未处理、性能问题
HACK 临时解决方案 为了快速上线写的临时代码,后续需要重写
XXX 需要讨论或确认 设计不确定、需要团队评审

每个标记必须包含:

  • 责任人:谁负责处理?用Git用户名或真实姓名
  • 日期:什么时候标记的?方便判断优先级
  • 具体描述:要做什么?为什么?怎么重现?
团队实践:我建议在CI流程中加一个检查,统计TODO和FIXME的数量。超过一定阈值(比如50个)就报警。这样可以防止技术债务无限累积。

3.6 知识体系总览

下面这张图总结了注释规范的核心要点:

C++ 注释规范体系 文件头注释 函数注释 类注释 代码行注释 TODO / FIXME @file @brief @author @date @version @note @warning @param @return @retval @exception @code @note @warning 设计意图 生命周期 线程模型 使用示例 @see @since 解释「为什么」 复杂算法 特殊处理 性能优化 已知缺陷 责任人 + 日期 具体描述 TODO / FIXME / HACK / XXX 核心原则:解释「为什么」,而不是「是什么」

3.7 总结

注释规范这件事,说白了就是「为别人着想」。你写注释的时候,想想三个月后的自己,或者刚接手这个模块的新同事,他们最需要知道什么。

我个人经验是:好的注释能减少50%以上的沟通成本。与其花时间开会解释代码,不如花10分钟把注释写清楚。

最后记住一句话:注释是代码的一部分,不是附属品。它和代码一样,需要维护、需要更新、需要被认真对待。

核心要点回顾:

  • 文件头注释:给文件一个完整的「身份证」
  • 函数注释:重点写参数、返回值、异常、示例
  • 类注释:写清楚设计意图、生命周期、线程模型
  • 行注释:解释「为什么」,而不是「是什么」
  • TODO/FIXME:责任人+日期+描述,缺一不可