C++智能指針


C++智能指針

來源 https://zhuanlan.zhihu.com/p/30933682

參考 https://www.zhihu.com/question/319277442/answer/1094961099

========================

智能指針只能代替T*的一部分功能,而這部分本來就不適合用T*(因為容易造成bug)。

如果本身就沒有所有權,Bjarne(P1408)的建議是直接用T*

========================

智能指針表示的是某個資源的“所有權”的概念,

unique_ptr表示唯一的所有權;

shared_ptr表示可共享的所有權;

weak_ptr表示可共享的未來的所有權。

然而資源擁有者如果只是要把一個對象借給別人用一下,用完就歸還呢?

void borrow_sth(??? resource);

有兩種選擇: T*const std::unique_ptr<T>&

我更喜歡用第一種

========================

嗯,大家都在說普通指針和智能指針,少說了一個“引用”

先考慮用引用和值,減少90%的指針,特別是函數之間,基本上沒有必要傳指針。

再考慮使用unique_ptr,替換類的成員不能用引用的情況下使用指針。

最后考慮使用share_ptr/weak_ptr,解決生命周期復雜到一個類管不了的情況。

最后才是合理的指針使用場景。

========================

明確資源所歸屬的類,用unique_ptr的容器作為類成員變量+移動語義+RAII管理資源,其他地方裸指針訪問資源,這樣能把c++寫出java的感覺,也不用擔心pointer invalidation之類的問題。

shared_ptr用的很少,資源歸屬不明確的情況下先看看是否代碼結構設計有問題,實在不行再用shared_ptr。

========================

濫用智能指針會把你的代碼污染.

  • 確定持有者 :使用unique_ptr
  • 不需要持有對象 or 解決互相強引用 :使用waek_ptr
  • 需要共享時 :使用shared
  • 無主對象,與C交互,傳遞可能為空的對象,傳遞需要修改的對象:指針
  • 傳遞不能為nullptr的對象:引用,const 引用

綜上:

你代碼是這樣寫,沒問題,但是歷史代碼,或者C代碼,別人的代碼,可不會這么讓你用的 隨心所欲.

========================

1 兩種指針的天賦各不相同

a 智能指針天生負責對象生命期管理(這里假設智能指針作為類的非靜態成員變量,並借助類的構造函數和析構函數來完成動態對象的自動化管理):所以動態對象的創建和析構全都由unique_ptr和shared_ptr來做。

b 原始指針天生不負責對象生命周期管理:原始指針擅長調用動態對象,原因就是簡化接口。

2 創建者與使用者

c 所以你看MFC,BCG,QT,操作系統API,在結合業務數據尤其是動態對象的時候都是原始指針。這樣做的好處是它明確告訴你它不管理你的動態對象,只負責使用!只負責使用!只負責使用!並暗示你,用之前你要創建好,用完了它不負責清理。

d 上面c主要講了原始指針作為動態對象使用者的場景。這就再次暗示我們,在類內部我們使用智能指針來管理動態對象,表示動態對象的擁有者;在類外部我們提供原始指針接口供動態對象的使用者使用;

e 補充一下d,全局函數和類的靜態函數肯定可以看做是類的外部。因為我們之所以使用智能指針可以自動化管理就是利用了類的構造函數和析構函數,而全局函數和類的靜態函數顯然利用不了。

~~~~~~~~~~~更新5/11~~~~~~

f 補充一下d,有些業務場景,比如流水線處理的,生產者消費者模式下,用原始指針比較簡單明了,誰使用誰釋放。這時候沒必要再用智能指針了,因為很難做到自動化釋放。

下面是示例代碼:

class T3;
class T2;
class T1;
 
class A
{
    T3* m_t2;//創建和釋放都由A之外的代碼管理,A只負責(借用)使用;業務邏輯上保證A在使用m_t2指向的對象的時候,對象始終是存在的
    shared_ptr<T2> m_t2;//由加載程序創建m_t2指向的對象,執行時,交給A來管理,涉及動態對象的管理權的交接
    unique_ptr<T1> m_t1;//A管理該對象的生命周期,A的構造函數構造m_t1, A的析構函數釋放m_t1;
};

