从异常安全说起
使用 raw pointer 管理动态内存时,经常会遇到这样的问题:
- 忘记
delete
内存,造成内存泄露。 - 出现异常时,不会执行
delete
,造成内存泄露。
下面的代码解释了,当一个操作发生异常时,会导致delete
不会被执行:
在 C++98 中我们需要用一种笨拙的方式,写出异常安全的代码:
使用智能指针能轻易写出异常安全的代码,因为当对象退出作用域时,智能指针将自动调用对象的析构函数,避免内存泄露:
unique_ptr 的原理
让我们了解一下unique_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。
当然,我们可以使用 C++11 的 alias template 特性,这样就可以避免指定 deleter 的类型:
unique_ptr
为数组提供了模板偏特化,因此unique_ptr
也可以指向数组:
当unique_ptr
指向数组时,可以使用[]
来访问数组元素。default_delete
也为数组提供模板偏特化,因此当unique_ptr
被销毁时,会调用delete []
释放数组内存。
一些陷阱
unique_ptr
是用来独占地持有对象的,所以通过同一原生指针来初始化多个unique_ptr
,下面是一种错误的使用方式:
当p1
和p2
各自被销毁的时候,它们指向的Widget
将被delete
两次。
再谈异常安全
C++14 提供了std::make_unique<T>()
函数用来直接创建unique_ptr
,但 C++11 并没有提供,不过其实现并不复杂:
思考一下使用make_unique
的好处?
使用unique_ptr
并不能绝对地保证异常安全,你可能很惊讶于这个结论。让我们看看一个例子:
C++ 标准并没有规定编译器对函数参数的求值次序,所以有可能出现这样的次序:
- 调用
new T
分配动态内存。 - 调用
func_throw_exception()
函数。 - 调用
unique_ptr
的构造函数。
调用func_throw_exception()
函数会抛出异常,所以无法构造unique_ptr
,导致new T
所分配的内存不能回收,造成了内存泄露。解决这个问题,需要使用make_unique
函数:
这种情况下,成功解决了内存泄露的问题。
make_unique
在初始化对象的时候使用的()
而不是{}
,所以下面的代码显然是初始化10
个元素:
但是如果使用std::initializer_list
来初始化对象时,要怎样做呢?嗯嗯,看看下面的代码: