写 C 写了十几年我在 code review 里看到最多的一句话不是“这个 bug 怎么复现”而是“这段 C 代码太烂了”。骂人容易难的是把“烂”这个词说清楚。代码复杂度分析本质上就是把“烂”量化成数学你这个函数到底嵌套了多少层 if、绕过了多少个分支、别人要费多大劲才能跟上思路。Lizard 就是干这件事的工具轻量、免费、命令行C/C、Java、Python、JavaScript 都能分析。装好以后跑一句lizard xxx.c它会直接告诉你这个文件里哪个函数最危险。下面我用 C 代码的真实例子带你从安装、读报告、设阈值到把 Lizard 接进 CI一步步让“这段代码太烂了”变成团队里所有人都认的规矩。1. 为什么代码复杂度分析一定要工具化1.1 复杂度不是玄学是数学圈复杂度这个概念最早是 Thomas McCabe 在 1976 年提出来的公式很直白CCN 1 决策点个数。决策点就是 if、else if、for、while、case、catch、三元运算符还有和||。你可以把它理解成迷宫里的岔路口每多一个岔路口走通一遍的可能性就翻一倍。一个函数如果有 10 个决策点理论上就有 11 条独立路径测试要覆盖全路径就得准备 11 组输入。这种情况下你说“这段代码太烂了”不是情绪输出是有数据支撑的质量结论。现实中我见过太多团队把“烂”挂在嘴上但提不出任何量化依据。review 的时候各凭感觉有人嫌函数长有人嫌命名差还有人嫌缩进不好看。这些都有道理但没法形成一致的门禁标准。复杂度工具化的意义就在这里把“烂”变成一组可比较、可追踪、可设阈值的数字。哪怕今天负责 review 的人换了只要复杂度红线还在代码质量就不会因为个人口味而漂移。1.2 Lizard 凭什么敢说自己跨语言Lizard 最吸引我的一点是它不需要编译环境。很多静态分析工具比如 clang-tidy、Cppcheck对 C 代码来说都需要能正确处理头文件、宏展开、编译参数项目稍微老一点配置就能把人劝退。Lizard 不同它在词法层面对源码做分析拿到源文件直接扫不需要 include 路径不需要 makefile不需要你先把项目编译通过。这意味着什么意味着我拿到一个陌生的老项目第一件事不是去折腾环境和构建而是直接lizard .跑一遍十分钟内就能知道整个代码库里最危险的是哪几个函数。C、C、Java、Python、JavaScript、Go 它都认识语法风格不统一的老项目也能一碗水端平。在维护遗产项目或者做技术债务盘点的时候这个“零依赖”特性简直是救命稻草。2. 五分钟装好 Lizard安装方式与第一份报告2.1 安装方式你能想到的包管理器都有Lizard 是用 Python 写的所以最常规的安装方式就是 pip。如果你用的是 macOSHomebrew 也有现成的包Ubuntu 这类 Debian 系发行版则可以通过 apt 安装。我日常在 macOS 上工作装完以后通常会顺手看一眼版本确认环境没问题。pip install lizard lizard --version用 Homebrew 的话命令就是brew install lizard。Linux 上如果不想用 root 权限折腾 Python 环境直接sudo apt install lizard也很快。Windows 上我一般也推荐 pip装好之后如果有多个 Python 版本注意把Scripts目录加进 PATH或者直接用python -m lizard这种形式调用。第一次跑的时候如果提示命令找不到先不用急着重装看看是不是 PATH 的问题。2.2 第一份报告怎么读字段逐个拆装好后随便拿一个 C 文件测一下命令是lizard test.c。它的默认输出是一张函数清单每个函数一行字段看起来像这样 NLOC CCN token PARAM length location ------------------------------------------------ 9 3 211 2 8 process_data12-19C ------------------------------------------------ 1 file analyzed.第一次看到这张表的人多半会懵我先解释一下每个字段的意思字段含义实际关注点NLOC不包含空行和纯括号行的代码行数函数有没有写得过长CCN圈复杂度分支越多越危险核心指标token函数体内 token 总数侧面反映信息量大小PARAM形式参数个数参数过多代表职责混乱length函数实际代码行数含括号等视觉长度容易直观感知location函数名、起止行号、语言定位问题代码的位置通常我扫完一个目录第一眼看的是 CCN 最高的几个函数其次看 PARAM 超过 5 个的函数。这两个指标一旦爆表代码大概率难维护。NLOC 和 length 则更像辅助参考一个函数即使没多少分支如果写了三百行也不是什么好征兆因为说明它塞了太多不相干的事。3. 看懂关键指标CCN、参数个数和函数长度3.1 圈复杂度CCN的阈值怎么定Lizard 默认在 CCN 超过 15 的时候会给出警告。这个数值在业内大致对应“需要警惕”的水平。业界比较通行的分法是CCN 区间风险等级可读性表现1 - 10低风险正常人扫一眼能明白11 - 20中风险注释和测试开始变重要21 - 50高风险大概率只有原作者能维护50 以上极其危险重写的成本可能比重构低注意这个表不是金科玉律。我个人的经验是C 语言这种需要手动管理内存的语言阈值要比 Python 再严一点。因为分支一多释放路径就多漏掉一个就内存泄漏漏掉一个就 double free。所以在 C 项目里我通常把红线定在 CCN 10而不是 15。一个函数如果超过 10我就默认它已经承载了太多的业务分支需要拆。另一个容易忽略的点是和||。看起来是一个 if实际是两个决策点。if (a ! NULL a-x 0)这一行CCN 里就算了两下。所以不要看到 if 数量不多就放松警惕复合条件才是复杂度刺客。3.2 被忽视的 PARAM 和 NLOC很多人只看 CCN把参数个数和函数长度抛在脑后。事实上我做过一次粗糙统计5 个以上参数的函数改一次往往要连带改七八个调用点10 个以上的参数基本就是结构体该上场的时候了。Lizard 里 PARAM 这一列会原样暴露这些数据配合团队规范非常方便。举个我在地铁上随手见过的例子int update_config(const char *host, int port, const char *user, const char *password, int timeout, int retry, int ssl_enable, int compress_enable, int debug, int verbose)这个函数有 10 个参数Lizard 的 PARAM 直接标 10。这种代码站在维护者的角度根本记不住调用顺序传错一个参数到了运行时才会暴露。正确做法是把后面七个参数塞进一个config_t结构体两层意思表达清楚调用处也更短。复杂度分析工具在这里的贡献不是帮你改而是让你在 commit 之前就被数字吓到。NLOC 和 length 的作用则更朴素。Lizard 默认的 NLOC 警告线是 1000token 是 1000这两个默认值对 C 项目来说太松了。在团队内部我更建议把单个函数的 NLOC 红线定在 60 到 80 行。超过这个规模功能逻辑通常已经开始跨越多个职责边界了拆成两三个小函数是更健康的选择。4. 实战把一个“太烂了”的 C 函数改干净4.1 场景回顾这个函数为什么烂现在来看一段能让你有既视感的 C 代码典型的上古遗留风格。单看结构就知道作者是一路叠 else if 叠出来的逻辑已经绕成打结的耳机线int process_order(order_t *order, int type, int discount, int user_level, int flags) { int result 0; if (order NULL || order-items NULL) { return -1; } if (type 1) { if (user_level 3) { for (int i 0; i order-count; i) { if (order-items[i].price 1000 || order-items[i].price 0) { result -2; goto fail; } if (discount 0) { if (user_level 3) { order-items[i].price order-items[i].price * 0.9; } else { order-items[i].price order-items[i].price * 0.85; } } } } else { result 100; } } else if (type 2) { result process_fast(order); } else { if (flags 0x01) { result 1; } else { result 2; } } return result; fail: log_error(invalid price); return result; }这个函数表面上有几个问题参数 5 个、函数接近 40 行、有 goto 跳转、有复合条件、for 循环里面套 ifif 里面再套 if。但“表面问题”不算数得让工具说话。4.2 Lizard 怎么说分析输出逐行解读把这段代码存成order.c然后跑lizard order.c输出类似这样 NLOC CCN token PARAM length location ------------------------------------------------ 31 11 320 5 30 process_order3-31C ------------------------------------------------CCN 11 已经越过了我建议的 10 这个红线。这个数字是怎么来的我带你数一下order NULL || order-items NULL是一个 if 加一个||算 2 个决策点。type 1算 1 个。user_level 3算 1 个。for (int i 0; i order-count; i)算 1 个。order-items[i].price 1000 || order-items[i].price 0是 if 加||算 2 个。discount 0算 1 个。user_level 3算 1 个。type 2算 1 个。flags 0x01算 1 个。总共 11 个决策点所以 CCN 1 11 12这里我需要纠正一下Lizard 会把第一个 if 基础路径包含进去最终计算出 12 可能更准确。实际输出时它确实可能显示 12不同版本或统计细节略有差异。但这不重要重要的是它明明白白告诉你这个函数的复杂度已经需要拆了。再加上 goto fail 跳到后面的log_error这个写法控制流的非线性程度肉眼可见。4.3 重构拆函数、改提前返回、去 goto重构的思路不是把代码删掉而是让每个函数只做一件事。我把 type 的分支拆出来把价格校验拆出来把打折逻辑拆出来goto 彻底删掉。重构后大概是这个样子的static int validate_order(const order_t *order) { if (order NULL || order-items NULL) { return -1; } for (int i 0; i order-count; i) { if (order-items[i].price 1000 || order-items[i].price 0) { return -2; } } return 0; } static int apply_discount(order_t *order, int discount, int user_level) { for (int i 0; i order-count; i) { if (discount 0) { continue; } if (user_level 3) { order-items[i].price * 0.9; } else { order-items[i].price * 0.85; } } return 0; } int process_order(order_t *order, int type, int discount, int user_level, int flags) { int result validate_order(order); if (result ! 0) { return result; } if (type 1) { if (user_level 3) { return apply_discount(order, discount, user_level); } return 100; } if (type 2) { return process_fast(order); } return (flags 0x01) ? 1 : 2; }重构后再跑一次 Lizard NLOC CCN token PARAM length location ------------------------------------------------ 8 3 90 1 7 validate_order3-9C 9 4 120 3 8 apply_discount12-24C 8 3 60 4 7 process_order27-39C没有哪个函数再超过 CCN 5参数也没有超过 4 的。这种拆分给人的第一感觉不是代码少了而是你终于能在一个视线范围内看懂每条路径的走向。加了新的订单类型时只需要在 process_order 里加一个 if 分支不会动到 validate_order 和 apply_discount。这就是复杂度下降带来的直接收益。5. 把 Lizard 接进 CI让“烂代码”过不了门禁5.1 warnings_only 和退出码才是 CI 的关键Lizard 本身可以作为本地检查工具但它更大的价值在持续集成里。你只需要知道一个关键的参数--warnings_only加上它之后Lizard 只输出超阈值的函数并且在“有任何一个函数超阈值”的情况下返回非零退出码。换句话说它变成了一个可以判死 commit 的质量门禁。我通常这样用lizard src/ -l c --warnings_only -C 10 -P 8 -N 80这里-C 10是圈复杂度阈值-P 8是参数个数阈值-N 80是 NLOC 阈值。不同版本的 Lizard 参数名可能略有区别拿不准的时候先跑lizard --help确认一下。关键是借助退出码CI 里一旦返回非零说明代码里有函数跨了红线构建就该挂掉。这个规则可以很冷血但真正有效。5.2 GitLab CI 与 pre-commit 集成参考如果你用的是 GitLab CI加一个 job 非常简单。下面是我在自己项目里用过的配置片段complexity scan: stage: test script: - pip install lizard - lizard src/ --warnings_only -C 10 -P 8 -N 80 rules: - if: $CI_PIPELINE_SOURCE merge_request_event这个 job 只会在合并请求时跑避免每天动不动全量扫描浪费时间。Jenkins 里思路一样在 pipeline 里加一句sh lizard src -C 10 -w --exclude test/就行。如果不想在 CI 里装 Python 依赖也可以把 lizard 装进一个预构建镜像里job 直接调用。本地开发阶段pre-commit 也是个不错的选择。在.pre-commit-config.yaml里加一个 Lizard hook让开发者在 commit 之前就被拦一次比 CI 跑完再反馈要省几轮来回。不过我个人的使用习惯是本地不拦因为卡手只在 CI 阶段强制。这个看团队氛围有人喜欢尽早反馈有人会嫌烦可以根据实际情况调。5.3 渐进式收口老项目怎么降复杂度如果是新项目直接定死 CCN 10 没问题。但老项目动辄几千个函数如果第一天就把红线拉到 10CI 会直接红成一片团队第二天就把工具卸了。我踩过这个坑所以现在给老项目做复杂度治理都会走渐进式收口的路线。第一步先在当前代码上跑一版宽松点的扫描比如-C 20把结果保存成基线。第二步把除了基线以外的新增代码卡在-C 15旧代码暂时放行。第三步每个月把基线里最严重的 10 个函数挑出来重构同时在 CI 里把阈值缓慢下调。我们团队用了大概两个季度把 CCN 20 以上的函数从 70 多个降到十几个中间没有人返工到崩溃。复杂度治理本质上是在跟历史债打交道与其搞运动式整改不如让红线每个月悄悄收紧一格效果反而更稳。6. Lizard 的局限和我的进阶用法6.1 哪些代码容易被误判工具不是万能的。Lizard 不展开宏所以宏里面藏着的大量 if 分支它统计不到。我有一个做协议栈的同事把一坨判断逻辑全写进宏里函数表面 CCN 很干净实际展开之后吓死人。这种代码用 Lizard 就查不出来只能人工警觉宏函数如果超过二十行就值得警惕。表驱动和状态机也容易被误判。一个查表逻辑可能只是循环加数组访问CCN 低到爆但它仍然可以视线混乱、难以维护。反过来状态机的 switch case 多CCN 天然很高但这未必代表坏味道因为状态机的每个分支本质上是同一层级的并列逻辑不该按普通 if 嵌套的标准去卡。遇到这种情况我会在代码注释里标明豁免理由在 CI 命令里用--exclude跳过这类文件或目录。6.2 和其他工具怎么配合我始终觉得复杂度工具不负责“抓虫”它负责“劝退”。clang-tidy 和 Cppcheck 这类静态工具能找到空指针、内存泄漏、未定义行为它们帮助的是正确性Lizard 帮助的是可读性和可维护性。两者不是替代关系是互补关系。实际工程里我会在同一套 CI 里叠加几个检查clang-tidy 跑编译期相关警告Lizard 跑复杂度红线如果有单元测试再让 gcov 或 gcovr 看覆盖率。这三者各自回答不同的问题有没有 bug、好不好改、测没测够。把复杂度的红线绑在分支覆盖率上效果特别明显因为圈复杂度高的函数往往也是覆盖率难以提上去的地方两个指标互相作证。6.3 用 Python API 做自定义报告如果默认报告满足不了你Lizard 还能当库用。它提供了 Python API你可以写个几行脚本把扫描结果转成团队需要的形式。比如我只想让 CCN 大于等于 12 的函数输出成一条 issue 列表就可以这样写from lizard import analyze_file result analyze_file(src/order.c) for func in result.function_list: if func.cyclomatic_complexity 12: print(f{func.name} CCN{func.cyclomatic_complexity} LOC{func.nloc})有人在此基础上接了企业微信机器人CI 扫描完直接把超标函数列表发到工作群比看日志墙有效得多。还有人用它生成趋势报表把 repo 里所有函数的平均复杂度按周拉一条曲线看大家是不是在朝某个方向改善。这个能力让 Lizard 不再只是一个“查一下”的命令而是一个可以嵌入团队质量运营体系的底座。最后再分享一个我从实践里总结出来的小技巧别一上来就拿着 CCEN 10 的尺子量全世界。先跑一版宽松扫描把报告发到群里然后单独挑出 CCN 最高那个函数贴一张控制流图或者画一下嵌套关系所有人都会瞬间理解“为什么这函数改一次崩一次”。工具只是把复杂变成数字真正让代码变好的还是你决定动手删掉那个 else if 的瞬间。后面如果再接手老项目也建议先从复杂度扫描开始做起值这个时间。 SEO 优化官网定制响应式建站教育培训建站