1. 程式人生 > >[C++]智慧指標的原理與使用

[C++]智慧指標的原理與使用

1、智慧指標的原理及作用 

       C++程式中不僅包含靜態記憶體和棧記憶體,還有一個記憶體池,記憶體池中的記憶體被稱為自由空間或者堆。程式通常使用堆來儲存動態分配的物件(程式執行時分配的物件),當動態物件不再被使用時,程式碼必須顯式的將它們銷燬。動態記憶體的管理是通過運算子new和delete完成的。

new運算子:在動態記憶體中為物件分配一塊空間並返回一個指向該物件的指標。

int i;

int *p0 = &i;

int *p1 = new int; //指向一個動態分配的、未初始化的無名物件

int *p2 = new int(2); //*p2初始化值為2

int *p3 = new int[1000]; //申請1000個單位的記憶體空間

delete運算子:指向一個動態物件的指標,銷燬物件並釋放與其相關聯的記憶體。在delete之後,指標變成了懸空指標(指向一塊曾經儲存資料物件但現在已經無效的記憶體地址)。想要避免懸空指標,需要在delete之後將nullptr賦值給指標變數,這樣就清楚的指出指標不指向任何物件。

delete p0; //error, p0指標不是用new動態申請的

delete p1;
p1 = nullptr;

delete p2;
p2 = nullptr;

delete[] p3; //在用new申請時用了[],所以在delete時也要用[]

使用new和delete運算子進行動態記憶體的管理雖然可以提高程式的效率,但是也非常容易出問題。

      (1)忘記釋放記憶體,造成記憶體洩漏

      (2)在尚有指標引用記憶體的情況下就將其釋放,產生引用非法記憶體的指標

      (3)程式發生異常後進入catch忘記釋放記憶體以及多次釋放同一塊記憶體,造成記憶體洩漏

       為了讓動態記憶體的使用更加安全、簡便,C++引入了智慧指標的概念。什麼是智慧指標?智慧指標是借用RAII技術對普通指標進行封裝,其實質是一個物件,行為表現為一個指標,也就是智慧的管理動態記憶體的釋放。那RAII技術具體又是什麼呢?RAII技術(Resource Acquisition Is Initialization),也稱為“資源獲取就是初始化”,是C++語言的一種管理資源、避免洩漏的慣用法。使用類來封裝資源的分配和初始化,在建構函式中完成資源的分配和初始化,在解構函式中完成資源的清理,可以保證正確的初始化和資源釋放。     

2、智慧指標的分類和使用

      C++11版本之後提供的智慧指標包含在標頭檔案<memory>中,分別是auto_ptr、shared_ptr、unique_ptr、weak_ptr。其中auto_ptr已被棄用,所以這裡不再贅述。

2.1  shared_ptr

      shared_ptr是一種強引用指標,允許多個指標指向相同的物件,是一個標準的共享所有權的智慧指標。每個shared_ptr都會有一個計數器與之相關聯,通常稱其為引用計數。下面,我們簡單介紹下shared_ptr的用法:

(1)初始化

    可以使用建構函式初始化(指定型別,傳入指標即可),也可以使用make_shared函式初始化(最安全的方法,推薦使用)

std::shared_ptr<T> sp; //空shared_ptr,可以指向型別為T的物件

std::shared_ptr<int> sp(new int(5)); //指定型別,傳入指標通過建構函式初始化

std::shared_ptr<int> sp = std::make_shared<int>(5); //使用make_shared函式初始化

//智慧指標是一個模板類,不能將一個原始指標直接賦值給一個智慧指標,因為一個是類,一個是指標
std::shared_ptr<int> sp = new int(1); //error

//shared_ptr不能直接支援動態陣列,需要顯示指定刪除器

std::shared_ptr<int> sp(new int[10], [](int* p){delete[] p;}); //指定delete[]

std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>()); //指定std::default_delete

(2)拷貝和賦值

       當拷貝一個shared_ptr時,內部的引用計數加1;當給shared_ptr賦值或shared_ptr被銷燬時,內部的引用計數減1;當引用計數減為0時將會自動釋放自己所管理的物件。

std::shared_ptr<int> sp = std::make_shared<int>(5); //sp指向的int物件只有一個引用者,引用計數為1
std::shared_ptr<int> spCopy(sp); //spCopy是sp的拷貝,此操作會遞增sp指向物件的引用計數

std::shared_ptr<int> sp1 = std::make_shared<int>(6);
sp = sp1; //給sp賦值,使它指向另一個地址,此操作會遞增sp1指向物件的引用計數,遞減sp原來指向物件的引用計數

