在C语言中进行文件操作时,通过 fopen 函数打开文件时,若第二个参数 mode(打开模式)中包含 b 标志,则表示以二进制方式打开文件。当以二进制模式操作文件时,我们通常使用 fread 和 fwrite 这类专门用于二进制读写的函数。
二进制文件的读/写操作
fread 和 fwrite 是处理二进制文件读写最常用的两个函数。
| 函数 |
说明 |
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); |
从文件流 stream 中读取 size * nmemb 个字节存入 ptr 指向的缓冲区中。 |
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); |
将 ptr 指向的缓存区中的 size * nmemb 个字节写入文件 stream 中。 |
核心参数说明
size 是每个数据成员的长度,例如一个结构体的大小;nmemb 是要读/写的成员个数。最终操作的总字节数是 size * nmemb。
size 和 nmemb 都是大于等于1的正整数。
- 读取时,
ptr 指向的内存空间必须大于等于 size * nmemb,否则可能导致内存管理相关的越界问题,引发程序崩溃或数据错误。
- 使用这两个函数需要包含头文件
stdio.h。
- 如果读/写文件成功,函数返回成功操作的数据块数量
nmemb。如果遇到磁盘空间已满(写操作)或到达文件末尾(读操作),返回值可能小于 nmemb,甚至返回 0。因此,检查返回值是确保操作完整性的关键。
示例:将结构体数组写入文件
下面是一个完整的例子,演示如何将一个包含学生信息的结构体数组写入二进制文件。
// filename: binary_write.c
#include <stdio.h>
struct student {
char name[32];
int age;
int score;
};
int main(int argc, char *argv[]) {
FILE *pf = NULL; // 用于保存已经打开的文件
struct student stus[2] = {{"zhangsan", 18, 100}, {"lisi", 19, 80}};
int rw_value = 0; // 用于保存读写文件的返回值
printf("每个结构体的长度是: %ld\n", sizeof(struct student));
printf("结构体的数据个数是: %ld\n", sizeof(stus)/sizeof(stus[0]));
// 以二进制写的方式打开文件
pf = fopen("mydata.txt", "wb");
if (NULL == pf) {
perror("打开文件 mydata.tx");
return -1;
}
rw_value = fwrite(stus, sizeof(struct student),
sizeof(stus)/sizeof(stus[0]), pf);
// 检查写入结果
if (rw_value < sizeof(stus)/sizeof(stus[0])) {
printf("写入文件的数据不够完整,可能都是数据!\n");
}
printf("fwrite returned: %d\n", rw_value);
// 关闭文件
fclose(pf);
return 0;
}
编译并运行这个程序,看看效果:
weimingze@mzstudio:~$ ls
binary_write.c
weimingze@mzstudio:~$ gcc -o binary_write binary_write.c
weimingze@mzstudio:~$ ./binary_write
每个结构体的长度是: 40
结构体的数据个数是: 2
fwrite returned: 2
weimingze@mzstudio:~$ ls
binary_write binary_write.c mydata.txt
weimingze@mzstudio:~$ ls -l mydata.txt
-rw-r--r-- 1 weimingze weimingze 80 12月 6 10:16 mydata.txt
从运行结果可以看到,每个 struct student 结构体在内存中占用40个字节,数组中有两个元素。fwrite 函数成功返回2,表明两个结构体都被写入了。生成的文件 mydata.txt 大小正好是80字节,等同于两个结构体在内存中的总大小。这印证了二进制写操作的本质:将内存中的数据原样复制到文件中。
如果你用记事本之类的文本编辑器打开 mydata.txt,会发现除了“zhangsan”、“lisi”这些字符串部分可见,其余内容都是乱码。这是因为二进制方式保存的数据并非可读的文本编码,文本编辑器无法正确解析,所以显示为乱码。
使用二进制文件存储数据的注意事项
在实际开发中,使用二进制文件存储数据需要注意几个关键问题:
-
字节序问题:年龄和成绩这类整数数据,如果以小端字节序存入文件,而读取文件的机器默认是大端字节序,解析出来的数据就会出错。因此,在涉及跨平台或网络传输的场景下,必须在文件格式或协议中明确规定字节序。
-
结构体字节对齐问题:编译器为了优化内存访问速度,会对结构体进行字节对齐,这可能导致相同结构体在不同编译器或编译设置下占用不同大小的内存。通常可以使用 #pragma pack(1) 指令对结构体进行单字节对齐(压缩),以避免因对齐不一致导致的读写错误。
-
数据安全与冗余问题:上述例子中,name 数组长度为32,但实际可能只用了前面一部分存储姓名,数组后面未使用的内存也可能被写入文件。如果这些内存中残留了密码等敏感信息,就会造成安全隐患。因此,在写入前,应确保待写入缓冲区数据的正确性和纯净性,例如将字符串后的剩余部分显式清零。
示例:从文件中读取结构体数据
理解了如何写入,读取就顺理成章了。下面的程序演示如何读取刚才生成的 mydata.txt 文件,并将信息打印出来。
// filename: binary_read.c
#include <stdio.h>
struct student {
char name[32];
int age;
int score;
};
// 定义数组的最大元素个数是 100
#define STU_LEN (100)
int main(int argc, char *argv[]) {
FILE *pf=NULL;// 用于保存已经打开的文件
struct student stus[STU_LEN];
int rw_value = 0; // 用于保存读写文件的返回值
int i;
// 以二进制 读 的方式打开文件
pf = fopen("mydata.txt", "rb");
if (NULL == pf){
perror("打开文件 mydata.txt");
return -1;
}
// 计划读取 100 个数据信息
rw_value=fread(stus, sizeof(struct student), STU_LEN, pf);
// 检查写入结果
printf("fread returned: %d\n", rw_value);
// 打印所有学生信息:
for (i = 0; i < rw_value; i++) {
printf("姓名: %s, 年龄: %d, 成绩: %d\n",
stus[i].name, stus[i].age, stus[i].score);
}
// 关闭文件
fclose(pf);
return 0;
}
编译并运行这个读取程序:
weimingze@mzstudio:~$ ls
binary_read.c mydata.txt
weimingze@mzstudio:~$ ls -l mydata.txt
-rw-r--r-- 1 weimingze weimingze 80 12月 6 10:16 mydata.txt
weimingze@mzstudio:~$ gcc -o binary_read binary_read.c
weimingze@mzstudio:~$ ls
binary_read binary_read.c mydata.txt
weimingze@mzstudio:~$ ./binary_read
fread returned: 2
姓名: zhangsan, 年龄: 18, 成绩: 100
姓名: lisi, 年龄: 19, 成绩: 80
程序原本计划读取100个结构体数据,但 fread 只返回了2,这是因为文件里实际只存储了两个结构体的数据。返回值准确地告知我们成功读取的项目数量,这是处理变长或未知大小二进制数据的重要依据。
动手实验
为了加深对二进制文件操作和注意事项的理解,可以尝试以下实验:
- 修改
struct student 结构体,将年龄(age)改用 unsigned char 类型(一个字节)表示,成绩(score)改用 float 类型表示。使用 #pragma pack(1) 压缩结构体后,再以二进制方式将数据保存到文件中。
- 编写一个新的程序,读取上述实验生成的文件,并正确打印出文件中的数据。
通过这两个实验,你能更直观地体会到字节对齐、数据类型大小以及二进制数据精确还原的特点。掌握这些细节,是进行高效、可靠C语言开发的基础。更多深入的编程技巧和讨论,欢迎访问云栈社区与大家交流。