4. Cppcheck 实战:安装、配置、命令行使用、自定义规则编写

好,咱们进入实战环节。前面讲了那么多理论,说白了,工具得用起来才有价值。这一章我带你亲手把 Cppcheck 玩转,从安装到写自定义规则,一步不落。最后我会分享一个真实案例——我在一个电机控制项目里,用它抓到了一个隐藏极深的空指针。

4.1 安装与基础配置

Cppcheck 的安装,其实没什么难度。我习惯在 Linux 下做开发,一条命令搞定:

sudo apt-get install cppcheck

Windows 用户去官网下载安装包就行。macOS 用户用 brew:

brew install cppcheck

装完之后,验证一下:

cppcheck --version

嗯,看到版本号就说明成了。

我个人习惯在项目根目录放一个 .cppcheck 配置文件。这样团队里每个人跑出来的结果都一样,避免「你机器上能过,我机器上就报错」的尴尬。

# .cppcheck 示例
--suppress=missingIncludeSystem
--enable=all
--inconclusive
--std=c99
--platform=unix64
--output-file=cppcheck_report.xml
--xml
小提示: --inconclusive 这个选项,我建议你加上。它会让 Cppcheck 报告一些「不确定但可疑」的问题。虽然会有误报,但有时候真能发现意想不到的 bug。

4.2 命令行使用技巧

命令行才是 Cppcheck 的精髓。你想想看,CI 流程里不可能有人盯着 GUI 点点点,对吧?

最基本的用法:

cppcheck --enable=all --suppress=missingIncludeSystem ./src

这行命令会检查 ./src 目录下所有 C/C++ 文件,启用所有检查项,同时忽略头文件找不到的警告(嵌入式项目里经常有交叉编译的头文件路径问题)。

我常用的几个参数:

参数 作用 我的经验
--enable=all 启用所有检查 别怕误报,宁可错杀一千
--suppress= 抑制特定警告 第三方库的警告直接压掉
--xml 输出 XML 格式 CI 解析用,方便集成
--std=c99 指定 C 标准 嵌入式老项目常用 C99
-j 4 多线程检查 大项目能快 3-4 倍

举个例子,我在一个电机控制项目里,代码量大概 10 万行。不加 -j 要跑 40 秒,加上 -j 4 只要 12 秒。CI 流水线里,每一秒都很宝贵。

4.3 自定义规则编写

Cppcheck 最强大的地方,就是你可以写自己的规则。说白了,就是写一个 C++ 插件,让 Cppcheck 在分析代码时调用你的逻辑。

为什么要写自定义规则?我遇到过这种情况:项目里有个约定,所有 malloc 之后必须检查返回值。但 Cppcheck 默认不检查这个。怎么办?自己写一个规则呗。

创建一个 myrule.cpp

#include "cppcheck.h"
#include "tokenize.h"

class MyRule : public Check {
public:
    MyRule() : Check(myName()) {}
    
    static std::string myName() {
        return "MyRule";
    }

    void runChecks(const Tokenizer &tokenizer, ErrorLogger &errorLogger) override {
        // 遍历所有 token
        for (const Token *tok = tokenizer.tokens(); tok; tok = tok->next()) {
            // 检查 malloc 后是否立即检查 NULL
            if (tok->str() == "malloc") {
                const Token *next = tok->next();
                if (next && next->str() == ";") {
                    // malloc 后面直接跟分号,没有检查 NULL
                    errorLogger.reportError(
                        tok,
                        Severity::warning,
                        "mallocWithoutCheck",
                        "malloc 后未检查返回值是否为 NULL",
                        "CWE-476"
                    );
                }
            }
        }
    }
};

// 注册插件
extern "C" Check *createCheck() {
    return new MyRule();
}

编译成动态库:

g++ -shared -fPIC -o myrule.so myrule.cpp -I/path/to/cppcheck/include

使用自定义规则:

cppcheck --library=myrule.so ./src
注意: 自定义规则的 API 在不同版本的 Cppcheck 之间可能有变化。我建议你锁定 Cppcheck 版本,否则规则可能失效。我曾经因为升级 Cppcheck 没注意 API 变化,导致 CI 静默跳过了所有自定义检查,白白浪费了两周时间。

4.4 实战案例:电机控制项目中的空指针

好,重点来了。我在一个电机控制项目里,用 Cppcheck 抓到了一个空指针。这个 bug 藏得很深,代码审查都没发现。

项目背景:一个无刷直流电机(BLDC)的 FOC 控制算法。代码是前辈写的,跑了两年都没出问题。直到有一次,电机在特定工况下突然停转。

我接手后,第一件事就是跑 Cppcheck:

cppcheck --enable=all --suppress=missingIncludeSystem --xml ./src 2> report.xml

结果出来,有一条警告让我眼前一亮:

./src/motor_control.c:247: error: Possible null pointer dereference: pwm_config

我打开代码一看:

void motor_set_pwm(MotorConfig *config) {
    PWMConfig *pwm_config = config->pwm;  // 这里可能为 NULL
    pwm_config->frequency = 20000;        // 直接解引用
    pwm_config->duty_cycle = 0.5;
}

问题出在哪?config->pwm 在初始化时可能为 NULL。正常情况下,初始化代码会先分配 pwm_config,但有一个异常路径跳过了分配。这个路径只在电机启动电流过大时触发,概率极低。

修复很简单:

void motor_set_pwm(MotorConfig *config) {
    PWMConfig *pwm_config = config->pwm;
    if (pwm_config == NULL) {
        // 记录错误日志,返回安全值
        log_error("PWM config is NULL");
        return;
    }
    pwm_config->frequency = 20000;
    pwm_config->duty_cycle = 0.5;
}
关键点: 这个 bug 之所以没被代码审查发现,是因为它藏在异常路径里。人眼很难覆盖所有分支,但静态分析工具可以。Cppcheck 会遍历所有可能的执行路径,这就是它的价值所在。

我曾经以为这种低级错误不会出现在成熟项目里。但现实是,越复杂的系统,越容易有这种「灯下黑」的问题。从那以后,我把 Cppcheck 加到了 CI 的必过环节,任何空指针警告都不允许合并。

4.5 集成到 CI 的最佳实践

最后,说说怎么把 Cppcheck 集成到 CI 里。我用的 Jenkins,但思路通用。

  1. 设置阈值:我一般设 error 为 0,warning 不超过 10 个。超过就构建失败。
  2. 生成报告:用 --xml 输出,然后用 Jenkins 的 Warnings Next Generation 插件解析。
  3. 增量检查:大项目全量检查太慢。我写了个脚本,只检查 git diff 涉及的文件。
  4. 基线管理:第一次跑出来的警告,建立基线。后续只报告新增的警告。

嗯,这一章内容不少。从安装到自定义规则,再到实战案例,你应该对 Cppcheck 有了全面的认识。下一章,咱们聊聊如何把 Cppcheck 和 Jenkins 深度整合,实现自动化门禁。