C++11 --- 智能指针

news2024/12/23 9:42:29

序言

 在使用 C / C++ 进行编程时,许多场景都需要我们在堆上申请空间,堆内存的申请和释放都需要我们自己进行手动管理。这就存在容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题,这对长期运行的程序来说是致命的!
 但在 C++11 中引入了智能指针,使我们将内存的管理交给智能指针。


1. 什么是智能指针

1.1 概念

 该指针旨在自动管理动态分配的内存,减少内存泄漏和野指针的问题。智能指针是模板类,它们的行为类似于指针,但提供了自动的内存管理功能

1.2 RAII 思想

 智能指针在 C++ 中主要使用了 资源获取即初始化(Resource Acquisition Is Initialization, RAII) 的思想,以及所有权管理的概念。RAII 的核心思想是,资源的分配(获取)和初始化发生在对象的构造期间,而 资源的释放(清理)则发生在对象的析构期间。这种方式通过对象的生命周期来自动管理资源,避免了忘记释放资源(如内存泄漏)的问题。

1.3 体现 RAII 思想

 在这里为大家举一个简单的例子来体现 RAII 思想

这是我们现在的内存管理,我们需要手动释放申请的资源:

int main()
{
	int* Ptr = new int[5];

	// Do Otherthing......

	delete[] Ptr;
	return 0;
}

现在实现一个简单的智能指针:

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* Ptr)
		:_Ptr(Ptr)
	{}

	// Ohther Functions... 

	~SmartPtr()
	{
		std::cout << "Delete Ptr" << std::endl;
		delete _Ptr;
	}

private:
	T* _Ptr;
};

int main()
{
	SmartPtr<int> sp(new int(1));
	return 0;
}

我们将申请的资源交给智能指针为我们管理,当程序结束时,将自动执行析构函数释放资源:
在这里插入图片描述


2. 三种主要的智能指针

2.1 unique_ptr

unique_ptr 是一种 独占所有权 的智能指针,意味着 同一时间内只能有一个 unique_ptr 指向给定的对象,他直接删除了他的拷贝构造和赋值运算符重载:
在这里插入图片描述在这里插入图片描述
注意:保留了对将亡值的赋值运算符重载,因为就修改操作后将亡值就释放了,依旧满足独占所有权的特性!

简单使用一下该指针:

void Ptr_1()
{
	std::unique_ptr<MyClass> up1(new MyClass); 
	up1->DoSomething();

	std::unique_ptr<MyClass> up2(move(up1)); // 移动构造
	std::cout << up1 << std::endl; // up1 这时已被悬空
}

程序输出结果:

I am working!
0000000000000000
~MyClass()

 该智能指针还是比较简单的,但是一定要注意指针被悬空后的情况!

2.2 shared_ptr

shared_ptr 是一种 共享所有权 的智能指针,允许多个 shared_ptr 实例指向同一个对象。每个 shared_ptr 都有一个与之关联的计数器,称为控制块,用于跟踪有多少个 shared_ptr 实例指向该对象。当最后一个指向对象的 shared_ptr 被销毁或重置时,对象才会被删除。

简单使用一下该指针:

void Ptr_2()
{
	std::shared_ptr<MyClass> sp1(new MyClass); // 构造 
	std::shared_ptr<MyClass> sp2(sp1); // 拷贝构造
	sp2->DoSomething();

	std::cout << "The use counts = " << sp1.use_count() << std::endl; // 查看计数器
}

程序输出结果:

I am working!
The use counts = 2
~MyClass()


循环引用问题

 使用该指针需要注意一个非常特殊的情况,一不小心掉入坑中!这种情况就是循环引用:

struct Node
{
	Node(int val)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}

	~Node()
	{
		std::cout << "~Node()" << std::endl;
	} 

	int _val;
	std::shared_ptr<Node> _prev;
	std::shared_ptr<Node> _next;
};

void Ptr_3()
{
	std::shared_ptr<Node> sp1(new Node(1));
	std::shared_ptr<Node> sp2(new Node(2));

	// 相互指向
	sp1->_next = sp2;
	sp1->_prev = sp1;
}

