【C++11】智能指针

📝前言:

这篇文章我们来讲讲C++11------智能指针:

🎬个人简介:努力学习ing

📋个人专栏:C++学习笔记

🎀CSDN主页 愚润求学

🌄其他专栏:C语言入门基础python入门基础python刷题专栏Linux


文章目录

一,什么是智能指针

传统指针的问题

  • 内存泄漏(忘记delete)
  • 悬垂指针(delete后继续访问)
  • 异常安全问题(抛出异常后,后面的delete语句没办法执行)

为了解决上面的问题就出现了:RAII和智能指针设计思路

RAII和智能指针设计思路

RAII

  • 全称:Resource Acquisition Is Initialization(资源获取即初始化)
  • 是⼀种管理资源的类的设计思想.
  • 本质是⼀种利用对象生命周期来管理获取到的动态资源 ,避免资源泄漏。这里的资源可以是内存、⽂件指针、网络连接、互斥锁等等。
    • 对象控制对资源的访问
    • 获取资源 与对象的构造绑定
    • 释放资源 与对象的析构绑定

智能指针

  • 智能指针就是利用了RAII的设计思想。同时,还要求这个对象能够满足普通指针的行为,方便访问资源。
  • 即:重载 operator*/operator->/operator[] 等运算符,方便访问资源

二,智能指针的使用

(1)C++标准库中的智能指针

总体概述

  • 所在头文件:<memory>
  • 除了weak_ptr他们都符合RAII和像指针⼀样访问的行为。(weak_ptr不能直接管理资源)
  • 不同的指针,区别主要是:解决智能指针拷贝时的思路不同

不同指针的特点

下面讲讲不同的智能指针的特点:

auto_ptr

  • 这是C++98的,是一个糟糕的指针
  • 拷贝时:把被拷贝对象的资源管理权移交给拷贝对象
  • 问题:这样会导致被拷贝对象悬空,后续访问会报错
  • 这指针能不用就不用,C++11也有更好的

unique_ptr

  • 特点:不支持拷贝,只支持移动(传move(ptr)移动构造,移动后,原ptr也会悬空,但是这是用户自己的行为)
  • 如果不需要拷贝的场景就十分建议使用

shared_ptr

  • 支持拷贝,也支持移动
  • 底层通过引用计数实现

weak_ptr

  • 不支持RAII,也就是:不能用它直接管理资源
  • 主要用于:解决shared_ptr的循环引用导致的内存泄漏问题

delete的问题

  • 智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源(如:malloc、new[]、fopen...),交给智能指针管理,析构时就会崩溃

为了解决上面这个问题:

  • 智能指针支持用户提供一个删除器,在删除器中实现资源的释放。
  • 但是提供位置不同
    • unique_ptr作为模板参数提供(建议提供仿函数)
    • shared_ptr作为构造函数参数提供(仿函数、lambda、函数指针等都可以,建议lambda)
  • 删除器的本质是一个可调用对象。当提供了删除器,在智能指针析构的时候,就会调用这个删除器去释放资源。(这个删除器的调用在智能指针析构函数内部)
  • 因为new[]经常使⽤,所以为了简洁⼀点,unique_ptrshared_ptr都特化了⼀份[]的版本
删除器提供示例

示例:

cpp 复制代码
unique_ptr<Date> up1(new Date[5]);

程序崩溃:

使用特化版本[]的:

cpp 复制代码
unique_ptr<Date[]> up1(new Date[5]);

shared_ptr的删除器

示例(使用仿函数):

cpp 复制代码
class Del
{
public:
	void operator()(Date* ptr)
	{
		delete[] ptr;
	}
};
// 传入仿函数删除器
shared_ptr<Date> up1(new Date[5], Del());
return 0;

示例(使用lambda):

cpp 复制代码
shared_ptr<Date> up1(new Date[5], [](Date* ptr) {delete[] ptr; });

unique_ptr删除器

示例(使用仿函数):

cpp 复制代码
// 模版参数处提仿函数作为删除器
unique_ptr<Date, Del> up1(new Date[5]);

其他知识

  • shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造

示例(普通:使用指向资源的指针构造):

cpp 复制代码
	shared_ptr<Date> sp1(new Date); // 使用指向Date的指针构造
	shared_ptr<Date> sp2(sp1); // 用 sp1 拷贝构造 sp2
	shared_ptr<Date> sp3 = sp2;
	// 上面这三管理同一块空间

