M.6 — std::unique_ptr
Key Takeaway
- 智能指针永远都不应该被动态创建,总是应该在栈上
std::unique_ptr
智能指针是其管理资源的唯一拥有者,且只有移动语义std::unique_ptr
并不一定总是管理着某个对象——有可能是它被初始化为空(使用默认构造函数或nullptr
字面量作为参数),也有可能是它管理的资源被转移了,通过直接对它判断true还是false可以知道它是否正在管理资源- 模板化函数
std::make_unique
比直接使用std::unique_ptr
更好用也更安全,它返回的是管理资源的std::unique_ptr
- 函数传参时,一般不希望转移所有权,否则资源会在函数结束时被销毁。此时最好传递智能指针管理的资源本身(按值传递或按地址传递都可以),但是不要把智能指针的引用直接传进去
在本章开始的时候,我们讨论了使用指针可能会引发的bug和内存泄漏问题。例如,函数的提前返回、异常的抛出和指针删除不当都可能导致上述问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
到目前为止,我们已经学习了移动语义的基本内容,接下来可以继续回到智能指针类的讨论了。回忆一下,智能指针是一个用于管理动态分配内存的类。尽管智能指针也提供其他功能,但是它最本质的功能还是管理动态分配的资源,并且确保在恰当的时候(通常是智能指针离开作用域时),该动态资源可以被正确地清理。
因此,智能指针本身永远都不应该被动态创建(否则智能指针对象本身可能会被忘记释放,从而导致内存泄漏问题)。默认情况下,智能指针应该被创建在栈上(作为局部变量或作为其他类的成员),我们保证智能指针在离开作用域时(包含该指针的函数或对象结束时),它所拥有的资源会被恰当地释放。
C++11 标准库提供了4种智能指针类:std::auto_ptr
(在 C++17 中已经删除了), std::unique_ptr
, std::shared_ptr
和 std::weak_ptr
。其中 std::unique_ptr
是目前最为常用的一个指针类,所以我们稍后会首先介绍它,在接下来的课程中,我们会介绍 std::shared_ptr
和std::weak_ptr
。
std::unique_ptr
std::unique_ptr
在C++11中被用来替代 std::auto_ptr
。该智能指针被用来管理动态分配的对象,且该对象并不被多个其他对象所共享。也就是说, std::unique_ptr
单独拥有它所管理的资源,而不会和其他类共享资源的所有权。 std::unique_ptr
被定义在 <memory>
头文件中。
再看一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
因为 std::unique_ptr
被分配在栈上,所以它最终一定会离开作用域,当它离开时,它会确保Resource
被删除。
和std::auto_ptr
不同的是 std::unique_ptr
正确地实现了移动语义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
程序运行结果为:
1 2 3 4 5 6 7 |
|
因为 std::unique_ptr
在设计时考虑了移动语义,所以其拷贝构造函数和拷贝赋值运算符都被禁用了。如果你想要转移 std::unique_ptr
管理的资源的话,就必须使用移动语义。在上面的例子中,移动语义是通过 std::move
实现的(它把res1
转换成了一个右值,因此触发了移动赋值而不是拷贝赋值)。
访问被智能指针管理的对象
std::unique_ptr
类重载了解引用运算符和成员访问运算符->,所以我们可以通过这两个运算符来返回被管理的对象。解引用运算符会返回被管理资源的引用,而operator->
返回一个指针。
记住,std::unique_ptr
并不一定总是管理着某个对象——有可能是它被初始化为空(使用默认构造函数或nullptr
字面量作为参数),也有可能是它管理的资源被转移了。 所以在使用这些操作符之前,必须首先检查 std::unique_ptr
是否有资源。因为std::unique_ptr
会被隐式转换为布尔类型,所以只需要对该指针进行条件判断就可以,当返回true
时说明它包含资源。
Here’s an example of this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
打印结果:
1 2 3 |
|
在上面的例子中,我们使用了重载的解引用运算符来获取 std::unique_ptr
res
管理的 Resource
对象,然后将其送去 std::cout
打印。
std::unique_ptr
和数组
和 std::auto_ptr
不一样的是,std::unique_ptr
足够智能,它懂得使用恰当的delete
去删除内存(普通 delete
或数组 delete
),所以,std::unique_ptr
既可以用于一般对象,也可以用于数组。
但是,相对于使用 std::unique_ptr
管理一个固定数组或C风格字符串,使用std::array
或者 std::vector
(或者 std::string
) 总是更好的选择。
最佳实践
相对于使用 std::unique_ptr
管理一个固定数组、动态数组或C风格字符串,使用std::array
或者 std::vector
(或者 std::string
) 总是更好的选择。
std::make_unique
C++14 新增了一个名为 std::make_unique()
的函数。这个模板化函数基于一个模板类型构建对象,并使用传入的参数对其进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
程序输出:
1 2 |
|
使用 std::make_unique()
是一种可选的办法,但是相对于直接创建 std::unique_ptr
,我们更推荐这种做法。不关因为使用 std::make_unique
的代码更加简洁(尤其是使用了类型推断后),它还可以解决C++参数求值顺序没有规范而引发的异常安全问题。
最佳实践
使用 std::make_unique()
来代替 std::unique_ptr
和 new
。
异常安全问题
如果你对上文提到的异常安全问题感到好奇的话,这一小结我们会详细进行介绍。
考虑下面的代码:
1 |
|
对于如何执行上述函数调用,编译器有很大的灵活性。它可以先创建新的类型T,然后调用function_that_can_throw_exception()
吗,然后创建std::unique_ptr
去管理动态分配的T。如果 function_that_can_throw_exception()
抛出了依次,那么T在被分配内存后,没有被释放,因为用于管理它的智能指针还没有被创建。显然这会导致内存泄漏。
std::make_unique()
克服了整个问题,因为对象T和 std::unique_ptr
的创建都是在 std::make_unique()
中完成的,执行顺序没有歧义。
函数返回 std::unique_ptr
std::unique_ptr
可以安全地从函数中按值返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在上面的代码中,createResource()
按值返回了 std::unique_ptr
。如果这个值没有被赋值给任何其他的对象,那么临时对象就会离开作用域,Resource
也会被清理。如果它被赋值给其他对象 (像 main()
中那样),在C++14或更早的代码中,移动语义会被用来将返回值Resource
转移给被赋值的对象(上面例子中的ptr
)。在C++17或更新的代码中,这个返回值会被省略,因此通过 std::unique_ptr
返回资源返回原始指针要安全的多!
一般情况下,你不应该将 std::unique_ptr
按指针返回(永远不要)或者按引用返回(除非你有足够的理由)。
传递 std::unique_ptr
给函数
如果你需要函数获取 std::unique_ptr
管理的资源,使用按值传递。注意,由于拷贝语义被禁用了,所以必须对实际传入的参数调用 std::move
。
译者注
需不需要使用std::move
要看传入的是左值还是右值,左值才需要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
程序输出结果为:
1 2 3 4 |
|
注意,在这个例子中,Resource
的所有权被转移给了takeOwnership()
,所以 Resource
会在 takeOwnership()
函数结束时被销毁,而不是 main()
。
但是,大多数时候,我们不会希望某个函数拥有资源的所有权。尽管你可以按引用传递std::unique_ptr
(这样一来函数就无需考虑所有权直接使用对象本身),但是这种做法只有在函数可能需要改变被管理的对象时才使用。
相反,最好是直接传递资源本身(通过指针或引用,取决于null是否是有效参数)。这样一来,函数就无需关心资源是如何被管理的了。要从std::unique_ptr
中获取原始资源指针,你可以使用get()
成员函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
程序运行结果:
1 2 3 4 |
|
std::unique_ptr
和类
当然,你也可以将 std::unique_ptr
作为一个类成员(形成组合关系关系)。这样一来,你就不需要在类的析构函数中删除该动内存,因为std::unique_ptr
会在类对象销毁时自动销毁。
但是,如果类没有被正确地销毁的话(例如,动态分配了内存但没有被释放),于是 std::unique_ptr
成员也不会被销毁,所以它管理的资源也不会被释放。
std::unique_ptr
的误用
std::unique_ptrs
有两种典型的误用,但是它们其实很容易避免。首先,不要让多个类管理同一个资源,例如:
1 2 3 |
|
尽管语法上完全正确,但是最终的结果是res1
和 res2
都会去删除资源Resource
,进而导致未定义行为。
第二,不要手动释放已经在 std::unique_ptr
管理下的资源:
1 2 3 |
|
这么做的话,std::unique_ptr
就会再次删除这个已经被释放的资源,导致未定义行为。
使用 std::make_unique()
可以避免上述两个问题。