水木杉 发表于 2022-6-15 09:18

C++读取二进制结构体数据

最近遇到一个C++读取二进制结构体数据的问题,希望能得到大佬指点,背景是这样的,有一个程序需要保存很多参数,这些参数都存放在一个比较复杂的结构体中,保存和读取都是直接通过fstream中的write和read实现的,现在问题是这个结构体随着软件升级会发生一些改变(比如添加几个参数之类),改变后的结构体就无法解析之前保存的数据了,我想请教一下,有没有一种方法能做到升级后的结构体仍可以解析以前保存的数据呢?二进制分析这种技术有可能解决问题吗?


下面是二进制保存结构体的例子,摘自http://c.biancheng.net/view/302.html。

保存CStudent结构体:
#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
public:
    char szName;
    int age;
};
int main()
{
    CStudent s;
    ofstream outFile("students.dat", ios::out | ios::binary);
    while (cin >> s.szName >> s.age)
      outFile.write((char*)&s, sizeof(s));
    outFile.close();
    return 0;
}

读取CStudent结构体:
#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
    public:
      char szName;
      int age;
};
int main()
{
    CStudent s;      
    ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开
    if(!inFile) {
      cout << "error" <<endl;
      return 0;
    }
    while(inFile.read((char *)&s, sizeof(s))) { //一直读到文件结束
      int readedBytes = inFile.gcount(); //看刚才读了多少字节
      cout << s.szName << " " << s.age << endl;   
    }
    inFile.close();
    return 0;
}

现在的问题相当于CStudent这个结构体变了,如何在读取的时候 inFile.read((char *)&s, sizeof(s)) 仍然可以把以前的数据解析到 (char *)&s 中呢? 我自己想了一种办法就是使用命名空间,将以前的CStudent结构体定义在OldClass命名空间之下,读取的时候判断是不是用OldClass命名空间中的结构体读取解析数据,然后再利用memcpy拷贝到新的结构体中,但是这样做需要每次变动结构体后都需要专门针对结构体的转化写很多东西,有没有更方便更优雅的方式实现这个需求呢?

JuncoJet 发表于 2022-6-15 09:29

最优雅的当时是内存映射

JuncoJet 发表于 2022-6-15 09:32

内存映射,配合结构指针

DayBreak 发表于 2022-6-15 09:43

不如保存成一个json 或者 xml,来保存这种结构体,或者序列化,反序列化

as36601987 发表于 2022-6-15 09:49

加密后写完文件。

only998 发表于 2022-6-15 10:18

用类继承,首先考虑制作一个基类,里面实现所有的操作接口。然后用子类去扩展
比如 最开始基类有 abc三个成员 ,子类扩展多一个 d成员。
读取的时候根据版本实例化不同的子类,但是传递的时候都可以通过基类的指针传递,操作的API也保持了一致。

metoo2 发表于 2022-6-15 10:24

给文件加个版本数据,每次根据不同的版本读取不同的结构

iceee_creammm 发表于 2022-6-15 10:37

保存到数据库里面

plauger 发表于 2022-6-15 10:40

要做到这一点很简单的,只要改变一下这个配置结构设计就可以做到,比如:
typedef struct _config_param
{
    // params...
} config_param;

typedef struct _config_file
{
    int version;
    config_param params;
} config_file;

注意config_file,这就是写入文件的结构,第一个字段version是真正配置参数结构config_param 的版本号,每次读配置的时候先把version读出来,根据版本的不同来读取相应版本的config_param。

pzx521521 发表于 2022-6-15 10:45

本帖最后由 pzx521521 于 2022-6-15 11:10 编辑

#### 这是一个序列化和反序列化的问题,
+ 6L 9L 明显就不够优雅, 一个简单的结构体的序列化, 还要每一个版本写一个类
+ 二进制不二进制先不考虑, 因为这不是一个问题

#### 解决方案
+ **如果你能确定更新的时候, 只增不删, 每次增加字段的时候都在最后面最后面增加的话, **
在你现有的方法上, 判断流有没有到最后就好了
+ **如果不能保证**, 存的数据不包含字段名称(filed)/顺序id(以数字代替filed)的话是没有办法, 进行版本更新的
+ 从原理上也知道, 你根本无法去确定那个字段对应那个值

#### 如果要包含field,
+ 4L 说的可以序列化成json/xml/yml/ini 这些都有现成的包
json/xml/yml/ini 这些都可以存成二进制(可以直接被识别为文本的二进制)
+ 如果怕被看到就在内存里面加个密就好了, 可以自己随便写个异或加密或者用现有的aes之类的

#### 顺序id(以数字代替filed)
一般用二进制的目的是占用空间很小, 因为没有field相关的信息
所以这里要补充一下
顺序id(以数字代替filed), 这个其实就是protobuffer
附上网址:
https://developers.google.cn/protocol-buffers
及原理:
https://developers.google.cn/protocol-buffers/docs/encoding
因为用了一些压缩的算法(varint), 比内存流直接序列化的更小, 而且不可读

当然你也可以参考protobuffer 自己给字段前加一个id, 写一个自己序列化/反序列化的东西
ps: C++没有反射, 怎么用代替反射 你可以参考各种开源的json包
页: [1]
查看完整版本: C++读取二进制结构体数据