【C++练级之路】【Lv.25】智能指针

news2025/4/16 3:19:14



快乐的流畅:个人主页


个人专栏:《算法神殿》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

  • 一、智能指针的引入
  • 二、智能指针的概念
    • 1.1 RAII
    • 1.2 指针特性
    • 1.3 拷贝问题
    • 1.4 auto_ptr
  • 三、智能指针的模拟实现
    • 2.1 unique_ptr
    • 2.2 shared_ptr
    • 2.3 weak_ptr
    • 2.4 定制删除器
  • 总结

一、智能指针的引入

什么是智能指针?为什么要引入它呢?在此之前,先来看一段代码:

void test()
{
	int* p1 = new int;
	int* p2 = new int;
	delete p1;
	delete p2;
}

C++内存分配中,使用 new 操作符,如果分配失败,可能会抛出 std::bad_alloc 异常。那么请想一想:

  • p1抛异常会怎么样?
  • p2抛异常会怎么样?

如果p1抛出异常,则不会有问题出现。而如果p2申请时抛出异常,那么就会导致p1申请的空间无法释放,导致内存泄漏

二、智能指针的概念

鉴于异常导致执行流乱跳,可能造成内存泄漏,我们期望一种“智能”的指针,可以在抛异常时,自动释放已申请的空间

1.1 RAII

智能指针的核心思想,就是 RAII。

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

void TestSmartPtr()
{
	SmartPtr<int> sp1 = new int;
	SmartPtr<int> sp2 = new int;
}

我们将申请的空间托管给 SmartPtr 对象,申请空间时构造对象,对象析构时释放空间。这样,就可以在 new 抛异常时,伴随着对象的析构而自动释放空间。

1.2 指针特性

除了最核心的 RAII 思想,我们还要实现指针的特性,让类能拥有指针的行为,即为重载指针运算符

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		delete _ptr;
	}

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

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

void TestSmartPtr()
{
	SmartPtr<int> sp1 = new int;
	*sp1 = 2;

	SmartPtr <pair<int, string>> sp2 = new pair<int, string>(1, "Black Myth:");
	sp2->first += 2;
	sp2->second += "WuKong";
	cout << sp2->first << ":" << sp2->second << endl;
}

我们重载了*操作符和->操作符,使得 SmartPtr 类拥有了和指针一样的行为。这点早在迭代器的实现时,便已经详细讲解过。

1.3 拷贝问题

智能指针的难点,便是拷贝问题

void TestSmartPtr()
{
	SmartPtr<int> sp1 = new int;
	SmartPtr<int> sp2 = sp1;
}

在上述代码的拷贝构造中,以往常规意义的浅拷贝和深拷贝都不对:

  • 浅拷贝,导致内存重复释放
  • 深拷贝,导致指针意义不对,没有指向同一份资源

而我们接下来介绍的智能指针,将围绕这点来展开讨论。

1.4 auto_ptr

C++98 时,提供了第一个智能指针auto_ptr

auto_ptr 原理:独占资源的所有权,并且在拷贝时转移资源的所有权。

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~auto_ptr()
	{
		delete _ptr;
	}

	auto_ptr(auto_ptr<T>& ap)
		: _ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}

	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		if (this != &ap)
		{
			delete _ptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
	}

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

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

void test_auto_ptr()
{
	auto_ptr<int> ap1 = new int;
	auto_ptr<int> ap2 = ap1;//所有权转移

	*ap1 = 1;//悬空指针
}

auto_ptr 在拷贝时,将资源的所有权转移,从而使自身置空。这种行为虽然可以防止多个 auto_ptr 同时释放同一块内存,但是由于悬空指针的情况出现,在后续的代码中极易出现访问空指针的错误。

由于其设计上的一些缺陷和危险性,它在 C++11 中被弃用,并最终在 C++17 中被完全移除

三、智能指针的模拟实现

