C++含vector的结构体(struct)文件存读崩溃问题求助
我是C++新手,已掌握C#基础语法(使用近一年),还有2年PHP和Python使用经验。尝试将包含vector的struct写入文件并后续读取,结构体定义如下:
struct inventoryItem { std::string itemName; bool unlocked{false}; }; struct saveData { std::string userName{"John"}; int hunger{100}; int health{100}; int money{5}; std::vector<inventoryItem> inventory; };
保存和读取代码如下:
void saveGame::createSave() { saveData saveObj; inventoryItem newItem; newItem.itemName = "shoes"; newItem.unlocked = true; saveObj.inventory.push_back(newItem); newItem.itemName = "knife"; saveObj.inventory.push_back(newItem); save(saveObj); return; } void saveGame::save(saveData toSave) { std::fstream fileW(saveGame::fileName, std::ios_base::in); if (!fileW.good() || !fileW.is_open()) { system("exit"); } else { fileW.write((char *) &toSave, sizeof(saveData)); fileW.close(); } return; }
saveGame::saveData saveGame::getSave() { std::fstream fileR(saveGame::fileName, std::ios_base::out); saveData toRetrieve; if (!fileR.good() || !fileR.is_open()) system("exit"); else { fileR.read((char *) &toRetrieve, sizeof(saveData)); } return toRetrieve; }
测试代码:
saveGame::saveData data = saveObj.getSave(); std::cout<<data.userName<<std::endl; std::cout<<data.health<<std::endl; std::cout<<data.inventory[0].itemName;
运行现象:除vector内的结构体数据外,其他数据均可正常访问,但一旦涉及vector相关数据,程序就无报错崩溃。
- 期望输出:
*CONSOLE OUTPUT* John //username 100 // health shoes // inventory.itemName (string) true // inventory.unlocked (bool) knife // inventory.itemName (string) false // inventory.unlocked (bool)
- 实际输出:
*CONSOLE OUTPUT* John //username 100 // health
输出后程序直接崩溃。
崩溃原因
你当前用fileW.write((char *)&toSave, sizeof(saveData))直接将结构体的内存二进制写入文件,但std::vector和std::string属于非值类型容器:它们内部仅存储指针(指向实际数据的内存地址)、大小、容量等元数据,真正的字符串内容、vector元素都存放在堆内存的其他区域。
举个简单例子:std::string userName里并没有直接存储"John"字符,而是存了一个指针,指向堆内存中存放"John"的地址。当你把整个结构体写入文件时,保存的只是这个指针的数值,而非"John"本身。
当你从文件读回结构体时,这个指针指向的内存地址早已失效(程序重启后原内存已被回收或占用),此时访问inventory[0].itemName就是在读取无效内存,直接触发程序崩溃。
另外代码还有两个基础错误:
- 保存文件时用了
std::ios_base::in(只读模式),根本无法写入文件,应改为std::ios_base::out | std::ios_base::binary - 读取文件时用了
std::ios_base::out(只写模式),根本无法读取文件,应改为std::ios_base::in | std::ios_base::binary
解决方案:手动序列化结构体成员
既然直接读写内存不可行,那就手动将每个成员的实际数据按顺序写入文件,读取时再按相同顺序解析还原。
1. 修改保存函数(save)
void saveGame::save(saveData toSave) { // 二进制写模式打开文件,不存在则创建,存在则覆盖 std::ofstream fileW(saveGame::fileName, std::ios_base::out | std::ios_base::binary); if (!fileW.is_open()) { std::cerr << "无法打开保存文件!" << std::endl; return; } // 保存std::string:先存字符串长度,再存实际字符 size_t nameLen = toSave.userName.size(); fileW.write((char*)&nameLen, sizeof(nameLen)); fileW.write(toSave.userName.c_str(), nameLen); // 保存int类型基础数据 fileW.write((char*)&toSave.hunger, sizeof(toSave.hunger)); fileW.write((char*)&toSave.health, sizeof(toSave.health)); fileW.write((char*)&toSave.money, sizeof(toSave.money)); // 保存vector:先存元素个数,再逐个保存每个inventoryItem size_t itemCount = toSave.inventory.size(); fileW.write((char*)&itemCount, sizeof(itemCount)); for (const auto& item : toSave.inventory) { // 保存item的itemName size_t itemNameLen = item.itemName.size(); fileW.write((char*)&itemNameLen, sizeof(itemNameLen)); fileW.write(item.itemName.c_str(), itemNameLen); // 保存item的unlocked布尔值 fileW.write((char*)&item.unlocked, sizeof(item.unlocked)); } fileW.close(); }
2. 修改读取函数(getSave)
saveGame::saveData saveGame::getSave() { saveData toRetrieve; // 二进制读模式打开文件 std::ifstream fileR(saveGame::fileName, std::ios_base::in | std::ios_base::binary); if (!fileR.is_open()) { std::cerr << "无法打开读取文件!" << std::endl; return toRetrieve; // 返回默认初始化的结构体 } // 读取userName:先读长度,再读字符 size_t nameLen; fileR.read((char*)&nameLen, sizeof(nameLen)); toRetrieve.userName.resize(nameLen); fileR.read(&toRetrieve.userName[0], nameLen); // 读取int类型基础数据 fileR.read((char*)&toRetrieve.hunger, sizeof(toRetrieve.hunger)); fileR.read((char*)&toRetrieve.health, sizeof(toRetrieve.health)); fileR.read((char*)&toRetrieve.money, sizeof(toRetrieve.money)); // 读取vector:先读元素个数,再逐个读取inventoryItem size_t itemCount; fileR.read((char*)&itemCount, sizeof(itemCount)); toRetrieve.inventory.reserve(itemCount); // 预分配空间提升效率 for (size_t i = 0; i < itemCount; ++i) { inventoryItem item; // 读取itemName size_t itemNameLen; fileR.read((char*)&itemNameLen, sizeof(itemNameLen)); item.itemName.resize(itemNameLen); fileR.read(&item.itemName[0], itemNameLen); // 读取unlocked fileR.read((char*)&item.unlocked, sizeof(item.unlocked)); toRetrieve.inventory.push_back(item); } fileR.close(); return toRetrieve; }
3. 修改测试代码
现在可以正常遍历vector输出所有内容:
saveGame::saveData data = saveObj.getSave(); std::cout << data.userName << std::endl; std::cout << data.health << std::endl; for (const auto& item : data.inventory) { std::cout << item.itemName << std::endl; std::cout << std::boolalpha << item.unlocked << std::endl; // std::boolalpha让bool输出true/false而非1/0 }
额外说明
和你熟悉的C#不同,C没有内置的对象序列化机制(类似.NET的BinaryFormatter),所以需要手动处理每个成员。如果后续开发大型项目,可以使用Boost.Serialization这类第三方库简化序列化流程,但新手先掌握手动序列化逻辑,能帮你更深刻理解C的内存模型。
内容的提问来源于stack exchange,提问作者NikkieDev

