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 知识体系总览
下面这张图总结了注释规范的核心要点:
3.7 总结
注释规范这件事,说白了就是「为别人着想」。你写注释的时候,想想三个月后的自己,或者刚接手这个模块的新同事,他们最需要知道什么。
我个人经验是:好的注释能减少50%以上的沟通成本。与其花时间开会解释代码,不如花10分钟把注释写清楚。
最后记住一句话:注释是代码的一部分,不是附属品。它和代码一样,需要维护、需要更新、需要被认真对待。
核心要点回顾:
- 文件头注释:给文件一个完整的「身份证」
- 函数注释:重点写参数、返回值、异常、示例
- 类注释:写清楚设计意图、生命周期、线程模型
- 行注释:解释「为什么」,而不是「是什么」
- TODO/FIXME:责任人+日期+描述,缺一不可