C++11 时,新增了unique_ptrshared_ptrweak_ptr等智能指针,以代替auto_ptr。

2.1 unique_ptr

unique_ptr 原理:独占资源的所有权,并禁止任何拷贝

template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~unique_ptr()
	{
		delete _ptr;
	}

	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

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

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

void test_unique_ptr()
{
	unique_ptr<int> up1 = new int;
	//unique_ptr<int> up2 = up1;//独占所有权

	*up1 = 1;
}

unique_ptr 通过显式 delete 拷贝构造和赋值重载,使得其对象不能进行任何拷贝,只能进行移动。

它是 auto_ptr 的直接替代品,推荐用于管理独占所有权的动态内存。

2.2 shared_ptr

shared_ptr 原理:共享资源的所有权,通过引用计数来实现,多个指针可以共享同一个资源,当最后一个指针销毁时,资源才会被释放。

如何实现引用计数呢?常规的 int 类型的成员变量或者静态成员变量都不行:

  • int count:属于每个对象,没有公有属性来“共享”。
  • static int count:属于整个类,有公有属性,但并不是我们期望的。

所以,我们转换角度,引用计数不应该与对象或类挂钩,而是与资源相挂钩

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~shared_ptr()
	{
		release();
	}

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

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)//防止同一资源的指针相互赋值
		{
			release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}

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

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

	T* get() const
	{
		return _ptr;
	}

	int use_count() const
	{
		return *_pcount;
	}
private:
	void release()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
	}
	
	T* _ptr;
	int* _pcount = new int(1);//指向当前资源的指针数
};

void test_shared_ptr()
{
	shared_ptr<int> sp1 = new int;
	shared_ptr<int> sp2 = sp1;//共享所有权

	shared_ptr<int> sp3 = new int;
	sp3 = sp2;
}

我们为了实现引用计数,新增一个成员变量 _pcount,指向当前资源的计数(表示有多少指针共享这个资源)。

  • 构造时计数初始化为1,此后每次拷贝都增加计数,实现资源共享。
  • 析构时减少计数,当计数减为0时,才释放资源。

值得一提的是,赋值重载的判断,不再是以对象来判断(this != &sp),而是以资源来判断(_ptr != sp._ptr),防止同一资源的指针相互赋值。跨资源赋值时,先将当前资源 release(计数减1,如果为0则释放资源),再指向目标资源,增加计数。


但是,shared_ptr 的引用计数,会出现一种问题——循环引用

template<class T>
struct ListNode
{
	T _val;
	shared_ptr<ListNode<T>> _prev;
	shared_ptr<ListNode<T>> _next;

	ListNode(const T& val = T())
		: _val(val)
	{}
};

void test_shared_ptr()
{
	shared_ptr<ListNode<int>> n1 = new ListNode<int>;
	shared_ptr<ListNode<int>> n2 = new ListNode<int>;

	//循环引用
	n1->_next = n2;
	n2->_prev = n1;
}

在上述代码中:

  • 经过 n1 和 n2 的构造和节点的相互链接,两个节点的计数均为2.
  • 析构时,n2 先析构,使得 n2 指向的节点计数减到1,接着 n1 再析构,使得 n1 指向的节点计数减到1。
  • 而两块资源的计数都没有减到0,导致无法析构节点。

这就是循环引用,这会导致引用计数不会减到0,从而内存泄漏

2.3 weak_ptr

为了解决 shared_ptr 的循环引用问题,weak_ptr 应运而生。

weak_ptr 原理:共享资源的所有权,但是一种弱引用,不参与引用计数,不管理资源的生命周期

template<class T>
class weak_ptr
{
public:
	weak_ptr()
		: _ptr(nullptr)
	{}

	weak_ptr(const shared_ptr<T>& sp)
		: _ptr(sp.get())
	{}

	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

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

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

template<class T>
struct ListNode
{
	T _val;
	weak_ptr<ListNode<T>> _prev;
	weak_ptr<ListNode<T>> _next;

