本文档整理了笔记中所有思考类问题实操类练习 / 作业题,按标准 IO 基础标准 IO 实操系统 IO 基础综合作业分类,每个问题给出核心解答+代码实现 / 原理分析,贴合 Linux C 编程的实际应用场景。

一、标准 IO 基础思考问题

问题 1:fopen 函数的返回值是指向 FILE 类型的指针,请问 FILE 类型是什么?

解法 / 解答

FILE 类型是C 标准库定义的结构体数据类型,其本质是对文件操作的封装,包含了标准 IO 库管理文件所需的所有核心信息,存储在堆内存中。核心成员包括:

  • 文件描述符_fileno:关联内核的系统 IO 文件描述符;
  • 缓冲区相关指针:_IO_read_ptr/_IO_write_ptr等,管理标准 IO 的输入输出缓冲区;
  • 状态标志:_flags(错误标志、结束标志等);
  • 链表指针chain:将多个打开的 FILE 结构体串联成链表,内核通过链表管理所有打开的文件流;
  • 位置偏移量_old_offset:记录文件当前的读写位置。补充:stdio.h 中仅声明 FILE 类型,实际定义在 Linux 系统的/usr/include/libio.h中,结构体名为struct _IO_FILE

问题 2:stdio.h 中无 FILE 结构体的定义,那该结构体的成员有哪些?

解法 / 解答
  • 查找路径:stdio.h 中通过条件编译包含了libio.h头文件,FILE 结构体的实际定义在 libio.h 中(路径:/usr/include/libio.h);
  • 核心成员(关键必记):
成员名 作用
_flags 文件流的状态标志(错误 / 结束)
_IO_read_ptr 缓冲区当前读指针
_IO_write_ptr 缓冲区当前写指针
_fileno 关联的内核文件描述符
chain 链表指针,指向下一个 FILE
_old_offset 文件读写位置偏移量

问题 3:为什么内核为文件流申请内存时要申请堆内存?

解法 / 解答

核心原因是堆内存的生命周期不受函数作用域限制,满足文件流的使用需求,具体依据:

  • 作用域需求:fopen 函数调用后,文件流(FILE 结构体)需要在整个程序生命周期内有效,直到调用 fclose 手动释放;若申请栈内存,函数执行完毕后栈内存会被回收,导致文件流指针成为野指针;
  • 动态管理需求:程序中打开的文件数量是动态变化的,堆内存支持按需申请 / 释放(fopen 申请、fclose 释放),而栈内存大小固定,无法灵活管理多个文件流;
  • 内核管理需求:内核通过链表管理所有打开的文件流,堆内存的地址稳定,可通过链表指针chain安全串联,栈内存地址随函数调用变化,无法实现稳定的链表管理。

问题 4:多次调用 fclose 关闭同一个文件流,是否会有影响?

解法 / 解答

会产生严重影响,直接导致程序段错误(Segment Fault),原因:

  • fopen 打开文件时,内核从堆内存中申请一块空间存储 FILE 结构体;
  • 第一次调用 fclose 时,内核会释放该堆内存,并将文件流从管理链表中移除;
  • 第二次调用 fclose 时,传入的 FILE 指针已指向被释放的堆内存(野指针),此时对野指针的操作会触发堆内存重复释放,内核会终止程序并抛出段错误。补充:系统 IO 的 close 函数可多次调用,无段错误(因 open 未申请堆内存,仅操作内核资源),但多次调用无实际意义。

问题 5:fgets 读取到换行符\n时会结束?fgets 的参数 n 的意义是什么?

解法 / 解答
(1)读取到\n结束的原因

本质是标准 IO 的行缓冲机制+fgets 的按行读取设计

  • fgets 是按行读取函数,设计初衷是读取文本文件的一行数据,而文本文件中以\n作为行分隔符;
  • 标准 IO 对终端等字符设备采用行缓冲\n是行缓冲的刷新标志,fgets 识别该标志并终止读取,保证读取的数据为完整的一行。
(2)参数 n 的核心意义

n表示自定义缓冲区的最大字节数,fgets最多读取 n-1 个字符,剩余 1 个字节用于自动添加字符串结束符\0目的是防止缓冲区溢出

fgets 的终止读取条件(满足其一即停止):

① 读取到\n(保留\n在缓冲区中);

② 读取到文件末尾(EOF);

③ 已读取 n-1 个字符。

问题 6:fread 返回值小于 nmemb 时,如何区分是读取到文件末尾还是发生读写错误?

解法 / 解答

通过标准 IO 提供的两个状态判断函数结合使用,可精准区分,函数原型均为#include <stdio.h>

  1. feof(FILE *stream):判断文件流是否到达末尾(EOF)
    • 返回值:非 0 → 到达末尾;0 → 未到达末尾;
  2. ferror(FILE *stream):判断文件流是否发生读写错误
    • 返回值:非 0 → 发生错误;0 → 无错误。
size_t ret = fread(buf, 1, 1024, fp);
if (ret < 1024) {
    if (feof(fp)) {
        printf("读取到文件末尾,实际读取%zu字节\n", ret);
    } else if (ferror(fp)) {
        perror("fread读取错误");
        fclose(fp);
        exit(1);
    }
}

补充:两个函数的判断仅在读 / 写操作后有效,未执行 IO 操作时,文件流的结束 / 错误标志均为 0。

二、标准 IO 实操练习问题

练习 1:用 fgetc 打开 demo.txt,将字符逐字符输出到屏幕,读完结束

