C语言文件操作完全指南
写程序的时候,数据怎么存是个大问题。你肯定遇到过这种情况:程序跑得好好的,结果一关掉,刚才输入的数据全没了。下次再运行,又得重新来一遍。
这就是文件发挥作用的地方——它能让数据持久化保存,程序退出后数据还在,下次运行还能读出来继续用。
今天就把C语言文件操作从头到尾梳理一遍,包括文件的打开关闭、读写方式、随机定位、结束判定这些核心知识点。
一、为什么需要文件?
程序运行时,数据都存在内存里。内存有个特点:程序一退出,内存回收,数据就没了。
如果你想让数据保存下来,比如:
- 游戏存档
- 配置信息
- 日志记录
- 用户数据
那就得把数据写到文件里,存到硬盘上。硬盘上的数据不会因为程序退出而丢失,这就是文件的意义——数据持久化。
二、什么是文件?
从程序员的角度看,文件分两类:
程序文件
跟程序本身相关的文件:
- 源程序文件(.c)
- 目标文件(.obj)
- 可执行文件(.exe)
数据文件
程序运行时读写的数据,比如:
- 从文件读取输入数据
- 把结果输出到文件
本章讨论的是数据文件。
文件名
一个完整的文件名包含三部分:
文件路径 + 文件名主干 + 文件后缀
比如 c:\\code\\test.txt,平时我们说的"文件名"通常就指这个完整路径。
三、二进制文件和文本文件
根据数据的存储形式,数据文件分为文本文件和二进制文件。
存储方式的区别
- 文本文件:数据以ASCII码形式存储,每个字符占一个字节
- 二进制文件:数据以二进制形式直接存储,不加转换
举个例子,整数 10000:
| 文本文件 | 5字节 | ‘1’ ‘0’ ‘0’ ‘0’ ‘0’ 五个字符的ASCII码 |
| 二进制文件 | 4字节 | 10000的二进制表示(int类型) |
文本文件存的是字符,二进制文件存的是原始数据。
写个测试代码
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb"); // 以二进制写入模式打开
fwrite(&a, 4, 1, pf); // 把a的二进制形式写入文件
fclose(pf);
pf = NULL;
return 0;
}
用VS打开这个文件,选择"二进制编辑器",就能看到 10000 的二进制表示(小端存储):10 27 00 00。
四、文件的打开和关闭
流(Stream)的概念
C程序要跟外部设备(键盘、显示器、硬盘等)打交道,不同设备的操作方式不一样。为了统一处理,C语言抽象出了流的概念——你可以把它想象成一条流淌着字符的河。
一般操作流程是:打开流 → 读写数据 → 关闭流。
三个标准流
C程序启动时,默认打开了三个流:
| stdin | 标准输入流 | 通常对应键盘,scanf 就从这里读 |
| stdout | 标准输出流 | 通常对应显示器,printf 往这里写 |
| stderr | 标准错误流 | 通常对应显示器,用于输出错误信息 |
这三个流的类型都是 FILE*,叫文件指针。我们用 scanf、printf 时没感觉在"打开流",就是因为它们已经默认打开了。
文件指针 FILE*
每个被使用的文件,系统都会在内存里开辟一个文件信息区,存放文件的各种信息(文件名、状态、当前位置等)。这些信息保存在一个 FILE 结构体里。
// VS2013 中 FILE 的定义(不同编译器可能略有差异)
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
我们通过 FILE* 指针来操作文件:
FILE* pf; // 定义一个文件指针变量
pf 指向某个文件的文件信息区,通过它就能间接访问那个文件。
fopen 和 fclose
文件读写前要先打开,用完后要关闭。
// 打开文件
FILE* fopen(const char* filename, const char* mode);
// 关闭文件
int fclose(FILE* stream);
filename 是文件名,mode 是打开模式。
文件打开模式
| "r" | 只读(文本文件) | 出错 |
| "w" | 只写(文本文件) | 创建新文件 |
| "a" | 追加(文本文件) | 创建新文件 |
| "rb" | 只读(二进制文件) | 出错 |
| "wb" | 只写(二进制文件) | 创建新文件 |
| "ab" | 追加(二进制文件) | 创建新文件 |
| "r+" | 读写(文本文件) | 出错 |
| "w+" | 读写(文本文件) | 创建新文件 |
| "a+" | 读写(文本文件,从末尾开始) | 创建新文件 |
注意:"w" 模式会清空原文件内容!如果文件已存在,用 "w" 打开后文件就变成空的了。想保留原内容并在后面追加,用 "a" 模式。
基本使用示例
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("myfile.txt", "w"); // 以写入模式打开文件
if (pFile != NULL) // 检查是否打开成功
{
fputs("fopen example", pFile); // 写入字符串
fclose(pFile); // 关闭文件
}
return 0;
}
养成好习惯:打开文件后一定要检查返回值是否为 NULL,防止文件打开失败导致后续操作出错。
五、文件的顺序读写
顺序读写就是从头到尾依次读写,文件指针自动往后移动。
常用读写函数
| fgetc | 读取一个字符 | 所有输入流 |
| fputc | 写入一个字符 | 所有输出流 |
| fgets | 读取一行 | 所有输入流 |
| fputs | 写入一行 | 所有输出流 |
| fscanf | 格式化读取 | 所有输入流 |
| fprintf | 格式化写入 | 所有输出流 |
| fread | 二进制读取 | 文件输入流 |
| fwrite | 二进制写入 | 文件输出流 |
"所有输入流"包括 stdin 和文件输入流,"所有输出流"包括 stdout 和文件输出流。
fgetc / fputc —— 字符读写
int fgetc(FILE* stream); // 从流中读取一个字符
int fputc(int c, FILE* stream); // 向流中写入一个字符
示例:把文件内容逐字符读出来打印
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("打开文件失败");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) // 读到文件末尾返回EOF
{
putchar(ch);
}
fclose(fp);
return 0;
}
fgets / fputs —— 行读写
char* fgets(char* str, int n, FILE* stream); // 读取一行(最多n-1个字符)
int fputs(const char* str, FILE* stream); // 写入一行
fgets 会读到换行符或文件末尾,最多读 n-1 个字符,末尾自动加 \\0。
fscanf / fprintf —— 格式化读写
这两个函数跟 scanf、printf 用法几乎一样,只是多了一个文件指针参数:
int fscanf(FILE* stream, const char* format, ...);
int fprintf(FILE* stream, const char* format, ...);
示例:写入格式化数据,再读出来
#include <stdio.h>
int main()
{
FILE* fp = fopen("data.txt", "w");
if (fp == NULL) return 1;
// 写入格式化数据
fprintf(fp, "%d %f %s\\n", 100, 3.14, "hello");
fclose(fp);
// 重新打开读取
fp = fopen("data.txt", "r");
int a;
float b;
char s[20];
fscanf(fp, "%d %f %s", &a, &b, s);
printf("读出: %d, %.2f, %s\\n", a, b, s); // 读出: 100, 3.14, hello
fclose(fp);
return 0;
}
fread / fwrite —— 二进制读写
这两个是二进制读写的核心函数,适合读写结构体、数组等:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
- ptr:数据存放地址
- size:每个元素的大小
- count:元素个数
- 返回值:实际读写的元素个数
示例:写入并读取一个数组
#include <stdio.h>
int main()
{
double a[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
// 写入二进制文件
FILE* fp = fopen("data.bin", "wb");
fwrite(a, sizeof(double), 5, fp); // 写入5个double
fclose(fp);
// 读取二进制文件
double b[5];
fp = fopen("data.bin", "rb");
size_t ret = fread(b, sizeof(double), 5, fp);
if (ret == 5)
printf("读取成功\\n");
else
printf("读取不完整,只读了 %zu 个\\n", ret);
fclose(fp);
return 0;
}
scanf/fscanf/sscanf 对比
| scanf | 从标准输入(stdin)读取 |
| fscanf | 从指定文件流读取 |
| sscanf | 从字符串读取 |
同理,printf、fprintf、sprintf 是输出版本,sprintf 可以把格式化数据写到字符串里。
六、文件的随机读写
顺序读写只能从头到尾,但有时候我们需要跳来跳去,比如直接定位到文件中间某个位置。这就需要随机读写函数。
fseek —— 定位文件指针
int fseek(FILE* stream, long int offset, int origin);
- offset:偏移量
- origin:起始位置,有三个选项:
- SEEK_SET:文件开头
- SEEK_CUR:当前位置
- SEEK_END:文件末尾
示例:修改文件中间的内容
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("example.txt", "wb");
fputs("This is an apple.", pFile); // 写入字符串
fseek(pFile, 9, SEEK_SET); // 定位到第9个字节('a'的位置)
fputs(" sam", pFile); // 把 "an a" 替换成 " sam"
fclose(pFile);
return 0;
}
执行后文件内容变成:This is a sample.
ftell —— 获取当前位置
long int ftell(FILE* stream);
返回文件指针相对于开头的偏移量。常用技巧:配合 fseek 获取文件大小。
#include <stdio.h>
int main()
{
FILE* pFile;
long size;
pFile = fopen("myfile.txt", "rb");
if (pFile == NULL)
perror("打开文件失败");
else
{
fseek(pFile, 0, SEEK_END); // 定位到文件末尾
size = ftell(pFile); // 此时偏移量就是文件大小
fclose(pFile);
printf("文件大小: %ld 字节\\n", size);
}
return 0;
}
rewind —— 回到文件开头
void rewind(FILE* stream);
把文件指针重置到文件开头,相当于 fseek(fp, 0, SEEK_SET)。
示例:先写入26个字母,再从头读取
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile); // 写入 A-Z
rewind(pFile); // 回到开头
fread(buffer, 1, 26, pFile); // 读取刚才写入的内容
fclose(pFile);
buffer[26] = '\\0';
printf("%s\\n", buffer); // 输出: ABCDEFGHIJKLMNOPQRSTUVWXYZ
return 0;
}
七、文件读取结束的判定
feof 的正确用法
大坑警告:很多人以为 feof 返回真就表示读到文件末尾了,这是错误的理解。
feof 的真正作用是:在文件读取结束后,判断结束的原因是不是"遇到文件尾"。
正确的判断方式:
文本文件
- fgetc:判断返回值是否为 EOF
- fgets:判断返回值是否为 NULL
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意用int,不是char,因为要处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("打开文件失败");
return EXIT_FAILURE;
}
// fgetc 读取失败或遇到文件末尾都会返回EOF
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
// 判断结束原因
if (ferror(fp))
puts("读取时发生I/O错误");
else if (feof(fp))
puts("成功读到文件末尾");
fclose(fp);
return 0;
}
二进制文件
fread 返回实际读取的元素个数,判断是否小于预期个数:
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1., 2., 3., 4., 5.};
FILE* fp = fopen("test.bin", "wb");
fwrite(a, sizeof *a, SIZE, fp); // 先写入
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp);
if (ret_code == SIZE) {
puts("读取成功,内容:");
for (int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\\n');
}
else {
if (feof(fp))
printf("错误:文件意外结束\\n");
else if (ferror(fp))
perror("读取错误");
}
fclose(fp);
return 0;
}
总结一下判断逻辑:
八、文件缓冲区
缓冲机制
C语言采用缓冲文件系统:系统自动为每个正在使用的文件在内存中开辟一块缓冲区。
- 写文件时:数据先送到内存缓冲区,缓冲区装满后才一起写到磁盘
- 读文件时:从磁盘读入数据填充缓冲区,再从缓冲区逐个送给程序
这样做的目的是减少磁盘I/O次数,提高效率。
缓冲区的影响
看个实验:
#include <stdio.h>
#include <windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf); // 数据先进入输出缓冲区
printf("睡眠10秒——此时打开test.txt,发现文件是空的\\n");
Sleep(10000);
printf("刷新缓冲区\\n");
fflush(pf); // 强制把缓冲区内容写入磁盘
printf("再睡眠10秒——此时打开test.txt,文件有内容了\\n");
Sleep(10000);
fclose(pf); // fclose 也会刷新缓冲区
pf = NULL;
return 0;
}
运行这个程序,你会发现:
- 写入 fputs 后立即打开文件,内容还没进去
- 调用 fflush 后,内容才真正写入磁盘
- fclose 关闭文件时也会自动刷新缓冲区
结论
重要:因为有缓冲区的存在,操作文件时要注意:
- 写完数据后及时 fflush 或 fclose
- 否则程序异常退出时,缓冲区数据可能丢失
九、总结与速查表
文件操作流程
打开文件 (fopen) → 检查是否成功 → 读写数据 → 关闭文件 (fclose)
核心函数速查
| 打开文件 | fopen | 返回 FILE*,失败返回 NULL |
| 关闭文件 | fclose | 关闭并刷新缓冲区 |
| 字符读写 | fgetc / fputc | 一次一个字符 |
| 行读写 | fgets / fputs | 一次一行 |
| 格式化读写 | fscanf / fprintf | 类似 scanf/printf |
| 二进制读写 | fread / fwrite | 读写任意类型数据 |
| 定位 | fseek | 移动文件指针 |
| 获取位置 | ftell | 返回当前位置 |
| 回到开头 | rewind | 重置到文件开头 |
| 判断结束原因 | feof | 读完后判断是否因EOF结束 |
| 判断错误 | ferror | 判断是否发生I/O错误 |
| 刷新缓冲区 | fflush | 强制写入磁盘 |
常见易错点
把这些知识点搞清楚,C语言文件操作就基本拿下了。多写几个小练习——比如实现一个简单的配置文件读写、日志记录功能,手感很快就上来了。
C语言文件操作完全指南
写程序的时候,数据怎么存是个大问题。你肯定遇到过这种情况:程序跑得好好的,结果一关掉,刚才输入的数据全没了。下次再运行,又得重新来一遍。
这就是文件发挥作用的地方——它能让数据持久化保存,程序退出后数据还在,下次运行还能读出来继续用。
今天就把C语言文件操作从头到尾梳理一遍,包括文件的打开关闭、读写方式、随机定位、结束判定这些核心知识点。
一、为什么需要文件?
程序运行时,数据都存在内存里。内存有个特点:程序一退出,内存回收,数据就没了。
如果你想让数据保存下来,比如:
- 游戏存档
- 配置信息
- 日志记录
- 用户数据
那就得把数据写到文件里,存到硬盘上。硬盘上的数据不会因为程序退出而丢失,这就是文件的意义——数据持久化。
二、什么是文件?
从程序员的角度看,文件分两类:
程序文件
跟程序本身相关的文件:
- 源程序文件(.c)
- 目标文件(.obj)
- 可执行文件(.exe)
数据文件
程序运行时读写的数据,比如:
- 从文件读取输入数据
- 把结果输出到文件
本章讨论的是数据文件。
文件名
一个完整的文件名包含三部分:
文件路径 + 文件名主干 + 文件后缀
比如 c:\\code\\test.txt,平时我们说的"文件名"通常就指这个完整路径。
三、二进制文件和文本文件
根据数据的存储形式,数据文件分为文本文件和二进制文件。
存储方式的区别
- 文本文件:数据以ASCII码形式存储,每个字符占一个字节
- 二进制文件:数据以二进制形式直接存储,不加转换
举个例子,整数 10000:
| 文本文件 | 5字节 | ‘1’ ‘0’ ‘0’ ‘0’ ‘0’ 五个字符的ASCII码 |
| 二进制文件 | 4字节 | 10000的二进制表示(int类型) |
文本文件存的是字符,二进制文件存的是原始数据。
写个测试代码
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb"); // 以二进制写入模式打开
fwrite(&a, 4, 1, pf); // 把a的二进制形式写入文件
fclose(pf);
pf = NULL;
return 0;
}
用VS打开这个文件,选择"二进制编辑器",就能看到 10000 的二进制表示(小端存储):10 27 00 00。
四、文件的打开和关闭
流(Stream)的概念
C程序要跟外部设备(键盘、显示器、硬盘等)打交道,不同设备的操作方式不一样。为了统一处理,C语言抽象出了流的概念——你可以把它想象成一条流淌着字符的河。
一般操作流程是:打开流 → 读写数据 → 关闭流。
三个标准流
C程序启动时,默认打开了三个流:
| stdin | 标准输入流 | 通常对应键盘,scanf 就从这里读 |
| stdout | 标准输出流 | 通常对应显示器,printf 往这里写 |
| stderr | 标准错误流 | 通常对应显示器,用于输出错误信息 |
这三个流的类型都是 FILE*,叫文件指针。我们用 scanf、printf 时没感觉在"打开流",就是因为它们已经默认打开了。
文件指针 FILE*
每个被使用的文件,系统都会在内存里开辟一个文件信息区,存放文件的各种信息(文件名、状态、当前位置等)。这些信息保存在一个 FILE 结构体里。
// VS2013 中 FILE 的定义(不同编译器可能略有差异)
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
我们通过 FILE* 指针来操作文件:
FILE* pf; // 定义一个文件指针变量
pf 指向某个文件的文件信息区,通过它就能间接访问那个文件。
fopen 和 fclose
文件读写前要先打开,用完后要关闭。
// 打开文件
FILE* fopen(const char* filename, const char* mode);
// 关闭文件
int fclose(FILE* stream);
filename 是文件名,mode 是打开模式。
文件打开模式
| "r" | 只读(文本文件) | 出错 |
| "w" | 只写(文本文件) | 创建新文件 |
| "a" | 追加(文本文件) | 创建新文件 |
| "rb" | 只读(二进制文件) | 出错 |
| "wb" | 只写(二进制文件) | 创建新文件 |
| "ab" | 追加(二进制文件) | 创建新文件 |
| "r+" | 读写(文本文件) | 出错 |
| "w+" | 读写(文本文件) | 创建新文件 |
| "a+" | 读写(文本文件,从末尾开始) | 创建新文件 |
注意:"w" 模式会清空原文件内容!如果文件已存在,用 "w" 打开后文件就变成空的了。想保留原内容并在后面追加,用 "a" 模式。
基本使用示例
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("myfile.txt", "w"); // 以写入模式打开文件
if (pFile != NULL) // 检查是否打开成功
{
fputs("fopen example", pFile); // 写入字符串
fclose(pFile); // 关闭文件
}
return 0;
}
养成好习惯:打开文件后一定要检查返回值是否为 NULL,防止文件打开失败导致后续操作出错。
五、文件的顺序读写
顺序读写就是从头到尾依次读写,文件指针自动往后移动。
常用读写函数
| fgetc | 读取一个字符 | 所有输入流 |
| fputc | 写入一个字符 | 所有输出流 |
| fgets | 读取一行 | 所有输入流 |
| fputs | 写入一行 | 所有输出流 |
| fscanf | 格式化读取 | 所有输入流 |
| fprintf | 格式化写入 | 所有输出流 |
| fread | 二进制读取 | 文件输入流 |
| fwrite | 二进制写入 | 文件输出流 |
"所有输入流"包括 stdin 和文件输入流,"所有输出流"包括 stdout 和文件输出流。
fgetc / fputc —— 字符读写
int fgetc(FILE* stream); // 从流中读取一个字符
int fputc(int c, FILE* stream); // 向流中写入一个字符
示例:把文件内容逐字符读出来打印
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("打开文件失败");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) // 读到文件末尾返回EOF
{
putchar(ch);
}
fclose(fp);
return 0;
}
fgets / fputs —— 行读写
char* fgets(char* str, int n, FILE* stream); // 读取一行(最多n-1个字符)
int fputs(const char* str, FILE* stream); // 写入一行
fgets 会读到换行符或文件末尾,最多读 n-1 个字符,末尾自动加 \\0。
fscanf / fprintf —— 格式化读写
这两个函数跟 scanf、printf 用法几乎一样,只是多了一个文件指针参数:
int fscanf(FILE* stream, const char* format, ...);
int fprintf(FILE* stream, const char* format, ...);
示例:写入格式化数据,再读出来
#include <stdio.h>
int main()
{
FILE* fp = fopen("data.txt", "w");
if (fp == NULL) return 1;
// 写入格式化数据
fprintf(fp, "%d %f %s\\n", 100, 3.14, "hello");
fclose(fp);
// 重新打开读取
fp = fopen("data.txt", "r");
int a;
float b;
char s[20];
fscanf(fp, "%d %f %s", &a, &b, s);
printf("读出: %d, %.2f, %s\\n", a, b, s); // 读出: 100, 3.14, hello
fclose(fp);
return 0;
}
fread / fwrite —— 二进制读写
这两个是二进制读写的核心函数,适合读写结构体、数组等:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
- ptr:数据存放地址
- size:每个元素的大小
- count:元素个数
- 返回值:实际读写的元素个数
示例:写入并读取一个数组
#include <stdio.h>
int main()
{
double a[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
// 写入二进制文件
FILE* fp = fopen("data.bin", "wb");
fwrite(a, sizeof(double), 5, fp); // 写入5个double
fclose(fp);
// 读取二进制文件
double b[5];
fp = fopen("data.bin", "rb");
size_t ret = fread(b, sizeof(double), 5, fp);
if (ret == 5)
printf("读取成功\\n");
else
printf("读取不完整,只读了 %zu 个\\n", ret);
fclose(fp);
return 0;
}
scanf/fscanf/sscanf 对比
| scanf | 从标准输入(stdin)读取 |
| fscanf | 从指定文件流读取 |
| sscanf | 从字符串读取 |
同理,printf、fprintf、sprintf 是输出版本,sprintf 可以把格式化数据写到字符串里。
六、文件的随机读写
顺序读写只能从头到尾,但有时候我们需要跳来跳去,比如直接定位到文件中间某个位置。这就需要随机读写函数。
fseek —— 定位文件指针
int fseek(FILE* stream, long int offset, int origin);
- offset:偏移量
- origin:起始位置,有三个选项:
- SEEK_SET:文件开头
- SEEK_CUR:当前位置
- SEEK_END:文件末尾
示例:修改文件中间的内容
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("example.txt", "wb");
fputs("This is an apple.", pFile); // 写入字符串
fseek(pFile, 9, SEEK_SET); // 定位到第9个字节('a'的位置)
fputs(" sam", pFile); // 把 "an a" 替换成 " sam"
fclose(pFile);
return 0;
}
执行后文件内容变成:This is a sample.
ftell —— 获取当前位置
long int ftell(FILE* stream);
返回文件指针相对于开头的偏移量。常用技巧:配合 fseek 获取文件大小。
#include <stdio.h>
int main()
{
FILE* pFile;
long size;
pFile = fopen("myfile.txt", "rb");
if (pFile == NULL)
perror("打开文件失败");
else
{
fseek(pFile, 0, SEEK_END); // 定位到文件末尾
size = ftell(pFile); // 此时偏移量就是文件大小
fclose(pFile);
printf("文件大小: %ld 字节\\n", size);
}
return 0;
}
rewind —— 回到文件开头
void rewind(FILE* stream);
把文件指针重置到文件开头,相当于 fseek(fp, 0, SEEK_SET)。
示例:先写入26个字母,再从头读取
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile); // 写入 A-Z
rewind(pFile); // 回到开头
fread(buffer, 1, 26, pFile); // 读取刚才写入的内容
fclose(pFile);
buffer[26] = '\\0';
printf("%s\\n", buffer); // 输出: ABCDEFGHIJKLMNOPQRSTUVWXYZ
return 0;
}
七、文件读取结束的判定
feof 的正确用法
大坑警告:很多人以为 feof 返回真就表示读到文件末尾了,这是错误的理解。
feof 的真正作用是:在文件读取结束后,判断结束的原因是不是"遇到文件尾"。
正确的判断方式:
文本文件
- fgetc:判断返回值是否为 EOF
- fgets:判断返回值是否为 NULL
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意用int,不是char,因为要处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("打开文件失败");
return EXIT_FAILURE;
}
// fgetc 读取失败或遇到文件末尾都会返回EOF
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
// 判断结束原因
if (ferror(fp))
puts("读取时发生I/O错误");
else if (feof(fp))
puts("成功读到文件末尾");
fclose(fp);
return 0;
}
二进制文件
fread 返回实际读取的元素个数,判断是否小于预期个数:
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1., 2., 3., 4., 5.};
FILE* fp = fopen("test.bin", "wb");
fwrite(a, sizeof *a, SIZE, fp); // 先写入
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp);
if (ret_code == SIZE) {
puts("读取成功,内容:");
for (int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\\n');
}
else {
if (feof(fp))
printf("错误:文件意外结束\\n");
else if (ferror(fp))
perror("读取错误");
}
fclose(fp);
return 0;
}
总结一下判断逻辑:
八、文件缓冲区
缓冲机制
C语言采用缓冲文件系统:系统自动为每个正在使用的文件在内存中开辟一块缓冲区。
- 写文件时:数据先送到内存缓冲区,缓冲区装满后才一起写到磁盘
- 读文件时:从磁盘读入数据填充缓冲区,再从缓冲区逐个送给程序
这样做的目的是减少磁盘I/O次数,提高效率。
缓冲区的影响
看个实验:
#include <stdio.h>
#include <windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf); // 数据先进入输出缓冲区
printf("睡眠10秒——此时打开test.txt,发现文件是空的\\n");
Sleep(10000);
printf("刷新缓冲区\\n");
fflush(pf); // 强制把缓冲区内容写入磁盘
printf("再睡眠10秒——此时打开test.txt,文件有内容了\\n");
Sleep(10000);
fclose(pf); // fclose 也会刷新缓冲区
pf = NULL;
return 0;
}
运行这个程序,你会发现:
- 写入 fputs 后立即打开文件,内容还没进去
- 调用 fflush 后,内容才真正写入磁盘
- fclose 关闭文件时也会自动刷新缓冲区
结论
重要:因为有缓冲区的存在,操作文件时要注意:
- 写完数据后及时 fflush 或 fclose
- 否则程序异常退出时,缓冲区数据可能丢失
九、总结与速查表
文件操作流程
打开文件 (fopen) → 检查是否成功 → 读写数据 → 关闭文件 (fclose)
核心函数速查
| 打开文件 | fopen | 返回 FILE*,失败返回 NULL |
| 关闭文件 | fclose | 关闭并刷新缓冲区 |
| 字符读写 | fgetc / fputc | 一次一个字符 |
| 行读写 | fgets / fputs | 一次一行 |
| 格式化读写 | fscanf / fprintf | 类似 scanf/printf |
| 二进制读写 | fread / fwrite | 读写任意类型数据 |
| 定位 | fseek | 移动文件指针 |
| 获取位置 | ftell | 返回当前位置 |
| 回到开头 | rewind | 重置到文件开头 |
| 判断结束原因 | feof | 读完后判断是否因EOF结束 |
| 判断错误 | ferror | 判断是否发生I/O错误 |
| 刷新缓冲区 | fflush | 强制写入磁盘 |
常见易错点
把这些知识点搞清楚,C语言文件操作就基本拿下了。多写几个小练习——比如实现一个简单的配置文件读写、日志记录功能,手感很快就上来了。


