从异常安全说起
使用 raw pointer 管理动态内存时,经常会遇到这样的问题:
- 忘记
delete
内存,造成内存泄露。 - 出现异常时,不会执行
delete
,造成内存泄露。
下面的代码解释了,当一个操作发生异常时,会导致delete
不会被执行:
1 2 3 4 5 6 7 8 9 | void func() { auto ptr = new Widget; // 执行一个会抛出异常的操作 func_throw_exception(); delete ptr; } |
在 C++98 中我们需要用一种笨拙的方式,写出异常安全的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void func() { auto ptr = new Widget; try { func_throw_exception(); } catch(...) { delete ptr; throw; } delete ptr; } |
使用智能指针能轻易写出异常安全的代码,因为当对象退出作用域时,智能指针将自动调用对象的析构函数,避免内存泄露:
1 2 3 4 5 6 | void func() { std::unique_ptr<Widget> ptr{ new Widget }; func_throw_exception(); } |
unique_ptr 的原理
让我们了解一下unique_ptr
的实现细节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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; }; } |
从上面的代码中,我们可以了解到:
unique_ptr
内部存储一个 raw pointer,当unique_ptr
析构时,它的析构函数将会负责析构它持有的对象。unique_ptr
提供了operator*()
和operator->()
成员函数,像 raw pointer 一样,我们可以使用*
解引用unique_ptr
,使用->
来访问unique_ptr
所持有对象的成员。unique_ptr
并不提供 copy 操作,这是为了防止多个unique_ptr
指向同一对象。- 但
unique_ptr
提供了 move 操作,因此我们可以用std::move()
来转移unique_ptr
。
很显然,缺省情况下,unique_ptr
会使用delete
析构对象,不过我们可以使用自定义的 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 }; |
当然,我们可以使用 C++11 的 alias template 特性,这样就可以避免指定 deleter 的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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
也可以指向数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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
是用来独占地持有对象的,所以通过同一原生指针来初始化多个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
两次。
再谈异常安全
C++14 提供了std::make_unique<T>()
函数用来直接创建unique_ptr
,但 C++11 并没有提供,不过其实现并不复杂:
1 2 3 4 5 6 | template <typename T, typename... Ts> std::unique_ptr<T> make_unique( Ts&&... params ) { return std::unique_ptr<T>( new T( std::forward<Ts>(params)... ) ); } // ... auto ptr = make_unique<std::string>("senlin"); |
思考一下使用make_unique
的好处?
使用unique_ptr
并不能绝对地保证异常安全,你可能很惊讶于这个结论。让我们看看一个例子:
1 | func(unique_ptr<T>{ new T }, func_throw_exception()); |
C++ 标准并没有规定编译器对函数参数的求值次序,所以有可能出现这样的次序:
- 调用
new T
分配动态内存。 - 调用
func_throw_exception()
函数。 - 调用
unique_ptr
的构造函数。
调用func_throw_exception()
函数会抛出异常,所以无法构造unique_ptr
,导致new T
所分配的内存不能回收,造成了内存泄露。解决这个问题,需要使用make_unique
函数:
1 | func(make_unique<T>(), func_throw_exception()); |
这种情况下,成功解决了内存泄露的问题。
make_unique
在初始化对象的时候使用的()
而不是{}
,所以下面的代码显然是初始化10
个元素:
1 2 3 | auto up = make_unique<vector<int>>( 10, 100 ); cout << "size: " << up->size() << endl; // size: 10 |
但是如果使用std::initializer_list
来初始化对象时,要怎样做呢?嗯嗯,看看下面的代码:
1 2 3 4 | auto initList = { 1, 2, 3, 4, 5 }; auto up = make_unique<vector<int>>( initList ); cout << "size: " << up->size() << endl; // size: 5 |