Day19 C 语言标准 IO 机制

发布于:2025-08-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

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,  // 年份需加上1900
                ptm->tm_mon + 1,      // 月份从0开始,需+1
                ptm->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/fread read()/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);                   // 写入stdout
    return 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的机制。

网站公告

今日签到

点亮在社区的每一天
去签到