C++ 智能指針 unique_ptr 介紹與示例

unique_ptrC++ 11 提供的用於防止內存泄漏的智能指針中的一種實現,獨享被管理對象指針所有權的智能指針。unique_ptr對象包裝一個原始指針,並負責其生命週期。當該對象被銷燬時,會在其析構函數中刪除關聯的原始指針。
unique_ptr具有->*運算符重載符,因此它可以像普通指針一樣使用。
查看下面的示例:

#include <iostream>
#include <memory>

struct Task {
    int mId;
    Task(int id ) :mId(id) {
        std::cout << "Task::Constructor" << std::endl;
    }
    ~Task() {
        std::cout << "Task::Destructor" << std::endl;
    }
};

int main()
{
    // 通過原始指針創建 unique_ptr 實例
    std::unique_ptr<Task> taskPtr(new Task(23));

    //通過 unique_ptr 訪問其成員
    int id = taskPtr->mId;
    std::cout << id << std::endl;

    return 0;
}

輸出:

Task::Constructor
23
Task::Destructor

unique_ptr <Task> 對象 taskPtr 接受原始指針作爲參數。現在當main函數退出時,該對象超出作用範圍就會調用其析構函數,在unique_ptr對象taskPtr 的析構函數中,會刪除關聯的原始指針,這樣就不用專門delete Task對象了。
這樣不管函數正常退出還是異常退出(由於某些異常),也會始終調用taskPtr的析構函數。因此,原始指針將始終被刪除並防止內存泄漏。

unique_ptr 獨享所有權

unique_ptr對象始終是關聯的原始指針的唯一所有者。我們無法複製unique_ptr對象,它只能移動。
由於每個unique_ptr對象都是原始指針的唯一所有者,因此在其析構函數中它直接刪除關聯的指針,不需要任何參考計數。

創建一個空的 unique_ptr 對象

創建一個空的unique_ptr<int>對象,因爲沒有與之關聯的原始指針,所以它是空的。

std::unique_ptr<int> ptr1;

檢查 unique_ptr 對象是否爲空

有兩種方法可以檢查 unique_ptr 對象是否爲空或者是否有與之關聯的原始指針。

// 方法1
if(!ptr1)
	std::cout<<"ptr1 is empty"<<std::endl;
// 方法2
if(ptr1 == nullptr)
	std::cout<<"ptr1 is empty"<<std::endl;

使用原始指針創建 unique_ptr 對象

要創建非空的 unique_ptr 對象,需要在創建對象時在其構造函數中傳遞原始指針,即:

std::unique_ptr<Task> taskPtr(new Task(22));

不能通過賦值的方法創建對象,下面的這句是錯誤的

// std::unique_ptr<Task> taskPtr2 = new Task(); // 編譯錯誤

使用 std::make_unique 創建 unique_ptr 對象 / C++14

std::make_unique<>() 是C++ 14 引入的新函數

std::unique_ptr<Task> taskPtr = std::make_unique<Task>(34);

獲取被管理對象的指針

使用get()·函數獲取管理對象的指針。

Task *p1 = taskPtr.get();

重置 unique_ptr 對象

在 unique_ptr 對象上調用reset()函數將重置它,即它將釋放delete關聯的原始指針並使unique_ptr 對象爲空。

taskPtr.reset();

unique_ptr 對象不可複製

由於 unique_ptr 不可複製,只能移動。因此,我們無法通過複製構造函數或賦值運算符創建unique_ptr對象的副本。

// 編譯錯誤 : unique_ptr 不能複製
std::unique_ptr<Task> taskPtr3 = taskPtr2; // Compile error

// 編譯錯誤 : unique_ptr 不能複製
taskPtr = taskPtr2; //compile error

轉移 unique_ptr 對象的所有權

我們無法複製 unique_ptr 對象,但我們可以轉移它們。這意味着 unique_ptr 對象可以將關聯的原始指針的所有權轉移到另一個 unique_ptr 對象。讓我們通過一個例子來理解:

// 通過原始指針創建 taskPtr2
std::unique_ptr<Task> taskPtr2(new Task(55));
// 把taskPtr2中關聯指針的所有權轉移給taskPtr4
std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
// 現在taskPtr2關聯的指針爲空
if(taskPtr2 == nullptr)
	std::cout<<"taskPtr2 is  empty"<<std::endl;

// taskPtr2關聯指針的所有權現在轉移到了taskPtr4中
if(taskPtr4 != nullptr)
	std::cout<<"taskPtr4 is not empty"<<std::endl;

// 會輸出55
std::cout<< taskPtr4->mId << std::endl;

std::move() 將把 taskPtr2 轉換爲一個右值引用。因此,調用 unique_ptr 的移動構造函數,並將關聯的原始指針傳輸到 taskPtr4。在轉移完原始指針的所有權後, taskPtr2將變爲空。

釋放關聯的原始指針

