You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 19:52:50