使用unique_ptr独占内存资源

手动处理堆内存的分配和释放是C++最具争议的功能之一。 必须在正确的作用域内将所有分配与相应的删除操作正确配对。例如,如果内存分配的作用域是一个函数,并且需要在该函数返回之前释放内存,而且必须在所有返回路径上这么做,包括由于异常而导致函数返回的异常情况。C++ 11特性,例如右值和移动语义,使得智能指针得以发展。这些指针可以管理内存资源,并在智能指针被销毁时自动释放它。在本文中,我们将了解到std::unique_ptr:一个智能指针,该指针拥有并管理在堆上分配的另一个对象或对象数组,并在智能指针超出作用域时执行释放操作。

做好准备

对于此文,您需要熟悉移动语义和std::move()转换函数。在标头的std命名空间中,可以使用unique_ptr类。

为了简单和易读,在此文中,我们将不使用标准名称std::unique_ptr和std::shared_ptr,而使用unique_ptr和shared_ptr。

在以下示例中,我们将使用随后的类:

class foo
{
  int a;
  double b;
  std::string c;
public:
  foo(int const a = 0, double const b = 0, std::string const & c = "") 
  :a(a), b(b), c(c)
  {}

  void print() const
  {
    std::cout << '(' << a << ',' << b << ',' << std::quoted(c) << ')' 
              << std::endl;
  }
};

怎么做……

以下是使用unique_ptr需要了解的典型操作的列表:

  • 使用可用的重载构造函数创建一个unique_ptr,该对象通过指针管理对象或对象数组。 默认构造函数创建一个不管理任何对象的指针:
std::unique_ptr<int>   pnull;
std::unique_ptr<int>   pi(new int(42));
std::unique_ptr<int[]> pa(new int[3]{ 1,2,3 });
std::unique_ptr<foo>   pf(new foo(42, 42.0, "42"));
  • 或者,使用C++ 14中可用的std::make_unique()函数模板来创建unique_ptr对象:
std::unique_ptr<int>   pi = std::make_unique<int>(42);
std::unique_ptr<int[]> pa = std::make_unique<int[]>(3);
std::unique_ptr<foo>   pf = std::make_unique<foo>(42, 42.0, "42");
  • 如果默认的删除操作符不适合销毁托管对象或数组,请使用带自定义删除器的重载构造函数:
struct foo_deleter
{
  void operator()(foo* pf) const
  {
    std::cout << "deleting foo..." << std::endl;
    delete pf;
  }
};

std::unique_ptr<foo, foo_deleter> pf(new foo(42, 42.0, "42"),
                                     foo_deleter());
  • 使用std::move()将对象的所有权从一个unique_ptr转移到另一个:
auto pi = std::make_unique<int>(42);
auto qi = std::move(pi);
assert(pi.get() == nullptr);
assert(qi.get() != nullptr);
  • 要访问指向托管对象的原始指针,如果要保留对象的所有权,请使用get();如果还要释放所有权,请使用release():
void func(int* ptr)
{
  if (ptr != nullptr)
    std::cout << *ptr << std::endl;
  else
    std::cout << "null" << std::endl;
}

std::unique_ptr<int> pi;
func(pi.get()); // prints null

pi = std::make_unique<int>(42);
func(pi.get()); // prints 42
  • 使用operator *和operator->解除对托管对象的指针的引用:
auto pi = std::make_unique<int>(42);
*pi = 21;

auto pf = std::make_unique<foo>();
pf->print();
  • 如果unique_ptr管理对象数组,则可以使用operator []访问数组的各个元素:
std::unique_ptr<int[]> pa = std::make_unique<int[]>(3);
for (int i = 0; i < 3; ++i)
  pa[i] = i + 1;
  • 要检查unique_ptr是否可以管理对象,请使用显式运算符bool或检查get()!= nullptr(这是布尔运算符的作用):
std::unique_ptr<int> pi(new int(42));
if (pi) std::cout << "not null" << std::endl;
  • unique_ptr对象可以存储在容器中。 make_unique()返回的对象可以直接存储。 如果要将托管对象的所有权放弃给容器中的unique_ptr对象,则可以使用std::move()将左值对象静态转换为右值对象:
std::vector<std::unique_ptr<foo>> data;
for (int i = 0; i < 5; i++)
  data.push_back(
std::make_unique<foo>(i, i, std::to_string(i)));

auto pf = std::make_unique<foo>(42, 42.0, "42");
data.push_back(std::move(pf));

这是如何起作用的……

