云计算百科
云计算领域专业知识百科平台

C 语言迷你日志系统:标准 IO 实现日志查询、筛选与统计 | 附核心知识点详解

文章目录

  • 前言
  • 一、项目整体架构与主函数设计
    • 1.1 命令行参数 argc/argv
    • 1.2 main 函数分发逻辑
  • 二、标准 IO vs 系统调用:为什么 fgets 简化了行读取
    • 2.1 原生 read 的痛点:半行截断问题
    • 2.2 fgets 的优势:原生支持按行读取
    • 2.3 两套 API 对应关系
  • 三、核心字符串函数详解
    • 3.1 strcmp:字符串相等判断
    • 3.2 strstr:子串查找
    • 3.3 strtok:字符串分割
  • 四、三大功能模块完整实现
    • 4.1 日志列表展示 log_list
    • 4.2 按等级筛选日志 log_find
    • 4.3 日志等级统计 log_count
  • 五、高频踩坑点总结
  • 六、运行效果演示
  • 总结

前言

在上一篇中我们基于 open/write 系统调用完成了日志写入功能,本篇我们继续完善日志系统的查询、筛选、统计三大核心功能。 很多同学会困惑:为什么手写 read 读取日志要写复杂的 leftover 残留缓冲区?换成 C 标准 IO 的 fgets 为什么就能大幅简化代码?本文会从底层原理到代码实现一步步拆解,同时梳理 strtok/strcmp/strstr 等高频字符串函数的用法与坑点。


一、项目整体架构与主函数设计

整个日志程序通过命令行参数驱动,支持 write/list/find/count/help 五个子命令,主函数负责参数校验与功能分发,是整个程序的入口调度中心。

1.1 命令行参数 argc/argv

C 语言主函数的两个参数是命令行编程的基础:

int main(int argc, char* argv[])

  • argc:参数总个数,程序名本身算第 0 个参数
  • argv:字符串指针数组,每个元素对应一个命令行参数
    • argv[0]:程序本身路径,如 ./minilog
    • argv[1]:子命令,如 write/list/find
    • argv[2]/argv[3]:子命令的附加参数

举个例子,执行 ./minilog write INFO "system start":

  • argc = 4
  • argv[0] = "./minilog",argv[1] = "write",argv[2] = "INFO",argv[3] = "system start"

1.2 main 函数分发逻辑

主函数采用「先校验参数,再分支匹配命令」的结构,用 strcmp 匹配用户输入的子命令,分发到对应功能函数:

int main(int argc, char* argv[])
{
// 参数过少:打印帮助退出
if (argc < 2) {
fprintf(stderr, "Error: missing command\\n");
print_help();
return 1;
}
const char* cmd = argv[1];

// 写日志命令
if (0 == strcmp(cmd, "write")) {
if (argc != 4) {
fprintf(stderr, "Error: bad arguments for write\\n");
return 1;
}
return do_write(argv) == 0 ? 0 : 1;
}
// 查看全部日志
else if (0 == strcmp(cmd, "list")) {
return log_list(FILENAME) == 0 ? 0 : 1;
}
// 按等级筛选日志
else if (0 == strcmp(cmd, "find")) {
if (argc != 3) {
fprintf(stderr, "Error: missing level for find\\n");
return 1;
}
return log_find(FILENAME, argv[2]) == 0 ? 0 : 1;
}
// 统计日志数量
else if (0 == strcmp(cmd, "count")) {
return log_count(FILENAME) == 0 ? 0 : 1;
}
// 打印帮助
else if (0 == strcmp(cmd, "help")) {
print_help();
return 0;
}
// 未知命令
else {
fprintf(stderr, "Error: unknown command '%s'\\n", cmd);
print_help();
return 1;
}
}

设计细节:

  • 常量左写 0 == strcmp(…):避免手误写成 = 赋值导致的隐蔽 bug
  • 统一返回值规范:函数返回 0 代表成功,非 0 代表失败,主函数透传返回值给操作系统
  • 提前校验参数:每个子命令先检查参数个数是否合法,不合法直接报错退出

二、标准 IO vs 系统调用:为什么 fgets 简化了行读取

2.1 原生 read 的痛点:半行截断问题