解题思路
  1. fopen("demo.txt", "r")以只读方式打开文件,判断打开是否成功;
  2. fgetc逐字符读取,循环条件为读取的字符不等于 EOF
  3. 读取一个字符则打印一个字符,读完后调用fclose关闭文件流。
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    // 1. 打开文件
    FILE *fp = fopen("demo.txt", "r");
    if (fp == NULL) {
        perror("fopen demo.txt error");
        return -1;
    }

    // 2. 逐字符读取并输出,ch用int接收(兼容EOF(-1))
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        printf("%c", ch);
    }
    printf("\n"); // 换行优化输出

    // 3. 关闭文件
    fclose(fp);
    fp = NULL; // 置空避免野指针
    return 0;
}

练习 2:用标准 IO 计算文件大小,文件名通过命令行传递,用ls -l验证

解题思路(两种方法,推荐方法 2 更高效)
方法 1:fgetc 逐字符计数(基础版)
  1. 校验命令行参数:argc 必须等于 2(程序名 + 文件名);
  2. 以 ** 二进制只读模式rb** 打开文件(避免文本模式对特殊字符的解析,保证大小准确);
  3. 用 fgetc 逐字符读取,每读一个字符计数器 + 1,直到读取到 EOF;
  4. 输出计数器值,即为文件大小(字节数)。
方法 2:fseek+ftell(高效版,推荐)
  1. 同样校验参数 + 二进制打开文件;
  2. fseek(fp, 0, SEEK_END)将文件指针移到文件末尾
  3. ftell(fp)获取当前指针相对于文件开头的偏移量,即为文件大小;
  4. 无需循环,效率远高于逐字符计数。
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    // 1. 校验命令行参数:必须传入1个文件名
    if (argc != 2) {
        fprintf(stderr, "使用方法: %s <文件名>\n", argv[0]);
        fprintf(stderr, "示例: %s demo.txt\n", argv[0]);
        exit(1);
    }

    // 2. 二进制只读打开文件,保证大小计算准确
    FILE *fp = fopen(argv[1], "rb");
    if (fp == NULL) {
        perror("fopen error");
        exit(1);
    }

    // 3. 移到文件末尾 + 获取偏移量(文件大小)
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    if (file_size == -1L) {
        perror("ftell error");
        fclose(fp);
        exit(1);
    }

    // 4. 输出结果 + 关闭文件
    printf("文件[%s]的大小: %ld 字节\n", argv[1], file_size);
    fclose(fp);
    printf("验证命令: ls -l %s\n", argv[1]); // ls -l第5列为文件字节数
    return 0;
}

练习 3:用标准 IO 实现文件拷贝,源文件 / 目标文件通过命令行传递,B 不存在则创建,验证拷贝正确性

解题思路
  1. 校验命令行参数:argc 必须等于 3(程序名 + 源文件 A + 目标文件 B);
  2. 源文件 A:** 二进制只读rb** 打开(兼容所有文件类型:文本 / 图片 / 视频);
  3. 目标文件 B:** 二进制写入wb** 打开(不存在则创建,存在则清空,兼容所有文件类型);
  4. 定义缓冲区(4KB 为宜,#define BUF_SIZE 4096),用fread+fwrite循环拷贝:
    • fread 从 A 读数据到缓冲区,返回实际读取的字节数;
    • fwrite 将缓冲区数据写入 B,严格按实际读取的字节数写入;
  5. 循环终止条件:fread 返回 0(读取到文件末尾);
  6. 验证方式:ls -l A B(对比大小)+diff A B(对比内容,无输出则一致)。
#include <stdio.h>
#include <stdlib.h>

#define BUF_SIZE 4096 // 4KB缓冲区,兼顾效率和内存

int main(int argc, char const *argv[])
{
    // 1. 校验命令行参数
    if (argc != 3) {
        fprintf(stderr, "使用方法: %s <源文件> <目标文件>\n", argv[0]);
        fprintf(stderr, "示例: %s src.txt dst.txt\n", argv[0]);
        exit(1);
    }

    FILE *src_fp = NULL, *dst_fp = NULL;
    char buf[BUF_SIZE];
    size_t read_len; // 实际读取的字节数

    // 2. 打开源文件
    src_fp = fopen(argv[1], "rb");
    if (src_fp == NULL) {
        perror("fopen 源文件 error");
        exit(1);
    }

    // 3. 打开目标文件
    dst_fp = fopen(argv[2], "wb");
    if (dst_fp == NULL) {
        perror("fopen 目标文件 error");
        fclose(src_fp); // 已打开的文件必须关闭
        exit(1);
    }

    // 4. 循环拷贝:读->写
    while ((read_len = fread(buf, 1, BUF_SIZE, src_fp)) > 0) {
        // 按实际读取的字节数写入,避免多余空数据
        size_t write_len = fwrite(buf, 1, read_len, dst_fp);
        if (write_len != read_len) {
            perror("fwrite 写入错误");
            fclose(src_fp);
            fclose(dst_fp);
            exit(1);
        }
    }

    // 5. 校验读取是否为正常结束(非错误)
    if (ferror(src_fp)) {
        perror("fread 读取源文件错误");
        fclose(src_fp);
        fclose(dst_fp);
        exit(1);
    }

    // 6. 关闭文件 + 提示验证
    fclose(src_fp);
    fclose(dst_fp);
    printf("文件拷贝成功!\n");
    printf("验证大小: ls -l %s %s\n", argv[1], argv[2]);
    printf("验证内容: diff %s %s(无输出则内容一致)\n", argv[1], argv[2]);
    return 0;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