C++进阶:智能指针之unique_ptr

3,181 阅读6分钟

我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛

概念

unique_ptr是C++11提供的用于防止内存泄漏的智能指针中的一种实现,用来替代auto_ptr,独享被管理对象指针所有权的智能指针。

unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中delete其关联的原始指针释放内存。具有->*运算符重载符,因此它可以像普通指针一样使用。

unique_ptr唯一拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针,unique_ptr使用RAII的特性,使得在出现异常的情况下,动态资源能得到释放。 unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向具体对象,则将其所指对象销毁,默认使用delete操作符,当然用户也可自定义其他deleter。 unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权等。

API

下面来看下unique_ptr模板类的声明:

template <class T, class D = default_delete<T>>
class unique_ptr
{
public:
    using pointer = /* 见定义 */;
    using element_type = T;
    using deleter_type = D;

    // 构造函数
    constexpr unique_ptr() noexcept;
    explicit unique_ptr(pointer p) noexcept;
    unique_ptr(pointer p, /* 见定义 */ d1) noexcept;
    unique_ptr(pointer p, /* 见定义 */ d2) noexcept;
    unique_ptr(unique_ptr &&u) noexcept;
    constexpr unique_ptr(nullptr_t) noexcept;
    template <class U, class E>
    unique_ptr(unique_ptr<U, E> &&u) noexcept;

    // 析构函数
    ~unique_ptr();

    // 赋值
    unique_ptr &operator=(unique_ptr &&u) noexcept;
    template <class U, class E>
    unique_ptr &operator=(unique_ptr<U, E> &&u) noexcept;
    unique_ptr &operator=(nullptr_t) noexcept;

    // 探查函数
    add_lvalue_reference_t<T> operator*() const;
    pointer operator->() const noexcept;
    pointer get() const noexcept;
    deleter_type &get_deleter() noexcept;
    const deleter_type &get_deleter() const noexcept;
    explicit operator bool() const noexcept;

    // 修改函数
    pointer release() noexcept;
    void reset(pointer p = pointer()) noexcept;
    void swap(unique_ptr &u) noexcept;

    // 禁用从左值复制
    unique_ptr(const unique_ptr &) = delete;
    unique_ptr &operator=(const unique_ptr &) = delete;
};

下面这个是针对数组的版本特例化:

//Specialization for arrays:
template <class T, class D>
class unique_ptr<T[], D>
{
public:
    using pointer = /* 见定义 */;
    using element_type = T;
    using deleter_type = D;

    // 构造函数
    constexpr unique_ptr() noexcept;
    template <class U>
    explicit unique_ptr(U p) noexcept;
    template <class U>
    unique_ptr(U p, /* 见定义 */ d) noexcept;
    template <class U>
    unique_ptr(U p, /* 见定义 */ d) noexcept;
    unique_ptr(unique_ptr &&u) noexcept;
    template <class U, class E>
    unique_ptr(unique_ptr<U, E> &&u) noexcept;
    constexpr unique_ptr(nullptr_t) noexcept;

    // 析构函数
    ~unique_ptr();

    // 赋值
    unique_ptr &operator=(unique_ptr &&u) noexcept;
    template <class U, class E>
    unique_ptr &operator=(unique_ptr<U, E> &&u) noexcept;
    unique_ptr &operator=(nullptr_t) noexcept;

    // 探察函数
    T &operator[](size_t i) const;
    pointer get() const noexcept;
    deleter_type &get_deleter() noexcept;
    const deleter_type &get_deleter() const noexcept;
    explicit operator bool() const noexcept;

    // 修改函数
    pointer release() noexcept;
    template <class U>
    void reset(U p) noexcept;
    void reset(nullptr_t = nullptr) noexcept;
    void swap(unique_ptr &u) noexcept;

    // 禁用从左值复制
    unique_ptr(const unique_ptr &) = delete;
    unique_ptr &operator=(const unique_ptr &) = delete;
};

从上面可以看到unique_ptr有两个模板类,一种是对象指针,一种是数组指针,如下:

  • 指向单个对象

    std::unique_ptr<Type> p1; // p1关联Type对象
    
  • 指向一个数组

    unique_ptr<Type[]> p2; // p2关联Type对象数组
    

unique_ptr模板类满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 的要求,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的要求。

