我很喜歡新的C++11的智慧指標。在很多時候,對很多討厭自己管理記憶體的人來說是天賜的禮物。在我看來,C++11的智慧指標能使得C++新手教學更簡單。
其實,我已經使用C++11兩年多了,我無意中發現多種錯誤使用C++11智慧指標的案例,這些錯誤會使程式效率很低或者直接崩潰。為了方便查詢,我把它們按照下文進行了歸類。
在開始之前,我們用一個簡單的Aircraft類來展示一下這些錯誤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class Aircraft { private: string m_model; public: int m_flyCount; weak_ptr<aircraft> myWingMan; void Fly() { cout << "Aircraft type" << m_model << "is flying !" << endl; } Aircraft(string model) { m_model = model; cout << "Aircraft type " << model << " is created" << endl; } Aircraft() { m_model = "Generic Model"; cout << "Generic Model Aircraft created." << endl; } ~Aircraft() { cout << "Aircraft type " << m_model << " is destroyed" << endl; } }; </aircraft> |
錯誤#1:當唯一指標夠用時卻使用了共享指標
我最近在一個繼承的程式碼庫專案中工作,它使用了一個shared_ptr(譯者注:共享指標)建立和管理所有的物件。我分析了這些程式碼,發現在90%的案例中,被shared_ptr管理的資源並非是共享的。
有兩個理由可以指出這是錯誤的:
1、如果你真的需要使用獨有的資源(物件),使用shared_ptr而不是unique_ptr會使你的程式碼容易出現資源洩露和一些bug。
不易察覺的bug:有沒有想過這種情況,如果有其他程式設計師無意間通過賦值給另一個共享指標而修改了你共享出來的資源/物件,而你卻從沒有預料到這種事情!
不必要的資源使用:即使其他的指標不會修改你的物件資源,但也可能會過長時間地佔用你的記憶體,甚至已經超出了原始shared_ptr的作用範圍。
2、建立shared_ptr比建立unique_ptr更加資源密集。
shared_ptr需要維護一個指向動態記憶體物件的執行緒安全的引用計數器以及背後的一個控制塊,這使它比unique_ptr更加複雜。
建議 – 預設情況下,你應該使用unique_ptr。如果接下來有共享這個物件所有權的需求,你依然可以把它變成一個shared_ptr。
錯誤#2:沒有保證shared_ptr共享的資源/物件的執行緒安全性!
Shared_ptr可以讓你通過多個指標來共享資源,這些指標自然可以用於多執行緒。有些人想當然地認為用一個shared_ptr來指向一個物件就一定是執行緒安全的,這是錯誤的。你仍然有責任使用一些同步原語來保證被shared_ptr管理的共享物件是執行緒安全的。
建議– 如果你沒有打算在多個執行緒之間來共享資源的話,那麼就請使用unique_ptr。
錯誤#3:使用auto_ptr!
auto_ptr的特性非常危險,並且現在已經被棄用了。當該指標被當作引數進行值傳遞時會被拷貝建構函式轉移所有權,那麼當原始auto指標被再次引用時就會造成系統致命的崩潰。看看下面這個例子:
1 2 3 4 5 6 7 |
int main() { auto_ptr<aircraft> myAutoPtr(new Aircraft("F-15")); SetFlightCountWithAutoPtr(myAutoPtr); // Invokes the copy constructor for the auto_ptr myAutoPtr->m_flyCount = 10; // <span style="color: #ff0000;">CRASH !!!</span> } </aircraft> |
建議 – unique_ptr可以實現auto_ptr的所有功能。你應該搜尋你的程式碼庫,然後找到其中所有使用auto_ptr的地方,將其替換成unique_ptr。最後別忘了重新測試一下你的程式碼!
錯誤#4:沒有使用make_shared來初始化shared_ptr!
相較於使用裸指標,make_share有兩個獨特的優點:
1.效能: 當你用new建立一個物件的同時建立一個shared_ptr時,這時會發生兩次動態申請記憶體:一次是給使用new申請的物件本身的,而另一次則是由shared_ptr的建構函式引發的為資源管理物件分配的。
1 2 |
shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); // Two Dynamic Memory allocations - SLOW !!! </aircraft> |
與此相反,當你使用make_shared的時候,C++編譯器只會一次性分配一個足夠大的記憶體,用來儲存這個資源管理者和這個新建物件。
1 2 |
shared_ptr<aircraft> pAircraft = make_shared<aircraft>("F-16"); // Single allocation - FAST ! </aircraft></aircraft> |
2、在看了MS編譯器的memory標頭檔案實現以後,我發現當記憶體分配失敗時,這個物件就會被刪除掉。這樣的話使用裸指標初始化也不用擔心安全問題了。
建議- 使用make_shared而不是裸指標來初始化共享指標。
錯誤#5:在建立一個物件(裸指標)時沒有立即把它賦給shared_ptr。
一個物件應該在被建立的時候就立即被賦給shared_ptr。裸指標永遠不應該被再次使用。
看看下面則個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int main() { Aircraft* myAircraft = new Aircraft("F-16"); shared_ptr<aircraft> pAircraft(myAircraft); cout << pAircraft.use_count() << endl; // ref-count is 1 shared_ptr<aircraft> pAircraft2(myAircraft); cout << pAircraft2.use_count() << endl; // ref-count is 1 return 0; } </aircraft> |
這將會造成ACCESS VIOLATION(譯者注:非法訪問)並導致程式崩潰!!!
這樣做的問題是當第一個shared_ptr超出作用域時,myAircraft物件就會被銷燬,當第二個shared_ptr超出作用域時,程式就會再次嘗試銷燬這個已經被銷燬了的物件!
建議– 如果不使用make_shared建立shared_ptr,至少應該像下面這段程式碼一樣建立使用智慧指標管理的物件:
1 2 |
shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); </aircraft> |
錯誤#6:刪掉被shared_ptr使用的裸指標!
你可以使用shared_ptr.get()這個api從一個shared_ptr獲得一個裸指標的控制程式碼。然而,這是非常冒險的,應該儘量避免這種情況。看看下面這段程式碼:
1 2 3 4 5 6 7 |
void StartJob() { shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); Aircraft* myAircraft = pAircraft.get(); // returns the raw pointer delete myAircraft; // myAircraft is gone } </aircraft> |
一旦我們從這個共享指標中獲取到對應的裸指標(myAircraft),我們可能會刪掉它。然而,當這個函式結束後,共享指標pAircraft就會因為超出作用域而去試圖刪除myAircraft這個已經被刪除過的物件,而這樣做的結果就是我們非常熟悉的ACCESS VIOLATION(非法訪問)!
建議 – 在你從共享指標中獲取對應的裸指標之前請仔細考慮清楚。你永遠不知道別人什麼時候會呼叫delete來刪除這個裸指標,到那個時候你的共享指標(shared_ptr)就會出現Access Violate(非法訪問)的錯誤。
錯誤#7:當使用一個shared_ptr指向指標陣列時沒有使用自定義的刪除方法!
看看下面這段程式碼:
1 2 3 4 5 |
void StartJob() { shared_ptr<aircraft> ppAircraft(new Aircraft[3]); } </aircraft> |
這個共享指標將僅僅指向Aircraft[0] —— Aircraft[1]和Aircraft[2]將會在智慧指標超出作用域時未被刪除而造成記憶體洩露。如果你在使用Visual Studio 2015,就會出現堆損壞(heap corruption)的錯誤。
建議 – 保證在使用shared_ptr管理一組物件時總是傳遞給它一個自定義的刪除方法。下面這段程式碼就修復了這個問題:
1 2 3 4 5 |
void StartJob() { shared_ptr<aircraft> ppAircraft(new Aircraft[3], [](Aircraft* p) {delete[] p; }); } </aircraft> |
錯誤#8:在使用共享指標時使用迴圈引用!
在很多情況下,當一個類包含了shared_ptr引用時,就有可能陷入迴圈引用。試想以下場景:我們想要建立兩個Aircraft物件,一個由Maverick駕駛而另一個是由Iceman駕駛的(我忍不住要引用一下《壯志凌雲》(TopGun)!!!)。Maverick和Iceman的僚機駕駛員(Wingman)互相指向對方。
所以我們最初的設計會在Aircraft類中引入一個指向自己的shared_ptr。
1 2 3 4 5 6 7 8 |
class Aircraft { private: string m_model; public: int m_flyCount; shared_ptr<Aircraft> myWingMan; …. |
然後在main()函式中,建立Aircraft型物件Maverick和Goose,然後給每個物件指定他們的wingman:
1 2 3 4 5 6 7 8 9 10 11 |
int main() { shared_ptr<aircraft> pMaverick = make_shared<aircraft>("Maverick: F-14"); shared_ptr<aircraft> pIceman = make_shared<aircraft>("Iceman: F-14"); pMaverick->myWingMan = pIceman; // So far so good - no cycles yet pIceman->myWingMan = pMaverick; // now we got a cycle - neither maverick nor goose will ever be destroyed return 0; } </aircraft> |
當main()函式返回時,我們希望的是這兩個共享指標都被銷燬——但事實是它們兩個都不會被刪除,因為它們之間造成了迴圈引用。即使這兩個智慧指標本身被從棧上銷燬,但由於它們指向的物件的引用計數都不為0而使得那兩個物件永遠不會被銷燬。
下面是這段程式執行的輸出結果:
Aircraft type Maverick: F-14 is created
Aircraft type Iceman: F-14 is created
所以應該怎麼修復這個Bug呢?我們應該替換Aircraft類中的shared_ptr為weak_ptr!下面是修改後的main()程式再次執行的輸出結果:
Aircraft type Maverick: F-14 is created
Aircraft type Iceman: F-14 is created
Aircraft type Iceman: F-14 is destroyed
Aircraft type Maverick: F-14 is destroyed
注意到如何銷燬兩個Aircraft物件了嗎。
建議 – 在設計類的時候,當不需要資源的所有權,而且你不想指定這個物件的生命週期時,可以考慮使用weak_ptr代替shared_ptr。
錯誤#9:沒有刪除通過unique_ptr.release()返回的裸指標!
Release()方法不會銷燬unique_ptr指向的物件,但是呼叫Release後unique_ptr則從銷燬物件的責任中解脫出來。其他人(你!)必須手動刪除這個物件。
下面這段程式碼會出現記憶體洩露,因為Aircraft物件會一直存活,即使main()已經退出。
1 2 3 4 5 6 7 |
int main() { unique_ptr<aircraft> myAircraft = make_unique<aircraft>("F-22"); Aircraft* rawPtr = myAircraft.release(); return 0; } </aircraft> |
建議 – 無論何時,在對unique_ptr使用Release()方法後,記得一定要刪除對應的裸指標。如果你是想要刪掉unique_ptr指向的物件,可以使用unique_ptr.reset()方法。
錯誤#10:在呼叫weak_ptr.lock()的時候沒檢查它的有效性!
在使用weak_ptr之前,你需要呼叫lock()方法來獲取這個weak_ptr。lock()方法的本質是把這個weak_ptr升級為一個shared_ptr,這樣你就可以像使用shared_ptr一樣使用它了。然而,當weak_ptr指向的這個shared_ptr物件不再有效的時候,這個weak_ptr就為空了。使用一個失效的weak_ptr進行任何呼叫都會造成ACESS VIOLATION(非法訪問)。
舉個例子,在下面這段程式碼中,名為“myWingMan”的weak_ptr指向的這個shared_ptr,在呼叫pIceman.reset()時已經被銷燬。如果此時呼叫這個weak_ptr執行任何操作,都會造成非法訪問。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int main() { shared_ptr<aircraft> pMaverick = make_shared<aircraft>("F-22"); shared_ptr<aircraft> pIceman = make_shared<aircraft>("F-14"); pMaverick->myWingMan = pIceman; pIceman->m_flyCount = 17; pIceman.reset(); // destroy the object managed by pIceman cout << pMaverick->myWingMan.lock()->m_flyCount << endl; // <span style="color: #ff0000;">ACCESS VIOLATION</span> return 0; } </aircraft> |
這個問題的修復方法很簡單,在使用myWingMan這個weak_ptr之前進行一下有效性檢查就可以了。
1 2 3 4 |
if (!pMaverick->myWingMan.expired()) { cout << pMaverick->myWingMan.lock()->m_flyCount << endl; } |
校正:我的很多讀者指出,上面這段程式碼不能在多執行緒的環境下使用 – 如今99%的軟體都使用了多執行緒。weak_ptr可能會在被檢查有效性之後、獲取lock返回值之前失效。非常感謝我的讀者們指出這個問題!我將採用Manuel Freiholz給出的解決方案:在使用shared_ptr之前,呼叫lock()函式之後再檢查一下shared_ptr是否為空。
1 2 3 4 5 |
shared_ptr<aircraft> wingMan = pMaverick->myWingMan.lock(); if (wingMan) { cout << wingMan->m_flyCount << endl; } |
建議 – 一定要檢查weak_ptr是否有效 — 其實就是在使用共享指標之前,檢查lock()函式的返回值是否為空。
所以,接下來是什麼呢?
如果你想學習更多關於C++11智慧指標的細節或者C++11的更多知識,我向你推薦下面這些書。
1. C++ Primer (5th Edition) by Stanley Lippman (譯者注:C++ Primer(第五版),作者:Stanley Lippman)
2. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14 by Scott Meyers (譯者注:C++模板進階指南:42個改善C++11和C++14用法的細節,作者:Scott Meyers)
希望你在探索C++11特性的旅途中一切順利。如果你喜歡這篇文章請分享給你的朋友們!
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!