智能指针原理与使用场景全解析

目录

智能指针使用场景分析

RAII和智能指针的设计思路

C++标准库智能指针的使用

auto_ptr

unique_ptr

shared_ptr

weak_ptr

底层

定制删除器

make_shared

补充

智能指针的原理

shared_ptrd的实现

shared_ptr和weak_ptr

shared_ptr的线程安全问题

内存泄漏


智能指针使用场景分析

如下图程序,我们new了以后,也delete,但是因为抛异常的原因,后面的delete没能执行,所以造成了内存泄漏

异常可能会引起内存泄漏类似的安全问题

在中间捕获异常后,不是为了拦截异常,只为了让资源释放,异常还是由最外层处理,而是重新抛出让最外层处理是为了捕获到异常可以记录日志

new也会抛异常,因为new底层调用operator new,operator new封装了malloc,封装malloc的核心目的是符合C++库里面处理错误的机制

复制代码
#include <iostream>
using namespace std;

double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
	// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
	// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
	// 是智能指针,否则代码太戳了
	int* array1 = new int[10];
	int* array2 = new int[10]; // 抛异常呢
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;
		delete[] array1;
		delete[] array2;
		throw; // 异常重新抛出,捕获到什么抛出什么
	}
	// ...
	cout << "delete []" << array1 << endl;
	delete[] array1;
	cout << "delete []" << array2 << endl;
	delete[] array2;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

RAII和智能指针的设计思路

仅看这里是很难明白他的设计意图的

换个角度解释:获取到一个资源之后,不要单独管理,建议把这个资源委托给一个对象去管理,这个资源在这个对象的生命周期内都是有效的,生命周期结束后自动释放该资源,就不需要手动释放资源,而是自动释放资源

迭代器底层可能是链表,数组,dequeue这样各种各样复杂的结构,那用一个类型进行封装,去重载operator*,operator->,达到拥有像指针一样的访问功能,这样就屏蔽底层的细节,这样就同样只是一个迭代器,是一种面向对象设计的一种巧妙的方式,也是一种封装的体现,智能指针也是如此,他是一个类,通过RAII体现了智能释放的那一部分,通过运算符重载来体现指针的那一部分,如下图

C++标准库智能指针的使用

智能指针真正的问题,是拷贝的问题

比如这里这个东西有智能指针管理了,但你不能排除你要传参传值,或者你自己要进行拷贝,比如这里的sp2拷贝sp1,但是拷贝就要出问题

这里有个指针管理这里的资源,我们拷贝的时候不写拷贝构造,也能支持拷贝,编译器会默认生成一个拷贝构造,默认生成的拷贝构造对于内置类型(任何类型的指针都是内置类型),对于内置类型只能完成浅拷贝或者值拷贝,所以这里运行会崩溃,同一份资源被两次释放

这里还不能单纯地使用深拷贝解决问题,因为比如容器(vector、list、string等),那些资源是属于这些容器的,这里一个容器拷贝给另外一个容器,就是期望说你把我的内容拷贝一份保存起来,拷贝跟我一样大的空间,一样大的值,智能指针跟这些容器不同,智能指针是代管资源,这些资源不属于它,但是那些资源是属于那些容器的

智能指针模拟的是指针的行为,让一个指针拷贝给另一个指针,期望还是浅拷贝,但是浅拷贝就会造成释放多次的情况,所以历史上就出现了多种方案

auto_ptr

支持自动释放,解决拷贝问题使用到了一个管理权转移的概念(浅拷贝),不是一个很漂亮的用法,会导致一个悬空的问题

有向前兼容的概念,用了它以后,取消的话,代码可能就编不过

unique_ptr

特点是不支持拷贝,只支持移动,如果你是一个右值(临时对象,匿名对象),把你的资源移动给别人,也就不存在悬空的问题,可以移动,但不支持拷贝

但是如果将一个左值move了以后,只要移动的话,右值的话就是将你的资源转走,就会悬空,所以要谨慎使用移动,因为他以为你是右值,普通的右值没问题,但是是move以后的右值就要小心了,一样存在悬空的问题

boost库中被称为scoped_ptr

shared_ptr

有些场景必然还是要拷贝的,但是要拷贝的话,还是浅拷贝,但是会析构多次,那在这里怎么办呢?这里就需要使用一个叫引用计数的方案来解决

