【C语言进阶】文件操作
文章大纲
- 前言
- 一、文件概念
- 1.1 文件的作用
- 1.2 文件分类
- 1.2.1 程序文件
- 1.2.2 数据文件
- 1.2.3 文件名
- 1.2.4 二进制文件与文本文件
- 二、文件操作
- 2.1 文件的打开与关闭
- 2.1.1 文件指针
- 2.1.2 打开文件
- 2.1.3 关闭文件
- 2.2 文件的顺序读写
- 2.2.1 fgetc 与 fputc
- 2.2.2 fgets与 fputs
- 2.2.3 fscanf 与 fprintf
- 2.2.4 fread 与 fwrite
- 2.3 文件的随机读写
- 2.3.1 fseek
- 2.3.2 ftell
- 2.3.3 rewind
- 2.4 文件读取结束的判定
- 2.4.1 feof函数
- 2.4.2 判定方式
- 三、文件缓冲区
- 总结
前言
语言中的文件操作是编程中非常重要的一部分,它允许程序与外部数据进行交互,如读取用户输入的数据、保存程序生成的结果到文件中,或者修改现有文件的内容。C语言通过一系列的标准库函数来支持文件操作,这些函数定义在<stdio.h>
头文件中。
一、文件概念
1.1 文件的作用
如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使用文件。
1.2 文件分类
磁盘(硬盘)上的⽂件是⽂件。
但是在程序设计中,我们⼀般谈的⽂件有两种:程序文件、数据文件(从⽂件功能的角度来分类的)。
1.2.1 程序文件
程序文件,也被称为可执行文件或代码文件,是包含了一系列指令(代码)的文件,这些指令可以被计算机的处理器(CPU)执行以完成特定的任务或功能。程序文件通常是用高级编程语言(如C、C++、Java、Python等)编写的,然后经过编译器或解释器转换成计算机可以直接执行的机器语言或字节码。
程序文件的主要特点包括:
- 包含可执行的代码。
- 通常具有特定的文件扩展名,如.exe(Windows可执行文件)、.com(旧式DOS可执行文件)、.bat(批处理文件)、.py(Python脚本文件,需要解释器执行)、.jar(Java归档文件,包含Java字节码)等。
- 不能直接被用户编辑(除非使用相应的源代码编辑器并重新编译或解释)。
- 执行时,程序文件会加载到内存中,CPU按照程序中的指令序列执行操作。
1.2.2 数据文件
数据文件是存储了数据(如文本、数字、图像等)的文件,这些数据可以被程序读取、修改、添加或删除。数据文件不包含可执行代码,而是作为程序运行的输入或输出。数据文件可以是任何类型的文件,包括但不限于文本文件、图像文件、音频文件、视频文件、数据库文件等。
数据文件的主要特点包括:
- 存储数据,不包含可执行的代码。
- 可以是多种格式,包括文本(.txt)、CSV(.csv,逗号分隔值)、JSON(.json,JavaScript对象表示法)、XML(.xml,可扩展标记语言)、数据库文件(如.db、.sqlite等)等。
- 可以被程序读取和写入,作为程序运行的输入或输出。
- 用户可以直接查看或编辑数据文件(使用相应的编辑器或查看器)。
下面将主要探讨数据文件。
通常我们所处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处理的就是磁盘上⽂件。
1.2.3 文件名
1.2.4 二进制文件与文本文件
根据数据的组织形式不同,数据文件被分为二进制文件和文本文件。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是⽂本⽂件。
⼀个数据在⽂件中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
例如,整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),而⼆进制形式输出,则在磁盘上只占4个字节(VS2019测试)。
测试:
int main()
{int a = 10000;FILE* pf = fopen("test.txt", "wb");fwrite(&a, 4, 1, pf);fclose(pf);pf = NULL;return 0;
}
使用VS打开二进制文件
二、文件操作
2.1 文件的打开与关闭
2.1.1 文件指针
**在C语言中,文件是通过文件指针来访问的。**文件指针是一个指向FILE类型的指针,FILE是在<stdio.h>
中定义的一个结构体类型,用于表示一个打开的文件。文件指针用于存储文件的相关信息,如文件的位置、状态等。
例如,VS编译环境中提供的<stdio.h>
头文件中的文件类型申明:
struct _iobuf {char *_ptr;int _cnt;char *_base;int _flag;int _file;int _charbuf;int _bufsiz;char *_tmpfname;};
typedef struct _iobuf FILE;
2.1.2 打开文件
在C语言中,使用fopen函数来打开文件。fopen函数的原型如下:
FILE *fopen(const char *filename, const char *mode);
- filename 是要打开的文件名(包括路径,如果必要的话)。
- mode 是打开文件的模式,如 “r”(只读)、“w”(只写,文件不存在则创建,存在则清空)、“a”(追加,写入的数据会添加到文件末尾)、“r+”(读写)、“w+”(读写,文件不存在则创建,存在则清空)、“a+”(读写,写入的数据会添加到文件末尾)等。
- 如果fopen成功,它返回一个指向FILE对象的指针;如果失败,则返回NULL。
2.1.3 关闭文件
完成文件操作后,应该使用fclose函数关闭文件。关闭文件是一个好习惯,可以释放文件相关的资源,并确保所有的输出都被正确地写入文件。fclose的原型如下:
int fclose(FILE *stream);
- stream 是指向FILE对象的指针。
- 如果fclose成功,它返回0;如果失败,则返回EOF。
2.2 文件的顺序读写
一旦文件被成功打开,就可以使用一系列函数来读写文件了。常用的读写函数包括:
- fscanf、fscanf_s(安全版本,在某些编译器中可用)用于从文件中读取数据。
- fprintf、printf(通过标准输出流)用于向文件写入格式化数据。
- fgets、fputs用于读写字符串。
- fread、fwrite用于读写数据块(二进制文件)。
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输⼊函数 | 所有输⼊流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | ⽂本⾏输⼊函数 | 所有输⼊流 |
fputs | ⽂本⾏输出函数 | 所有输出流 |
fscanf | 格式化输⼊函数 | 所有输⼊流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | ⼆进制输⼊ | ⽂件 |
fwrite | ⼆进制输出 | ⽂件 |
上⾯说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流);
所有输出流⼀般指适⽤于标准输出流和其他输出流(如⽂件输出流)。
2.2.1 fgetc 与 fputc
fgetc 函数
fgetc 函数用于从指定的文件流中读取下一个字符(一个无符号字符),并将其作为 int 类型返回(返回该字符的ASCII码值)。如果到达文件末尾(EOF)或发生错误,则返回 EOF。 EOF 是一个在 stdio.h
中定义的宏,通常是一个负值,用于表示文件结束或读取错误。(值为-1)
函数原型如下:
int fgetc(FILE *stream);
- stream 是指向 FILE 对象的指针,该对象标识了要从中读取字符的文件流。
fputc 函数
fputc 函数用于将一个字符(一个无符号字符,但是作为 int 类型传递)写入到指定的文件流中。如果成功,它会返回写入的字符;如果发生错误,则返回 EOF。
函数原型如下:
int fputc(int char, FILE *stream);
- char 是要写入文件的字符(虽然参数类型是 int,但通常传递一个字符常量或字符变量的值)。
- stream 是指向 FILE 对象的指针,该对象标识了要写入字符的文件流。
测试实例:
int main()
{FILE* pf = fopen("read.txt", "r");if (pf == NULL){perror("fopen fail");return 1;}//读文件, fgetc 顺序读int ch = fgetc(pf);printf("%c", ch);ch = fgetc(pf);printf("%c", ch);ch = fgetc(pf);printf("%c", ch);ch = fgetc(pf);printf("%c", ch);//fputc()char ch = 'a';fputc(ch, pf);fclose(pf);pf = NULL;return 0;
}
2.2.2 fgets与 fputs
fgets 函数
fgets 函数用于从指定的文件流中读取一行文本(直到遇到换行符、文件结束符 EOF 或已读取了 n-1 个字符为止,其中 n 是指定的最大字符数),并将其存储在提供的字符串中。如果成功,它会返回指向该字符串的指针;如果发生错误或到达文件末尾而没有读取任何字符,则返回 NULL。
函数原型如下:
char *fgets(char *str, int n, FILE *stream);
- str 是指向用于存储读取的行的字符数组的指针。
- n 是要读取的最大字符数(包括最后的空字符 ‘\0’)。
- stream 是指向 FILE 对象的指针,该对象标识了要从中读取文本的文件流。
fputs 函数
fputs 函数用于将一个字符串写入到指定的文件流中,但不包括字符串末尾的空字符 ‘\0’。如果成功,它返回一个非负数;如果发生错误,则返回 EOF。
int fputs(const char *str, FILE *stream);
- str 是指向要写入文件的字符串的指针。
- stream 是指向 FILE 对象的指针,该对象标识了要写入文本的文件流。
注意:fputs与puts函数不同,不会自动换行,所以要想换行得自己手动输入。
测试实例:
int main()
{FILE* pf = fopen("fgets.txt", "r");FILE* wpf = fopen("fputs.txt", "w");if (pf == NULL || wpf == NULL){perror("file fail");return 1;}char buf[100] = { 0 };fgets(buf, 100, pf);for (int i = 0; i < strlen(buf); i++){printf("%c", buf[i]);}fputs(buf, wpf);return 0;
}
2.2.3 fscanf 与 fprintf
fscanf函数:
**fscanf函数用于从指定的文件流中读取数据,并根据提供的格式字符串解析这些数据,**然后将它们存储到相应的变量中。其原型如下:
int fscanf(FILE *stream, const char *format, ...);
- stream:指向FILE对象的指针,表示要从中读取数据的文件流。
- format:格式字符串,指定了后续参数的预期格式和数据类型。
- …:可变参数列表,表示要接收读取数据的变量的地址
fscanf函数根据format字符串中的格式说明符读取数据,并将读取到的数据存储在对应的变量中。如果读取成功,它会返回成功赋值的输入项的数量;如果到达文件末尾或发生读取错误,则可能返回EOF或小于请求的项目数。
注意事项:
- 在使用fscanf之前,需要确保文件已经被正确打开。
- fscanf在遇到空格、制表符或换行符时会停止读取当前字段,这可能会影响字符串的读取。
- 当fscanf的第一个参数设置为stdin时,其行为与scanf相同,即从标准输入读取数据。
fprintf函数:
fprintf函数用于向指定的文件流中写入格式化的数据。其原型如下:
int fprintf(FILE *stream, const char *format, ...);
- stream:指向FILE对象的指针,表示要写入数据的文件流。
- format:格式字符串,指定了后续参数的数据类型和输出格式。
- …:可变参数列表,表示要写入文件的具体数据
fprintf函数根据format字符串中的格式说明符将后续参数格式化为字符串,并将这些字符串写入到指定的文件流中。如果写入成功,它会返回写入的字符数(不包括结尾的空字符);如果发生错误,则返回负值。
注意事项:
- 在使用fprintf之前,需要确保文件已经被正确打开,并且以写入模式(如"w"、“a"或"w+”)打开。
- fprintf的工作方式类似于printf,但输出目标是文件而不是标准输出。
- 当fprintf的第一个参数设置为stdout时,其行为与printf相同,即向标准输出写入数据。
2.2.4 fread 与 fwrite
fread函数
函数原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
- ptr:指向存储读取数据的缓冲区的指针。
- size:每个数据单元的大小(以字节为单位)。
- nmemb:要读取的数据单元的数量。
- stream:文件指针,指向要读取的文件。
功能:
从指定的文件流中读取数据块,读取的字节数为size * nmemb,读取的数据存放在ptr为起始地址的内存中。如果读取成功,fread返回实际读取的数据单元数量(即nmemb的值,如果成功读取了所有请求的数据项)。如果返回值小于nmemb,则可能是遇到了文件结尾或发生了读取错误。
注意事项:
- 在使用fread之前,需要确保文件已经被正确打开,并且以读取模式(如"r"、“rb”)打开。
- fread在读取时不会停留在任何特定的分隔符(如空格、换行符等)上,而是连续读取指定数量的字节。
- 当以二进制模式(“rb”)打开文件时,fread可以读取任何类型的数据,包括结构体和数组。
fwrite函数
函数原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
- ptr:指向要写入数据的缓冲区的指针。
- size:每个数据单元的大小(以字节为单位)。
- nmemb:要写入的数据单元的数量。
- stream:文件指针,指向要写入的文件。
功能:
将内存中从ptr地址开始的数据块写入到指定的文件流中,写入的字节数为size * nmemb。如果写入成功,fwrite返回实际写入的数据单元数量(即nmemb的值,如果成功写入了所有请求的数据项)。如果返回值小于nmemb,则可能是发生了写入错误。
注意事项:
- 在使用fwrite之前,需要确保文件已经被正确打开,并且以写入模式(如"w"、“wb”、“a”、“ab”)打开。
- fwrite在写入时也不会考虑任何分隔符,而是连续写入指定数量的字节。
- 当以二进制模式(“wb”)打开文件时,fwrite可以写入任何类型的数据,包括结构体和数组。
2.3 文件的随机读写
2.3.1 fseek
根据文件指针的位置和偏移量来定位指针。
三个常用参数:
SEEK_SET 文件起始位置
SEEK_CUR 文件当前位置
SEEK_END 文件末尾位置
2.3.2 ftell
返回文件指针相对于起始位置的偏移量
2.3.3 rewind
让文件指针回到起始位置
2.4 文件读取结束的判定
2.4.1 feof函数
牢记:在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。
feof 的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束。
2.4.2 判定方式
- ⽂本⽂件读取是否结束,判断返回值是否为
EOF ( fgetc )
,或者NULL ( fgets )
- fgetc 判断是否为 EOF
- fgets 判断返回值是否为 NULL
- ⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。
例如:
fread判断返回值是否⼩于实际要读的个数。
三、文件缓冲区
ANSIC标准采⽤“缓冲⽂件系统”处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为程序中每⼀个正在使⽤的⽂件开辟⼀块**“⽂件缓冲区”**。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。
#include <stdio.h>
#include <windows.h>
//VS2019 WIN11环境测试
int main()
{FILE* pf = fopen("test.txt", "w");fputs("abcdef", pf);//先将代码放在输出缓冲区 printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");Sleep(10000);printf("刷新缓冲区\n");fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘) //注:fflush 在⾼版本的VS上不能使⽤了 printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");Sleep(10000);fclose(pf);//注:fclose在关闭⽂件的时候,也会刷新缓冲区 pf = NULL;return 0;
}
结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭文件。
如果不做,可能导致读写⽂件的问题。
总结
C语言中的文件操作是编程中不可或缺的一部分,它允许程序读取、写入、修改和创建存储在硬盘上的文件。
在使用文件操作时,要合理选择适合的函数进行操作,还有一些隐含知识没有写到,如:scanf/fscanf/sscanf, printf/fprintf/sprintf
函数的对比,流的概念以及标准流等,有需要可以自行了解。
初学这部分内容时可能觉得比较绕,不知道到底输入或者输出到哪里,建议对比之前的printf和scanf函数学习,这俩函数属于标准流函数,只适用于标准流。