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的其它用法如下:

C++内存管理——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

 

欢迎大家批评指正、评论和转载(请注明源出处),谢谢!

上一篇:智能指针


下一篇:C++ 智能指针 - 全部用法详解