第二次作业

从零实现一个命令行文本统计工具:C 语言课程作业复盘

项目地址:https://gitee.com/wang01130/second-assignment
开发环境:Windows + Dev-C++ 5.11 + C99
项目功能:字符数、单词数、句子数、代码行、空行、注释行统计

前言

这次课程作业要求实现一个类似 Linux wc 的命令行文本统计程序。基础功能是统计纯英文 txt 文件中的字符数、单词数和句子数;扩展功能则要求统计代码行、空行和注释行,其中注释行必须正确识别 C 语言的 // 和 /* ... */。

一开始我以为最难的是命令行参数,真正写起来才发现,难点主要在两个地方:

  1. 句子结束符的判定不能简单地数 .、!、?;
  2. 注释统计不能只判断某一行里有没有 // 或 /*,还要处理字符串、字符常量、多行注释和代码行混排的情况。

这篇文章记录我从空项目做到 v0.3 的过程,包括设计思路、关键代码、单元测试、性能测试和一些踩坑记录。

一、需求分析

程序的使用方式如下:

wc.exe [参数] [文件名]

基础功能:

参数 功能
-c 统计字符数
-w 统计单词数
-s 统计句子数

扩展功能:

参数 功能
-l 统计代码行
-b 统计空行
-m 统计注释行

开发约束是使用纯 C 语言和 C99 标准,不引入第三方库,代码尽量简单易懂。程序一次接收一个参数和一个文件名,输出对应的统计结果。

二、项目结构

项目按功能拆分成几个部分:

text-counter\
├─ src\
│  ├─ main.c             命令行入口
│  ├─ wc.c               核心统计函数
│  ├─ wc.h               函数声明
│  ├─ wc.dev             Dev-C++ 项目
│  ├─ sample.txt         普通示例文本
│  └─ comment_sample.txt 带注释的示例文本
├─ tests\
│  ├─ test_wc.c          单元测试
│  ├─ run_tests.bat      双击运行单元测试
│  ├─ performance_test.c 性能测试
│  └─ run_performance.bat
├─ docs\
│  ├─ screenshots\       运行截图
│  └─ performance\       性能测试结果
├─ README.md
├─ AI_Prompts.md
└─ .gitignore

main.c 只负责参数解析、文件读取和结果输出;wc.c 负责真正的统计逻辑。这样拆分之后,单元测试可以直接调用 wc.c 里的函数,不需要每次都启动程序。

三、文件读取

每次统计只读取一次文件,把内容保存到一块动态内存中,然后交给不同统计函数使用:

char *wc_read_file(const char *filename)
{
    FILE *fp;
    long size;
    size_t read_count;
    char *text;

    fp = fopen(filename, "r");
    if (fp == NULL) {
        return NULL;
    }

    if (fseek(fp, 0, SEEK_END) != 0) {
        fclose(fp);
        return NULL;
    }

    size = ftell(fp);
    if (size < 0) {
        fclose(fp);
        return NULL;
    }

    rewind(fp);

    text = (char *)malloc((size_t)size + 1);
    if (text == NULL) {
        fclose(fp);
        return NULL;
    }

    read_count = fread(text, 1, (size_t)size, fp);
    text[read_count] = '\0';
    fclose(fp);

    return text;
}

这里用 fseek、ftell 得到文件大小,再用 malloc 分配缓冲区。Windows 下以文本模式打开文件时,CRLF 会被转换成 LF,所以字符数统计的是转换后的文本内容。

四、基础功能实现

4.1 字符数

字符数最简单,直接使用 strlen:

long wc_count_chars(const char *text)
{
    return (long)strlen(text);
}

空格、标点和换行都会计入字符数。

4.2 单词数

单词按空白字符分隔。用一个 in_word 标记当前是否在单词内部,遇到空白就结束当前单词,遇到非空白且之前不在单词内就增加计数:

long wc_count_words(const char *text)
{
    long count = 0;
    int in_word = 0;
    size_t i;

    for (i = 0; text[i] != '\0'; i++) {
        if (isspace((unsigned char)text[i])) {
            in_word = 0;
        } else if (!in_word) {
            count++;
            in_word = 1;
        }
    }

    return count;
}

