如何将含复杂类型的结构体写入二进制文件并跨程序按相同格式读取
核心错误分析
你现有代码的问题本质是对C++对象内存布局理解不到位,主要错误点如下:
- 直接对包含
std::string、std::vector等非POD类型的结构体执行fwrite完全错误:这类动态容器内部只存储指向堆内存的指针,直接写结构体只会把指针地址和少量内部元数据写入文件,实际存储的字符串、数组元素根本不会被写入,读取时拿到的是无效地址,必然出现段错误或乱码。 - 写入计数逻辑错误:比如
fwrite(&prevars.timevec,sizeof(prevars.timevec),prevars.timevec.size(),outfile)中sizeof(timevec)是vector对象本身的固定大小(通常仅24字节左右,和元素数量无关),乘上元素数量后写入的是N个vector对象的内存,完全不是实际的time_t数据。 - 缺少错误处理:
fopen、fwrite都有可能执行失败,没有判断直接操作会导致逻辑异常。 - 文件操作逻辑冗余:
wb模式打开文件时会自动截断原有内容,不需要先打开删除再重新打开。
解决思路
序列化动态结构必须遵守「先写长度、再写实际内容」的规则,递归处理所有子结构,不能直接批量写整个结构体。我们可以封装通用读写工具函数,再为每个自定义结构体实现对应的序列化逻辑,即可大幅简化代码同时避免运行错误。
代码实现示例
第一步:封装通用基础写入工具
#include <cstdio> #include <vector> #include <string> #include <stdexcept> #include <type_traits> // 基础可平凡拷贝类型写入 template<typename T> void write_val(FILE* fp, const T& val) { static_assert(std::is_trivially_copyable_v<T>, "仅支持可平凡拷贝的基础类型"); if (fwrite(&val, sizeof(T), 1, fp) != 1) { throw std::runtime_error("基础类型写入失败"); } } // 写入std::string void write_str(FILE* fp, const std::string& s) { size_t len = s.size(); write_val(fp, len); if (fwrite(s.data(), sizeof(char), len, fp) != len) { throw std::runtime_error("字符串写入失败"); } } // 写入一维std::vector template<typename T> void write_vec(FILE* fp, const std::vector<T>& vec) { size_t len = vec.size(); write_val(fp, len); if constexpr (std::is_trivially_copyable_v<T>) { // 基础类型直接批量写入 if (fwrite(vec.data(), sizeof(T), len, fp) != len) { throw std::runtime_error("向量写入失败"); } } else { // 自定义类型循环调用对应写入方法 for (const auto& item : vec) { write_val(fp, item); } } } // 写入二维vector template<typename T> void write_vec2(FILE* fp, const std::vector<std::vector<T>>& vec) { size_t outer_len = vec.size(); write_val(fp, outer_len); for (const auto& inner_vec : vec) { write_vec(fp, inner_vec); } } // 写入三维vector template<typename T> void write_vec3(FILE* fp, const std::vector<std::vector<std::vector<T>>>& vec) { size_t outer_len = vec.size(); write_val(fp, outer_len); for (const auto& mid_vec : vec) { write_vec2(fp, mid_vec); } }
第二步:为每个子结构体实现序列化逻辑
// 写入parameters结构体 void write_params(FILE* fp, const parameters& params) { write_val(fp, params.N); write_val(fp, params.radius); write_val(fp, params.t_sum); write_val(fp, params.obj); write_val(fp, params.dt); write_str(fp, params.t0); write_val(fp, params.lon0); write_val(fp, params.lat0); write_val(fp, params.lonn); write_val(fp, params.latn); write_val(fp, params.x0); write_val(fp, params.y0); write_val(fp, params.dl); } // 写入landmask结构体 void write_landmask(FILE* fp, const landmask& lm) { write_vec2(fp, lm.value); } // 写入data3D结构体 void write_data3D(FILE* fp, const data3D& data) { write_vec3(fp, data.u); write_vec3(fp, data.v); } // 写入position结构体 void write_position(FILE* fp, const position& pos) { write_vec(fp, pos.x); write_vec(fp, pos.y); } // 补充你未给出定义的obj_prop、griddf、cross_wind结构体的写入逻辑,写法和上面一致即可
第三步:实现主保存函数
void save_preprocessing(const preprocessing_variables& prevars) { const char* filename = "preprocessing.data"; FILE* outfile = fopen(filename, "wb"); if (!outfile) { throw std::runtime_error("无法打开输出文件"); } try { write_params(outfile, prevars.params); write_objprop(outfile, prevars.objprop); // 你自己补充实现的函数 write_griddf(outfile, prevars.grid); // 你自己补充实现的函数 write_vec(outfile, prevars.timevec); write_landmask(outfile, prevars.lmdomain); write_data3D(outfile, prevars.w1_array); write_data3D(outfile, prevars.w2_array); write_data3D(outfile, prevars.w3_array); write_vec(outfile, prevars.orientation); write_position(outfile, prevars.pos); write_crosswind(outfile, prevars.crosswind); // 你自己补充实现的函数 } catch (...) { fclose(outfile); throw; } fclose(outfile); }
读取逻辑注意事项
读取时的顺序必须和写入顺序完全一致,每个动态长度的结构先读长度再读内容,逻辑和写入完全对应即可。
额外建议
- 如果需要跨平台使用,要额外处理大小端字节序,统一使用大端或小端写入读取
- 尽量使用固定长度的基础类型(比如
int32_t、float32_t)代替int、long等长度受平台影响的类型,避免不同平台读取时长度不匹配 - 如果不需要跨语言调用,也可以直接使用现成的序列化库,不用自己手写序列化逻辑,稳定性更高
内容的提问来源于stack exchange,提问作者Huyền Pizgy
相关产品推荐
相关产品推荐