示例(使用make_shared

cpp 复制代码
	shared_ptr<Date> sp1(new Date);
	// 使用语法:make_shared<要创建对象的类型>(初始化对象的参数列表)
	shared_ptr<Date> sp2 = make_shared<Date>(2025, 5, 2); // 用初始化对象的值直接构造

可见,管理的是新分配的空间。

  • shared_ptrunique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true

示例(直接把智能指针对象给if判断是否为空):

cpp 复制代码
	shared_ptr<Date> sp2 = make_shared<Date>(2025, 5, 2); // 用初始化对象的值直接构造

	if (sp2) // 实际上是 if(sp2.operator bool())
	{
		cout << "p2不为空" << endl;
	}
  • shared_ptrunique_ptr 的构造函数都用了 explicit 修饰,防止普通指针隐式类型转换成智能指针对象

(2)C++11和boost库中智能指针的关系

  • Boost 库是 C++ 标准库的扩展程序库集合
  • Boost 库由 Boost 社区的成员进行维护。社区成员贡献了许多 C++ 标准库中未包含的功能和工具(有好有坏)
  • C++ 委员会在制定新的 C++ 标准时,经常参考 Boost 库,取其精华
  • 比如:C ++ 11 引入的unique_ptrshared_ptrweak_ptr。分别是参考boost的scoped_ptrshared_ptrweak_ptr

三,智能指针的原理

unique_ptr模拟实现

unique_ptr的思路是不支持拷贝,只支持移动,在移动的时候,把资源管理权交给另一个指针。

实现代码(不带删除器版本的):

cpp 复制代码
template<class T>
class unique_ptr
{
public:
	explicit unique_ptr(T* ptr)
		:_ptr(ptr)
	{
		cout << "init: " << _ptr << endl;
	}
	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete: " << _ptr << endl;
			delete _ptr;
		}
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}

	// 禁用拷贝构造和拷贝赋值
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

	unique_ptr(unique_ptr<T>&& up)
		:_ptr(up._ptr)
	{
		up._ptr = nullptr; // 原来的置空,相当于转移了管理权
	}

	unique_ptr<T>& operator=(unique_ptr<T>&& up)
	{
		delete _ptr; // 先释放当前所管理的空间
		_ptr = up._ptr;
		up._ptr = nullptr;
	}

private:
	T* _ptr;
};

shared_ptr模拟实现(重点)

底层利用引用计数器来实现多个指针同时管理。

模拟实现代码(带删除器)

cpp 复制代码
template<class T>
class shared_ptr
{
public:

	explicit shared_ptr(T* ptr = nullptr) // 没有删除器的时候,匹配这个
		:_ptr(ptr)
		, _count(new int(1))
	{}

	template<class D>
	shared_ptr(T* ptr, D del) // 有删除器的时候会匹配这个
		:_ptr(ptr)
		,_count(new int(1))
		,_del(del)
	{}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_count(sp._count)
		,_del(sp._del)
	{
		++(*_count);
	}

	// 移动构造函数(区别就是_count不变,并且原来的指针要置空)
	shared_ptr(shared_ptr<T>&& sp)
		: _ptr(sp._ptr)
		, _count(sp._count)
		, _del(move(sp._del)) // 这里也调用del的移动构造函数,提高效率
	{
		sp._ptr = nullptr;
		sp._count = nullptr;
	}

	void release() // 不只用于析构,赋值的时候,release也可能要使用
	{
		if (_count && --(*_count) == 0)
		{
			_del(_ptr); // 调用删除器,删除管理的资源
			delete _count;
			_ptr = nullptr;
			_count = nullptr; // 在把自己的资源释放了
		}
	}

	~shared_ptr()
	{
		release();
	}
	
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			release();
			_ptr = sp._ptr;
			_count = sp._count;
			_del = sp._del;
			++(*_count);
		}
		return *this;
	}

	// 移动赋值
	shared_ptr<T>& operator=(shared_ptr<T>&& sp)
	{
		if (this != &sp)
		{
			release();
			_ptr = sp._ptr;
			_count = sp._count;
			_del = move(sp._del);
			sp._ptr = nullptr;
			sp._count = nullptr;
		}
		return *this;
	}

	T* operator->()
	{
		return _ptr;
	}

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

	int use_count() const 
	{
		return _count ? *_count : 0; // 如果 _count 为空,返回 0
	}

private:
	T* _ptr;
	int* _count; 
	function<void(T*)> _del = [](T* ptr) {delete ptr;}; // 删除器,并提供缺省值
};

测试代码:

cpp 复制代码
int main()
{

	// 构造
	tr::shared_ptr<Date> sp1(new Date);
	// 拷贝构造
	tr::shared_ptr<Date> sp2(sp1);
	cout << sp2.use_count() << endl;

	tr::shared_ptr<Date> sp3(new Date);
	cout << sp3.use_count() << endl;
	// 拷贝赋值
	sp3 = sp1;
	cout << sp1.use_count() << endl;

	cout << "------------------------------------------------------------" << endl;

	// 移动构造
	tr::shared_ptr<Date> sp4(move(sp1));
	cout << sp4.use_count() << endl;
	cout << sp1.use_count() << endl; // sp1已经悬空了(不应该继续使用sp1,这里输出0是因为在use_count 里面有判断)

	// 移动赋值
	sp4 = move(sp2);
	cout << sp4.use_count() << endl; // sp2也悬空了,不再管理Date资源

	cout << "------------------------------------------------------------" << endl;

	tr::shared_ptr<Date> sp5(new Date[5], [](Date* ptr) {delete[] ptr; });
	cout << sp5.use_count() << endl;

	return 0;
}

