目录
1.简介
2.使用方法
2.1.创建 unique_ptr
2.2.删除对象
2.3.转移所有权
2.4.自定义删除器
2.5.从函数返回 std::unique_ptr
2.6.将 std::unique_ptr 作为函数参数
3.适用场景
4.与原始指针的区别
5.优缺点
6.源码分析
6.1.构造函数
6.2.存储分析
6.3.默认删除器
6.4.修改器
6.5.make_unique
7.总结
1.简介
unique_ptr
是 C++11 引入的一种智能指针,用于管理动态分配的内存。它保证一个对象只会有一个 unique_ptr
指向它,从而避免内存泄漏和多重释放的问题。
std::unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr。这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。我们建议你将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr,而当构造 unique_ptr 时,可使用make_unique 函数。
std::unique_ptr 实现了独享所有权的语义。一个非空的 std::unique_ptr 总是拥有它所指向的资源。转移一个 std::unique_ptr 将会把所有权也从源指针转移给目标指针(源指针被置空)。拷贝一个 std::unique_ptr 将不被允许,因为如果你拷贝一个 std::unique_ptr ,那么拷贝结束后,这两个 std::unique_ptr 都会指向相同的资源,它们都认为自己拥有这块资源(所以都会企图释放)。因此 std::unique_ptr 是一个仅能移动(move_only)的类型。当指针析构时,它所拥有的资源也被销毁。默认情况下,资源的析构是伴随着调用 std::unique_ptr 内部的原始指针的 delete 操作的。
2.使用方法
2.1.创建 unique_ptr
你可以使用 std::make_unique
(C++14 引入)或者 new
关键字配合 std::unique_ptr
的构造函数来创建 unique_ptr
。
#include <memory>
#include <iostream>int main() {// 使用 std::make_unique (C++14 及以上)std::unique_ptr<int> ptr1 = std::make_unique<int>(10);std::cout << "ptr1: " << *ptr1 << std::endl;// 使用 new 关键字和 std::unique_ptr 构造函数std::unique_ptr<int> ptr2(new int(20));std::cout << "ptr2: " << *ptr2 << std::endl;return 0;
}
2.2.删除对象
当 unique_ptr
被销毁时,它会自动删除所管理的对象。因此,不需要手动调用 delete
。
{std::unique_ptr<int> ptr = std::make_unique<int>(30);// 当 ptr 离开作用域时,它所指向的对象会被自动删除
}
2.3.转移所有权
你可以使用 std::move
将 unique_ptr
的所有权从一个变量转移到另一个变量。
#include <memory>
#include <iostream>int main() {std::unique_ptr<int> ptr1 = std::make_unique<int>(40);std::cout << "ptr1: " << *ptr1 << std::endl;std::unique_ptr<int> ptr2 = std::move(ptr1); // 转移所有权// 此时 ptr1 不再拥有该对象,ptr2 拥有if (!ptr1) {std::cout << "ptr1 is now null" << std::endl;}std::cout << "ptr2: " << *ptr2 << std::endl;return 0;
}
2.4.自定义删除器
你可以为 unique_ptr
指定一个自定义的删除器。
#include <memory>
#include <iostream>void customDeleter(int* p) {std::cout << "Custom deleter called" << std::endl;delete p;
}int main() {std::unique_ptr<int, decltype(&customDeleter)> ptr(new int(50), customDeleter);// 当 ptr 离开作用域时,customDeleter 会被调用return 0;
}
C++智能指针的自定义销毁器(销毁策略)_c++指针销毁-CSDN博客
2.5.从函数返回 std::unique_ptr
函数可以返回 std::unique_ptr
来传递所有权。
由于返回值优化(RVO)或移动语义,这种方式是安全的。
//调用 createMyClass 函数将返回一个 std::unique_ptr<MyClass>
std::unique_ptr<MyClass> createMyClass(args...) { return std::make_unique<MyClass>(args...);
}
比如:
// 调用 createMyClass 函数将返回一个 std::unique_ptr<int>
std::unique_ptr<int> createMyClass(int val) {return std::make_unique<int>(val);
}int main() {// 使用 createMyClass 函数创建 std::unique_ptr<int>auto myPtr = createMyClass(42);// 现在 myPtr 拥有 int 对象的所有权,不需要手动释放资源// 当 myPtr 超出作用域时,int 对象将被正确销毁return 0;
}
2.6.将 std::unique_ptr
作为函数参数
你可以通过将 std::unique_ptr
作为右值引用参数传递给函数,来转移所有权。
void takeOwnership(std::unique_ptr<MyClass>&& myPtr) {
// 函数内部拥有了 myPtr 的所有权
}
auto myPtr = std::make_unique<MyClass>(args...);
//在这种情况下,myPtr 的所有权被传递给了函数 takeOwnership。
takeOwnership(std::move(myPtr));
比如:
#include <iostream>
#include <memory>
#include <utility>void takeOwnership(std::unique_ptr<int>&& myPtr) {// 函数内部拥有了 myPtr 的所有权auto ptr = std::move(myPtr);std::cout << "Inside takeOwnership: " << *ptr << std::endl;
}int main() {// 创建一个 std::unique_ptr<int>auto myPtr = std::make_unique<int>(42);// 在这种情况下,myPtr 的所有权被传递给函数 takeOwnershiptakeOwnership(std::move(myPtr));// 此时 myPtr 不再拥有所有权,它变成了空指针if (!myPtr) {std::cout << "myPtr is now empty." << std::endl;}return 0;
}
3.适用场景
- 独占资源所有权:当你需要一个对象拥有对某个资源的独占所有权时,
std::unique_ptr
是一个很好的选择。它保证了资源只能通过一个指针来访问和管理。 - 作用域退出时的资源释放:在函数或代码块结束时自动释放资源,无需手动调用
delete
。 - 传递所有权:当你需要将资源的所有权从一个对象转移到另一个对象时,可以使用
std::move
来转移std::unique_ptr
的所有权。 - 与 RAII (Resource Acquisition Is Initialization) 模式配合:使用
std::unique_ptr
可以实现 RAII 设计模式,确保资源的获取即初始化,并在资源不再需要时自动释放。 - 动态数组管理:从 C++14 开始,
std::unique_ptr
可以用来管理动态数组,通过在模板参数中使用方括号语法。 - 自定义删除器:可以提供自定义删除器给
std::unique_ptr
,这在管理非内存资源(如文件句柄、网络连接等)时非常有用。
4.与原始指针的区别
与原始指针相比,std::unique_ptr
提供了以下优势:
- 自动资源管理:
std::unique_ptr
确保对象在智能指针超出作用域时自动释放,避免内存泄漏。 - 独占所有权:确保同一时间只有一个
std::unique_ptr
拥有某个对象,避免多重释放问题。 - 不可复制:防止不必要的复制操作,确保资源管理的唯一性。
- 自定义删除器:允许在对象销毁时执行特定的清理操作。
5.优缺点
优点
- 自动资源管理:
std::unique_ptr
通过其析构函数自动管理资源的释放,避免了忘记释放资源导致的内存泄漏。 - 异常安全:即使发生异常,
std::unique_ptr
也可以保证资源的正确释放。 - 轻量级:
std::unique_ptr
通常与原始指针具有相同的大小和性能,因为它不需要支持引用计数。 - 可定制性:可以通过提供自定义删除器来扩展
std::unique_ptr
的行为。
缺点
- 不支持共享所有权:
std::unique_ptr
不允许多个指针共享对同一资源的所有权。如果需要共享所有权,应使用std::shared_ptr
。 - 不支持循环引用:由于
std::unique_ptr
不是为共享所有权设计的,它无法处理循环引用问题。在循环引用的场景中,应使用std::shared_ptr
和std::weak_ptr
组合来管理资源。 - 移动语义限制:
std::unique_ptr
只支持移动语义,不能被复制。这意味着你需要使用std::move
来传递所有权,这可能在某些情况下不够直观。
6.源码分析
以VS2019为例,一步步分析它的实现原理。
6.1.构造函数
unique_ptr有两个版本,第一个版本是默认的管理单个对象的版本,第二个版本是通过偏特化实现的管理动态分配的数组的版本。在cppreference网站上这个模板类的声明是这个样子:
在vs2019中它是这个样子:
两个版本的unique_ptr都是第一个模板参数是持有指针对应的类型(这里已经去除了单个对象和数组对象的区别,那么如何在析构函数中调用正确的delete呢?往后看),第二个模板参数是删除器的类型。
template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr { // non-copyable pointer to an object
public:using pointer = typename _Get_deleter_pointer_type<_Ty, remove_reference_t<_Dx>>::type;using element_type = _Ty;using deleter_type = _Dx;。。。
};template <class _Ty, class _Dx>
class unique_ptr<_Ty[], _Dx> { // non-copyable pointer to an array object
public:using pointer = typename _Get_deleter_pointer_type<_Ty, remove_reference_t<_Dx>>::type;using element_type = _Ty;using deleter_type = _Dx;。。。
};
- 标准中要求的pointer成员类型由_Get_deleter_pointer_type类导出。首先通过类型萃取得到去除引用的删除器类型:_Dx_noref。可以看到,pointer类型由_Get_deleter_pointer_type这个模板类导出。
// STRUCT TEMPLATE _Get_deleter_pointer_type
template <class _Ty, class _Dx_noref, class = void>
struct _Get_deleter_pointer_type { // provide fallbackusing type = _Ty*;
};template <class _Ty, class _Dx_noref>
struct _Get_deleter_pointer_type<_Ty, _Dx_noref, void_t<typename _Dx_noref::pointer>> { // get _Dx_noref::pointerusing type = typename _Dx_noref::pointer;
};
C++17之std::void_t_c++17 判断数据类型是void*-CSDN博客
这个类通过特化实现了这样一个逻辑:如果_Dx_noref类型中有pointer的成员类型,则将其作为type类型导出,否则将_Ty* 作为type类型导出。导出的就是unique_ptr中的pointer类型了。也就是说,我们可以通过对删除器中添加一个pointer的成员类型来定制化unique_ptr。
对于pointer的类型,标准中有如下说明:
如果std::remove_reference<Deleter>::type::pointer存在的话,就是这个类型,否则就是T*类型,这与源代码是相一致的。但是还有一个限制:必须要符合NullablePointer。我们在标准中继续查看NullablePointer的含义。NullablePointer类型是指 该类型的对象能够与std::nullptr_t类型对象进行比较的类似于指针的对象。
std::unique_ptr的构造函数如下:
//1
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
constexpr unique_ptr() noexcept : _Mypair(_Zero_then_variadic_args_t{}) {}//2
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
constexpr unique_ptr(nullptr_t) noexcept : _Mypair(_Zero_then_variadic_args_t{}) {}//3
template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{}, _Ptr) {}//4
template <class _Dx2 = _Dx, enable_if_t<is_constructible_v<_Dx2, const _Dx2&>, int> = 0>unique_ptr(pointer _Ptr, const _Dx& _Dt) noexcept : _Mypair(_One_then_variadic_args_t{}, _Dt, _Ptr) {}//5
template <class _Dx2 = _Dx,enable_if_t<conjunction_v<negation<is_reference<_Dx2>>, is_constructible<_Dx2, _Dx2>>, int> = 0>unique_ptr(pointer _Ptr, _Dx&& _Dt) noexcept : _Mypair(_One_then_variadic_args_t{}, _STD move(_Dt), _Ptr) {}//6
template <class _Dx2 = _Dx,enable_if_t<conjunction_v<is_reference<_Dx2>, is_constructible<_Dx2, remove_reference_t<_Dx2>>>, int> = 0>unique_ptr(pointer, remove_reference_t<_Dx>&&) = delete;
总结分为两类:1)_Zero_then_variadic_args_t 类型,_Dx2的初始化使用默认删除器,上面代码中的1、2和3两个构造函数使用了此。
2)_One_then_variadic_args_t 类型, _Dx2使用外部传入的删除器,上面代码 中的4和5两个构造函数使用了此。
C++ 的 Tag Dispatching(标签派发) 惯用法_c++ tag dispatch-CSDN博客
值得注意的是,我们看到std::unique_ptr的构造函数限制了const std::unique_ptr复制构造:
//1
unique_ptr(const unique_ptr&) = delete;//2
template <class _Dx2 = _Dx, enable_if_t<is_move_constructible_v<_Dx2>, int> = 0>unique_ptr(unique_ptr&& _Right) noexcept: _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx>(_Right.get_deleter()), _Right.release()) {}//3
template <class _Ty2, class _Dx2,enable_if_t<conjunction_v<negation<is_array<_Ty2>>, is_convertible<typename unique_ptr<_Ty2, _Dx2>::pointer, pointer>,conditional_t<is_reference_v<_Dx>, is_same<_Dx2, _Dx>, is_convertible<_Dx2, _Dx>>>,int> = 0>unique_ptr(unique_ptr<_Ty2, _Dx2>&& _Right) noexcept: _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx2>(_Right.get_deleter()), _Right.release()) {}
如果这样写:
会产生如下的一个编译错误:
- 这个错误是说,尝试去调用unique_ptr的复制构造函数,而这个函数如上所说已经被我们删除。在C++中,具有const属性的rvalue expression并不能被右值引用所捕获,其只能被常量左值引用所捕获。在第二个版本中,std::move(ptr)返回的值是一个具有const属性的xvalue的值,其只能被复制构造函数所捕获。
- unique_ptr可以被一个不完整类型T构造(其第一个模板参数T可以是不完整类型),如果使用默认的删除器的话,在删除器被调用的点处T类型必须要是完整的,这些点包括析构函数、移动赋值函数、reset成员函数(而对应的shared_ptr不能够被一个不完整类型的指针构造,但是可以在T为不完整类型处释放)。
- 如果T是某个基类B的派生类,那么std::unique_ptr<T>将会被隐式的转化为std::unique_ptr<B>,而std::unique_ptr<B>的默认删除器将会按照B类型来释放指针,如果B的析构函数不是virtual的话将会导致未定义行为。注意std::shared_ptr表现不同,即使基类的析构函数不是virtual的,其也可以调用正确的析构函数。
6.2.存储分析
unique_ptr模版中有一变量:
这里又引入一个新的模板类:_Compressed_pair。这个模板类其实就存储了两个对象,第一个是删除器类型的对象,第二个是我们存储的指针类型的对象。既然如此简单我们为什么要专门再用一个模板类来做一层抽象呢?这里其实对当删除器类型是一个空类的情况做了一个优化。例如默认的删除器,其实我们只需要调用它的某个成员函数即可,完全不必要存储任何成员变量,但是c++中的空类(即没有数据成员的类)会占据一个内存字节(这是为了让对象的实例能相互识别,为了让每个实例在内存中都有独一无二的地址),当这个空类作为成员函数在另外一个类中存在时,由于内存对齐的原因会消耗更多的内存地址。看下面这个例子:
- A是一个空类,其作为B类中第一个数据成员,B中第二个数据成员为int类型变量。由于int类型变量占据4个字节,由于内存对齐的原因,a1和a2之间还有3个字节的空白。Sizeof(B)返回的结果是8个字节。
- _Compressed_pair模板类就是针对这个现象做了一个优化,当删除器类型是空类时,通过继承的方式获得其成员函数和成员类型等信息而不用额外的内存消耗。
template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && !is_final_v<_Ty1>>
class _Compressed_pair final : private _Ty1 { // store a pair of values, deriving from empty first
public:_Ty2 _Myval2;using _Mybase = _Ty1; // for visualizationtemplate <class... _Other2>constexpr explicit _Compressed_pair(_Zero_then_variadic_args_t, _Other2&&... _Val2) noexcept(conjunction_v<is_nothrow_default_constructible<_Ty1>, is_nothrow_constructible<_Ty2, _Other2...>>): _Ty1(), _Myval2(_STD forward<_Other2>(_Val2)...) {}template <class _Other1, class... _Other2>constexpr _Compressed_pair(_One_then_variadic_args_t, _Other1&& _Val1, _Other2&&... _Val2) noexcept(conjunction_v<is_nothrow_constructible<_Ty1, _Other1>, is_nothrow_constructible<_Ty2, _Other2...>>): _Ty1(_STD forward<_Other1>(_Val1)), _Myval2(_STD forward<_Other2>(_Val2)...) {}constexpr _Ty1& _Get_first() noexcept {return *this;}constexpr const _Ty1& _Get_first() const noexcept {return *this;}
};
- _Compressed_pair模板类有三个模板参数,第一个是删除器类型,第二个是存储的指针对应的类型,第三个是为了做特化的类型bool。如果删除器类型是空类型并且是一个final类型(不能被继承),就使用默认的版本,即私有继承删除器类型,仅仅有一个_Ty2类型的成员变量。当第三个模板参数求值为false时,采用其特化版本:
template <class _Ty1, class _Ty2>
class _Compressed_pair<_Ty1, _Ty2, false> final { // store a pair of values, not deriving from first
public:_Ty1 _Myval1;_Ty2 _Myval2;template <class... _Other2>constexpr explicit _Compressed_pair(_Zero_then_variadic_args_t, _Other2&&... _Val2) noexcept(conjunction_v<is_nothrow_default_constructible<_Ty1>, is_nothrow_constructible<_Ty2, _Other2...>>): _Myval1(), _Myval2(_STD forward<_Other2>(_Val2)...) {}template <class _Other1, class... _Other2>constexpr _Compressed_pair(_One_then_variadic_args_t, _Other1&& _Val1, _Other2&&... _Val2) noexcept(conjunction_v<is_nothrow_constructible<_Ty1, _Other1>, is_nothrow_constructible<_Ty2, _Other2...>>): _Myval1(_STD forward<_Other1>(_Val1)), _Myval2(_STD forward<_Other2>(_Val2)...) {}constexpr _Ty1& _Get_first() noexcept {return _Myval1;}constexpr const _Ty1& _Get_first() const noexcept {return _Myval1;}
};
- 可以看到,当删除器类型不是空类型时,则按照常规处理方法保持两个成员变量即可。
C++惯用法之空基类优化-CSDN博客
6.3.默认删除器
- default_delete同样根据_Ty类型是单一类型还是数组类型做了特化处理。
// STRUCT TEMPLATE default_delete
template <class _Ty>
struct default_delete { // default deleter for unique_ptrconstexpr default_delete() noexcept = default;template <class _Ty2, enable_if_t<is_convertible_v<_Ty2*, _Ty*>, int> = 0>default_delete(const default_delete<_Ty2>&) noexcept {}void operator()(_Ty* _Ptr) const noexcept /* strengthened */ { // delete a pointerstatic_assert(0 < sizeof(_Ty), "can't delete an incomplete type");delete _Ptr;}
};template <class _Ty>
struct default_delete<_Ty[]> { // default deleter for unique_ptr to array of unknown sizeconstexpr default_delete() noexcept = default;template <class _Uty, enable_if_t<is_convertible_v<_Uty (*)[], _Ty (*)[]>, int> = 0>default_delete(const default_delete<_Uty[]>&) noexcept {}template <class _Uty, enable_if_t<is_convertible_v<_Uty (*)[], _Ty (*)[]>, int> = 0>void operator()(_Uty* _Ptr) const noexcept /* strengthened */ { // delete a pointerstatic_assert(0 < sizeof(_Uty), "can't delete an incomplete type");delete[] _Ptr;}
};
template <class _Uty, enable_if_t<is_convertible_v<_Uty (*)[], _Ty (*)[]>, int> = 0>void operator()(_Uty* _Ptr) const noexcept /* strengthened */ { // delete a pointerstatic_assert(0 < sizeof(_Uty), "can't delete an incomplete type");delete[] _Ptr;}
- default_delete有一个构造函数,该函数是一个模板函数,如果_Ty2 是_Ty1 的子类,则可以用_Ty2类型的删除器删除_Ty1类型的指针。
- 对()运算符的重载则是删除时真正调用的函数了,可以看到在单一类型的版本中调用了delete,而在数组类型的版本中调用了delete[]。
C++不完整类型(Incomplete Type)的检测与避免_imcomplete和incomplete-CSDN博客
6.4.修改器
pointer release() noexcept {return _STD exchange(_Mypair._Myval2, nullptr);
}
release从字面意思讲就是unique_ptr把自己管理的指针脱离自身,类似detach。这其中使用一个非常经典的系统函数std::exchange,用空指针去交换掉实际的值。
C++惯用法之copy and swap_c++ copy and swap-CSDN博客
//[1] 单对象版本
void reset(pointer _Ptr = nullptr) noexcept {pointer _Old = _STD exchange(_Mypair._Myval2, _Ptr);if (_Old) {_Mypair._Get_first()(_Old);}
}//[2] 数组版本
void reset(nullptr_t = nullptr) noexcept {reset(pointer());
}template <class _Uty, class = _Enable_ctor_reset<_Uty, false_type>>
void reset(_Uty _Ptr) noexcept {pointer _Old = _STD exchange(_Mypair._Myval2, _Ptr);if (_Old) {_Mypair._Get_first()(_Old);}
}
从上面的代码看到,reset是先把自己管理的指针保存到一个临时变量,然后调用删除器再把这个临时变量的内存释放掉。
//单对象版本_NODISCARD add_lvalue_reference_t<_Ty> operator*() const noexcept /* strengthened */ {return *_Mypair._Myval2;}_NODISCARD pointer operator->() const noexcept {return _Mypair._Myval2;}//数组版本
_NODISCARD _Ty& operator[](size_t _Idx) const noexcept /* strengthened */ {return _Mypair._Myval2[_Idx];
}
单对象版本可以通过重载符号*和->实现对象的访问,数组版本通过重载[] 来实现单个对象的访问。
6.5.make_unique
// FUNCTION TEMPLATE make_unique
//[1] 单对象版本
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0>
_NODISCARD unique_ptr<_Ty> make_unique(_Types&&... _Args) { // make a unique_ptrreturn unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...));
}//[2] 数组版本
template <class _Ty, enable_if_t<is_array_v<_Ty> && extent_v<_Ty> == 0, int> = 0>
_NODISCARD unique_ptr<_Ty> make_unique(const size_t _Size) { // make a unique_ptrusing _Elem = remove_extent_t<_Ty>;return unique_ptr<_Ty>(new _Elem[_Size]());
}//[3] 数组版本
template <class _Ty, class... _Types, enable_if_t<extent_v<_Ty> != 0, int> = 0>
void make_unique(_Types&&...) = delete;
源代码中有3个版本,通过is_array_v判断出是非单对象版本;通过extent_v判断数组的维度,0维度的才能满足make_unique产生对象的要求,于是不能满足要求的数组就禁止调用make_unique。extent_v可以通过几个例子来理解它:
#include <iostream>
#include <type_traits>int main()
{std::cout << std::extent<int[3]>::value << '\n'; // < 默认维度为 0 std::cout << std::extent<int[3][4], 0>::value << '\n';std::cout << std::extent<int[3][4], 1>::value << '\n';std::cout << std::extent<int[3][4], 2>::value << '\n';std::cout << std::extent<int[]>::value << '\n';const auto ext = std::extent<int[9]>{};std::cout << ext << '\n'; // < 隐式转换到 std::size_tconst int ints[] = {1,2,3,4};std::cout << std::extent<decltype(ints)>::value << '\n'; // < 数组大小
}输出:
3
3
4
0
0
9
4
7.总结
unique_ptr
不能复制,只能移动(通过std::move
)。- 你可以使用
nullptr
来重置unique_ptr
,使其不再拥有任何对象。 unique_ptr
通常比裸指针更安全,因为它会自动管理资源。
通过合理使用 std::unique_ptr
,可以大大简化内存管理,减少内存泄漏和悬挂指针的风险。
参考资料:
https://zh.cppreference.com/w/cpp/memory/unique_ptr