数据流和缓冲区是什么?
在C语言中,将在不同的输入/输出设备之间进行传递的数据抽象为“流”。
流实际上就是一个字节序列,输入函数的字节序列被称为输入流,输出函数的字节序列称为输出流。
根据数据形式,输入输出流可以分为 文本流(字符流)
和 二进制流
。
- 数据流 是指程序与数据的交互是以流的形式进行的。进行C语言文件的存取时,都会先进行“打开文件”操作,这个操作就是在打开数据流,而“关闭文件”操作就是关闭数据流。
- 缓冲区(Buffer) 是指程序执行时所提供的额外内存,可用来暂时存放做准备执行的数据。它的设置是为了提高存取效率,因为内存的存取速度比磁盘驱动器快得多。
C语言中带缓冲区的文件处理:当使用标准I/O函数(包含在头文件stdio.h中)时,系统会自动设置缓冲区,并通过数据流来读写文件。当进行文件读取时,不会直接对磁盘进行读取,而是先打开数据流,将磁盘上的文件信息拷贝到缓冲区内,然后程序再从缓冲区中读取所需数据
。当写入文件时,并不会马上写入磁盘中,而是先写入缓冲区,只有在缓冲区已满或“关闭文件”时,才会将数据写入磁盘
。
一、为什么使用文件?
在C语言中,使用文件的主要原因是为了实现数据的持久化存储和共享。文件是一种用于存储和读取数据的常见方式,它可以保存各种类型的数据,包括文本、图像、音频、视频等。
通过使用文件,程序可以实现以下功能:
- 数据持久化:程序可以将数据写入文件,以便在程序关闭后仍然可以读取和访问这些数据。这对于需要保存用户设置、配置信息或临时数据等非常有用。
- 数据共享:文件可以用于在不同的程序或系统之间共享数据。例如,一个程序可以生成一个包含分析结果的报告文件,另一个程序可以读取该文件以获取所需的信息。
- 备份和恢复:通过将数据写入文件,程序可以定期备份数据,并在需要时从备份中恢复。这有助于确保数据的完整性和可靠性。
- 外部数据访问:对于一些需要与外部系统交互的程序,文件可以作为不同系统之间的桥梁。例如,一个程序可以通过读取外部传感器生成的日志文件来获取数据。
总之,使用文件为C语言程序提供了持久化存储、数据共享、备份和恢复以及外部数据访问等功能,这些功能对于实现程序的灵活性和可扩展性非常重要。
二、什么是文件
在C语言中,文件被看作是数据源的一种。它能够存储在磁盘或其他存储设备上,提供了一种便捷的方式来读取和写入数据。(即磁盘上的文件是文件
)
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件
(从文件功能的角度来分类的)。
2.1 程序文件
包括
源程序文件
(后缀为.c),目标文件
(windows环境后缀为.obj),可执行程序
(windows环境后缀为.exe)。
2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中
读取数据的文件
,或者输出内容的文件
。
本章讨论的是数据文件。
在学习文件操作之前处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
三、文件的打开和关闭
3.1 文件指针
缓冲文件系统中,关键的概念是 “文件类型指针” ,简称 “文件指针” 。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名 FILE
.
例如,VS2013
编译环境提供的 stdio.h
头文件中有以下的文件类型申明:
struct _iobuf { char* _ptr; int _cnt; char* _base; int _flag; int _file; int _charbuf; int _bufsiz; char* _tmpfname; }; typedef struct _iobuf FILE;
不同的C编译器的 FILE
类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况 自动创建 一个 FILE
结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个 FILE
的指针来维护这个 FILE
结构的变量,这样使用起来更加方便。
下面我们可以创建一个 FILE*
的指针变量:
FILE* pf;//文件指针变量
定义 pf
是一个指向 FILE
类型数据的指针变量。可以使 pf
指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
例如:
3.2 文件的打开和关闭(fopen和fclose)
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个 FILE*
的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用 fopen
函数来打开文件,fclose
来关闭文件。
3.2.1 fopen
函数声明:
//打开文件 FILE * fopen ( const char * filename, const char * mode );
参数
filename
- - - 字符串,表示要打开的文件名称。mode
- - - 字符串,表示文件的访问模式,可以是以下表格中的值:
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
返回值
该函数返回一个 FILE
指针。否则返回 NULL
,且设置全局变量 errno
来标识错误。
3.2.2 fclose
函数声明:
//关闭文件 int fclose(FILE *stream);
参数
stream
- - - 这是指向FILE
对象的指针,该FILE
对象指定了要被关闭的流。
返回值
如果流成功关闭,则该方法返回零。如果失败,则返回EOF
。
实例代码:
/* fopen fclose example */ #include <stdio.h> int main() { FILE* pFile; //打开文件 pFile = fopen("myfile.txt", "w"); //文件操作 if (pFile != NULL) { fputs("fopen example", pFile); //关闭文件 fclose(pFile); } return 0; }
四、 文件的顺序读写
常见的读写函数如下标表所示:
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
4.1 fgetc函数(读一个字符)
C 库函数 int fgetc(FILE *stream)
从指定的流 stream
获取下一个字符(一个无符号字符),并把位置标识符往前移动。
声明:
int fgetc(FILE *stream);
参数
stream
- - - 这是指向FILE
对象的指针,该FILE
对象标识了要在上面执行操作的流。返回值
- 该函数以无符号
char
强制转换为int
的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回EOF
。
4.2 fputc函数(写一个字符)
C 库函数 int fputc(int char, FILE *stream)
把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream
中,并把位置标识符往前移动。
声明:
int fputc(int char, FILE *stream);
参数
char
- - - 这是要被写入的字符。该字符以其对应的int
值进行传递。stream
- - - 这是指向FILE
对象的指针,该FILE
对象标识了要被写入字符的流。返回值
- 如果没有发生错误,则返回被写入的字符。如果发生错误,则返回
EOF
,并设置错误标识符。
4.3 fgets函数(读一行字符串)
C 库函数 char *fgets(char *str, int n, FILE *stream)
从指定的流 stream
读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1)
个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
声明:
char *fgets(char *str, int n, FILE *stream);
参数
str
- - - 这是指向一个字符数组的指针,该数组存储了要读取的字符串。n
- - - 这是要读取的最大字符数(包括终止空字符)。通常是使用以str
传递的数组长度。stream
- - - 指向标识输入流的FILE
对象的指针。stdin
可以用作从标准输入读取的参数。返回值
- 成功后,函数返回
str
。- 如果在尝试读取字符时 遇到文件末尾,则设置
eof
指示器 (feof
)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(str
的内容保持不变)。- 如果发生读取错误,则设置错误指示器(
ferror
),并返回 空指针(但str
指向的内容可能已更改)。
4.4 fputs函数(写一行字符串)
C 库函数 int fputs(const char *str, FILE *stream)
把字符串写入到指定的流 stream
中,但 不包括空字符。
声明:
int fputs(const char *str, FILE *stream);
参数
str
- - - 这是一个数组,包含了要写入的以空字符终止的字符序列。stream
- - - 这是指向FILE
对象的指针,该FILE
对象标识了要被写入字符串的流。返回值
- 成功时,将返回非负值。
出错时,该函数返回EOF
并设置错误指示器(ferror
)。
4.5 fscanf函数
C 库函数 int fscanf(FILE *stream, const char *format, ...)
从流 stream
读取格式化输入。
声明:
int fscanf(FILE *stream, const char *format, ...);
该函数与scanf
函数类似,只是多了最前面的参数stream
- - -指向标识要从中读取数据的输入流的 FILE
对象的指针。若传入的输入流对象为 stdin
,则功能与 scanf
一样。
4.6 fprint函数
C 库函数 int fprintf(FILE *stream, const char *format, ...)
发送格式化输出到流 stream
中。
声明:
int fprintf(FILE *stream, const char *format, ...);
该函数与printf
函数类似,只是多了最前面的参数stream
- - -指向标识输出流的 FILE
对象的指针。若传入的输出流对象为 stdout
,则功能与 scanf
一样。
4.7 fread函数(二进制形式读数据)
C 库函数 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
从给定流 stream
读取数据到 ptr
所指向的数组中。
声明:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数
ptr
- - - 这是指向带有最小尺寸size*nmemb
字节的内存块的指针。size
- - - 这是要读取的每个元素的大小,以字节为单位。nmemb
- - - 这是元素的个数,每个元素的大小为size
字节。stream
- - - 这是指向FILE
对象的指针,该FILE
对象指定了一个输入流。返回值
- 返回成功读取的元素总数。
- 如果此数字与
count
参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都会设置正确的指标,可以分别用ferror
和feof
进行检查。- 如果大小或计数为零,则该函数返回零,并且流状态和
pt
r 指向的内容保持不变。- size_t 是无符号整数类型。
4.8 fwrite函数(二进制形式写数据)
C 库函数 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
把 ptr
所指向的数组中的数据写入到给定流 stream
中。
声明:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数
ptr
- - - 这是指向要被写入的元素数组的指针。size
- - - 这是要被写入的每个元素的大小,以字节为单位。nmemb
- - - 这是元素的个数,每个元素的大小为 size 字节。stream
- - - 这是指向FILE
对象的指针,该FILE
对象指定了一个输出流。返回值
- 返回成功写入的元素总数。
- 如果此数字与
count
参数不同,则写入错误阻止函数完成。在这种情况下,将为流设置错误指示器(ferror
)。- 如果大小或计数为零,则该函数返回零,错误指示器保持不变。
【拓展】:sscanf 和 sprintf
sscanf 函数声明:
int sscanf ( const char * str, const char * format, ...);
从字符串中读取格式化数据
- 从
str
读取数据并根据参数格式将它们存储到附加参数给出的位置,就像使用scanf
一样,但是从字符数组str
读取,而不是标准输入 (stdin
) 读取。 - 参数:
str
- - - 这是 C 字符串,是函数检索数据的源。 - 其他参数与
scanf
函数相同。
sprintf 函数声明:
int sprintf ( char * str, const char * format, ... );
将格式化数据写入字符串
- 使用与在
printf
上使用格式时打印的文本相同的文本组成字符串,但内容不是打印,而是作为 C 字符串存储在str
指向的缓冲区中。 str
- - - 指向存储生成的 C 字符串的缓冲区的指针。缓冲区应足够大以包含生成的字符串。- 终止空字符会自动追加到内容之后。
- 其他参数与
sprintf
函数相同。
五、文件的随机读写
5.1 fseek
根据文件指针的位置和偏移量来定位文件指针。
C 库函数 int fseek(FILE *stream, long int offset, int origin )
设置流 stream
的文件位置为给定的偏移 offset
,参数 offset
意味着从给定的 origin
位置查找的字节数。
设置的指针的位置是 起始位置 + 偏移量
声明:
int fseek(FILE *stream, long int offset, int origin);
参数
stream
- - - 这是指向FILE
对象的指针,该FILE
对象标识了流。offset
- - - 这是相对origin
的偏移量,以字节为单位。origin
- - - 这是表示开始添加偏移量offset
的位置。它一般指定为下列常量之一:
常量 | 描述 |
SEEK_SET | 文件的开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件的末尾 |
返回值
- 如果成功,该函数将返回零。
- 否则,它将返回非零值。
- 如果发生读取或写入错误,则设置错误指示器(
ferror
)。
5.2 ftell
C 库函数 long int ftell(FILE *stream)
返回给定流 stream
的当前文件位置。
声明:
long int ftell(FILE *stream);
参数
stream
- - - 这是指向FILE
对象的指针,该FILE
对象标识了流。返回值
- 成功后,返回仓位指标的当前值。
- 失败时,返回
-1L
,并将errno
设置为特定于系统的正值。
5.3 rewind
让文件指针的位置回到文件的起始位置。
C 库函数 void rewind(FILE *stream)
设置文件位置为给定流 stream
的文件的开头。
声明:
void rewind(FILE *stream);
参数
stream
- - - 这是指向FILE
对象的指针,该FILE
对象标识了流。返回值
- 该函数不返回任何值。
六、文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2022测试)。
测试代码:
#include <stdio.h> int main() { int a = 10000; FILE* pf = fopen("test.txt", "w"); fwrite(&a, 4, 1, pf);//二进制的形式写到文件中 fclose(pf); pf = NULL; return 0; }
两种数据存储形式的转化过程:
七、文件读取结束的判定
7.1 被错误使用的 feof
牢记: 在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
- 文本文件读取是否结束,判断返回值是否为
EOF ( fgetc )
,或者NULL ( fgets )
例如:
fgetc
判断是否为EOF
.fgets
判断返回值是否为NULL
.
- 二制文件的读取结束判断,判断返回值是否小于实际要读的个数。例如:
fread
判断返回值是否小于实际要读的个数。
正确的使用:
文本文件的例子:
#include <stdio.h> #include <stdlib.h> int main() { int c; // 注意:int,非char,要求处理EOF FILE* fp = fopen("test.txt", "r"); if (!fp) { perror("File opening failed"); return EXIT_FAILURE; } //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环 { putchar(c); } //判断是什么原因结束的 if (ferror(fp)) puts("I/O error when reading");//读取过程中出错 else if (feof(fp)) puts("End of file reached successfully");//读到文件尾结束 fclose(fp); fp = NULL; return 0; }
二进制文件的例子:
#include <stdio.h> #include <stdlib.h> enum { SIZE = 5 }; int main() { double a[SIZE] = { 1.,2.,3.,4.,5. }; FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式 if (!fp) { perror("File opening failed"); return EXIT_FAILURE; } fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组 fclose(fp); double b[SIZE]; fp = fopen("test.bin", "rb");//以二进制形式读文件 if (!fp) { perror("File opening failed"); return EXIT_FAILURE; } size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组 if (ret_code == SIZE) {//实际读取的元素个数与要读取的相同 puts("Array read successfully, contents: "); for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]); putchar('\n'); } else { // error handling if (feof(fp)) //读到文件末尾结束 printf("Error reading test.bin: unexpected end of file\n"); else if (ferror(fp)) {//读取文件过程中出错 perror("Error reading test.bin"); } } fclose(fp); fp = NULL; return 0; }
【总结】:ferror 和 feof
ferror
和 feof
是C语言中用于文件操作的函数,它们分别用于检查文件错误和文件结束。
ferror
函数用于检查文件是否发生了错误。它接受一个文件指针作为参数,并返回一个非零值(真)或零(假),表示文件是否发生了错误。如果文件发生了错误,ferror
函数将返回一个非零值,否则返回零。feof
函数用于检查文件是否已经到达文件末尾。它接受一个文件指针作为参数,并返回一个非零值(真)或零(假),表示文件是否已经到达末尾。如果已经到达文件末尾,feof
函数将返回一个非零值,否则返回零。- 这两个函数通常用于文件读取操作中,用于检查文件的状态。例如,在使用
fread
函数读取文件时,可以使用ferror
函数检查是否发生了错误,例如文件无法打开或读取失败等。同时,可以使用feof
函数检查是否已经到达文件末尾,以便在循环中跳出读取操作。
八、文件缓冲区
ANSIC 标准采用 “缓冲文件系统” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块 “文件缓冲区” 。
- 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
- 如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
- 缓冲区的大小根据C编译系统决定的。
#include <stdio.h> #include <windows.h> //VS2022 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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件的操作。
如果不做,可能导致读写文件的问题。