本文由 伯樂在線 - 欣仔 翻譯,wrm 校稿。未經許可,禁止轉載!
英文出處:Deb Haldar。歡迎加入翻譯組。
我很喜歡新的C++11的智能指針。在很多時候,對很多討厭自己管理內存的人來說是天賜的禮物。在我看來,C++11的智能指針能使得C++新手教學更簡單。
其實,我已經使用C++11兩年多了,我無意中發現多種錯誤使用C++11智能指針的案例,這些錯誤會使程序效率很低或者直接崩潰。爲了方便查找,我把它們按照下文進行了歸類。
在開始之前,我們用一個簡單的Aircraft類來展示一下這些錯誤。
C++
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指針被再次引用時就會造成系統致命的崩潰。看看下面這個例子:
C++
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的構造函數引發的爲資源管理對象分配的。
C++
1 2 |
shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); // Two Dynamic Memory allocations - SLOW !!! </aircraft> |
與此相反,當你使用make_shared的時候,C++編譯器只會一次性分配一個足夠大的內存,用來保存這個資源管理者和這個新建對象。
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。裸指針永遠不應該被再次使用。
看看下面則個例子:
C++
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,至少應該像下面這段代碼一樣創建使用智能指針管理的對象:
C++
1 2 |
shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); </aircraft> |
錯誤#6:刪掉被shared_ptr使用的裸指針!
你可以使用shared_ptr.get()這個api從一個shared_ptr獲得一個裸指針的句柄。然而,這是非常冒險的,應該儘量避免這種情況。看看下面這段代碼:
C++
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指向指針數組時沒有使用自定義的刪除方法!
看看下面這段代碼:
C++
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管理一組對象時總是傳遞給它一個自定義的刪除方法。下面這段代碼就修復了這個問題:
C++
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。
C++
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:
C++
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()已經退出。
C++
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執行任何操作,都會造成非法訪問。
C++
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之前進行一下有效性檢查就可以了。
C++
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是否爲空。
C++
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)