二进制文件和文本文件
二进制文件
二进制文件存储的是原始的字节数据,不进行任何文本编码或字符转换。这意味着文件中的数据以二进制形式存储,可以包括任意的字节值(0-255),避免了字符编码和格式化的开销,对于大文件或需要频繁读写的应用,二进制操作通常能提供更好的性能读取和写入操作以字节为单位进行。所以我们需要用特定的软件和库来解读二进制文件。
读写二进制文件可以使用 fread
和 fwrite,
这些函数可以直接处理内存中的二进制数据,不涉及任何字符转换。二进制文件没有特定的扩展名,但通常会使用 .bin
、.dat
、.exe
等扩展名,常见的压缩包,图片,视频等都是二进制文件。
文本文件
文本文件存储的是经过编码的字符数据,如 ASCII、UTF-8 或其他字符编码。读取和写入操作以字符为单位进行,使用的函数包括 fgets
、fputs
、fprintf
和 fscanf
。这些函数会处理字符编码和换行符等转换。文本文件通常使用 .txt
、.csv
、.log
、.xml
等扩展名。
能否用fgetc/fputc,fgets/fputs来对二进制文件进行读写操作呢?
不可以,原因如下:
- 在文本模式下,文件系统可能对数据进行转换(如换行符的转换),这在处理二进制文件时可能导致数据损坏。例如,在文本模式下,换行符可能被转换为 CRLF(回车换行)序列,这对二进制数据是不可取的。
当我们使用文本文件I/O函数的时候,我们是会对读取到的字符进行识别和处理的,比如说fgets函数识别到换行符后,本次读写会直接停止。所以文本I/O函数可能对数据进行识别并且进行对应的处理转换,无法正确读取二进制数据。对于二进制文件,我们通常希望原封不动地读写数据,而不进行任何转换或处理。这是二进制文件处理的一个核心特点。
二进制文件中的数据就是实际地数据,没有格式上的约束,如果你拿文本文件中格式上的规则来解读二进制文件,很可能就会产生误解。
fread和fwrite
函数介绍
使用fread/fwrite这组函数我们也需要自定义一个缓冲区。
两个函数的参数完全一样,都是需要四个参数。分别是缓冲区指针,数据块大小,数据块的个数,文件指针。
每次读取/写入的字节数=size*nmemb。
对于无结构的文件,我们通常会将fread和fwrite的第二个参数size设置为1。表明每个元素的大小是 1 字节,即每次读取的块大小是字节级别的。对于有结构的文件(例如存储了结构体或特定格式数据的文件),fread
和 fwrite
的第二个参数(size
)应该设置为每个数据元素的字节大小。这样可以确保以结构化的方式正确读取和写入数据。
返回值
返回值返回的是实际读取到/写入的完整的的元素块的个数, 向下取整。
成功读取的元素数量: 如果读取操作成功,fread
将返回成功读取的元素数量(count
的值)。如果返回值等于 count
,说明读取操作成功且没有遇到文件末尾。
小于 count
: 如果返回值小于 count
,则可能是因为遇到文件末尾 或读取过程中发生了错误。
fread/fwrite复制文件示例
对于无结构的文件
定义缓冲区,打开文件,读取和写入(fread从源文件中读取数据到缓冲区。每次读取的字节数由 bytesRead
记录。使用 fwrite
将缓冲区中的数据写入目标文件,如果 fwrite
的返回值与 bytesRead
不匹配,表示写入错误),关闭文件。对于无结构的文件通常会将每个元素的大小设置为1个字节。
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 1024 // 定义缓冲区大小
int main() {
FILE *sourceFile, *destinationFile;
char buffer[BUFFER_SIZE];
size_t bytesRead;
// 打开源文件
sourceFile = fopen("source.txt", "rb");
if (sourceFile == NULL) {
perror("Error opening source file");
return 1;
}
// 打开目标文件
destinationFile = fopen("destination.txt", "wb");
if (destinationFile == NULL) {
perror("Error opening destination file");
fclose(sourceFile);
return 1;
}
// 逐块读取源文件并写入目标文件
while ((bytesRead = fread(buffer, 1, BUFFER_SIZE, sourceFile)) > 0) {
if (fwrite(buffer, 1, bytesRead, destinationFile) != bytesRead) {
//判断的是每次写入的字节数和读取到的字节数是否相等
perror("Error writing to destination file");
fclose(sourceFile);
fclose(destinationFile);
return 1;
}
}
// 检查读取是否成功完成
if (ferror(sourceFile)) {
perror("Error reading from source file");
}
// 关闭文件
fclose(sourceFile);
fclose(destinationFile);
printf("File copied successfully.\n");
return 0;
}
对于有结构的文件
读写步骤和上面读写无结构文件的步骤一样。
有结构的文件:日志(每条日志都是一个结构体变量,大小一样),表格信息等
不一样的点是:1. 缓冲区数组元素类型都是结构体 Person buffer[BUFFER_SIZE];
2. fread和fwrite的第二个参数size是结构体的大小sizeof(Person),不再是以字节为单位进行读取
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char name[50];
int age;
} Person;
#define BUFFER_SIZE 1024 // 定义缓冲区大小为1024个结构体
int main() {
FILE *sourceFile, *destinationFile;
Person buffer[BUFFER_SIZE];
size_t elementsRead;
// 打开源文件
sourceFile = fopen("source.dat", "rb");
if (sourceFile == NULL) {
perror("Error opening source file");
return 1;
}
// 打开目标文件
destinationFile = fopen("destination.dat", "wb");
if (destinationFile == NULL) {
perror("Error opening destination file");
fclose(sourceFile);
return 1;
}
// 逐块读取源文件并写入目标文件
while ((elementsRead = fread(buffer, sizeof(Person), BUFFER_SIZE, sourceFile)) > 0) {
// 将读取的数据写入目标文件
if (fwrite(buffer, sizeof(Person), elementsRead, destinationFile) != elementsRead) {
perror("Error writing to destination file");
fclose(sourceFile);
fclose(destinationFile);
return 1;
}
}
// 检查读取是否成功完成
if (ferror(sourceFile)) {
perror("Error reading from source file");
}
// 关闭文件
fclose(sourceFile);
fclose(destinationFile);
printf("File copied successfully.\n");
return 0;
}
文件位置有关函数
fseek
- 使用
lseek
函数时,你直接操作文件描述符,通常用于系统级的文件处理。 - 使用
fseek
函数时,你操作的是文件指针,适用于标准 C 库的文件操作,通常与fopen
和fread
/fwrite
函数配合使用。
fseek函数的使用方法和之前的lseek函数使用方法一样,依旧是三个参数
ftell
ftell
函数是 C 标准库提供的一个用于获取文件指针当前位置的函数。它常用于在文件操作过程中获取当前的文件位置,以便进行后续的操作,例如恢复文件指针的位置或计算文件的大小
返回值
- 成功: 返回当前文件指针相对于文件开头的字节偏移量。这个偏移量是一个
long
类型的值。 - 失败: 返回
-1
,并且设置errno
以指示错误原因。具体的错误原因可以通过ferror
函数进一步了解。
使用场景
获取当前文件位置: 在文件读取或写入过程中,可以使用 ftell
获取文件指针的当前位置。
计算文件大小: 可以通过 fseek
将文件指针移动到文件末尾,然后使用 ftell
获取文件的总大小。
恢复文件位置: 在进行复杂的文件操作时,可以保存当前位置,并在需要时恢复到该位置。
格式化读写文本文件函数(scanf/printf,fscanf/fprintf)
这些函数主要是为了方便我们按照指定的格式来读写文件内容,都需要用到我们的格式化字符串来控制输入和输出内容的格式,它们定义了如何将数据格式化为字符串,或者如何从字符串中解析数据。
scanf/printf函数:scanf函数作用是从标准输入流(stdin)中读取格式化的数据,printf函数作用是向标准输出流(stdout)中写入格式化的数据。
从标准输入流 (stdin
) 中读取的数据通常是存储到程序中定义的变量或申请的内存中,并在程序中对数据进行进一步处理。标准输出流对应的我们的终端,所以向标准输出流中写入数据会直接显示在我们的终端屏幕上。
从标准输入流中读取数据的过程其实大致可分为两步:1.将用户从键盘中输入的数据在按下回车后直接放入到标准输入缓冲区。2.从标准输入流缓冲区中格式化读取数据放入到变量或者申请的内存中。所以我们在scanf读取数据的时候,并不是直接从键盘输入读取数据,而是从标准输入缓冲区中读取的。
补充:在输出printf以及下面的fprintf中不一定都是要写格式化字符串,输出单纯字符串常量可以不用写格式化字符串,但是为了保持格式一致,我还是更愿意加上"%s"这个格式化字符串。
fscanf/fprintf函数
scanf
和printf
: 只能用于标准输入 (stdin
) 和标准输出 (stdout
) 的格式化读写操作。fscanf
和fprintf
: 提供了更灵活的功能,可以在任意打开的文件流中进行格式化读写操作,包括标准输入流和标准输出流外的其他文件。
这种设计使得 scanf
和 printf
更加简洁,适合简单的标准输入输出场景,而 fscanf
和 fprintf
提供了对文件操作的支持,适合需要处理不同文件流的场景。
sscanf/sprintf/snprintf函数
sscanf
: 从字符串中读取格式化的数据。sprintf
: 将格式化的数据写入到字符串中,不安全,不推荐使用。snprintf
: 将格式化的数据写入到字符串中,指定缓冲区的最大大小,以避免缓冲区溢出,推荐使用。
我们对这三个函数不做详细的介绍
标准I/O缓冲区的类型
在上一篇文章中我们简单的介绍了当读写文件时的缓冲区,介绍了从文件中读数据或者向文件中写数据时缓冲区起到的作用。系统I/O函数(fopen,fclose,文件指针,按字符读写(fgetc,fputc),按行读写(fgets,fputs),检测文件状态(feof,ferror),标准I/O的缓冲区)-CSDN博客
- 标准 I/O 缓冲区不是我们自定义的缓冲区,而是使用标准方式打开文件,系统自动为文件流分配的缓冲区。
- 标准 I/O 缓冲区是 C 语言中用于优化文件输入输出和终端的输入输出操作的机制。它通过在内存中使用缓冲区来减少对磁盘的读写操作次数,从而提高程序的效率。标准 I/O 库(
stdio.h
)提供了三种主要类型的缓冲区:全缓冲、行缓冲和无缓冲。 - 每个通过
fopen
、fdopen
或freopen
创建的文件流,系统都会为其分配一个独立的缓冲区,系统默认打开的三个文件流stdin,stdout,stderr也有对应的缓冲区。
这篇文章接下来将更加全面地介绍一下标准I/O缓冲区。
- 全缓冲: 数据积累到缓冲区满后才写入文件,提高效率,适用于大文件操作。
- 行缓冲: 遇到行结束符或缓冲区满时写入数据,适用于交互式输出。
- 无缓冲: 每次写操作立即写入,适用于需要实时输出的操作。
无缓冲
特点:每次写操作都会立即将数据写入文件或终端,没有缓冲。
适用情况:适用于需要实时输出的操作,如日志记录或调试信息。
例子:标准错误(stderr
)通常是无缓冲的,以确保错误信息可以实时输出。
全缓冲
特点:数据在缓冲区中积累到一定量后才会被实际写入文件或终端。
适用情况:适用于需要大量数据传输的操作,提高效率,通过减少实际的 I/O 操作次数,例如文件操作。
例子:对于我们普通方式下打开的文件,默认的文件流缓冲区都是全缓冲类型。
全缓冲区中的内容被写入文件的时机:
1.缓冲区满了
2.显式刷新(适用fflash函数)
3. 文件关闭,当使用fclose关闭文件时,标准 I/O 库会自动将缓冲区中的所有数据写入到文件。这是确保所有缓冲数据在文件关闭时被正确写入的机制。
4.程序终止时,当程序正常终止(例如通过调用 exit
)时,标准 I/O 库会自动将所有缓冲区中的数据写入文件。这是为了防止程序异常终止时丢失缓冲区中的数据。
5.缓冲区类型发生改变,可以使用 setvbuf
函数来改变标准 I/O 缓冲区的类型。
行缓冲
特点:数据在缓冲区中积累到行结束符(例如 \n
),会被立即调用系统I/O写入文件或终端
适用情况:提供了较好的用户交互体验,因为数据会在遇到换行符或缓冲区满时被立即输出
例子:标准输入流(stdin
)和标准输出流(stdout
)通常是行缓冲的。
行缓冲区中的内容被写入文件的时机:
1.读取到了换行符时,立即将缓冲区中数据写入文件
2. + 全缓冲区的写入时机
setvbuf函数
setbuf
: 提供基本的缓冲区设置功能,只能选择全缓冲或无缓冲模式,并且只能使用固定大小的缓冲区。setvbuf功能完全包含了setbuf的功能,所以接下来我们仅仅介绍setvbuf函数用法。
setvbuf
函数不仅可以设置文件流的缓冲模式和缓冲区大小,还可以让自定义的缓冲区来代替系统自动分配的缓冲区发挥作用。
自定义缓冲区代替系统分配缓冲区例子
当读取的文件十分巨大时,可以自定义一个很大的数组作为缓冲区,这样节省系统I/O次数,提高效率。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("Error opening file");
return 1;
}
char customBuffer[8192]; // 自定义缓冲区
if (setvbuf(file, customBuffer, _IOFBF, sizeof(customBuffer)) != 0) {
perror("Error setting buffer");
fclose(file);
return 1;
}
fprintf(file, "This is a test message.\n");
fclose(file);
return 0;
}
fflash函数
作用:强制刷新文件流对应的缓冲区,让缓冲区中的内容写到目标文件中
验证三种缓冲区不同例子
1. 展示行缓冲和无缓冲的区别
#include <stdio.h> int main(int argc, char **argv){ fprintf(stdout,"%s","stdout"); fprintf(stderr,"%s","stderr"); return 0; }
#include <stdio.h> int main(int argc, char **argv){ fprintf(stdout,"%s","stdout\n"); fprintf(stderr,"%s","stderr\n"); return 0; }
我们可以发现在第一段代码中首先输出的是stderr而不是stdout,因为stdout文件流对应的是行缓冲区,stderr对应的是无缓冲区,这里第一段代码中没有读取到换行符,所以数据会保留在行缓冲区中,直到程序退出才会将缓冲区中数据放到标准输出流输出显示出来。在stderr文件流中放置数据虽然代码靠后,但是由于stderr对应无缓冲区,所以这里直接会将数据放置到stderr中,直接输出显示。
第二段代码由于读取到了换行符,行缓冲区直接调用系统I/O来立即将数据从行缓冲区写入到了stdout文件流,输出显示了出来。所以这里先输出了stdout,再输出了stderr。
2.验证全缓冲区在写满缓冲区的时候才会将内存从缓冲区写到文件中
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char **argv) { FILE *fp = fopen("a.txt", "w"); // 打开文件以进行写入 for (int i = 0; i < 5000; i++) { fputc('A', fp); // 写入字符'A' } pause(); return 0; }
在这里经过测试,我们可以看到系统给文件分配的默认缓冲区的大小为4096个字节,即4k。
具体测试是当写入2000,3000个字节的时候我们阻塞数据在缓冲区我们会发现另开一个命令行窗口检测a.txt的大小为0,说明还没有写入数据过,几次测试后就可以确认缓冲区的大小。