统计文件内容:统计一个文本文件中字符、单词、行数。
涉及的知识点
文件 I/O
FILE *
、fopen
/fclose
、fgetc
。处理 EOF 结束条件。
字符分类
<ctype.h>
提供isspace
、isalpha
等函数。通过
isspace
判断空白字符,便于单词分隔。
有限状态机
用
in_word
状态跟踪是否在单词内部,实现准确的单词计数。
命令行参数
argc
/argv
的使用,以提高程序的通用性。
错误处理
检查
fopen
是否成功,用perror
输出具体系统错误;在读写循环中也可择机检查
ferror
。
边界情况
是否把最后一行未以
\n
结尾也计作一行;空文件或只有空白字符的文件处理。
题目描述
编写一个 C 程序,统计一个文本文件中 字符数、单词数 和 行数(类似于 Unix 下的 wc
命令),要求:
从命令行接收一个文件名作为参数;
以只读模式打开该文件,字符逐个读取;
用有限状态机方法识别“单词”边界(单词由空白字符分隔);
输出统计结果并做充分的错误检查;
代码要有详细注释,并在最后总结涉及的知识点。
参考代码:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>int main(int argc, char *argv[])
{if(argc != 2){fprintf(stderr,"用法:%s<文本文件名>\n",argv[0]);return EXIT_FAILURE;}const char *filename = argv[1];FILE *fp = fopen(filename,"r");if(!fp){perror("打开文件失败");return EXIT_FAILURE;}long char_count = 0;//字符计数long word_count = 0;//单词计数long line_count = 0;//行计数int in_word = 0;//单词状态机:0=在空白外,1=在单词内int ch;//逐字符读取直到EOFwhile((ch = fgetc(fp)) != EOF){char_count++;//每输入一个字符就加1if(ch == '\n'){line_count++;//遇到换行符就算作一行}//单词边界检测if(isspace(ch)){//如果当前是空白,切换到“非单词”状态in_word = 0;}else if (!in_word){//如果当前不是空白,且之前不在单词中,这是一个新的单词的开始in_word = 1;word_count++;}}//如果最后一行没有以‘\n’结尾,也应记为一行if(char_count>0 &&ch !='\n'){line_count++;}fclose(fp);//输出结果printf("统计结果——文件:%s\n",filename);printf("行数:%ld",line_count);printf("单词数:%ld",word_count);printf("字符数:%ld",char_count);return EXIT_SUCCESS;
}
注:空格和换行符也统计在内了
代码讲解与注释
命令行参数检查
if (argc != 2) { fprintf(stderr, "用法: %s <文本文件名>\n", argv[0]); return EXIT_FAILURE; }
确保用户传入了一个文件名,否则打印使用说明并退出。
打开文件并检查
FILE *fp = fopen(filename, "r"); if (!fp) { perror("打开文件失败"); return EXIT_FAILURE; }
"r"
模式打开只读文件;perror
会打印系统错误原因(如“权限不足”)。
初始化计数器与状态
long char_count = 0, word_count = 0, line_count = 0; int in_word = 0; int ch;
in_word
用于跟踪是否处在一个单词内部:0 = 否,1 = 是。
逐字符读取
while ((ch = fgetc(fp)) != EOF) { char_count++; if (ch == '\n') line_count++; ... }
fgetc
返回下一个字符(作为int
),或到达 EOF 时返回EOF
;每读取一个字符即增加
char_count
;碰到换行符
\n
则line_count++
。
单词识别状态机
if (isspace(ch)) { in_word = 0; } else if (!in_word) { in_word = 1; word_count++; }
isspace(ch)
判断是否为空白(空格、换行、制表等);当遇到空白时,重置
in_word
;当遇到非空白且刚好从空白转入,这就是新单词的开头,
word_count++
。
文件关闭和结果输出
fclose(fp); printf("行数: %ld, 单词数: %ld, 字符数: %ld\n", line_count, word_count, char_count);
用
fclose
关闭文件流;打印最终统计结果。
下面我按每一段代码来说明它的作用和执行流程:
int main(int argc, char *argv[])
{
程序入口:
argc
保存命令行参数个数,argv
是参数字符串数组。argc
至少为 1(程序名本身)。
if (argc != 2){fprintf(stderr, "用法:%s <文本文件名>\n", argv[0]);return EXIT_FAILURE;}
参数检查:程序要求恰好一个额外参数(要统计的文件名),否则打印“用法”提示并以失败状态退出。
const char *filename = argv[1];
从 argv
中取出用户指定的文件名,保存到 filename
。
FILE *fp = fopen(filename, "r");if (!fp){perror("打开文件失败");return EXIT_FAILURE;}
打开文件:以只读模式 (
"r"
) 打开。若返回
NULL
,表示打开失败,用perror
输出系统错误信息,然后退出。
long char_count = 0; // 字符计数long word_count = 0; // 单词计数long line_count = 0; // 行计数
三个计数器,分别用于统计字符总数、单词数 和 行数。
int in_word = 0; // 单词状态机:0=不在单词中,1=在单词中int ch;
in_word
用于跟踪是否当前已经处在一个单词内部,初始为 0(不在单词中)。ch
用来存fgetc
读取到的每个字符(int
类型,以便区分EOF
)。
// 逐字符读取直到 EOFwhile ((ch = fgetc(fp)) != EOF){char_count++; // 每读取一个字符就 +1
读取循环:
fgetc(fp)
每次读取一个字符,直到文件末尾返回EOF
。字符计数:无论是什么字符,只要不是
EOF
,char_count
就加 1。
if (ch == '\n'){line_count++; // 遇到换行符就 +1 行}
行计数:每次读到换行符 '\n'
,认为是一行结束,line_count
加 1。
// 单词边界检测if (isspace(ch)){// 遇到任何空白字符(空格、制表、换行等),标记为“非单词”状态in_word = 0;}else if (!in_word){// 当前字符不是空白,且之前不在单词中// 说明这是一个新单词的开头in_word = 1;word_count++;}
单词计数:
用
isspace(ch)
判断是否空白。如果空白,就重置状态
in_word = 0
;如果不是空白且
in_word == 0
(此前不在单词里),说明遇到新单词,word_count++
并置in_word = 1
。
}// 如果最后一行没有以 '\n' 结尾,也应算作一行if (char_count > 0 && ch != '\n'){line_count++;}
末尾行处理:
循环结束后,
ch
已经是EOF
(不是'\n'
)。如果文件非空且最后一个读到的字符不是换行,那么最后一行没有被前面的
if (ch=='\n')
统计过,就要再加 1 行。
fclose(fp);
关闭文件,释放资源。
// 输出结果printf("统计结果——文件:%s\n", filename);printf("行数:%ld\n", line_count);printf("单词数:%ld\n", word_count);printf("字符数:%ld\n", char_count);return EXIT_SUCCESS;
}
打印统计:按照行、单词、字符的顺序输出,并以
EXIT_SUCCESS
正常结束程序。
涉及知识点
命令行参数:
argc
/argv
的使用。文件 I/O:
fopen
、fgetc
、fclose
以及EOF
判断。字符分类:
isspace
判断空白字符。有限状态机:用
in_word
控制单词边界检测。行尾处理:对不以
'\n'
结尾的最后一行进行单独统计。
难点讲解:
下面一行一行来拆解这些最常见的命令行参数处理写法:
int main(int argc, char *argv[])
main
函数的签名int main(...)
表示程序的入口函数,返回一个整数给操作系统(0
常表示成功,非0
表示失败)。argc
、argv
用来接收从命令行传给程序的参数。
argc
(Argument Count)类型是
int
,表示“参数个数”。它至少为 1,因为命令行最少有一个参数:程序本身的名字。
例如当你在命令行输入
./stats myfile.txt
那么
argc == 2
第 1 个参数
argv[0]
是"./stats"
(程序名)第 2 个参数
argv[1]
是"myfile.txt"
(用户输入的文件名)
argv
(Argument Values)类型是
char *argv[]
,也就是“字符串指针数组”。argv[i]
(i
是下标,从 0 开始)是第i
个参数的 C 字符串指针。具体:
argv[0]
总是程序自身的名称或路径。argv[1]
、argv[2]
…依次是用户在命令行输入的各个参数。
为什么用
[]
下标?argv
是一个数组名,argv[i]
就是数组索引操作,取第i
项。中括号里的 “0” 和 “1” 就是索引:
argv[0]
→ 第 0 项 → 程序名argv[1]
→ 第 1 项 → 第一个用户参数
if (argc != 2)fprintf(stderr, "用法:%s <文本文件名>\n", argv[0]);
if (argc != 2)
我们的程序设计成必须带一个参数(文件名)才能正常工作。
如果
argc
不是 2(即没有给出,或者给了过多的参数),就进入if
里报错并退出。
fprintf(stderr, "用法:%s <文本文件名>\n", argv[0]);
stderr
是“标准错误输出”,用于打印错误或帮助信息。格式串里的
%s
会被argv[0]
替换,也就是程序名。这样用户看到的提示就会是:
用法:./stats <文本文件名>
无论程序是怎么被调用的,提示都会自动显示正确的可执行文件名。
const char *filename = argv[1];
filename
就指向了用户在命令行中输入的第一个真正参数(文件名),类型是const char *
,意味着“只读的 C 字符串指针”。后续你就可以用
fopen(filename, "r")
来打开这个文件,而不用再写argv[1]
,让代码更具可读性。
小结
argc
:参数总数,包括程序本身名字。argv
:参数值数组,argv[0]
是程序名,argv[1]
…是用户实际给的参数。if (argc != expected)
:常用来校验用户是否按预期提供了足够(也不太多)的参数。argv[i]
:通过数组下标取出第i
个参数,下标从 0 开始。