4.3 句子数

句子统计不能只数标点,因为 ...、?! 这类连续标点会出现。我的规则是:

  • 遇到 .、!、? 时,如果前面有实际内容,就结束一句;
  • 连续的标点只算一次结束;
  • 最后一段没有标点但包含文字,也算一句;
  • 只有标点的内容不算句子。

核心代码如下:

long wc_count_sentences(const char *text)
{
    long count = 0;
    int has_content = 0;
    size_t i;

    for (i = 0; text[i] != '\0'; i++) {
        unsigned char c = (unsigned char)text[i];

        if (c == '.' || c == '!' || c == '?') {
            if (has_content) {
                count++;
                has_content = 0;
            }
        } else if (!isspace(c)) {
            has_content = 1;
        }
    }

    if (has_content) {
        count++;
    }

    return count;
}

示例文件 sample.txt 的运行结果:

Characters: 120
Words: 23
Sentences: 3

五、扩展功能:代码行、空行、注释行

5.1 三个统计口径

先明确三个概念:

类型 判定方式
代码行 这一行包含代码。代码后面带注释,也算代码行
空行 只有空白字符,没有代码,也没有注释
注释行 只有注释,没有代码

例如:

int x = 0;  // 这是代码行,不是注释行

// 这是注释行

多行注释 /* ... */ 内部的每一行都算注释行;多行注释内部的空行也算注释行,因为它仍然属于注释块。

5.2 注释识别的状态机

注释统计最难的地方是:不能看到 // 或 /* 就直接当成注释,因为字符串里也可能出现这些字符:

printf("http://example.com");
printf("/* not a comment */");

这两行都是代码行,不能把 http:// 里的 // 当成注释。

我的处理方式是逐行扫描,同时维护两个状态:

  • in_block_comment:当前是否位于多行注释内部;
  • 当前行的 has_code 和 has_comment。

扫描时大致按下面的优先级处理:

if (in_block_comment) {
    /* 在块注释中,只寻找结束标记 */
} else if (c == '/' && next == '/') {
    /* 单行注释开始 */
} else if (c == '/' && next == '*') {
    /* 多行注释开始 */
} else if (c == '"' || c == '\'') {
    /* 字符串或字符常量,跳过其中的内容 */
} else if (!isspace(c)) {
    has_code = 1;
}

几个关键细节:

  1. 字符串和字符常量要整体跳过,否则 "http://..." 会被误判为注释。
  2. 代码后面的注释仍然算代码行,只有整行都是注释时才计入注释行。
  3. 多行注释跨行保持状态,下一行开始时如果还在注释内部,先标记 has_comment = 1。
  4. 多行注释内部的空行算注释行,不算空行。
  5. 注释结束符后面的代码要重新识别,例如 */ int x; 这一行包含代码,应算代码行。

comment_sample.txt 的测试结果:

Code lines: 6
Blank lines: 1
Comment lines: 4

这个文件一共 11 行,三种类型的和为 11,说明分类没有遗漏。

六、单元测试

项目使用 C 语言自己写了一个极简测试框架,不依赖第三方库。测试文件是 tests\test_wc.c,编译后直接运行。

测试覆盖了:

  • 空文本;
  • 空文件;
  • 普通英文文本;
  • 空格、制表符、换行符;
  • 连续句末标点;
  • 没有句末标点的最后一句;
  • 代码行、空行、注释行;
  • 单行注释和多行注释;
  • 字符串中的 // 和 /* ... */;
  • 文件不存在。

运行方式:

cd /d D:\c-projects\text-counter
tests\run_tests.bat

测试结果:

34 tests run, 0 failed
All unit tests passed.

测试代码里也踩过一个坑:文件读取测试最初使用相对路径,如果不在项目根目录运行,就会找不到测试文件。后来把测试脚本固定为先 cd 到项目根目录,问题就解决了。

七、性能测试

