C++代码统计工具:状态机逐字符解析注释与代码行
发布时间:2026/10/16 6:44:15 锦皓数字建站

简介这套C/C代码统计工具压缩包主要面向需要评估项目规模、代码可读性与维护性的C开发人员、项目经理及代码审计场景可用于开发进度估算、代码审查和教学演示。工具可统计代码行、注释行与空行区分单行和多行注释并支持按类或函数分类统计、计算代码密度以及对历史统计结果做趋势分析能有效辅助重构决策和代码质量监控。包内共一百零二个文件压缩后约七点零四兆字节文件构成以工程源文件及编译产物为主含九个C源文件、十一个头文件以及大量obj、sbr、pdb等中间文件另附一个可直接运行的exe可执行程序基本涵盖完整开发工程与项目文档便于二次开发或直接使用。工具源码中包括了主框架、目录树、文件列表、对话框等模块体现了MFC架构的典型组织方式。目前已有883人学习下载适合需要量化代码规模、开展代码评审或进行技术学习的中高级程序员使用。1. C 代码统计工具为什么需要它而不是一个正则脚本拿到一个多年没人碰过的遗留代码库第一件事不是跑起来而是先弄清楚它到底有多大、注释率怎么样、哪些文件早就没人动过。编辑器自带功能看单文件行数可以批量统计几百个文件就得靠专门的工具。这里说的 C 代码统计工具做的就是按文件把总行数、代码行、注释行、空行拆开再输出可读报表。CodeAnalysis 是这类工具的通用叫法核心难点不在“数行”而在“怎么判断这一行是注释还是代码”——直接用正则去匹配//会把字符串字面量里的http://误判成注释统计立刻翻车。这个源码包按状态机逐字符扫描适合接手陌生项目、代码评审前摸规模、以及做注释覆盖率盘点的人。2. 统计口径与状态机把注释、字符串、代码逐字符分开工具有没有价值看统计结果准不准。一个物理行里可能同时有代码和注释也可能跨行的块注释横在中间这些情况决定了统计工具不能靠“按行含不含某个符号”来工作。2.1 先定口径四类行的归属规则统计工具的输入是文件产出是“行”的归类。要做准先定义清楚四类行并确定优先级混合行单独统计不强行塞进代码行或注释行。否则会出现同一行在代码行和注释行里各算一次总数对不上。行类型判定条件示例空白行去掉空白字符后长度为 0\n纯注释行整行处于行注释或块注释状态// 说明或/* 多行 */纯代码行至少一个代码字符不含注释int a 1;混合行代码和注释同时存在于一行if (x) { // 判断CodeAnalysis 默认输出六个字段文件名、总行数、纯代码行、纯注释行、空白行、混合行。前三个和空白行都只统计“纯”的行混合行单独计列。这样总行数 纯代码行 纯注释行 空白行 混合行恒成立数字怎么加都对得上。常见误区是有人用正则^\s*//匹配注释行遇到块注释跨行就废掉一半还有人看“行里有没有//”来判断字符串里的http://直接误伤。所以这里直接用状态机逐字符扫状态机的核心思路是字符串和字符字面量内部遇到//或/*不能切换注释状态只有真正跳到注释状态时才识别注释。2.2 状态机核心代码先把状态枚举拿出来看。扫描器只维护五个状态代码里没有正则表达式没有复杂的回溯逻辑。enum ScanState { ST_CODE, // 普通代码状态 ST_LINE_COMMENT, // 行注释状态遇到 \n 退出 ST_BLOCK_COMMENT, // 块注释状态遇到 */ 退出 ST_STRING, // 字符串字面量 ... ST_CHAR // 字符字面量 x };主循环是单文件扫描的核心。这里不直接读文件而是接收整个文件内容字符串逐字符扫一遍。struct FileStat { std::string fileName; int totalLines 0; int codeLines 0; // 纯代码行 int commentLines 0; // 纯注释行 int blankLines 0; // 空白行 int mixedLines 0; // 混合行 }; void finalizeLine(FileStat stat, bool hasCode, bool hasComment, bool isBlank) { stat.totalLines; if (hasCode hasComment) { stat.mixedLines; return; } if (hasComment) { stat.commentLines; return; } if (hasCode) { stat.codeLines; return; } if (isBlank) { stat.blankLines; return; } } FileStat scanFile(const std::string path, const std::string content) { FileStat stat; stat.fileName path; ScanState state ST_CODE; bool lineHasCode false; bool lineHasComment false; bool lineIsBlank true; for (size_t i 0; i content.size(); i) { char c content[i]; if (c \r) continue; // 统一忽略 CR避免 CRLF 干扰 if (c \n) { finalizeLine(stat, lineHasCode, lineHasComment, lineIsBlank); lineHasCode lineHasComment false; lineIsBlank true; continue; } char next (i 1 content.size()) ? content[i 1] : \0; if (state ST_STRING || state ST_CHAR) { lineIsBlank false; lineHasCode true; if (c \\) { i; continue; } // 跳过转义字符如 \ 或 \ if ((state ST_STRING c ) || (state ST_CHAR c \)) { state ST_CODE; } continue; } switch (state) { case ST_BLOCK_COMMENT: lineHasComment true; lineIsBlank false; if (c * next /) { i; state ST_CODE; } break; case ST_LINE_COMMENT: lineHasComment true; lineIsBlank false; break; case ST_CODE: default: lineIsBlank false; lineHasCode true; if (c / next /) { i; state ST_LINE_COMMENT; lineHasComment true; } else if (c / next *) { i; state ST_BLOCK_COMMENT; lineHasComment true; } else if (c ) { state ST_STRING; } else if (c \) { state ST_CHAR; } break; } } // 文件最后一行没有换行符时也要收尾 if (lineHasCode || lineHasComment || lineIsBlank) { finalizeLine(stat, lineHasCode, lineHasComment, lineIsBlank); } return stat; }逻辑说明循环开头先处理\n换行时把当前行的标记结算一次。\r直接在开头忽略是为了兼容 Windows 保存的文件。字符串和字符态放在最前面这样http://example里的//根本不会进入注释判断。遇到反斜杠就跳过下一个字符处理\和\这两个常见转义。参数说明状态切换消耗字符的细节要注意//和/*都是两个字符所以进入相关状态后要用i再跳过一个字符块注释退出*/时同样要多跳一个字符否则/*/这种边界会判断出错。这个写法把“当前行有没有代码/注释”记在三个布尔变量里行终结时统一结算比扫到注释符号直接lineCount的口径干净得多。2.3 统计结构体与多文件汇总单个文件扫完只是开始实际使用要给它传整个目录。汇总部分把多个文件的统计结果收集到数组里再交给输出模块。std::vectorFileStat scanDirectory(const std::string root, bool recursive, const std::vectorstd::string exts, const std::vectorstd::string ignoreDirs);文件递归用后序收集先扫子目录再汇总父目录这样报表里每个子目录的统计也能对上。扫描时只在扩展名匹配时调用scanFileignoreDirs用路径前缀匹配跳过第三方目录。这个函数签名把“按什么扩展名”和“排除哪些目录”作为参数暴露方便后续命令行直接映射。到这里扫描核心就完成了。它不碰文件系统不决定输出格式只接受“路径 内容字符串”这是刻意做的解耦。后面无论输出 txt、CSV 还是 JSON都不需要再改扫描逻辑。3. 编译、参数与输出格式三个命令跑出统计报告核心逻辑写完后把它落成一个命令行工具。源码包内部按模块拆成五个文件扫一眼结构就能知道每个文件负责什么。src/ ├── main.cpp # 命令行参数解析 ├── scanner.cpp # 状态机扫描核心 ├── scanner.h ├── report.cpp # 输出模块支持 txt / csv / json └── report.h3.1 编译成命令行工具编译建议用 C17因为代码里用到std::filesystem做目录递归。如果本机编译器比较老比如 GCC 7 以下需要把目录递归改成手动opendir但主流环境直接按下面命令编就行。g -stdc17 -O2 -o codelint src/main.cpp src/scanner.cpp src/report.cpp -I src/编译成 64 位可执行文件-O2对大批量文件扫描有明显提速。扫描模块和输出模块分开编译成两个.o再链接也是常规做法这里为了演示方便直接多文件一次性编译。跑通之后如果想嵌入自己的代码分析流水线把scanner.cpp和report.cpp的接口抽出来做成静态库即可。3.2 命令行参数与常用组合这个工具的输入、过滤、输出全靠参数控制没有交互式菜单。参数不多但每个都值得实际用一遍再调。参数作用默认值-p, --path输入文件或目录必填无-r, --recursive递归扫描子目录不递归-e, --ext扩展名白名单逗号分隔.c,.h,.cpp,.hpp--ignore排除子目录逗号分隔无-f, --format输出格式txt / csv / jsontxt--no-mixed不单独区分混合行并入库代码行区分常用组合直接在 shell 里跑就行。第一个组合是日常最常用的统计整个源码目录的 C/C 文件./codelint -p ./legacy/src -r -e .c,.h -f txt第二个组合跳过第三方目录输出 JSON 格式方便后续脚本处理./codelint -p ./legacy/src -r --ignore ./legacy/3rdparty -f json参数解析里--ignore的路径用前缀匹配所以写./legacy/3rdparty时内部会把它转成绝对路径后再比对避免相对路径在不同工作目录下失效。--no-mixed这个开关是给懒人用的不关心混合行就把它直接并入代码行统计口径从六字段变五字段。3.3 输出格式与批量统计txt 格式是给人看的每行一个文件末尾给汇总。跑实际代码库时的输出长这样File: legacy/src/core.c total 482 | code 310 | comment 120 | blank 52 | mixed 14 File: legacy/src/parser.c total 2158 | code 1743 | comment 289 | blank 126 | mixed 0 Summary: files 2 | total 2640 | comment_rate 15.5%JSON 格式是给机器看的字段直接对应FileStat结构适合接入报表系统。批量跑多个项目可以写一个简单循环把每个项目的结果单独存文件for proj in projA projB projC; do ./codelint -p $proj -r -f json report-$proj.json done跑完之后用jq .[] | {file: .fileName, total: .totalLines}这类命令聚合可以直接看出哪个项目注释率最低哪些大文件混杂了大量空行。输出模块要同时维护三种格式建议在report.cpp里用一个fmt函数根据/format参数分发而不是让调用方判断格式后各写一遍逻辑。4. 避坑排查五类让统计结果翻车的边界情况统计工具最容易栽在注释与字符串的边界上。这五条是实际拆解代码库时反复踩过的坑每条都按现象、原因、解决的顺序整理出来对应的是扫描器代码里最容易出错的位置。4.1 字符串里的//被当成注释现象某一行url http://example.com/get;被整行统计成注释计算出来的代码量明显偏低。原因逐行正则或朴素扫描器看到//就认定是行注释完全不理会它是不是在字符串里。字符串里的//在 URL、Windows 路径、协议头里经常出现误报率很高。解决把字符串态放在注释态之前判断。扫描到或就先进入ST_STRING/ST_CHAR这个状态下遇到//、/*一律不切换。代码里第 29-38 行已经做了这个顺序处理修这条坑的关键是状态切换顺序别写反。4.2 块注释内部出现//导致提前切换状态现象/* 这里示例使用 // 作为分隔符 */这样的注释块在某一行开始之后状态错乱后面的代码都被当成注释处理。原因扫描器如果在ST_BLOCK_COMMENT状态下还去判断//就会把一个块注释错误地掐断切到行注释状态。这属于状态机分支优先级错误不把每个状态能处理什么定死就会翻车。解决块注释状态下只认*/这一个退出条件行注释状态下只认\n退出其它符号不切换状态。主循环里ST_BLOCK_COMMENT分支只检查*加/是刻意为之的。4.3 文件末尾没有换行符导致最后一行丢失现象同一个文件编辑器显示 500 行工具统计出来只有 499 行永远差一行。原因文件最后一行没有以\n结尾时很多基于getline的实现不会为最后一段内容触发行终结逻辑。这个文件末行常年在统计里被吞掉。解决scanFile的收尾部分要显式补一次finalizeLine。注意要用lineHasCode || lineHasComment || lineIsBlank三个条件判断是否需要补这一行否则空文件会被额外计入一行。4.4 CRLF 换行让空行统计失败现象某个 Windows 上保存的.c文件空白行被统计成代码行注释率报表明显失真。原因文件以\r\n作为换行符getline读出的行尾带一个\r。如果空白判断只看“去掉空格后长度是否为 0”\r会让空行变得“不空”。解决在扫描循环开头直接if (c \r) continue;忽略回车再处理\n。这样无论文件是 LF 还是 CRLF统一按\n切分物理行空行判断逻辑不需要关心平台换行差异。4.5 嵌套块注释与预处理指令的干扰现象某些编译器方言支持/* /* */ */这种嵌套块注释标准的扫描器在这类文件上注释行数差异巨大还有宏定义里跨行的注释块会被拆成两段。原因标准 C/C 是不允许块注释嵌套的但部分嵌入式项目编译器做了扩展。扫描器按标准实现遇到第一个*/就退出块注释态后面内容被误判成代码。预处理指令里跨行注释本身符合状态机规则但统计人员容易误以为“一行宏等于一行代码注释跨行不算”。解决在文档里明确标注“不支持嵌套块注释”这是符合标准的口径。宏定义里的注释走常规状态机逻辑不做特殊豁免。万一真需要支持嵌套把ST_BLOCK_COMMENT改成用一个int depth计数每次进入/*加一、退出*/减一但必须测试/*出现在字符串里的情况别为了支持方言又引入字符串误判。5. 进阶实验回归自检与增量统计改造工具能跑只是第一步统计结果经得起核验才有价值。我习惯给每个扫描核心配一个边界样例文件改动之后用它对拍几十秒就能确认口径有没有被破坏。5.1 用边界样例做回归自检boundary.c专门用来覆盖最容易翻车的情况字符串里的//、跨行块注释、混合行、末尾无换行。跑一遍期望值一眼就能核验。#include stdio.h // 这是行注释 char *url http://example/x; // 混合行 /* 块注释 跨行 */ int main(void) { return 0; // 行尾混合 }期望统计总行数 9纯代码行 2第 1 行和第 7 行纯注释行 3第 2、5、6 行空白行 1第 4 行混合行 2第 3、8 行。写一个几十行校验脚本或者手工对一下./codelint -p samples/boundary.c -f txt输出一眼扫过就知道状态机有没有串行。从那以后我每次改完扫描器都强制走一遍这个样例再信结果。这个习惯救了我很多次尤其是改动转义处理逻辑时字符串状态一个细节就能毁掉整个统计。5.2 扩展注释率排行与报告生成注释率低于 10% 的项目维护风险通常比较高。用 JSON 输出加jq排序能直接拉出风险文件。./codelint -p src -r -f json \ | jq [.[] | select(.totalLines 100) | {file: .fileName, rate: (.commentLines / .totalLines * 100)}] | sort_by(.rate) | .[0:10]totalLines过滤是为了排除小文件干扰排序取前十一眼看到注释率最低的文件。这一步不是工具自带功能但 JSON 输出让报表定制变得很简单通常我还会加一个avg汇总字段。5.3 扩展提交前注释率提醒把这个工具挂到 commit 前检查注释率低于阈值直接提醒。脚本写法比想象中简单#!/bin/bash ./codelint -p ./src -r -f json /tmp/comment-report.json rate$(jq [.[].commentLines] | add / ([.[].totalLines] | add) * 100 /tmp/comment-report.json) if (( $(echo $rate 15 | bc -l) )); then echo 注释率 $rate% 低于 15%提交前请补充模块头部说明 fi注意bc做浮点比较别直接if (( rate 15 ))bash 整数比较会让14.8误判成通过。这个钩子对单人小项目和团队协作用处不一样小项目防止自己偷懒不写注释大项目可以直接去掉。如果你也要做一次代码规模盘点这个工具包可以直接拿出来改把扫描核心和输出模块的接口熟悉一遍就能接进自己的流水线。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。