当前位置: 首页 > news >正文

Day19 C 语言标准 IO 机制

day19 C 语言标准 IO 机制

题目要求与程序实现

编写一个C程序,持续向文件 test.txt 中追加写入带序号和时间戳的日志行,每秒一行。程序需支持:

  • 追加写入,不覆盖原有内容
  • 序号自动接续上次最后的编号
  • Ctrl+C 可中断程序
  • 再次运行时能正确读取已有行数并继续编号

完整代码实现(含注释)

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>/*** 函数:get_line* 功能:统计文件当前总行数* 参数:FILE *fp - 文件指针* 返回值:文件行数* 说明:使用 fgets 逐行读取,通过判断每行末尾是否为换行符 '\n' 来计数*/
int get_line(FILE *fp)
{char buf[1024];int cnt = 0;// 重置文件位置到开头fseek(fp, 0, SEEK_SET);while (fgets(buf, sizeof(buf), fp) != NULL){// 判断读取的字符串是否以换行符结尾(说明是一整行)if (buf[strlen(buf) - 1] == '\n')cnt++;}return cnt;
}/*** 函数:do_log* 功能:循环写入日志数据* 参数:FILE *fp - 文件指针* 说明:*   1. 先调用 get_line 获取当前文件行数,作为起始序号*   2. 每隔1秒写入一行 "序号, YYYY-MM-DD HH:MM:SS"*   3. 使用 time() 和 localtime() 获取本地时间*   4. 调用 fflush 强制刷新缓冲区,确保数据立即写入磁盘*/
void do_log(FILE *fp)
{// 1. 统计当前文件已有行数,决定起始序号int line = get_line(fp);printf("lines = %d\n", line);// 2. 开始无限循环写入日志time_t t;while (1){time(&t);  // 获取当前时间(UTC秒数)struct tm *ptm = localtime(&t);  // 转换为本地时间结构体// 格式化输出到标准输出和文件fprintf(stdout, "%d,%04d-%02d-%02d %02d:%02d:%02d\n",line,ptm->tm_year + 1900,  // 年份需加上1900ptm->tm_mon + 1,      // 月份从0开始,需+1ptm->tm_mday,         // 日期ptm->tm_hour,         // 小时ptm->tm_min,          // 分钟ptm->tm_sec);         // 秒fprintf(fp, "%d,%04d-%02d-%02d %02d:%02d:%02d\n",line,ptm->tm_year + 1900,ptm->tm_mon + 1,ptm->tm_mday,ptm->tm_hour,ptm->tm_min,ptm->tm_sec);fflush(fp);  // 强制刷新文件缓冲区,确保写入磁盘line++;      // 序号递增sleep(1);    // 程序休眠1秒}
}/*** 主函数* 功能:解析命令行参数,打开文件,启动日志写入* 参数:argc - 参数个数,argv - 参数数组* 运行方式:./a.out test.txt*/
int main(int argc, const char *argv[])
{if (argc != 2){printf("Usage: %s <filename>\n", argv[0]);return -1;}// 以 "a+" 模式打开文件:可读可写,追加写入,文件不存在则创建FILE *fp = fopen(argv[1], "a+");if (fp == NULL){perror("fopen fail");return -1;}do_log(fp);  // 启动日志写入循环fclose(fp);  // 关闭文件(实际不会执行到此处,因循环无限)return 0;
}

理想运行结果示例

首次运行程序后,test.txt 内容如下:

1,2007-07-30 15:16:42
2,2007-07-30 15:16:43
3,2007-07-30 15:16:44

中断后再次运行,内容追加为:

1,2007-07-30 15:16:42
2,2007-07-30 15:16:43
3,2007-07-30 15:16:44
4,2007-07-30 15:19:02
5,2007-07-30 15:19:03
6,2007-07-30 15:19:04

提示:程序会持续输出类似内容,直到用户按下 Ctrl+C 中断。


标准IO机制深入解析

用户空间与内核空间交互模型

[用户空间]printf("hello world!\n");|    //缓存 --> ["hello world!\n"] //|
---------[系统调用]---------------------------------
[内核空间]|----->[屏幕]
----------------------------------------------------

标准IO基本概念

  • 标准输入(stdin):默认设备为键盘 /dev/input
  • 标准输出(stdout):默认设备为显示器
  • 标准错误(stderr):用于输出错误信息

在Linux中,所有IO操作本质上都是对文件的操作。标准IO是对底层文件IO的封装,具有良好的可移植性。


标准IO的优势与原理

标准IO库由Dennis Ritchie于1975年编写,基于Mike Lesk的可移植IO库改进而来。50年来基本未变,稳定可靠。

标准IO处理的关键细节:
  1. 自动管理缓冲区分配
  2. 优化读写块长度
  3. 封装系统调用,内部使用文件描述符

优点:使用方便,无需手动优化块大小
⚠️ 注意:不同系统实现可能存在差异,不能完全保证跨平台兼容性


缓冲机制分类

