C++内存列传之RAII宇宙:智能指针

news2025/6/7 12:12:33

文章目录

  • 1.为什么需要智能指针?
  • 2.智能指针原理
    • 2.1 RAll
    • 2.2 像指针一样使用
  • 3.C++11的智能指针
    • 3.1 auto_ptr
    • 3.2 unique_ptr
    • 3.3 shared_ptr
    • 3.4 weak_ptr
  • 4.删除器
  • 希望读者们多多三连支持
  • 小编会继续更新
  • 你们的鼓励就是我前进的动力!

智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new / delete 操作,从而显著减少内存泄漏和悬空指针的风险

1.为什么需要智能指针?

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

如果 p1 这里 new 抛异常会如何?

p1 未成功分配,值为 nullptr
函数直接跳转到 catch 块,p2 未分配,无内存泄漏

如果 p2 这里 new 抛异常会如何?

p1 已分配但未释放,导致内存泄漏
函数跳转到 catch 块,p2 未分配,delete p1delete p2 均未执行

如果 div 调用这里又会抛异常会如何?

p1p2 均已分配但未释放,导致双重内存泄漏
函数跳转到 catch 块,打印错误信息(如 “除 0 错误”)

C++ 不像 java 具有垃圾回收机制,能够自动回收开辟的空间,需要自行手动管理,但是自己管理有时又太麻烦了,况且这里只是两个指针就产生了这么多问题,因此在 C++11 就推出了智能指针用于自动管理内存

2.智能指针原理

2.1 RAll

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

private:
	T* _ptr;
};

int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<string> sp2(new string("xxx"));
	return 0;
}

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

简单来说,就是把创建的对象给到 SmartPtr 类来管理,当对象的生命周期结束的时候,刚好类也会自动调用析构函数进行内存释放

这种做法有两大好处:

  • 不需要显式地释放资源
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效

2.2 像指针一样使用

都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可
以通过 -> 去访问所指空间中的内容,因此类中还得需要将 *-> 重载下,才可让其像指针一样去使用

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

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

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

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

* 重载返回对象,-> 重载返回地址,这部分的知识点在迭代器底层分析已经讲过很多遍了,就不过多叙述了,可自行翻阅前文

3.C++11的智能指针

智能指针一般放在 <memery> 文件里,C++11 也参考了第三方库 boost

  1. C++ 98 中产生了第一个智能指针 auto_ptr
  2. C++ boost 给出了更实用的 scoped_ptrshared_ptrweak_ptr
  3. C++ TR1,引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版
  4. C++ 11,引入了 unique_ptrshared_ptrweak_ptr。需要注意的是 unique_ptr对应 boostscoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的

3.1 auto_ptr

template<class T>
class auto_ptr
{
public:
	// RAII
	// 像指针一样
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~auto_ptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

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

	T* operator->()
	{
		return _ptr;
	}
	
	// ap3(ap1)
	// 管理权转移
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}

	auto_ptr<T>& operator=(auto_ptr<T>& ap) 
	{
		if (this != &ap) 
		{
			_ptr = ap._ptr;       // 转移所有权
			ap._ptr = nullptr;    // 原指针置空
		}
		return *this;
	}
private:
	T* _ptr;
};

auto_ptrC++98 就已经被引入,实现了智能指针如上面所讲的最基础的功能,同时他还额外对拷贝构造、= 重载进行了显式调用,但是这种拷贝虽然能解决新对象的初始化,但是对于被拷贝的对象,造成了指针资源所有权被转移走,跟移动构造有些类似

因此,auto_ptr 会导致管理权转移,拷贝对象被悬空,auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr

3.2 unique_ptr

template<class T>
class unique_ptr
{
public:
	// RAII
	// 像指针一样
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

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

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

	// ap3(ap1)
	// 管理权转移
	// 防拷贝
	unique_ptr(unique_ptr<T>& ap) = delete;
	unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
private:
	T* _ptr;
};

unique_ptr 很简单粗暴,直接禁止了拷贝机制

因此,建议在不需要拷贝的场景使用该智能指针

3.3 shared_ptr

template<class T>
class shared_ptr
{
public:
	// RAII
	// 像指针一样
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}

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

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

	// sp3(sp1)
	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)
			return *this;

		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);

		return *this;
	}

	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}

private:
	T* _ptr;
	int* _pcount;
};

