Skip to content

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_ptrstd::share_ptrstd::weak_ptrstd::unique_ptr 是目前最多使用的智能指针类,因此我们也第一个来讲它。在之后的课程里,我们会讲 std::shared_ptrstd::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_ptrstd::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 objectsarrays

然而,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 };

这在语义上是合法的,最后的结果就是 res1res2 尝试删除资源,会引发未定义行为。

第二,不要手动删除 std::unique_ptr 管理的资源

Resource *res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;

如果你那样做,std::unique_ptr 将会尝试删除一个早就删除的资源,也会引起未定义行为。

我们注意到,std::make_unique() 刚好无意中就避免了这两种情况的发生。