类型缓冲大小典型用途刷新条件
行缓冲1KB终端交互(stdout)换行 \n、缓冲区满、程序结束、fflush
全缓冲4KB普通文件读写缓冲区满、程序结束、fflush
无缓冲0KB错误输出(stderr)直接输出,不缓存

设计原则

  • 与终端关联 → 行缓冲
  • 普通文件 → 全缓冲
  • 错误处理 → 无缓冲

缓冲区信息查看代码

#include <stdio.h>int main(void)
{// 输出标准流的文件描述符printf("stdin fileno = %d\n", stdin->_fileno);printf("stdout fileno = %d\n", stdout->_fileno);printf("stderr fileno = %d\n", stderr->_fileno);getchar();  // 等待用户输入// 输出各标准流缓冲区大小printf("stdin buffer size = %ld\n", stdin->_IO_buf_end - stdin->_IO_buf_base);printf("stdout buffer size = %ld\n", stdout->_IO_buf_end - stdout->_IO_buf_base);printf("stderr buffer size = %ld\n", stderr->_IO_buf_end - stderr->_IO_buf_base);return 0;
}

理想输出示例

stdin fileno = 0
stdout fileno = 1
stderr fileno = 2
stdin buffer size = 1024
stdout buffer size = 1024
stderr buffer size = 0

文件定位函数

