C++ std::unique_ptr用法详解(附带实例)
手动处理堆内存分配和释放是 C++ 中最有争议的特性。所有分配都必须在正确作用域里有相应匹配的删除操作。例如,如果内存分配在函数里完成且需要在函数返回时释放,则这必须发生在所有返回路径上,包括函数因为异常而返回的非正常场景。
C++11 特性(如右值和移动语义)允许了智能指针的开发,这些指针可以管理内存资源并在智能指针销毁时自动释放内存资源。在本节中,我们将看到 std::unique_ptr,此智能指针拥有并管理分配在堆上的对象或一组对象,并且当智能指针在作用域外时执行清除操作。
在接下来的讲解中,我们使用以下类:
1) 使用重载构造函数创建 std::unique_ptr,通过指针来管理对象或一组对象。默认构造函数创建一个不管理任何对象的指针:
2) C++14 中可另外使用 std::make_unique() 函数模板来创建 std::unique_ptr 对象:
3) C++20 中可使用 std::make_unique_for_overwrite() 函数模板来创建 std::unique_ptr,以管理默认初始化的对象或一组对象。这些对象之后应该被确定的值所覆盖:
4) 如果默认 delete 操作符不适用于销毁托管对象或数组时,使用接收自定义 deleter 的重载构造函数:
5) 使用 std::move() 将对象所有权从一个 std::unique_ptr 转移到另一个上:
6) 访问托管对象的原始指针,如果你想要保留对象所有权就使用 get(),如果你想同时释放所有权就使用 release():
7) 使用 operator* 和 operator-> 来解引用指向托管对象的指针:
8) 如果使用 std::unique_ptr 管理一组对象,则 operator[] 可用于访问数组中的单独元素:
9) 为了检查 std::unique_ptr 是否管理对象,可使用显式操作符 bool 或检查 get()!=nullptr(即操作符 bool 所做的):
10) std::unique_ptr 对象可存储在容器中。由 make_unique() 返回的对象可直接被存储。如果你想放弃托管对象的所有权到容器里的 std::unique_ptr 对象,可使用 std::move() 将左值对象静态转换为右值对象:
当智能指针在作用域外时,为其分配一个带有 operator= 的新指针,或通过 release() 方法放弃所有权,它会执行适当的析构操作。默认情况下,操作符 delete 被用于销毁所管理的对象。然而,在构造智能指针时,用户可能想提供自定义的删除器。删除器必须是函数对象,要么是函数,要么是函数对象的左值引用,这个可调用对象必须接收类型为 unique_ptr<T, Deleter>::pointer 的单一参数。
C++14 添加了 std::make_unique() 实用函数模板来创建 std::unique_ptr。它避免了某些特定上下文中的内存泄漏,但它有一些限制:
1) 它只能用于分配数组,你不能用它来初始化数组,而 std::unique_ptr 构造函数能这么做。
以下两代码片段是等价的:
2) 它不能用于创建带有用户自定义删除器的 std::unique_ptr 对象。
如我们刚刚提到的,make_unique() 最主要的好处是帮助我们避免在抛出异常的某些上下文中发生内存泄漏。如果分配失败或它创建的对象的构造函数抛出任何异常,则 make_unique() 会抛出 std::bad_alloc。让我们考虑以下示例:
不管用 foo 的分配和构造发生了什么,也不管你使用 make_unique() 还是 std::unique_ptr 构造函数,都不会有内存泄漏。然而,代码稍微改变下则不然:
如果这个函数调用抛出异常,则使用 std::unique_ptr 构造函数创建智能指针可能会造成内存泄漏。这是因为在调用 some_other_function() 时,编译器可能先调用 foo,然后再调用 function_that_throws(),再是 std::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 对象被构造且所有权转移给了 std::unique_ptr。如果后者调用 function_that_throws() 抛出异常,那么在栈展开时会析构 std:unique_ptr 且 foo 对象会从智能指针析构函数里销毁。
C++20 添加了新函数 std::make_unique_for_overwrite()。跟 make_unique() 类似,只不过它默认初始化对象或一组对象。此函数用于不知道类型模板参数是否可被复制的通用代码中。此函数表示创建指向对象的指针,但此对象没初始化,稍后应该会被覆盖。
常量 std::unique_ptr 对象不能将托管对象或数组的所有权转移到另一个 std::unique_ptr 对象。另外,托管对象原始指针可通过 get() 或 release() 获取访问。前者只返回底层的指针,但后者如名称所示,还会释放托管对象的所有权。调用 release() 后,std::unique_ptr 对象为空,调用 get() 将返回 nullptr。
如果 Derived 继承自 Base,管理 Derived 类对象的 std::unique_ptr 可隐式地转化为管理 Base 类对象的 std::unique_ptr。当 Base 有虚析构函数(所有基类都应该有)时,此隐式转换才安全;否则,行为未定义:
C++11 特性(如右值和移动语义)允许了智能指针的开发,这些指针可以管理内存资源并在智能指针销毁时自动释放内存资源。在本节中,我们将看到 std::unique_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) << ')' << '\n'; } };
unique_ptr 类在 <memory> 头文件的 std 命名空间中可用。
C++ std::unique_ptr的使用方式
当使用 std::unique_ptr 时,以下是你需要知道的典型操作列表:1) 使用重载构造函数创建 std::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"));
2) C++14 中可另外使用 std::make_unique() 函数模板来创建 std::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");
3) C++20 中可使用 std::make_unique_for_overwrite() 函数模板来创建 std::unique_ptr,以管理默认初始化的对象或一组对象。这些对象之后应该被确定的值所覆盖:
std::unique_ptr<int> pi = std::make_unique_for_overwrite<int>(); std::unique_ptr<foo[]> pa = std::make_unique_for_overwrite<foo[]>();
4) 如果默认 delete 操作符不适用于销毁托管对象或数组时,使用接收自定义 deleter 的重载构造函数:
struct foo_deleter { void operator()(foo* pf) const { std::cout << "deleting foo..." << '\n'; delete pf; } }; std::unique_ptr<foo, foo_deleter> pf( new foo(42, 42.0, "42"), foo_deleter());
5) 使用 std::move() 将对象所有权从一个 std::unique_ptr 转移到另一个上:
auto pi = std::make_unique<int>(42); auto qi = std::move(pi); assert(pi.get() == nullptr); assert(qi.get() != nullptr);
6) 访问托管对象的原始指针,如果你想要保留对象所有权就使用 get(),如果你想同时释放所有权就使用 release():
void func(int* ptr) { if (ptr != nullptr) std::cout << *ptr << '\n'; else std::cout << "null" << '\n'; } std::unique_ptr<int> pi; func(pi.get()); // prints null pi = std::make_unique<int>(42); func(pi.get()); // prints 42
7) 使用 operator* 和 operator-> 来解引用指向托管对象的指针:
auto pi = std::make_unique<int>(42); *pi = 21; auto pf = std::make_unique<foo>(); pf->print();
8) 如果使用 std::unique_ptr 管理一组对象,则 operator[] 可用于访问数组中的单独元素:
std::unique_ptr<int[]> pa = std::make_unique<int[]>(3); for (int i = 0; i < 3; ++i) pa[i] = i + 1;
9) 为了检查 std::unique_ptr 是否管理对象,可使用显式操作符 bool 或检查 get()!=nullptr(即操作符 bool 所做的):
std::unique_ptr<int> pi(new int(42)); if(pi) std::cout << "not null" << '\n';
10) std::unique_ptr 对象可存储在容器中。由 make_unique() 返回的对象可直接被存储。如果你想放弃托管对象的所有权到容器里的 std::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));
深度剖析std::unique_ptr
std::unique_ptr 是智能指针,它通过原始指针管理分配在堆上的一个对象或一组对象。当智能指针在作用域外时,为其分配一个带有 operator= 的新指针,或通过 release() 方法放弃所有权,它会执行适当的析构操作。默认情况下,操作符 delete 被用于销毁所管理的对象。然而,在构造智能指针时,用户可能想提供自定义的删除器。删除器必须是函数对象,要么是函数,要么是函数对象的左值引用,这个可调用对象必须接收类型为 unique_ptr<T, Deleter>::pointer 的单一参数。
C++14 添加了 std::make_unique() 实用函数模板来创建 std::unique_ptr。它避免了某些特定上下文中的内存泄漏,但它有一些限制:
1) 它只能用于分配数组,你不能用它来初始化数组,而 std::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;
2) 它不能用于创建带有用户自定义删除器的 std::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() 还是 std::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() 有额外的参数:整型值。传递给这个函数的整型参数是另一个函数的返回值。
如果这个函数调用抛出异常,则使用 std::unique_ptr 构造函数创建智能指针可能会造成内存泄漏。这是因为在调用 some_other_function() 时,编译器可能先调用 foo,然后再调用 function_that_throws(),再是 std::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 对象被构造且所有权转移给了 std::unique_ptr。如果后者调用 function_that_throws() 抛出异常,那么在栈展开时会析构 std:unique_ptr 且 foo 对象会从智能指针析构函数里销毁。
C++20 添加了新函数 std::make_unique_for_overwrite()。跟 make_unique() 类似,只不过它默认初始化对象或一组对象。此函数用于不知道类型模板参数是否可被复制的通用代码中。此函数表示创建指向对象的指针,但此对象没初始化,稍后应该会被覆盖。
常量 std::unique_ptr 对象不能将托管对象或数组的所有权转移到另一个 std::unique_ptr 对象。另外,托管对象原始指针可通过 get() 或 release() 获取访问。前者只返回底层的指针,但后者如名称所示,还会释放托管对象的所有权。调用 release() 后,std::unique_ptr 对象为空,调用 get() 将返回 nullptr。
如果 Derived 继承自 Base,管理 Derived 类对象的 std::unique_ptr 可隐式地转化为管理 Base 类对象的 std::unique_ptr。当 Base 有虚析构函数(所有基类都应该有)时,此隐式转换才安全;否则,行为未定义:
struct Base { virtual ~Base() { std::cout << "~Base()" << '\n'; } }; struct Derived : public Base { virtual ~Derived() { std::cout << "~Derived()" << '\n'; } }; std::unique_ptr<Derived> pd = std::make_unique<Derived>(); std::unique_ptr<Base> pb = std::move(pd);std::unique_ptr 可存储在容器中,如 std::vector。因为任一时间只有一个 std::unique_ptr 对象拥有托管对象,所以此智能指针不能被复制到容器,它必须被移动。使用 std::move() 是可行的,static_cast 将其转换为右值引用类型。这允许将托管对象所有权转移到容器中创建的 std::unique_ptr 对象。