当前位置: 首页 > news >正文

C语言进阶知识--文件操作

目录

  1. 为什么使用文件?
  2. 什么是文件?
  3. 二进制文件和文本文件?
  4. 文件的打开和关闭
  5. 文件的顺序读写
  6. 文件的随机读写
  7. 文件读取结束的判定
  8. 文件缓冲区

正文开始

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.txtdata\test.txt表示C:\code\data\test.txt,适用于项目内部文件的引用,更灵活。
  • 文件名主干:用户自定义的文件名称,如teststudent_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个标准流,因此我们可以直接使用scanfprintf等函数,无需手动打开:

  • stdin(标准输入流):默认关联“键盘”,是程序获取输入数据的默认来源。scanfgetchar等函数本质就是从stdin中读取数据。
  • stdout(标准输出流):默认关联“显示器”,是程序输出正常结果的默认目标。printfputs等函数本质就是向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;
}

关键注意事项

  1. 必须检查fopen的返回值:若文件路径错误、权限不足(如试图写入只读文件),fopen会返回NULL,此时若直接使用pFile操作文件,会导致程序崩溃(空指针访问)。
  2. 必须调用fclose关闭文件:fclose会自动刷新文件缓冲区(将缓冲区中未写入磁盘的数据写入),并释放FILE结构体占用的内存;若不关闭文件,可能导致数据丢失(缓冲区数据未写入)或内存泄漏。
  3. 关闭后将指针置空: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/sscanfprintf/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中无有效数据,输出乱码。
  • rewindfseek的区别:rewind无返回值,无法判断是否成功;fseek有返回值,可通过返回值判断是否移动成功(如文件损坏时fseek返回非0)。

7. 文件读取结束的判定

在文件读取过程中,最容易犯的错误是“用feof函数直接判断文件是否结束”——feof的设计目的是“判断文件读取结束的原因”(是到了文件尾,还是读取出错),而非“判断是否该结束读取”。正确的判定方式需区分“文本文件”和“二进制文件”。

7.1 为什么不能直接用feof

feof的工作机制是:当文件读取操作(如fgetcfread尝试读取超出文件范围的数据时,系统会设置文件的“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 cchar类型(尤其是unsigned char)的范围是0-255,无法存储EOF(-1);若用char cEOF会被转换为255,导致c != EOF始终为真,循环无限执行。
  • 先读再判:(c = fgetc(fp)) != EOF的执行顺序是“先调用fgetc读取字符并存入c,再判断c是否为EOF”,确保每次循环都基于“已读取的有效数据”。

7.3 二进制文件的读取结束判定

二进制文件的读取通常使用fread函数,fread的返回值是“成功读取的nmemb个数”(即“元素个数”,而非字节数),因此判定结束的核心是“判断返回值是否小于预期读取的元素个数”。

例如预期读取5个intnmemb=5):

  • fread返回5:读取成功,无结束。
  • fread返回0-4:读取结束(可能是文件尾,或读取出错)。

同样,读取结束后用feofferror判断原因:

  • 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的用法:*bdouble类型,sizeof *bsizeof(double)(8字节),比直接写8更通用(若后续将double改为float,无需修改此参数)。
  • size_t的类型:size_t是无符号整数,不能用int接收fread的返回值(否则可能出现负数错误),需用size_tunsigned int

8. 文件缓冲区

C语言的“缓冲文件系统”中,系统会为每个正在使用的文件自动在内存中开辟一块“文件缓冲区”——这是一块临时存储数据的内存区域,用于协调“程序的高速数据处理”和“磁盘的低速数据读写”,提高文件操作效率。

8.1 缓冲区的工作机制

缓冲区的核心作用是“批量读写”,减少磁盘的I/O次数(磁盘I/O速度远低于内存操作速度,频繁读写会严重影响效率),具体机制如下:

  • 写操作:程序调用fputcfprintf等函数向文件写入数据时,数据不会直接写入磁盘,而是先存入“输出缓冲区”;当缓冲区被填满(通常默认大小为4KB或8KB)、或调用fflush函数、或关闭文件(fclose)时,系统才会将缓冲区中的数据一次性写入磁盘。
  • 读操作:程序调用fgetcfread等函数从文件读取数据时,系统会先从磁盘读取一批数据(填满“输入缓冲区”),再从缓冲区中逐个将数据返回给程序;当缓冲区中的数据被读完后,系统再从磁盘读取下一批数据填满缓冲区。

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;
}

运行现象

  1. 程序启动后,先执行fputs("abcdef", pf),然后睡眠10秒——此时打开test.txt,文件大小为0,无任何内容(数据在缓冲区中)。
  2. 10秒后,执行fflush(pf),刷新缓冲区——此时再打开test.txt,可看到“abcdef”(数据已写入磁盘)。
  3. 再睡眠10秒后,fclose(pf)关闭文件(再次刷新缓冲区,确保无数据残留)。

8.4 关键注意事项

  1. 必须刷新或关闭文件:若程序异常退出(如崩溃),未刷新的缓冲区数据会丢失(因缓冲区在内存中,程序退出后内存被回收)。例如上述代码中,若在Sleep(10000)期间强制关闭程序,test.txt中不会有数据。
  2. fflush的使用限制fflush仅对“输出流”有效,对“输入流”(如stdin)的行为是未定义的(不同编译器可能有不同处理),避免调用fflush(stdin)
  3. 关闭文件的重要性fclose函数在关闭文件前会自动调用fflush刷新缓冲区,因此即使未手动调用fflush,只要正确关闭文件,数据也不会丢失;若不关闭文件,缓冲区数据可能始终不写入磁盘,导致数据丢失。
http://www.dtcms.com/a/618079.html

相关文章:

  • 网站建设需要哪些资料扶贫网站建设方案
  • C++标准模板库(STL)——list的模拟实现
  • 幽冥大陆(二十二)dark语言智慧农业电子秤读取——东方仙盟炼气期
  • 5.驱动led灯
  • RTL8367RB的国产P2P替代方案用JL6107-PC的可行性及实现方法
  • <MySQL——L1>
  • 有没有网站做字体变形自学软装设计该怎么入手
  • 做网站咸阳贵州省建设厅城乡建设网站
  • 分布式监控Skywalking安装及使用教程(保姆级教程)
  • 可信数据空间的分布式数字凭证和分布式数字身份
  • 分布式WEB应用中会话管理的变迁之路
  • 徐州市建设局招投标网站河南网站建站推广
  • 第44节:物理引擎进阶:Bullet.js集成与高级物理模拟
  • C++ Qt程序限制多开
  • 数据结构算法-哈希表:四数之和
  • 杭州翰臣科技有限公司优化方案物理必修三电子版
  • ASC学习笔记0024:移除一个现有的属性集
  • 洛阳做网站公司在哪建设工程信息管理网
  • win10安装miniforge+mamba替代miniconda
  • 旅游类网站策划建设_广告排版设计图片
  • Linux进程间通信三System V 共享内存完全指南原理系统调用与 C 封装实现
  • 云计算与大数据:数字化转型的双重引擎
  • 怎么弄免费的空间做网站铂爵旅拍婚纱摄影官网
  • 郑州中原区网站建设北京冬奥会网页设计
  • Java接口自动化测试之接口加密
  • 插值——Hermite 插值与分段三次 Hermite 插值
  • 外贸建站服务网站计划
  • tcp_Calculator(自定义协议,序列化,反序列化)
  • 【12】FAST角点检测:从算法原理到OpenCV实时实现详解
  • 设计模式实战精讲:全景目录