C语言进阶知识--文件操作
目录
- 为什么使用文件?
- 什么是文件?
- 二进制文件和文本文件?
- 文件的打开和关闭
- 文件的顺序读写
- 文件的随机读写
- 文件读取结束的判定
- 文件缓冲区
正文开始
1. 为什么使用文件?
在程序开发中,数据的存储位置直接影响其持久性——如果没有文件,程序运行时产生的临时数据(如计算结果、用户输入信息等)会被存储在计算机的内存中。而内存的核心特性是“断电/程序退出即回收”,一旦程序关闭或电脑重启,内存中的数据会立即丢失。比如我们用计算器计算一组复杂数据,若未保存,关闭计算器后之前的计算结果就再也找不到了。
若要实现数据的持久化保存(即数据长期存在,不受程序退出或设备重启影响),文件就是最基础、最常用的载体。日常生活中的场景也能直观体现这一点:用记事本写日记后保存为.txt文件、用Excel处理数据后保存为.xlsx文件、用相机拍照后生成的.jpg文件,本质都是通过文件将数据固化到磁盘(硬盘、U盘等)中,后续需要时可再次打开使用。
2. 什么是文件?
从物理层面看,磁盘(硬盘、U盘、SD卡等)上以二进制形式存储的、具有完整标识的数据流就是文件。但在程序设计领域,我们更关注文件的“功能属性”,通常将其分为两类:程序文件和数据文件。
2.1 程序文件
程序文件是支撑程序编译、链接和运行的文件,主要包括三类,三者存在明确的“生成流程”关系:
- 源程序文件:后缀为
.c(C语言)、.cpp(C++)等,是程序员编写的纯文本代码文件,可直接用记事本、VS Code等工具打开编辑,例如main.c就是一个包含main函数的源程序文件。 - 目标文件:在Windows环境下后缀为
.obj,在Linux环境下后缀为.o。它是编译器对源程序文件编译后生成的“中间文件”——此时代码已从文本转为二进制,但尚未关联系统库(如printf函数所在的stdio.h库),无法直接运行。 - 可执行程序:Windows下后缀为
.exe,Linux下无后缀(如a.out)。它是链接器将目标文件与系统库、其他依赖文件整合后生成的文件,包含完整的运行指令,双击或在终端执行即可启动程序。
2.2 数据文件
数据文件的核心作用是“存储程序运行时需要的输入数据,或保存程序运行后的输出数据”,其内容不是可执行代码,而是纯数据(如文本、数值、图像像素等)。本章讨论的“文件操作”,核心就是针对这类文件。
在之前的学习中,我们处理数据的输入输出都以“终端”为对象:通过键盘(输入终端)输入数据,通过显示器(输出终端)查看结果。但实际开发中,数据量往往更大(如存储1000个学生的成绩),此时就需要将数据写入磁盘文件——后续程序运行时,直接从文件读取数据,无需重复手动输入;程序结束后,结果也能保存在文件中供后续分析(如用Excel打开成绩文件统计平均分)。
2.3 文件名
为了让系统能唯一识别和定位文件,每个文件都需要一个“文件标识”,即我们常说的“文件名”。完整的文件名包含三部分,缺一不可:
- 文件路径:描述文件在磁盘中的存储位置,分为“绝对路径”和“相对路径”。
- 绝对路径:从磁盘根目录开始的完整路径,如
C:\Users\Admin\Desktop\test.txt(Windows)、/home/user/Desktop/test.txt(Linux),适用于定位任意位置的文件。 - 相对路径:以“当前程序运行目录”为起点的路径,如程序在
C:\code目录下运行,test.txt就表示C:\code\test.txt,data\test.txt表示C:\code\data\test.txt,适用于项目内部文件的引用,更灵活。
- 绝对路径:从磁盘根目录开始的完整路径,如
- 文件名主干:用户自定义的文件名称,如
test、student_score,用于直观区分文件用途。 - 文件后缀:用
.分隔的后缀名,标识文件类型,如.txt(文本文件)、.bin(二进制文件)、.jpg(图像文件),系统和程序通过后缀识别文件的打开方式(如记事本默认打开.txt文件)。
例如C:\code\student_score.txt中,C:\code\是路径,student_score是主干,.txt是后缀。
3. 二进制文件和文本文件?
根据数据在磁盘中的“存储形式”,数据文件可分为文本文件和二进制文件,二者的核心区别在于是否经过“ASCII码转换”。
数据在内存中始终以“二进制形式”存储(无论文本还是数值),但写入磁盘时会根据文件类型决定是否转换:
- 若直接将内存中的二进制数据写入磁盘,不做任何转换,生成的就是二进制文件(如
.exe、.bin、.jpg)。 - 若先将内存中的数据转换为“ASCII码形式”,再写入磁盘,生成的就是文本文件(如
.txt、.c)。
关键差异:数值型数据的存储
字符型数据(如'a'、'1')的存储比较特殊——它在内存中本身就以ASCII码(二进制)存储,因此写入文本文件或二进制文件时,存储形式本质相同(都是ASCII码的二进制),区别仅在于文本文件可被记事本等工具识别为字符,二进制文件打开后会显示乱码(因工具默认按文本解析)。
但数值型数据(如整数10000、浮点数3.14)的存储差异非常明显:
- 以文本文件存储:需将数值拆分为单个字符,每个字符以ASCII码存储。例如整数
10000,需拆分为'1'、'0'、'0'、'0'、'0'五个字符,每个字符占1字节(ASCII码范围0-127),共占用5字节。 - 以二进制文件存储:直接将数值在内存中的二进制形式写入磁盘,无需转换。例如在32位系统中,
int类型占4字节,10000的二进制表示为00000000 00000000 00100111 00010000,直接写入磁盘后仅占用4字节,比文本文件更节省空间。
直观区分方法
- 文本文件:用记事本、VS Code等文本编辑器打开,可直接看到清晰的字符(如文字、数字、符号),例如打开
test.txt能看到“Hello 123”。 - 二进制文件:用文本编辑器打开会显示乱码(因编辑器试图将二进制数据解析为ASCII字符,但数据本身不是字符编码),例如打开
.exe文件会看到大量“�”“¥”等乱码;需用专用工具(如VS的“二进制编辑器”、WinHex)打开,才能看到二进制数据。
测试代码解析
以下代码演示如何以二进制形式写入整数10000到文件:
#include <stdio.h>
int main()
{int a = 10000; // 内存中以二进制存储:0x00002710(十六进制,对应4字节)FILE* pf = fopen("test.txt", "wb"); // "wb"表示以二进制只写模式打开fwrite(&a, 4, 1, pf); // 从a的地址开始,写4字节(1个int)到pf指向的文件fclose(pf); // 关闭文件,刷新缓冲区pf = NULL; // 避免野指针return 0;
}
在VS中查看该文件的方法:右键文件→“打开方式”→选择“二进制编辑器”,可看到文件内容为10 27 00 00(小端存储,即低位字节在前),对应10000的十六进制0x2710,验证了二进制文件的存储形式。
4. 文件的打开和关闭
文件操作遵循“先打开,后操作,最后关闭”的原则——打开文件是建立“程序与文件”的关联,关闭文件是释放关联资源(避免内存泄漏或文件损坏)。要理解这一过程,需先掌握“流”和“文件指针”的概念。
4.1 流和标准流
4.1.1 流(Stream)
程序需要与不同的外部设备(键盘、显示器、磁盘、打印机等)进行数据交互,但不同设备的硬件接口和数据传输协议差异极大(如键盘是字符流输入,磁盘是块数据读写)。为了简化程序员的操作,C语言抽象出“流”的概念——将所有设备的输入输出都统一视为“字符流”(或字节流),屏蔽设备底层差异。
可以将“流”想象成一条“流淌着数据的河”:
- 输入操作:数据从设备(如键盘、磁盘文件)流入程序,即“从流中读取数据”。
- 输出操作:数据从程序流入设备(如显示器、磁盘文件),即“向流中写入数据”。
无论操作的是键盘还是磁盘文件,程序员只需调用统一的流操作函数(如fgetc读、fputc写),无需关心设备底层细节——这是C语言文件操作的核心设计思想。
4.1.2 标准流(Standard Stream)
通常情况下,操作流前需要手动“打开”,但C语言程序启动时,会默认自动打开3个标准流,因此我们可以直接使用scanf、printf等函数,无需手动打开:
- stdin(标准输入流):默认关联“键盘”,是程序获取输入数据的默认来源。
scanf、getchar等函数本质就是从stdin中读取数据。 - stdout(标准输出流):默认关联“显示器”,是程序输出正常结果的默认目标。
printf、puts等函数本质就是向stdout中写入数据。 - stderr(标准错误流):默认关联“显示器”,专门用于输出程序的错误信息(如“文件打开失败”)。它与
stdout的核心区别是无缓冲区——stdout会先将数据存入缓冲区,满了或遇到\n才输出;而stderr会立即输出错误信息,避免因缓冲区延迟导致错误信息被遗漏(例如程序崩溃前,stderr的错误提示能及时显示,stdout的内容可能还在缓冲区中未输出)。
这三个标准流的类型都是FILE*(文件指针),C语言通过它们维护与标准设备的关联。
4.2 文件指针(FILE*)
在“缓冲文件系统”(C语言默认的文件处理方式)中,每个被打开的文件都会在内存中开辟一块“文件信息区”——这是一个结构体(由系统定义,类型名为FILE),用于存储文件的核心信息,包括:
- 文件的路径、名称、后缀;
- 文件的当前读写位置(“光标”位置);
- 文件的大小、存储模式(文本/二进制);
- 缓冲区的地址、大小;
- 文件的状态(是否可读、是否出错)等。
不同编译器的FILE结构体定义略有差异(如VS2022的FILE包含_ptr(缓冲区指针)、_cnt(缓冲区剩余字节数)、_flag(文件状态标志)等成员),但程序员无需关心其内部细节——我们只需通过“文件指针”(FILE*类型的变量)间接访问这个结构体。
例如:
FILE* pf; // 定义一个文件指针变量pf
pf本身是一个指针,它的值是“文件信息区”的首地址。当我们通过fopen打开文件时,系统会自动创建FILE结构体并填充信息,然后返回该结构体的地址,赋值给pf。后续对文件的读写、关闭操作,只需通过pf即可间接操作文件信息区,进而控制文件。
4.3 文件的打开和关闭
4.3.1 核心函数
ANSI C标准规定,使用fopen函数打开文件,使用fclose函数关闭文件,二者的函数原型如下:
// 打开文件:成功返回FILE*指针(指向文件信息区),失败返回NULL
FILE * fopen ( const char * filename, const char * mode );// 关闭文件:成功返回0,失败返回EOF(-1)
int fclose ( FILE * stream );
filename:字符串,指定要打开的文件路径(绝对路径或相对路径),如"test.txt"、"C:\\code\\data.bin"(Windows路径中\需转义为\\)。mode:字符串,指定文件的“打开模式”(读写权限、文本/二进制模式),是fopen的核心参数。stream:要关闭的文件指针(即fopen返回的FILE*变量)。
4.3.2 常用打开模式详解
不同的打开模式决定了文件的操作权限和行为,下表整理了常用模式的核心信息(重点关注文本和二进制模式的差异):
| 文件使用方式 | 核心含义 | 适用场景 | 如果指定文件不存在 | 注意事项 |
|---|---|---|---|---|
| “r”(文本只读) | 打开已存在的文本文件,仅用于读取数据 | 读取配置文件(如config.txt) | 打开失败,返回NULL | 不能向文件写入数据,否则会出错 |
| “w”(文本只写) | 打开文本文件,仅用于写入数据;若文件已存在,会清空原有内容 | 保存程序运行结果(如日志文件) | 自动创建新文件 | 谨慎使用,避免误删已有数据 |
| “a”(文本追加) | 打开文本文件,仅在文件末尾追加数据;不覆盖原有内容 | 日志记录(如每次运行程序追加新日志) | 自动创建新文件 | 写入位置始终在文件末尾,无法修改已有内容 |
| “rb”(二进制只读) | 打开已存在的二进制文件,仅用于读取数据 | 读取图片(.jpg)、音频(.mp3)等二进制数据 | 打开失败,返回NULL | 与“r”的区别是按二进制格式解析数据 |
| “wb”(二进制只写) | 打开二进制文件,仅用于写入数据;若文件已存在,清空内容 | 保存二进制数据(如整数数组、结构体) | 自动创建新文件 | 与“w”的区别是按二进制格式写入数据 |
| “a+”(文本读写追加) | 打开文本文件,可读写;写入时仅追加到末尾 | 既要读历史日志,又要追加新日志 | 自动创建新文件 | 读取时可移动指针,写入时固定在末尾 |
| “rb+”(二进制读写) | 打开已存在的二进制文件,可读写 | 修改二进制文件中的部分数据(如修改图片元信息) | 打开失败,返回NULL | 支持随机读写,需通过fseek移动指针 |
4.3.3 示例代码与注意事项
以下代码演示“打开文件→写入数据→关闭文件”的完整流程:
/* fopen fclose 完整示例 */
#include <stdio.h>
#include <stdlib.h> // 包含EXIT_FAILURE宏定义int main ()
{FILE * pFile;// 1. 打开文件:以文本只写模式打开myfile.txtpFile = fopen ("myfile.txt","w");// 2. 检查文件是否成功打开(关键!避免空指针操作)if (pFile == NULL) {perror("File opening failed"); // 打印错误原因(如“File opening failed: No such file or directory”)return EXIT_FAILURE; // 退出程序,返回错误码}// 3. 文件操作:向文件写入字符串fputs ("Hello, File Operation!", pFile);// 4. 关闭文件:必须关闭,否则缓冲区数据可能丢失fclose (pFile);pFile = NULL; // 将指针置空,避免后续误操作(野指针风险)return 0;
}
关键注意事项:
- 必须检查
fopen的返回值:若文件路径错误、权限不足(如试图写入只读文件),fopen会返回NULL,此时若直接使用pFile操作文件,会导致程序崩溃(空指针访问)。 - 必须调用
fclose关闭文件:fclose会自动刷新文件缓冲区(将缓冲区中未写入磁盘的数据写入),并释放FILE结构体占用的内存;若不关闭文件,可能导致数据丢失(缓冲区数据未写入)或内存泄漏。 - 关闭后将指针置空:
fclose后,pFile仍指向原FILE结构体的地址(但该地址已被系统回收),成为“野指针”;将pFile = NULL可避免后续误操作野指针。
5. 文件的顺序读写
“顺序读写”是指文件的读写操作从“当前指针位置”开始,按数据的存储顺序依次进行——读完一个字符后,指针自动移动到下一个字符;写完一个字符后,指针也自动后移,无法跳过数据直接操作中间内容(如需跳过,需用后续的“随机读写”)。
5.1 核心顺序读写函数
C语言提供了一组专门用于顺序读写的函数,覆盖“字符、文本行、格式化、二进制”四种数据类型,下表整理了函数的核心信息:
| 函数名 | 功能描述 | 函数原型 | 适用于 | 示例 |
|---|---|---|---|---|
fgetc | 从流中读取一个字符(返回ASCII码或EOF) | int fgetc(FILE *stream); | 所有输入流(stdin、文件流等) | int c = fgetc(pFile); // 从pFile文件读1个字符 |
fputc | 向流中写入一个字符(成功返回字符,失败返回EOF) | int fputc(int ch, FILE *stream); | 所有输出流(stdout、文件流等) | fputc('A', pFile); // 向pFile文件写字符’A’ |
fgets | 从流中读取一行文本(最多读n-1个字符,自动加’\0’) | char *fgets(char *str, int n, FILE *stream); | 所有输入流 | char buf[100]; fgets(buf, 100, pFile); // 读1行到buf |
fputs | 向流中写入一行文本(不自动加’\n’,需手动添加) | int fputs(const char *str, FILE *stream); | 所有输出流 | fputs("Hello\n", pFile); // 向文件写“Hello”并换行 |
fscanf | 从流中格式化读取数据(类似scanf,但可指定流) | int fscanf(FILE *stream, const char *format, ...); | 所有输入流 | int a; fscanf(pFile, "%d", &a); // 从文件读整数到a |
fprintf | 向流中格式化写入数据(类似printf,但可指定流) | int fprintf(FILE *stream, const char *format, ...); | 所有输出流 | fprintf(pFile, "a=%d\n", a); // 向文件写“a=10” |
fread | 从文件流中读取二进制数据(按字节读取,效率高) | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | 仅文件输入流 | int arr[5]; fread(arr, 4, 5, pFile); // 读5个int到arr |
fwrite | 向文件流中写入二进制数据(按字节写入,无转换) | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); | 仅文件输出流 | fwrite(arr, 4, 5, pFile); // 写arr的5个int到文件 |
函数细节补充:
fgetc返回int而非char:因为fgetc读取失败或到文件尾时返回EOF(值为-1),而char类型(尤其是unsigned char)无法存储-1,会导致EOF被误判为普通字符;用int可正确区分。fgets的“行读取”限制:n是缓冲区str的大小,fgets最多读取n-1个字符(预留1个字节存字符串结束符'\0');若一行数据超过n-1个字符,fgets会先读n-1个字符,剩余部分留到下次读取。fread/fwrite的返回值:size_t是无符号整数类型,返回值是“成功读取/写入的nmemb个数”(而非字节数)。例如fread(arr,4,5,pFile),若成功读5个int(共20字节),返回5;若只读到3个,返回3,可据此判断是否读完整。
5.2 三组易混淆函数对比
在顺序读写函数中,scanf/fscanf/sscanf和printf/fprintf/sprintf是三组功能相似但适用场景不同的函数,需重点区分:
5.2.1 输入类函数对比(scanf vs fscanf vs sscanf)
| 函数 | 核心差异:数据来源 | 适用场景 | 示例 |
|---|---|---|---|
scanf | 固定从标准输入流(stdin) 读取(键盘输入) | 程序需要用户手动输入数据时 | scanf("%d", &a); // 从键盘读整数a |
fscanf | 可从任意输入流读取(如文件流、stdin) | 从文件或其他设备读取格式化数据时 | fscanf(pFile, "%d", &a); // 从文件读整数a |
sscanf | 从指定字符串中读取(数据来源是内存中的字符串) | 解析字符串中的数据(如拆分日志内容) | char str[] = "Name: Tom, Age: 20"; sscanf(str, "Name: %s, Age: %d", name, &age); // 从str中提取姓名和年龄 |
5.2.2 输出类函数对比(printf vs fprintf vs sprintf)
| 函数 | 核心差异:数据目标 | 适用场景 | 示例 |
|---|---|---|---|
printf | 固定输出到标准输出流(stdout) (显示器) | 向用户展示程序运行结果时 | printf("a=%d\n", a); // 显示器输出a的值 |
fprintf | 可输出到任意输出流(如文件流、stdout) | 向文件保存格式化数据时 | fprintf(pFile, "a=%d\n", a); // 向文件写a的值 |
sprintf | 输出到指定字符串(数据目标是内存中的字符串) | 拼接字符串或格式化生成字符串时 | char buf[50]; sprintf(buf, "a=%d, b=%d", a, b); // 生成字符串“a=10, b=20”存入buf |
示例:用fscanf/fprintf读写学生信息
#include <stdio.h>// 定义学生结构体
struct Student {char name[20];int age;float score;
};int main() {struct Student s1 = {"Zhang San", 18, 95.5};struct Student s2;FILE* pf = fopen("student.txt", "w+"); // 读写模式打开文件if (pf == NULL) {perror("Open failed");return 1;}// 用fprintf写入学生信息(文本格式)fprintf(pf, "%s %d %.1f\n", s1.name, s1.age, s1.score);// 移动指针到文件开头(准备读取)rewind(pf);// 用fscanf读取学生信息fscanf(pf, "%s %d %f", s2.name, &s2.age, &s2.score);// 输出读取到的信息(验证结果)printf("Read from file: Name=%s, Age=%d, Score=%.1f\n", s2.name, s2.age, s2.score);fclose(pf);pf = NULL;return 0;
}
运行后,student.txt文件中会保存Zhang San 18 95.5,程序会从文件中读回数据并在显示器输出,验证了fscanf/fprintf的正确性。
6. 文件的随机读写
“随机读写”是指通过手动移动文件指针(“光标”),直接定位到文件的任意位置进行读写操作,无需按顺序遍历数据。例如修改文件中间的某个字符、读取文件第100个字节的数据,都需要用到随机读写。C语言提供了三个核心函数实现随机读写:fseek(移动指针)、ftell(获取指针位置)、rewind(重置指针到开头)。
6.1 fseek:移动文件指针
fseek是随机读写的核心函数,用于根据“基准位置”和“偏移量”移动文件指针,函数原型如下:
// 成功返回0,失败返回非0
int fseek ( FILE * stream, long int offset, int origin );
stream:要操作的文件指针。offset:偏移量,单位是“字节”,可正可负:- 正值:指针从基准位置向文件末尾方向移动。
- 负值:指针从基准位置向文件开头方向移动。
origin:基准位置,必须是以下三个宏之一(定义在stdio.h中):SEEK_SET:基准位置为文件开头(offset必须≥0)。SEEK_CUR:基准位置为当前指针位置(offset可正可负)。SEEK_END:基准位置为文件末尾(offset可正可负,正值会指向文件末尾之后的位置)。
示例:修改文件内容
以下代码先向文件写入"This is an apple.",然后用fseek移动指针到第9个字符位置,修改内容为"This is a sample.":
/* fseek 示例:修改文件中间内容 */
#include <stdio.h>int main ()
{FILE * pFile;// 以二进制只写模式打开文件(避免文本模式的换行符转换影响指针位置)pFile = fopen ( "example.txt", "wb" );if (pFile == NULL) {perror("Open failed");return 1;}// 1. 写入原始字符串:"This is an apple."(共16个字符,索引0-15)fputs ( "This is an apple.", pFile );// 2. 移动指针:以文件开头为基准,偏移9字节(指向第9个字符,索引9)// 原始字符串索引:0:T,1:h,2:i,3:s,4: ,5:i,6:s,7: ,8:a,9:n,...fseek ( pFile, 9, SEEK_SET );// 3. 写入新内容:" sam"(替换索引9-12的字符:n→s, →a, a→m, p→空格?不,实际是覆盖写入)// 写入" sam"后,字符串变为"This is a sample."("an ap"被替换为"a sam")fputs ( " sam", pFile );fclose ( pFile );pFile = NULL;return 0;
}
关键说明:
- 文本模式下慎用
fseek:文本文件中,\n(换行符)在Windows下会被存储为\r\n(两个字节),导致文件实际字节数与字符数不一致,fseek的偏移量计算容易出错;因此随机读写建议用二进制模式(rb/wb/rb+等),避免换行符转换。 - 偏移量超出范围:若
offset过大,导致指针指向文件末尾之后的位置,写入数据时会在文件末尾和指针位置之间填充“空字节”(\0),导致文件变大。
6.2 ftell:获取指针当前位置
ftell用于返回文件指针相对于“文件开头”的偏移量(单位:字节),可用于计算文件大小、记录指针位置等,函数原型如下:
// 成功返回偏移量(long int),失败返回-1L
long int ftell ( FILE * stream );
示例:计算文件大小
文件大小 = 文件末尾的指针偏移量 - 文件开头的指针偏移量(0),因此只需用fseek将指针移到文件末尾,再用ftell获取偏移量即可:
/* ftell 示例:计算文件大小 */
#include <stdio.h>int main ()
{FILE * pFile;long size; // 存储文件大小(字节数)// 以二进制只读模式打开文件(计算大小不受文本模式转换影响)pFile = fopen ("myfile.txt","rb");if (pFile == NULL) {perror ("Error opening file");return 1;}// 1. 将指针移到文件末尾fseek (pFile, 0, SEEK_END); // offset=0,基准为文件末尾→指针停在末尾// 2. 获取末尾相对于开头的偏移量→即文件大小size = ftell (pFile);// 3. 关闭文件并输出结果fclose (pFile);pFile = NULL;printf ("Size of myfile.txt: %ld bytes.\n", size);return 0;
}
注意:long int的范围是-2^31 ~ 2^31-1(约2GB),若文件大小超过2GB,ftell会返回-1L(错误);此时需使用64位编译器,或用_ftelli64(VS)、ftello(Linux)等支持大文件的函数。
6.3 rewind:重置指针到文件开头
rewind是一个简化版的fseek,功能是将文件指针直接移动到“文件开头”,相当于fseek(stream, 0, SEEK_SET),但无需判断返回值,函数原型如下:
// 无返回值
void rewind ( FILE * stream );
示例:先写后读文件
以下代码先向文件写入A-Z的26个大写字母,再用rewind重置指针到开头,最后读取并输出这些字符:
/* rewind 示例:先写后读 */
#include <stdio.h>int main ()
{int n;FILE * pFile;char buffer [27]; // 存储26个字符+1个'\0'// 以读写模式打开文件(w+:可写可读,文件不存在则创建)pFile = fopen ("myfile.txt","w+");if (pFile == NULL) {perror("Open failed");return 1;}// 1. 写入A-Z(ASCII码65-90)for (n = 'A'; n <= 'Z'; n++) {fputc (n, pFile); // 逐个写入字符}// 2. 重置指针到文件开头(此时指针在文件末尾,不重置无法读取数据)rewind (pFile);// 3. 读取26个字符到buffer(1个字符占1字节,读26个)fread (buffer, 1, 26, pFile);// 4. 添加字符串结束符,避免输出乱码buffer[26] = '\0';// 5. 输出结果(应显示ABCDEFGHIJKLMNOPQRSTUVWXYZ)printf("%s\n", buffer);fclose (pFile);pFile = NULL;return 0;
}
关键说明:
- 若不调用
rewind:写入26个字符后,指针停在文件末尾(偏移量26),此时fread会从末尾开始读,只能读到0个字符,buffer中无有效数据,输出乱码。 rewind与fseek的区别:rewind无返回值,无法判断是否成功;fseek有返回值,可通过返回值判断是否移动成功(如文件损坏时fseek返回非0)。
7. 文件读取结束的判定
在文件读取过程中,最容易犯的错误是“用feof函数直接判断文件是否结束”——feof的设计目的是“判断文件读取结束的原因”(是到了文件尾,还是读取出错),而非“判断是否该结束读取”。正确的判定方式需区分“文本文件”和“二进制文件”。
7.1 为什么不能直接用feof?
feof的工作机制是:当文件读取操作(如fgetc、fread)尝试读取超出文件范围的数据时,系统会设置文件的“EOF标志”,此时feof返回非0(真);若读取操作未超出范围(或未进行读取),feof返回0(假)。
若直接用while(!feof(fp))循环读取,会导致“多读取一次数据”:例如文件只有5个字符,第5次读取后指针到文件末尾,但feof仍返回0(因未尝试读取第6个字符),循环会继续执行第6次读取——第6次读取失败(返回EOF或0),但循环已进入,导致处理无效数据。
7.2 文本文件的读取结束判定
文本文件的读取通常使用fgetc(读字符)或fgets(读行),判定结束的核心是“判断读取函数的返回值”:
- 用
fgetc读取:若返回值为EOF(-1),表示读取结束(可能是文件尾或出错)。 - 用
fgets读取:若返回值为NULL,表示读取结束(可能是文件尾或出错)。
读取结束后,再用feof判断是“文件尾”还是“读取出错”:
- 若
feof(fp)返回非0:读取结束原因是“到了文件尾”(正常结束)。 - 若
ferror(fp)返回非0:读取结束原因是“读取出错”(如磁盘错误、文件损坏)。
文本文件读取示例(正确写法)
#include <stdio.h>
#include <stdlib.h>int main(void)
{int c; // 必须用int,不能用char(避免EOF被误判)FILE* fp = fopen("test.txt", "r"); // 文本只读模式打开// 1. 检查文件是否成功打开if (!fp) {perror("File opening failed");return EXIT_FAILURE;}// 2. 核心循环:先读取,再判断返回值是否为EOFwhile ((c = fgetc(fp)) != EOF) { putchar(c); // 读取成功,输出字符}// 3. 判断读取结束的原因if (ferror(fp)) { // 检查是否出错puts("\nI/O error when reading");} else if (feof(fp)) { // 检查是否到文件尾puts("\nEnd of file reached successfully");}// 4. 关闭文件fclose(fp);fp = NULL;return 0;
}
关键细节:
int c而非char c:char类型(尤其是unsigned char)的范围是0-255,无法存储EOF(-1);若用char c,EOF会被转换为255,导致c != EOF始终为真,循环无限执行。- 先读再判:
(c = fgetc(fp)) != EOF的执行顺序是“先调用fgetc读取字符并存入c,再判断c是否为EOF”,确保每次循环都基于“已读取的有效数据”。
7.3 二进制文件的读取结束判定
二进制文件的读取通常使用fread函数,fread的返回值是“成功读取的nmemb个数”(即“元素个数”,而非字节数),因此判定结束的核心是“判断返回值是否小于预期读取的元素个数”。
例如预期读取5个int(nmemb=5):
- 若
fread返回5:读取成功,无结束。 - 若
fread返回0-4:读取结束(可能是文件尾,或读取出错)。
同样,读取结束后用feof和ferror判断原因:
- 若
feof(fp)非0:文件尾(如文件只有3个int,预期读5个,返回3)。 - 若
ferror(fp)非0:读取出错(如磁盘损坏导致只读到2个)。
二进制文件读取示例(正确写法)
#include <stdio.h>// 定义要读取的元素个数
enum { SIZE = 5 };int main(void)
{// 1. 准备数据:写入5个double到二进制文件double a[SIZE] = {1.1, 2.2, 3.3, 4.4, 5.5};FILE *fp = fopen("test.bin", "wb"); // 二进制只写模式if (!fp) {perror("File opening failed");return 1;}fwrite(a, sizeof *a, SIZE, fp); // 写入5个double(每个8字节,共40字节)fclose(fp);fp = NULL;// 2. 读取二进制文件double b[SIZE];fp = fopen("test.bin", "rb"); // 二进制只读模式if (!fp) {perror("File opening failed");return 1;}// 3. 核心读取:预期读SIZE个元素,返回实际读取的个数size_t ret_code = fread(b, sizeof *b, SIZE, fp);// 4. 处理读取结果if (ret_code == SIZE) { // 读取成功(实际个数=预期个数)puts("Array read successfully, contents: ");for (int n = 0; n < SIZE; ++n) {printf("%.1f ", b[n]); // 输出1.1 2.2 3.3 4.4 5.5}putchar('\n');} else { // 读取结束(实际个数<预期个数)if (feof(fp)) { // 原因1:文件尾(如文件被截断,只有3个元素)printf("Error reading test.bin: unexpected end of file (read %zu elements)\n", ret_code);} else if (ferror(fp)) { // 原因2:读取出错perror("Error reading test.bin");}}// 5. 关闭文件fclose(fp);fp = NULL;return 0;
}
关键细节:
sizeof *b的用法:*b是double类型,sizeof *b即sizeof(double)(8字节),比直接写8更通用(若后续将double改为float,无需修改此参数)。size_t的类型:size_t是无符号整数,不能用int接收fread的返回值(否则可能出现负数错误),需用size_t或unsigned int。
8. 文件缓冲区
C语言的“缓冲文件系统”中,系统会为每个正在使用的文件自动在内存中开辟一块“文件缓冲区”——这是一块临时存储数据的内存区域,用于协调“程序的高速数据处理”和“磁盘的低速数据读写”,提高文件操作效率。
8.1 缓冲区的工作机制
缓冲区的核心作用是“批量读写”,减少磁盘的I/O次数(磁盘I/O速度远低于内存操作速度,频繁读写会严重影响效率),具体机制如下:
- 写操作:程序调用
fputc、fprintf等函数向文件写入数据时,数据不会直接写入磁盘,而是先存入“输出缓冲区”;当缓冲区被填满(通常默认大小为4KB或8KB)、或调用fflush函数、或关闭文件(fclose)时,系统才会将缓冲区中的数据一次性写入磁盘。 - 读操作:程序调用
fgetc、fread等函数从文件读取数据时,系统会先从磁盘读取一批数据(填满“输入缓冲区”),再从缓冲区中逐个将数据返回给程序;当缓冲区中的数据被读完后,系统再从磁盘读取下一批数据填满缓冲区。
8.2 缓冲区的类型
根据缓冲方式的不同,缓冲区分为三类:
- 全缓冲:适用于磁盘文件,缓冲区填满后才进行I/O操作。例如
fputs写入数据到磁盘文件,会先存到全缓冲缓冲区,满了才写入磁盘。 - 行缓冲:适用于标准输入流(
stdin)和标准输出流(stdout),遇到\n(换行符)或缓冲区满时进行I/O操作。例如printf("Hello")会将数据存入行缓冲缓冲区,若不写\n,数据可能留在缓冲区中不输出;若写printf("Hello\n"),遇到\n会立即输出。 - 无缓冲:适用于标准错误流(
stderr),数据不经过缓冲区,直接进行I/O操作。例如fprintf(stderr, "Error")会立即输出错误信息,不会延迟。
8.3 缓冲区存在的证明(示例代码)
以下代码通过睡眠函数(Sleep)演示缓冲区的存在——未刷新缓冲区时,文件中无数据;刷新后,数据才写入文件:
#include <stdio.h>
#include <windows.h> // Windows下的Sleep函数,单位为毫秒(需包含此头文件)
// Linux下需替换为#include <unistd.h>,且Sleep改为sleep(单位为秒)int main()
{FILE* pf = fopen("test.txt", "w"); // 以文本只写模式打开文件if (pf == NULL) {perror("File opening failed");return 1;}// 1. 向文件写入数据:数据先存入输出缓冲区,未写入磁盘fputs("abcdef", pf); printf("睡眠10秒-此时打开test.txt,文件无内容(数据在缓冲区中)\n");Sleep(10000); // 睡眠10秒,期间可手动打开test.txt查看// 2. 刷新缓冲区:将缓冲区中的数据写入磁盘printf("刷新缓冲区\n");fflush(pf); // 强制刷新输出缓冲区(高版本VS可能提示“fflush对输入流未定义”,但对输出流有效)// 3. 再次睡眠:此时打开test.txt,文件有内容(数据已写入磁盘)printf("再睡眠10秒-此时打开test.txt,文件有内容(数据已写入磁盘)\n");Sleep(10000);// 4. 关闭文件:fclose会自动刷新缓冲区(即使未调用fflush,也会写入数据)fclose(pf);pf = NULL;return 0;
}
运行现象:
- 程序启动后,先执行
fputs("abcdef", pf),然后睡眠10秒——此时打开test.txt,文件大小为0,无任何内容(数据在缓冲区中)。 - 10秒后,执行
fflush(pf),刷新缓冲区——此时再打开test.txt,可看到“abcdef”(数据已写入磁盘)。 - 再睡眠10秒后,
fclose(pf)关闭文件(再次刷新缓冲区,确保无数据残留)。
8.4 关键注意事项
- 必须刷新或关闭文件:若程序异常退出(如崩溃),未刷新的缓冲区数据会丢失(因缓冲区在内存中,程序退出后内存被回收)。例如上述代码中,若在
Sleep(10000)期间强制关闭程序,test.txt中不会有数据。 fflush的使用限制:fflush仅对“输出流”有效,对“输入流”(如stdin)的行为是未定义的(不同编译器可能有不同处理),避免调用fflush(stdin)。- 关闭文件的重要性:
fclose函数在关闭文件前会自动调用fflush刷新缓冲区,因此即使未手动调用fflush,只要正确关闭文件,数据也不会丢失;若不关闭文件,缓冲区数据可能始终不写入磁盘,导致数据丢失。