========================

推薦看 Effective Modern C++;大師的著作真乃實用之王。

我摘錄一下:

std::unique_ptr:

1 小巧、高速、具備只移型別的智能指針,對托管資源實施專屬所有權語義。

2 默認地,資源析構采用delete運算符來實現,但可以指定自定義刪除器。有狀態的刪除器和采用函數指針實現的刪除器會增加std::unique_ptr型別的對象尺寸

3 將std::unique_ptr 轉換成std::shared_ptr是容易實現的

std::shared_ptr:

1 提供方便的手段,實現了任意資源在共享所有權語義下進行生命周期管理的垃圾回收

2 與std::unique_ptr 相比,std::shared_ptr的尺寸通常是裸指針尺寸的兩倍,它還會帶來控制塊的開銷,並要求原子化的引用計數操作

3 默認的資源析構通過delete運算符進行,但同時也支持定制刪除器。刪除器的型別對std::shared_ptr的型別沒有影響

4 避免使用裸指針型別的變量來創建 std::shared_ptr 指針

std::weak_ptr:

1 使用std::weakptr 來代替可能空懸的std::shared_ptr

2 std::weak_ptr 可能的用武之地包括緩存,觀察者列表,以及避免std::shared_ptr 指針環路。

看看大師的總結,完美!!

具體到完全代替裸指針,恐怕也要再掂量掂量。

下面寫一些劣勢

std::shared_ptr 會增加內存開銷,復制的時候cpu消耗提高【原子count操作】

std::unique_ptr 內存占用小,幾乎可以媲美裸指針;但是它畢竟是一個類,使用的時候,不能復制,導致你一個作用域內只能有一個可用的實例【類似rust的所有權吧,你用起來有點束手束腳】;

std::weak_ptr 必須跟std::shared_ptr配合使用。

優勢:

省去你自己判斷啥時候該釋放資源【異步回調時候智能指針可以完美避免手動控制生命周期;enable_shared_frome_this 已經可以算是一種特別的編程技巧了

媲美裸指針的操作習慣

解放了雙手,C++跟腳本的代碼越來越像了。

補充一下【之前審題不太好】

1.對於性能和內存使用有嚴格要求的場景,不要過於依賴智能指針。【比如嵌入式這些的,實際上C+class就夠了】

2.對於類型不敏感的數據【也就是內存了】,可以考慮使用std::array或者std::vector等。因為這個時候,你實際上就是C的操作,類型對於該內存僅僅起到一個布局描述的作用,頻繁的類型轉換【非繼承關系】、字段偏移等操作,用智能指針也沒有什么好處【因為你還是會拿到裸指針去操作】

3.其他的對類型敏感,或者對作用域敏感的數據內存,可以都考慮使用智能指針。局部作用域使用uniqe_ptr , 多作用域的使用shared_ptr,緩存可能失效的情況下使用weak_ptr。

我做一般應用的時候,除了容器,幾乎一上來全部使用uniqe_ptr,當需要拋出一個副本的時候,使用shared_ptr。當功能完成的時候,哪個內存是全局生命周期,改成裸指針【全局裸指針我都不判空】。如果該項目不是那么重要,甚至我都會全部用shared_ptr,不用關心性能問題,因為C++本身的編譯性能已經很高了,隨便寫寫性能就夠了,只要不飛,內存泄漏不是問題。

當我要去判斷某一個內存之后的操作會失效,但是不知道什么時候失效的時候,我使用weak_ptr和shared_ptr。通過weak_ptr接口可以線程安全的獲取我之前的智能指針是否還生效。【這個時候,裸指針,幾乎是沒有辦法的了,很容易出現野指針】

========================

 

早在1994年,Gregory Colvin就向C++標准委員會提出了智能指針的提案(Smart Pointers - 1.54.0)。但早期的設計並不好用。各個庫都有自己的一套智能指針,沒有標准化。經過20多年的發展,特別是C++11標准引入shared_ptr和unique_ptr之后,智能指針技術趨於成熟。然而在實踐中,大多數項目還在使用自己山寨的引用計數解決方案。智能指針還沒有成為C++程序員的常備技能。

