C语言进阶教程:文件操作高级 - 二进制文件的读写
1. 什么是二进制文件?
与文本文件不同,二进制文件存储的是原始的字节数据,而不是可读的字符。这意味着二进制文件可以存储任何类型的数据,如图像、音频、视频、程序的可执行文件、或者自定义的结构体数据等。它们不依赖于特定的字符编码(如ASCII或UTF-8),而是直接反映数据在内存中的表示。
与文本文件的区别:
- 文本文件:以字符为单位进行读写,内容是人类可读的文本。在不同操作系统上,行尾结束符可能不同(例如,Windows使用CRLF \r\n,Linux/macOS使用LF \n)。C语言在读写文本文件时,可能会对这些行尾符进行转换。
- 二进制文件:以字节为单位进行读写,内容是原始的二进制数据流。读写时不会进行任何转换,写入什么字节,读出来的就是什么字节。
2. 打开二进制文件
在C语言中,使用 fopen() 函数打开文件。为了以二进制模式打开文件,需要在模式字符串中包含 "b" 字符。
- "rb": 以二进制只读方式打开文件。文件必须存在。
- "wb": 以二进制只写方式打开文件。如果文件存在,则清空文件内容;如果文件不存在,则创建新文件。
- "ab": 以二进制追加方式打开文件。如果文件不存在,则创建新文件。写入的数据会添加到文件末尾。
- "rb+": 以二进制读写方式打开文件。文件必须存在。
- "wb+": 以二进制读写方式打开文件。如果文件存在,则清空文件内容;如果文件不存在,则创建新文件。
- "ab+": 以二进制读写追加方式打开文件。如果文件不存在,则创建新文件。读取从文件头开始,写入从文件尾开始。
#include <stdio.h>
int main() {
FILE *fp_read, *fp_write;
// 以二进制只读方式打开
fp_read = fopen("data.bin", "rb");
if (fp_read == NULL) {
perror("Error opening data.bin for reading");
return 1;
}
printf("File data.bin opened for binary reading.\n");
// 以二进制只写方式打开
fp_write = fopen("output.bin", "wb");
if (fp_write == NULL) {
perror("Error opening output.bin for writing");
fclose(fp_read); // 关闭已打开的文件
return 1;
}
printf("File output.bin opened for binary writing.\n");
// ... 文件操作 ...
fclose(fp_read);
fclose(fp_write);
printf("Files closed.\n");
return 0;
}
3. 读写二进制数据
对于二进制文件,通常使用 fread() 和 fwrite() 函数进行读写操作。这两个函数直接操作字节块,非常适合处理结构体、数组等数据。
fwrite()函数
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
- ptr: 指向要写入数据的内存块的指针。
- size: 要写入的每个数据项的大小(以字节为单位)。
- nmemb: 要写入的数据项的数量。
- stream: 指向 FILE 对象的指针,该对象指定了输出流。
函数返回成功写入的数据项的总数。如果发生错误,返回值会小于 nmemb。
fread()函数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
- ptr: 指向用于存储读取数据的内存块的指针。
- size: 要读取的每个数据项的大小(以字节为单位)。
- nmemb: 要读取的数据项的数量。
- stream: 指向 FILE 对象的指针,该对象指定了输入流。
函数返回成功读取的数据项的总数。如果到达文件末尾或发生错误,返回值会小于 nmemb。可以使用 feof() 和 ferror() 来区分这两种情况。
示例:读写结构体
#include <stdio.h>
#include <stdlib.h>
// 定义一个结构体
typedef struct {
int id;
char name[50];
double score;
} Student;
int main() {
FILE *outfile, *infile;
Student student_out = {1, "Alice", 95.5};
Student student_in;
// 写入结构体到二进制文件
outfile = fopen("students.dat", "wb");
if (outfile == NULL) {
perror("Error opening students.dat for writing");
return 1;
}
size_t written_count = fwrite(&student_out, sizeof(Student), 1, outfile);
if (written_count < 1) {
perror("Error writing to students.dat");
fclose(outfile);
return 1;
}
printf("Student data written to students.dat\n");
fclose(outfile);
// 从二进制文件读取结构体
infile = fopen("students.dat", "rb");
if (infile == NULL) {
perror("Error opening students.dat for reading");
return 1;
}
size_t read_count = fread(&student_in, sizeof(Student), 1, infile);
if (read_count < 1) {
if (feof(infile)) {
printf("End of file reached before reading student data.\n");
} else if (ferror(infile)) {
perror("Error reading from students.dat");
}
fclose(infile);
return 1;
}
printf("\nStudent data read from students.dat:\n");
printf("ID: %d\n", student_in.id);
printf("Name: %s\n", student_in.name);
printf("Score: %.2f\n", student_in.score);
fclose(infile);
return 0;
}
示例:读写数组
#include <stdio.h>
#define ARRAY_SIZE 5
int main() {
FILE *fp;
int numbers_out[ARRAY_SIZE] = {10, 20, 30, 40, 50};
int numbers_in[ARRAY_SIZE];
size_t count;
// 写入整数数组到二进制文件
fp = fopen("numbers.bin", "wb");
if (fp == NULL) {
perror("Error opening numbers.bin for writing");
return 1;
}
count = fwrite(numbers_out, sizeof(int), ARRAY_SIZE, fp);
if (count < ARRAY_SIZE) {
perror("Error writing numbers to numbers.bin");
}
printf("%zu integers written to numbers.bin\n", count);
fclose(fp);
// 从二进制文件读取整数数组
fp = fopen("numbers.bin", "rb");
if (fp == NULL) {
perror("Error opening numbers.bin for reading");
return 1;
}
count = fread(numbers_in, sizeof(int), ARRAY_SIZE, fp);
if (count < ARRAY_SIZE) {
if (feof(fp)) {
printf("Reached EOF before reading all numbers.\n");
} else if (ferror(fp)) {
perror("Error reading numbers from numbers.bin");
}
}
printf("%zu integers read from numbers.bin\n", count);
fclose(fp);
printf("Numbers read: ");
for (int i = 0; i < count; i++) {
printf("%d ", numbers_in[i]);
}
printf("\n");
return 0;
}
4. 注意事项
- 字节序(Endianness): 当在不同体系结构(大端 vs. 小端)的机器之间交换二进制文件时,需要注意字节序问题。fwrite 和 fread 不会自动处理字节序转换。如果需要跨平台兼容性,可能需要手动进行字节序转换,或者使用标准化的数据格式(如 Protocol Buffers, JSON (文本), XML (文本))。
- 数据对齐(Data Alignment): 结构体在内存中的存储可能会因为数据对齐而包含填充字节。当直接将结构体写入文件并在不同编译器或平台上读取时,如果对齐方式不同,可能会导致问题。可以使用 #pragma pack (或编译器特定指令) 来控制结构体打包,但这会牺牲一些性能。
- 指针: 不能直接将包含指针的结构体写入二进制文件并期望在另一个程序或同一程序的不同运行实例中正确读取。指针存储的是内存地址,这些地址在不同的上下文是无效的。如果需要存储链式结构,需要将其序列化(例如,将指针转换为相对偏移量或ID,并在读取时重建链接)。
- 文件大小: 二进制文件可以非常大,确保有足够的磁盘空间,并在读写时进行错误检查。
- 错误处理: 始终检查 fopen(), fread(), fwrite() 等函数的返回值,并使用 ferror() 和 feof() 来确定错误原因。
总结
二进制文件的读写是C语言文件操作中的重要部分,它允许程序高效地存储和检索非文本数据。通过 fopen() 以二进制模式打开文件,并使用 fread() 和 fwrite() 进行数据块的读写,可以方便地处理各种复杂的数据结构。然而,在进行二进制I/O时,务必注意字节序、数据对齐和指针等潜在问题,以确保数据的正确性和可移植性。