第17章 文件操作(一):文件的概念、文件的打开与关闭、文件的读写
各位同学,今天我们来聊聊C语言里一个非常实用的主题——文件操作。说实话,我刚开始学C的时候,觉得文件操作挺神秘的。程序跑完就没了,数据怎么保存下来?后来才明白,文件就是程序和外部世界沟通的桥梁。
我个人习惯把文件操作比作「存取快递」。你要先找到快递柜(打开文件),然后往里面放东西(写文件)或者取东西(读文件),最后记得关好柜门(关闭文件)。这个比喻虽然简单,但核心逻辑就是这么回事。
17.1 文件的概念
文件是什么?说白了,就是存储在外部介质上的一组相关数据的集合。在C语言里,文件被抽象成一个「流」(stream)。你不需要关心数据到底存在硬盘的哪个扇区,只需要通过流来读写数据就行。
C语言把文件分为两类:
- 文本文件:存储的是字符序列。每个字符对应一个ASCII码。你用记事本打开能看懂的那种。
- 二进制文件:存储的是数据的二进制形式。比如一个int类型的整数12345,在二进制文件里就是4个字节的原始数据。你用记事本打开会看到乱码。
我记得刚入行时,有个同事把二进制文件当文本文件读,结果数据全乱了。嗯,这里要注意:读写方式必须匹配,否则会出大问题。
核心区别:
- 文本文件:按字符读写,遇到换行符会做转换(Windows下\r\n转\n)
- 二进制文件:按字节原样读写,不做任何转换
你想想看,如果你在Linux上写了一个文本文件,拿到Windows上打开,换行符可能就乱了。这就是文本模式带来的麻烦。所以跨平台传输数据时,我建议用二进制模式。
17.2 文件的打开与关闭
在C语言里,操作文件的第一步就是打开它。标准库提供了fopen函数,原型是这样的:
FILE *fopen(const char *filename, const char *mode);
它返回一个FILE *指针。如果打开失败,返回NULL。这个指针就是你和文件之间的「遥控器」,所有后续操作都靠它。
第二个参数mode决定了你怎么使用这个文件。我整理了一张表,你一看就明白:
| 模式 | 含义 | 文件不存在时 | 文件存在时 |
|---|---|---|---|
"r" |
只读(文本) | 返回NULL | 正常打开,位置在开头 |
"w" |
只写(文本) | 创建新文件 | 清空原有内容 |
"a" |
追加(文本) | 创建新文件 | 位置在末尾,保留原有内容 |
"rb" |
只读(二进制) | 返回NULL | 正常打开 |
"wb" |
只写(二进制) | 创建新文件 | 清空原有内容 |
"ab" |
追加(二进制) | 创建新文件 | 位置在末尾 |
"r+" |
读写(文本) | 返回NULL | 正常打开,位置在开头 |
"w+" |
读写(文本) | 创建新文件 | 清空原有内容 |
"a+" |
读写追加(文本) | 创建新文件 | 位置在末尾 |
打开文件后,一定要记得关闭。用fclose函数:
int fclose(FILE *stream);
关闭成功返回0,失败返回EOF。为什么要关闭?因为每个进程能打开的文件数量是有限的。我曾经在项目里见过一个bug:程序跑了一整天后突然打不开文件了,排查了半天才发现是文件打开后没关闭,文件描述符泄漏了。嗯,这种错误很隐蔽,但后果很严重。
避坑指南: 每次fopen后,一定要检查返回值是否为NULL。我曾经见过新手直接拿NULL指针去读写,程序直接崩溃。养成习惯:
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("文件打开失败");
return -1;
}
17.3 文件的读写——字符级操作
文件打开后,就可以开始读写了。先从最基础的字符级操作说起。
17.3.1 fgetc:读一个字符
int fgetc(FILE *stream);
这个函数从文件中读取一个字符。返回的是int类型,不是char。为什么?因为除了返回字符的ASCII码,它还要返回EOF(-1)来表示文件结束。如果用char,-1和0xFF就分不清了。
来看个例子:
#include <stdio.h>
int main() {
FILE *fp = fopen("hello.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
return 0;
}
这段代码逐字符读取文件内容并打印到屏幕上。注意循环条件:(ch = fgetc(fp)) != EOF。这个写法很经典,赋值和判断一步完成。
17.3.2 fputc:写一个字符
int fputc(int c, FILE *stream);
把字符c写入文件。成功返回写入的字符,失败返回EOF。
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
fputc('H', fp);
fputc('e', fp);
fputc('l', fp);
fputc('l', fp);
fputc('o', fp);
fclose(fp);
这段代码会在output.txt里写入"Hello"。很简单,对吧?
小技巧: 如果你要写入一个字符串,别用fputc一个个写,太慢了。后面会介绍fputs和fprintf,效率高得多。
17.4 文件的读写——字符串级操作
字符级操作虽然灵活,但效率不高。实际项目中,我们更常用字符串级的读写函数。
17.4.1 fgets:读一行字符串
char *fgets(char *s, int size, FILE *stream);
这个函数从文件中读取一行,最多读取size - 1个字符。遇到换行符或EOF就停止。读取的内容会存入s指向的缓冲区,并在末尾自动添加\0。
成功返回s,失败或读到文件末尾返回NULL。
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
char buffer[256];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("读取到: %s", buffer);
}
fclose(fp);
return 0;
}
这里有个细节:fgets会保留换行符。如果一行内容超过size - 1个字符,fgets只读取前size - 1个,剩下的留在下一次读取。所以缓冲区大小要合理设置,我一般设成256或1024。
避坑指南: 我曾经在项目里看到有人用gets函数。千万别用!gets不检查缓冲区大小,很容易造成缓冲区溢出。标准库已经把它标记为废弃了。永远用fgets代替gets。
17.4.2 fputs:写一行字符串
int fputs(const char *s, FILE *stream);
把字符串s写入文件。注意:fputs不会自动添加换行符。你需要自己加\n。
FILE *fp = fopen("log.txt", "a");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
fputs("系统启动成功\n", fp);
fputs("用户登录: admin\n", fp);
fclose(fp);
这段代码以追加模式打开文件,写入两行日志。每次运行都会在文件末尾追加内容,不会覆盖之前的记录。这在写日志文件时非常实用。
17.5 知识体系总览
说了这么多,我们来画一张图,把本章的知识点串起来。这样你脑子里会有一个清晰的框架。
17.6 综合示例:文件复制工具
最后,我们来写一个完整的例子。把source.txt的内容复制到dest.txt。这个程序综合运用了本章的所有知识点。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *src, *dst;
int ch;
// 打开源文件(只读)
src = fopen("source.txt", "r");
if (src == NULL) {
perror("无法打开源文件");
return 1;
}
// 打开目标文件(只写)
dst = fopen("dest.txt", "w");
if (dst == NULL) {
perror("无法创建目标文件");
fclose(src);
return 1;
}
// 逐字符复制
while ((ch = fgetc(src)) != EOF) {
fputc(ch, dst);
}
printf("文件复制完成!\n");
// 关闭文件
fclose(src);
fclose(dst);
return 0;
}
这个程序虽然简单,但包含了文件操作的所有关键步骤:打开、检查、读写、关闭。你想想看,如果中间漏了任何一个检查,程序都可能出问题。
个人建议: 在实际项目中,我习惯把文件操作封装成函数。比如open_file_safe()、read_line_safe(),这样错误处理逻辑可以复用,代码也更清晰。
好了,这一章的内容就到这里。文件操作是C语言里非常实用的技能,几乎每个项目都会用到。多写几个例子,你就能熟练掌握。
公众号:蓝海资料掘金营,微信deep3321