	ListNode(const T& val = T())
		: _val(val)
	{}
};

void test_shared_ptr()
{
	shared_ptr<ListNode<int>> n1 = new ListNode<int>;
	shared_ptr<ListNode<int>> n2 = new ListNode<int>;

	n1->_next = n2;
	n2->_prev = n1;
}

正因其作用就是配合 shared_ptr 打破循环引用,所以 weak_ptr 没有原生指针的构造函数,只有默认构造和 shared_ptr 的构造函数。

将节点内的指针换为 weak_ptr,则让两个节点的计数均为1,当 n1 和 n2 析构时,计数便能正常减到0,从而释放节点空间。

2.4 定制删除器

试想一下,如果不是用 new 来申请资源,应该如何进行正确地释放资源呢?这就涉及到定制删除器的设计。

默认情况下,我们应该用 delete 进行释放,而对于特殊的资源申请方式,我们要传对应的删除器(如函数对象、lambda表达式等)进行特定的删除。

unique_ptr

template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr = nullptr, function<void(T*)> del = [](T* ptr) {delete ptr; })
		: _ptr(ptr)
		, _del(del)
	{}

	~unique_ptr()
	{
		_del(_ptr);
	}

	//...
private:
	T* _ptr;
	function<void(T*)> _del;
};

template<class T>
struct DelArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

void test_unique_ptr()
{
	unique_ptr<ListNode<int>> up1(new ListNode<int>[10], DelArray<ListNode<int>>());
	unique_ptr<ListNode<int>> up2(new ListNode<int>[10], [](ListNode<int>* ptr) {delete[] ptr; });
	unique_ptr<FILE> up3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });

	unique_ptr<ListNode<int>> up4(new ListNode<int>);
}

shared_ptr

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr, function<void(T*)> del = [](T* ptr) {delete ptr; })
		: _ptr(ptr)
		, _del(del)
	{}

	~shared_ptr()
	{
		release();
	}
	
	//...
private:
	void release()
	{
		if (--(*_pcount) == 0)
		{
			_del(_ptr);
			delete _pcount;
		}
	}
	
	T* _ptr;
	int* _pcount = new int(1);
	function<void(T*)> _del;
};

template<class T>
struct DelArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

void test_shared_ptr()
{
	shared_ptr<ListNode<int>> sp1(new ListNode<int>[10], DelArray<ListNode<int>>());
	shared_ptr<ListNode<int>> sp2(new ListNode<int>[10], [](ListNode<int>* ptr) {delete[] ptr; });
	shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });

	shared_ptr<ListNode<int>> sp4(new ListNode<int>);
}

我们为了实现定制删除器,新增了一个成员变量 _del,由于参数类型无法显式定义,所以使用function函数包装器,默认缺省为 delete 的lambda表达式。

同时,默认构造函数简化为一个双参数构造函数,可以自由选择是否显式传入自定义删除器。

总结

C++智能指针的优势:

  1. 自动内存管理:智能指针在超出作用域时自动释放所管理的内存资源,避免内存泄漏、悬挂指针和重复释放
  2. 异常安全:智能指针的使用符合RAII原则,在对象创建时获取资源,并在对象销毁时释放资源,即使发生异常也能确保资源被正确释放。
  3. 定制删除器:智能指针允许自定义删除器,可以管理各种资源,而不仅仅是内存,比如文件句柄、网络连接等。
  4. 线程安全:一些智能指针(如shared_ptr)的引用计数是线程安全的,可以安全地在多线程环境中使用。
  5. 调试和维护:智能指针提供的接口和操作符重载使得调试信息更加清晰明了,易于追踪对象的生命周期,提高了代码的可维护性和可读性。

真诚点赞,手有余香

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1867899.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux基础 - 使用 ssh 服务管理远程主机(window linux vscode)