像其他技術一樣,智能指針有一定學習成本,如果被誤用,同樣也會帶來各種bug。本文簡要回顧C++11智能指針shared_ptr/unique_ptr/weak_ptr的核心概念,並試圖總結其正確使用方法。

Ownership Logic

正確使用智能指針的前提是搞清楚業務邏輯需要。其中最重要的是設計資源管理,即ownership,並據此選擇是否使用智能指針,使用哪種智能指針。智能指針有其內在的ownership logic。

所謂own某個指針,意味着有責任在合適的時候釋放該指針。獲得、引用和使用某個指針,並不一定需要負責釋放該指針所指向的資源。

shared_ptr是shared ownership,owner發起釋放操作,只是減引用計數,只有所有owner都釋放,所指向的對象才真正釋放。

weak_ptr不控制對象的生命周期,但是它觀察着shared_ptr管理的對象,有辦法知道對象是否還活着。

unique_ptr則是unique ownership,對象的管理權可以轉移,但是同一時刻只有一個owner,否則編譯就會報錯。

shared_ptr, weak_ptr

shared_ptr在底層使用了兩個技術,一個是引用計數,另一個是引入了一個中間層(Be Smart About C++11 Smart Pointers)。

為了管理目標對象,所創建的中間層被稱為manager object。其中除了目標對象的裸指針,還有兩個引用計數。一個用於shared_ptr,一個用於weak_ptr。當shared count減到0的時候,managed object就會被銷毀。只有shared count和weak count都減到0的時候,manager object才會被銷毀。

enable_shared_from_this

如果涉及到將this指針提升為shared_ptr的情況,直接提升會新建一個manager object。

