把代码交给编译器-Wall只会告诉你「语法有没有问题、有没有明显的未初始化」。但更隐蔽的 bug对象被std::move之后又被读、整数除法悄悄截断了小数、还在用NULL和 C 风格转换-Wall一概不报因为它们语法上完全合法。clang-tidy 和 cppcheck 就是来补这块的静态分析static analysis工具不运行程序直接读源码找问题。这篇讲清楚它们各自抓什么、怎么落地。1. 引子一段能跑、但有味道的代码下面这段代码能正常编译运行但静态分析工具能一眼挑出三处「坏味道」#includeiostream#includevector#includecstddefintmain(){int*pNULL;// modernize-use-nullptr 会抓std::vectorintv{1,2,3};std::vectorint::iterator itv.begin();// modernize-use-auto 会抓intn(int)v.size();// C 风格转换std::coutnn first*it\n;(void)p;}n3 first1它跑起来完全正常-Wall也不会说什么NULL是合法的宏、迭代器显式声明类型合法、C 风格转换合法。但 clang-tidy 的modernize-*系列会分别报modernize-use-nullptrNULL应写成nullptr、modernize-use-auto迭代器类型可推导为auto、以及 C 风格转换建议改static_cast。这些不是 bug是「可维护性负债」静态分析的价值正在于在它们累积成 bug 之前就拦下来。2. clang-tidy 是什么基于 clang AST 的 checkclang-tidy 是 LLVM 项目的静态分析工具它复用 clang 的前端把源码解析成 AST抽象语法树然后一条一条地跑 check。每个 check 针对一类具体问题用-checks指定要开哪些。它的 check 按前缀分组前缀抓什么例子modernize-*建议用现代 C 写法modernize-use-nullptr、modernize-use-auto、modernize-use-overridebugprone-*容易踩坑的 bugbugprone-use-after-move、bugprone-integer-division、bugprone-narrowing-conversionscppcoreguidelines-*C Core Guidelines 对应的规则cppcoreguidelines-init-variables、cppcoreguidelines-avoid-magic-numbersperformance-*性能问题performance-unnecessary-copy-initialization、performance-move-const-argreadability-*可读性readability-implicit-bool-conversion、readability-identifier-naming一个 check 抓一类用起来就是「开一批 check 跑一遍」。命令长这样--后面是传给编译器的参数clang-tidy main.cpp-checksmodernize-*,bugprone-*---stdc17官方文档C Core Guidelinescppcoreguidelines-*这一组 check 是 Core Guidelines 的机器化落地看 check 前先看对应规则。3. 修复后clang-tidy 想让你写成什么样modernize-*的修复方向是「让类型和语义都更显式」。上一节的坏味道修复后是这个样子#includeiostream#includevector#includecstddefintmain(){int*pnullptr;// modernize-use-nullptrstd::vectorintv{1,2,3};autoitv.begin();// modernize-use-autoautonstatic_caststd::size_t(v.size());// 命名转换类型显式std::coutnn first*it\n;(void)p;}n3 first1三处改动都是「等价但更清晰」nullptr有独立类型不会隐式转成整数、auto省掉重复写类型名、static_cast让转换意图显式。这类modernize修复多数是机械的clang-tidy 甚至支持-fix自动改。官方文档nullptr — cppreferencemodernize-use-nullptr想让你用的nullptr到底是什么、为什么优于NULL。4. bugprone 才是真正抓 bug 的那组modernize管风格bugprone才是抓真 bug。它盯的是「语法合法、语义危险」的写法。最典型的一个是 use-after-move对象被std::move转走之后再读它就是未定义行为std::string s hello; auto t std::move(s); // s 的内容被搬走了 │ ▼ s 进入 valid-but-unspecified合法但内容不确定状态 │ ▼ std::cout s; // ← bugprone-use-after-move 在这里报警另一个经典是整数除法total / count两个int相除结果先截断成整数再赋给double小数部分已经丢了。bugprone-integer-division会提醒你「这里真的是想要整数除法吗」。use-after-move 的坏代码长这样片段展示bugprone-use-after-move的目标// 反例move 之后又读原对象bugprone-use-after-move 会在这里报警片段std::string shello;autotstd::move(s);// s 的内容被搬走std::couts\n;// ← s 处于 valid-but-unspecified读它是 UB注意这段编译能过、-Wall不报但读 move 后的s是未定义行为undefined behavior。这是它比modernize那组更危险的地方不是风格问题是真能出 bug 的问题。double avg(int total, int count) { return total / count; } │ ▼ total / count 是 int/int - 先做整数除法小数被截断 再隐式转成 double 返回此时精度已经没了整数除法的坑可以真跑出来看avg(5, 2)返回2而不是2.5#includeiostreamdoubleavg(inttotal,intcount){returntotal/count;// bugprone-integer-divisionint/int 先截断}intmain(){std::coutavg(5, 2) avg(5,2)\n;std::cout正确写法 5.0/2\n;}avg(5, 2) 2 正确写法 2.5avg里total / count是两个int相除5 / 2先算出2小数被截断再隐式转成double返回精度在除法那一刻就丢了。bugprone-integer-division正是要在这里提醒你「确认一下是不是想要整数除法」。这两类问题-Wall都不报甚至-Wextra也不报因为它们不违反语法。这正是 clang-tidy 存在的理由编译器管「合不合法」clang-tidy 管「合不合意」。官方文档Compiler Explorer — godbolt.orggodbolt 的工具面板里内置了 clang-tidy可以把同一段代码的 check 结果和汇编并列看。5. cppcheck独立引擎和 clang-tidy 互补cppcheck 是另一个思路它不依赖 clang用自己的轻量解析器跑得比 clang-tidy 快也不需要完整的编译参数。它对「未初始化变量、越界访问、内存泄漏」这类值流分析更擅长但对「现代 C 写法建议」不如 clang-tidy。cppcheck--enableall main.cpp两者不是二选一而是互补clang-tidy 擅长「现代化改造 基于 AST 的语义检查」cppcheck 擅长「跨函数的资源泄漏 值流」这类需要全局视角的问题。维度-Wallclang-tidycppcheck引擎编译器前端clang AST独立解析器抓什么语法/浅层语义风格 语义 bugmove 后再用等泄漏、越界、未初始化需要完整编译参数是是要--传参数否相对宽松速度快编译顺带中快自动修复无有-fix无适用每次编译必开CI 里的「改造 深度检查」快速全量扫描6. .clang-tidy 配置把规则写进文件单个工程要固定开哪些 check写一个.clang-tidy配置文件放在项目根目录clang-tidy 会自动读取# .clang-tidy片段 Checks: modernize-*,bugprone-*,-modernize-use-trailing-return-type WarningsAsErrors: bugprone-*Checks指定开哪些-前缀排除个别WarningsAsErrors把某一类升级成编译错误逼着 CI 里必须修。这样「开哪些 check」就从命令行挪进文件进了版本控制全队一致。一个更完整的配置长这样# .clang-tidy Checks: modernize-*, bugprone-*, performance-*, readability-*, -modernize-use-trailing-return-type, -readability-magic-numbers WarningsAsErrors: bugprone-* CheckOptions: - key: readability-identifier-naming.ClassCase value: CamelCase是 YAML 的多行折叠写法让一长串 check 能换行CheckOptions给具体 check 传参数比如指定命名风格。.clang-tidy放进项目根目录后clang-tidy 会向上逐级查找所以子目录可以放一份覆盖父目录的规则。也可以用-checks直接列在命令行适合临时跑一次、不想建文件的情况clang-tidy main.cpp-checksmodernize-*,bugprone-*,-modernize-use-trailing-return-type---stdc17 clang-tidy main.cpp-fix# 让能自动修的直接改源码clang-tidy main.cpp -list-checks# 列出所有可用的 check 名7. 完整示例一段适合「过一遍 tidy」的代码把前面几处问题放进一个完整程序。它是对的但值得过一遍静态分析#includeiostream#includevectorinttotalEven(conststd::vectorintv){intsum0;for(constautox:v){if(x%20)sumx;// 偶数求和}returnsum;}intmain(){std::vectorintv{1,2,3,4,5};std::coutsumEventotalEven(v)\n;}sumEven6for (const auto x : v)这种写法本身就是 clang-tidy 和 Core Guidelines 想引导你去的方向范围 for autoconst避免手写索引、避免拷贝、意图清楚。再给一个「把 clang-tidy 的建议都落实到位」的多态示例override、explicit、make_unique、const成员函数、范围 for 全齐这些是modernize-*和cppcoreguidelines-*反复念叨的写法#includeiostream#includememory#includevectorstructShape{virtual~Shape()default;virtualdoublearea()const0;};structCircle:Shape{doubler{1.0};explicitCircle(doubleradius):r{radius}{}doublearea()constoverride{return3.14159*r*r;}};intmain(){std::vectorstd::unique_ptrShapeshapes;shapes.push_back(std::make_uniqueCircle(2.0));for(constautos:shapes){std::coutareas-area()\n;}}area12.5664这段如果反过来写struct Circle : Shape { double area() { ... } }漏掉override、构造函数没加explicit、用裸new而不是make_uniqueclang-tidy 会逐条点出来modernize-use-override、cppcoreguidelines-*、cppcoreguidelines-owning-memory各司其职。8. 实测clang-tidy 真跑出来的诊断clang-tidy 的诊断文本这篇已经实测通过 Compiler Explorer 的tools字段请求clang-tidy (trunk)把第 1 节那段「有味道的代码」扔进去跑。下面是真实输出-checksbugprone-*,modernize-*,performance-*已省略非用户代码的 218 条警告只留本文件里这几条source:11:35: warning: s used after it was moved [bugprone-use-after-move] 11 | std::printf(s size %zu\n, s.size()); | ^ source:10:21: note: move occurred here 10 | std::string t std::move(s); | ^ source:15:5: warning: use range-based for loop instead [modernize-loop-convert] 15 | for (std::vectorint::iterator it v.begin(); it ! v.end(); it) { | ^ source:15:10: warning: use auto when declaring iterators [modernize-use-auto] source:20:5: warning: do not declare C-style arrays, use std::array instead [modernize-avoid-c-arrays] 20 | int arr[5] {1, 2, 3, 4, 5}; | ^~~~~~~~~~几件事一眼看清bugprone-use-after-move精确指向「move 之后又s.size()」的那一行连 move 发生在哪一行都用note标出来了。modernize-loop-convert和modernize-use-auto对同一个 for 循环各报一条各管各的现代化改造点不重复。modernize-avoid-c-arrays盯着int arr[5]这种 C 风格数组建议换std::array。这些和正文前几节逐条对得上check 名、报的是什么、怎么修都是真的。cppcheck 是独立引擎这篇环境里没有它godbolt 的 CMake 沙箱find_program探测返回NOTFOUND所以 cppcheck 的具体诊断文本仍是官方文档用法、未实测这点如实说明。9. 延伸阅读C Core Guidelinesclang-tidy 里cppcoreguidelines-*一组 check 的规范出处先读规则再开 check。nullptr — cppreferencemodernize-use-nullptr建议的目标理解nullptr优于NULL的原因。Compiler Explorer — godbolt.org内置 clang-tidy 工具面板可在线跑 check和汇编并列看。clang-tidy 官方手册在clang.llvm.org/extra/clang-tidy不在这篇白名单内故不列链接自行搜索。本知识库内的相关篇目《gdb 实战断点、watchpoint、core dump 与多线程调试》 —— 程序崩了只会看日志这篇系统讲 gdb 的断点、单步、print、backtrace、watchpoin《二进制分析nm、objdump、readelf 看懂符号表与段》 —— 链接报 multiple definition、undefined reference《perf 与火焰图从热点到定位接 Perfetto》 —— 程序慢在哪这篇讲 Linux perf 采样型 profiler 的原理周期采样 栈回溯、pe10. 一句话总结-Wall管「合不合法」clang-tidy 管「合不合意」前者是编译顺带的底线检查后者用modernize-*管现代化改造、bugprone-*抓 move 后再用/整数除法这类语义 bugcppcheck 再用独立引擎补资源泄漏和越界。落地就三步写.clang-tidy定规则、CI 里跑起来、WarningsAsErrors把bugprone升级成必须修。
阅读完成 · 觉得有帮助?