1. 程式人生 > 其它 >C++記憶體管理——unique_ptr

C++記憶體管理——unique_ptr

1. 概述

本想將unique_ptr, shared_ptr和weak_ptr寫在同一篇文章中,無奈越(廢)寫(話)越(連)長(篇),本著不給自己和讀者太大壓力的原則,最終決定分為三篇去描述它們(不是惡意湊文章數哦)。
本篇文章主要描述了unique_ptr,在此之前先給出了auto_ptr的介紹,廢話不說,直入正題。

2. auto_ptr

auto_ptr是在C++ 98中引入的,在C++ 17中被移除掉。它的引入是為了管理動態分配的記憶體,它的移除是因為本身有嚴重的缺陷,並且已經有了很好的替代者(unique_ptr)。
auto_ptr採用"Copy"語義,期望實現"Move"語義,有諸多的問題。標準庫中的auto_ptr和《Move語義和Smart Pointers先導(以一個例子說明)》中的AutoPtr2十分類似,此處再次給出程式碼並分析它的問題。

template<typename T>
struct AutoPtr2
{
    AutoPtr2(T* ptr = nullptr)
        : ptr(ptr)
    {
    }

    ~AutoPtr2()
    {
        if(this->ptr != nullptr)
        {
            delete this->ptr;
            this->ptr = nullptr;
        }
    }

    AutoPtr2(AutoPtr2& ptr2) // not const
    {
        this->ptr = ptr2.ptr;
        ptr2.ptr = nullptr;
    }

    AutoPtr2& operator=(AutoPtr2& ptr2) // not const
    {
        if(this == &ptr2)
        {
            return *this;
        }

        delete this->ptr;
        this->ptr = ptr2.ptr;
        ptr2.ptr = nullptr;
        return *this;
    }

    T& operator*() const
    {
        return *this->ptr;
    }

    T* operator->() const
    {
        return this->ptr;
    }

    bool isNull() const
    {
        return this->ptr == nullptr;
    }

private:
    T* ptr;
};

以上採用"Copy"語義,期望實現"Move"語義的實現有以下三大問題:

  1. auto_ptr採用拷貝構造和拷貝賦值構造去實現"Move"語義,若將auto_ptr採用值傳遞作為函式的引數,當函式執行結束時會導致資源被釋放,若之後的程式碼再次訪問此auto_ptr則會是nullptr;
  2. 由於auto_ptr總是使用"non-array delete",所以它不能用於管理array類的動態記憶體;
  3. auto_ptr不能和STL容器和演算法配合工作,因為STL中的"Copy"真的是"Copy",而不是"Move"。

由於auto_ptr有諸多問題,需要一個更加完美的"Smart Point",unique_ptr也就應運而生了。

3. unqiue_ptr

3.1 Smart Points簡介

Smart Points是什麼,或者說它是用來幹什麼的?它是用來管理動態分配的記憶體的,它能夠動態地分配資源且能夠在適當的時候釋放掉曾經動態分配的記憶體。
此時對智慧指標來說就有兩條原則:

  1. 智慧指標本身不能是動態分配的,否則它自身有不被釋放的風險,進而可能導致它所管理物件不能正確地被釋放;
  2. 在棧上分配智慧指標,讓它指向堆上動態分配的物件,這樣就能保證智慧指標所管理的物件能夠合理地被釋放。

3.2 unique_ptr的實現

unique_ptr是獨佔式的,即完全擁有它所管理物件的所有權,不和其它的物件共享。標準庫中的實現和《Move constructors 和 Move assignment constructors簡介》中的AutoPtr4十分相似,程式碼如下:

template<typename T>
struct AutoPtr4
{
    AutoPtr4(T* ptr = nullptr)
        : ptr(ptr)
    {
    }

    ~AutoPtr4()
    {
        if(this->ptr != nullptr)
        {
            delete this->ptr;
            this->ptr = nullptr;
        }
    }

    AutoPtr4(const AutoPtr4& ptr4) = delete; // disable copying

    AutoPtr4(AutoPtr4&& ptr4) noexcept // move constructor
        : ptr(ptr4)
    {
        ptr4.ptr = nullptr;
    }