目录 零. 简介 一. 打开linux shh 二. window连接linux 三. linux连接linux 四. VSCode远程 零. 简介 SSH&#xff08;Secure Shell&#xff09;服务是一种网络协议&#xff0c;主要用于在不安全的网络环境中为计算机之间的通信提供安全的加密连接。 SSH 服务具有以下重要…

git 中有关 old mode 100644、new mode 10075的问题解决小结

问题&#xff1a; 同一个文件被修改后&#xff0c;最后代码没有变&#xff0c;文件变了&#xff0c;导致提交了一个空文件 git diff 提示 filemode 发生改变&#xff08;old mode 100644、new mode 10075&#xff09; 解决办法 &#xff1a; 原来是 filemode 的变化&#xff…

[leetcode]first-unique-character-in-a-string 字符串中的第一个唯一字符

. - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int firstUniqChar(string s) {unordered_map<int, int> frequency;for (char ch: s) {frequency[ch];}for (int i 0; i < s.size(); i) {if (frequency[s[i]] 1) {return i;}}return -1;} };

【Android】【Compose】Compose里面的Row和Column的简单使用

内容 Row和Column的简单使用方式和常用属性含义 Row 在 Jetpack Compose 中&#xff0c;Row 是一种用于在水平方向排列子元素的布局组件。它类似于传统 Android 中的 LinearLayout&#xff0c;但更加灵活和强大。 Row的代码 Composable inline fun Row(modifier: Modifier…

马斯克的SpaceX星舰有多牛?我们离殖民火星还有多远?

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 埃隆马斯克是一位知名的企业家和工程师&#xff0c;他掌握着多家公司&#xff0c;涉及多个领域&#xff0c;包括电动汽车、太空探索、太阳能、脑…

钉钉在MAKE 2024大会上宣布开放AI生态;NBC将用AI主播播报巴黎奥运会内容

&#x1f680; 钉钉在MAKE 2024大会上宣布开放AI生态 摘要&#xff1a;钉钉总裁叶军在MAKE 2024生态大会上宣布&#xff0c;钉钉将对所有大模型厂商开放&#xff0c;构建“国内最开放AI生态”。目前已有六家大模型厂商接入钉钉&#xff0c;用户可直接使用七家大模型产品。未来…

无人机赋能空间规划

城乡规划 高效构建实景三维模型&#xff0c;直 观反映地貌与建筑信息&#xff0c;辅 助设计人员进行科学规划。 业务挑战 BIM设计图、道路矢量图、卫星影像图无法进行精准匹配 传统测绘方式获得的二维图无法展示三维环境信息 BIM设计图与实景模型差异大&#xff0c;规划效…

Taro +vue3 中的微信小程序中的分享

微信小程序 右上角分享 的触发 以及配 useShareAppMessage(() > {return {title: "电影属全国通兑券",page: /pages/home/index,imageUrl: "http:///chuanshuo.jpg",};}); 置 就是Taro框架中提供的一个分享Api 封装好的

MQTT遗嘱信息(1)

本文内容参考&#xff1a; 什么是MQTT遗嘱消息&#xff1f;如何配置和处理遗嘱消息&#xff1f;_mqtt last will-CSDN博客 【MQTT基础篇&#xff08;十五&#xff09;】MQTT遗嘱_last-will qos-CSDN博客 MQTT 协议学习&#xff1a;Retained&#xff08;保留消息&#xff09;…

【Python实战】零基础实战教程(一) Hello World!

【Python实战】零基础Python实战教程 一、前言二、官方 Python3.12 版本安装教程1、下载2、安装3、测试 三、安装 Python 编译器1、下载2、安装3、启动 四、Hello World!1、创建项目2、创建 hellow_world.py 文件3、在控制台打印 Hellow World&#xff01; 一、前言 由于公司新…

期货的杠杆怎么计算?

