LinuxC编程实战:标准IO与系统IO全解析
·
本文档整理了笔记中所有思考类问题和实操类练习 / 作业题,按标准 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>:
- feof(FILE *stream):判断文件流是否到达末尾(EOF)
- 返回值:非 0 → 到达末尾;0 → 未到达末尾;
- 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,将字符逐字符输出到屏幕,读完结束
解题思路
- 用
fopen("demo.txt", "r")以只读方式打开文件,判断打开是否成功; - 用
fgetc逐字符读取,循环条件为读取的字符不等于 EOF; - 读取一个字符则打印一个字符,读完后调用
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 逐字符计数(基础版)
- 校验命令行参数:argc 必须等于 2(程序名 + 文件名);
- 以 ** 二进制只读模式
rb** 打开文件(避免文本模式对特殊字符的解析,保证大小准确); - 用 fgetc 逐字符读取,每读一个字符计数器 + 1,直到读取到 EOF;
- 输出计数器值,即为文件大小(字节数)。
方法 2:fseek+ftell(高效版,推荐)
- 同样校验参数 + 二进制打开文件;
- 用
fseek(fp, 0, SEEK_END)将文件指针移到文件末尾; - 用
ftell(fp)获取当前指针相对于文件开头的偏移量,即为文件大小; - 无需循环,效率远高于逐字符计数。
#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 不存在则创建,验证拷贝正确性
解题思路
- 校验命令行参数:argc 必须等于 3(程序名 + 源文件 A + 目标文件 B);
- 源文件 A:** 二进制只读
rb** 打开(兼容所有文件类型:文本 / 图片 / 视频); - 目标文件 B:** 二进制写入
wb** 打开(不存在则创建,存在则清空,兼容所有文件类型); - 定义缓冲区(4KB 为宜,
#define BUF_SIZE 4096),用fread+fwrite循环拷贝:- fread 从 A 读数据到缓冲区,返回实际读取的字节数;
- fwrite 将缓冲区数据写入 B,严格按实际读取的字节数写入;
- 循环终止条件:fread 返回 0(读取到文件末尾);
- 验证方式:
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;
}
更多推荐

所有评论(0)