    AutoPtr4& operator=(const AutoPtr4& ptr4) = delete; // disable copy assignment

    AutoPtr4& operator=(AutoPtr4&& ptr4) noexcept // move assignment
    {
        if(this == &ptr4)
        {
            return *this;
        }

        delete this->ptr;
        this->ptr = ptr4.ptr;
        ptr4.ptr = nullptr;
        return *this;
    }

    T& operator*() const
    {
        return *this->ptr;
    }

    T* operator->() const
    {
        return this->ptr;
    }

    bool isNull() const
    {
        return this->ptr == nullptr;
    }

private:
    T* ptr;
};

從中可以看到,unique_ptr禁用了拷貝構造和拷貝賦值構造,僅僅實現了移動構造和移動賦值構造,這也就使得它是獨佔式的。

3.3 unique_ptr的使用

3.3.1 unique_ptr的基本使用

下面是一個unique_ptr的例子,此處的res是在棧上的區域性變數,在main()結束時會被銷燬,它管理的資源也會被釋放掉。

#include <iostream>
#include <memory> // for std::unique_ptr

struct Resource
{
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    // allocate a Resource object and have it owned by std::unique_ptr
    std::unique_ptr<Resource> res{ new Resource() };

    return 0;
} // the allocated Resource is destroyed here

以下的程式碼講解unique_ptr和"Move"語義:

#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

struct Resource
{
    Resource() 
    { 
        std::cout << "Resource acquired" << std::endl; 
    }
    
    ~Resource() 
    {
        std::cout << "Resource destroyed" << std::endl; 
    }
};

int main()
{
    std::unique_ptr<Resource> res1{ new Resource{} };
    std::unique_ptr<Resource> res2{};

    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null" : "null") << std::endl;
    std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null" : "null") << std::endl;

    // res2 = res1; // Won't compile: copy assignment is disabled
    res2 = std::move(res1); // res2 assumes ownership, res1 is set to null

    std::cout << "Ownership transferred" << std::endl;

    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null" : "null") << std::endl;
    std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null" : "null") << std::endl;

    return 0;
} // Resource destroyed here

以上程式碼的執行結果如下:

Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed

由於unique_ptr禁止了"Copy"語義,所以"res2 = res1;"不能編譯通過。如果我們想轉移unique_ptr管理的一個物件的所有權怎麼辦?可以採用"Move"語義,即通過move()將res1轉化為一個右值,此時再將它賦值給res2就會呼叫移動賦值建構函式實現所有權的轉移。

3.3.2 訪問管理的物件

unique_ptr有過載的"operator*"和"operator->",即它和普通的指標具有相似的訪問物件的方法。
其中"operator*"返回它管理物件的引用,"operator->"返回一個指向它管理物件的指標。

3.3.3 unique_ptr和array

不同於auto_ptr只能有"delete",unique_ptr可以有"delete"和"array delete"。其中,unique_ptr對於std::array, std::vector和std::string的支援比較友好。

3.3.4 make_unique

std::make_unique是C++ 14才引入的(詳見參考文獻3,此處不詳細展開),它能夠建立並返回 unique_ptr 至指定型別的物件。它完美傳遞了引數給物件的建構函式,從一個原始指標構造出一個std::unique_ptr,返回建立的std::unique_ptr。其大概的實現如下:

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

此處需要記住優選std::make_unique(),而不是自己去建立一個std::unique_ptr。

3.3.5 unique_ptr作為函式的返回值

unique_ptr可以作為函式的返回值,如下的程式碼:

struct Resource
{
    ...
};

std::unique_ptr<Resource> createResource()
{
     return std::make_unique<Resource>();
}

int main()
{
    auto ptr{ createResource() };

    ...

    return 0;
}

可以看到unique_ptr作為值在createResource()函式中返回,並在main()函式中通過"Move"語義將所有權轉移給ptr。

3.3.6 unique_ptr作為函式引數傳遞

若要函式接管指標的所有權,可以通過值傳遞unique_ptr,且要採用"Move"語義。

#include <iostream>
#include <memory>
#include <utility>

struct Resource
{
    Resource()
    {
        std::cout << "Resource acquired" << std::endl;
    }

