14、文件操作(上):文件指针、fopen/fclose、fgetc/fputc、fgets/fputs
文件操作,说白了就是让程序跟硬盘上的文件打交道。你想想看,程序跑在内存里,一断电数据全没了。但文件不一样,它存在硬盘上,关了机再打开,数据还在。这就是文件操作的价值所在。
我个人习惯把文件操作分成三步:打开文件、读写文件、关闭文件。这三步缺一不可,尤其是最后一步,很多人容易忘。嗯,咱们今天就把这三步彻底讲透。
14.1 文件指针——操作文件的“遥控器”
在C语言里,所有文件操作都离不开一个东西——FILE指针。你可以把它理解成一个遥控器,你按哪个键(调用哪个函数),它就帮你操作对应的文件。
FILE是一个结构体类型,定义在FILE指针。
声明一个文件指针很简单:
FILE *fp; // fp就是文件指针变量
注意,这时候fp还没指向任何文件,它只是个空壳子。真正让它“上岗”的,是fopen函数。
核心要点:文件指针是文件操作的“身份证”,没有它,寸步难行。
14.2 fopen / fclose——开门与关门
fopen负责打开文件,fclose负责关闭文件。这两个函数必须成对出现,就像你进门要开门,出门要关门一样。
14.2.1 fopen:打开文件
函数原型:
FILE *fopen(const char *filename, const char *mode);
第一个参数是文件名,第二个参数是打开模式。模式决定了你能对文件做什么——读、写、还是追加。
常用的模式有这些:
| 模式 | 含义 | 文件不存在时 | 文件存在时 |
|---|---|---|---|
| "r" | 只读 | 返回NULL | 正常打开 |
| "w" | 只写 | 创建新文件 | 清空内容 |
| "a" | 追加 | 创建新文件 | 保留内容,在末尾写 |
| "r+" | 读写 | 返回NULL | 正常打开 |
| "w+" | 读写 | 创建新文件 | 清空内容 |
| "a+" | 读写追加 | 创建新文件 | 保留内容,在末尾写 |
我在项目中遇到过一个问题:用"w"模式打开一个配置文件,结果文件被清空了。后来才发现,那个文件里存着系统参数,我只是想读一下,却用了写模式。嗯,从此以后,读文件一定用"r",写文件才用"w",这个习惯我一直保持到现在。
代码示例:
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
printf("文件打开失败!\n");
return -1;
}
// 接下来可以读文件了...
重要:fopen返回NULL时,一定要处理。不要假设文件一定存在。我曾经见过一个程序,没检查返回值就直接用fp读写,结果程序直接崩溃了。
14.2.2 fclose:关闭文件
函数原型:
int fclose(FILE *stream);
关闭文件有两个作用:一是把缓冲区里没写完的数据刷到硬盘上,二是释放系统资源。如果你只fopen不fclose,程序跑久了,系统会告诉你“打开的文件太多了”。
代码示例:
fclose(fp); // 关闭文件
fp = NULL; // 好习惯:置空指针
个人技巧:我习惯在fclose之后立刻把fp置为NULL。这样万一后面不小心用了这个指针,程序会立刻崩溃在明处,而不是在某个诡异的地方出bug。
14.3 fgetc / fputc——一个字符一个字符地读写
这两个函数是最基础的文件读写函数。一次只操作一个字符,简单但有效。
14.3.1 fputc:写一个字符
函数原型:
int fputc(int ch, FILE *stream);
把字符ch写到文件里。成功返回写入的字符,失败返回EOF。
代码示例:
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("打开文件失败\n");
return -1;
}
fputc('H', fp);
fputc('e', fp);
fputc('l', fp);
fputc('l', fp);
fputc('o', fp);
fclose(fp);
运行后,output.txt里就写入了"Hello"。
14.3.2 fgetc:读一个字符
函数原型:
int fgetc(FILE *stream);
从文件里读一个字符。成功返回字符的ASCII码,读到文件末尾或出错时返回EOF。
代码示例:
FILE *fp = fopen("output.txt", "r");
if (fp == NULL) {
printf("打开文件失败\n");
return -1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch); // 输出到屏幕
}
fclose(fp);
这个循环很经典,逐字符读取直到文件结束。注意ch要声明为int而不是char,因为EOF的值是-1,char可能存不下。
避坑指南:我曾经在嵌入式项目里用char类型接收fgetc的返回值,结果文件里有个0xFF的字节,被当成EOF提前结束了循环。排查了半天才发现是类型问题。从此以后,fgetc的返回值一律用int。
14.4 fgets / fputs——按行读写,效率更高
逐字符读写太慢了,尤其是处理大文件的时候。fgets和fputs按行操作,效率高得多。
14.4.1 fputs:写一个字符串
函数原型:
int fputs(const char *str, FILE *stream);
把字符串str写入文件。注意,它不会自动加换行符。如果你需要换行,得自己加\n。
代码示例:
FILE *fp = fopen("log.txt", "a");
if (fp == NULL) {
printf("打开文件失败\n");
return -1;
}
fputs("系统启动成功\n", fp);
fputs("用户登录\n", fp);
fclose(fp);
这里用了"a"模式,每次写入都追加到文件末尾,不会覆盖之前的内容。做日志文件时特别实用。
14.4.2 fgets:读一行字符串
函数原型:
char *fgets(char *str, int n, FILE *stream);
从文件里读一行,最多读n-1个字符,遇到换行符或EOF就停止。读到的内容会存到str里,包括换行符(如果有的话)。
代码示例:
FILE *fp = fopen("log.txt", "r");
if (fp == NULL) {
printf("打开文件失败\n");
return -1;
}
char buffer[256];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("读到一行:%s", buffer);
}
fclose(fp);
注意fgets返回NULL时表示读到文件末尾或出错。这个循环会一直读到文件结束。
实用建议:fgets的第二个参数n一定要给够。如果一行数据超过n-1个字符,fgets会分多次读取,每次读n-1个字符。我一般给256或1024,够用又不浪费内存。
14.5 知识体系总览
下面这张图把本章的核心知识点串起来了。你一看就明白:文件操作就是围绕文件指针展开的,打开、读写、关闭,三步走。
14.6 综合示例:文件复制程序
把上面的知识串起来,写一个文件复制程序。从源文件读,写到目标文件里。
#include <stdio.h>
int main() {
FILE *src, *dst;
int ch;
// 打开源文件(只读)
src = fopen("source.txt", "r");
if (src == NULL) {
printf("无法打开源文件\n");
return -1;
}
// 打开目标文件(只写)
dst = fopen("target.txt", "w");
if (dst == NULL) {
printf("无法创建目标文件\n");
fclose(src);
return -1;
}
// 逐字符复制
while ((ch = fgetc(src)) != EOF) {
fputc(ch, dst);
}
printf("文件复制完成!\n");
// 关闭文件
fclose(src);
fclose(dst);
return 0;
}
这个程序虽然简单,但包含了文件操作的全部核心步骤。你可以试试改成用fgets/fputs实现,效率会更高。
总结一下:文件操作没那么神秘。记住三步走——打开、读写、关闭。再记住四个函数——fgetc/fputc、fgets/fputs。剩下的就是多写多练了。
公众号:蓝海资料掘金营,微信 deep3321