在C++程序开发中,经常会遇到需要将内存中的结构体对象持久化保存到文件的需求,将结构体存入二进制文件是一种高效的实现方式,fwrite函数作为C标准库中的二进制写入函数,能够很好地完成这一任务,实现简单的对象序列化功能。
基础结构体序列化实现
对于不包含指针成员、内存布局连续的基础结构体,使用fwrite直接写入二进制文件是最简单的方式。首先需要定义结构体类型,然后创建结构体实例并赋值,最后调用fwrite将结构体数据写入文件。
下面是一个简单的示例代码,演示基础结构体的序列化过程:
#include <stdio.h>
#include <string.h>
// 定义基础结构体,无指针成员
struct Student {
int id;
char name[20];
float score;
};
int main() {
// 创建并初始化结构体对象
Student stu;
stu.id = 1001;
strcpy(stu.name, "张三");
stu.score = 89.5f;
// 以二进制写入模式打开文件
FILE* file = fopen("student.dat", "wb");
if (file == NULL) {
printf("文件打开失败n");
return 1;
}
// 使用fwrite将结构体写入二进制文件
// 参数依次为:数据地址、单个数据字节数、数据个数、文件指针
size_t writeCount = fwrite(&stu, sizeof(Student), 1, file);
if (writeCount != 1) {
printf("数据写入失败n");
} else {
printf("结构体序列化成功n");
}
// 关闭文件
fclose(file);
return 0;
}
从二进制文件读取结构体数据
序列化后的数据需要能够读取回来,对应的读取操作可以使用fread函数完成,流程和写入类似,先打开二进制文件,再读取数据到结构体实例中。
读取的示例代码如下:
#include <stdio.h>
struct Student {
int id;
char name[20];
float score;
};
int main() {
Student stu;
// 以二进制读取模式打开文件
FILE* file = fopen("student.dat", "rb");
if (file == NULL) {
printf("文件打开失败n");
return 1;
}
// 使用fread读取二进制数据到结构体
size_t readCount = fread(&stu, sizeof(Student), 1, file);
if (readCount != 1) {
printf("数据读取失败n");
} else {
printf("读取到的学生信息:n");
printf("ID:%dn", stu.id);
printf("姓名:%sn", stu.name);
printf("分数:%.2fn", stu.score);
}
fclose(file);
return 0;
}
需要注意的核心问题
内存对齐影响
编译器默认会对结构体进行内存对齐,不同编译器、不同平台的对齐规则可能存在差异,这会导致结构体的实际大小不等于成员大小的简单相加。如果使用fwrite写入,读取时如果结构体对齐规则变化,就会出现数据解析错误。
可以通过编译器指令取消内存对齐,保证结构体布局的一致性,以GCC编译器为例,可以使用__attribute__((packed))属性:
// 取消内存对齐的结构体定义
struct __attribute__((packed)) Student {
int id;
char name[20];
float score;
};
指针成员处理
如果结构体包含指针成员,直接使用fwrite写入只会保存指针本身的值(也就是内存地址),而不会保存指针指向的实际数据。当程序重启后,原来的内存地址已经无效,读取到的指针值也没有意义,还会导致内存访问错误。
对于包含指针成员的结构体,需要先序列化指针指向的数据,读取时再重新分配内存并赋值指针。例如结构体包含字符串指针的场景:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 包含指针成员的结构体
struct Person {
int age;
char* name; // 指针成员
};
int main() {
Person p;
p.age = 25;
p.name = (char*)malloc(20);
strcpy(p.name, "李四");
FILE* file = fopen("person.dat", "wb");
if (file == NULL) {
printf("文件打开失败n");
free(p.name);
return 1;
}
// 先写入age
fwrite(&p.age, sizeof(int), 1, file);
// 写入name字符串的长度
int nameLen = strlen(p.name) + 1;
fwrite(&nameLen, sizeof(int), 1, file);
// 写入name指向的实际字符串
fwrite(p.name, sizeof(char), nameLen, file);
fclose(file);
free(p.name);
// 读取逻辑
Person p2;
file = fopen("person.dat", "rb");
if (file == NULL) {
printf("文件打开失败n");
return 1;
}
fread(&p2.age, sizeof(int), 1, file);
int readLen;
fread(&readLen, sizeof(int), 1, file);
p2.name = (char*)malloc(readLen);
fread(p2.name, sizeof(char), readLen, file);
printf("读取到的人员信息:n");
printf("年龄:%dn", p2.age);
printf("姓名:%sn", p2.name);
fclose(file);
free(p2.name);
return 0;
}
跨平台兼容性
不同平台的大小端字节序、类型长度可能存在差异,比如int类型在有的平台是2字节,有的是4字节,直接使用fwrite写入的结构体在跨平台读取时也会出现问题。如果需要在不同平台间传输序列化数据,还需要额外处理字节序和类型长度的兼容问题。
适用场景总结
使用fwrite实现结构体二进制序列化适合以下场景:单平台运行、结构体无指针成员或者已经做好指针数据处理、不需要跨平台兼容的简单数据持久化需求。如果是复杂场景或者需要跨平台使用,建议使用更成熟的序列化库来实现,避免手动处理各类兼容问题。