最近由于业务需要在写内存池子时遇到了一个doule-free的问题。折腾半个晚上以为自己的眼睛花了。开始以为是编译器有问题(我也是够自信的),但是在windows下使用qtcreator vs2017 和Linux下 使用gcc纷纷编译执行得到相同的结果。有一点要说的是使用gcc和qtcreator(mingW)虽然都double-free了,但是都没有给出错误的执行代码,vs在执行到析构函数时却可以给出给出异常提示。不得不说vs的运行检查更严格一些。下面我们先说正事后聊段子。
下面先贴出代码,如果你能一眼看出问题,请在评论里叫我一声”弟弟“
#include <list> #include <mutex> #include <algorithm> #include <memory> #include <assert.h> #include <iostream> using namespace std; template<typename T> class DataObjectPool { public: DataObjectPool(){} ~DataObjectPool(){} public: T* Get() { std::lock_guard<std::mutex> lockGuard(m_mutex); typename PtrList::iterator it = m_freeList.begin(); std::unique_ptr<T> memoryPtr; T* pReturn = nullptr; if (it == m_freeList.end()) { memoryPtr.reset(new T()); } else { memoryPtr.reset((*it).release()); m_freeList.pop_front(); } pReturn = memoryPtr.get(); m_usedList.push_back(std::move(memoryPtr)); return pReturn; } void Free(T* pData) { std::lock_guard<std::mutex> lockGuard(m_mutex); std::unique_ptr<T> memoryPtr; memoryPtr.reset(pData); auto itr = std::find(m_usedList.begin(),m_usedList.end(),memoryPtr); if(itr != m_usedList.end()) { m_freeList.push_back(std::move(memoryPtr)); m_usedList.erase(itr); } } private: typedef std::list<std::unique_ptr<T>> PtrList; private: std::mutex m_mutex; PtrList m_usedList; PtrList m_freeList; }; class Test { public: Test() { cout << "Test()" << endl; } ~Test() { cout << "~Test()" << endl; } }; int main(int argc, char *argv[]) { DataObjectPool<Test> PoolTest; auto pt1 = PoolTest.Get(); auto pt2 = PoolTest.Get(); PoolTest.Free(pt1); PoolTest.Free(pt2); }
运行结果:
Test() Test() ~Test() ~Test() ~Test() ~Test() D:\workspace\Test\build-Test2-Desktop_Qt_5_8_0_MinGW_32bit-Debug\debug\Test2.exe exited with code 0
看代码太麻烦,这里画出逻辑图,肯定是Free过程中除了什么问题,代码对图再看一下。
分析:
1) memoryPtr通过reset方法获取到指针后通过std::move(memoryPtr)将指针转移了,所以它不会释放指针。
2) list::erase方法删除迭代器确实会释放释放相关内存,可是内存在释放之前不是已经std::move吗?
问题的关键就在这里:
std::unique_ptr<T> memoryPtr 和 auto itr迭代器所包裹的指针是一样的,即都包裹了pData,可是他们根本不是一个东西。而是两个对象,看下面这段代码,可以更清楚:
#include <iostream> #include <memory> using namespace std; class Test { public: Test() { cout<<"Test()"<<endl; } ~Test() { cout<<"~Test()"<<endl; } }; int main() { Test *t = new Test(); unique_ptr<Test> upt1; //!依照上面的模型upt1可以看作是某个迭代器它包含t指针; upt1.reset(t); unique_ptr<Test> upt2; //!后来者对t也宣布了所有权 upt2.reset(t); if(upt1 == upt2) { cout<<upt1.pointer()<<endl; cout<<"upt1==upt2"<<endl; } }
一个裸指针在丢给一个unique_ptr对象之前,该unique_ptr认为对该指针所有权是唯一的,当多个unique_ptr对象引用同一个裸的指针是无法检查该指针被谁引用,这一点,所以也double free也就不稀奇了。
奇怪的是虽然std::unique_ptr是指针的所有权是独占的,指针只能转移,但是其却重载了operator ==运算符。既然是独占的,也就是std::unique_ptr不能在逻辑上承认多个unique_ptr对象对同一指针同时占有。这个操作让人非常疑惑。这也就是代码"auto itr = std::find(m_usedList.begin(),m_usedList.end(),memoryPtr)"能够返回有效迭代器和"if(upt1 == upt2)"能够成立的原因了。下面贴出std::unique_ptr "=="操作符重载的实现:
其内部是对两个裸指针的比较,让人费解的实现。
回归正题,原因弄明白了,我们来修改一下来让来避免这个问题:
推荐一篇文章:
《C++ 智能指针的正确使用方式》总结的不错!
https://www.cyhone.com/articles/right-way-to-use-cpp-smart-pointer/