C++ | 智能指针
这是 C++ 网课学习笔记
建议先阅读上一节
右值引用 & 移动语义
或对内容有一定了解后再来阅读本篇笔记
1 引入: 为什么我们需要智能指针
1.1 问题一
观察以下代码
void science(double* data, int N) { |
不难理解, 函数 science()
接受两个参数, 一个表示数组 data
的首元素地址, 一个表示数组的长度
这说明了一个数组类型包含了三个信息: 数组元素类型, 首元素地址和长度; 然而当我们把数组作为指针传入时, 我们却丧失了长度这个信息, 只能用一个额外的 N
储存这个信息
回到函数体本身, 这个函数实现的功能很简单, 假如 needed()
为真, 则对 data
调用 calculate()
. 但是这个函数却有一个很大很大的缺陷, 假如 needed()
为假, 我们 new 出来的 temp
指针所指向的地址的内容就无法被释放, 成为垃圾了
这是传统指针遇到的第一个问题, 我们无法通过指针类型判断一个指针应该在什么时候被释放
1.2 问题二
float* science(float* x, float* y, int N) { |
这个函数也不难理解, 我们通过 x 和 y 计算出了 z, 同时 x 和 y 在函数内部就被释放了, 而 z 被保留, 这些东西都需要用户自己清楚, 很可能导致 UB
这是传统指针遇到的第二个问题, 传统指针是用户不友好的, 很多信息你只能在代码层面解读, 而无法从接口层告知
1.3 指针包含了太多信息
-
对于单个对象或数组
- 单个对象: 我们使用 new 和 delete, 不支持 ++, – 或 []
- 数组: 我们使用 new[] 和 delete[], 支持 ++, – 或 []
然而不管指向什么东西的指针, 他们都长一个样:
T*
, 但是我们却需要根据其指向对象的类型的不同使用不同的操作, 这显然是不友好的 -
所有权
- 所有者必须在使用完成后及时释放内存
- 非所有者无权释放内存
防止没有释放导致浪费或重复释放导致出错
-
是否可以为空
正是因为指针包括了很多很多无法简单被类型描述的特征, 这些特征又是刚需的, 所以我们需求一种 智能 的指针
- 他知道使用
delete
/delete[]
- 生命周期结束后自动销毁
- 是否可以为空指针
其实, STL 容器就包含了这种 智能 , 我们只负责调用某个容器, 如 string
, 而并不关心它什么时候销毁, 或者是否可以为空和销毁方式
2 垃圾回收
回收不可达的对象
2.1 对象的可达性
首先第一个问题就是, 如何判断一个对象的可达性?
- 维护一个 reference counting, 每有一个指针指向这个对象, 就 ++, 否则就 –
- 周期运行 mark-and-sweep (from root set), 每次运行的时候, 通过递归的方式标记有哪些内存是可访问的, 回收其他所有不可达的内存
2.2 回收多少内存
第二个问题是, 回收多少内存空间
C++ 并不是一个类型安全的语言
- 例如一个
void*
类型指针指向某个地址, 当我们回收它时, 我们并不知道具体要回收多少内存
3 std::unique_ptr
unique_ptr
假设自己是对象唯一的所有者(由用户保证), 这样就能保证 unique_ptr 在析构的时候可以直接释放, 不用考虑其他指针
正因如此, unique_ptr
无法被拷贝, 只能移动
std::unique_ptr<T> p(new T(/* some parameters */)), q; |
举个栗子
WidgetBase* create_widget(InputType); |
在这个代码段里面, 我们定义了一个 MyClass
类, 拥有一个私有成员 owner
, 类型是 unique_ptr
MyClass 的构造函数利用了一个返回值为 WidgetBase*
类型的辅助函数 create_widget()
, 为 owner
提供具体的构造实现
值得注意的是, 在析构函数里面, 我们只用 ~MyClass() = default;
, 而无需自己做内存释放, 这是因为一旦自己的生命周期结束, 作为 unique_ptr
的成员变量 owner
就会自己消亡, 递归的 delete
/ delete[]
属于 owner
的内存
因为 unique_ptr
的不可拷贝的特点, MyClass
将无法拥有拷贝构造函数
3.1 实现思路
成员类型
template <typename T> |
构造与析构
template <typename T> |
移动构造与移动赋值
template <typename T> struct unique_ptr { |
重载运算符
template <typename T> |
其他成员函数
template <typename T> struct unique_ptr { |
实际上, unique_ptr 就是它的成员 ptr 的一个包装, 通过特定的基本函数和运算符重载使他具有一定的特性–智能
3.2 std::make_unique()
回顾之前的一段实例代码
std::unique_ptr<T> p(new T(/* some parameters */)), q; |
我们发现一个很别扭的事情: 有 new
, 却没有 delete
这种不美观的事情 C++ 是不愿意看到的, 因此 C++11 引入了 make_unique()
函数把 std::unique_ptr<T> p(new T(/* some parameters */)), q;
改写为:
auto p = std::make_unique<T>(/* some parameters */); |
NO MORE RAW NEW!!!
make_unique
内部实现如下:
template <typename T, typename... Args> |
类似于完美转发
3.3 数组类型
事实上, std::unique_ptr
也为数组类型做出了偏特化 std::unique_otr<T[]>
- 在析构函数中, 默认使用
delete[]
, 而不是delete
- 提供了
operator[]
同时, std::make_unique
也为数组类型做了偏特化
- 接受的参数是数组大小而非构造函数参数
有了这么多工具, 我们就可以对
void science(double* data, int N) { |
进行升级了:
void science(double* data, int N) { |
3.4 所有权转移
由于 unique_ptr
的所有权是 unique 的, 所以, 我们可以通过上一节中学习的移动构造或移动赋值函数完成所有权的转移
auto a = std::make_unique<T>(); |
回到之前被诟病的代码:
float* science(float* x, float* y, int N) { |
我们之前认为其无法清晰的向用户表达信息的转移和变化, 但是智能指针可以, 升级如下:
std::unique_ptr<float[]> science( |
这样, 在用户调用这个函数的时候, 就能清晰的知道, upx
和 upy
作为参数, 只能用右值引用的方式赋予, 用户一下就明白这个所有权实际上交接给了函数内部
需要给一个函数传递所有权时或需要从一个函数返回所有权时,按值传递 unique_ptr
unique_ptr<widget> factory(); // 返回所有权 |
不是所有地方都要用智能指针的, 在不涉及所有权转移的地方, 可以继续使用指针和引用
4 std::shared_ptr
顾名思义, 这是可以共享的
unique_ptr
shared_ptr
是对象的所有者(但不唯一), 当且仅当指向该对象的最后一个 shared_ptr
不再指向它时, 对象被释放
可以被拷贝!
4.1 接口
具体实现比较麻烦, 略去不讲
template <typename T> |
拷贝构造和移动构造
template <typename T> |
其他成员函数
template <typename T> |
4.2 std::make_shared()
template <typename T, typename... Args> |
减少一次内存申请, 一次性申请 T
和 count
的内存空间
4.3 所有权转移
shared_ptr<widget> factory(); // source + shared ownership |