fseek — 定位文件指针
int fseek(FILE *stream, long offset, int whence);
  • 功能:将文件指针定位到指定位置
  • 参数
    • stream:文件指针
    • offset:偏移量(可正可负)
    • whence:参考点(SEEK_SET, SEEK_CUR, SEEK_END
  • 返回值:成功返回0,失败返回-1并设置 errno

示例

  • fseek(fp, 0, SEEK_SET); → 定位到文件开头
  • fseek(fp, 0, SEEK_END); → 定位到文件末尾
  • fseek(fp, 100, SEEK_SET); → 从头偏移100字节

注意:允许偏移超出文件范围,形成“空洞”,但需一次写操作才能真正扩展文件。


创建空洞文件示例
#include <stdio.h>int main(void)
{FILE *fp = fopen("hole.txt", "w");if (fp == NULL){perror("fopen fail");return -1;}int n = 0;scanf("%d", &n);fseek(fp, n - 1, SEEK_SET);  // 定位到目标位置前一个字节fputc('\0', fp);              // 写入一个空字符,创建空洞fclose(fp);return 0;
}

效果:创建大小为 n 字节的空洞文件,中间填充 \0


模拟云盘下载:复制文件并创建空洞

#include <stdio.h>int main(int argc, const char* argv[])
{if (argc != 3){printf("Usage: %s <src> <dest>\n", argv[0]);return -1;}FILE *fp_s = fopen(argv[1], "r");FILE *fp_d = fopen(argv[2], "w");if (fp_s == NULL || fp_d == NULL){perror("fopen fail");return -1;}// 获取源文件大小fseek(fp_s, 0, SEEK_END);long len = ftell(fp_s);printf("len = %ld\n", len);// 创建同大小空洞文件fseek(fp_d, len - 1, SEEK_SET);fputc('\0', fp_d);fflush(fp_d);// 重置指针,开始复制rewind(fp_s);rewind(fp_d);int ret = 0;while ((ret = fgetc(fp_s)) != EOF){fputc(ret, fp_d);}fclose(fp_s);fclose(fp_d);return 0;
}

理想效果:成功复制文件,目标文件大小与源一致,内容完全相同。


标准IO vs 文件IO(系统调用)

对比项标准IO(库函数)文件IO(系统调用)
操作对象FILE * 流指针文件描述符 int fd
打开函数fopen()open()
读写函数fgetc/fgets/freadread()/write()
关闭函数fclose()close()
定位函数fseek()/ftell()/rewind()lseek()
缓冲机制有(行/全/无缓冲)无(直接系统调用)
可移植性依赖系统
性能高效(减少系统调用)低效(每次调用都进内核)

关系:标准IO库函数最终调用系统调用来实现功能。


open 系统调用详解

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • flags
    • 必选:O_RDONLY, O_WRONLY, O_RDWR
    • 可选:O_APPEND, O_CREAT, O_TRUNC
  • mode:创建文件权限(如 0666),仅在含 O_CREAT 时需要
  • 返回值:成功返回文件描述符(>=3),失败返回-1

等价关系

  • fopen("1.txt","r")open("1.txt", O_RDONLY)
  • fopen("1.txt","w")open("1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666)

文件权限与 umask

实际文件权限 = mode & ~umask

例如:

mode:  0666 (rw-rw-rw-)
umask: 0022 (----w--w-)
~umask:111101101
结果:0644 (rw-r--r--)

readwrite 系统调用

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
  • read:从文件描述符读取数据到缓冲区
  • write:将缓冲区数据写入文件描述符

注意read 读取的数据若用于字符串处理,需手动添加 \0 结尾。


使用 read/write 实现 cat 功能

#include<stdio.h>
#include<unistd.h>int main(void)
{char buf[1024];int ret = read(0, buf, sizeof(buf));  // 从stdin读write(1, buf, ret);                   // 写入stdoutreturn 0;
}

运行示例

$ ./a.out
hello
ret = 6 buf = hello

lseek 定位文件偏移

off_t lseek(int fd, off_t offset, int whence);
  • 功能:移动文件读写位置
  • 常用操作
    • lseek(fd, 0, SEEK_SET); → 定位开头
    • lseek(fd, 0, SEEK_END); → 定位末尾
    • off_t len = lseek(fd, 0, SEEK_END); → 获取文件大小

文件描述符与流指针转换

  • int fileno(FILE *stream); → 将 FILE* 转为 fd
  • FILE *fdopen(int fd, const char *mode); → 将 fd 转为 FILE*
int fd = open("1.txt", O_WRONLY);
FILE *fp = fdopen(fd, "w");  // 关联FILE*指针

BMP图像合并示例(作业)

#include<stdio.h>
#include<stdlib.h>int main(void)
{FILE *fp1 = fopen("1.bmp","r");FILE *fp2 = fopen("2.bmp","r");FILE *fp3 = fopen("3.bmp","w");if(fp1==NULL || fp2==NULL || fp3==NULL){printf("No open bmp!\n");return -1;}char head[54];fread(head,1,54,fp1);           // 读取BMP头fwrite(head,1,54,fp3);          // 写入新文件头fseek(fp2,54,SEEK_SET);         // 跳过第二张图头for(int i=0; i<600; ++i){char row1[2400], row2[2400], row3[2400];fread(row1,1,2400,fp1);fread(row2,1,2400,fp2);for(int j=0; j<800; ++j){int idx = j*3;char r1=row1[idx], g1=row1[idx+1], b1=row1[idx+2];char r2=row2[idx], g2=row2[idx+1], b2=row2[idx+2];if(r1==r2 && g1==g2 && b1==b2){row3[idx] = r1; row3[idx+1] = g1; row3[idx+2] = b1;}else{row3[idx] = (r1+r2)/2;row3[idx+1] = (g1+g2)/2;row3[idx+2] = (b1+b2)/2;}}fwrite(row3,1,2400,fp3);}fclose(fp1); fclose(fp2); fclose(fp3);printf("Output file:3.bmp\n");return 0;
}

功能:合并两张600×800的BMP图片,相同像素保留,不同则取平均值。


在这里插入图片描述
:这是 标准IO(C语言标准输入输出库)在 Linux 系统中的工作原理示意图,涉及以下实体和概念:

  • 标准IO组件:stdin(标准输入)、stdout(标准输出)、stderr(标准错误输出 ),通过FILE *fp 指针操作,对应结构体 struct IO_FILE,包含缓存、文件描述符(fileNo ,关联内核文件操作)。
  • 空间划分:分用户空间、内核空间,标准IO在用户空间,通过“系统调用”和内核交互。
  • 内核文件管理结构:
    • 文件描述符表:用 fd(如 0、1、2 分别对应标准输入、输出、错误)索引,关联文件操作。
    • 文件表项:存文件状态标志、当前偏移量、v - node 指针、引用计数等,管理文件读写状态。
    • vnode 节点:含 v - node、i - node 节点信息、文件长度等,最终关联磁盘文件,是用户操作和实际存储的桥梁 。
      整体展示了标准IO从用户调用到内核操作文件(磁盘)的流程,体现缓存、系统调用封装、多结构协作管理文件IO的机制。
http://www.lryc.cn/news/620080.html

相关文章:

  • React useMemo 深度指南:原理、误区、实战与 2025 最佳实践
  • React常见的Hooks
  • 万字详解C++11列表初始化与移动语义
  • OpenCV的实际应用
  • 类和对象----中
  • 【COMSOL】Comsol学习案例时的心得记录分享
  • Mysql数据库迁移到GaussDB注意事项
  • pycharm配置连接服务器
  • 3.Cursor提效应用场景实战
  • MySQL相关概念和易错知识点(6)(视图、用户管理)
  • 大厂语音合成成本深度对比:微软 / 阿里 / 腾讯 / 火山 API 计费拆解与技术选型指南
  • trace分析之查找点击事件
  • cisco无线WLC flexconnect配置
  • python类--python011
  • 数仓建模理论-数据域和主题域
  • 8.13服务器安全检测技术和防御技术
  • 免费生成视频,Coze扣子工作流完全免费的视频生成方案,实现图生视频、文生视频
  • [ Mybatis 多表关联查询 ] resultMap
  • LeetCode Day5 -- 二叉树
  • 使用 HTML5 Canvas 打造炫酷的数字时钟动画
  • Kubernetes-03:Service
  • 对线面试官之幂等和去重
  • 【OpenGL】LearnOpenGL学习笔记07 - 摄像机
  • 会议征稿!IOP出版|第二届人工智能、光电子学与光学技术国际研讨会(AIOT2025)
  • 【Android】RecyclerView多布局展示案例
  • [系统架构设计师]架构设计专业知识(二)
  • Linux 计划任务
  • 《书写范式》——代码如诗,诗娟代码(Python)(附精巧“九九表”生成代码)
  • Coze Studio 概览(十)--文档处理详细分析
  • k8s资源管理