C++ 智能指针
我们今天的主题是简单的智能指针。智能指针主要有三种:std::unique_ptr
,std::shared_ptr
,std::weak_ptr
,第三种被广泛认为是解决一个我们将要谈到的“循环引用”的topic服务的,我们实际上主要把目光放在前两个。
std::unique_ptr
入门
_EXPORT_STD 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 _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0> constexpr unique_ptr() noexcept : _Mypair(_Zero_then_variadic_args_t{}) {} template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0> constexpr unique_ptr(nullptr_t) noexcept : _Mypair(_Zero_then_variadic_args_t{}) {}
如MSVC所见,这就是你锁定到的定义,但是有些复杂,我们回到cpp_reference来看看:
template< class T, class Deleter = std::default_delete<T> > class unique_ptr; template < class T, class Deleter > class unique_ptr<T[], Deleter>;
清晰了:实际上,这个unique_ptr就是一个负责托管资源的类。它需要一个实际的类和可能的删除器来实例化对象。举个例子:
#include <memory> #include <vld.h> // 自行寻找vld库 #include <iostream> int main() { int* leak = new int; }
在C++11之前,我们可能会写出这样的代码:你立马反应过来,有问题!内存泄漏了!
为此,我引入vld检测小工具,马上就得到了证实:
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini Visual Leak Detector Version 2.5.1 installed. WARNING: Visual Leak Detector detected memory leaks! ---------- Block 1 at 0x0000000055796C60: 4 bytes ---------- Leak Hash: 0x67B77119, Count: 1, Total 4 bytes Call Stack (TID 16744): Data: CD CD CD CD ........ ........
泄漏了一个int,符合我们的预期。现在,我们让他交给一个智能指针进行托管:
int main() { int* leak = new int; std::unique_ptr<int> no_more_leak(leak); }
现在不会泄漏了!
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini Visual Leak Detector Version 2.5.1 installed. No memory leaks detected. Visual Leak Detector is now exiting.
由此可以看见,std::unique_ptr
就是一个可以自动执行析构的内存管理类。换而言之,他会在这个变量应该结束声明周期的时候自动结束所托管资源的生命。
我们还没有结束话题!仔细看看:
template< class T, class Deleter = std::default_delete<T> > class unique_ptr;
我们还有一个Deleter没有谈到!这个Deleter就是我们用户自己定义的Deleter。毕竟,当我们的资源很简单但是需要在删除的时候做处理时,就没有必要单独封装了。举个例子
#include <memory> // std::unique_ptr #include <vld.h> #include <iostream> // std::cout #include <functional> // std::function using MyIntDeleter = std::function<void(int*)>; void deleter(int* be_del) { std::cout << "将要删除指针:> " << be_del << ", 资源值是:> " << *be_del << "\n"; delete be_del; } int main() { int* leak = new int; std::unique_ptr<int, MyIntDeleter> no_more_leak(leak, deleter); *no_more_leak = 114514; }
看看效果:
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini Visual Leak Detector Version 2.5.1 installed. 将要删除指针:> 0000029931EA1750, 资源值是:> 114514 No memory leaks detected. Visual Leak Detector is now exiting.
需要注意:
int* leak = new int; *leak = 114514; std::unique_ptr<int, MyIntDeleter> no_more_leak(leak, deleter); delete leak; // is Legal ?是不可行的,这里当我们托管了指针之后,就不要在使用原始指针去操作数据了!否则程序就会因为二次释放而崩溃!因此,一个合适的使用智能指针的方式是:
std::unique_ptr<int, MyIntDeleter> no_more_leak(new int, deleter); // 尽可能不给外界提供原始操作接口 *no_more_leak = ...;
这样行不行呢?
int* leak = new int; std::unique_ptr<int, MyIntDeleter> no_more_leak = leak; // Is Legal? *no_more_leak = 114514;
不行!std::unique_ptr
是一个独占性的资源管理器!另一个说法是:unique_ptr
不共享它的指针。它无法复制到其他 unique_ptr
,自然也就没办法无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr
。这意味着,内存资源所有权将转移到另一个 unique_ptr
,并且原始 unique_ptr
不再拥有此资源。一言以蔽之:对于一个实例,只允许有一个资源管理器在管理它!
于是,在原始指针和智能指针之间,只存在直接的赋值:
std::unique_ptr<int, MyIntDeleter> no_more_leak = leak; // Is illegal std::unique_ptr<int, MyIntDeleter> no_more_leak = std::move(leak); // Is illegal std::unique_ptr<int, MyIntDeleter> no_more_leak(leak); // legal std::unique_ptr<int, MyIntDeleter> no_more_leak(std::move(leak)); // legal, and is more obvious for readers that the function calls the move_constructor of the sources
但是,我们可以在智能指针之间便捷的使用等号进行资源管理的传递!注意到:我们的资源托管是独占的,意味着直接使用operator=和复制构造是不可能的!(=delete
)
std::unique_ptr<int, MyIntDeleter> no_more_leak(leak, deleter); std::unique_ptr<int, MyIntDeleter> no_more_leak_other = no_more_leak; // Error std::unique_ptr<int, MyIntDeleter> no_more_leak_other(no_more_leak); // Error
怎么办?那就std::move手动告知我们是移动资源即可
std::unique_ptr<int, MyIntDeleter> no_more_leak_other(std::move(no_more_leak)); std::unique_ptr<int, MyIntDeleter> no_more_leak_other = std::move(no_more_leak);
现在,我们就可以将资源的管理权进行移动了!这样我们就实现了资源管理的传递性。
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 操作的。
API
构造
我们如何产生一个unique_ptr实例呢?答案是:使用默认的构造:也就是当前的unique_ptr不托管任何对象
std::unique_ptr<int, MyIntDeleter> no_more_leak;
这就是一个例子!当前的no_more_leak不会托管任何对象。或者是为他给予一个可以被移动的类型(就比如说一个int!他当然可以被移动!)
int* leak = new int; std::unique_ptr<int, MyIntDeleter> no_more_leak(leak, deleter);
或者:使用std::move来移动另一个智能指针。
std::unique_ptr<int, MyIntDeleter> no_more_leak_other(std::move(no_more_leak)); std::unique_ptr<int, MyIntDeleter> no_more_leak_other = std::move(no_more_leak);
在C++14中(据说是标准会那帮人忘记加了(大雾.png)),可以使用make_unique来返回一个独占的智能指针
_EXPORT_STD template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0> _NODISCARD_SMART_PTR_ALLOC _CONSTEXPR23 unique_ptr<_Ty> make_unique(_Types&&... _Args) { // make a unique_ptr return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...)); }
实际上是:
template< class T, class... Args > unique_ptr<T> make_unique( Args&&... args );
也就是说:我们可以在C++14及以上使用这个函数返回智能指针了。
在另一方面,智能指针支持对一个原始数组的管理。这里我们不重复上面的陈述了,只需要这样使用就可以管理一个数组:
using MyIntDeleter = std::function<void(int[])>; void deleter(int be_del[] ) { std::cout << "将要删除指针:> " << be_del << "\n"; delete[] be_del; } int main() { std::unique_ptr<int[], MyIntDeleter> h(new int[10], deleter); for (int i = 0; i < 10; i++) h[i] = i; // Make Write }
修改
修改主要使用的是三个API:
// 释放管理器对资源的管理 pointer release() noexceptpointer release() noexcept // 替换被管理对象 void reset( pointer ptr = pointer() ) noexcept; template< class U > void reset( U ptr ) noexcept; void reset( std::nullptr_t = nullptr ) noexcept; // 交换 *this 和另一 unique_ptr 对象 other 的被管理对象和关联的删除器。 void swap( unique_ptr& other ) noexcept;
先看第一个:释放管理。release也就是这个意思。注意:他不会删除被管理的资源,单纯只是解除了管理关系,如果不知道怎么删,那就get_deleter()
删除。
using IntDeleter = std::function<void(int*)>; void make_del(int* ptr) { std::cout << "Del Int" << ptr << " :" << *ptr; delete ptr; } int main() { std::unique_ptr<int, IntDeleter> intHandle(new int, make_del); *intHandle = 110; intHandle.get_deleter()(intHandle.release()); // 一个紧凑的写法 }
下一个就是reset了:reset自如其名:就是重置管理的资源。他比release做了一个进一步的工作:就是释放原先的资源,然后再去托管新的资源
using IntDeleter = std::function<void(int*)>; void make_del(int* ptr) { std::cout << "Del Int" << ptr << " :" << *ptr << "\n"; delete ptr; } int main() { std::unique_ptr<int, IntDeleter> intHandle(new int, make_del); *intHandle = 110; intHandle.reset(new int); *intHandle = 220; }
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini Visual Leak Detector Version 2.5.1 installed. Del Int000001C5F36A27B0 :110 Del Int000001C5F36A2F30 :220 No memory leaks detected. Visual Leak Detector is now exiting.
程序首先接受了初始的资源并使用,在reset的流程中删除了旧的资源,转向托管新的资源。
最后一个是swap,说的是两个智能指针之间交换托管资源:
using IntDeleter = std::function<void(int*)>; void make_del(int* ptr) { std::cout << "Del Int" << ptr << " :" << *ptr << "\n"; delete ptr; } int main() { std::unique_ptr<int, IntDeleter> intHandle(new int, make_del); std::unique_ptr<int, IntDeleter> intHandle2(new int, make_del); *intHandle = 110; *intHandle2 = 220; std::cout << "IntHandle handles:> " << intHandle.get() << "with value:> " << *intHandle << "\n"; std::cout << "IntHandle2 handles:> " << intHandle2.get() << "with value:> " << *intHandle2 << "\n"; intHandle.swap(intHandle2); std::cout << "IntHandle handles:> " << intHandle.get() << "with value:> " << *intHandle << "\n"; std::cout << "IntHandle2 handles:> " << intHandle2.get() << "with value:> " << *intHandle2 << "\n"; }
很简单,他会交换两个智能指针所托管的资源。注意到资源地址没有改变,是在内存层面交换值而不是简单的交换地址。
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini Visual Leak Detector Version 2.5.1 installed. IntHandle handles:> 00000140C0A511F0with value:> 110 IntHandle2 handles:> 00000140C0A50EF0with value:> 220 IntHandle handles:> 00000140C0A50EF0with value:> 220 // 资源地址没变,但是值变了 IntHandle2 handles:> 00000140C0A511F0with value:> 110 Del Int00000140C0A511F0 :110 Del Int00000140C0A50EF0 :220 No memory leaks detected. Visual Leak Detector is now exiting.
资源观察器
这里,我们将会讨论的是智能指针这个管理器内部的参数是如何被获取的。有三个API:
_NODISCARD _CONSTEXPR23 _Dx& get_deleter() noexcept { return _Mypair._Get_first(); } _NODISCARD _CONSTEXPR23 const _Dx& get_deleter() const noexcept { return _Mypair._Get_first(); } _NODISCARD _CONSTEXPR23 pointer get() const noexcept { return _Mypair._Myval2; } _CONSTEXPR23 explicit operator bool() const noexcept { return static_cast<bool>(_Mypair._Myval2); }
MSVC的实现很简单,他就是使用一个Pair实现的智能指针:有趣的是,这个智能指针的内部资源就是智能指针本身在托管,很有趣的实现。
template <class, class> friend class unique_ptr; _Compressed_pair<_Dx, pointer> _Mypair;
首先我们要说的是get()
,返回指向被管理对象的指针,如果无被管理对象,则为 nullptr。另一个就是我上面他提到的get_deleter
返回删除器。注意到,对于没有安装删除器(初始化的时候没有指定删除器)的智能指针返回空。
explicit operator bool() const noexcept;
当然,他也可以返回bool:检查 *this 是否占有对象,即是否有 get() != nullptr。
剩下的实在是很好解决了:那些重载运算符同你一般的使用指针是完全一致的。
以上就是独占式的智能指针常见的API。