引用计数:记录有多少个人指向这块资源,最后一个人析构后才释放资源

下图是演示:

支持拷贝

use_count就是可以获取其引用计数

这里有三次析构是因为前面还有auto_ptr和unique_ptr

也支持移动,这里将一个左值move了以后,只要移动的话,右值的话就是将你的资源转走,所以要谨慎使用移动,因为他以为你是右值,普通的右值没问题,但是是move以后的右值就要小心了,一样存在悬空的问题

weak_ptr

不是智能指针,也称为"弱指针",是shared_ptr的"小弟"

底层

定制删除器

智能指针析构时默认是进行delete释放资源的,就意味着如果不是new出来的资源,交给智能指针管理的话,析构时就会崩溃。智能指针支持在构造时给一个删除器(就是一个仿函数一般,可调用对象),如果将这个可调用对象传给其以后,就不会写死是delete,就会用这里删除器(仿函数)去释放

因为这里底层写死了是delete,所以这里的解决方案是给了一个删除器,支持在构造的时候给一个删除器,就是一个仿函数,在仿函数的operator\[\]中进行释放,它会用这个对象,把资源给这个对象来进行释放,这个删除器你想以什么方式释放你就定制就行,在构造的时候给

演示一下删除器的概念

一个是要传类模板类型,一个是要传函数对象

unique_ptr不能在构造的时候给,shared_ptr只能在构造的时候给

shared_ptr直接将构造函数写成了一个类模板,所以这个类型可以让你自己去推,传什么仿函数他就自己推

这里的函数指针是模板,没有实例化成具体的函数,用不了

unique_ptr这里的void(*)(Date*),void返回值,Date*的参数,我有这里的一个函数指针类型,但是定义出来一个空指针,我不知道这个指针指向谁,只是定义出来这一个类型的函数指针,所以unique_ptr有些情况下构造函数也可以传参数,是指在使用函数指针的情况下,用函数指针你传类型,类型实例化出来的对象你使用不了,所以在构造函数的时候还得传过去,相当于用函数指针底层定义一个类型,那你在构造的时候还要将函数指针具体的值传过来

lambda底层有类型但是上层没有,底层编译器编译成仿函数了,那个类型名称是编译器生成的,我们在语法层拿不到那个类型,只能拿给auto自己推,或者把lambda对象给shared_ptr这个模板自己推

decltype是C++的一个东西,可以推导我们的一个对象的类型,这个类型可以拿来模板传参

总结:shared_ptr的定制删除器好用

当然,定制删除器有时候有点重,在这里写个反函数或者lambda,可以是可以,但是用起来有点繁琐,但是也有方式用起来比较简单:类模板这个位置实例化的时候不要对应给类型,而是给对应类型的\[\],这样他就会自动调用delete\[\],通过特化的方式实现的

\[\]的直接走特化版本即可

复制代码
// 解决⽅案1
// 因为new[]经常使⽤,所以unique_ptr和shared_ptr
// 实现了⼀个特化版本,这个特化版本析构时⽤的delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);

更复杂的类型写lambda更舒服

make_shared

make_shared是一个可变参数模板,传几个参数都可以帮你推,构造一个资源,并返回shared_ptr,会让引用计数效率高一点

shared_ptr还可以借助make_shared(make_pair类似)去构造

补充

可以通过get来获取底层的原生指针

operator bool是将自定义类型转换成内置类型,在这里可以判断是否为空指针

现实中如下图!sp4这么写,就会隐式类型转换成bool

这里体现智能指针能直接做条件逻辑判断

智能指针也支持直接打印,因为重载了流插入

拷贝就用shared_ptr,不拷贝就用unique_ptr

上图写法unique_ptr和shared_ptr都是不支持的,因为他们的构造函数带了explicit,不让隐式类型转换,这里的意思是返回一个Date*,构造一个Date对象,再去拷贝构造,编译器这里优化成直接构造,目的是不想这种指针在有些场景被转换成智能指针,从而发生一些问题

智能指针的原理

对于shared_ptr中的引用计数的编写,不能各自均有一个引用计数,这样的话资源就永远也析构不了,给对应的count加上static也是不行的

所以以上方法是不行的

正确的方法是一个资源配一个引用计数

定义一个指针指向这个计数,而不是把计数存起来,而计数实际是在堆上

shared_ptrd的实现

默认是不需要写删除器的