在我上一篇文章介绍的auto_ptr就是因为满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的要求导致一系列的问题,所以从C++11开始,支持移动语义后,重新定义的unique_ptr去掉了复制构造和复制赋值增加了移动构造和移动赋值。这是为了防止多个unique_ptr指向同一对象。

unique_ptr内部存储一个 raw pointer,当unique_ptr析构时,它的析构函数将会负责delete它持有的对象。

unique_ptr提供了operator*()operator->()成员函数,像 raw pointer 一样,我们可以使用*解引用unique_ptr,使用->来访问unique_ptr所持有对象的成员。

unique_ptr提供了 move 操作,因此我们可以用std::move()来转移unique_ptr的所有权。

缺省情况下,unique_ptr会使用delete析构对象,不过我们可以使用自定义的 deleter。

我们来看一个完整例子:

#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include <fstream>
#include <cassert>
#include <functional>

struct B {
    virtual void bar() { std::cout << "B::bar\n"; }
    virtual ~B() = default;
};
struct D : B {
    D() { std::cout << "D::D\n"; }
    ~D() { std::cout << "D::~D\n"; }
    void bar() override { std::cout << "D::bar\n"; }
};

// 消费 unique_ptr 的函数能以值或以右值引用接收它
std::unique_ptr<D> pass_through(std::unique_ptr<D> p) {
    p->bar();
    return p;
}

void close_file(std::FILE *fp) { std::fclose(fp); }

int main() {
    std::cout << "unique ownership semantics demo\n";
    {
        auto p = std::make_unique<D>(); // p 是占有 D 的 unique_ptr
        auto q = pass_through(std::move(p));
        assert(!p); // 现在 p 不占有任何内容并保有空指针
        q->bar();   // 而 q 占有 D 对象
    }               // ~D 调用于此

    std::cout << "Runtime polymorphism demo\n";
    {
        // p 是占有 D 的 unique_ptr, 作为指向基类的指针
        std::unique_ptr<B> p = std::make_unique<D>();
        p->bar(); // 虚派发
        // unique_ptr 能存储于容器
        std::vector<std::unique_ptr<B>> v;
        v.push_back(std::make_unique<D>());
        v.push_back(std::move(p));
        v.emplace_back(new D);
        for (auto &p : v)
            p->bar(); // 虚派发
    }  // ~D called 3 times

    std::cout << "Custom deleter demo\n";
    std::ofstream("demo.txt") << 'x'; // 准备要读的文件
    {
        std::unique_ptr<std::FILE, void (*)(std::FILE *)> fp(
            std::fopen("demo.txt", "r"), close_file);
        if (fp) // fopen 可以打开失败;该情况下 fp 保有空指针
            std::cout << (char)std::fgetc(fp.get()) << '\n';
    } // fclose() 调用于此,但仅若 FILE* 不是空指针(即 fopen 成功)

    std::cout << "Custom lambda-expression deleter demo\n";
    {
        std::unique_ptr<D, std::function<void(D *)>> p(new D, [](D *ptr)
            {
                std::cout << "destroying from a custom deleter...\n";
                delete ptr;
            }); // p 占有 D
        p->bar();
    } // 调用上述 lambda 并销毁 D

    std::cout << "Array form of unique_ptr demo\n";
    {
        std::unique_ptr<D[]> p{new D[3]};
    } // 调用 ~D 3 次
}

从上面的例子,我们可以知道unique_ptr的特点,主要具有以下:

  • 独享所有权,在作用域结束时候,自动释放所关联的对象

  • 无法进行拷贝与赋值操作,虽然unique_ptr不支持拷贝操作,但却有一个例外:可以从函数中返回一个unique_ptr:

    unique_ptr<int> ptr(new int(1));
    unique_ptr<int> ptr1(ptr) ; // error
    unique_ptr<int> ptr2 = ptr; //error
    
      unique_ptr<int> clone(int p)
      {
      	unique_ptr<int> pInt(new int(p));
      	return pInt;    // OK,返回unique_ptr
      }
    
  • 显示的所有权转移(通过move语义)

    unique_ptr<int> ptr(new int(1));
    unique_ptr<int> ptr1 = std::move(ptr) ; // ok
    
  • 作为容器元素存储在容器中。

  • 可以使用自定义的delete析构对象。

  • 自c++14起,可以使用std::make_uniqueunique_ptr进行初始化,如果在c++11中使用上述方法进行初始化,会得到下面的错误提示:

    error: ‘make_unique’ is not a member of ‘std’
    

    因此,如果为了使得c++11也可以使用std::make_unique,我们可以自己进行封装,如下:

    namespace details {
        #if __cplusplus >= 201402L // C++14及以后使用STL实现的
        using std::make_unique;
        #else
        template<typename T, typename... Args>
        std::unique_ptr<T> make_unique(Args &&... args)
        {
            return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
        }
        #endif
    } // namespace details
    