read 是 POSIX 系统调用,只认字节、不认换行符,每次按固定大小的字节块读取数据。 如果一行日志刚好跨越两次 read 的边界,就会出现「一行日志被切成两半」的情况,必须手动维护 leftover 残留缓冲区保存半行数据,下一次读取再拼接成完整行,代码复杂且容易出 bug。 典型场景:

  • 文件末尾最后一行没有换行符,会直接丢失
  • 单次读取包含多行后,末尾剩余的半行会被截断丢弃

2.2 fgets 的优势:原生支持按行读取

fgets 是 C 标准库 <stdio.h> 提供的函数,底层依然调用 read,但库内部已经帮我们实现了缓冲区和半行拼接逻辑,我们直接就能拿到完整的一行:

char *fgets(char *s, int size, FILE *stream);

  • 遇到 \\n 或读到文件末尾就停止,自动把换行符存入缓冲区
  • 自动在末尾补 \\0,得到合法的 C 字符串
  • 返回 NULL 代表读到文件末尾或读取失败

2.3 两套 API 对应关系

功能POSIX 系统调用C 标准 IO 库
打开文件 open() fopen()
读取数据 read() fgets()/fread()
写入数据 write() fwrite()/printf()
关闭文件 close() fclose()
句柄类型 int fd 文件描述符 FILE* 文件流指针

注意:两套 API 绝对不能混用,不能用 fopen 打开却用 close 关闭。

三、核心字符串函数详解

日志解析的核心是字符串处理,三个函数是高频必用工具,我们逐个拆解用法和避坑点。

3.1 strcmp:字符串相等判断

int strcmp(const char *s1, const char *s2);

  • 功能:逐字节比较两个字符串,返回值为 0 代表完全相等
  • 用途:判断命令是否匹配、判断日志等级是否匹配
  • 注意:只能比较以 \\0 结尾的合法 C 字符串

3.2 strstr:子串查找

char *strstr(const char *haystack, const char *needle);

  • 功能:在大字符串中查找是否包含目标子串
  • 返回值:找到返回子串起始地址,找不到返回 NULL
  • 用途:快速判断一行日志中是否包含某个等级标签

// 判断line中是否包含[INFO]
if (strstr(line, "[INFO]") != NULL) {
printf("%s", line);
}

3.3 strtok:字符串分割

char *strtok(char *str, const char *delim);

  • 功能:按指定分隔符切割字符串,第一次传原串,后续传 NULL
  • 用途:把一行日志按空格拆分成「日期、时间、等级、内容」多个字段
  • ⚠️ 重大坑点:strtok 会修改原字符串,把分隔符替换成 \\0,切割后原串会被破坏

示例:拆分一行日志

char line[] = "[2026-08-24 10:30:15] [INFO] system start";
char *args[5] = {NULL};
args[0] = strtok(line, " "); // 第一次调用传原串
for (int i = 1; i < 5; i++) {
args[i] = strtok(NULL, " "); // 后续调用传NULL
}
// args[0] = "[2026-08-24"
// args[1] = "10:30:15]"
// args[2] = "[INFO]"
// args[3] = "system"

四、三大功能模块完整实现

我们统一采用 fopen + fgets + 死循环while(1) 的写法,对齐标准代码风格。

4.1 日志列表展示 log_list

最简单的功能,逐行读取文件直接输出即可:

int log_list(const char* filename)
{
FILE *fp = fopen(filename, "r");
if (NULL == fp) {
perror("log_list fopen error");
return -1;
}
char line_buf[512];
while (1) {
if (NULL == fgets(line_buf, sizeof(line_buf), fp)) {
break;
}
// fgets自带换行符,不用额外加\\n
printf("%s", line_buf);
}

fclose(fp);
return 0;
}

4.2 按等级筛选日志 log_find

这里有个关键细节:不能直接对原行用 strtok 拆分,否则原字符串被破坏,打印会残缺。我们用临时副本做拆分判断,原行保留完整内容用于输出。