复制代码
#pragma once
namespace rh {
	template<class T>
	class shared_ptr {
	public:
		explicit shared_ptr(T* ptr == nullptr)
			:_ptr(ptr),
			_pcount(_pcount)
		{}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			_pcount(sp._pcount)
		{
			++(*_pcount);
		}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;

				_ptr = nullptr;
				_pcount = nullptr;
			}

			T& operator*()
			{
				return *_ptr;
			}

			T* operator->()
			{
				return _ptr;
			}
		}
	private:
		T* _ptr;
		int* _pcount;
	};

}

shared_ptr和weak_ptr

循环引用的问题是shared_ptr的缺陷,特殊场景下就会出现循环引用的问题

这里导致了内存泄漏

为什么循环引用会导致内存泄漏?

对于中间步骤图,n1有两个智能指针管理者n1和prev,n2也有两个智能指针管理者n2和next,这样看着没有问题

但是最后一步,n1和n2析构了就出问题了,n1和n2出了作用域调用析构函数将引用计数都减为了1,然后现在两个节点分别由next和prev分别管理,这样就导致内存泄漏了

_next是左边节点的成员,左边节点delete以后,_next就释放了

_prev是右边节点的成员,右边节点delete之后,_prev就释放了

就形成了回旋镖似的循环引用,谁都会释放就形成了循环引用,导致内存泄漏

遇到这种场景,将_next和_prev的shared_ptr改成weak_ptr

weak不支持RAII,不是一个常规的指针

最大的特点是不增加引用计数

你把你的资源给我,但是我不加你的计数,不参与这个资源释放的管理,仅仅是指向这个资源,能找到这个资源,所以不增加引用计数,也就是这里的next和prev不参与

weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的
资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用
lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

weak_ptr的底层实现还是会比现象的复杂一些,因为他还是要拿到计数,因为不拿到计数的话,他都不知道指向的资源是有效还是无效

expired返回false为没过期,返回true为过期了

下图这里就过期了

假设不提供operator*和operator->就是怕过期了你再去访问

但你就是想访问你指向的资源,你就可以调用lock这个函数,新增一个shared_ptr去管理这个资源,就可以通过这个shared_ptr去访问了

总结:weak_ptr有计数,但是不增加计数,不参与资源释放,指向的资源是有可能过期,可以用expired进行判断,不支持直接访问,原因就是害怕过期你还继续访问,里面只剩野指针,在没过期的情况下进行lock,哪怕其他的shared_ptr都释放了,还有一个lock新增的shared_ptr管理,计数++,是安全的,就相当于锁住这个资源不让他提前释放

weak_ptr不一定是有效的,因为它指向的shared_ptr可能被赋值,资源释放了,就失效了

shared_ptr的线程安全问题

比如说这里有个智能指针,将这个智能指针拷贝给另外一个智能指针,这两个智能指针同时在两个线程上,但是他们管理同一份资源,多个线程上的多个智能指针管理同一个资源的时候,进行拷贝和析构就会++计数,计数是共享的,同时去++的话,就会有所谓的线程安全问题

传统的方式就是加锁或者原子操作atomic<int>*就可以保证线程安全

智能指针本身是线程安全的,但是指向的资源不是线程安全的,指向的资源他管不了,因为在外面,所以要加锁

内存泄漏

资源不用了不释放就是内存泄漏

普通程序出现内存泄漏是问题不大

相关推荐
珊瑚里的鱼1 小时前
【动态规划】买卖股票的最佳时机Ⅲ
算法·动态规划
码界索隆1 小时前
Python转Java系列:面向对象基础
java·开发语言·python
逻辑星辰1 小时前
x-ds-pow-response逆向分析
开发语言·人工智能·python·深度学习·算法
CQU_JIAKE1 小时前
6.9【aAAA]
算法
Lewiis1 小时前
白话桶排序
数据结构·算法·golang·排序算法
非生而知之者1 小时前
基于灰狼算法优化的电量预测
python·算法·群体智能算法·电力预测
ywl4708120871 小时前
‌HashMap 1.8 的扩容机制(Resize)‌
算法·哈希算法
AI科技星1 小时前
《全域数学/数术工坊》体系总览
c语言·开发语言·汇编·electron·概率论
范什么特西2 小时前
Maven中dependencies和dependencyManagement区别
java·开发语言·maven