void f(shared_ptr<Thing>); class Thing { public: void foo() { //f(shared_ptr<Thing>(this)); //new manager object A  f(shared_from_this()); //use manager object B  } }; int main() { shared_ptr<Thing> sp(new Thing()); //new manager object B  sp->foo(); } 

使用兩個manager object管理同一個對象會造成不可預知的后果。為避免這種情況,需要在對象中維護一個weak_ptr。這是通過enable_shared_from_this自動完成的。

當需要在object內部使用this指針時,調用shared_from_this()就可以避免新建manager object。需要注意的是,在構造函數中,對象還未構造完畢,並沒有交由shared_ptr管理,即manager object還未創建,所以不能使用shared_from_this。

unique_ptr

unique_ptr是對裸指針的簡單封裝,不需要額外的manager object。和shared_ptr基本用法一致,只是unique_ptr沒有引用計數,內部指針要么有效,要么沒有。

unique_ptr可以用(unique_ptr rvalue)初始化,但不允許copy construction或copy assignment。這符合其unique ownership語義。所以如果函數參數中有unique_ptr,應該傳引用或指針,不能傳值。

unique_ptr<Thing> create() { unique_ptr<Thing> ptr(new Thing); return ptr; //rvalue } unique_ptr<Thing> ptr2(create()); //unique_ptr<Thing> ptr3(ptr2); //copy construction //unique_ptr<Thing> ptr3 = ptr2; //copy assignment 

Race Condition

c++20將有atomic_shared_ptr/atomic_weak_ptr,這是不是意味着shared_ptr/weak_ptr並不是線程安全的呢?

一般地講,manager object中的引用計數增減是原子操作,所以是線程安全的。同時讀shared_ptr也是安全的。但是如果有線程在讀寫同一個shared_ptr,就不是安全的(shared_ptr - 1.57.0),這和操作一般指針是一致的。如果兩個線程在操作兩個shared_ptr,即使他們指向同一個manager object,只要沒有訪問所管理的對象,就是安全的(Lesson #4: Smart Pointers)。

shared_ptr<int> p(new int(42)); // thread A shared_ptr<int> p2(p); // reads p p2.reset(new int(1912)); //writes p2  // thread B shared_ptr<int> p3(p); // OK, multiple reads are safe p3.reset(); // OK, writes p3 

需要指出的是,從weak_ptr.lock()提升為shared_ptr是線程安全的。unique_ptr的所有權轉讓也是安全的。但是使用unique_ptr操作對象是不安全的(Atomic Smart Pointers)。

智能指針正確用法

使用shared_ptr/weak_ptr/unique_ptr必須要注意:

1)必須保證所有managed object只有一個manager object

當object第一次創建的時候,就要立刻交由一個shared_ptr管理,其他的shared_ptr和weak_ptr都必須從第一個shared_ptr拷貝或賦值得到。具體來說,就是調用shared_ptr(raw ptr)和make_shared函數。

class Thing; shared_ptr<Thing> p1(new Thing); shared_ptr<Thing> p2(make_shared<Thing>()); 

其中使用make_shared一次性分配managed object和manager object兩塊內存,效率更高。而且對於強迫症來說,看到new看不到delete總是感覺挺難受的,還不如連new都不要看到。

2)能用裸指針解決問題的情況下,就不要使用智能指針。
如果決定了用智能指針,那就不要用裸指針管理同一個對象。

雖然通過智能指針get()函數可以得到裸指針,但是盡量不要用。對象一經創建,就應該納入shared_ptr/unique_ptr的管理之下。為了保證沒有裸指針暴露出來,應該只用shared_ptr(raw ptr)/unique_ptr(raw_ptr)和make_shared/make_unique函數創建對象。

3)ownership logic是正確使用智能指針的基礎,不要濫用shared_ptr

weak_ptr是為了解決循環引用而引入的。當系統中出現了循環引用,且都是使用shared_ptr管理對象,那么一定是shared_ptr被濫用了。

weak_ptr觀察着shared_ptr管理的對象,必須從shared_ptr或weak_ptr創建。其唯一正確的使用方法是先用lock()調用提升為shared_ptr,然后使用shared_ptr。如果直接用shared_ptr(weak_ptr)來構造,可能會在weak_ptr已經expire的情況下拋出異常。

shared_ptr可以用==,!=,<來比較,實際比較的是他們管理的裸指針。

shared_ptr有拷貝開銷,作為參數時,應該盡量用const reference。

4) 能用unique_ptr管理的對象,不要使用shared_ptr/weak_ptr

這其實是說盡量在單一的固定地方管理資源,如果不能保證ownership固定,可以轉移所有權,盡量保證只有一個owner(An overview on smart pointersGoogle C++ Style GuideTop 10 dumb mistakes to avoid with C++ 11 smart pointers - A CODER'S JOURNEY)。

最佳實踐實例

1)main thread擁有對象,加載子線程只管加載。

//main thread class Thing; shared_ptr<Thing> p1(make_shared<Thing>()); if (p1->getState() == eLoadFinish) ... //use after loading finish  //loading thread weak_ptr<Thing> p2(p1); shared_ptr<Thing> p3 = p2.lock(); if (p3 && p3->getState() != eLoadFinish) { ... //loading  p3->setState(eLoadFinish); //set loading finish flag } 

此處不宜使用unique_ptr。雖然在子線程的加載過程中可以上鎖,但是對象中途若被主線程釋放,將會宕機。

2)擁有者和使用者都在main thread,使用者需要定期對對象做操作。

class Thing; void funcUsePtr(const shared_ptr<Thing> &p){ p->xxx(); //method call } void funcPassToUser(shared_ptr<Thing> &p){ pUser = p; } //owner shared_ptr<Thing> p1(make_shared<Thing>()); funcUsePtr(p1); //normal use funcPassToUser(p1); //pass to pUser  //user shared_ptr<Thing> p2 = pUser.lock(); if (p2) p2->yyy(); //method call 

此處雖然只有一個線程,但也不宜用unique_ptr。試想owner如果中途要釋放對象,user是不知道的。此時用weak_ptr維持一個弱引用,當需要的時候檢查一下有效性,是比較合理的。

 

============== End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM