【C++】智能指针模拟实现及详解

news2024/9/22 18:09:10

目录

什么是智能指针:

为什么要有智能指针:

auto_ptr:

unique_ptr:

shared_ptr:

shared_ptr的缺陷:

weak_ptr:


什么是智能指针:

概念:

        智能指针是一种特殊的类模板,用于自动管理具有动态分配生命周期的对象。它们通过模拟指针的行为来工作,但提供了自动的内存管理功能,从而减少了内存泄漏的风险。

        使用智能指针可以确保当它们所指向的对象超出作用域或被显式删除时,所指向的对象也会被自动删除。

通俗一点就是:智能指针就是一个类,使用RAII(Resource Acquisition Is Initialization)机制对普通指针进行一层封装,并重载*、-> 符号,让其可以像指针一样去使用,也就是让其用起来像个指针,本质是是一个对象,这样就可以方便的去管理一个对象的生命周期,减少内存泄漏的风险。

RAII:资源获取即初始化

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:1、不需要显示的释放资源,因为出了作用域会自动销毁。2、可以保证对象所需的资源在其生命周期内始终保持有效。

为什么要有智能指针:

在上面是什么智能指针的讲述中提到过一点:智能指针最重要的功能是提供自动的内存管理功能,减少内存泄露的风险。

这里先讲讲内存泄漏:

内存泄漏:

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

内存泄漏的危害:

        长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

内存泄漏分类:

我们一般关心以下两种方面的内存泄漏:

堆内存泄漏(Heap leak):

        堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统内存泄漏:

        指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

那么我们把眼光放在堆内存泄漏上,来看一下下面这段代码有没有什么问题:

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;
}

这段程序中可以看出:如果发生除0错误抛异常的话会直接跳到try catch中,那么Func函数中创建出来的两个指针就不会被delete掉,从而引发内存泄漏。

那么如何解决呢? 这就可以使用智能指针去解决了,使用智能指针因为这是一个对象,在对象被创建时初始化好里面的指针,出了作用域会自动销毁去调用对象的析构函数,再在析构函数中释放掉指针即可:简易的智能指针模板代码形式如下:

template<class T>
class smart_ptr
{
public:

	// 创建时初始化
	smart_ptr(T* ptr) :_ptr(ptr)
	{}

	~smart_ptr()
	{
		// 不需要显示的调用,因为对象除了作用域会自动调用析构函数
		if (_ptr)
			delete _ptr;
	}

	// 提供调用接口: 重载* ->
	T& operator*()
	{
		return *_ptr;
	}

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


private:
	T* _ptr = nullptr;
};

理解完智能指针的概念就来看看以下的几种智能指针:

auto_ptr:

auto_ptr的实现原理:就是直接转移管理权,但是现在不推荐使用auto_ptr,先来看一下auto_ptr的简化版:

#include<memory>
template<class T>
class auto_Ptr
{
public:
        auto_Ptr(T* ptr):_ptr(ptr)
        {}

        auto_Ptr(auto_Ptr<T>& sp)
        {
                // 进行置空,交接sp的管理权
                _ptr = sp._ptr;
                sp._ptr = nullptr;
        }

        auto_Ptr<T>& operator=(auto_Ptr<T>& sp)
        {
                // 检测是否自赋值:
                if (this != &sp)
                {
                        if (_ptr)
                                delete _ptr;

                        _ptr = sp._ptr;
                        sp._ptr = NULL;
                }

                return *this;
        }


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

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

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


private:
        T* _ptr;
};


int main()
{
        auto_Ptr<int> p1(new int[10]);
        auto_Ptr<int> p2(p1);


        auto_Ptr<int> p3(new int[5]);
        auto_Ptr<int> p4 = p3;


        return 0;
}

通过上面的代码我们可以看出auto_ptr就只是简单的转移资源,为什么不推荐使用的原因也很简单,就是假如我把p1的资源转移了,但是我忘记了,或者我并不知道,然后又对p1进行操作,那么此时就会发生崩溃。所以很多公司都禁止使用auto_ptr,而是使用unique_ptr、shared_ptr。

unique_ptr:

C++11中开始提供更靠谱的unique_ptr,实现的原理就是:简单粗暴的防拷贝,下面是简化模拟实现的unique_ptr:

namespace yue
{
    template<class T>
    class unique_ptr
    {
    public:
        unique_ptr(T* ptr)
            :_ptr(ptr)
        {}
        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }

        // 像指针一样使用
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
        
        // 获取_ptr的地址
        T* Get()
        {
            return _ptr;
        }

        unique_ptr(const unique_ptr<T>& sp) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
    private:
        T* _ptr;
    };
}

