最近由於業務需要在寫內存池子時遇到了一個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/
