跳到主要内容
Linux 基础 IO 系列(二):C 语言标准库 IO 接口实战 | 极客日志
C
Linux 基础 IO 系列(二):C 语言标准库 IO 接口实战 综述由AI生成 系统讲解了 C 语言标准库 IO 的核心接口,包括 fopen、fclose、fread、fwrite 及 feof 函数的用法与参数细节。文章对比了 6 种文件打开模式,介绍了 stdin、stdout、stderr 三个默认流,并提供了 errno、perror 等错误处理技巧。通过实现简化版 cat 命令和文件拷贝工具,帮助开发者掌握跨平台文件操作的规范与原理。
随缘 发布于 2026/3/30 更新于 2026/5/25 30 浏览
在学具体接口前,我们得先明白一个核心问题:操作系统已经提供了 open/read/write 等系统调用,为什么还要 C 库 IO(fopen/fread/fwrite)?
答案有两个:
封装复杂逻辑,降低开发难度 :系统调用需要处理很多底层细节(比如文件描述符管理、权限校验),C 库 IO 把这些细节 '包起来',提供更简单的接口。比如用 fopen("test.txt", "r") 就能打开文件,不用关心底层 open 函数的 flags 位标志位、mode 权限位。
保证跨平台兼容性 :不同操作系统的系统调用不一样(比如 Linux 的 open 和 Windows 的 CreateFile),但 C 库 IO 是 '标准接口'—— 只要你的代码用的是 fopen/fread,在 Linux、Windows、macOS 上都能跑(前提是不依赖系统特有功能)。
简单说,C 库 IO 是 '用户友好型中间商':它封装了系统调用,给我们提供简单、通用的文件操作方式。
一、核心接口拆解:从打开到关闭的全流程
C 语言标准库提供了一套完整的文件操作接口,核心是 '打开→读写→判断结束→关闭' 的流程。我们逐个拆解每个接口的用法、参数细节和注意事项。
1. 第一步:打开文件 —— fopen
要操作文件,第一步必须 '打开文件'—— 用 fopen 函数建立程序与文件的关联,就像 '打开房门才能进房间'。
1. 函数语法
#include <stdio.h>
FILE *fopen (const char *filename, const char *mode) ;
2. 参数解析
参数名 含义 示例 filename要打开的文件路径(相对路径 / 绝对路径) "test.txt"(当前目录)、"/home/user/log.txt"(绝对路径)mode打开模式(决定文件可读写性、是否创建、是否追加等) "r"(只读)、"w"(只写)、"a"(追加)
3. 返回值
成功 :返回一个 FILE* 类型的指针(称为 '文件句柄'),后续所有操作都要靠这个指针定位文件;
失败 :返回 NULL(比如文件不存在、权限不足),此时需要用 perror 或 strerror 查看错误原因。
4. 关键细节:路径解析逻辑 如果 filename 不带路径(比如 "test.txt"),fopen 会默认在**进程的当前工作目录(CWD)**下找文件。进程的 CWD 从哪里来?—— 来自进程 PCB(进程控制块)中的 cwd 字段(上一篇提到的 /proc/[PID]/cwd 符号链接就是它的映射)。
比如你的程序在 /home/user 目录下运行,fopen("test.txt", "r") 会去找 /home/user/test.txt;如果程序在 /tmp 目录下运行,就会找 /tmp/test.txt。
5. 示例代码:打开文件并判断是否成功 #include <stdio.h>
#include <string.h>
#include <errno.h>
int main () {
FILE *fp = fopen("test.txt" , "r" );
if (fp == NULL ) {
perror("fopen failed" );
return 1 ;
}
printf ("文件打开成功!\n" );
fclose(fp);
return 0 ;
}
fopen failed: No such file or directory
2. 最后一步:关闭文件 —— fclose 文件操作完成后,必须用 fclose 关闭文件 —— 就像 '离开房间要关门',否则会导致资源泄漏(比如文件描述符被占用、缓冲区数据丢失)。
1. 函数语法 #include <stdio.h>
int fclose (FILE *stream) ;
2. 参数与返回值
参数 stream :fopen 返回的 FILE* 指针(要关闭的文件句柄);
返回值 :
成功:返回 0;
失败:返回 EOF(通常是 -1,比如文件已经被关闭),错误原因存放在 errno 中。
3. 为什么必须关闭文件?
释放资源:每个进程能打开的文件数量有限(默认一般是 1024 个),不关闭会导致 '文件描述符耗尽',后续无法打开新文件;
刷新缓冲区:C 库 IO 默认有 '用户态缓冲区'(下一篇详细讲),fclose 会自动刷新缓冲区 —— 如果不关闭,缓冲区中未写入磁盘的数据会丢失。
4. 示例代码:关闭文件并判断 #include <stdio.h>
int main () {
FILE *fp = fopen("test.txt" , "w" );
if (fp == NULL ) {
perror("fopen failed" );
return 1 ;
}
const char *msg = "hello C lib IO!" ;
fwrite(msg, strlen (msg), 1 , fp);
if (fclose(fp) != 0 ) {
perror("fclose failed" );
return 1 ;
}
printf ("文件关闭成功!\n" );
return 0 ;
}
3. 写入数据 —— fwrite fwrite 用于将内存中的数据以二进制形式写入文件 ,支持写入文本、整数、结构体等任意数据(注意:写入的是 '原始二进制',不是 '文本格式')。
1. 函数语法 #include <stdio.h>
size_t fwrite (const void *ptr, size_t size, size_t nmemb, FILE *stream) ;
2. 参数解析(重点!容易混淆) fwrite 的参数设计很巧妙,用 '单个数据块大小 × 数据块个数' 来描述要写入的数据,而不是直接传 '总字节数'—— 这样更灵活(比如写入数组时不用手动算总字节数)。
参数名 含义 示例 ptr指向 '要写入数据' 的内存起始地址(比如数组名、变量地址) &num(变量地址)、msg(字符串数组名)size单个数据块的字节数 (比如 sizeof(int)、sizeof(char))写入 int 时传 sizeof(int),写入字符时传 1 nmemb要写入的数据块个数 写入 5 个 int 时传 5,写入字符串时传 strlen(msg) stream目标文件的 FILE* 指针 fp(之前 fopen 返回的指针)
3. 返回值
成功 :返回 '实际写入的数据块个数'(正常情况下等于 nmemb);
失败 / 部分写入 :返回值小于 nmemb(比如磁盘满了、文件被意外关闭),此时需要结合 ferror 判断是否为错误。
4. 关键场景:写入文本 vs 写入二进制 很多人会用 fwrite 写入文本,但容易忽略 '二进制' 特性 —— 我们通过两个例子对比:
场景 1:写入字符串(文本) #include <stdio.h>
#include <string.h>
int main () {
FILE *fp = fopen("text.txt" , "w" );
if (fp == NULL ) {
perror("fopen" );
return 1 ;
}
const char *msg = "hello fwrite!" ;
size_t write_cnt = fwrite(msg, 1 , strlen (msg), fp);
if (write_cnt != strlen (msg)) {
printf ("写入失败!实际写入%d个字符\n" , write_cnt);
} else {
printf ("写入成功!共写入%d个字符\n" , write_cnt);
}
fclose(fp);
return 0 ;
}
运行后用 cat text.txt 查看,会显示 hello fwrite! —— 因为我们写入的是 '字符的 ASCII 码',文本编辑器能正常解析。
场景 2:写入整数(二进制) #include <stdio.h>
int main () {
FILE *fp = fopen("binary.bin" , "wb" );
if (fp == NULL ) {
perror("fopen" );
return 1 ;
}
int num = 1234567 ;
size_t write_cnt = fwrite(&num, sizeof (int ), 1 , fp);
if (write_cnt != 1 ) {
printf ("整数写入失败!\n" );
} else {
printf ("整数写入成功!\n" );
}
fclose(fp);
return 0 ;
}
运行后用 cat binary.bin 查看,会显示乱码 —— 因为 fwrite 写入的是 1234567 的原始二进制 (00010010 11010110 10000111),不是文本格式的 '1234567'。如果要让整数以文本形式写入,需要先用 sprintf 转成字符串,再用 fwrite 写入。
4. 读取数据 —— fread fread 与 fwrite 对应,用于从文件中读取二进制数据到内存缓冲区 ,同样支持读取文本、整数、结构体等数据。
1. 函数语法 #include <stdio.h>
size_t fread (void *ptr, size_t size, size_t nmemb, FILE *stream) ;
2. 参数解析(与 fwrite 对称) 参数名 含义 示例 ptr指向 '存储读取数据' 的内存缓冲区(比如数组、变量地址) buf(字符数组名)、&num(int 变量地址)size单个数据块的字节数 读 int 时传 sizeof(int),读字符时传 1 nmemb计划读取的数据块个数 计划读 1024 个字符时传 1024 stream源文件的 FILE* 指针 fp(fopen 返回的指针)
3. 返回值(重点!需结合场景判断) fread 的返回值是 '实际读取的数据块个数',有三种常见情况:
等于 nmemb :读取成功,获取到了期望的所有数据;
小于 nmemb 但大于 0 :部分读取(比如文件剩余数据不足,或非阻塞 IO 场景);
等于 0 :两种可能 ——① 到达文件末尾(EOF);② 读取错误(需用 feof 和 ferror 区分)。
4. 示例:读取文本文件(实现简单 '读文件' 功能) #include <stdio.h>
#include <string.h>
#define BUF_SIZE 1024
int main () {
FILE *fp = fopen("test.txt" , "r" );
if (fp == NULL ) {
perror("fopen" );
return 1 ;
}
char buf[BUF_SIZE] = {0 };
while (1 ) {
size_t read_cnt = fread(buf, 1 , BUF_SIZE - 1 , fp);
if (read_cnt > 0 ) {
buf[read_cnt] = '\0' ;
printf ("读取到%d字节:%s" , read_cnt, buf);
}
if (feof(fp)) {
printf ("\n文件读取完毕!\n" );
break ;
}
if (ferror(fp)) {
perror("fread failed" );
break ;
}
}
fclose(fp);
return 0 ;
}
运行后会逐段打印 test.txt 的内容 —— 这就是 cat 命令的核心逻辑之一。
5. 判断文件末尾 —— feof 很多人会误用 feof:以为 'feof 返回真就是读取失败',但实际它的作用是判断 '上一次读取操作是否到达文件末尾' (不是 '是否即将到达末尾')。
1. 函数语法 #include <stdio.h>
int feof (FILE *stream) ;
2. 返回值
非 0 值(真) :上一次读取操作已经到达文件末尾(EOF);
0(假) :上一次读取操作未到达末尾,或文件未被读取过。
3. 常见误区:用 feof 判断 '是否继续读取'
while (!feof(fp)) {
fread(buf, 1 , BUF_SIZE, fp);
printf ("%s" , buf);
}
为什么错?因为 feof 只有在 '读取到 EOF 后' 才会返回真 —— 第一次进入循环时,文件还没读,feof 返回假,执行 fread;如果 fread 刚好读到文件末尾,feof 还是假,会再次进入循环,执行 fread(此时 fread 返回 0,读取空数据),最后 feof 才返回真,跳出循环。
正确写法:先读取,再用 feof 判断是否结束 (就像前面 fread 示例那样):
while (1 ) {
size_t read_cnt = fread(buf, 1 , BUF_SIZE, fp);
if (read_cnt > 0 ) {
}
if (feof(fp)) {
break ;
}
if (ferror(fp)) {
perror("read failed" );
break ;
}
}
二、关键知识点:C 库 IO 的 6 种核心打开模式 fopen 的 mode 参数决定了文件的 '操作权限' 和 '行为特性',这是最容易混淆的部分 —— 我们用表格清晰对比 6 种核心模式,再结合场景说明用法。
1. 6 种模式对比表 模式 读写权限 文件不存在时 文件存在时 写入行为 适用场景 r只读 打开失败 保留内容 不允许写入 读取已存在的文本 / 配置文件 r+读写 打开失败 保留内容 从文件开头覆盖写入 读写已存在的文件(不创建) w只写 创建文件 清空内容(截断) 从文件开头写入 创建新文件或覆盖旧文件 w+读写 创建文件 清空内容(截断) 从文件开头写入 读写新文件或覆盖旧文件 a只写(追加) 创建文件 保留内容 从文件末尾追加写入 写日志(不覆盖旧内容) a+读写(追加) 创建文件 保留内容 写入:末尾追加;读取:从头开始 读写日志(既能读历史,又能追加)
2. 关键模式辨析(避坑指南)
w 和 a 的区别 :
w:无论文件是否存在,都会清空内容(即使文件有 100MB,用 w 打开后也会变成 0KB);
a:不会清空文件,新数据永远追加到末尾(比如日志文件必须用 a 模式,否则会覆盖历史日志)。
r+ 和 w+ 的区别 :
r+:文件必须已存在,否则打开失败(适合修改已有的文件);
w+:文件不存在则创建,存在则清空(适合创建新的读写文件)。
二进制模式(b 后缀) :在 Windows 下,文本文件和二进制文件的换行符处理不同(文本用 \r\n,二进制用 \n),所以需要加 b 后缀(比如 rb/wb);但在 Linux 下,文本和二进制文件的换行符处理一致,b 后缀可加可不加 ——为了跨平台兼容,建议写二进制文件时加 b (比如 wb/rb)。
三、C 语言默认打开的 3 个流:stdin、stdout、stderr 你可能没注意过:C 程序启动时,会默认打开 3 个 '特殊文件' —— 它们是标准输入(stdin)、标准输出(stdout)、标准错误输出(stderr),对应的 FILE* 指针由 C 库预先定义,直接就能用。
1. 3 个默认流的作用 流名称 对应设备 文件描述符(底层) 作用 常用接口 stdin键盘 0 接收用户输入(比如 scanf) fscanf(stdin, ...)、fread(..., stdin)stdout显示器 1 输出正常信息(比如 printf) printf(...)(等价于 fprintf(stdout, ...))stderr显示器 2 输出错误信息(比如 perror) fprintf(stderr, "错误:...")
2. 为什么默认打开这 3 个流? 因为程序的核心是 '数据处理',而数据处理需要 '输入(来源)' 和 '输出(去向)'——stdin 是默认输入源(键盘),stdout/stderr 是默认输出目标(显示器),这样程序启动就能直接交互,不用手动打开。
3. 实战:输出信息到显示器的 3 种方法 我们可以用 printf、fprintf、fwrite 三种方式输出到 stdout(最终都显示在显示器上),代码如下:
#include <stdio.h>
#include <string.h>
int main () {
printf ("hello printf!\n" );
fprintf (stdout , "hello fprintf!\n" );
const char *msg = "hello fwrite!\n" ;
fwrite(msg, strlen (msg), 1 , stdout );
return 0 ;
}
hello printf ! hello fprintf ! hello fwrite!
三种方法的本质:printf 是 fprintf 的简化版(默认传 stdout),fwrite 是直接写入 stdout 对应的文件 —— 最终都通过 stdout 输出到显示器。
四、实战案例:用 C 库 IO 实现两个常用工具 光学接口不够,我们通过两个实战案例巩固知识点:实现 cat 命令(读取文件并输出)、实现文件拷贝工具(读源文件→写目标文件)。
1. 案例 1:实现简化版 cat 命令 cat 命令的核心逻辑是:打开指定文件→循环读取内容→输出到 stdout→关闭文件。我们还要加上 '命令行参数校验'(用户必须传入文件名)。
完整代码 #include <stdio.h>
#include <string.h>
#define BUF_SIZE 1024
int main (int argc, char *argv[]) {
if (argc != 2 ) {
fprintf (stderr , "用法:%s <文件名>\n" , argv[0 ]);
return 1 ;
}
FILE *fp = fopen(argv[1 ], "r" );
if (fp == NULL ) {
perror("fopen failed" );
return 1 ;
}
char buf[BUF_SIZE] = {0 };
while (1 ) {
size_t read_cnt = fread(buf, 1 , BUF_SIZE - 1 , fp);
if (read_cnt > 0 ) {
buf[read_cnt] = '\0' ;
fwrite(buf, 1 , read_cnt, stdout );
}
if (feof(fp)) {
break ;
}
if (ferror(fp)) {
perror("fread failed" );
break ;
}
}
fclose(fp);
return 0 ;
}
编译与运行
gcc mycat.c -o mycat
echo "hello mycat!" > test.txt
./mycat test.txt
2. 案例 2:实现文件拷贝工具(mycopy) 文件拷贝的逻辑是:打开源文件(读)→ 打开目标文件(写)→ 读源文件→写目标文件→关闭两个文件。
完整代码 #include <stdio.h>
#include <string.h>
#define BUF_SIZE 1024
int main (int argc, char *argv[]) {
if (argc != 3 ) {
fprintf (stderr , "用法:%s <源文件> <目标文件>\n" , argv[0 ]);
return 1 ;
}
const char *src_file = argv[1 ];
const char *dest_file = argv[2 ];
FILE *src_fp = fopen(src_file, "r" );
if (src_fp == NULL ) {
perror("fopen src failed" );
return 1 ;
}
FILE *dest_fp = fopen(dest_file, "w" );
if (dest_fp == NULL ) {
perror("fopen dest failed" );
fclose(src_fp);
return 1 ;
}
char buf[BUF_SIZE] = {0 };
size_t total_copy = 0 ;
while (1 ) {
size_t read_cnt = fread(buf, 1 , BUF_SIZE, src_fp);
if (read_cnt > 0 ) {
size_t write_cnt = fwrite(buf, 1 , read_cnt, dest_fp);
if (write_cnt != read_cnt) {
fprintf (stderr , "写入目标文件失败!\n" );
break ;
}
total_copy += write_cnt;
}
if (feof(src_fp)) {
printf ("拷贝成功!共拷贝%d字节\n" , total_copy);
break ;
}
if (ferror(src_fp)) {
perror("读源文件失败" );
break ;
}
if (ferror(dest_fp)) {
perror("写目标文件失败" );
break ;
}
}
fclose(dest_fp);
fclose(src_fp);
return 0 ;
}
编译与运行
gcc mycopy.c -o mycopy
dd if =/dev/zero of=src.bin bs=1024 count=100
./mycopy src.bin dest.bin
diff src.bin dest.bin
五、C 库 IO 的错误处理技巧 写文件操作代码时,'忽略错误' 是最常见的 bug 来源 —— 比如 fopen 返回 NULL 不处理,程序会崩溃;fwrite 返回值小于 nmemb 不处理,会导致数据丢失。这里教你一套完整的错误处理方法。
1. 核心工具:errno + perror + strerror
**errno**:全局变量(定义在 <errno.h> 中),存储最近一次系统调用 / 库函数的错误码(0 表示无错误);
**perror(const char *s)**:打印错误信息,格式是 's: 错误描述'(比如 perror("fopen") 会输出 fopen: No such file or directory);
**strerror(int errnum)**:将错误码转为字符串描述(比如 strerror(2) 返回 No such file or directory)。
2. 错误处理流程(通用模板) #include <stdio.h>
#include <errno.h>
#include <string.h>
int main () {
FILE *fp = fopen("test.txt" , "r" );
if (fp == NULL ) {
perror("fopen failed" );
return 1 ;
}
char buf[1024 ] = {0 };
size_t read_cnt = fread(buf, 1 , 1024 , fp);
if (read_cnt == 0 ) {
if (feof(fp)) {
printf ("已到达文件末尾\n" );
} else if (ferror(fp)) {
perror("fread failed" );
}
}
if (fclose(fp) != 0 ) {
perror("fclose failed" );
return 1 ;
}
return 0 ;
}
六、常见坑与避坑指南
忘记关闭文件(资源泄漏) :每次 fopen 后必须对应 fclose,尤其是在函数中途返回时(比如 fopen 目标文件失败,要先关闭源文件再返回)。
fwrite 写入字符串时带 \0 :字符串末尾的 \0 是 C 语言的 '字符串结束符',不是内容的一部分 —— 用 fwrite 写入时,要传 strlen(msg) 而不是 strlen(msg)+1,否则文件会多一个 \0(文本编辑器打开可能显示乱码)。
误用 feof 判断读取循环 :记住 '先读取,再判断 feof',不要 '先判断 feof,再读取',避免多读一次空数据。
打开模式选错导致数据丢失 :写日志用 a 模式,不要用 w 模式;修改已有文件用 r+ 模式,不要用 w+ 模式(w+ 会清空文件)。
七、总结 这篇我们系统学习了 C 语言标准库 IO 的核心接口:
打开 / 关闭文件:fopen/fclose(记得判断返回值);
读写数据:fread/fwrite(理解 '数据块大小 × 个数' 的参数设计);
判断末尾:feof(先读再判断,避免误用);
打开模式:6 种模式的区别(重点是 w/a、r+/w+ 的差异)。
通过实战案例(实现 cat、文件拷贝),我们把这些接口串起来,理解了 '文件操作的全流程'。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online