15.5 std::unique_ptr¶
By Alex on March 15th, 2017 | last modified by nascardriver on July 12th, 2020
翻译by dashjay 2020.07.18
在本章的开始,我们讨论了在一些条件下,如何使用指针会引起bug和内存泄露。例如,这就可能会发生在,当一个函数提前退出,或抛出异常,或者指针没有被合理的删除时。
#include <iostream>
void someFunction()
{
auto *ptr{ new Resource() };
int x{};
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
throw 0; // the function returns early, and ptr won’t be deleted!
// do stuff with ptr here
delete ptr;
}
既然我们已经知道了 移动语义 的操作,我们可以回到 智能指针 的话题了。提醒一下,智能指针 是一个管理着 动态分配资源 的类,并且保证动态分配的对象在合适的时间被合适的 释放,(通常是智能指针离开作用域时)。
因为这样,智能指针本身应该从不被动态分配(否则,如果他们自己本身就可能有没被合适地释放的风险,这意味着它持有的对象将不会被释放,造成内存泄露)。通过始终只在栈区创建智能指针的方式(作为局部变量或者是其他类的组成),我们保证那样的智能指针将会合理地离开作用域,当函数结束或者对象离开作用域时时,能确保智能指针 持有的对象 被合适的释放。
C++11 标准库附带四种指针类型:std::auto_ptr
(不应该使用————在C++17中已经被移除),std::unique_ptr
,std::share_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>
头中。
让我们来看一下这个简单的智能指针的例子:
# include <iostream>
# include <memory> // for std::unique_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// allocate a Resource object and have it owned by std::unique_ptr
std::unique_ptr<Resource> res{ new Resource() };
return 0;
} // res goes out of scope here, and the allocated Resource is destroyed
因为 std::unique_ptr
在栈区上被分配,它最终会离开作用域,并且会自动删除它管理的资源。
不像 std::auto_ptr
,std::unique_ptr
更适合用来实现移动语义。
# include <iostream>
# include <memory> // for std::unique_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
std::unique_ptr<Resource> res1{ new Resource{} }; // Resource created here
std::unique_ptr<Resource> res2{}; // Start as nullptr
std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");
// res2 = res1; // Won't compile: copy assignment is disabled
res2 = std::move(res1); // res2 assumes ownership, res1 is set to null
std::cout << "Ownership transferred\n";
std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");
return 0;
} // Resource destroyed here when res2 goes out of scope
这个例子会打印:
Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed
因为 std__unique_ptr
在设计时考虑了移动语义,拷贝初始化 和 拷贝赋值 都被禁用了。如果你我相要转移一个被 std::unique_ptr
管理的内容,你必须使用 移动语义。在以上的程序中,我们使用 std::move
(将 res1 转化成一个右值,可以触发一个移动赋值,而不是拷贝赋值) 来完成。
访问被管理的对象¶
std::unique_ptr
重载了 *操作符
和 ->操作符
可以用来返回所管理的资源,operator*
返回一个资源的引用,operator->
返回一个指针。
记住 std::unique_ptr
可能不总是管理一个资源,它也可能被创造为空(使用默认的构造函数并且传入一个空指针作为参数),或者因为它管理的资源被移动到另一个 std::unique_ptr
. 因此在我们使用这些指针之前,我们应该检查 std::unique_ptr
是否管理一个资源。幸运的是,这很简单:std::unique_ptr
有一个可以转化成一个bool值的函数,如果该unique_ptr
管理一份资源则返回 true。
这是一个简单的例子:
# include <iostream>
# include <memory> // for std::unique_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
friend std::ostream& operator<<(std::ostream& out, const Resource &res)
{
out << "I am a resource\n";
return out;
}
};
int main()
{
std::unique_ptr<Resource> res{ new Resource{} };
if (res) // use implicit cast to bool to ensure res contains a Resource
std::cout << *res << '\n'; // print the Resource that res is owning
return 0;
}
输出:
Resource acquired
I am a resource
Resource destroyed
在上方的程序中,我们使用重载的operator*
来获得 unique_ptr
持有的资源,然后送到 std::cout
打印。
std::unique_ptr 和数组¶
不像 std::auto_ptr
那样,std::unique_ptr
是足够智能的知道是否使用标量删除(scalar delete)或数组删除(array delete[]),因此 std::unique_ptr
可以同时搭配 scalar objects
和 arrays
。
然而,std::array
或者 std::vector
(或 std::string
) 总会是一个更好的选择,相比起用固定数组(fixed array
),C类型字符串(C-style string
) 或 动态数组(dynamic array
)搭配 std::unique_ptr
。
规则:更加偏好使用 std::array
, std::vector
或者 std::string
而不是智能指针管理的定长数组,动态数组,或者C风格字符串。
std::make_unique¶
C++14 带有一个附加的函数叫做 std::make_unique()
。此模板化函数构造模板类型的对象,并使用传递给函数的参数对其进行初始化。
# include <memory> // for std::unique_ptr and std::make_unique
# include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
Fraction(int numerator = 0, int denominator = 1) :
m_numerator{ numerator }, m_denominator{ denominator }
{
}
friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
};
int main()
{
// Create a single dynamically allocated Fraction with numerator 3 and denominator 5
// We can also use automatic type deduction to good effect here
auto f1{ std::make_unique<Fraction>(3, 5) };
std::cout << *f1 << '\n';
// Create a dynamically allocated array of Fractions of length 4
auto f2{ std::make_unique<Fraction[]>(4) };
std::cout << f2[0] << '\n';
return 0;
}
输出
3/5
0/1
std::make_unique
的使用是一个可选项,但是是非常推荐的。这是因为使用 std::make_unique
很简单,并且它也需要写更少的代码。(当使用自动类型判断时),更进一步来说,它也解决了一个异常安全问题,这会引起C++... (Furthermore it resolves an exception safety issue that can result from C++ leaving the order of evaluation for function arguments unspecified.)
规定: 使用 std::make_unique()
而不是自己手动创建。
异常安全问题细节¶
给那些想知道上方提到的是什么 “异常安全问题”的人一些解释,这里有一个关于此问题的描述
思考一个像这样的表达式:
some_function(std::unique_ptr<T>(new T), function_that_can_throw_exception());
编译器在如何处理这个调用方面有很大的活动空间。它可能创建一个新的 T,然后再调用 function_that_can_throw_exception()
,然后创建std::unique_ptr
管理这动态分配的T。如果 function_that_can_throw_exception()
抛出一个异常,然后 T
被分配但是没有被释放,因为用来释放该资源的智能指针还没有被创建,这引起了 T
的泄露。
std::make_unique()
不会遇到这个问题,因为对象 T 的创建和创建 std::unique_ptr
都发生在 std::make_unique()
函数,不存在执行顺序模糊的问题。
从函数返回 std::unique_ptr
¶
std::unique_ptr
可以被从一个函数安全的返回:
std::unique_ptr<Resource> createResource()
{
return std::make_unique<Resource>();
}
int main()
{
auto ptr{ createResource() };
// do whatever
return 0;
}
在上方的代码中,createResource()
通过值返回一个 std::unique_ptr
。如果这值不会被赋值给任何对象,返回的临时值将会离开作用域并且被清理。如果它被赋值(像main中展示的那样),在C++14或者更早,移动语义将会被使用来从返回值转移资源到即将赋值的对象(上方例子中的ptr),在C++17或者更新,返回将会被省略,这使得相比返回原指针,返回一个 unique_ptr
的资源更加安全。
总体来讲,你应该从不通过指针或引用返回 std::unique_ptr
(除非你有特殊的原因来这样做)。
向函数传入 std::unique_ptr
¶
如果你想要函数来获得指针内容的所有权,通过值传一个 std::unique_ptr
。注意,因为拷贝语义已经被禁用,你将会使用 std::move
来传值进入函数。
# include <memory> // for std::unique_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
friend std::ostream& operator<<(std::ostream& out, const Resource &res)
{
out << "I am a resource\n";
return out;
}
};
void takeOwnership(std::unique_ptr<Resource> res)
{
if (res)
std::cout << *res << '\n';
} // the Resource is destroyed here
int main()
{
auto ptr{ std::make_unique<Resource>() };
// takeOwnership(ptr); // This doesn't work, need to use move semantics
takeOwnership(std::move(ptr)); // ok: use move semantics
std::cout << "Ending program\n";
return 0;
}
输出:
Resource acquired
I am a resource
Resource destroyed
Ending program
注意在这个例子里,资源的所有权被传给了 takeOwnership()
,因此资源将会被销毁在 takeOwnership()
函数结束时,而不是 main()
然而,在大多数情况下,你不想让函数得到资源的所有权。虽然你可以传入一个 std::unique_ptr
的引用(这允许函数使用对象,而不得到所有权),你应该仅仅在 调用函数会修改或者改变持其管理的对象的情况下使用。
相反,更好的方式是传入一个资源本身(通过指针或者引用,根据null是否是一个合法的参数),这允许函数保持调用者管理资源。为了从一个 std::unique_ptr
得到原来的资源指针,你可以使用 get()
成员函数:
# include <memory> // for std::unique_ptr
# include <iostream>
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
friend std::ostream& operator<<(std::ostream& out, const Resource &res)
{
out << "I am a resource\n";
return out;
}
};
// The function only uses the resource, so we'll accept a pointer to the resource, not a reference to the whole std::unique_ptr<Resource>
void useResource(Resource *res)
{
if (res)
std::cout << *res << '\n';
}
int main()
{
auto ptr{ std::make_unique<Resource>() };
useResource(ptr.get()); // note: get() used here to get a pointer to the Resource
std::cout << "Ending program\n";
return 0;
} // The Resource is destroyed here
输出
Resource acquired
I am a resource
Ending program
Resource destroyed
std::unique_ptr
和类型¶
当然,你可以使用 std::unique_ptr
作为你的类型中的组成部分,以这个方式,你将不用担心确保你的类型的析构函数释放动态内存了,因为 std::uniqut_ptr
将会自动的销毁,当类型对象被销毁时。然而,记住如果你的类型是动态分配的,那么对象本身就有风险不能被正确的释放,在这种情况下,智能指针也不能帮你。
std::unique_ptr
的误用¶
这有两个简单的例子,误用 std::unique_ptr
,他们都是非常容易避免的。
首先,不要让多个指针管理同样的资源,例如:
Resource *res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
std::unique_ptr<Resource> res2{ res };
这在语义上是合法的,最后的结果就是 res1
和 res2
尝试删除资源,会引发未定义行为。
第二,不要手动删除 std::unique_ptr
管理的资源
Resource *res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;
如果你那样做,std::unique_ptr
将会尝试删除一个早就删除的资源,也会引起未定义行为。
我们注意到,std::make_unique()
刚好无意中就避免了这两种情况的发生。