C++11 中的智能指针就属 shared_ptr 使用的最多,因为它解决了赋值造成的资源被转移可能会被错误访问的问题

类中增加一个新的指针 _pcount 用于计数,即计数有多少个 _ptr 指向同一片空间,多个 shared_ptr 可以同时指向同一个对象,每次创建新的 shared_ptr 指向该对象,引用计数加 1;每次 shared_ptr 析构或者被赋值为指向其他对象,引用计数减 1。当最后一个指向该对象的 shared_ptr 析构时,对象会被自动删除,从而避免内存泄漏

🔥值得注意的是: shared_ptr 同时也支持了无法自己给自己赋值,这里还涉及一些关于线程安全的知识点,待 Linux 学习过后再来补充

3.4 weak_ptr

看似完美的 shared_ptr 其实也会有疏漏,比如:引用循环

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

当执行 node1->next = node2node2->prev = node1 时,node1 内部的 _next 指针指向 node2node2 内部的 _prev 指针指向 node1 。这就导致两个节点之间形成了循环引用关系。此时,由于互相引用,每个节点的引用计数都变为 2 ,因为除了外部的智能指针引用,还多了来自另一个节点内部指针的引用

node1node2 智能指针对象离开作用域开始析构时,它们首先会将所指向节点的引用计数减 1 。此时,每个节点的引用计数变为 1 ,而不是预期的 0 。这是因为 node1_next 还指向 node2node2_prev 还指向 node1 ,使得它们的引用计数无法归零

对于 shared_ptr 来说,只有当引用计数变为 0 时才会释放所管理的资源。由于这种循环引用的存在,node1 等待 node2 先释放(因为 node2_prev 引用着 node1 ),而 node2 又等待 node1 先释放(因为 node1_next 引用着 node2 ),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏

class ListNode 
{
public:
    weak_ptr<ListNode> _next; 
    weak_ptr<ListNode> _prev;
};

为了解决 shared_ptr 的循环引用问题,通常可以使用 weak_ptrweak_ptr 是一种弱引用智能指针,它不会增加所指向对象的引用计数。将循环引用中的某一个引用(比如 ListNode 类中的 _prev_next 其中之一)改为 weak_ptr 类型,就可以打破循环引用

因此,weak_ptr 是一种专门解决循环引用问题的指针

4.删除器

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class A 
{
public:
    ~A() 
    { 
        cout << "A::~A()" << endl; 
    }
};

// 仿函数删除器:用于释放malloc分配的内存
template<class T>
struct FreeFunc 
{
    void operator()(T* ptr) const 
    {
        cout << "FreeFunc: free memory at " << ptr << endl;
        free(ptr);
    }
};

// 仿函数删除器:用于释放数组
template<class T>
struct DeleteArrayFunc 
{
    void operator()(T* ptr) const 
    {
        cout << "DeleteArrayFunc: delete[] memory at " << ptr << endl;
        delete[] ptr;
    }
};

int main() 
{
    // 使用FreeFunc删除器的shared_ptr
    shared_ptr<int> sp1((int*)malloc(sizeof(int)), FreeFunc<int>());
    *sp1 = 100;
    cout << "sp1: " << *sp1 << " at " << sp1.get() << endl;
    // 离开作用域时调用FreeFunc删除器

    // 使用DeleteArrayFunc删除器的shared_ptr
    shared_ptr<int> sp2(new int[5], DeleteArrayFunc<int>());
    for (int i = 0; i < 5; ++i) {
        sp2.get()[i] = i;
    }
    cout << "sp2 array:";
    for (int i = 0; i < 5; ++i) {
        cout << " " << sp2.get()[i];
    }
    cout << endl;
    // 离开作用域时调用DeleteArrayFunc删除器

    // 使用lambda删除器管理A对象数组
    shared_ptr<A> sp4(new A[3], [](A* p) {
        cout << "Lambda: deleting array at " << p << endl;
        delete[] p;
        });
    cout << "sp4 array of A objects created" << endl;
    // 离开作用域时调用lambda删除器

    // 使用lambda删除器管理文件句柄
    shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {
    if (p) 
    {
        cout << "Lambda: closing file" << endl;
        fclose(p);
    }
    });
    if (sp5) 
    {
        fprintf(sp5.get(), "Hello, shared_ptr with deleter!\n");
        cout << "File written" << endl;
    }
    // 离开作用域时调用lambda删除器关闭文件

    return 0;
}

