文章目录
- 智能指针shared_ptr模版类
- week_ptr模版类
- unique_ptr
C++中是没有内存回收机制的,我在之前的一篇文章中提到使用指针的一些基本方法。C++在std标准库中也提供了三种封装过的指针模版类,称作为智能指针:
- shared_ptr
- unique_ptr
- week_ptr
我这里没打算详细介绍这三个指针的使用方式,主要是想从上一篇文章中提到的,在使用指针的过程中,容易出现的几个问题,如果是用智能指针的话,可以如何去解决。当然,也首先要介绍一下这三个指针的基本逻辑。
智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
先从shared_ptr说起。
智能指针shared_ptr模版类
直接上一段demo代码:
#include <stdio.h>
#include <memory>
using namespace std;
//自定义释放规则
void deleteInt(int* p) {
printf("abdcd\n");
delete []p;
}
int main()
{
auto aPtr = new float[10];
*aPtr = 1.0f;
printf("%f\n", *aPtr);
std::shared_ptr<int> p1(new int(10), deleteInt);
std::shared_ptr<int> p2(p1);
std::shared_ptr<int> p3(p1);
*p1 = 1;
printf("%d\n", *(p1.get()));
printf("%d\n", *(p2.get()));
printf("use_count, %d\n", p1.use_count());
printf("use_count, %d\n", p2.use_count());
printf("use_count, %d\n", p3.use_count());
p3.reset();
printf("use_count, %d\n", p1.use_count());
printf("use_count, %d\n", p2.use_count());
printf("use_count, %d\n", p3.use_count());
p2.reset();
printf("use_count, %d\n", p1.use_count());
printf("use_count, %d\n", p2.use_count());
printf("use_count, %d\n", p3.use_count());
p1.reset();
printf("use_count, %d\n", p1.use_count());
printf("use_count, %d\n", p2.use_count());
printf("use_count, %d\n", p3.use_count());
}
- std::shared_ptr p1(new int(10), deleteInt);这里是指定了一个删除器,也就是说当引用计数降到0时,调用的释放内存的函数。
- 只有当引用计数降为0的时候才会调用删除器
输出结果是:
1.000000
1
1
use_count, 3
use_count, 3
use_count, 3
use_count, 2
use_count, 2
use_count, 0
use_count, 1
use_count, 0
use_count, 0
abdcd
use_count, 0
use_count, 0
use_count, 0
来分析一下这个结果:
上面的代码实际上就是形成这样一个内存布局,三个shared_ptr指向了同一块内存。
- 这一块内存当前有3个指针引用,也就是引用计数为3。此时不管是p1,p2,p3,调用use_count的时候,都是输出的3。
- 第一个是p3.reset,这个函数执行后,就是p3到memory的这条线没有了。memory的引用计数就变成2了,所以p1和p2的use_count的输出就变成了2,而p3因为已经和memory这个内存没有关系了,所以use_count就变成了0.
- 第二个p2.reset以后,逻辑和上一条一样,p2,p3都变成了0,memory的引用计数变成了1.
- 当p1.reset执行以后,这块内存引用计数就变成0了,就会调用删除器了。
实际上,上图中的memory部分可以拆成两个部分:
- data field,就是数据部分,比如上面代码中的new int(10),一块十个正形大小的内存区域。
- control block。控制块,这个控制块就是shared_ptr的关键数据结构了,里面包含了use_count(强引用计数),weak_count(弱引用计数),删除器,构造器之类的一些内容。
- 控制块是一块单独的在堆上分配的内存空间。
- 回到上面的例子中,p3.reset相当于是p3这个变量的指向control block赋值为空(就是上面图中提到的,那条指向的线断了),于是通过p3.use_count就会返回0,但是在这一个control block中,p3执行reset之后,只是把引用计数减一,不会影响到p1和p2的返回。
- 引用计数的增加和减少是原子操作,可以在多线程中使用,是线程安全的(只是说这个操作是原子的,不是说所有的通过shared_ptr的操作都是线程安全的,是两个完全不同的概念)。
- 强烈推荐使用make_shared函数来初始化一个shared_ptr。
- 赋值运算符在shared_ptr中是一个移动拷贝函数,不会创建一个新的对象,只是让两个不同的指针指向同一个位置。
下面这段代码中,只会初始化两次构造函数,在p2 = p1这个地方是不会创建新的对象的。class Base { public: Base(){printf("base construct\n");} ~Base(){printf("base destruct\n");} }; class Derived: public Base { public: Derived(){printf("Derived construct\n");} ~Derived(){printf("Derived destruct\n");} }; int main() { std::shared_ptr<Derived> p1 = std::make_shared<Derived>(); std::shared_ptr<Derived> p2 = p1; std::shared_ptr<Derived> p3(new Derived()); printf("p1 use count: %d\n", p1.use_count()); printf("p2 use count: %d\n", p2.use_count()); printf("p3 use count: %d\n", p3.use_count()); }
week_ptr模版类
再来看一下weak_ptr,在上面的shared_ptr中,实际上无法准确的引用一个智能指针变量去判断某一片数据内存是否被使用,或者说准确的引用数是多少,因为在程序运行过程中,每一个shared_ptr的智能指针都可能会执行reset(),执行之后,这块内存的引用数就不能通过这个变量去调用use_count来获取了,这样在程序中非常的不方便。
那么,c++17中引入了week_ptr这个指针来解决这个问题。
week_ptr被称作为一个“观察者”,这个观察者可以做用于某一块数据指针,这个观察者可以“观察到”某一块内存空间上的引用者,或者说某一些shared_ptr对应的某一个control block。
week_ptr有下面几个特征,来说明它的“观察者”身份:
- shared_ptr重载了*和->运算符,还有get函数来获得内存区域的控制权和试用权,而week_ptr没有这些东西,无法直接使用内存空间。
- 不管shared_ptr如何的reset和构建,少一个指向内存区域的线,通过week_ptr调用use_count()就会减一,反之则加一。
- week_ptr自己释放和构建,是不会影响引用计数的。
- week_ptr可以通过lock函数,去获取一个可以访问这篇内存空间的shared_ptr智能指针。当然,如果所有指向这篇内存的shared_ptr都已经被释放了,那么lock函数返回空。
- lock获得的shared_ptr也会增加一个引用计数。
- 只能通过shared_ptr或者一个week_ptr来创建week智能指针,不能直接初始化。
还是上代码来看:
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>();
std::shared_ptr<int> p2 = p1;
std::weak_ptr<int> wp1(p1);
*p1 = 1;
printf("content is: %d\n", *p1);
printf("week ptr cnt is: %d\n", wp1.use_count());
std::shared_ptr<int> t = wp1.lock();
printf("content though week ptr is: %d\n", *t);
printf("p1 address is %x\n", p1.get());
printf("p2 address is %x\n", p2.get());
printf("wp1 address is %x\n", wp1.lock().get());
t.reset();
p2.reset();
t = wp1.lock();
printf("content though week ptr is: %d\n", *t);
t.reset();
printf("week ptr cnt is: %d\n", wp1.use_count());
p1.reset();
t = wp1.lock();
if(t)
{
printf("content though week ptr is: %d\n", *t);
}
else
{
printf("all ptr is released\n");
}
t.reset();
printf("week ptr cnt is: %d\n", wp1.use_count());
}
输出结果为:
content is: 1
week ptr cnt is: 2
content though week ptr is: 1
p1 address is 65c059a8
p2 address is 65c059a8
wp1 address is 65c059a8
content though week ptr is: 1
week ptr cnt is: 1
all ptr is released
week ptr cnt is: 0
所以说,week_ptr的作用之一就是担任某一块内存空间使用的“观察者”,程序员可以通过这个“观察者”来判断空间的使用情况。
unique_ptr
除了上面两个智能指针模板,标准库还提供了一个unique_ptr的模板类。这个和shared_ptr很类似。
但是unique_ptr的基本逻辑保证了同一块内存只有一个unique_ptr指针指向。
unique_ptr不能通过拷贝构造函数来生成,必须通过移动构造函数来生成:
看下下面的代码:
int main()
{
std::unique_ptr<int> u_ptr(new int(5));
*u_ptr = 10;
printf("u_ptr: %d\n", *u_ptr);
// std::unique_ptr<int> u1_ptr(u_ptr); //无法编译通过,必须使用移动构造函数
std::unique_ptr<int> u1_ptr = std::move(u_ptr);
// printf("u_ptr: %d\n", *u_ptr); // 编译可以通过,但是因为u_ptr已经不再指向这个地址了,会出现段错误
printf("u1_ptr: %d\n", *u1_ptr);
}
输出结果为:
u_ptr: 10
u1_ptr: 10