什么是杠杆系数 杠杆系数是指期货合约价值与保证金之间的比例。它表示投资者只需投入少量资金&#xff0c;就可以控制价值更高的期货合约。杠杆系数越高&#xff0c;投资者的资金放大倍数就越大&#xff0c;但风险也越大。 什么是期货保证金呢&#xff1f; 期货保证金&…

微前端框架是为了解决项目应用在大型项目中带来的复杂性和维护难题而提出的技术方案。

微前端框架是为了解决单页应用&#xff08;SPA&#xff09;在大型项目中带来的复杂性和维护难题而提出的技术方案。Qiankun.js、MicroApp 和 Wujie 是三种流行的微前端框架。以下是对这三种框架的优缺点分析&#xff1a; Qiankun.js 优点 成熟度高&#xff1a;Qiankun.js 基…

麦克风什么牌子的音质效果好?揭秘领夹麦克风哪个牌子音质好

近年来&#xff0c;随着网络直播、短视频、网课等新兴行业的兴起&#xff0c;大家对麦克风的需求量不断增加。而领夹麦克风作为一种轻便、便携的录音设备&#xff0c;也是得到了广大用户的青睐。一款优质的领夹麦克风能够带来更清晰、更真实的声音效果&#xff0c;让我们在在各…

SAP ABAP 常用实用类

文章目录 前言一、输出 展示 数据信息 a.将 JSON 格式化为可读 并以弹框形式输出 b.将内表内容以表格形式输出 c.弹框形式显示 HTML 内容。也能显示包含js 的html。也可以显示pdf 图片 二、输入 获取 数据信息 a.弹框 添加 输入框…

python e怎么表示

exp()方法返回x的指数&#xff0c;ex。 语法 以下是 exp() 方法的语法: import math math.exp( x ) 注意&#xff1a;exp()是不能直接访问的&#xff0c;需要导入 math 模块&#xff0c;通过静态对象调用该方法。 参数 x -- 数值表达式。 返回值 返回x的指数&#xff0c;…

怎样恢复数据?原来只要3个方法,真是救大命了

无论是工作文件&#xff0c;还是个人的照片、视频&#xff0c;手机数据都承载着我们的记忆和努力。但如果不小心删除了&#xff0c;我们该怎样恢复数据呢&#xff1f;其实&#xff0c;恢复数据并不是一件复杂的事情&#xff0c;只要掌握正确的方法&#xff0c;我们就能有效地找…

Amazon Q:对话智能赋能企业发展

在最近举办的亚马逊云科技大会上&#xff0c;引人瞩目的消息是 Amazon Q 的推出&#xff0c;这是一款专注于生成式 AI 支持的新型助手。与其他智能助手相比&#xff0c;Amazon Q 助手聚焦在办公场景&#xff0c;具有针对性&#xff0c;旨在提供更为准确和个性化的服务。 一、Am…

TikTok短视频矩阵系统

随着数字化时代的到来&#xff0c;短视频已成为人们获取信息、娱乐消遣的重要渠道。TikTok&#xff0c;作为全球最受欢迎的短视频平台之一&#xff0c;其背后的短视频矩阵系统是支撑其成功的关键因素。本文将深入探讨TikTok短视频矩阵系统的构成、功能以及它在新媒体时代中的影…

面试-Java线程池

1.利用Excutors创建不同的线程池满足不同场景的需求 分析&#xff1a; 如果并发的请求的数量非常多&#xff0c;但每个线程执行的时间非常短&#xff0c;这样就会频繁的创建和销毁线程。如此一来&#xff0c;会大大降低系统的效率。 可能出现&#xff0c;服务器在为每个线程创建…

【Python实战因果推断】2_因果效应异质性2

目录 CATE with Regression Evaluating CATE Predictions CATE with Regression 我想你可能已经预料到了&#xff1a;与应用因果推理中的大多数情况一样&#xff0c;答案往往从线性回归开始。但在走这条路之前&#xff0c;让我们把事情变得更具体一些。假设你在一家遍布全国的…