c++智能指针--unique_ptr

众所周知,c语言与c++需要自行管理动态的内存。许多代码写到最后,防止内存泄漏需要花费程序员大量的心力。在c++11标准中,提出了智能指针来帮助程序员管理动态内存(如果使用智能指针来管理栈上的内存,则在最后析构阶段释放内存时会出现错误)。智能指针主要有三种,分别是shared_ptrunique_ptrweak_ptr。本文主要讲的就是unique_ptr使用中的问题。

unique_ptr

shared_ptr不同,某个时刻只能有一个unique_ptr指向其管理的动态内存上的对象。当这个unique_ptr销毁时,它所指向的对象也会被销毁。

原理分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace std {
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept;
~unique_ptr() noexcept;
T& operator*() const;
T* operator->() const noexcept;
unique_ptr(const unique_ptr &) = delete;
unique_ptr& operator=(const unique_ptr &) = delete;
unique_ptr(unique_ptr &&) noexcept; //右值引用
unique_ptr& operator=(unique_ptr &&) noexcept;
// ...
private:
pointer __ptr;
};
}

由上源代码我们可以了解到: 1. unique_ptr内部存储一个内置指针,当unique_ptr析构时,它的析构函数将会负责析构它持有的对象。

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

  2. unique_ptr并不提供 copy 操作,这是为了防止多个unique_ptr指向同一对象。

  3. unique_ptr提供了 move 操作,因此我们可以用std::move()来转移unique_ptr。

  4. 两种构造函数,一种传入内置指针,一种传入unique_ptr的右值。第一种是explicit的,第二种不是(所以在函数传参时,赋值时~)。

构造选择

shared_ptr不同,并没有类似make_shared()的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,只能使用直接初始化,参数是一个指向动态内存的内置指针或者一个unique_ptr类型的右值引用(通过 move 获得)

1
2
unique_ptr<string> p1(new string("hello~"));
unique_ptr<string> p2(std::move(p1)); //p1管理的动态内存转移到p2,p1可以正常释放,但不再可用

销毁操作

缺省情况下,unique_ptr会使用delete析构对象,试用于new申请的动态内存。如果不适用,我们可以使用自定义的 deleter。

1
2
3
4
5
6
7
struct Widget{  };
// ...
auto deleter = []( Widget *p ) {
cout << "delete Widget!" << endl;
delete p;
};
unique_ptr<Widget, decltype(deleter)> ptr{ new Widget, deleter }; //注意!此处模板需要指定删除器的类型,这一点与shared_ptr不同
我们发现,此处模板需要指定删除器的类型,这一点与shared_ptr不同。当然,我们可以使用 C++11 的 alias template 特性,这样就可以避免指定 deleter 的类型:
1
2
3
4
5
6
7
8
9
10
11
struct Widget{  };
template <typename T>
using uniquePtr = unique_ptr<T, void(*)(T*)>;
void func()
{
uniquePtr<Widget> ptr( new Widget,
[]( Widget *p ) {
cout << "delete Widget!" << endl;
delete p;
});
}

除此之外,unique_ptr为数组提供了模板偏特化,因此unique_ptr也可以指向数组,下为unique_ptr源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace std {
template <typename T, typename D>
class unique_ptr<T[], D>
{
public:
// ...
T& operator[]( size_t i ) const;
};
template <typename T>
class default_delete<T[]>
{
public:
// ...
void operator()( T *p ) const; // call delete[] p
};
}
unique_ptr指向数组时,可以使用[]来访问数组元素。default_delete也为数组提供模板偏特化,因此当unique_ptr被销毁时,会调用delete []释放数组内存。
1
2
3
unique_ptr<string[]> ptr{ new string[100] };
ptr[0] = "hello";
ptr[1] = "world";
### 使用unique_ptr的坑--需要注意的问题1 unique_ptr是用来独占地持有对象的,所以通过同一原生指针来初始化多个unique_ptr,下面是一种错误的使用方式:
1
2
3
4
struct Widget{  };
Widget *ptr = new Widget;
unique_ptr<Widget> p1{ ptr };
unique_ptr<Widget> p2{ ptr }; // ERROR: multiple ownership
当p1和p2各自被销毁的时候,它们指向的Widget将被delete两次。

使用unique_ptr的坑--需要注意的问题2

不能拷贝或赋值一个unique_ptr,但是可以拷贝或赋值一个将要被销毁unique_ptr。最常见的例子是从函数返回一个unique_ptr,还有在函数实参中构造unique_ptr。在这种情况下,编译器知道这个对象将要销毁,将会执行一种特殊的“拷贝”(其实是移动操作)。

使用unique_ptr的坑--需要注意的问题2

使用unique_ptr并不能绝对地保证异常安全,你可能很惊讶于这个结论。让我们看看一个例子:

1
func(unique_ptr<T>{ new T }, func_throw_exception());
*** C++ 标准并没有规定编译器对函数参数的求值次序,所以有可能出现这样的次序: 1. 调用new T分配动态内存。 2. 调用func_throw_exception()函数。 3. 调用unique_ptr的构造函数。

调用func_throw_exception()函数会抛出异常,所以无法构造unique_ptr,导致new T所分配的内存不能回收,造成了内存泄露。解决这个问题,最好有一个make_unique函数(原子操作)。但是c++11标准库中并没有提供。好消息是c++14中提供了。

参考资料

  • [1] C++ Primer(第5版)
  • [2] http://senlinzhan.github.io/2015/04/20/%E8%B0%88%E8%B0%88C-%E7%9A%84%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/

c++智能指针--unique_ptr
http://line.com/2018/10/20/2018-10-20-cpp-unique-ptr/
作者
Line
发布于
2018年10月20日
许可协议