[C++11新特性] 智能指針詳解


[C++11新特性] 智能指針詳解

 

 


C++ 程序設計中使用堆內存是非常頻繁的操作,堆內存的申請和釋放都由程序員自己管理。但使用普通指針,容易造成內存泄露(忘記釋放)、二次釋放、程序發生異常時內存泄露等問題等。所有 C++11 就引入了智能指針。

一、原始指針容易發生內存泄漏

C 語言中最常使用的是malloc()函數分配內存,free()函數釋放內存,而 C++ 中對應的是newdelete關鍵字。malloc()只是分配了內存,而new則更進一步,不僅分配了內存,還調用了構造函數進行初始化。使用示例:

int main() { // malloc返回值是 void* int* argC = (int*)malloc(sizeof(int)); free(argC); char *age = new int(25); // 做了兩件事情 1.分配內存 2.初始化 delete age; } 

newdelete必須成對出現,有時候是不小心忘記了delete,有時候則是很難判斷在這個地方自己是不是該delete,這個和資源的生命周期有關,這個資源是屬於我這個類管理的還是由另外一個類管理的(其它類可能要使用),如果是別人管理的就由別人delete

如果需要自己管理內存的話,最好顯示的將自己的資源傳遞進去,這樣的話,就能知道是該資源確實應該由自己來管理。

char *getName(char* v, size_t bufferSize) { //do something return v; } 

上面還是小問題,自己小心一點,再仔細看看文檔,還是有機會避免這些情況的。但是在 C++ 引入異常的概念之后,程序的控制流就發生了根本性的改變,在寫了 delete 的時候還是有可能發生內存泄漏。如下例:

void badThing(){ throw 1;// 拋出一個異常 } void test() { char* a = new char[1000]; badThing(); // do something delete[] a; } int main() { try { test(); } catch (int i){ cout << "error happened " << i << endl; } } 

上面的newdelete是成對出現的,但是程序在中間的時候拋出了異常,由於沒有立即捕獲,程序從這里退出了,並沒有執行到delete,內存泄漏還是發生了。

二、使用構造函數和析構函數解決內存泄漏

C++ 中的構造函數和析構函數十分強大,可以使用構造和析構解決上面的內存泄漏問題,比如:

class SafeIntPointer { public: explicit SafeIntPointer(int v) : m_value(new int(v)) { } ~SafeIntPointer() { delete m_value; cout << "~SafeIntPointer" << endl; } int get() { return *m_value; } private: int* m_value; }; void badThing(){ throw 1;// 拋出一個異常 } void test() { SafeIntPointer a(5); badThing(); } int main() { try { test(); } catch (int i){ cout << "error happened " << i << endl; } } // 結果 // ~SafeIntPointer // error happened 1 

可以看到,就算發生了異常,也能夠保證析構函數成功執行!這里的例子是這個資源只有一個人使用,我不用了就將它釋放掉。但還有種情況,一份資源被很多人共同使用,要等到所有人都不再使用的時候才能釋放掉,對於這種問題,就需要對上面的SafeIntPointer增加一個引用計數,如下:

class SafeIntPointer { public: explicit SafeIntPointer(int v) : m_value(new int(v)), m_used(new int(1)) { } ~SafeIntPointer() { cout << "~SafeIntPointer" << endl; (*m_used) --; // 引用計數減1 if(*m_used <= 0){ delete m_used; delete m_value; cout << "real delete resources" << endl; } } SafeIntPointer(const SafeIntPointer& other) { m_used = other.m_used; m_value = other.m_value; (*m_used)++; // 引用計數加1 } SafeIntPointer& operator= (const SafeIntPointer& other) { if (this == &other) // 避免自我賦值!! return *this; m_used = other.m_used; m_value = other.m_value; (*m_used)++; // 引用計數加1 return *this; } int get() { return *m_value; } int getRefCount() { return *m_used; } private: int* m_used; // 引用計數 int* m_value; }; int main() { SafeIntPointer a(5); cout << "ref count = " << a.getRefCount() << endl; SafeIntPointer b = a; cout << "ref count = " << a.getRefCount() << endl; SafeIntPointer c = b; cout << "ref count = " << a.getRefCount() << endl; } /* ref count = 1 ref count = 2 ref count = 3 ~SafeIntPointer ~SafeIntPointer ~SafeIntPointer real delete resources */ 

可以看到每一次賦值,引用計數都加一,最后每次析構一次后引用計數減一,知道引用計數為 0,才真正釋放資源。要寫出一個正確的管理資源的包裝類還是蠻難的,比如上面那個例子就不是線程安全的,只能屬於一個玩具,在實際工程中簡直沒法用。

所以 C++11 中引入了智能指針(Smart Pointer),它利用了一種叫做 RAII(資源獲取即初始化)的技術將普通的指針封裝為一個棧對象。當棧對象的生存周期結束后,會在析構函數中釋放掉申請的內存,從而防止內存泄漏。這使得智能指針實質是一個對象,行為表現的卻像一個指針。

智能指針主要分為shared_ptrunique_ptrweak_ptr三種,使用時需要引用頭文件<memory>。C++98 中還有auto_ptr,基本被淘汰了,不推薦使用。而 C++11 中shared_ptrweak_ptr都是參考boost庫實現的。

三、shared_ptr共享的智能指針

3.1 shared_ptr的初始化

最安全的分配和使用動態內存的方法是調用一個名為 make_shared 的標准庫函數。 此函數在動態內存中分配一個對象並初始化它,返回指向此對象的 shared_ptr。與智能指針一樣,make_shared 也定義在頭文件 memory 中。

// 指向一個值為42的int的shared_ptr shared_ptr<int> p3 = make_shared<int>(42); // p4 指向一個值為"9999999999"的string shared_ptr<string> p4 = make_shared<string>(10,'9'); // p5指向一個值初始化的int shared_ptr<int> p5 = make_shared<int>(); 

我們還可以用 new 返回的指針來初始化智能指針,不過接受指針參數的智能指針構造函數是 explicit 的。因此,我們不能將一個內置指針隱式轉換為一個智能指針,必須使用直接初始化形式來初始化一個智能指針:

shared_ptr<int> pi = new int (1024); // 錯誤:必須使用直接初始化形式 shared_ptr<int> p2(new int(1024)); // 正確:使用了直接初始化形式 

出於相同的原因,一個返回 shared_ptr 的函數不能在其返回語句中隱式轉換一個普通指針:

shared_ptr<int> clone(int p) { return new int(p); // 錯誤:隱式轉換為 shared_ptr<int> } 

3.2 shared_ptr的基本使用

std::shared_ptr的基本使用很簡單,看幾個例子就明白了:

#include <memory> #include <iostream> class Test { public: Test() { std::cout << "Test()" << std::endl; } ~Test() { std::cout << "~Test()" << std::endl; } }; int main() { std::shared_ptr<Test> p1 = std::make_shared<Test>(); std::cout << "1 ref:" << p1.use_count() << std::endl; { std::shared_ptr<Test> p2 = p1; std::cout << "2 ref:" << p1.use_count() << std::endl; } std::cout << "3 ref:" << p1.use_count() << std::endl; return 0; } 

輸出如下:

Test()
1 ref:1 2 ref:2 3 ref:1 ~Test() 

針對代碼解讀如下:

  • std::make_shared里面調用了 new 操作符分配內存;
  • 第二個p1.use_count()之所以顯示為 2,是因為增加了引用對象 p2,而隨着大括號的結束,p2 的作用域結束,所以 p1 的引用計數變回 1,而隨着 main 函數的結束,p1 的作用域結束,此時檢測到計數為 1,那就會在銷毀 p1 的同時,調用 p1 的析構函數 delete 掉之前分配的內存空間;

3.3 shared_ptr常用操作

下面列出了shared_ptr獨有的操作:

make_shared<T>(args) // 返回一個shared_ptr,指向一個動態分配的類型為T的對象。使用args初始化此對象 shared_ptr<T> p(q) // p是shared_ptr q的拷貝;此操作會遞增q中的引用計數。q中的指針必須能轉換成T* p = q // p和q都是shared_ptr,所保存的指針必須能相互轉換。此操作會遞減p中的引用計數,遞增q中的引用計數。若p中的引用計數變為0,則將其管理的原內存釋放 p.unique() // 若p.use_count()為1,返回true;否則返回false p.use_count() // 返回與p共享對象的智能指針數量;可能很慢,主要用於調試 

下面介紹一些改變shared_ptr的其他方法:

p.reset () //若p是唯一指向其對象的shared_ptr,reset會釋放此對象。 p.reset(q) //若傳遞了可選的參數內置指針q,會令P指向q,否則會將P置為空。 p.reset(q, d) //若還傳遞了參數d,將會調用d而不是delete 來釋放q 

四、weak_ptr弱引用的智能指針

4.1 shared_ptr相互引用會有什么后果?

shared_ptr的一個最大的陷阱是循環引用,循環引用會導致堆內存無法正確釋放,導致內存泄漏。看下面的例子:

#include <iostream> #include <memory> class Parent; // Parent類的前置聲明 class Child { public: Child() { std::cout << "hello child" << std::endl; } ~Child() { std::cout << "bye child" << std::endl; } std::shared_ptr<Parent> father; }; class Parent { public: Parent() { std::cout << "hello Parent" << std::endl; } ~Parent() { std::cout << "bye parent" << std::endl; } std::shared_ptr<Child> son; }; void testParentAndChild() { } int main() { std::shared_ptr<Parent> parent(new Parent()); // 1 資源A std::shared_ptr<Child> child(new Child()); // 2 資源B parent->son = child; // 3 child.use_count() == 2 and parent.use_count() == 1 child->father = parent; // 4 child.use_count() == 2 and parent.use_count() == 2 return 0; } /* 輸出: hello Parent hello child */ 

很驚訝的發現,用了shared_ptr管理資源,沒有調用 Parent 和 Child 的析構函數,表示資源最后還是沒有釋放!內存泄漏還是發生了。

分析:

  • 執行編號1的語句時,構造了一個共享智能指針p,稱呼它管理的資源叫做資源Anew Parent()產生的對象)吧, 語句2構造了一個共享智能指針c,管理資源B(new Child()產生的對象),此時資源AB的引用計數都是1,因為只有1個智能指針管理它們,執行到了語句3的時候,是一個智能指針的賦值操作,資源B的引用計數變為了2,同理,執行完語句4,資源A的引用計數也變成了2
  • 出了函數作用域時,由於析構和構造的順序是相反的,會先析構共享智能指針c,資源B的引用計數就變成了1;接下來繼續析構共享智能指針p,資源A的引用計數也變成了1。由於資源AB的引用計數都不為1,說明還有共享智能指針在使用着它們,所以不會調用資源的析構函數!
  • 這種情況就是個死循環,如果資源A的引用計數想變成0,則必須資源B先析構掉(從而析構掉內部管理資源A的共享智能指針),資源B的引用計數想變為0,又得依賴資源A的析構,這樣就陷入了一個死循環。

4.2 weak_ptr如何解決相互引用的問題

要想解決上面循環引用的問題,只能引入新的智能指針std::weak_ptrstd::weak_ptr有什么特點呢?與std::shared_ptr最大的差別是在賦值的時候,不會引起智能指針計數增加。

  • weak_ptr被設計為與shared_ptr共同工作,可以從一個shared_ptr或者另一個weak_ptr對象構造,獲得資源的觀測權。但weak_ptr沒有共享資源,它的構造不會引起指針引用計數的增加。
  • 同樣,在weak_ptr析構時也不會導致引用計數的減少,它只是一個靜靜地觀察者。weak_ptr沒有重載operator*->,這是特意的,因為它不共享指針,不能操作資源,這是它弱的原因。
  • 如要操作資源,則必須使用一個非常重要的成員函數lock()從被觀測的shared_ptr獲得一個可用的shared_ptr對象,從而操作資源。

當我們創建一個weak_ptr時,要用一個shared_ptr來初始化它:

auto p = make_shared<int>(42); weak_ptr<int> wp(p); // wp弱共享p; p的引用計數未改變 

我們在上面的代碼基礎上使用std::weak_ptr進行修改,如下:

#include <iostream> #include <memory> class Parent; // Parent類的前置聲明 class Child { public: Child() { std::cout << "hello child" << std::endl; } ~Child() { std::cout << "bye child" << std::endl; } // 測試函數 void testWork() { std::cout << "testWork()" << std::endl; } std::weak_ptr<Parent> father; }; class Parent { public: Parent() { std::cout << "hello Parent" << std::endl; } ~Parent() { std::cout << "bye parent" << std::endl; } std::weak_ptr<Child> son; }; void testParentAndChild() { } int main() { std::shared_ptr<Parent> parent(new Parent()); std::shared_ptr<Child> child(new Child()); parent->son = child; child->father = parent; std::cout << "parent_ref:" << parent.use_count() << std::endl; std::cout << "child_ref:" << child.use_count() << std::endl; // 把std::weak_ptr類型轉換成std::shared_ptr類型,以調用內部成員函數 std::shared_ptr<Child> tmp = parent.get()->son.lock(); tmp->testWork(); std::cout << "tmp_ref:" << tmp.use_count() << std::endl; return 0; } /* 輸出: hello Parent hello child parent_ref:1 child_ref:1 testWork() tmp_ref:2 bye child bye parent */ 

由以上代碼運行結果我們可以看到:

  • 所有的對象最后都能正常釋放,不會存在上一個例子中的內存沒有釋放的問題;
  • parent 和 child 在 main 函數中退出前,引用計數均為 1,也就是說,對std::weak_ptr的相互引用,不會導致計數的增加。

4.3 weak_ptr常用操作

weak_ptr<T> w;	// 空weak_ptr可以指向類型為T的對象 weak_ptr<T> w(shared_ptr p); // 與p指向相同對象的weak_ptr, T必須能轉換為sp指向的類型 w = p; // p可以是shared_ptr或者weak_ptr,賦值后w和p共享對象 w.reset(); // weak_ptr置為空 w.use_count(); // 與w共享對象的shared_ptr的計數 w.expired(); // w.use_count()為0則返回true,否則返回false w.lock(); // w.expired()為true,返回空的shared_ptr;否則返回指向w的shared_ptr 

五、unique_ptr獨占的智能指針

5.1 unique_ptr的基本使用

unique_ptr相對於其他兩個智能指針更加簡單,它和shared_ptr使用差不多,但是功能更為單一,它是一個獨占型的智能指針,不允許其他的智能指針共享其內部的指針,更像原生的指針(但更為安全,能夠自己釋放內存)。不允許賦值和拷貝操作,只能夠移動

std::unique_ptr<int> ptr1(new int(0)); std::unique_ptr<int> ptr2 = ptr1; // 錯誤,不能復制 std::unique_ptr<int> ptr3 = std::move(ptr1); // 可以移動 

在 C++11 中,沒有類似std::make_shared的初始化方法,但是在 C++14 中,對於std::unique_ptr引入了std::make_unique方法進行初始化。

#include <iostream> #include <memory> int main() { std::unique_ptr<std::string> ptr1(new std::string("unique_ptr")); std::cout << "ptr1 is " << *ptr1 << std::endl; std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("make_unique init!"); std::cout << "ptr2 is " << *ptr2 << std::endl; return 0; } /* 輸出: ptr1 is unique_ptr ptr2 is make_unique init! */ 

5.2 unique_ptr常用操作

下面列出了unique_ptr特有的操作。

unique_ptr<T> u1 // 空unique_ptr,可以指向類型為T的對象。u1會使用delete來釋放它的指針 unique_ptr<T, D> u2 // u2會使用一個類型為D的可調用對象來釋放它的指針 unique_ptr<T, D> u(d) // 空unique_ptr,指向類型為T的對象,用類型為D的對象d替代delete u = nullptr // 釋放u指向的對象,將u置為空 u.release() // u放棄對指針的控制權,返回指針,並將u置為空 u.reset() // 釋放u指向的對象 u.reset(q) // 如果提供了內置指針q,另u指向這個對象;否則將u置為空 u.reset(nullptr) 

雖然我們不能拷貝或賦值unique_ptr,但可以通過調用 release 或 reset 將指針的所有權從一個(非const)unique_ptr轉移給另一個unique_ptr

unique_ptr<string> p1(new string("Stegosaurus")); // 將所有權從pl (指向string Stegosaurus)轉移給p2 unique_ptr<string> p2(p1, release()); // release 將 p1 置為空 unique_ptr<string> p3(new string("Trex")); // 將所有權從p3轉移給p2 p2.reset(p3.release()); // reset 釋放了 p2 原來指向的內存 

調用 release 會切斷unique_ptr和它原來管理的對象間的聯系,如果我們不用另一個智能指針來保存 release 返回的指針,我們的程序就要負責資源的釋放:

p2.release(); // 錯誤:p2不會釋放內存,而且我們丟失了指針 auto p = p2.release(); // 正確,但我們必須記得 delete(p) delete(p); 

5.3 傳遞unique_ptr參數和返回unique_ptr

不能拷貝 unique_ptr 的規則有一個例外:我們可以拷貝或賦值一個將要被銷毀的 unique_ptr。最常見的例子是從函數返回一個unique_ptr

unique_ptr<int> clone (int p) { unique_ptr<int> ret(new int (p)); // ... return ret; } 

對於上面這段代碼,編譯器都知道要返回的對象將要被銷毀。在此情況下,編譯器執行一種特殊的“拷貝”,在《C++ Primer》13.6.2節(第473頁)中有介紹。

六、性能與安全的權衡

使用智能指針雖然能夠解決內存泄漏問題,但是也付出了一定的代價。以shared_ptr舉例:

  • shared_ptr的大小是原始指針的兩倍,因為它的內部有一個原始指針指向資源,同時有個指針指向引用計數。
  • 引用計數的內存必須動態分配。雖然一點可以使用make_shared()來避免,但也存在一些情況下不能夠使用make_shared()
  • 增加和減小引用計數必須是原子操作,因為可能會有讀寫操作在不同的線程中同時發生。比如在一個線程里有一個指向一塊資源的shared_ptr可能調用了析構(因此所指向的資源的引用計數減一),同時,在另一線程里,指向相同對象的一個shared_ptr可能執行了拷貝操作(因此,引用計數加一)。原子操作一般會比非原子操作慢。但是為了線程安全,又不得不這么做,這就給單線程使用環境帶來了不必要的困擾。

我覺得還是分場合吧,看應用場景來進行權衡,我也沒啥經驗,但我感覺安全更重要,現在硬件已經足夠快了,其他例如java這種支持垃圾回收的語言不還是用的很好嗎。


參考:

《C++ Primer 第5版》

c++11&14-智能指針專題

c++11]智能指針學習筆記

轉載自:https://www.cnblogs.com/linuxAndMcu/p/10409723.html


免責聲明!

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



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