
07_C语言-文件篇
本文最后更新于 2025-03-18,学习久了要注意休息哟
第一章 文件与流
1.1 基本概念:文件 & 流
:one: 文件抽象
- 在操作系统中,“文件”就是对存储设备(如磁盘、闪存等)上数据的一种抽象。
- 它提供了命名和组织数据的方法,使我们能通过文件名定位并访问其中的数据。
- 不管是文本文件
.txt
还是二进制文件.bin
,本质上都是一串比特流,只是阅读或解释方式不同。
示意图
[ 硬盘 / SSD 等物理介质 ]
↓
[文件系统]
↓
[文件抽象]
就像大仓库里有一个个小盒子,每个小盒子都用独特的标签(文件名)标识。要找某份资料时,你只要找对盒子名就能打开取东西啦 (•̀ᴗ•́)و ̑̑
:two: 流的抽象
- 在 C 语言中,使用“流”这一概念来管理数据在程序与外部之间的输入/输出。
- 可以把“流”看作数据流动的管道,从“程序内部”连到“外部文件”或“设备”的通道。
- 一旦我们打开一个文件,就会得到一个对应的“文件流对象(
FILE*
)”,通过它进行读写。
可以想象家里水管系统:打开水龙头(打开文件),水(数据)就流入或流出。关掉水龙头(关闭文件),水管就不再有水流动了。
:three: 文件与流
- 当我们在 C 语言中对文件进行操作时,实际上是把数据装入/取出流,再由系统负责将数据写入/读出物理文件。
- 文件和流并不对立,而是**“文件” = 操作系统层面的抽象**,“流” = C 语言层面的抽象。
示意:
程序(用户视角) --(流)--> FILE*(C库层) --(文件系统)--> 磁盘文件(操作系统层)
:smiley_cat: 总结:文件是“数据盒子”,流是“管道”;通过管道(流)读写数据盒子(文件)。
1.2 标准输入输出流
:one: 标准流
stdin
(standard input): 代表标准输入(默认从键盘获取输入)stdout
(standard output): 代表标准输出(默认输出到终端屏幕)stderr
(standard error): 代表标准错误输出(默认也输出到屏幕,但通常与stdout
分开缓冲或重定向)
在我们写 printf()
的时候,默认就是往 stdout
输出;当我们用 scanf()
读取数据时,默认就是从 stdin
读取。
一个小例子
如下简单程序读取用户输入的数字,并将其打印出来。我们用 stdin
/ stdout
来演示,虽然在代码中并不需要显式写出 stdin
、stdout
,但背后的概念就是如此。
#include <stdio.h>
int main(void)
{
int number = 0;
// 提示用户输入
printf("请输入一个整数: ");
// scanf默认从stdin读取数据
scanf("%d", &number);
// printf默认往stdout输出数据
printf("你输入的整数是: %d\n", number);
return 0;
}
编译 & 运行示例
gcc example_stdio.c -o example_stdio
./example_stdio
示例输出
请输入一个整数: 2023
你输入的整数是: 2023
:two: 重定向
- 重定向指把程序的标准输入或输出从默认位置(终端)“改道”到其他地方,如文件、管道等。
- 在 Linux 命令行环境下,我们可以使用
>
、<
、2>
等符号做重定向。
常见用法举例
-
输出重定向:
./example_stdio > out.txt
这意味着原本打印到屏幕(
stdout
)的内容,现在全部写进out.txt
文件中。 -
输入重定向:
./example_stdio < input_data.txt
程序中的
scanf()
将会从input_data.txt
文件中读取数据,而不是等待键盘输入。 -
错误输出重定向:
./my_prog 2> error_log.txt
将
stderr
中的错误信息重定向到error_log.txt
。有助于分离正常输出和错误信息。
:three: 缓冲模式
-
行缓冲(Line Buffering)
:当遇到换行符
\n
或缓冲区满时,才把数据真正写到目的地。
stdout
在连接到终端时通常默认是行缓冲,这样可以保证用户在看终端时,一行行输出比较及时。
-
全缓冲(Full Buffering)
:只有当缓冲区填满或手动调用
fflush()
或程序结束时,数据才会写出去。
- 常见于对文件流的写操作。
-
无缓冲(Unbuffered):任何输出操作立即生效,不进行缓冲积攒,比如
stderr
通常是无缓冲,确保错误信息能尽快显示。
小Tips: 如果在调试程序时发现“为什么 printf 之后没有立即输出?”,那可能是因为数据还在缓冲里没写出去,可以试试输出
\n
或fflush(stdout)
来强制刷新。
1.3 FILE*
与 C 标准库
:one: FILE
结构体
- 在 C 标准库中,
FILE
是一个结构体类型(在<stdio.h>
中定义),内部包含当前流的缓冲区、文件指针位置、流状态标志等信息。 - 我们平时在写代码时使用
FILE *fp
来表示一个“文件指针”,其实就是指向了这样一个结构体,用于管理与该文件(或设备)之间的数据通道。
有人会将 FILE *fp
理解成“文件本身”,但其实它只是用来操作文件的“指针+控制块”。就像你手上拿到的不是整个仓库,而是仓库的钥匙和出入记录而已。
:two: 读写函数
常见函数有:
fopen()
:打开文件并返回一个FILE*
fprintf()
、fscanf()
:带格式的输出/输入fgets()
、fputs()
:行操作fgetc()
、fputc()
:字符操作fclose()
:关闭文件
后续章节会更详细地讲解文件操作的各种API,现在先来个初步小示例,让学生直观体会:
#include <stdio.h>
int main(void)
{
// 1. 打开一个名为 "demo.txt" 的文件,模式 "w" 表示写入
// 📂 如果没有就自动创建,如果有则清空文件内容
FILE *fp = fopen("demo.txt", "w");
if(fp == NULL)
{
// 打开失败时,fp返回NULL
// (╯°□°)╯︵ ┻━┻ 只能叹气~
perror("无法打开文件");
return -1;
}
// 2. 通过 fprintf 往文件中写入一行文本
fprintf(fp, "Hello, this is a demo file!\n");
// 3. 写完后记得关闭,防止资源泄露
fclose(fp);
printf("文件已写入: demo.txt\n");
return 0;
}
编译 & 运行示例
gcc demo_file_write.c -o demo_file_write
./demo_file_write
运行结果
文件已写入: demo.txt
此时可以使用 cat demo.txt
命令查看刚写入的内容。
:three: 读写流程示意
概括来说,对文件的读写流程大多是这样:
- 打开文件:
fopen()
,指定文件名与模式(如只读r
、写入w
、追加a
等)。 - 读或写操作:使用各种 I/O 函数(
fscanf
、fprintf
、fread
、fwrite
、fgetc
、fputc
等)。 - 关闭文件:
fclose()
,释放资源,把缓冲区剩余数据写出。
如果不小心忘记 fclose()
,可能导致文件数据丢失或内存泄漏(虽然程序结束后操作系统通常会帮你回收资源,但这并不是好习惯)。
第二章 文件操作
2.1 文件打开与关闭
:one: fopen()
、freopen()
常见模式
fopen()
常见模式
模式 | 含义 |
---|---|
r | 以只读方式打开文件,文件必须存在,否则返回 NULL |
w | 以写入方式打开文件,若文件不存在则创建,若存在则清空 |
a | 以追加方式打开文件,若文件不存在则创建,写入内容追加到文件末尾 |
r+ | 以读写方式打开文件,文件必须存在,不清空原内容 |
w+ | 以读写方式打开文件,若文件不存在则创建,若文件存在则清空 |
a+ | 以读写方式打开文件,若文件不存在则创建,写入从文件末尾追加 |
像
r+
、w+
、a+
既能读又能写,但具体能否移动读写指针、覆盖写等细节要根据模式来区分。(。・∀・)ノ゙
freopen()
文件重定向
- 作用:将已存在的流(如
stdin
、stdout
、stderr
)重定向到新的文件。 - 常用于在同一程序中,给
stdout
或stdin
改换输出/输入文件,而不用在命令行层面重定向。
示例:
#include <stdio.h>
int main(void)
{
// 使用freopen将stdout重定向到文件out.txt
if(freopen("out.txt", "w", stdout) == NULL)
{
perror("freopen 失败");
return -1;
}
// 此后所有的printf都写到 out.txt 文件中,而不是屏幕
printf("这行文字不会出现在屏幕上,而会写入 out.txt 哦!\n");
// 如果想恢复stdout到屏幕,需要再调用freopen绑定到"/dev/tty" (Linux)
// freopen("/dev/tty", "w", stdout);
return 0;
}
编译 & 运行示例
gcc freopen_example.c -o freopen_example
./freopen_example
输出示例
(屏幕上无输出)
然后查看 out.txt
文件,就能看到那行文字:“这行文字不会出现在屏幕上,而会写入 out.txt 哦!”
:two: fclose()
及常见错误处理
fclose(fp)
:关闭文件指针fp
,释放相关资源,将缓冲区剩余数据写入文件。- 如果你忘记关闭文件,可能导致数据没有及时刷新到磁盘(尤其在全缓冲模式下),也可能浪费系统资源。
示例:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// 试图以写方式打开文件
FILE *fp = fopen("data.txt", "w");
if(!fp)
{
// 打开失败,返回NULL
perror("无法打开 data.txt 文件");
return -1;
}
fprintf(fp, "小测试:写入一些文字到 data.txt\n");
// ⚠ 如果我们不调用 fclose,可能导致后续数据丢失
// 正确方式:写完记得关闭
if(fclose(fp) != 0)
{
perror("关闭文件时出错");
return -1;
}
printf("操作完成\n");
return 0;
}
编译 & 运行示例
gcc fclose_example.c -o fclose_example
./fclose_example
输出示例
操作完成
打开 data.txt
,可见写入的文字。
错误处理:若
fclose()
返回非 0 表示出错,比如网络文件系统断线、磁盘被移除等极端情况。
:three: 打开文件失败的处理
fopen()
如果返回NULL
,要用perror()
或strerror(errno)
查看出错原因。- 常见失败原因:文件不存在且使用
r
模式、权限不够、磁盘满等。
小Tips:(✿◡‿◡) 学好错误处理!否则调试排错时痛哭流涕就晚了。
2.2 文件读写:文本模式
:one: 字符级读写:fgetc()
、fputc()
int fgetc(FILE *stream)
:从文件流读取一个字符(返回其 ASCII 码整型),若到达文件尾或出错则返回EOF
。int fputc(int c, FILE *stream)
:向文件流写入一个字符。
示例:将文件内容一字符一字符地拷贝:
#include <stdio.h>
int main(void)
{
// 打开源文件source.txt只读
FILE *fpsrc = fopen("source.txt", "r");
if(!fpsrc)
{
perror("无法打开 source.txt");
return -1;
}
// 打开(或创建)目标文件dest.txt写入
FILE *fpdst = fopen("dest.txt", "w");
if(!fpdst)
{
perror("无法打开(或创建) dest.txt");
fclose(fpsrc); // 别忘了先关source
return -1;
}
int ch;
// 循环读取每个字符,直到EOF
while((ch = fgetc(fpsrc)) != EOF)
{
// 将字符写入目标文件
fputc(ch, fpdst);
}
// 关闭文件
fclose(fpsrc);
fclose(fpdst);
printf("拷贝完成\n");
return 0;
}
示例输出
拷贝完成
(可以比较 source.txt
和 dest.txt
内容是否一致)
:two: 行级读写:fgets()
、fputs()
char *fgets(char *str, int n, FILE *stream)
:从文件中读取一行(或 n-1 个字符),以\0
结尾;若到达文件尾或出错,则返回NULL
。int fputs(const char *str, FILE *stream)
:向文件中写字符串,不自动加换行符(需自行添加"\n"
)。
示例:读取 source.txt 的前 3 行并打印:
#include <stdio.h>
int main(void)
{
FILE *fp = fopen("source.txt", "r");
if(!fp)
{
perror("无法打开 source.txt");
return -1;
}
char buffer[100]; // 用于存放读取的行
int line_count = 0;
while(line_count < 3)
{
// 尝试读取一行
if(fgets(buffer, sizeof(buffer), fp) == NULL)
{
// 如果已经EOF或出错,就停止
break;
}
// 将读取到的一行打印到屏幕
fputs(buffer, stdout);
line_count++;
}
fclose(fp);
return 0;
}
示例输出
(视source.txt内容而定, 显示该文件前3行)
:three: 格式化读写:fprintf()
、fscanf()
常见格式化占位符
占位符 | 对应类型 | 示例 |
---|---|---|
%d | int | printf("%d", 123); |
%ld | long | printf("%ld", 123456789L); |
%f | float/double | printf("%f", 3.14); |
%c | char | printf("%c", 'A'); |
%s | 字符串 | printf("%s", "Hello"); |
... | ... | ... |
在 scanf/fscanf
中,需要根据实际变量类型来使用匹配的占位符,否则可能出现读取错误或崩溃。
精确控制输入输出的技巧
- 可以使用宽度限定,如:
%5s
表示最多读入5个字符(含终止符)。 - 可以使用
%*d
来跳过某个整数而不赋值。 - 可以在输出中用格式控制对齐方式或小数精度:如
printf("%8.2f", 3.14159);
。
示例:读取并写入学生成绩表:
#include <stdio.h>
int main(void)
{
FILE *fp_in = fopen("scores.txt", "r"); // 形式: 姓名 分数
FILE *fp_out = fopen("results.txt", "w");
if(!fp_in || !fp_out)
{
perror("无法打开输入或输出文件");
return -1;
}
char name[50];
int score;
// 从scores.txt逐行读取 "姓名 分数",直到读不出
while(fscanf(fp_in, "%s %d", name, &score) == 2)
{
// 写入results.txt,附加一些文字
fprintf(fp_out, "学生: %s 的分数是: %d\n", name, score);
}
fclose(fp_in);
fclose(fp_out);
printf("处理完成,结果写入 results.txt\n");
return 0;
}
示例输出
处理完成,结果写入 results.txt
(查看 results.txt
可以看到格式化后的成绩列表)
2.3 文件读写:二进制模式
:one: 读写函数
-
用于二进制块读写,常配合结构体或字节数组。
-
函数签名:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr
:数据存放/读取的内存地址size
:单个数据块的大小(字节)nmemb
:数据块的个数- 函数返回实际读/写的块数(可能小于
nmemb
如果到达EOF或出错)
:two: 结构体读写
示例:写入并读取一个结构体到二进制文件:
#include <stdio.h>
#include <stdlib.h>
// 定义一个简单的结构体
typedef struct {
int id; // 4字节
float score; // 4字节
} Record;
int main(void)
{
// 1. 写阶段
FILE *fp_write = fopen("data.bin", "wb"); // wb: write binary
if(!fp_write)
{
perror("无法打开data.bin写");
return -1;
}
Record rec_out = {1001, 98.5f};
// fwrite(地址, 单个结构体大小, 写几个, 流)
size_t written = fwrite(&rec_out, sizeof(Record), 1, fp_write);
if(written < 1)
{
perror("写结构体出错");
}
fclose(fp_write);
// 2. 读阶段
FILE *fp_read = fopen("data.bin", "rb"); // rb: read binary
if(!fp_read)
{
perror("无法打开data.bin读");
return -1;
}
Record rec_in;
size_t read_count = fread(&rec_in, sizeof(Record), 1, fp_read);
if(read_count < 1)
{
perror("读结构体出错或EOF");
}
fclose(fp_read);
// 3. 打印读到的结构体内容
printf("从data.bin读到结构体: id=%d, score=%.2f\n", rec_in.id, rec_in.score);
return 0;
}
编译 & 运行示例
gcc binary_rw.c -o binary_rw
./binary_rw
示例输出
从data.bin读到结构体: id=1001, score=98.50
查看 data.bin
是一堆不可读的二进制数据。
:three: 大小端问题
- 如果在不同CPU架构(如 x86(小端) vs. ARM大端) 之间共享二进制文件,可能导致读到的字节序不匹配,引起数值错误。
- 常见解决方案:采用“网络字节序”或统一用小端储存,或者以文本格式(如 JSON、XML)进行跨平台传输。
- 在嵌入式系统中,我们可以使用字节序转换函数(如
htons()
,htonl()
等)或自己写宏来确保一致性。
(╯°□°)╯︵ ┻━┻ 大小端问题是老生常谈,一定要在跨平台或网络通信时留意。
第三章 文件操作技巧
3.1 文件定位
:one: fseek
文件定位
fseek(FILE *stream, long offset, int whence)
-
用于在文件中移动“文件指针”(即当前读写位置,也可称“游标”)。
-
offset
:偏移量(字节数,可正可负)。 -
whence
:基准位置,常见取值:
SEEK_SET
:文件开头SEEK_CUR
:当前位置SEEK_END
:文件末尾
示例:fseek(fp, 10, SEEK_SET)
表示从文件开头向后移动 10 个字节,把游标定位到第10字节处。
:two: ftell
当前位置
ftell(FILE *stream)
- 返回当前“文件指针”相对于文件开头的偏移量(单位:字节)。
- 如果出错(比如对无效流调用),返回 -1L 并设置
errno
。
:three: rewind
文件开头
rewind(FILE *stream)
- 等价于
fseek(stream, 0, SEEK_SET)
,即将文件指针移动到文件开头。 - 同时清除
ferror()
和feof()
状态。
:four: 文本模式与二进制模式下的差异
-
二进制模式 (binary mode):
fseek()
和ftell()
的偏移量通常就是字节数,定位比较直观。 -
文本模式 (text mode):在某些系统(尤其是 Windows)中,换行符
\n
可能在实际文件中被存储为\r\n
,导致fseek()
的计算会与想象中的字符数不完全一致。 -
在绝大多数 Linux/Unix 系统中,文本文件只使用
\n
作为换行符,所以文本模式和二进制模式定位差别不大。不过,在可移植场合还是要小心。((╯°□°)╯︵ ┻━┻ Windows的换行符又来捣乱……)
因此,如果需要跨平台精确定位文件中的字节位置,最好使用二进制模式 (
"rb"
,"wb"
, etc.`) 打开文件,或者对换行符做适配处理。
:five: “文件游标”的概念
- 当我们打开一个文件时,系统内部会维护一个“文件指针”或“游标”,记录当前读取或写入的位置。
- 每次执行读写操作,游标会向前移动相应的字节数,就像在磁带上不断往前读写一样。
fseek()
、ftell()
、rewind()
等函数就是操纵或读取游标位置的接口。
示意图:
文件内容: [字节0][字节1][字节2] ... [字节N]
^
(游标当前位置)
📌示例1 读写并移动游标
下面的示例演示了如何:
- 打开文件
- 写入几行文本后,把游标移回文件开头
- 重新读取并输出文件中的文本
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// 1. 以读写模式打开文件 (若文件不存在则创建)
FILE *fp = fopen("seek_example.txt", "w+");
if(!fp)
{
perror("无法打开 seek_example.txt");
return -1;
}
// 2. 写入几行数据
fputs("第1行\n", fp); // 游标移动到第1行末尾
fputs("第2行\n", fp); // 游标移动到第2行末尾
fputs("第3行\n", fp); // 游标移动到第3行末尾
// 3. 获取当前游标位置
long pos = ftell(fp); // ftell返回相对于文件开头的字节偏移
printf("当前游标位置(字节) = %ld\n", pos);
// 4. 移动游标到文件开头
rewind(fp); // 或者 fseek(fp, 0, SEEK_SET);
// 5. 逐行读取并打印
char buffer[100];
while(fgets(buffer, sizeof(buffer), fp))
{
printf("读取到: %s", buffer);
}
// 6. 关闭文件
fclose(fp);
return 0;
}
编译 & 运行示例(Linux)
gcc fseek_ftell_example.c -o fseek_ftell_example
./fseek_ftell_example
示例输出(可能略有差异)
当前游标位置(字节) = 18
读取到: 第1行
读取到: 第2行
读取到: 第3行
(你也可以用 cat seek_example.txt
查看文件内容。)
📌 示例2 从文件末尾倒数 N 个字节读取
有时我们想读取“距离文件末尾 N 字节处的内容”,这在查看日志尾部、读取文件尾标记等场景很常见。我们可以使用 fseek(fp, -N, SEEK_END)
来实现。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("testdata.bin", "rb");
if(!fp)
{
perror("无法打开 testdata.bin");
return -1;
}
// 定义要倒数读取的大小,例如 10 个字节
int N = 10;
// 移动到文件末尾倒数 N 字节处
// SEEK_END 表示文件末尾为基准点
if(fseek(fp, -N, SEEK_END) != 0)
{
perror("移动游标出错 - 文件可能比10字节还小");
fclose(fp);
return -1;
}
// 读取这 N 个字节到一个缓冲区
char buffer[11]; // 多留1字节给'\0'
// fread(目的地址, 每块大小, 块个数, 流)
size_t read_count = fread(buffer, 1, N, fp);
// 确保以'\0'结尾,便于打印(若只是二进制数据需要另处理)
buffer[read_count] = '\0';
printf("文件末尾倒数%d个字节: %s\n", N, buffer);
fclose(fp);
return 0;
}
编译 & 运行示例
gcc fseek_end_example.c -o fseek_end_example
./fseek_end_example
示例输出(视 testdata.bin
的实际内容而定)
文件末尾倒数10个字节: ...SomeTailData...
(如果文件不足10个字节,则会报错或读取不到那么多字节。)
3.2 错误与结束判断
:one: 函数说明
int ferror(FILE *stream)
:检查流中是否发生了读写错误。若出错则返回非0。int feof(FILE *stream)
:检查是否已到达文件末尾(EOF)。若到达则返回非0。
重要提示:
feof()
只有在试图读取EOF后才会返回非0(并不是在真正到达EOF时立刻被置位)。所以一般写法是在读函数返回值为“读取失败”后,再用feof()
确认是否EOF导致,还是其他错误。EOF
是一个宏,通常定义为-1
。
:two: EOF
的含义
EOF
(End Of File) 并不一定代表“真正结束了文件”,有时也可能是因为读取出错而返回EOF
。- 读/写函数返回
EOF
时,需要配合ferror()
或feof()
来进一步判断。
:three: 异常处理
- 如果
ferror(stream)
返回非0,可根据具体错误决定是否重试或退出。 - 若文件IO是关键操作(如写日志、配置信息),应加更多健壮性检查,以免数据丢失。
#include <stdio.h>
int main(void)
{
FILE *fp = fopen("test.txt", "r");
if(!fp)
{
perror("无法打开 test.txt");
return -1;
}
int ch;
// 逐字符读取
while((ch = fgetc(fp)) != EOF)
{
putchar(ch); // 打印到stdout
}
// 读取失败跳出循环后,需要判断是EOF还是错误
if(ferror(fp))
{
// 如果 ferror(fp)为非0,说明出错
perror("读文件时出错");
}
else if(feof(fp))
{
// 如果feof(fp)非0,说明是正常到达文件末尾
printf("\n已到达文件末尾.\n");
}
fclose(fp);
return 0;
}
示例输出
(视test.txt内容而定,若正常读取到末尾,就显示: 已到达文件末尾.)
- 感谢你赐予我前进的力量