
上一篇我们搞懂了 Linux 文件的本质:既是磁盘上的 "内容 + 属性",也是系统资源的统一抽象。但光理解本质不够,我们还得知道 "怎么操作文件"------ 这篇就聚焦C 语言标准库 IO 接口 (也称 "标准 IO"),手把手教你用fopen打开文件、fread/fwrite读写数据、feof判断文件末尾,再通过实战案例(实现cat命令、文件拷贝工具)巩固知识点,让你从 "会用" 到 "懂原理"。
文章目录
-
- [一、先搞懂:为什么需要 C 语言标准库 IO?](#一、先搞懂:为什么需要 C 语言标准库 IO?)
- 二、核心接口拆解:从打开到关闭的全流程
-
- [2.1 第一步:打开文件 ------fopen](#2.1 第一步:打开文件 ——fopen)
-
- [1. 函数语法](#1. 函数语法)
- [2. 参数解析](#2. 参数解析)
- [3. 返回值](#3. 返回值)
- [4. 关键细节:路径解析逻辑](#4. 关键细节:路径解析逻辑)
- [5. 示例代码:打开文件并判断是否成功](#5. 示例代码:打开文件并判断是否成功)
- [2.2 最后一步:关闭文件 ------fclose](#2.2 最后一步:关闭文件 ——fclose)
-
- [1. 函数语法](#1. 函数语法)
- [2. 参数与返回值](#2. 参数与返回值)
- [3. 为什么必须关闭文件?](#3. 为什么必须关闭文件?)
- [4. 示例代码:关闭文件并判断](#4. 示例代码:关闭文件并判断)
- [2.3 写入数据 ------fwrite](#2.3 写入数据 ——fwrite)
-
- [1. 函数语法](#1. 函数语法)
- [2. 参数解析(重点!容易混淆)](#2. 参数解析(重点!容易混淆))
- [3. 返回值](#3. 返回值)
- [4. 关键场景:写入文本 vs 写入二进制](#4. 关键场景:写入文本 vs 写入二进制)
-
- [场景 1:写入字符串(文本)](#场景 1:写入字符串(文本))
- [场景 2:写入整数(二进制)](#场景 2:写入整数(二进制))
- [2.4 读取数据 ------fread](#2.4 读取数据 ——fread)
-
- [1. 函数语法](#1. 函数语法)
- [2. 参数解析(与 fwrite 对称)](#2. 参数解析(与 fwrite 对称))
- [3. 返回值(重点!需结合场景判断)](#3. 返回值(重点!需结合场景判断))
- [4. 示例:读取文本文件(实现简单 "读文件" 功能)](#4. 示例:读取文本文件(实现简单 “读文件” 功能))
- [2.5 判断文件末尾 ------feof](#2.5 判断文件末尾 ——feof)
-
- [1. 函数语法](#1. 函数语法)
- [2. 返回值](#2. 返回值)
- [3. 常见误区:用 feof 判断 "是否继续读取"](#3. 常见误区:用 feof 判断 “是否继续读取”)
- [三、关键知识点:C 库 IO 的 6 种核心打开模式](#三、关键知识点:C 库 IO 的 6 种核心打开模式)
-
- [3.1 6 种模式对比表(重点!建议收藏)](#3.1 6 种模式对比表(重点!建议收藏))
- [3.2 关键模式辨析(避坑指南)](#3.2 关键模式辨析(避坑指南))
- [四、C 语言默认打开的 3 个流:stdin、stdout、stderr](#四、C 语言默认打开的 3 个流:stdin、stdout、stderr)
-
- [4.1 3 个默认流的作用](#4.1 3 个默认流的作用)
- [4.2 为什么默认打开这 3 个流?](#4.2 为什么默认打开这 3 个流?)
- [4.3 实战:输出信息到显示器的 3 种方法](#4.3 实战:输出信息到显示器的 3 种方法)
- [五、实战案例:用 C 库 IO 实现两个常用工具](#五、实战案例:用 C 库 IO 实现两个常用工具)
- [六、C 库 IO 的错误处理技巧](#六、C 库 IO 的错误处理技巧)
-
- [6.1 核心工具:errno + perror + strerror](#6.1 核心工具:errno + perror + strerror)
- [6.2 错误处理流程(通用模板)](#6.2 错误处理流程(通用模板))
- 七、常见坑与避坑指南
- 八、总结与下一篇预告
一、先搞懂:为什么需要 C 语言标准库 IO?
在学具体接口前,我们得先明白一个核心问题:操作系统已经提供了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 语言标准库提供了一套完整的文件操作接口,核心是 "打开→读写→判断结束→关闭" 的流程。我们逐个拆解每个接口的用法、参数细节和注意事项。
2.1 第一步:打开文件 ------fopen
要操作文件,第一步必须 "打开文件"------ 用fopen函数建立程序与文件的关联,就像 "打开房门才能进房间"。
1. 函数语法
c
#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. 示例代码:打开文件并判断是否成功
c
#include <stdio.h>
#include <string.h> // 用于strerror
#include <errno.h> // 用于errno
int main() {
// 以只读模式打开当前目录下的test.txt
FILE *fp = fopen("test.txt", "r");
// 关键:判断打开是否成功(必须做!否则fp为NULL会导致后续操作崩溃)
if (fp == NULL) {
// 方法1:用perror直接打印错误(简单)
perror("fopen failed");
// 方法2:用strerror获取错误描述(更灵活)
// printf("fopen failed: %s\n", strerror(errno));
return 1; // 打开失败,退出程序
}
printf("文件打开成功!\n");
fclose(fp); // 记得关闭文件(后续讲)
return 0;
}

如果test.txt不存在,运行结果会显示:
bash
fopen failed: No such file or directory

2.2 最后一步:关闭文件 ------fclose
文件操作完成后,必须用fclose关闭文件------ 就像 "离开房间要关门",否则会导致资源泄漏(比如文件描述符被占用、缓冲区数据丢失)。
1. 函数语法
c
#include <stdio.h>
int fclose(FILE *stream);
2. 参数与返回值
- 参数
stream:fopen返回的FILE*指针(要关闭的文件句柄); - 返回值 :
- 成功:返回
0; - 失败:返回
EOF(通常是-1,比如文件已经被关闭),错误原因存放在errno中。
- 成功:返回
3. 为什么必须关闭文件?
- 释放资源:每个进程能打开的文件数量有限(默认一般是 1024 个),不关闭会导致 "文件描述符耗尽",后续无法打开新文件;
- 刷新缓冲区:C 库 IO 默认有 "用户态缓冲区"(下一篇详细讲),
fclose会自动刷新缓冲区 ------ 如果不关闭,缓冲区中未写入磁盘的数据会丢失。
4. 示例代码:关闭文件并判断
c
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("fopen failed");
return 1;
}
// 写入数据(后续讲fwrite)
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;
}
2.3 写入数据 ------fwrite
fwrite用于将内存中的数据以二进制形式写入文件,支持写入文本、整数、结构体等任意数据(注意:写入的是 "原始二进制",不是 "文本格式")。
1. 函数语法
c
#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:写入字符串(文本)
c
#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!"; // 字符串,末尾有隐藏的'\0'
// 写入字符串:单个字符1字节,共strlen(msg)个字符(不含'\0',避免乱码)
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:写入整数(二进制)
c
#include <stdio.h>
int main() {
FILE *fp = fopen("binary.bin", "wb"); // 用"wb"模式(二进制写)
if (fp == NULL) { perror("fopen"); return 1; }
int num = 1234567; // 整数在内存中占4字节(32位系统)
// 写入整数:单个int4字节,共1个数据块
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写入。
2.4 读取数据 ------fread
fread与fwrite对应,用于从文件中读取二进制数据到内存缓冲区,同样支持读取文本、整数、结构体等数据。
1. 函数语法
c
#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. 示例:读取文本文件(实现简单 "读文件" 功能)
c
#include <stdio.h>
#include <string.h>
#define BUF_SIZE 1024 // 缓冲区大小,每次读1024字节
int main() {
// 1. 打开文件(只读模式)
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) { perror("fopen"); return 1; }
// 2. 循环读取文件
char buf[BUF_SIZE] = {0}; // 存储读取数据的缓冲区
while (1) {
// 每次读1字节,最多读BUF_SIZE个字符(即1024字节)
size_t read_cnt = fread(buf, 1, BUF_SIZE - 1, fp); // 留1字节存'\0'
if (read_cnt > 0) {
buf[read_cnt] = '\0'; // 给字符串加结束符,避免乱码
printf("读取到%d字节:%s", read_cnt, buf);
}
// 3. 判断是否读取结束或出错
if (feof(fp)) { // 到达文件末尾
printf("\n文件读取完毕!\n");
break;
}
if (ferror(fp)) { // 读取错误
perror("fread failed");
break;
}
}
// 4. 关闭文件
fclose(fp);
return 0;
}
运行后会逐段打印test.txt的内容 ------ 这就是cat命令的核心逻辑之一。
2.5 判断文件末尾 ------feof
很多人会误用feof:以为 "feof返回真就是读取失败",但实际它的作用是判断 "上一次读取操作是否到达文件末尾"(不是 "是否即将到达末尾")。
1. 函数语法
c
c
#include <stdio.h>
int feof(FILE *stream);
2. 返回值
- 非 0 值(真):上一次读取操作已经到达文件末尾(EOF);
- 0(假):上一次读取操作未到达末尾,或文件未被读取过。
3. 常见误区:用 feof 判断 "是否继续读取"
错误写法(先判断feof,再读取):
c
c
// 错误示例:先判断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示例那样):
c
c
// 正确示例:先读,再判断
while (1) {
size_t read_cnt = fread(buf, 1, BUF_SIZE, fp);
if (read_cnt > 0) { /* 处理数据 */ }
if (feof(fp)) { // 上一次read到达末尾
break;
}
if (ferror(fp)) { // 上一次read出错
perror("read failed");
break;
}
}
三、关键知识点:C 库 IO 的 6 种核心打开模式
fopen的mode参数决定了文件的 "操作权限" 和 "行为特性",这是最容易混淆的部分 ------ 我们用表格清晰对比 6 种核心模式,再结合场景说明用法。
3.1 6 种模式对比表(重点!建议收藏)
| 模式 | 读写权限 | 文件不存在时 | 文件存在时 | 写入行为 | 适用场景 |
|---|---|---|---|---|---|
r |
只读 | 打开失败 | 保留内容 | 不允许写入 | 读取已存在的文本 / 配置文件 |
r+ |
读写 | 打开失败 | 保留内容 | 从文件开头覆盖写入 | 读写已存在的文件(不创建) |
w |
只写 | 创建文件 | 清空内容(截断) | 从文件开头写入 | 创建新文件或覆盖旧文件 |
w+ |
读写 | 创建文件 | 清空内容(截断) | 从文件开头写入 | 读写新文件或覆盖旧文件 |
a |
只写(追加) | 创建文件 | 保留内容 | 从文件末尾追加写入 | 写日志(不覆盖旧内容) |
a+ |
读写(追加) | 创建文件 | 保留内容 | 写入:末尾追加;读取:从头开始 | 读写日志(既能读历史,又能追加) |
3.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 库预先定义,直接就能用。
4.1 3 个默认流的作用
| 流名称 | 对应设备 | 文件描述符(底层) | 作用 | 常用接口 |
|---|---|---|---|---|
stdin |
键盘 | 0 | 接收用户输入(比如scanf) |
fscanf(stdin, ...)、fread(..., stdin) |
stdout |
显示器 | 1 | 输出正常信息(比如printf) |
printf(...)(等价于fprintf(stdout, ...)) |
stderr |
显示器 | 2 | 输出错误信息(比如perror) |
fprintf(stderr, "错误:...") |
4.2 为什么默认打开这 3 个流?
因为程序的核心是 "数据处理",而数据处理需要 "输入(来源)" 和 "输出(去向)"------stdin是默认输入源(键盘),stdout/stderr是默认输出目标(显示器),这样程序启动就能直接交互,不用手动打开。
4.3 实战:输出信息到显示器的 3 种方法
我们可以用printf、fprintf、fwrite三种方式输出到stdout(最终都显示在显示器上),代码如下:
c
#include <stdio.h>
#include <string.h>
int main() {
// 方法1:printf(默认输出到stdout)
printf("hello printf!\n");
// 方法2:fprintf(显式指定stdout)
fprintf(stdout, "hello fprintf!\n");
// 方法3:fwrite(二进制写入到stdout)
const char *msg = "hello fwrite!\n";
fwrite(msg, strlen(msg), 1, stdout);
return 0;
}
运行结果:
bash
hello printf!
hello fprintf!
hello fwrite!
三种方法的本质:printf是fprintf的简化版(默认传stdout),fwrite是直接写入stdout对应的文件 ------ 最终都通过stdout输出到显示器。
五、实战案例:用 C 库 IO 实现两个常用工具
光学接口不够,我们通过两个实战案例巩固知识点:实现cat命令(读取文件并输出)、实现文件拷贝工具(读源文件→写目标文件)。
5.1 案例 1:实现简化版cat命令
cat命令的核心逻辑是:打开指定文件→循环读取内容→输出到stdout→关闭文件。我们还要加上 "命令行参数校验"(用户必须传入文件名)。
完整代码
c
#include <stdio.h>
#include <string.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[]) {
// 1. 校验命令行参数(argc=程序名+文件名,共2个参数)
if (argc != 2) {
// 提示用法:./mycat 文件名
fprintf(stderr, "用法:%s <文件名>\n", argv[0]);
return 1;
}
// 2. 打开文件(只读模式)
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
perror("fopen failed");
return 1;
}
// 3. 循环读取文件,输出到stdout
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); // 输出到显示器
}
// 4. 判断结束或错误
if (feof(fp)) {
break;
}
if (ferror(fp)) {
perror("fread failed");
break;
}
}
// 5. 关闭文件
fclose(fp);
return 0;
}
编译与运行
bash
# 编译代码
gcc mycat.c -o mycat
# 创建测试文件
echo "hello mycat!" > test.txt
# 运行自定义cat命令
./mycat test.txt
# 输出结果:hello mycat!
5.2 案例 2:实现文件拷贝工具(mycopy)
文件拷贝的逻辑是:打开源文件(读)→ 打开目标文件(写)→ 读源文件→写目标文件→关闭两个文件。
完整代码
c
#include <stdio.h>
#include <string.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[]) {
// 1. 校验参数(argc=程序名+源文件+目标文件,共3个参数)
if (argc != 3) {
fprintf(stderr, "用法:%s <源文件> <目标文件>\n", argv[0]);
return 1;
}
const char *src_file = argv[1]; // 源文件路径
const char *dest_file = argv[2]; // 目标文件路径
// 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;
}
// 3. 循环拷贝:读源文件→写目标文件
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;
}
// 4. 判断结束或错误
if (feof(src_fp)) {
printf("拷贝成功!共拷贝%d字节\n", total_copy);
break;
}
if (ferror(src_fp)) {
perror("读源文件失败");
break;
}
if (ferror(dest_fp)) {
perror("写目标文件失败");
break;
}
}
// 5. 关闭文件(先关目标文件,再关源文件)
fclose(dest_fp);
fclose(src_fp);
return 0;
}
编译与运行
bash
# 编译代码
gcc mycopy.c -o mycopy
# 创建100KB的测试文件
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不处理,会导致数据丢失。这里教你一套完整的错误处理方法。
6.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)。
6.2 错误处理流程(通用模板)
c
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("test.txt", "r");
// 1. 检查fopen错误
if (fp == NULL) {
// 方法1:perror(简单直接)
perror("fopen failed");
// 方法2:strerror(灵活,可自定义格式)
// fprintf(stderr, "fopen failed: %s (错误码:%d)\n", strerror(errno), errno);
return 1;
}
char buf[1024] = {0};
// 2. 检查fread错误
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");
}
}
// 3. 检查fclose错误
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、文件拷贝),我们把这些接口串起来,理解了 "文件操作的全流程"。但这里有个疑问:C 库 IO 是怎么实现的?它和操作系统的open/read/write系统调用是什么关系?
下一篇我们就深入底层,讲解Linux 系统 IO 接口 (open/read/write/close),揭开 "C 库 IO 封装系统调用" 的神秘面纱,同时带你理解 "文件描述符"(fd)这个核心概念 ------ 敬请期待!