运行程序,我们会发现并没有正常的未释放资源!出现问题的原因,用图来表示:
在这里插入图片描述
当我们函数结束时,函数栈帧销毁,这时两个指针对象调用析构函数来释放资源:
在这里插入图片描述
这里的析构函数并不会真正意义上调用 delete ,而是减少引用!直到引用计数为 0 才会调用 delete,所以,这里的资源并没有真正的被释放,因为 next,prev 指针的存在,所以资源并不会被释放!

2.3 weak_ptr

weak_ptr 是一种 不拥有其所指向对象的智能指针,它主要 用于解决 shared_ptr 之间的循环引用问题weak_ptr 必须与 shared_ptr 一起使用,因为它不拥有对象,所以不会增加对象的共享计数。
 简单使用一下该指针:

void Ptr_4()
{
	// std::weak_ptr<int> wp(new int(1)); // 错误的使用方法 weak_ptr 不能直接管理对象的生命周期

	std::shared_ptr<int> sp(new int(1));
	std::weak_ptr<int> wp(sp);
	std::cout << wp.use_count() << std::endl; 
}

现在我们使用 weak_ptr 来解决循环引用的问题:

struct Node
{
	Node(int val)
		:_val(val)
	{}

	~Node()
	{
		std::cout << "~Node()" << std::endl;
	} 

	int _val;
	std::weak_ptr<Node> _prev;
	std::weak_ptr<Node> _next;
};

void Ptr_3()
{
	std::shared_ptr<Node> sp1(new Node(1));
	std::shared_ptr<Node> sp2(new Node(2));
	
	// 相互指向
	sp1->_next = sp2;
	sp2->_prev = sp1;
}

_prev, _next 修改为 weak_ptr 来代表不进行引用计数的增加,只是简单的指向,资源现在被正常的释放!

2.4 自定义删除器

 在智能指针的底层,对于资源的释放,单个就使用 delete,数组就是使用 delete[] ,大绝大多数场景下都是没问题的。但是,总是有特殊情况:

void Ptr_5()
{
	std::shared_ptr<FILE> sp(fopen("test.txt", "w"));
}

请问,这个使用 delete 可以删除吗?当然是不可以,有人会觉得,这不是在鸡蛋里挑骨头吗?其实,我们很多时候就是更应该想到极端情况,Bug 不能被消除,但可以被极力避免!我们程序的健壮性,肯定决定了我们运行的稳定性!

 这时,我们就可以使用自定义删除器:
在这里插入图片描述
我们需要传递一个可调用对象告诉他,该怎么删除。选择很多,包括函数指针,仿函数… 在这里我们选择 lambda ,这就非常的方便!

std::shared_ptr<FILE> sp(fopen("test.txt", "w"), [](FILE* file) {fclose(file); });

3. 简单实现

 在这里我们简单实现一个 shared_ptr

3.1 构造函数

 首先,先介绍三个成员变量:

T* _Ptr;
std::atomic<int>* _RefCounts; // 引用计数(保证原子性)
std::function<void(T*)> _Del; // 自定义删除器
  • _Ptr:是我们需要管理的资源
  • _RefCounts:计数器,记录多少指针指向该资源(本质就是 int,但是支持原子性操作)
  • _Del:删除器,保证资源正常的释放,有特殊删除需求可传入

一共实现了简单的三个构造函数:

// 构造函数(默认删除器)
SharedPtr(T* Ptr)
	: _Ptr(Ptr)
	, _RefCounts(new std::atomic<int>(1))
	, _Del([](T* val) { delete val; })
{}

// 构造函数(自定义删除器)
template<class D>
SharedPtr(T* Ptr, D Del)
	: _Ptr(Ptr)
	, _RefCounts(new std::atomic<int>(1))
	, _Del(Del)
{}

// 拷贝构造
SharedPtr(const SharedPtr<T>& sp)
	: _Ptr(sp._Ptr)
	, _RefCounts(sp._RefCounts)
	, _Del(sp._Del)
{
	++(*_RefCounts);
}

3.2 析构函数

 该函数在释放资源前需要判断,当引用计数为 0 时才可释放资源,避免正在使用的指针被悬空:

// 当计数置 0 时调用
void destructor()
{
	_Del(_Ptr);
	_Ptr = nullptr;
	delete _RefCounts;
	_RefCounts = nullptr;
}