int log_find(const char* filename, const char* level)
{
char target[32] = {0};
sprintf(target, "[%s]", level);
// 等级合法性校验
if (0 != strcmp(target, "[DEBUG]") &amp;&amp;
0 != strcmp(target, "[INFO]") &amp;&amp;
0 != strcmp(target, "[WARN]") &amp;&amp;
0 != strcmp(target, "[ERROR]"))
{
fprintf(stderr, "Error: invalid log level '%s'\\n", level);
return -1;
}

FILE *fp = fopen(filename, "r");
if (NULL == fp) {
perror("log_find fopen error");
return -1;
}

int i = 0;
while (1) {
char *args[5] = {NULL};
char line_buf[512] = {0};
char tmp_buf[512] = {0}; // 副本:专门用于strtok拆分

if (NULL == fgets(line_buf, sizeof(line_buf), fp)) {
break;
}

// 拷贝副本,拆分不影响原行
strcpy(tmp_buf, line_buf);
args[0] = strtok(tmp_buf, " ");
for (i = 1; i &lt; 5; i++) {
args[i] = strtok(NULL, " ");
}

// 等级匹配则打印原完整行
if (0 == strcmp(args[2], target)) {
printf("%s", line_buf);
}
}

fclose(fp);
return 0;
}

4.3 日志等级统计 log_count

统计功能只需要提取等级,不需要保留原行内容,所以可以直接拆分原字符串:

int log_count(const char* filename)
{
FILE *fp = fopen(filename, "r");
if (NULL == fp) {
perror("log_count fopen error");
return -1;
}
int count[4] = {0}; // 0:DEBUG 1:INFO 2:WARN 3:ERROR
int i = 0;

while (1) {
char *args[5] = {NULL};
char line_buf[512] = {0};

if (NULL == fgets(line_buf, sizeof(line_buf), fp)) {
break;
}

// 按空格拆分字段
args[0] = strtok(line_buf, " ");
for (i = 1; i &lt; 5; i++) {
args[i] = strtok(NULL, " ");
}

// 匹配等级并计数
if (0 == strcmp(args[2], "[DEBUG]")) count[0]++;
else if (0 == strcmp(args[2], "[INFO]")) count[1]++;
else if (0 == strcmp(args[2], "[WARN]")) count[2]++;
else if (0 == strcmp(args[2], "[ERROR]")) count[3]++;
}

fclose(fp);

// 格式化输出
printf("Debug:%d\\n", count[0]);
printf("INFO:%d\\n", count[1]);
printf("WARN:%d\\n", count[2]);
printf("ERROR:%d\\n", count[3]);
printf("Total:%d\\n", count[0] + count[1] + count[2] + count[3]);

return 0;
}

五、高频踩坑点总结

  • perror 不能乱用perror 只能用于系统调用 / 标准库函数失败后的错误打印(配合 errno),普通参数错误应该用 fprintf(stderr, …),否则会打印无意义的系统错误信息。
  • fgets 自带换行符fgets 会把行尾的 \\n 一起读进缓冲区,打印时不要再加 \\n,否则会出现双重空行。
  • strtok 破坏原字符串 如果拆分后还需要使用原字符串,一定要先拷贝一份副本再拆分。
  • 硬编码路径统一用宏 日志文件路径不要散写在各个函数里,在头文件定义 #define FILENAME "./logs/app.log",改路径只需要改一处。
  • 死循环写法等价性while(1) + if(…) break 和 while(条件) 逻辑完全等价,只是写法不同,死循环形式更便于在循环中间做退出判断。
  • 六、运行效果演示

    # 写入几条测试日志
    ./minilog write INFO "system start"
    ./minilog write DEBUG "init config"
    ./minilog write ERROR "disk full"
    ./minilog write WARN "memory low"
    查看全部日志
    ./minilog list
    只看ERROR等级日志
    ./minilog find ERROR
    统计各等级数量
    ./minilog count

    总结

    本篇我们从主函数命令行解析入手,对比了系统调用与标准 IO 的读取差异,完整实现了日志的查看、筛选、统计三大功能,核心收获的知识点:

  • 命令行参数 argc/argv 的使用与主函数分发设计
  • fopen/fgets/fclose 标准 IO 的用法,以及它解决了 read 的半行截断痛点
  • strcmp/strstr/tok 三个核心字符串函数的用法与坑点
  • 死循环 while(1) 的编程写法,以及模块化的代码设计思想
  • 常见的代码规范与避坑点
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » C 语言迷你日志系统:标准 IO 实现日志查询、筛选与统计 | 附核心知识点详解
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!