第27章 文档生成:Doxygen注释规范、文档生成配置、API文档自动化

说实话,很多C语言开发者写代码时,最头疼的就是写文档。

我自己刚入行那会儿,也觉得「代码即文档」这句话特别酷。直到有一次接手一个老项目,看到满屏的/* TODO: fix me */和完全没有注释的函数……嗯,那感觉就像在迷宫里找出口,连个路标都没有。

后来我学乖了。文档不是写给别人的,是写给三个月后的自己的。而Doxygen,就是帮你把注释自动转成漂亮文档的神器。

27.1 Doxygen注释规范

Doxygen支持多种注释风格。我个人习惯用/** ... */,因为它在IDE里高亮效果最好。

27.1.1 文件头注释

每个源文件和头文件,我都会加一个文件头。它就像身份证,告诉别人这个文件是干嘛的。

/**
 * @file    tcp_client.c
 * @brief   TCP客户端核心实现
 * @author  张工
 * @date    2024-03-15
 * @version 1.2.0
 *
 * 本文件实现了TCP客户端的连接、发送、接收功能。
 * 支持IPv4和IPv6双栈。
 */

这里有个坑——@file后面一定要写文件名。我曾经漏写过,结果Doxygen生成的文档里,这个文件的内容全堆到「未命名文件」里去了,找都找不到。

27.1.2 函数注释

函数注释要写清楚三件事:干什么、参数是什么、返回什么。

/**
 * @brief  建立TCP连接
 * @param  host    目标主机名或IP地址
 * @param  port    目标端口号(1-65535)
 * @param  timeout 超时时间,单位毫秒,0表示默认超时
 * @return 成功返回套接字描述符,失败返回-1
 * @retval -1  连接失败,errno已设置
 * @retval -2  参数无效
 *
 * 注意:本函数会阻塞调用线程,直到连接建立或超时。
 * 建议在子线程中调用。
 */
int tcp_connect(const char *host, uint16_t port, uint32_t timeout);

你看,@param后面跟参数名和说明,@return描述一般返回值,@retval描述特定返回值。这样生成的文档,别人一看就知道怎么用。

我的习惯:每个函数注释里都加一个「注意」段落。把那些容易踩的坑提前写出来,比事后解释强十倍。

27.1.3 结构体和枚举注释

结构体注释有两种写法。我推荐成员后置注释,因为对齐后特别清晰。

/**
 * @brief TCP连接配置参数
 */
typedef struct {
    char     host[256];   /**< 目标主机名或IP */
    uint16_t port;        /**< 端口号 */
    uint32_t timeout_ms;  /**< 超时时间(毫秒) */
    bool     keep_alive;  /**< 是否启用TCP保活 */
} tcp_config_t;

枚举也是一样:

/**
 * @brief 日志级别
 */
typedef enum {
    LOG_DEBUG,   /**< 调试信息 */
    LOG_INFO,    /**< 普通信息 */
    LOG_WARN,    /**< 警告信息 */
    LOG_ERROR    /**< 错误信息 */
} log_level_t;

27.2 Doxygen配置文件生成

Doxygen的配置项有上百个,但真正需要改的,其实就十几个。

生成配置文件很简单:

doxygen -g Doxyfile

这会生成一个默认的Doxyfile。我每次都会改下面这几个关键项:

配置项 推荐值 说明
PROJECT_NAME 你的项目名 显示在文档标题上
PROJECT_BRIEF 一句话描述 显示在文档副标题
OUTPUT_DIRECTORY doc/ 文档输出目录
INPUT src/ include/ 源代码目录,多个用空格隔开
RECURSIVE YES 递归搜索子目录
EXTRACT_ALL YES 提取所有实体,即使没有注释
GENERATE_HTML YES 生成HTML文档
GENERATE_LATEX NO 一般不需要PDF
HAVE_DOT YES 生成调用图、继承图(需安装Graphviz)
注意:HAVE_DOT设为YES后,如果没装Graphviz,Doxygen会报错。你可以先设为NO,等需要看图时再打开。

27.3 文档生成与自动化

配置好Doxyfile后,生成文档就一行命令:

doxygen Doxyfile

但手动执行太原始了。我一般在Makefile里加一个doc目标:

# Makefile 片段
.PHONY: doc clean-doc

doc:
    @echo "生成API文档..."
    @doxygen Doxyfile
    @echo "文档已生成到 doc/html/ 目录"
    @echo "用浏览器打开 doc/html/index.html 查看"

clean-doc:
    @rm -rf doc/html doc/latex

这样每次更新代码后,跑一下make doc,文档就自动更新了。

27.3.1 持续集成中的文档自动化

在CI流水线里,我习惯把文档生成和部署也加进去。比如GitHub Actions:

# .github/workflows/docs.yml
name: 生成并部署API文档

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'include/**'
      - 'Doxyfile'

jobs:
  build-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: 安装依赖
        run: sudo apt-get install -y doxygen graphviz
      - name: 生成文档
        run: make doc
      - name: 部署到GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./doc/html

你看,每次往main分支推送代码,文档就会自动重新生成并部署到GitHub Pages上。团队其他人打开浏览器就能看到最新文档。

核心思路:文档和代码是同步的。代码改了,文档必须跟着变。自动化就是保证这件事不出错的最好方式。

27.4 知识体系总览

下面这张图,是我对本章知识点的总结。你可以把它当成一个检查清单:

Doxygen文档生成知识体系 注释规范 • 文件头注释 • 函数注释 • 结构体注释 • 枚举注释 • 宏定义注释 常用标签: @brief @param @return @retval @note @see @file @author @date @version @warning 配置生成 • doxygen -g Doxyfile • 关键配置项: PROJECT_NAME INPUT / RECURSIVE EXTRACT_ALL GENERATE_HTML HAVE_DOT • 输出目录设置 • 图片/示例配置 • 标签文件配置 自动化 • Makefile集成 • CI/CD流水线 • GitHub Actions • GitLab CI • 文档部署: GitHub Pages Read the Docs 自建Web服务器 • 版本号管理 • 多版本文档 先写好注释 → 配置Doxygen → 自动化生成与部署

27.5 一些实用技巧

最后分享几个我踩过的坑和总结的技巧:

  • 注释要写「为什么」,而不是「是什么」。比如/* 加1 */ i++;这种注释,删了也不影响理解。但/* 绕过内核的TCP快速重传机制 */这种,才是真正有用的。
  • @ingroup给函数分组。当项目有几百个函数时,分组能让文档清晰十倍。我习惯按模块分组:网络组、日志组、配置组……
  • 生成文档后,一定要自己打开看看。我曾经以为注释写得挺好,结果生成出来发现@param和参数名对不上,排版全乱了。嗯,工具不是万能的。
  • 把Doxyfile纳入版本控制。这样团队所有人都用同一套配置,不会出现「我生成的文档和你的不一样」这种尴尬。
一个小技巧:在Doxyfile里设置WARN_AS_ERROR = YES。这样Doxygen遇到注释错误会直接报错退出,强迫你把注释写规范。我在CI里一定会开这个选项。

文档生成这件事,说白了就是「写一次注释,受益无数次」。你想想看,每次有人问你「这个函数怎么用」,你直接甩给他一个文档链接,多省事。

而且,当你半年后回来看自己的代码,看到那些清晰的注释和自动生成的文档,你会感谢当初那个认真写注释的自己。

公众号:蓝海资料掘金营,微信 deep3321