// 析构函数
~SharedPtr()
{
	// 引用减少
	--(*_RefCounts);
	if (*_RefCounts == 0)
	{
		destructor();
	}
}

3.3 赋值运算符重载

 赋值运算符重载需要极其注意,在指向其他资源前需要对当前资源释放(引用计数减一,若为 0,才真正释放资源):

void clear()
{
	// 引用减少
	--(*_RefCounts);
	if (*_RefCounts == 0)
	{
		destructor();
	}
}

SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
	if (this != &sp) // 防止自我赋值
	{
		clear(); // 释放当前资源

		_Ptr = sp._Ptr;
		_RefCounts = sp._RefCounts;
		_Del = sp._Del; // 复制删除器
		++(*_RefCounts);
	}
	return *this;
}

3.4 其余的运算符重载

 此部分为常用的运算符重载:

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

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

4. 总结

 在这篇文章中我们首先介绍了智能指针的思想,之后分别介绍了常用的三种智能指针(unique_ptr, shared_ptr, weak_ptr),最后我们简单的实现了第二个指针,希望大家有所收获!

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

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

相关文章

QML入门之创建可重用的组件(一)

我们在日常开发中都会封装一些组件以便于项目内重复利用。QML创建可重用组件一般有两种方法。 自定义Item使用Component创建自定义组件 自定义Item 以一个自定义按钮举例&#xff1a; import QtQuick 2.12Rectangle {id: root// 自定义属性property string btnDis: qsTr(&qu…

Windows环境利用VS2022编译 libvpx 源码教程

libvpx libvpx 是一个开源的视频编码库&#xff0c;由 WebM 项目开发和维护&#xff0c;专门用于 VP8 和 VP9 视频编码格式的编解码处理。它支持高质量的视频压缩&#xff0c;广泛应用于视频会议、在线教育、视频直播服务等多种场景中。libvpx 的特点包括跨平台兼容性、硬件加速…

JavaSE-易错题集-002

1. 下面有关java基本类型的默认值和取值范围&#xff0c;说法错误的是&#xff1f; A 字节型的类型默认值是0&#xff0c;取值范围是-2^7—2^7-1 B boolean类型默认值是false&#xff0c;取值范围是true\false C 字符型类型默认是0&#xff0c;取值范围是-2^15 —2^15-1 D l…

iOS——retain和release底层原理

retain实现原理 retain的源码&#xff1a; //使用此方法等价于使用[this retain] inline id objc_object::retain() {//确保对象不是tagged pointerASSERT(!isTaggedPointer());return rootRetain(false, RRVariant::FastOrMsgSend); }ALWAYS_INLINE id objc_object::rootR…

关系代数 | 数据库SQL

文章目录 关系运算符笛卡尔积笛卡尔积应用 运算符符号含义集合运算符并∪交∩差-笛卡尔积专门的关系运算符选择σ投影π连接⋈除 关系运算符 笛卡尔积 集合运算符中&#xff0c;主要对笛卡尔积做解释&#xff1a; 在数学中&#xff0c;两个集合X和Y的笛卡儿积&#xff08;英语…

【Linux】进程控制(一)

1. 进程创建 &#xff08;一&#xff09;认识fork函数 从已存在进程中创建一个新进程&#xff08;新进程为子进程&#xff0c;而原进程为父进程&#xff09; 进程调用fork&#xff0c;当控制转移到内核中的fork代码后&#xff0c;内核做&#xff1a; 分配新的内存块和内核数…

Allegro PCB--报错

1。 走线上打孔 问题&#xff1a;在走线上打的Via&#xff0c;我通过"Assign net to Via", 给与网络。成功后。 跑Tools\Database check\ Update all DRC(including batch), Via 网络又没有了 原因& 解决方法&#xff1a; VIA没有和走线完全重合 换个方法&#x…

【吊打面试官系列-Redis面试题】说说 Redis 哈希槽的概念?

大家好&#xff0c;我是锋哥。今天分享关于 【说说 Redis 哈希槽的概念&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 说说 Redis 哈希槽的概念&#xff1f; Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念&#xff0c;Redis 集群有 16384 个哈希槽&a…