对于所有的指针不一定是 new 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

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

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

相关文章

PVE 虚拟机安装 Ubuntu Server V24 系统 —— 一步一步安装配置基于 Ubuntu Server 的 NodeJS 服务器详细实录1

前言 最近在基于 NodeJS V22 写一个全栈的项目&#xff0c;写好了&#xff0c;当然需要配置服务器部署啦。这个过程对于熟手来说&#xff0c;还是不复杂的&#xff0c;但是对于很多新手来说&#xff0c;可能稍微有点困难。所以&#xff0c;我把整个过程全部记录一下。 熟悉我…

TDengine 开发指南——高效写入

高效写入 本章内容将介绍如何发挥 TDengine 最大写入性能&#xff0c;通过原理解析到参数如何配置再到实际示例演示&#xff0c;完整描述如何达到高效写入。 为帮助用户轻松构建百万级吞吐量的数据写入管道&#xff0c;TDengine 连接器提供高效写入的特性。 启动高效写入特性…

Linux kill 暂停命令

暂停进程 kill -19 在一台服务器上部署了360Pika服务&#xff0c;先用RedisClient连接一下&#xff0c;可以连接 现在暂停进程 暂停后发现再次连接无法连接 恢复进程 kill -18 恢复后可连接

2.0 阅读方法论与知识总结

引言 本文将详细分析考研英语阅读做题步骤&#xff0c;并对方法论进行总结&#xff0c;最后通过真题练习巩固方法。 一、做题步骤 所有技巧都建立在精读真题的基础上&#xff01;建议按以下节奏复习&#xff1a; 1️⃣ 做题 先看题干了解文章大致主旨&#xff08;看看有没有…

5. Qt中.pro文件(1)

本节主要讲.pro文件的作用和一些相关基础知识与操作。 本文部分ppt、视频截图原链接&#xff1a;[萌马工作室的个人空间-萌马工作室个人主页-哔哩哔哩视频] 1 PRO文件 1.1 pro文件作用 添加需要用到的QT模块&#xff0c;如通过QT module_name来添加需要用到的Qt模块。指定生…

简数采集技巧之快速获取特殊链接网址URL方法

简数采集器列表页提取器的默认配置规则&#xff1a;获取a标签的href属性值作为采集的链接网址&#xff0c;对于大部分网站都是适用的&#xff1b; 但有些网站不使用a标签作为链接跳转&#xff0c;而用javascript的onclick事件替代&#xff0c;那列表页提取器的默认规则将无法获…

AI 如何改变软件文档生产方式?

现代软件工程中的文档革命&#xff1a;从附属品到核心组件的范式升级 在数字化转型浪潮席卷全球的当下&#xff0c;软件系统的复杂度与规模呈现指数级增长。据Gartner最新研究显示&#xff0c;超过67%的企业软件项目延期或超预算的根本原因可追溯至文档系统的缺陷。这一现象在…

激光干涉仪:解锁协作机器人DD马达的精度密码

在工业4.0的浪潮中&#xff0c;协作机器人正以惊人的灵活性重塑生产线——它们与工人并肩作业&#xff0c;精准搬运零件&#xff0c;完成精密装配。还能协同医生完成手术&#xff0c;甚至制作咖啡。 标准的协作机器人关节模组由角度编码器、直驱电机(DD马达)、驱动器、谐波减速…

HOPE800系列变频器安装到快速调试的详细操作说明

以下是HOPE800系列变频器从安装到调试的详细操作说明及重要参数设置&#xff0c;适用于工程技术人员或具备电气基础的操作人员。请严格遵循安全规范操作。 以下面电机铭牌为例&#xff1a; HOPE800变频器安装与调试指南** &#xff08;安全第一&#xff01;操作前务必断电并确…

vCenter与ESXi主机每分钟周期性断连修复

问题概述 最近我的测试服务器借给客户用作临时中转&#xff0c;仅更改了ESXi的管理IP&#xff0c;设备拿回来改回原来IP&#xff0c;vCenter开启后重新接收证书&#xff0c;主机和所有VM管理运行正常&#xff0c;跑着跑着发现主机和vCenter会频繁断开连接后又马上自动恢复&…

web3-区块链困境破解指南:从数字化签名到Rollup 到分片

