智能指针weak_ptr
weak_ptr這個指針天生一副小弟的模樣,也是在C++11的時候引入的標準庫,它的出現完全是為了彌補它老大shared_ptr天生有缺陷的問題。
相比于上一代的智能指針auto_ptr來說,新進老大shared_ptr可以說近乎完美,但是通過引用計數實現的它,雖然解決了指針獨占的問題,但也引來了引用成環的問題,這種問題靠它自己是沒辦法解決的,所以在C++11的時候將shared_ptr和weak_ptr一起引入了標準庫,用來解決循環引用的問題。
循環引用
什么是循環引用的問題呢?在shared_ptr的使用過程中,當強引用計數為0時,就會釋放所指向的堆內存。那么問題來了,如果和死鎖一樣,當兩個shared_ptr互相引用,那么它們就永遠無法被釋放了。
例如:
#include <iostream> #include <memory>class CB; class CA {public:CA() {std::cout << "CA()" << std::endl;}~CA() {std::cout << "~CA()" << std::endl;}void set_ptr(std::shared_ptr<CB>& ptr) {m_ptr_b = ptr;}private:std::shared_ptr<CB> m_ptr_b; };class CB {public:CB() {std::cout << "CB()" << std::endl;}~CB() {std::cout << "~CB()" << std::endl;}void set_ptr(std::shared_ptr<CA>& ptr) {m_ptr_a = ptr;}private:std::shared_ptr<CA> m_ptr_a; };int main() {std::shared_ptr<CA> ptr_a(new CA());std::shared_ptr<CB> ptr_b(new CB());ptr_a->set_ptr(ptr_b);ptr_b->set_ptr(ptr_a);std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;return 0; }編譯并運行結果,打印為:
yngzmiao@yngzmiao-virtual-machine:~/test$ ./main? CA() CB() 2 2對于打印的內容,你可能會覺得很奇怪,為什么析構函數并沒有調用呢?
既然析構函數沒有調用,就說明ptr_a和ptr_b兩個變量的引用計數都不是0。下面分析一下例子中的引用情況:
起初定義完ptr_a和ptr_b時,只有①、③兩條引用,即ptr_a指向CA對象,ptr_b指向CB對象。然后調用函數set_ptr后又增加了②、④兩條引用,即CB對象中的m_ptr_a成員變量指向CA對象,CA對象中的m_ptr_b成員變量指向CB對象。
這個時候,指向CA對象的有兩個,指向CB對象的也有兩個。當main函數運行結束時,對象ptr_a和ptr_b被銷毀,也就是①、③兩條引用會被斷開,但是②、④兩條引用依然存在,每一個的引用計數都不為0,結果就導致其指向的內部對象無法析構,造成內存泄漏。
weak_ptr解決循環引用
weak_ptr的出現就是為了解決shared_ptr的循環引用的問題的。以上文的例子來說,解決辦法就是將兩個類中的一個成員變量改為weak_ptr對象,比如將CB中的成員變量改為weak_ptr對象,即CB類的代碼如下:
class CB {public:CB() {std::cout << "CB()" << std::endl;}~CB() {std::cout << "~CB()" << std::endl;}void set_ptr(std::shared_ptr<CA>& ptr) {m_ptr_a = ptr;}private:std::weak_ptr<CA> m_ptr_a; };編譯并運行結果,打印為:
yngzmiao@yngzmiao-virtual-machine:~/test$ ./main? CA() CB() 1 2 ~CA() ~CB()通過這次結果可以看到,CA和CB的對象都被正常的析構了。修改后例子中的引用關系如下圖所示:
流程與上一例子大體相似,但是不同的是④這條引用是通過weak_ptr建立的,并不會增加引用計數。也就是說,CA的對象只有一個引用計數,而CB的對象只有2個引用計數,當main函數返回時,對象ptr_a和ptr_b被銷毀,也就是①、③兩條引用會被斷開,此時CA對象的引用計數會減為0,對象被銷毀,其內部的m_ptr_b成員變量也會被析構,導致CB對象的引用計數會減為0,對象被銷毀,進而解決了引用成環的問題。
如果仔細看代碼的話,會覺得很神奇!定義m_ptr_a修改成std::weak_ptr類型,但是set_ptr函數定義的參數還是std::shared_ptr類型。這個時候為什么沒有報錯?weak_ptr和shared_ptr的聯系是什么呢?
weak_ptr的原理
weak_ptr是為了配合shared_ptr而引入的一種智能指針,它指向一個由shared_ptr管理的對象而不影響所指對象的生命周期,也就是,將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。不論是否有weak_ptr指向,一旦最后一個指向對象的shared_ptr被銷毀,對象就會被釋放。從這個角度看,weak_ptr更像是shared_ptr的一個助手而不是智能指針。初始化方式
通過shared_ptr直接初始化,也可以通過隱式轉換來構造;
允許移動構造,也允許拷貝構造。
需要注意,weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。
常用操作
w.user_count():返回weak_ptr的強引用計數;
w.reset(…):重置weak_ptr。
如何判斷weak_ptr指向對象是否存在?
既然weak_ptr并不改變其所共享的shared_ptr實例的引用計數,那就可能存在weak_ptr指向的對象被釋放掉這種情況。這時,就不能使用weak_ptr直接訪問對象。那么如何判斷weak_ptr指向對象是否存在呢?C++中提供了lock函數來實現該功能。如果對象存在,lock()函數返回一個指向共享對象的shared_ptr(引用計數會增1),否則返回一個空shared_ptr。weak_ptr還提供了expired()函數來判斷所指對象是否已經被銷毀。
由于weak_ptr并沒有重載operator ->和operator *操作符,因此不可直接通過weak_ptr使用對象,同時也沒有提供get函數直接獲取裸指針。典型的用法是調用其lock函數來獲得shared_ptr示例,進而訪問原始對象。
使用場景
共享對象的線程安全問題
例如:線程A和線程B訪問一個共享的對象,如果線程A正在析構這個對象的時候,線程B又要調用該共享對象的成員方法,此時可能線程A已經把對象析構完了,線程B再去訪問該對象,就會發生不可預期的錯誤。
在例子中,由于thread1等待2s,此時,main線程早已經把t對象析構了。打印m_id,自然不能打印出2了。可以通過shared_ptr和weak_ptr來解決共享對象的線程安全問題。
#include <iostream> #include <memory> #include <thread>class Test {public:Test(int id) : m_id(id) {}void showID() {std::cout << m_id << std::endl;}private:int m_id; };void thread2(std::weak_ptr<Test> t) {std::this_thread::sleep_for(std::chrono::seconds(2));std::shared_ptr<Test> sp = t.lock();if(sp)sp->showID(); ? ? ? ? ? ? ? ? ? ? ?// 打印結果:2 }int main() {std::shared_ptr<Test> sp = std::make_shared<Test>(2);std::thread t2(thread2, sp);t2.join();return 0; }如果想訪問對象的方法,先通過t的lock方法進行提升操作,把weak_ptr提升為shared_ptr強智能指針。提升過程中,是通過檢測它所觀察的強智能指針保存的Test對象的引用計數,來判定Test對象是否存活。ps如果為nullptr,說明Test對象已經析構,不能再訪問;如果ps!=nullptr,則可以正常訪問Test對象的方法。
如果設置t2為分離線程t2.detach(),讓main主線程結束,sp智能指針析構,進而把Test對象析構,此時showID方法已經不會被調用,因為在thread2方法中,t提升到sp時,lock方法判定Test對象已經析構,提升失敗!
觀察者模式
觀察者模式就是,當觀察者觀察到某事件發生時,需要通知監聽者進行事件處理的一種設計模式。
在多數實現中,觀察者通常都在另一個獨立的線程中,這就涉及到在多線程環境中,共享對象的線程安全問題(解決方法就是使用上文的智能指針)。這是因為在找到監聽者并讓它處理事件時,其實在多線程環境中,肯定不明確此時監聽者對象是否還存活,或是已經在其它線程中被析構了,此時再去通知這樣的監聽者,肯定是有問題的。
也就是說,當觀察者運行在獨立的線程中時,在通知監聽者處理該事件時,應該先判斷監聽者對象是否存活,如果監聽者對象已經析構,那么不用通知,并且需要從map表中刪除這樣的監聽者對象。其中的主要代碼為:
// 存儲監聽者注冊的感興趣的事件 unordered_map<int, list<weak_ptr<Listener>>> listenerMap;// 觀察者觀察到事件發生,轉發到對該事件感興趣的監聽者 void dispatchMessage(int msgid) {auto it = listenerMap.find(msgid);if (it != listenerMap.end()) {for (auto it1 = it->second.begin(); it1 != it->second.end(); ++it1) {shared_ptr<Listener> ps = it1->lock(); ? ? ? ? ? ?// 智能指針的提升操作,用來判斷監聽者對象是否存活if (ps != nullptr) { ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 監聽者對象如果存活,才通知處理事件ps->handleMessage(msgid);} else {it1 = it->second.erase(it1); ? ? ? ? ? ? ? ? ? ?// 監聽者對象已經析構,從map中刪除這樣的監聽者對象}}} }這個想法來源于:一個用C++寫的開源網絡庫,muduo庫,作者陳碩。大家可以在網上下載到muduo的源代碼,該源碼中對于智能指針的應用非常優秀,其中借助shared_ptr和weak_ptr解決了這樣一個問題,多線程訪問共享對象的線程安全問題。
解決循環引用
循環引用,簡單來說就是:兩個對象互相使用一個shared_ptr成員變量指向對方的會造成循環引用,導致引用計數失效。上文詳細講述了循環引用的錯誤原因和解決辦法。
監視this智能指針
在上文講述shared_ptr的博文中就有講述到:enable_shared_from_this中有一個弱指針weak_ptr,這個弱指針能夠監視this。在調用shared_from_this這個函數時,這個函數內部實際上是調用weak_ptr的lock方法。lock()會讓shared_ptr指針計數+1,同時返回這個shared_ptr。
?
總結
以上是生活随笔為你收集整理的智能指针weak_ptr的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 智能指针shared_ptr
- 下一篇: C++11 新的计时方法——std::c