高精度加法,减法,乘法,除法

加法&#xff1a; 大整数该如何储存&#xff1f; 用数组储存&#xff1a; 把个位放在数下标为0的位置&#xff0c;十位放在数组下标为1的位置&#xff08;也就是高位放在数组的后面&#xff09; 因为这样&#xff0c;如果需要增加一位最高位&#xff0c;那我们就可以直接在…

C语言小游戏--贪吃蛇实现

C语言小游戏--贪吃蛇实现 1.游戏实现背景2.Win32 API介绍2.1什么是Win32 API2.2控制台程序(Console)2.3控制台屏幕的坐标COORD2.4GetStdHandle2.4.1函数语法2.4.2函数的使用 2.5GetConsoleCursorInfo2.5.1函数语法2.5.2函数的使用 2.6CONSOLE_CURSOR_INFO2.6.1结构体结构2.6.2结…

自制游戏手柄--电位器的使用

在前面的讨论中&#xff0c;我们考虑了使用陀螺仪来获取手柄的运动情况来进行瞄准&#xff0c; 自制实战吃鸡手柄原理-CSDN博客 也可以使用图像识别来实现&#xff0c;这里我们再考虑下使用电位器来获取运动状态&#xff0c;一个电位器可以获取到一个平面上的旋转情况&#x…

2025年25届新文出炉:如何打造Java SpringBoot Vue个性化课程推荐系统?

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

根据NVeloDocx Word模板引擎生成Word(二)

前面讲到了根据“永久免费开放的E6低代码开发平台”的NVeloDocx Word模版引擎生成Word文件的基础取数方法&#xff0c;包括取本表单字段以及引用字段&#xff0c;详见《根据NVeloDocx Word模板引擎生成Word&#xff08;一&#xff09;》。 针对这种基本的取数方法&#xff0c;…

枚举相关知识点

1.是用户定义的数据类型&#xff0c;为一组相关的常量赋予有意义的名字。 2.enum常量本身带有类型信息&#xff0c;即Weekday.SUN类型是Weekday&#xff0c;编译器会自动检查出类型错误&#xff0c;在编译期间可检查错误。 3.enum定义的枚举类有什么特点。 a.定义的enu…

Ubuntu之源码编译安装nginx

参考&#xff1a;Ubuntu之源码编译安装nginx_ubuntu编译安装nginx-CSDN博客 1.下载源码后进入源码目录&#xff0c;如下&#xff1a; cd /home/jq/wf/nginx-1.26.1 2.下载相应依赖库&#xff1a; apt-get install libpcre3-dev apt-get install openssl libssl-dev apt-get…

互联网中的情绪价值

在互联网中&#xff0c;信息的传递和分享变得越来越便捷了&#xff0c;同时&#xff0c;「情绪价值」在虚拟世界中的作用也愈加凸显。 无论是在社交媒体、即时通讯工具、各类论坛社区以及短视频平台里&#xff0c;情绪价值已然成为一种无形但是至关重要的资产&#xff0c;在默默…

BaseCTF-[Week3] 出题人已疯-快坚持不下去的第六天

DIE 查&#xff0c;发现是 .NET&#xff0c;拖⼊ dnSpy 加密的代码 private void Btn_Submit_Click(object sender, RoutedEventArgs e) { char[] array this.Tb_Input.Text.ToCharArray(); char[] array2 string.Join("", this.sentences).ToCharArray(); for (i…

php、Java、python酒店预约与送餐系统 酒店管理系统 酒店预订入住系统(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

C++第四十七弹---深入理解异常机制:try, catch, throw全面解析

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1.C语言传统的处理错误的方式 2.C异常概念 3. 异常的使用 3.1 异常的抛出和捕获 3.2 异常的重新抛出 3.3 异常安全 3.4 异常规范 4.自定义…

宠物狗检测-目标检测数据集(包括VOC格式、YOLO格式)

宠物狗检测-目标检测数据集&#xff08;包括VOC格式、YOLO格式&#xff09; 数据集&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1roegkaGAURWUVRR-D7OzzA?pwddxv6 提取码&#xff1a;dxv6 数据集信息介绍&#xff1a; 共有20580 张图像和一一对应的标注文件 标…