C++智能指針

C++智能指針

具體詳細細節見https://www.cnblogs.com/lanxuezaipiao/p/4132096.html

void remodel(std::string & str)
{
    std::string * ps = new std::string(str);
    ...
    if (weird_thing())
        throw exception();
    str = *ps; 
    delete ps;
    return;
}

當出現異常時(weird_thing()返回true),delete將不被執行,因此將導致內存泄露。

如何避免這種問題?有人會說,這還不簡單,直接在throw exception();之前加上delete ps;不就行了。是的,你本應如此,問題是很多人都會忘記在適當的地方加上delete語句(連上述代碼中最後的那句delete語句也會有很多人忘記吧),如果你要對一個龐大的工程進行review,看是否有這種潛在的內存泄露問題,那就是一場災難!——採取另外一種方法(RAII技術)

若函數終止(不管是正常終止,還是由於出現了異常而終止),本地變量都將自動從棧內存中刪除—因此指針ps佔據的內存將被釋放,如果ps指向的內存也被自動釋放,那該有多好啊。

我們知道析構函數有這個功能。如果ps有一個析構函數,該析構函數將在ps過期時自動釋放它指向的內存。但ps的問題在於,它只是一個常規指針,不是有析構凼數的類對象指針。如果它指向的是對象,則可以在對象過期時,讓它的析構函數刪除指向的內存。

這正是 auto_ptr、unique_ptr和shared_ptr這幾個智能指針背後的設計思想。我簡單的總結下就是:將基本類型指針封裝爲類對象指針(這個類肯定是個模板,以適應不同基本類型的需求),並在析構函數裏編寫delete語句刪除指針指向的內存空間(RAII技術)故上述修改後的代買爲:

# include <memory>
void remodel (std::string & str)
{
    std::auto_ptr<std::string> ps (new std::string(str));
    ...
    if (weird_thing ())
        throw exception(); 
    str = *ps; 
    // delete ps; NO LONGER NEEDED
    return;
}

STL提供的四種智能指針

四種:auto_ptr、unique_ptr、shared_ptr和weak_ptr

模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時,如果您的編譯器不支持其他兩種解決力案,auto_ptr將是唯一的選擇。

智能指針使用注意點

  • 所有的智能指針類都有一個explicit構造函數,以指針作爲參數。因此不能自動將指針轉換爲智能指針對象,必須顯式調用。
//auto_ptr的類模板原型爲:
templet<class T>
class auto_ptr {
  explicit auto_ptr(X* p = 0) ; 
  ...
};

//必須顯示調用
shared_ptr<double> pd; 
double *p_reg = new double;
pd = p_reg;                               // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)
  • 對全部三種智能指針都應避免的一點:指向的內存過期時,delete運算符用於非堆內存。
string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation);   // No

爲什麼要摒棄auto_ptr

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; 
vocaticn = ps;

上述賦值語句將完成什麼工作呢?如果ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,因爲程序將試圖刪除同一個對象兩次——一次是ps過期時,另一次是vocation過期時。要避免這種問題,方法有多種:

  • 定義陚值運算符,使之執行深複製。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本,缺點是浪費空間,所以智能指針都未採用此方案。
  • 建立所有權(ownership)概念。對於特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的構造函數會刪除該對象。然後讓賦值操作轉讓所有權。這就是用於auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。
  • 創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱爲引用計數。例如,賦值時,計數將加1,而指針過期時,計數將減1,。當減爲0時才調用delete。這是shared_ptr採用的策略。

當然,同樣的策略也適用於複製構造函數。

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main() {
  auto_ptr<string> films[5] =
 {
  auto_ptr<string> (new string("Fowl Balls")),
  auto_ptr<string> (new string("Duck Walks")),
  auto_ptr<string> (new string("Chicken Runs")),
  auto_ptr<string> (new string("Turkey Errors")),
  auto_ptr<string> (new string("Goose Eggs"))
 };
 auto_ptr<string> pwin;
 pwin = films[2]; // films[2] loses ownership. 將所有權從films[2]轉讓給pwin,此時films[2]不再引用該字符串從而變成空指針

 cout << "The nominees for best avian baseballl film are\n";
 for(int i = 0; i < 5; ++i)
  cout << *films[i] << endl;
 cout << "The winner is " << *pwin << endl;
 cin.get();

 return 0;
}

運行下發現程序崩潰了,原因在上面註釋已經說的很清楚,films[2]已經是空指針了,下面輸出訪問空指針當然會崩潰了。但這裏如果把auto_ptr換成shared_ptr或unique_ptr後,程序就不會崩潰,原因如下:

  • 使用shared_ptr時運行正常,因爲shared_ptr採用引用計數,pwin和films[2]都指向同一塊內存,在釋放空間時因爲事先要判斷引用計數值的大小因此不會出現多次刪除一個對象的錯誤。
  • 使用unique_ptr時編譯出錯,與auto_ptr一樣,unique_ptr也採用所有權模型,但在使用unique_ptr時,程序不會等到運行階段崩潰,而在編譯器因下述代碼行出現錯誤:
unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.

這就是爲何要摒棄auto_ptr的原因,一句話總結就是:避免潛在的內存崩潰問題。

unique_ptr爲何優於auto_ptr?

  • 前面的例子已經說明了unique_ptr爲何優於auto_ptr,也就是安全問題.
  • 但unique_ptr還有更聰明的地方:有時候,會將一個智能指針賦給另一個並不會留下危險的懸掛指針。
unique_ptr<string> demo(const char * s)
{
    unique_ptr<string> temp (new string (s)); 
    return temp;
}

unique_ptr<string> ps;
ps = demo('Uniquely special");

demo()返回一個臨時unique_ptr,然後ps接管了原本歸返回的unique_ptr所有的對象,而返回時臨時的 unique_ptr 被銷燬,也就是說沒有機會使用 unique_ptr 來訪問無效的數據,換句話來說,這種賦值是不會出現任何問題的,即沒有理由禁止這種賦值。實際上,編譯器確實允許這種賦值,這正是unique_ptr更聰明的地方。

總之,程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這麼做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這麼做。

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

當然,您可能確實想執行類似於#1的操作,僅當以非智能的方式使用摒棄的智能指針時(如解除引用時),這種賦值纔不安全。要安全的重用這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你能夠將一個unique_ptr賦給另一個。下面是一個使用前述demo()函數的例子,該函數返回一個unique_ptr<string>對象:使用move後,原來的指針仍轉讓所有權變成空指針,可以對其重新賦值。

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

如何選擇智能指針

選擇建議:

  • 如果程序要使用多個指向同一個對象的指針,應選擇shared_ptr。這樣的情況包括:

  1. 有一個指針數組,並使用一些輔助指針來標示特定的元素,如最大的元素和最小的元素;
  2. 兩個對象包含都指向第三個對象的指針;
  3. STL容器包含指針。很多STL算法都支持複製和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行爲不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。
  • 如果程序不需要多個指向同一個對象的指針,則可使用unique_ptr。如果函數使用new分配內存,並返還指向該內存的指針,將其返回類型聲明爲unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智能指針將負責調用delete。可將unique_ptr存儲到STL容器在那個,只要不調用將一個unique_ptr複製或賦給另一個算法(如sort())。例如,可在程序中使用類似於下面的代碼段。

weak_ptr

 weak_ptr 是一種不控制對象生命週期的智能指針, 它指向一個 shared_ptr 管理的對象. 進行該對象的內存管理的是那個強引用的 shared_ptr. weak_ptr只是提供了對管理對象的一個訪問手段. 

weak_ptr 設計的目的是爲配合 shared_ptr 而引入的一種智能指針來協助 shared_ptr 工作, 它只可以從一個 shared_ptr 或另一個 weak_ptr 對象構造, 它的構造和析構不會引起引用記數的增加或減少. 

std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp);
wp = sp;
printf("%d\n", wp.use_count()); // 1
wp.reset();
printf("%d\n", wp); // 0

// 檢查 weak_ptr 內部對象的合法性.
if (std::shared_ptr<int> sp = wp.lock())
{
}

成員函數

  • weak_ptr 沒有重載*和->但可以使用 lock 獲得一個可用的 shared_ptr 對象. 注意, weak_ptr 在使用前需要檢查合法性.
  • expired 用於檢測所管理的對象是否已經釋放, 如果已經釋放, 返回 true; 否則返回 false.
  • lock 用於獲取所管理的對象的強引用(shared_ptr). 如果 expired 爲 true, 返回一個空的 shared_ptr; 否則返回一個 shared_ptr, 其內部對象指向與 weak_ptr 相同.
  • use_count 返回與 shared_ptr 共享的對象的引用計數.
  • reset 將 weak_ptr 置空.
  • weak_ptr 支持拷貝或賦值, 但不會影響對應的 shared_ptr 內部對象的計數.

引入weak_ptr的原因

解決 shared_ptr 因循環引用不能釋放資源的問題——引用計數的缺陷。

使用 shared_ptr 時, shared_ptr 爲強引用, 如果存在循環引用, 將導致內存泄露. 而 weak_ptr 爲弱引用, 可以避免此問題, 其原理:

對於弱引用來說, 當引用的對象活着的時候弱引用不一定存在. 僅僅是當它存在的時候的一個引用, 弱引用並不修改該對象的引用計數, 這意味這弱引用它並不對對象的內存進行管理.

weak_ptr 在功能上類似於普通指針, 然而一個比較大的區別是, 弱引用能檢測到所管理的對象是否已經被釋放, 從而避免訪問非法內存。

注意: 雖然通過弱引用指針可以有效的解除循環引用, 但這種方式必須在程序員能預見會出現循環引用的情況下才能使用, 也可以是說這個僅僅是一種編譯期的解決方案, 如果程序在運行過程中出現了循環引用, 還是會造成內存泄漏.

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章