shared_ptr的理解和注意事項
昨天把shared_ptr的原始碼分析了一遍,也看了一些有關shared_ptr分析的文章,今天自己來總結一下。
以獨立語句將newed物件置入智慧指標
對於下面這樣一個使用智慧指標的函式呼叫,可能會造成記憶體洩漏:
process(std::shared_ptr<int>(new int(10)), priority());
編譯器沒有規定傳入引數的呼叫順序,可能先執行new表示式,然後執行priority()函式,最後構造shared_ptr物件。如果priority()函式中丟擲異常,那麼new出來的記憶體將得不到釋放。這會造成記憶體洩漏。
所以要使用下面兩種方式:
方式一:
shared_ptr<int> p(new int(10));
process(p, priority());
方式二:
process(std::make_shared<int>(10), priority);
前者即是以獨立語句將new分配過的物件置入只能指標,後者是利用工廠函式,直接生成只能指標。
小知識:make_shared()和普通shared_ptr(new xx)構造方法的區別
make_shared的效率更高。直接使用shared_ptr(new xx)底層實際上涉及兩次記憶體分配,一次是為被管理的物件分配記憶體,一次是為ref_count控制塊分配記憶體。而make_shared則是一次性分配好一大塊記憶體來同時持有被管理的物件和ref_count控制塊。增加了程式碼執行速度,且能避免一些控制塊的薄記資訊(記憶體cookie),潛在減少了程式佔用的空間。不過make_shared有個缺點,使用make_shared會導致物件銷燬和記憶體被回收之間可能會有延遲。因為使用make_shared相當於把被管理物件記憶體和ref_count控制塊記憶體繫結在一起了,我們知道,ref_count控制塊記憶體本來是由use_count和weak_count共同維護的,假設被管理物件引用計數為0了,但是仍然有weak_ptr存在,那麼這一大塊記憶體就不能銷燬;而傳統的shared_ptr(new xx)方式,當物件引用計數為0時,物件記憶體銷燬。當weak_ptr引用計數為0時,ref_count控制塊記憶體銷燬,所有的記憶體都能夠及時清理。
- 相比於使用new,make函式可以消除程式碼重複,提高異常安全。而且std::make_shared生成的程式碼更小更快。不適用make函式的場合包括自定義刪除器和想要傳遞大括號初始值(這個暫時不太懂)。使用make函式不明智的場合包括:(1)自定義記憶體管理函式的類(2)記憶體緊張的系統,有非常大的物件,然後std::weak_ptr比std::shared_ptr長壽。
注意迴圈引用帶來的記憶體洩漏
迴圈引用就是指,兩個shared_ptr,你持有我,我持有你,雙方都認為對方死了自己才能銷燬,結果兩方都無法銷燬,造成記憶體洩漏。
示例程式碼:
class B;
class A {
public:
A(){
std::cout<<"A ctor"<<std::endl;
}
~A(){
std::cout<<"A dtor"<<std::endl;
}
void do_something() {
if(b_.lock()){
std::cout<<"lock success, still alive"<<std::endl;
std::cout<<"use_count="<<b_.use_count()<<std::endl; //輸出1
}
}
public:
//std::shared_ptr<B> b_;
std::weak_ptr<B> b_;
};
class B {
public:
B(){
std::cout<<"B ctor"<<std::endl;
}
~B(){
std::cout<<"B dtor"<<std::endl;
}
public:
std::shared_ptr<A> a_;
};
int main()
{
std::shared_ptr<A> pa(new A);
std::shared_ptr<B> pb(new B);
std::cout<<"pb->use_count"<<pb.use_count()<<std::endl;
pa->b_ = pb;
pb->a_ = pa;
pa->do_something();
std::weak_ptr<B> pbb(pb);
std::cout<<(pbb.lock()).use_count()<<std::endl; //輸出2
return 0;
我們可以使用weak_ptr來打破這個局面,而不是手動打破迴圈。一方持有另一方的shared_ptr(強引用),而另外一方則持有對方的weak_ptr(弱引用)。與之前的區別是,持有弱引用的一方在使用weak_ptr時,需要使用lock()方法,提升成為shared_ptr,提升失敗說明對方已死,提升成功便可正常使用。這樣就可以打破迴圈引用,避免記憶體洩漏。
程式碼中要注意的一點是,weak_ptr.lock()方法雖然會提升引用計數,但是如果我們僅僅把.lock()用作表示式,那麼實際上是臨時物件temp存在的時候引用計數為2,臨時物件在表示式結束後立即死亡,引用計數又回到1。所以,我們如果要使用weak_ptr提升成為shared_ptr,必須用一個shared_ptr來接收,否則weak_ptr不會提升成為shared_ptr。
類向外傳遞this與shared_ptr
當我們在一個類的成員函式中,想知道自己this所在的shared_ptr時,不可以用shared_ptr(this)直接構造,因為這樣會構造一個獨立的區域性shared_ptr,該shared_ptr銷燬時會釋放掉this。如果外部不知道該物件已經銷燬,再呼叫就會爆炸。所以應該用shared_from_this,所在類需要繼承std::enable_shared_from_this。
示例程式碼:
class A : public std::enable_shared_from_this<A> { //注意繼承要加模板引數
public:
A() { std::cout<<"ctor"<<std::endl; }
~A() { std::cout<<"dtor"<<std::endl; }
public:
void func() {
//std::shared_ptr<A> local_sp(this); //錯誤用法
//std::cout<<local_sp.use_count()<<std::endl; //引用計數為1,因為這個區域性的shared_ptr是用this單獨生成的,和外部沒有關係,所以會造成兩次銷燬doble delete
std::shared_ptr<A> local_sp = shared_from_this(); //正確用法
std::cout<<local_sp.use_count()<<std::endl; //引用計數為2
}
};
int main()
{
std::shared_ptr<A> sp(new A);
{
sp->func();
}
return 0;
}
上述錯誤情況輸出是:
正確情況輸出是:
可見,使用this在類內部再造一個shared_ptr是多麼可怕的事情,會造成多次釋放。
在多執行緒環境環境中,我們可能在成員函式中需要將this的智慧指標回撥給別的類物件,就可使用shared_from_this()。這樣,別的物件就能夠知道本物件是否還活著,避免在死亡之後的時候呼叫,產生可怕的後果。
shared_ptr的技術與陷阱
- 意外延長物件生命週期。可能容器持有shared_ptr,或者boost::function持有,這都會不經意間延長物件的生命期。我們可能會通過讓他們持有weak_ptr來避免生命期被延長。
函式引數,一個執行緒只要在最外層持有一個實體,那麼安全不成問題。比如:
void on_message(const string& msg){ shared_ptr<foo> f(new foo(msg)); //在最外層有一個實體,然後將shared_ptr傳給誰都可以,因為始終有一個引用計數為1存在。常引用方式傳遞效率更高。 if(validate(f)){ //const reference save(f); //const reference } }
- 虛析構不再是必須。因為shared_ptr有T和Y兩個引數。使用base類的shared_ptr接收derived的shared_ptr,ref_count中維護的指標型別仍是T,即base*型別。而shared_ptr類中維護的才是Y。刪除器還是按老樣子刪除。所以不要虛析構也成。
有一張圖可以看一下:
暫時完畢。
相關文章
- 直流負載箱的安全事項和注意事項有哪些?負載
- URLEncode和URLDecode的注意事項
- Xlistview的注意事項View
- Android Handler的使用方式和注意事項Android
- Oracle複合索引的建立和注意事項Oracle索引
- jquery 的ajax請求示例和注意事項jQuery
- 網站投放廣告的注意事項和技巧網站
- RandomAccessFile注意事項randomMac
- nginx 注意事項Nginx
- @Lombok注意事項Lombok
- 換工作的注意事項
- Shrink操作的注意事項
- Oracle使用*的注意事項Oracle
- MAVEN新增本地倉庫和注意事項!Maven
- 關於Golang struct{}{}用法和注意事項GolangStruct
- [譯] Vue.js — 注意事項和技巧Vue.js
- hive查詢注意事項和調優Hive
- jQuery 語法總結和注意事項jQuery
- PHP物件和介面抽象類注意事項PHP物件抽象
- SVN衝突解決和注意事項
- Model設計中常見的技巧和注意事項
- 低程式碼和無程式碼的注意事項
- 快取注意事項快取
- 使用parallel注意事項Parallel
- 字串分割注意事項字串
- 函式注意事項函式
- DUPLICATE DATABASE 注意事項Database
- bootstrap引用注意事項boot
- ovm搭建注意事項
- CSP 考前注意事項
- 生產注意事項
- 電量注意事項
- C++ queue的注意事項C++
- SQL 語句的注意事項SQL
- java equals()方法的注意事項Java
- oracle 轉pg 的注意事項Oracle
- fork 和 vfork 使用的注意事項和 system() 函式的替代函式
- iterator的romove方法的注意事項