在 unique_ptr 對象上調用 release()將釋放其關聯的原始指針的所有權,並返回原始指針。這裏是釋放所有權,並沒有delete原始指針,reset()會delete原始指針。

std::unique_ptr<Task> taskPtr5(new Task(55));
// 不爲空
if(taskPtr5 != nullptr)
	std::cout<<"taskPtr5 is not empty"<<std::endl;
// 釋放關聯指針的所有權
Task * ptr = taskPtr5.release();
// 現在爲空
if(taskPtr5 == nullptr)
	std::cout<<"taskPtr5 is empty"<<std::endl;

完整示例程序

#include <iostream>
#include <memory>

struct Task {
    int mId;
    Task(int id ) :mId(id) {
        std::cout<<"Task::Constructor"<<std::endl;
    }
    ~Task() {
        std::cout<<"Task::Destructor"<<std::endl;
    }
};

int main()
{
    // 空對象 unique_ptr
    std::unique_ptr<int> ptr1;

    // 檢查 ptr1 是否爲空
    if(!ptr1)
        std::cout<<"ptr1 is empty"<<std::endl;

    // 檢查 ptr1 是否爲空
    if(ptr1 == nullptr)
        std::cout<<"ptr1 is empty"<<std::endl;

    // 不能通過賦值初始化unique_ptr
    // std::unique_ptr<Task> taskPtr2 = new Task(); // Compile Error

    // 通過原始指針創建 unique_ptr
    std::unique_ptr<Task> taskPtr(new Task(23));

    // 檢查 taskPtr 是否爲空
    if(taskPtr != nullptr)
        std::cout<<"taskPtr is  not empty"<<std::endl;

    // 訪問 unique_ptr關聯指針的成員
    std::cout<<taskPtr->mId<<std::endl;

    std::cout<<"Reset the taskPtr"<<std::endl;
    // 重置 unique_ptr 爲空,將刪除關聯的原始指針
    taskPtr.reset();

    // 檢查是否爲空 / 檢查有沒有關聯的原始指針
    if(taskPtr == nullptr)
        std::cout<<"taskPtr is  empty"<<std::endl;

    // 通過原始指針創建 unique_ptr
    std::unique_ptr<Task> taskPtr2(new Task(55));

    if(taskPtr2 != nullptr)
        std::cout<<"taskPtr2 is  not empty"<<std::endl;

    // unique_ptr 對象不能複製
    //taskPtr = taskPtr2; //compile error
    //std::unique_ptr<Task> taskPtr3 = taskPtr2;

    {
        // 轉移所有權(把unique_ptr中的指針轉移到另一個unique_ptr中)
        std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
        // 轉移後爲空
        if(taskPtr2 == nullptr)
            std::cout << "taskPtr2 is  empty" << std::endl;
        // 轉進來後非空
        if(taskPtr4 != nullptr)
            std::cout<<"taskPtr4 is not empty"<<std::endl;

        std::cout << taskPtr4->mId << std::endl;

        //taskPtr4 超出下面這個括號的作用於將delete其關聯的指針
    }

    std::unique_ptr<Task> taskPtr5(new Task(66));

    if(taskPtr5 != nullptr)
        std::cout << "taskPtr5 is not empty" << std::endl;

    // 釋放所有權
    Task * ptr = taskPtr5.release();

    if(taskPtr5 == nullptr)
        std::cout << "taskPtr5 is empty" << std::endl;

    std::cout << ptr->mId << std::endl;

    delete ptr;

    return 0;
}

輸出:

ptr1 is empty
ptr1 is empty
Task::Constructor
taskPtr is  not empty
23
Reset the taskPtr
Task::Destructor
taskPtr is  empty
Task::Constructor
taskPtr2 is  not empty
taskPtr2 is  empty
taskPtr4 is not empty
55
Task::Destructor
Task::Constructor
taskPtr5 is not empty
taskPtr5 is empty
66
Task::Destructor

總結

new出來的對象是位於堆內存上的,必須調用delete才能釋放其內存。
unique_ptr 是一個裝指針的容器,且擁有關聯指針的唯一所有權,作爲普通變量使用時系統分配對象到棧內存上,超出作用域時會自動析構,unique_ptr對象的析構函數中會delete其關聯指針,這樣就相當於替我們執行了delete堆內存上的對象。

成員函數 作用
reset() 重置unique_ptr爲空,delete其關聯的指針。
release() 不delete關聯指針,並返回關聯指針。釋放關聯指針的所有權,unique_ptr爲空。
get() 僅僅返回關聯指針

unique_ptr不能直接複製,必須使用std::move()轉移其管理的指針,轉移後原 unique_ptr 爲空。std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);

創建unique_ptr對象有兩種方法:

//C++11: 
std::unique_ptr<Task> taskPtr(new Task(23));
//C++14: 
std::unique_ptr<Task> taskPtr = std::make_unique<Task>(34);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章