记住优选std::make_unique(),而不是自己去创建一个std::unique_ptr。

为了尽可能了解unique_ptr的使用姿势,我们再看下面示例代码:

#include <memory>
#include <utility> // std::move

void fun1(double *);
void fun2(std::unique<double> *);
void fun3(std::unique<double> &);
void fun4(std::unique<double> );

int main() {
  std::unique_ptr<double> p(new double(3.14));  // 1
  
  fun1(p.get()); // 2
  fun2(&p); // 3
  fun3(p); // 4
  
  if (p) { // 5
    std::cout << "is valid" << std::endl;
  }
  auto p2(p.release()); // 6 转移所有权
  auto p2.reset(new double(1.0)); // 7
  fun4(std::move(p2)); // 8
  
  return 0;
}

上述代码,基本覆盖了常见的unique_ptr用法:

  • 语句1,通过new创建一个unique_ptr对象
  • 语句2,通过get()函数获取其关联的原生指针
  • 语句3,通过unique_ptr对象的指针进行访问
  • 语句4,通过unique_ptr对象的引用进行访问
  • 语句5,通过if(p)来判断其是否有效
  • 语句6,通过release函数释放所有权,并将所有权进行转移
  • 语句7,通过reset释放之前的原生指针,并重新关联一个新的指针
  • 语句8,通过std::move转移所有权

使用场景

上面我们讲完了unique_ptr的基本用法,这节来简单讲一下unique_ptr的使用场景。

函数的返回值

上面我们也提到过,unique_ptr可以作为函数的返回值,如下的代码:

#include <iostream>

struct Resource {
    int a;
};

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

int main() {
    auto ptr{ createResourcePtr() };
    ptr->a = 1024;
    std::cout << ptr->a << "\n";

    return 0;
}

可以看到unique_ptr作为值在createResourcePtr()函数中创建并返回,并在main()函数中通过"Move"语义将所有权转移给ptr。

函数参数

若要函数接管指针的所有权,可以通过值传递unique_ptr,但必须要采用"Move"语义:

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

struct Resource {
    Resource() {
        std::cout << "Resource created" << 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 passPtr(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>() };
    passPtr(std::move(ptr)); // move semantics
    std::cout << "Ending program" << std::endl;

    return 0;
}

上面这种方式属于讲所有权完全交给函数,外部不能再使用了。如果想不交出所有权,可以传递智能指针引用:

#include<iostream>
#include<memory>
void test(std::unique_ptr<int> &p) {
    *p = 1;
}
int main() {
    std::unique_ptr<int> up(new int(42));
    test(up);
    std::cout<<*up<<std::endl; //输出1
    return 0;
}

当然我们还可以向函数中传递普通指针,使用get函数就可以获取裸指针,如:

#include<iostream>
#include<memory>
void test(int *p) {
    *p = 10;
}
int main() {
    std::unique_ptr<int> up(new int(42));
    test(up.get()); //传入裸指针作为参数
    std::cout<<*up<<std::endl; //输出10
    return 0;
}

类成员变量

通过在类中使用unique_ptr指针可以避免资源泄漏。使用unique_ptr而不是普通的指针,则不再需要析构函数,因为对象会随着成员的删除而被delete。

此外,unique_ptr有助于避免对象初始化期间引发的异常而引起的资源泄漏。因为类对象只有在完成构造后才调用析构函数,所以如果构造函数内部发生异常,则仅针对已完全构造的对象调用析构函数。如果在构造过程中第一个new执行成功而第二个new没有成功,则可能导致具有多个原始指针的类的资源泄漏。

#include <iostream>
#include <string>
#include <memory>
#include <sstream>
 
using namespace std;
 
class ClassA {
public:
    ClassA(const string & sName, const string & sOwnerName, int nVal)
        : m_sName(sName), m_sOwnerName(sOwnerName)
    {
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”开始构造" << endl;
        if (0 == nVal)
        {
            runtime_error oRtEx("值不能为0\n");
            throw oRtEx;
        } else {
            m_dVal = 1.0 / nVal;
        }
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”完成构造" << endl;
    }
 