unique_ptr直接将拷贝构造函数delete掉了,解决了因为拷贝带来的置空问题(“雀食” 是从根源上解决了问题)所以相对于auto_ptr来讲:

auto_ptr只是简单的转移管理权,被拷贝对象悬空,有风险,不建议使用。

unique_ptr不支持拷贝,没有风险,建议使用unique_ptr。

但是在某些情况下我确实是需要两个指针来管理同一块资源怎么办呢?我就是想让他能拷贝怎么办呢?诶!所以C++11就提供了更靠谱的并且支持拷贝的shared_ptr。

shared_ptr:

C++11提供了更靠谱的并且支持拷贝的shared_ptr,那么来思考一个问题,既然我这两个指针都是指向同一块资源,那么在两个指针都delete的时候,是不是就会引发崩溃呢?(因为同一块内存不能被释放两次)

既然shared_ptr支持拷贝,那么它就得确保即使多个指针指向同一块资源,在析构的时候也只会将内存释放一次:

可以看到程序是正常结束的,并没有出现崩溃,也就是并没有出现同一块资源被释放多次的行为,那是怎么做到的呢?

这里就要引入一个引用计数的概念了,如果有接触学过Linux的话应该很容易想起来,Linux建立硬链接的思路就是利用引用计数,如果不知道也没关系,请看如下:

也就是说在智能指针对象中再加一个参数,(假设为x),初始为1,当这个智能指针每被拷贝一次,这个x就++一下,被释放调用析构函数的时候这个值就--一下,如果这个值减到0了,就说明没有指针指向这块内存了,就可以直接delete掉这块内存了。

那么这个引用计数如何设计呢?

首先,可定不可以设计成(int x = 1;)这样子,因为多个智能指针管理同一个对象我们是只需要一个引用计数的,如果每个对象都有一个计数那肯定是不可以的,所以到这里,你是不是就立马想到设置成静态的、全局的(static),但是很遗憾,这样设置也不可以,为什么也不可以呢?

如果我们要使用static的话,那么大致模板是这样的:

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

	smart_Ptr(const smart_Ptr<T>& sp):_ptr(sp._ptr){
		i++;
	}

	smart_Ptr<T>& operator=(const smart_Ptr<T>& ptr){
		// 不能自己给自己赋值
		if (_ptr != ptr._ptr){
			_ptr = ptr._ptr;
			++i;
		}

		return *this;
	}

	~smart_Ptr() {
		if (--i == 0)
		{
			delete _ptr;
			cout << "~smart_Ptr()" << endl;
		}
	}

	int& Get()
	{
		return i;
	}

private:
	T* _ptr = nullptr;   
	static int i;
};

但是看这个使用案例好像并没有什么错啊,p1p2指向一个,引用计数是2,p3指向一个,引用计数是1。

看起来没问题是正常的,因为这是我埋的坑,因为我使用了不同的类型去实例化模板,一个int,一个double,所以当然不会出错啦,static全局唯一就意味着如果是相同类型的话就算几个指针指向不同的对象,这个引用计数也还是会叠加,如下:

那既然使用不了static那么还有没有其他办法呢?当然有,我们可以直接new一个int*的指针,指向智能指针的引用计数,然后其他的就都一样,这样即使是相同类型,引用计数也是各自独立的。但是要注意,因为这个int*也是new出来的,所以在最后一个delete时要记得一并delete掉。

shared_ptr简化实现:

template<class T>
class shared_Ptr
{
public:
    shared_Ptr(T* ptr) :_ptr(ptr), _pcount(new atomic<int>(1))
    {}

    // sp2(sp1)
    shared_Ptr(const shared_Ptr<T>& sp)
        :_ptr(sp._ptr)
        , _pcount(sp._pcount)
    {
        (*_pcount)++;
    }

    // sp1 = sp2
    // 赋值还要考虑一块资源是否只有一个指针管控
    shared_Ptr<T>& operator=(const shared_Ptr<T>& sp)
    {
        // 防止自赋值
        // 例如s1 = s1的情况,如果s1是管控资源的最后一个指针,按照逻辑上来是先析构的,会成随机值,引起错误
        if (_ptr != sp._ptr) 
        {
            this->release(); // 查询管控数量(如果sp1是管理原本那一块资源的最后一个智能指针,得要先进行释放,才能进行sp1 = sp2的操作)

            _ptr = sp._ptr;
            _pcount = sp._pcount;

            ++(*_pcount);
        }

        return *this;
    }

    // 把析构要进行的操作分离成一个子函数,以便后面其他函数调用(因为函数调用内部一般不直接显式调用析构)
    void release()
    {
        // 最后一个管理的对象,释放资源
        if (--(*_pcount) == 0)
        {
            cout << "delete: " << _ptr << endl;
            delete _ptr;
            delete _pcount;
        }
    }