web3-区块链三难困境破解指南&#xff1a;从数字化签名到Rollup 到分片 数字化签名 实体的签名&#xff1a;将交易和签名者绑定在一起 在数字世界的问题是&#xff1a; 任何人都可以从任一文档复制Bob的签名放到自己想放的地方。 解决方案&#xff1a;让签名由文件来决定 b…

李飞飞World Labs开源革命性Web端3D渲染器Forge!3D高斯溅射技术首次实现全平台流畅运行

在AI与3D技术深度融合的今天&#xff0c;李飞飞领衔的World Labs团队再次成为行业焦点。今日&#xff0c;他们正式开源了Forge——一款专为Web端设计的3D高斯溅射&#xff08;3D Gaussian Splatting&#xff09;渲染器&#xff0c;不仅支持THREE.js生态&#xff0c;更能在手机、…

小鹏汽车5月交付新车33525台 同比增长230%

6月1日&#xff0c;小鹏汽车公布5月交付数据&#xff0c;5月小鹏交付新车33,525台&#xff0c;同比增长230%&#xff0c;与4月交付35,045台相比下降4.3%&#xff0c;已连续7个月交付量突破30,000台。2025年1-5月&#xff0c;小鹏汽车累计交付新车162,578台&#xff0c;同比增长…

OpenCV——Mat类及常用数据结构

Mat类及常用数据结构 一、Mat类简介1.1、矩阵头1.2、矩阵的数据类型1.3、Mat的子类 二、矩阵数据的存储2.1、单通道2.2、多通道 三、创建矩阵的方法3.1、静态方法创建3.2、构造方法创建3.3、读取图像文件创建3.4、克隆创建 四、获取矩阵信息五、矩阵相关操作5.1、获取/修改像素…

每天总结一个html标签——Audio音频标签

Audio标签 文章目录 Audio标签一、audio标签的定义与介绍1. 定义介绍2. 语法3. 支持的格式4.文本提示 二、audio标签的HTML属性1. autoplay2. loop3. muted4. preload 三、audio标签的常用DOM属性四、audio标签的常用事件四、默认样式五、自定义样式1. 示例2. 代码 六、播放 m3…

web3-Remix部署智能合约到“荷兰式”拍卖及以太坊gas费机制细讲

web3-Remix部署智能合约到“荷兰式”拍卖及以太坊gas费机制细讲 一、使用Remix演示智能合约部署 智能合约的代码编写一般都是在Remix上&#xff0c;Remix的好处的话就是可以在浏览器中快速开发和部署合约&#xff0c;无需在本地安装任何程序&#xff0c;十分适合新手。 对应…

网络编程及原理(一)

目录 一 . 独立模式与网络互联 二 . 局域网 —— LAN &#xff08;1&#xff09;基于网线直连 &#xff08;2&#xff09;基于集线器组建 &#xff08;3&#xff09;基于交换机组建 &#xff08;4&#xff09;基于交换机和路由器组建 三 . 广域网 —— WAN 四 …

【Linux】进程 信号保存 信号处理 OS用户态/内核态

&#x1f33b;个人主页&#xff1a;路飞雪吖~ &#x1f320;专栏&#xff1a;Linux 目录 一、信号保存 ✨进程如何完成对信号的保存&#xff1f; ✨在内核中的表示 ✨sigset_t ✨信号操作函数 &#x1fa84;sigprocmask --- 获取或设置当前进程的 block表 &#x1fa84;s…

[ Qt ] | 与系统相关的操作(一):鼠标相关事件

目录 信号和事件的关系 (leaveEvent和enterEvent) 实现通过事件获取鼠标进入和鼠标离开 (mousePressEvent) 实现通过事件获得鼠标点击的位置 (mouseReleaseEvent) 前一个的基础上添加鼠标释放事件 (mouseDoubleClickEvent) 鼠标双击事件 鼠标移动事件 鼠标滚轮事件 …

stm32使用hal库模拟spi模式3

因为网上模拟spi模拟的都是模式0&#xff0c;很少有模式3的。 模式3的时序图&#xff0c;在clk的下降沿切换电平状态&#xff0c;在上升沿采样&#xff0c; SCK空闲为高电平 初始化cs&#xff0c;clk&#xff0c;miso&#xff0c;mosi四个io。miso配置为输入&#xff0c;cs、c…