(3)自動釋放所管理的物件以及相關聯的記憶體

      shared_ptr是通過其解構函式來完成自動釋放的工作。shared_ptr的解構函式會遞減其所指向物件的引用計數,當引用計數變為0時,解構函式就會銷燬物件並釋放其所佔用的記憶體。注意:若將shared_ptr存放於一個容器中,而後只使用其中部分元素,不再需要全部元素時,千萬記得用erase刪除不在需要的那些元素。

(4)和普通指標的混合使用

2.2  unique_ptr

       顧名思義,unique_ptr唯一擁有其所指的物件,在同一時刻只能有一個unique_ptr指向給定物件,因此unique_ptr不支援普通的拷貝和賦值操作。下面,我們簡單介紹下unique_ptr的用法:

(1)初始化

       unique_ptr不像shared_ptr一樣擁有make_shared函式來建立一個shared_ptr例項。因此我們需要將一個new操作符返回的指標傳遞給unique_ptr的建構函式來完成初始化。unique_ptr提供了對動態陣列的支援,指定刪除器是一個可選項。

std::unique_ptr<T> up; //空unique_ptr,可以指向型別為T的物件,up會使用delete來釋放它的指標

std::unique_ptr<int> up(new int(5)); //繫結動態物件

std::unique_ptr<int[]> up(new int[5]); //可選擇是否指定刪除器

(2)拷貝和賦值

      unique_ptr沒有copy建構函式,不支援普通的拷貝和賦值操作;但卻提供了一種移動機制來將指標的所有權從一個unique_ptr轉移給另一個unique_ptr(使用std::move函式,也可以呼叫release或reset)。

std::move:獲取一個繫結到左值上的右值引用

u.release:u放棄對指標的控制權,返回指標,並將u置為空

u.reset:釋放u指向的物件

u.reset(q):如果提供了內建指標q,令u指向這個物件,否則將u置為空

std::unique_ptr<int> up(new int(5));

std::unique_ptr<int> upCopy(up); //error,不能拷貝

std::unique_ptr<int> upAssign = up; //error,不能賦值

std::unique_ptr<int> upMove = std::move(up); //轉移所有權

std::cout << *up << std::endl; //error,up經過std::move後為空

std::unique_ptr<int> up1(new int(5));

std::unique_ptr<int> up2(up1.release()); //up2被初始化為up1原來儲存的指標,且up1置為空

std::unique_ptr<int> up3(new int(6));

up2.reset(up3.release()); //reset釋放了up2原來指向的記憶體,指向up3原來儲存的指標,且將up3置為空

(3)使用場景

      unique_ptr適用範圍比較廣泛,它可返回函式內動態申請資源的所有權;可在容器中儲存指標;支援動態陣列的管理。

std::unique_ptr<int> CloneUniquePtr(int p)
{
    std::unique_ptr<int> up(new int(p));
    return up; //返回unique_ptr
}

int main()
{
    //返回函式內動態申請資源的所有權,函式結束後,自動釋放資源
    int p = 5;
    std::unique_ptr<int> up = CloneUniquePtr(p);
    std::cout << *up << std::endl;

    //在容器中儲存指標
    std::vector<std::unique_ptr<int>> vecIntPtr;
    std::unique_ptr<int> up1(new int(5));
    vecIntPtr.push_back(std::move(up1)); //注意要使用std::move

    //支援動態陣列
    std::unique_ptr<int[]> up2(new int[5] {1, 2, 3, 4, 5});
    up2[0] = 0; //過載了operation[]
}

2.3  weak_ptr

       weak_ptr是一種弱引用指標,它是伴隨shared_ptr而來的,不具有普通指標的行為。它主要是解決了shared_ptr引用計數的問題:在迴圈引用時會導致記憶體洩漏的問題。

(1)初始化

      weak_ptr指向一個由shared_ptr管理的物件,將一個weak_ptr繫結到一個shared_ptr不會改變shared_ptr的引用計數。

std::weak_ptr<T> wp; //空weak_ptr,可以指向型別為T的物件

std::weak_ptr<int> wp(new int(5)); //使用weak_ptr物件構造

std::shared_ptr<int> sp = std::make_shared<int>(6);
std::weak_ptr<int> wp(sp); // 使用shared_ptr物件構造

(2)成員函式

wp.use_count():與該wp共享的shared_ptr的數量

wp.expired():如果wp.use_count() == 0,返回true,否則返回false

lock():如果wp.expired()為true,返回一個空shared_ptr,否則返回一個指向wp物件的shared_ptr

成員函式的使用如下:

std::shared_ptr<int> sp = std::make_shared<int>(5);
std::weak_ptr<int> wp(sp);
std::cout << wp.use_count() << std::endl;

if(!wp.expired())
{
    std::shared_ptr<int> sp2 = wp.lock();
    std::cout << *sp2 << std::endl;
}