    ~shared_Ptr()
    {
        release();
    }

    // 查询引用计数
    int use_count()
    {
        return *_pcount;
    }

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

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


private:
    T* _ptr;

    // 多线程环境下对同一shared_ptr执行写操作不是线程安全的,所以要设置成原子性的。
    atomic<int>* _pcount; // 引用计数
};

shared_ptr的缺陷:

得要注意一个循环引用的问题,来看下面这一段代码:

可以看到,并没有调用Node的析构函数(没有任何输出,正常应该输出~Node()),这就是因为循环引用造成了内存泄漏。

就算只是其中一个有指向都是可以正常释放的,如果都指向了就会出问题,就会触发循环引用,造成内存泄漏,其实就是当相互进行指向后,p1、p2的引用计数都为2,当主函数结束时,后定义的先析构,p2先析构,引用计数减到1,此时由p1的next管着p2,然后p1析构,引用计数也进行--,此时p1的引用计数也减到1,此时由p2的prev管理着p1,但是相互都不满足引用计数为0的条件,释放逻辑是一个死循环,无法释放这段内存,所以造成了内存泄漏。

那怎么解决呢?

这个地方是一个大坑,所以1为写代码的时候注意规范,尽量不要这么去进行操作相互指向。

2就是使用官方给出的weak_ptr,弱指针。

weak_ptr:

weak_ptr不支持直接管理智能指针,也就是不支持RAII,不单独管理资源,是用来辅助解决shared_ptr的循环引用问题。

如果你知道某个地方可能会构成循环引用,你就改成使用weak_ptr:

可以看到使用weak_ptr后就可以调用Node的析构函数,本质就是通过不增加引用计数来解决的,就算我知道p1的next指向p2,p2的prev指向p1,但是我不增加p1p2的引用计数,也就是:

赋值或拷贝时,只指向资源,但不增加shared_ptr的引用计数。

weak_ptr返回的引用计数就是shared_ptr的引用计数。

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

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

相关文章

Java律师法律咨询小程序

技术&#xff1a;Java、Springboot、mybatis、Vue、Mysql、微信小程序 1.代码干净整洁&#xff0c;可以快速二次开发和添加新功能 2.亮点可以添加AI法律咨询作为 创新点 系统分&#xff1a;用户小程序端&#xff0c;律师web端和管理员端 用户可以在小程序端登录系统进入首…

机器学习之非监督学习(二)异常检测(基于高斯概率密度)

机器学习之非监督学习&#xff08;二&#xff09;异常检测&#xff08;基于高斯概率密度&#xff09; 0. 文章传送1.案例引入2.高斯正态分布3.异常检测算法4.异常检测 vs 监督学习5.算法优化 0. 文章传送 机器学习之监督学习&#xff08;一&#xff09;线性回归、多项式回归、…

VMware安装CentOS7及其初始化配置教程

安装准备 VMware Workstation Pro 17&#xff1a;下载及安装教程 CentOS 7下载地址&#xff1a;centos-7.9.2009-isos-x86_64安装包下载_开源镜像站-阿里云 创建虚拟机 安装CentOS7 鼠标点一下屏幕中间&#xff0c;就可以进入虚拟机&#xff0c;按向上箭头选择安装&#xff0…

EECS498 Deep Learning for Computer Vision (一)软件使用指南

#最近开始学习深度学习的相关基础知识&#xff0c;记录一下相关笔记及学习成果# learning&#xff1a;building artificial systems that learn from data and experience deep learning(a set of machine learning): hierarchical learning algorithms with many "laye…

【基础算法总结】模拟篇

目录 一&#xff0c;算法介绍二&#xff0c;算法原理和代码实现1576.替换所有的问号495.提莫攻击6.Z字形变换38.外观数列1419.数青蛙 三&#xff0c;算法总结 一&#xff0c;算法介绍 模拟算法本质就是"依葫芦画瓢"&#xff0c;就是在题目中已经告诉了我们该如何操作…

helm安装promethues

1、添加 Helm 仓库&#xff1a; helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update 2、安装 Prometheus&#xff1a;安装promtheus到monitor名称空间中 kubectl create ns monitor helm search repo prometheus #查…

【WSL迁移】将WSL2迁移到D盘

首先查看WSL状态&#xff1a;wsl -l -v 以压缩包的形式导出到其他盘。 wsl --export Ubuntu D:\Ubuntu_WSL\ubuntu.tar 注销原有的linux系统 wsl --unregister Ubuntu 导入系统到D盘 wsl --import Ubuntu D:\Ubuntu_WSL D:\Ubuntu_WSL\Ubuntu.tar 恢复默认用户 Ubuntu co…