unique_ptr是一个智能指针,它通过原始指针管理在堆上分配的对象或数组,当智能指针超出作用域,使用operator=为其分配新指针或使用 release()方法。 默认情况下,操作员删除用于处理托管对象。但是,用户可以在构造智能指针时提供自定义删除器。 此删除器必须是一个函数对象,即对函数对象或函数的左值引用,并且此可调用对象必须采用类型为unique_ptr::pointer的单个参数。

C++ 14添加了std::make_unique()实用程序函数模板来创建unique_ptr。 它避免了某些特定情况下的内存泄漏,但是它有一些限制:

  • 它只能用于分配数组。 您也不能使用它来初始化它们,这可以通过unique_ptr构造函数实现。 以下两个示例代码是等效的:
// allocate and initialize an array
std::unique_ptr<int[]> pa(new int[3]{ 1,2,3 });

// allocate and then initialize an array
std::unique_ptr<int[]> pa = std::make_unique<int[]>(3);
for (int i = 0; i < 3; ++i)
  pa[i] = i + 1;
  • 它不能用于使用用户定义的删除器创建unique_ptr对象。

正如我们刚刚提到的,make_unique()的最大优点是,它有助于避免在某些情况下引发异常时的内存泄漏。 如果分配失败或它创建的对象的构造函数抛出任何异常,make_unique()本身可以抛出std::bad_alloc。 让我们考虑以下示例:

void some_function(std::unique_ptr<foo> p)
{ /* do something */ }

some_function(std::unique_ptr<foo>(new foo()));
some_function(std::make_unique<foo>());

不管foo的分配和构造如何,无论使用make_unique()还是unique_ptr的构造函数,都不会发生内存泄漏。但是,这种情况在代码的稍有不同的版本中发生了变化:

void some_other_function(std::unique_ptr<foo> p, int const v)
{
}

int function_that_throws()
{
  throw std::runtime_error("not implemented");
}

// possible memory leak
some_other_function(std::unique_ptr<foo>(new foo), 
                    function_that_throws());

// no possible memory leak
some_other_function(std::make_unique<foo>(), 
                    function_that_throws());

在这个例子中,some_other_function()有一个额外的参数:一个整数值。传递给该函数的整数参数是另一个函数的返回值。如果此函数调用抛出异常,则使用unique_ptr的构造函数创建智能指针可能会导致内存泄漏。这样做的原因是,在调用some_other_function()时,编译器可能会先调用foo,然后再调用function_that_throws(),然后再调用unique_ptr的构造函数。如果function_that_throws()引发错误,则分配的foo将泄漏。如果调用顺序是function_that_throws(),然后是new foo()和unique_ptr的构造函数,则不会发生内存泄漏。这是因为堆栈在分配foo对象之前开始展开。但是,通过使用make_unique()函数,可以避免这种情况。这是因为唯一的调用是对make_unique()和function_that_throws()的调用。如果首先调用function_that_throws(),则将完全不分配foo对象。如果首先调用make_unique(),则将构造foo对象,并将其所有权传递给unique_ptr。如果稍后对function_that_throws()的调用确实抛出,则在取消堆栈堆栈时,unique_ptr将被破坏,并且智能对象的析构函数中的foo对象将被破坏。

常量unique_ptr对象无法将托管对象或数组的所有权转移到另一个unique_ptr对象。 另一方面,可以使用get()或release()获得对指向托管对象的原始指针的访问。 第一个方法仅返回基础指针,而后一个方法还释放托管对象的所有权,因此释放名称。 调用release()之后,unique_ptr对象将为空,而调用get()将返回nullptr。

如果Derived是从Base派生的,则可以将管理Derived类的对象的unique_ptr隐式转换为管理Base类的对象的unique_ptr。 仅当Base具有虚拟析构函数(如所有基类应具有的析构函数)时,此隐式转换才是安全的。 否则,将使用未定义的行为:

struct Base
{
  virtual ~Base() 
  {
    std::cout << "~Base()" << std::endl;
  }
};

struct Derived : public Base
{
  virtual ~Derived()
  {
    std::cout << "~Derived()" << std::endl;
  }
};

std::unique_ptr<Derived> pd = std::make_unique<Derived>();
std::unique_ptr<Base> pb = std::move(pd);

unique_ptr可以存储在容器中,例如std::vector。 因为在任何时候只有一个unique_ptr对象可以拥有托管对象,所以无法将智能指针复制到容器中。 它必须被移动。 这可以通过std::move()将static_cast执行为右值引用类型来实现。 这允许将托管对象的所有权转移到在容器中创建的unique_ptr对象。

参见

  • 使用shared_ptr共享内存资源

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据