    ClassA(const ClassA & o2BeCopy) {
        m_dVal = o2BeCopy.m_dVal;
        m_sName = o2BeCopy.m_sName;
        m_sOwnerName = o2BeCopy.m_sOwnerName;
    }
 
    ~ClassA() {
        cout << "“" << m_sOwnerName << "”的ClassA对象“" << m_sName << "”析构" << endl;
    }
 
    void setOwnerName(const string & sOwnerName) { m_sOwnerName = sOwnerName; }
 
private:
    double m_dVal;
    string m_sName;
    string m_sOwnerName;
};
 
class ClassB {
public:
    //如果ptr2的初始化抛出异常将导致资源泄露
    ClassB (int nVal1, int nVal2, const string & sName)
    {
        cout << "名为“" << sName << "”的ClassB对象开始构造" << endl;
        m_ptr1 = unique_ptr<ClassA>(new ClassA("m_ptr1", sName, nVal1));
        m_ptr2 = unique_ptr<ClassA>(new ClassA("m_ptr2", sName, nVal2));
        m_sName = sName;
        cout << "名为“" << sName << "”的ClassB对象完成构造" << endl;
    }
 
    //拷贝构造
    //如果ptr2的初始化之前抛出异常将导致资源泄露
    ClassB (const ClassB& x)
    {
        cout << "名为“" << m_sName << "”的ClassB对象开始拷贝构造" << endl;
        m_ptr1 = unique_ptr<ClassA>(new ClassA(*(x.m_ptr1)));
        m_ptr1->setOwnerName("拷贝构造");
        ostringstream oss;
        oss << "名为“" << m_sName << "”的ClassB对象拷贝构造出现异常\n";
        runtime_error oRtEx(oss.str());
        throw oRtEx;
        m_ptr2 = unique_ptr<ClassA>(new ClassA(*(x.m_ptr2)));
        m_ptr2->setOwnerName("拷贝构造");
        cout << "名为“" << m_sName << "”的ClassB对象完成拷贝构造" << endl;
    }
 
    //赋值运算符
    const ClassB& operator= (const ClassB& x) {
        *m_ptr1 = *x.m_ptr1;
        *m_ptr2 = *x.m_ptr2;
        return *this;
    }
 
private:
        unique_ptr<ClassA> m_ptr1; //指针成员
        unique_ptr<ClassA> m_ptr2;
        string m_sName;
};
 
int main()
{
    try {
        ClassB oB(1, 0, "oB");
    } catch (const exception & ex) {
        cout << "ClassB oB(1, 0)执行出现异常,具体原因:" << ex.what();
    }
 
    cout << "=====================" << endl;
 
    try {
        ClassB oB1(1, 2, "oB1");
        ClassB oB2(oB1);
    } catch (const exception & ex) {
        cout << "ClassB oB2(oB1)执行出现异常,具体原因:" << ex.what();
    }
 
    return 0;
}

使用陷阱

unique_ptr是独占地持有对象的,所以通过同一原生指针来初始化多个unique_ptr是一种错误的使用方式:

struct Resource{  };
{
	Resource *ptr = new Resource;
	unique_ptr<Resource> p1{ ptr };
	unique_ptr<Resource> p2{ ptr }; // ERROR: multiple ownership 
} // double free

p1p2各自被销毁的时候,它们指向的Resource将被delete两次,再编译时不会提示错误,但是在运行期p1和p2 delete时会提示“double free”的错误。

另外,不要与裸指针混用,unique_ptr不允许两个unique_ptr指向同一个对象,在没有裸指针的情况下,我们只能用release获取内存的地址,同时放弃对对象的所有权,这样就有效避免了多个unique_ptr同时指向一个对象。 而使用裸指针就很容易打破这一点。

总结

unique_ptr独占管理对象,只有移动语义。unique_ptr可以不占用对象,即为空。可以通过reset()或者赋值nullptr释放管理对象。 标准库早期版本中定了auto_ptr,它具有unique_ptr的部分特征,但不是全部。例如不能在容器中保存auto_ptr,不能从函数中返回auto_ptr等等,这也是unique_ptr主要的使用场景。下一篇我们介绍shared_ptr。