    ~Resource()
    {
        std::cout << "Resource destroyed" << std::endl;
    }

    friend std::ostream& operator<<(std::ostream& out, const Resource& res)
    {
        out << "I am a resource";
        return out;
    }
};

void takeOwnership(std::unique_ptr<Resource> res)
{
     if (res)
     {
         std::cout << *res << std::endl;
     }
} // the Resource is destroyed here

int main()
{
    auto ptr{ std::make_unique<Resource>() };

    takeOwnership(std::move(ptr)); // move semantics

    std::cout << "Ending program" << std::endl;

    return 0;
}

以上的程式碼輸出如下:

Resource acquired
I am a resource
Resource destroyed
Ending program

從中可以看到,所有權被函式takeOwnership()接管,當函式執行完畢時資源即被釋放。
然而大多數時候我們只是想通過函式呼叫去改變智慧指標管理的物件,而不是讓函式接管所有權。此時我們可以通過傳遞原始的指標或者引用來實現,如下:

#include <iostream>
#include <memory>

struct Resource
{
    Resource()
    {
        std::cout << "Resource acquired" << std::endl;
    }

    ~Resource()
    {
        std::cout << "Resource destroyed" << std::endl;
    }

    friend std::ostream& operator<<(std::ostream& out, const Resource& res)
    {
        out << "I am a resource";
        return out;
    }
};

void useResource(const Resource* res)
{
    if (res)
    {
        std::cout << *res << std::endl;
    }
}

int main()
{
    auto ptr{ std::make_unique<Resource>() };

    useResource(ptr.get()); // get(): get a pointer to the Resource

    std::cout << "Ending program" << std::endl;

    return 0;
} // The Resource is destroyed here

以上程式碼的輸出如下:

Resource acquired
I am a resource
Ending program
Resource destroyed

3.3.7 unique_ptr作為類的成員變數

unique_ptr還可以作為類的成員變數,以下程式碼中的普通指標怎麼用unique_ptr替換?詳見參考文獻4。
普通指標版本:

struct Device 
{
    ...
};

struct Settings 
{
    Settings(Device* device) 
    {
        this->device = device;
    }

    Device* getDevice() 
    {
        return device;
    }

private:
    Device* device;
};    

int main() 
{
    Device* device = new Device();
    Settings settings(device);
    ...
    Device* myDevice = settings.getDevice();
    ...

    delete device;
}

unique_ptr版本:

#include <memory>

struct Device 
{
    ...
};

struct Settings 
{
    Settings(std::unique_ptr<Device> d) 
    {
        device = std::move(d);
    }

    Device& getDevice() 
    {
        return *device;
    }

private:
    std::unique_ptr<Device> device;
};

int main() 
{
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    ...
    Device& myDevice = settings.getDevice();
    ...
}

3.3.8 其它用法

unique_ptr的其它用法如下:

3.3.9 unique_ptr的誤用

常見的誤用有兩種:

  1. 多個智慧指標物件管理同一個資源:
Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
std::unique_ptr<Resource> res2{ res };

unique_ptr是獨佔的,另外res1和res2的生命週期結束後都會釋放同一塊資源,從而導致未定義的錯誤。

  1. unique_ptr管理資源後,又自定義了delete資源:
Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;

在res1的生命週期結束時會去釋放已經被delete釋放過的資源,從而導致未定義的錯誤。

4. 總結

本文通過對auto_ptr的介紹引出了unique_ptr,總結了unique_ptr的實現以及一些常用的方法,並給出了常見的錯誤使用。

5. 參考文獻

  1. Move語義和Smart Pointers先導(以一個例子說明),https://www.jianshu.com/p/0c9b4e1e7b9f
  2. Move constructors 和 Move assignment constructors簡介,https://www.jianshu.com/p/f97e211fdc2d
  3. c++ 之智慧指標:儘量使用std::make_unique和std::make_shared而不直接使用new,https://blog.csdn.net/p942005405/article/details/84635673
  4. C++智慧指標作為成員變數,https://www.jianshu.com/p/3402d90a5647

歡迎大家批評指正、評論和轉載(請註明源出處),謝謝!