Python编码系列—Python策略模式:灵活应对变化的算法策略

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

后端-navicat查找语句(单表与多表)

表格字段设置如图 语句&#xff1a; 1.输出 1.输出name和age列 SELECT name,age from student 1.2.全部输出 select * from student 2.where子语句 1.运算符&#xff1a; 等于 >大于 >大于等于 <小于 <小于等于 ! <>不等于 select * from stude…

大学生必看!60万人在用的GPT4o大学数学智能体有多牛

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作者&#x1…

Spring框架总体结构

1. Spring是什么 Spring 是一个开源框架 Spring 为简化企业级应用开发而生&#xff0c;使用 Spring 可以使简单的 JavaBean实现以前只有 EJB 才能实现的功能 Spring 是一个 IOC(DI) 和 AOP 容器框架 具体描述 Spring 轻量级&#xff1a;Spring 是非侵入性的 - 基于 Spring 开发…

【24华为杯数模研赛赛题思路已出】国赛B题思路丨附参考代码丨免费分享

2024年华为杯研赛B题解题思路 B题 WLAN组网中网络吞吐量建模 问题1 请根据附件WLAN网络实测训练集中所提供的网络拓扑、业务流量、门限、节点间RSSI的测试基本信息&#xff0c;分析其中各参数对AP发送机会的影响&#xff0c;并给出影响性强弱的顺序。通过训练的模型&#xff…

撤销与恢复的奥秘:设计模式之备忘录模式详解

备忘录模式 &#x1f3af; 备忘录模式&#xff08;Memento Pattern&#xff09;简介 备忘录模式 是一种行为型设计模式&#xff0c;用于保存对象的某一时刻状态&#xff0c;以便稍后可以恢复到该状态&#xff0c;而不破坏对象的封装性。备忘录模式将对象的状态封装在一个独立的…

技术周总结 09.16~09.22 周日(架构 C# 数据库)

文章目录 一、09.16 周一1.1&#xff09;问题01&#xff1a; 软件质量属性中"质量属性场景"、"质量属性环境分析"、"质量属性效用树"、"质量属性需求用例分析"分别是什么&#xff1f;1.2&#xff09;问题02&#xff1a; 软件质量属性中…

机器学习(1)sklearn的介绍和六个主要模块、估计器、模型持久化

文章目录 1.sklearn介绍2.sklearn的模块3.监督学习和无监督学习1. 监督学习 (Supervised Learning)例子 2. 无监督学习 (Unsupervised Learning)例子 4.估计器估计器的主要特性和方法包括&#xff1a;估计器的类型&#xff1a;示例&#xff1a;使用 scikit-learn 中的估计器 5.…

用最通俗易懂的语言和例子讲解三维点云

前言&#xff1a; 我整体的学习顺序是看的按B站那“唯一”的三维点云的视频学习的&#xff08;翻了好久几乎没有第二个...&#xff09;对于深度学习部分&#xff0c;由于本人并没有进行学习&#xff0c;所以没有深究。大多数内容都进行了自己的理解并找了很多网络的资源方便理解…

JavaScript可视化示例

JavaScript 可视化是指使用 JavaScript 编程语言来创建和操作图形、图表、动画等视觉元素的过程。以下是一些常见的 JavaScript 可视化库和工具&#xff0c;以及它们的主要特点&#xff1a; 1. D3.js 特点: D3.js&#xff08;Data-Driven Documents&#xff09;是一个非常强大…

MySQL高阶之存储过程

什么是存储过程? 存储过程可称为过程化SQL语言&#xff0c;是在普通SQL语句的基础上增加了编程语言的特点&#xff0c;把数据操作语句(DML)和查询语句(DQL)组织在过程化代码中&#xff0c;通过逻辑判断、循环等操作实现复杂计算的程序语言。 换句话说&#xff0c;存储过程其实…

Linux常用命令 笔记

Linux常用指令 查看命令ls 列出指定路径下的文件和目录cd 切换目录绝对路径相对路径 pwd 查看当前路径的绝对路径touch 创建空文件cat 显示文件内容echo 显示内容 & 写入文件vim 文本编辑器打开文件编辑文件保存退出 mkdir 创建目录rm 删除文件&目录删除文件删除目录 定…

java重点学习-设计模式

十三 设计模式 工厂模式&#xff1a;spring中使用&#xff08;目的是&#xff1a;解耦&#xff09; 1.简单工厂 所有的产品都共有一个工厂&#xff0c;如果新增产品&#xff0c;则需要修改代码&#xff0c;违反开闭原则是一种编程习惯&#xff0c;可以借鉴这种编程思路 2.工厂方…