运行结果:

四,shared_ptr和weak_ptr

(1)shared_ptr循环引用问题

如果节点有前后指针,prevnext

  • n1next指向n2的时候,n2的引用计数 +1 == 2
  • n2prev指向n1的时候,n1的引用计数 +1 == 2
  • n1n2析构的时候,引用计数分别-1变成1
  • 但是这时候,n1还依被prev管理,只有prev释放,n1的引用计数才能到0然后被释放
  • prev什么时候被释放呢?只有当n2被释放的时候才被释放,但是n2n1next管理
  • n1next什么时候释放?只有n1被释放才被释放。
  • 所以造成了循环。

(2)weak_ptr解决问题

  • weak_ptr不支持RAII,也不⽀持访问资源,weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数
  • weak_ptr也没有重载operator*operator->等,因为他不参与资源管理。
  • 如果weak_ptr绑定的shared_ptr已经释放了资源(或者指向了别的资源),那么他去访问资源就是很危险的(悬空)
  • expired()可以用来判断weak_ptr绑定的资源是否失效,失效时返回true,没失效返回false
  • lock()weak_ptr绑定的对象还存在的时候,weak_ptr.lock()可以返回⼀个管理资源的shared_ptr,这样weak_ptr就可以访问资源,并且就算原来的shared_ptr改变了,也有这个新的shared_ptr(相当于这就是用weak_ptr创建了一个新的shared_ptr,引用计数会增加)
weak_ptr使用示例

使用示例:

cpp 复制代码
int main()
{
	shared_ptr<string> sp1(new string("111111"));
	shared_ptr<string> sp2(sp1);
	weak_ptr<string> wp = sp1;

	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	// sp1和sp2都指向了其他资源,则weak_ptr就过期了
	sp1 = make_shared<string>("222222");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	sp2 = make_shared<string>("333333");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	cout << "-------------------------------------------------" << endl;
	wp = sp1; // 重新绑定
	//shared_ptr<string> sp3 = wp.lock(); 
	cout << wp.use_count() << endl;
	auto sp3 = wp.lock(); // 利用weak_ptr自己创建一个shared_ptr

	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	*sp3 += "###";
	cout << *sp1 << endl;
	return 0;
}

运行结果:

五,share_ptr线程安全问题

这里简单讲解一下,暂时不做具体讲解。

  • shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中同时访问修改引用计数,就会存在线程安全问题。
  • 即:原来是引用计数是1,两个线程同时访问增加:理论上结果应该是1 + 1 + 1 == 3,但是如果两个线程同时拿,拿到的都是 1,则变成两个1 + 1 == 2出现了错误
  • 解决方法:引用计数加锁原子操作atomic<int>
  • 说明一下:智能指针本身是线性安全的,但是指向的资源不是线性安全的。(智能指针指向的资源的线程安全性取决于资源本身的实现,与智能指针无关)

六,内存泄漏问题

内存泄漏

  • 因为疏忽或错误造成程序未能释放已经不再使⽤的内存。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害

  • 普通程序:运行完就结束。不害怕内存泄漏问题,因为进程正常结束,页表的映射关系解除,物理内存也可以释放。
  • 长期运行的程序:如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。

如何检测?

  • 用检测工具

如何预防?

  • 程序员编写代码时,正确使用智能指针,事前预防
  • 事后排错,用检测工具

🌈我的分享也就到此结束啦🌈

要是我的分享也能对你的学习起到帮助,那简直是太酷啦!

若有不足,还请大家多多指正,我们一起学习交流!

📢公主,王子:点赞👍→收藏⭐→关注🔍

感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

相关推荐
小黑随笔1 小时前
【Golang玩转本地大模型实战(一):ollma部署模型及流式调用】
开发语言·后端·golang
江沉晚呤时1 小时前
Redis缓存穿透、缓存击穿与缓存雪崩:如何在.NET Core中解决
java·开发语言·后端·算法·spring·排序算法
achene_ql1 小时前
缓存置换:用c++实现最近最少使用(LRU)算法
开发语言·c++·算法·缓存
高效匠人2 小时前
Python10天冲刺-设计模型之策略模式
开发语言·人工智能·python·策略模式
黄雪超2 小时前
JVM——JVM 是如何执行方法调用的?
java·开发语言·jvm
mahuifa2 小时前
(35)VTK C++开发示例 ---将图片映射到平面2
c++·vtk·cmake·3d开发
风暴之零2 小时前
文本中地理位置提取方法—正则和NLP模型
开发语言·python
Dxy12393102163 小时前
python合并word中的run
开发语言·python·word
why1513 小时前
百度网盘golang实习面经
开发语言·后端·golang
一匹电信狗3 小时前
【数据结构】堆的完整实现
c语言·数据结构·c++·算法·leetcode·排序算法·visual studio