C++程序出现double free错误,求解决对象存入unordered_map的问题
错误信息
free(): double free detected in tcache 2 Aborted (core dumped)
原始代码
#include <fstream> #include <string_view> #include <unordered_map> #include "nlohmann/json.hpp" using json = nlohmann::json; class Strategy { private: int mActionNum; double mNormSum; double* mRegretSum; double* mStrategy; double* mStrategySum; double* mAverageStrategy; public: Strategy (const int actionNum): mActionNum {actionNum} { mRegretSum = new double[actionNum]; mStrategy = new double[actionNum]; mStrategySum = new double[actionNum]; mAverageStrategy = new double[actionNum]; for (int a = 0; a < actionNum; ++a) { mRegretSum[a] = 0.0; mStrategy[a] = 1.0 / (double) actionNum; mStrategySum[a] = 0.0; mAverageStrategy[a] = 0.0; } } ~Strategy() { delete[] mRegretSum; delete[] mStrategy; delete[] mStrategySum; delete[] mAverageStrategy; } const double* get(const double weight) { mNormSum = 0.0; for (int a = 0; a < mActionNum; ++a) { mStrategy[a] = mRegretSum[a] > 0 ? mRegretSum[a] : 0; mNormSum += mStrategy[a]; } for (int a = 0; a < mActionNum; ++a) { if (mNormSum > 0) { mStrategy[a] /= mNormSum; } else { mStrategy[a] = 1 / mNormSum; } mStrategySum[a] += weight * mStrategy[a]; } return mStrategy; } const double* get_avg() { mNormSum = 0.0; for (int a = 0; a < mActionNum; ++a) { mNormSum += mStrategySum[a]; } for (int a = 0; a < mActionNum; ++a) { if (mNormSum > 0) { mAverageStrategy[a] = mStrategySum[a] / mNormSum; } else { mAverageStrategy[a] = 1.0 / (double) mActionNum; } } return mAverageStrategy; } }; int main () { std::ifstream f("/home/tomas/Dropbox/strategy.json"); json data = json::parse(f); std::unordered_map<std::string_view, Strategy> strategies; std::string info_set {}; for (auto& e : data) { info_set = e["cluster"].get<std::string>() + ',' + e["history"].get<std::string>(); Strategy strat { static_cast<int>(e["regret_sum"].size()) }; strategies.insert({info_set, strat}); } return 0; }
你的猜测与尝试
你猜测循环中的Strategy实例会被销毁,但这不是核心问题。你尝试将对象创建在单独函数中返回,问题依旧存在。
问题根源
Strategy类没有正确处理拷贝操作。C++默认的拷贝构造函数和赋值运算符是浅拷贝——只会复制指针的值,而不是指针指向的内存块。当你把strat插入到unordered_map时,会创建一个拷贝,原对象和拷贝对象的指针指向同一块堆内存。当原对象(循环里的strat)析构时,会释放这些内存;之后map里的拷贝对象析构时,又会尝试释放同一块内存,就触发了double free错误。
解决方案
下面提供三种可行的解决方式,按推荐程度排序:
1. 用标准容器替代原始指针(最推荐)
把Strategy里的double*换成std::vector<double>,标准容器会自动处理内存的拷贝、释放,不需要手动写析构函数,默认的拷贝构造和赋值就能正常工作。
修改后的Strategy类:
#include <vector> // 需要包含头文件 class Strategy { private: int mActionNum; double mNormSum; std::vector<double> mRegretSum; std::vector<double> mStrategy; std::vector<double> mStrategySum; std::vector<double> mAverageStrategy; public: Strategy (const int actionNum): mActionNum {actionNum}, mRegretSum(actionNum, 0.0), mStrategy(actionNum, 1.0 / (double)actionNum), mStrategySum(actionNum, 0.0), mAverageStrategy(actionNum, 0.0) {} // 不需要手动写析构函数,vector会自动管理内存 const double* get(const double weight) { mNormSum = 0.0; for (int a = 0; a < mActionNum; ++a) { mStrategy[a] = mRegretSum[a] > 0 ? mRegretSum[a] : 0; mNormSum += mStrategy[a]; } for (int a = 0; a < mActionNum; ++a) { if (mNormSum > 0) { mStrategy[a] /= mNormSum; } else { mStrategy[a] = 1.0 / (double)mActionNum; // 修复原代码除以0的问题 } mStrategySum[a] += weight * mStrategy[a]; } return mStrategy.data(); // 返回vector底层数组的指针 } const double* get_avg() { mNormSum = 0.0; for (int a = 0; a < mActionNum; ++a) { mNormSum += mStrategySum[a]; } for (int a = 0; a < mActionNum; ++a) { if (mNormSum > 0) { mAverageStrategy[a] = mStrategySum[a] / mNormSum; } else { mAverageStrategy[a] = 1.0 / (double) mActionNum; } } return mAverageStrategy.data(); } };
这样修改后,直接用原来的main代码就能正常工作,插入到unordered_map时的拷贝操作会正确复制所有数据,不会出现内存重复释放的问题。
2. 禁用拷贝,使用移动语义
如果一定要用原始指针,可以禁用拷贝构造和赋值运算符,实现移动构造和移动赋值,让对象的所有权转移而不是拷贝。
修改Strategy类:
class Strategy { // ... 原有成员和构造函数不变 ... // 禁用拷贝构造和赋值 Strategy(const Strategy&) = delete; Strategy& operator=(const Strategy&) = delete; // 实现移动构造 Strategy(Strategy&& other) noexcept: mActionNum(other.mActionNum), mNormSum(other.mNormSum), mRegretSum(other.mRegretSum), mStrategy(other.mStrategy), mStrategySum(other.mStrategySum), mAverageStrategy(other.mAverageStrategy) { // 将原对象的指针置空,避免析构时释放内存 other.mRegretSum = nullptr; other.mStrategy = nullptr; other.mStrategySum = nullptr; other.mAverageStrategy = nullptr; } // 实现移动赋值 Strategy& operator=(Strategy&& other) noexcept { if (this != &other) { // 先释放当前对象的内存 delete[] mRegretSum; delete[] mStrategy; delete[] mStrategySum; delete[] mAverageStrategy; // 转移所有权 mActionNum = other.mActionNum; mNormSum = other.mNormSum; mRegretSum = other.mRegretSum; mStrategy = other.mStrategy; mStrategySum = other.mStrategySum; mAverageStrategy = other.mAverageStrategy; // 置空原对象指针 other.mRegretSum = nullptr; other.mStrategy = nullptr; other.mStrategySum = nullptr; other.mAverageStrategy = nullptr; } return *this; } // ... 原有析构函数不变 ... };
然后main里插入时用std::move转移所有权:
strategies.insert({info_set, std::move(strat)}); // 或者直接emplace构造,避免临时对象 strategies.emplace(info_set, static_cast<int>(e["regret_sum"].size()));
3. 使用智能指针存储对象
把unordered_map的value类型改成std::unique_ptr<Strategy>,用智能指针自动管理内存,避免拷贝问题:
修改main函数:
#include <memory> // 包含头文件 int main () { std::ifstream f("/home/tomas/Dropbox/strategy.json"); json data = json::parse(f); std::unordered_map<std::string_view, std::unique_ptr<Strategy>> strategies; std::string info_set {}; for (auto& e : data) { info_set = e["cluster"].get<std::string>() + ',' + e["history"].get<std::string>(); // 创建智能指针,直接插入到map中 strategies.insert({info_set, std::make_unique<Strategy>(static_cast<int>(e["regret_sum"].size()))}); } return 0; }
这种方式不需要修改Strategy类(但要确保没有拷贝操作),智能指针会自动在对象不再被使用时释放内存。
关于对象存入unordered_map的说明
- 如果对象支持拷贝(比如用vector的方案),可以直接用
insert或emplace:insert会创建对象的拷贝,或者用std::move转移所有权;emplace直接在map的内存中构造对象,避免临时对象,效率更高。
- 如果对象不支持拷贝(比如禁用拷贝的方案),必须用移动语义或者智能指针,确保对象所有权正确转移,避免无效的拷贝操作。
- 关于
std::string_view作为key:要注意info_set是局部变量,string_view指向的是它的内存,当info_set在循环中被修改时,map里的key会变成无效的悬垂指针。建议把map的key改成std::string,这样会存储字符串的拷贝,避免悬空引用问题。
内容的提问来源于stack exchange,提问作者ViciniVicini

