8 標準庫
STL標準模板庫在不同產品使用程度不同,這裡列出一些基本規則和建議,供各團隊參考。
規則8.1 避免使用auto_ptr
說明:在stl庫中的std::auto_ptr具有一個隱式的所有權轉移行為,如下代碼:
auto_ptr<T> p1(new T);
auto_ptr<T> p2 = p1;
當執行完第2行語句後,p1已經不再指向第1行中分配的對象,而是變為NULL。正因為如此,auto_ptr
不能被置於各種標準容器中。
轉移所有權的行為通常不是期望的結果。對於必須轉移所有權的情境,也不應該使用隱式轉移的方式。
這往往需要程式員對使用auto_ptr的代碼保持額外的謹慎,否則出現對null 指標的訪問。
使用auto_ptr常見的有兩種情境,一是作為智能指標傳遞到產生auto_ptr的函數外部,二是使用
auto_ptr作為RAII管理類,在超出auto_ptr的生命週期時自動釋放資源。
對於第1種情境,可以使用boost::shared_ptr或者是std::tr1::shared_ptr(在C++11標準中是
std::shared_ptr)來代替。
對於第2種情境,可以使用boost::scoped_ptr或者C++11標準中的std::unique_ptr來代替。其中
std::unique_ptr是std::auto_ptr的代替品,支援顯式的所有權轉移。
例外:
在C++11標準得到普遍使用之前,在一定需要對所有權進行轉移的情境下,可以使用std::auto_ptr,
但是建議對std::auto_ptr進行封裝,並禁用封裝類的拷貝建構函式和賦值運算子,以使該封裝類無法
用於標準容器。
規則8.2 僅將scoped_ptr、shared_ptr和unique_ptr用於管理單個對象
說明:boost::scoped_ptr、boost::shared_ptr、std::tr1::shared_ptr(在C++11標準中是
std::shared_ptr)和std::unique_ptr(C++11標準)都是用於管理單一對象的智能指標。當這些智能指
針在銷毀所指向的對象時使用的都是delete而不是delete[],而使用delete刪除數組是undefined行
為,因此不可使用上述智能指標管理數組。
當需要一個具有RAII特性的數組時,可以使用boost::scoped_array、boost::shared_array、
std::vector<std::tr1::shared_ptr>或std::vector<std::unique_ptr>(C++11標準)代替。
shared_ptr是基於引用計數的智能指標,可以安全用於大部分情境中,更新引用計數時需要略微消耗
一些效能,一般來說對效能不會有顯著的影響。使用shared_ptr的另外一個需要注意的問題是不要產
生循環參考,基於引用計數的智能指標當出現循環參考時會造成記憶體流失。當需要循環參考時,其中
一個智能指標請使用weak_ptr。
boost::shared_ptr、std::tr1::shared_ptr(std::shared_ptr)的安全執行緒與stl中常見類型的線程一
致,即:
1. 多個線程可以同時讀同一個shared_ptr對象;
2. 多個線程可以同時寫不同的shared_ptr對象。
註:當多個不同的shared_ptr對象指向同一個底層對象時,同時寫這些shared_ptr對象本身是線程安
全的,但是需要額外操作保證底層對象的安全執行緒。
規則8.3 如果涉及循環參考,使用weak_ptr解開迴圈
說明:當使用各種基於引用計數的shared_ptr時,會遇到循環參考的問題,例如:
#include <memory>
class TChild;
class TParent
{
public:
void SetChild(std::shared_ptr<TChild> const& Child)
{
Child_ = Child;
}
private:
std::shared_ptr<TChild> Child_;
};
class TChild
{
public:
void SetParent(std::shared_ptr<TParent> const& Parent)
{
Parent_ = Parent;
}
private:
std::shared_ptr<TParent> Parent_;
};
int main()
{
std::shared_ptr<TParent> Parent = std::make_shared<TParent>();
std::shared_ptr<TChild> Child = std::make_shared<TChild>();
Parent->SetChild(Child);
Child->SetParent(Parent);
//到這裡Parent和Child產生了循環參考,當Parent、Child超出範圍後將產生記憶體流失。
}
為瞭解決循環參考導致的記憶體流失,需要引入weak_ptr。將代碼修改成下面這樣:
class TChild
{
public:
void SetParent(std::shared_ptr<TParent> const& Parent)
{
Parent_ = Parent;
}
void UseParent()
{
//使用weak_ptr指向的對象之前通過lock()獲得shared_ptr。
std::shared_ptr<TParent> Parent = Parent_.lock();
}
private:
std::weak_ptr<TParent> Parent_;
};
注意紅色部分以及新增的UseParent()函數,示範了如何使用weak_ptr。另外需要注意的是SetParent()
函數中形參依然是shared_ptr,這樣將使用了weak_ptr的細節隱藏了起來,從外部看全部是
shared_ptr。舉例使用的是C++11標準中的std::shared_ptr,但是對於boost::shared_ptr和
std::tr1::shared_ptr同樣適用。
規則8.4 使用make_shared代替new產生shared_ptr
說明:在代碼中,我們可以使用形如std::shared_ptr<T> A(new T)的方式初始化shared_ptr。但是在
涉及到shared_ptr的地方使用new涉及到3個潛在的風險。
一是容易出現下面的代碼,訪問懸null 指標:
T* A = new T;
std::shared_ptr<T> B(A);
A->xxxxx; //當B超出範圍後:A指向的記憶體被釋放,訪問出錯
二是容易出現下面的代碼,引起重複delete:
T* A = new T;
std::shared_ptr<T> B(A);
//在許多代碼之後再次出現:
std::shared_ptr<T> C(A);
當使用一個原生指標初始化一個shared_ptr時,引用計數會被置為1,於是出現了2組獨立的引用計數 ,
當這2組引用計數到達0時都會引發銷毀對象的操作,於是就會出現重複delete的問題。
三是可能出現記憶體流失的風險,考慮如下代碼:
int func1();
void func2(std::shared_ptr<T> const& P1, int P2);
int main()
{
func2(std::shared_ptr<T>(new T), func1());
}
在以上調用func2的代碼中,根據編譯器不同,可能會以以下順序執行代碼:
1. new T
2. func1()
3. std::shared_ptr<T>構造
在這種情況下如果在func1()中拋出了異常,將會造成new T泄漏。所以建議使用new初始化的
shared_ptr要放入單獨的語句中,即將調用func2的代碼修改為:
std::shared_ptr<T> temp(new T);
func2(temp, func1());
使用以上方法相對略微煩瑣,多引入了一個變數。跟shared_ptr配套存在的make_shared可以解決使用
new初始化的問題。上述兩種情況下的代碼可以修改為如下:
std::shared_ptr<T> B = std::make_shared<T>();
func2(std::make_shared<T>(), func1());
make_shared模板會構造一個指定類型的對象和一個shared_ptr並用該對象初始化shared_ptr,以上操
作是一步完成的,不存在中間拋出異常的風險。對於構造時需要參數的類型,將參數加在make_shared
模板後面的括弧中即可。
延伸閱讀材料: 《Effective C++中文版 第三版》 [美]Scott Meyers著 侯捷譯 2006 電子工業出版
社 75頁 條款17: 以獨立語句將newed對象置入智能指標
規則8.5 對於同一個對象一旦使用shared_ptr,後續就要處處使用shared_ptr
說明:規則8.4描述了混用原生指標和shared_ptr容易導致問題:使用懸null 指標和重複釋放。所以,同
一對象的指標要統一用法,要麼使用原生指標,要麼使用shared_ptr,不要混用。
規則8.6 對於返回自身的shared_ptr指標的對象,要從enable_shared_from_this類派生
說明:對於需要使用shared_ptr管理的對象,當需要this指標時也需要使用對應的shared_ptr,但是
從3.4中可以看出,如果直接使用shared_ptr<T>(this)構造一個shared_ptr將會導致嚴重錯誤。為此,
boost和stl都提供了對應的enable_shared_from_this類,該類提供了一個shared_from_this()函數返
回this指標對應的shared_ptr。
樣本:
class TClass : public std::enable_shared_from_this<TClass>
{
public:
std::shared_ptr<TClass> GetSelf()
{
return shared_from_this();
}
std::shared_ptr<TClass const> GetSelf() const
{
return shared_from_this();
}
};
規則8.7 不要將使用不同版本stl、boost等模板庫編譯的模組串連在一起
說明:模板庫大量使用了內嵌函式,不同版本的模板庫編譯的模組對同一種資料類型的操作都已固化
在該模組中。如果不同版本的模板庫中同一種資料類型的結構或者記憶體布局不同,在一個模組中定義
的對象被另外一個模組操作時可能會產生嚴重的錯誤。因為靜態串連的模組常常不會劃分出明確的接
口,常常會相互訪問其它模組中定義的對象。
例外:
有些現存的模組已經無法獲得源碼,不可能重新使用同一版本的模板庫重新編譯。在這種情況下,如
果模組定義了清晰的介面,且介面中沒有傳遞存在風險的資料類型,可以謹慎地混用,但是一定要進
行充足的驗證。
規則8.8 不要儲存string::c_str()指標
說明:在C++標準中並未規定string::c_str()指標持久有效,因此特定stl實現完全可以在調用
string::c_str()時返回一個臨時儲存區並很快釋放。所以為了保證程式的移植性,一定不要儲存
string::c_str()的結果,而是在每次需要時直接調用。
樣本://不好的例子:
std::string DemoStr = "demo";
const char* buf = DemoStr.c_str();
//在這裡buf指向的位置有可能已經失效。
strncpy (info_buf,buf, INFOBUF_SIZE - 1);
建議8.1 不要將stl、boost等模板庫中的資料類型傳遞到動態連結程式庫或者其它進程中
說明:跨動態連結程式庫或者其它進程還存在另外一個更複雜的問題,一般來說,記憶體配置與釋放要在同
一個模組中,如果將容器或者其它資料類型傳遞到其它模組中由其它模組進行了記憶體配置釋放操作,
將會造成不可預知的問題,通常是程式崩潰或者資料消失等但是也有可能在某些情況下程式完全正常
運行,因此定位錯誤會比較困難。
建議8.2 使用容器時要評估大量插入刪除是否會產生大量記憶體片段
說明:不同的作業系統和執行階段程式庫分配記憶體的策略各不相同,由於容器記憶體多是動態分配而來,對於
反覆大量插入刪除的操作,有可能會造成大量的記憶體片段或者記憶體無法收回。
對於長期啟動並執行服務程式,建議在使用容器時要對容器是否會造成記憶體片段進行評估和測試,如果存
在風險的,可以使用記憶體池(如boost::pool)來避免這個問題。
建議8.3 使用string代替char*
說明:使用string代替char*有很多優勢,比如:
1. 不用考慮結尾的’\0’;
2. 可以直接使用+, =, ==等運算子以及其它字串操作函數;
3. 不需要考慮記憶體配置操作,避免了顯式的new/delete,以及由此導致的錯誤;
需要注意的是某些stl實現中string是基於寫時複製策略的,這會帶來2個問題,一是某些版本的寫時
複製策略沒有實現安全執行緒,在多線程環境下會引起程式崩潰;二是當與動態連結程式庫相互傳遞基於寫
時複製策略的string時,由於引用計數在動態連結程式庫被卸載時無法減少可能導致懸掛指標。因此,慎
重選擇一個可靠的stl實現對於保證程式穩定是很重要的。
例外:
當調用系統或者其它第三方庫的API時,針對已經定義好的介面,只能使用char*。但是在調用介面之
前都可以使用string,在調用介面時使用string::c_str()獲得字元指標。
當在棧上分配字元數組當作緩衝區使用時,可以直接定義字元數組,不要使用string,也沒有必要使
用類似vector<char>等容器。
建議8.4 使用stl、boost等知名模板庫提供的容器,而不要自己實現容器
說明:stl、boost等知名模板庫已經提供較完善的功能,與其自行設計並維護一個不成熟且不穩定的
庫,不如掌握和使用標準庫,標準庫的使用經驗在業界已有成熟的經驗和提示。
建議8.5 使用新的標準庫標頭檔
說明:使用stl的時候, 標頭檔採用<vector>、<cstring>等,而不是<vector.h>、<string.h>。