相关内容:
什么是智能指针?
指针赋予了C++
非常强大的功能,但同时也带来了很多问题。正如C
语言中使用malloc
函数动态申请的内存都需要手动调用free()
函数来释放,以前的C++
中通过new/new[]
关键字申请的内存也需要手动使用delete/delete[]
关键字来释放,这导致了很多难以察觉的内存泄露。
解决这个问题的一个办法是自动管理内存,智能指针就是为此而设计的。自C++11
起,标准库提供三种类型的智能指针:shared_ptr
、weak_ptr
以及unique_ptr
(早期的auto_ptr
因存在瑕疵并完全可以被unique_ptr
所替代在C++11
中已经被移除,本文不涉及auto_ptr
,有兴趣参考cppreference: auto_ptr)。其中,weak_ptr
主要用来辅助shared_ptr
进行使用。
类别 | 功能 |
---|---|
auto_ptr |
C++ 早期的智能指针,不能共享对象,不能用于指向数组,存在浅拷贝的问题 |
shared_ptr |
可共享指针对象,可以赋值给shared_ptr 或weak_ptr ,指针所指对象在所有相关联的shared_ptr 生命周期结束时结束,是强引用 |
unique_ptr |
独占指针对象,并保证指针所指对象生命周期一致 |
weak_ptr |
它不能决定对象的生命周期,引用所指对象时,需要lock() 成shared_ptr 才能使用 |
shared_ptr
的使用:
shared_ptr
实现共享式拥有的概念,即多个shared_ptr
智能指针可以指向同一块动态分配的内存。shared_ptr
内部通过维护引用计数来保证当指向一块内存的最后一个shared_ptr
引用销毁(离开作用域、被赋值nullptr
等)时自动释放内存,这就完成了“当对象不再被使用时就销毁”的语义。以下例子展示shared_ptr
的使用:
1 |
|
类似于vector
,智能指针也是模板,所以在构造时必须提供其指向的类型作为模板参数。代码首先创建指向string
的shared_ptr
,名为ps1
,这里调用了shared_ptr
接收一个指针类型作为参数的构造函数,构造一个指向该类型的智能指针,注意shared_ptr
接收单一指针作为参数的构造函数是explicit
的,也就是说不可以进行隐式类型转换,但是列表初始化语法是被接受的:
1 | shared_ptr<string> p = new string("test"); // Error |
也可以使用便捷函数:
1 | auto p = std::make_shared<string>("test"); |
这个函数同时分配shared_ptr
的控制区块内存和所指向的动态内存,所以比以上构造方式速度快,也比较安全。C++20
中额外增加了几种make_shared
构造方式,例如:
1 | template<class T> |
用于方便的创建指向数组的智能指针,详细信息参考cppreference: std::make_shared, std::make_shared_default_init。 接下来,use_count()
函数返回智能指针所指向内存的引用计数,这里只有一个ps1
指向此刚分配的内存,所以结果是1;下两行,创建一个名为ps2
的shared_ptr
并将ps1
赋值给它,同样,调用拷贝构造函数来创建一个名为ps3
的shared_ptr
。这里需要注意的是,无论赋值还是拷贝操作,都不会有指针所指向的动态内存的拷贝,内存永远只有一份,智能指针的赋值/拷贝操作只是增加了指向这块内存的指针数量(类比于普通指针的赋值操作),所以接下来use_count
的结果是3;再下一行,通过将ps2
设置为空指针,减少了指向内存的指针数量,所以目前只有ps1
和ps3
仍然指向分配的内存,因此内存的引用计数是2,已经被置为空指针的ps2
计数为0。可以像普通指针一样使用操作符*
和->
访问智能指针所指向的对象,通过get()
方法获取普通指针,c++17
也支持对其使用[]
。
1 | cout << *ps1 << endl; // 输出: str1 |
此外,shared_ptr
支持向bool
的隐式转换,用来判断指针是否为空:
1 | if (ps2) |
自定义删除器:
删除器用来做智能指针引用计数降为0后的收尾工作,被自动调用。在C++11
中,shared_ptr
提供的默认删除器调用的是delete
,而在c++17
中情况发生了变化:如果智能指针模板类型T
是数组类型,删除器调用delete[]
,否则调用delete
。但是你也可以通过传入自定义删除器作为参数来完成手动的收尾工作,例如:
1 | shared_ptr<int> pi(new int[5], |
这里传入一个lambda
表达式作为删除器,在回收内存同时输出delete OK
。自定义删除器使得删除器不仅仅可以用来回收内存,也可以用来回收系统资源等:
1 | class FileDeleter |
这里自定义一个类作为删除器,通过实现它的operator()
使得该类的对象可以直接作为shared_ptr
的第二个参数。自定义删除器完成文件描述符的关闭和临时文件的清除工作。
自定义分配器:
类似于自定义删除器,你也可以通过指定shared_ptr
构造函数的第三个参数来指定内存分配器。这样做可以将内存分配与对象构造分离开,从而提供更大的自由性。此外,c++17
额外提供了pmr
来进行内存的分配管理,这里不多做介绍。
其他成员函数:
没有参数的reset()
等价于直接将shared_ptr
置空,也可以使用reset
接收一个原始指针参数(原始指针不能已经被其他智能指针所占有)来替换shared_ptr
所管理的对象,可选提供删除器和分配器,如果被替换前指向的对象再无其他指向,则被释放内存;也可以用swap
交换两个shared_ptr
的指向。
1 | shared_ptr<string> p1(new string("hello")); |
此外,c++11
中支持使用unique
来检查当前对象是否是仅有的shared_ptr
实例,等价于use_count()==1
,但是这个成员函数因为多线程环境下存在的问题在c++20
中已经被移除。
weak_ptr
的使用:
shared_ptr
缺陷:
使用shared_ptr
可以避免手动管理内存,但是也存在一个重要的缺陷:循环引用问题,例如:
1 | struct Node |
这里二叉树的节点不仅指向它的两个儿子,也指向它的父亲节点,这样就存在环形引用的问题,当parent
, left_child
, right_child
被销毁时,它们管理的内存没有任何的外部指向,但是却存在着相互的指向(parent
指向两个child
,同时两个child
也指向parent
),导致内存泄露问题。
基础部分:
解决环形引用问题的方法是引入weak_ptr
,作为shared_ptr
的拓展,weak_ptr
允许“共享但不拥有”对象。也就是说,使用weak_ptr
可以访问所指向对象但不会增加对象的引用计数。你不能通过*
, ->
或者[]
来访问weak_ptr
所指向的对象,必须使用lock()
成员函数创建管理被weak_ptr
引用对象的shared_ptr
才能访问对象。“不拥有对象”也使得你不能够像构造shared_ptr
一样来构造weak_ptr
,除了拷贝和赋值构造之外,只可以通过传入shared_ptr
来构造一个weak_ptr
。另外,c++14
给weak_ptr
增加了移动构造函数。
1 | using std::weak_ptr; |
从例子可以看出,当指向对象的所有shared_ptr被释放时,weak_ptr也失去了对象的引用,对象的内存被释放。use_count
是用来检查拥有被weak_ptr
共享管理对象拥有权的shared_ptr
对象的引用数量。特别的,可以用expired()
成员函数来检查weak_ptr
管理对象是否已经被删除,等价于use_count()==0
,但速度更快。
其他成员函数
同样,weak_ptr
也有swap
成员函数来交换两个weak_ptr
所管理的对象。另外,weak_ptr
也有reset
成员函数,但是只有不接收参数的版本,调用该函数会直接将weak_ptr
置为空。
shared_ptr
和weak_ptr
进阶:
enable_shared_from_this
:
如果我们想通过被智能指针所管理的对象成员函数返回管理该对象的智能指针,例如我们在Node
中定义set_child
成员函数:
1 | void set_child(shared_ptr<Node> left, shared_ptr<Node> right) |
我们在设置left
的parent
成员时需要获取获取该Node
对象的weak_ptr
,首先想到使用this
,但是this
是类型为Node *
的普通指针,使用该指针去构造智能指针再给parent
使用的做法是错误的,因为它给已经被管理的内存(这里的this
)设置了新的管理者(这里构造的智能指针),这种方法会导致重复性的释放内存,产生未定义的后果。
解决这个问题的方法是继承标准库中提供的enabled_shared_from_this
辅助类,然后通过public继承来的shared_from_this()
成员函数得到管理该对象的shared_ptr
:
1 | struct Node : public std::enable_shared_from_this<Node> |
如果像本例中需要使用的是weak_ptr
不需要那么麻烦,可以直接使用c++17
引入的weak_from_this()
返回共享*this
的weak_ptr
。这里public
继承enable_shared_from_this
是必须的,不加public
关键字会默认为private
继承从而导致错误。
为什么enable_shared_from_this
必须要public
继承?
因为Node
类是通过模板参数传给智能指针的,智能指针类需要能够访问到enable_shared_from_this
。
shared_ptr
别名构造函数:
有时候需要一种可能: 某对象拥有另一种对象。shared_ptr
构造函数中有一种接收一个shared_ptr
和一个普通指针作为参数的构造函数,称为别名(aliasing)构造函数,它会增加原始对象的引用计数,却指向着普通指针所指向的另一对象,使用这种构造函数你必须保证两种对象的寿命相称。例如:
1 | class Person |
可以看出,p
是一个shared_ptr
,指向动态分配的一个name
为test
的Person
对象,page
使用别名构造函数增加了对象test
的引用计数,但是却指向对象test
的成员age
,当p
销毁时,由于还有page
指向test
对象,所以对象并不会被回收内存,同样page
可以正常使用test
对象的age
成员。在page
被置为空后,其管理的名为test
的对象被回收内存,因此析构函数被调用,输出delete test
。此外,c++20
提供了别名构造函数的右值参数版本,第一个参数接收shared_ptr
的右值引用,用于将参数的控制权转移给新构造的智能指针对象。
shared_ptr类型转换:
我们知道,c++
提供四种转型操作:static_cast
, dynamic_cast
, const_cast
以及reinterpret_cast
,这些操作可以将普通指针转换类型,但是不能直接用于智能指针,c++11
提供了static_pointer_cast
, dynamic_pointer_cast
以及const_pointer_cast
用于智能指针的转型,c++17
补充了reinterpret_pointer_cast
,c++20
分别添加了它们的右值版本。
1 | shared_ptr<void> sp(new int); |
unique_ptr
的使用:
类似于shared_ptr
, unique_ptr
也可以自动管理内存,在资源不被使用时回收内存。不同之处在于unique_ptr
是“独占式持有”,也即一个动态分配的内存同一时间只能被一个unique_ptr
所持有,但unique_ptr
不一定持有对象,它可以是空。类似的,unique_ptr
也提供一个向bool
的类型转换来检查它是否为空,对于new
分配的内存提供*
, ->
操作,对于new []
分配的内存提供[]
操作(因为unique_ptr
提供了数组的偏特化版本),用swap
可以交换两个unique_ptr
所管理的对象,用reset
可以设置一个新分配的内存给此unique_ptr
管理并且释放以前管理的内存,用不接收任何参数的reset()
也等价于直接置空。此外,可以使用release()
成员函数来释放unique_ptr
的控制权并返回其所管理的内存,调用者必须手动接管此内存:
1 | using std::unique_ptr; |
由于unique_ptr
独占式持有的概念,它没有拷贝构造函数,但是提供移动构造:
1 | unique_ptr<int> p1(new int); |
以及右值赋值操作:
1 | unique_ptr<int> p4 = p1; // 错误 |
unique_ptr
的这种独占式特性就使得可以使用它在函数之间转移拥有权,比如作为函数参数传入函数并随着函数的结束而回收;作为函数返回值从函数中转移到函数外部:
1 | unique_ptr<string> get() |
这里涉及到返回值优化(RVO
),编译器会自动优化返回值,不必也不应该写成以下形式:
1 | return std::move(p); |
此例也用到了c++14
引入的make_unique
函数,该函数有好几个版本并且c++20
也进行了扩充,参加cppreference: std::make_unique, std::make_unique_default_init,此处不多做介绍。
自定义删除器:
与shared_ptr
类似,unique_ptr
也可以自定义删除器,不同之处在于你必须具体指定删除器的类型作为构造unique_ptr
的第二个模板参数,例如:
1 | unique_ptr<int,void(*)(int*)> p(new int[5], |
上述三种方式都可以,也可以使用c++11
提供的模板别名(alias template
):
1 | template<typename T> |
总结
标准库提供了三种智能指针类型:shared_ptr
, weak_ptr
和unique_ptr
。其中weak_ptr
作为shared_ptr
的辅助来使用。通常情况下,unique_ptr
比shared_ptr
效率更高,因为它不需要维护复杂的引用计数,因此在独占式场景下使用unique_ptr
可以带来基本等同于普通指针的效率。文中没有提到,unique_ptr
和shared_ptr
都重载了比较运算符用于直接比较地址值,shared_ptr
也重载输出运算符用来打印地址值(cout << sp;
等同于cout << sp.get();
),unique_ptr
在c++20
版本也提供了这个函数;此外,自c++11
,标准库也提供了一些针对shared_ptr
的特化原子操作函数模板,例如atomic_load
等,但是这些函数在c++20
中被弃用,取代者是 std::atomic
模板的特化: std::atomic
和 std::atomic
。
另外值得一提的一点是,使用智能指针可以避免对象初始化期间因为抛出异常而导致的资源泄露问题。总而言之,使用智能指针可以在保证安全的情况下最大可能的保证执行效率,所以建议使用智能指针来管理动态内存。
代码相关内容:
smart_point.h
:
1 |
|
main.cpp
:
1 |
|