C++单元测试中二进制文件加载引发内存访问违例问题求助
首先,你的核心问题出在直接对std::string和std::vector这类复杂STL对象进行二进制读写,以及循环读取条件的错误上,这些问题会导致内存中的无效指针,最终在对象析构(内存释放)时触发访问违例。
具体问题分析
1. std::string的错误序列化
Play::playName是std::string类型,它内部并非直接存储字符串内容,而是包含指向堆内存的指针、字符串长度、容量等成员。你用loadFile.read((char*)&curPlay.playName, sizeof(curPlay.playName))直接读取二进制数据到string对象中,相当于把旧的指针地址和状态加载了回来。当这个string对象析构时,它会尝试释放那个已经无效的旧指针指向的内存,这直接导致了访问违例。
2. std::vector的双重错误
- 你的
Play默认构造函数已经把playerPos等向量初始化为包含11个元素,但在load函数中又执行了11次push_back,这会让每个向量最终包含22个元素——这不仅会导致testPlays == testPlaysLoad断言失败,更重要的是:如果你的save函数也是直接写入vector的内存(而非逐个元素写入),那加载回来的vector内部指针同样是无效的,析构时会触发内存错误。 - 和
string一样,vector内部有指向元素存储区的指针、容量、大小等成员,直接读写这些二进制数据是完全不安全的,因为加载后的指针指向的是之前进程的内存空间,当前进程没有权限访问。
3. while (!loadFile.eof())的循环陷阱
这个循环条件会导致最后一次读取重复执行,因为eof()只有在读取操作尝试读取超过文件末尾后才会返回true。也就是说,当文件读到最后一个Play后,循环会再执行一次,这次读取会失败,但你仍然会把一个未初始化的curPlay加入到plays向量中,这个无效对象的析构也会引发内存错误。
排查与修复步骤
第一步:修复序列化逻辑(针对string和vector)
你需要手动实现Play对象的序列化/反序列化,不能直接读写整个对象的内存:
修改后的load函数关键部分
void Playbook::load(string fileName) { std::ifstream loadFile; string filePath = "./Playbooks/" + fileName + ".playbk"; loadFile.open(filePath, std::ios::binary); if (!loadFile.is_open()) { // 改用is_open判断,比bad()更准确 return; } // 先读取Play的数量(需要save函数也写入这个值) size_t playCount = 0; loadFile.read((char*)&playCount, sizeof(playCount)); for (size_t i = 0; i < playCount; ++i) { Play curPlay; // 读取string:先读长度,再读内容 size_t nameLen = 0; loadFile.read((char*)&nameLen, sizeof(nameLen)); std::vector<char> nameBuf(nameLen); loadFile.read(nameBuf.data(), nameLen); curPlay.playName = std::string(nameBuf.begin(), nameBuf.end()); // 读取基础类型 loadFile.read((char*)&curPlay.numDLine, sizeof(curPlay.numDLine)); loadFile.read((char*)&curPlay.numLB, sizeof(curPlay.numLB)); // 读取vector<int>:先读大小,再逐个读元素 size_t posSize = 0; loadFile.read((char*)&posSize, sizeof(posSize)); curPlay.playerPos.resize(posSize); for (size_t j = 0; j < posSize; ++j) { loadFile.read((char*)&curPlay.playerPos[j], sizeof(int)); } // 同理读取playerStance和playerBlitzGaps size_t stanceSize = 0; loadFile.read((char*)&stanceSize, sizeof(stanceSize)); curPlay.playerStance.resize(stanceSize); for (size_t j = 0; j < stanceSize; ++j) { loadFile.read((char*)&curPlay.playerStance[j], sizeof(int)); } size_t blitzSize = 0; loadFile.read((char*)&blitzSize, sizeof(blitzSize)); curPlay.playerBlitzGaps.resize(blitzSize); for (size_t j = 0; j < blitzSize; ++j) { loadFile.read((char*)&curPlay.playerBlitzGaps[j], sizeof(int)); } plays.push_back(curPlay); } loadFile.close(); setName(fileName); numPlays = plays.size(); }
对应的save函数需要同步修改
你需要确保save函数按照相同的格式写入数据:
void Playbook::save() { std::ofstream saveFile; string filePath = "./Playbooks/" + playbookName + ".playbk"; saveFile.open(filePath, std::ios::binary); if (!saveFile.is_open()) { return; } // 先写入Play的数量 size_t playCount = plays.size(); saveFile.write((char*)&playCount, sizeof(playCount)); for (const auto& play : plays) { // 写入string:先写长度,再写内容 size_t nameLen = play.playName.size(); saveFile.write((char*)&nameLen, sizeof(nameLen)); saveFile.write(play.playName.c_str(), nameLen); // 写入基础类型 saveFile.write((char*)&play.numDLine, sizeof(play.numDLine)); saveFile.write((char*)&play.numLB, sizeof(play.numLB)); // 写入vector<int>:先写大小,再逐个写元素 size_t posSize = play.playerPos.size(); saveFile.write((char*)&posSize, sizeof(posSize)); for (int val : play.playerPos) { saveFile.write((char*)&val, sizeof(int)); } // 同理写入playerStance和playerBlitzGaps size_t stanceSize = play.playerStance.size(); saveFile.write((char*)&stanceSize, sizeof(stanceSize)); for (int val : play.playerStance) { saveFile.write((char*)&val, sizeof(int)); } size_t blitzSize = play.playerBlitzGaps.size(); saveFile.write((char*)&blitzSize, sizeof(blitzSize)); for (int val : play.playerBlitzGaps) { saveFile.write((char*)&val, sizeof(int)); } } saveFile.close(); }
第二步:验证对象比较逻辑
确保operator==正确比较Play和Playbook的所有成员,尤其是向量的每个元素和字符串内容,避免因为序列化后的对象和原对象不一致导致断言失败。
第三步:调试时的额外排查点
- 用调试器查看
load后的testPlaysLoad.plays中的每个Play对象,检查playName的内部指针是否有效,向量的大小和元素是否正确。 - 检查
Play和Playbook的析构函数是否有自定义逻辑(目前看起来没有,但如果有,要确保没有重复释放内存的问题)。
调用栈信息的验证
你的调用栈显示_Container_base12::_Orphan_all_unlocked(),这是STL容器在管理内部迭代器/内存时的操作,正好对应我们分析的:无效的string或vector内部指针触发了内存访问错误,完全符合问题根源。
内容的提问来源于stack exchange,提问作者Andy K