性能测试使用 C 标准库的 clock() 函数,测试文本由重复的英文段落生成,大小约 0.82 MB,各统计函数执行 100 次。

测试命令:

cd /d D:\c-projects\text-counter
tests\run_performance.bat

测试结果:

=== wc performance test ===
Input size: 0.82 MB
Iterations: 100
CLOCKS_PER_SEC: 1000

Characters :    0.002 s  result=860000
Words      :    0.131 s  result=170000
Sentences  :    0.116 s  result=20000
Line stats :    0.131 s  result=10000

Total measured time: 0.380 s

字符统计直接使用 strlen,所以耗时非常短;单词、句子和行统计都需要逐字符扫描,耗时相对多一些。clock() 的分辨率与机器有关,这组数据只代表当前电脑上的一次运行结果。

八、版本管理与码云

项目按照作业要求拆成三个版本:

版本 内容
v0.1 空项目,完成 Dev-C++ 工程骨架
v0.2 完成 -c / -w / -s 基础功能和 24 项单元测试
v0.3 完成 -l / -b / -m 扩展功能、34 项测试和性能测试

每一版都有对应的 Git 提交和标签:

v0.1  空项目初始化
v0.2  完成基础功能
v0.3  完成扩展功能

项目中加入了 .gitignore,wc.exe、main.o、Makefile.win 这类编译产物不会提交,Dev-C++ 的 wc.dev 项目文件会保留。

码云地址:

https://gitee.com/wang01130/second-assignment

九、运行截图

基础功能:

基础功能运行截图

代码行和空行:

代码行和空行运行截图

注释行:

注释行运行截图

十、踩坑记录

1. 字符串里的 // 不是注释

最初只判断当前字符是不是 /,结果 http://example.com 被误判成注释。后来在扫描时增加了字符串和字符常量的跳过逻辑,才解决这个问题。

2. 多行注释内部的空行不是空行

从“物理空行”的角度看,/* ... */ 中间的空行确实是空行;但从代码统计的角度看,它属于注释块,应该算注释行。最后采用了“注释块内部优先算注释行”的规则。

3. 句子统计不能只数标点

Wait... what?! 里有很多标点,但只有两个句子。通过 has_content 标记和“连续标点只结束一次”的规则,才能得到正确结果。

4. 编译产物不要提交

Dev-C++ 会生成 .exe、.o、Makefile.win 等文件。如果不写 .gitignore,仓库会变得很乱。把源码、项目文件和文档提交即可。

5. 单元测试的相对路径

测试文件如果直接双击运行,工作目录不一定是项目根目录。测试脚本里先执行 cd /d "%~dp0..",再编译运行,可以避免找不到测试输入文件。

十一、总结

这个项目规模不大,但把命令行程序、文件读取、字符串扫描、状态机、单元测试和性能测试都串了一遍。最大的收获不是写出了多少个统计函数,而是意识到:

  • 统计规则必须先定义清楚,否则“字符数”“注释行”很容易产生歧义;
  • 注释识别不能靠简单的字符串查找,必须考虑上下文;
  • 测试要覆盖空输入、边界输入和错误输入;
  • 性能测试要关注算法,而不是只看程序能不能跑。

目前还有一些已知限制:

  • 只处理纯英文文本;
  • 句子统计对 Mr.、3.14 这类内容可能误判;
  • 一次只能使用一个统计参数;
  • 没有专门处理反斜杠续行等极端 C 语法。

如果后续继续改进,可以考虑支持多个参数组合、增加更多边界测试,以及把注释识别扩展为更完整的 C 词法分析。

附:项目与 AI 提示词

项目源码:

https://gitee.com/wang01130/second-assignment

README:

https://gitee.com/wang01130/second-assignment/blob/master/README.md

AI 对话提示词记录(按作业要求单独存放,没有写进 README):

https://gitee.com/wang01130/second-assignment/blob/master/AI_Prompts.md

本项目在 AI 辅助下完成,所有提示词均记录在 AI_Prompts.md 中。

posted @ 2026-10-30 12:47  水化墨生  阅读(17)  评论(0)    收藏  举报