从异常安全说起
使用 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
|