C++11【智能指针详解】

news2024/11/23 10:31:22

智能指针

    • 🏞️1. 为什么引入智能指针?
    • 🌁2. 智能指针的使用及原理
      • 📖2.1 RAII思想
      • 📖2.2 智能指针的原理
    • 🌠3. 常见智能指针
      • 📖3.1 auto_ptr
      • 📖3.2 unique_ptr
      • 📖3.3 shared_ptr
      • 📖3.4 shared_ptr的循环引用问题
      • 📖3.5 weak_ptr
    • 🌌4. 定制删除器
    • ⛺5. C++11和boost智能指针的关系

🏞️1. 为什么引入智能指针?

我们来看这样一段代码:

#include <iostream>
using namespace std;

int div()
{
	int a, b;
	cin >> a >> b;

	if (b == 0)
	{
		throw "除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;
}

在这段代码中,如果在div函数中发生了除0错误,我们在main函数捕获异常,那么最终异常抛出后会跳转到main函数中的catch处,对于p1p2申请的资源就没有得到释放,就造成了内存泄露问题.

关于内存泄露介绍,在另一篇文章中有详细的介绍:

🌁2. 智能指针的使用及原理

📖2.1 RAII思想

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

获取到资源以后去初始化一个对象,将资源交给对象管理:资源获取即初始化

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源. 借此,我们实际是把管理一份资源的责任托管给了一个对象,这种做法有两大好处

  1. 不需要显式的释放资源
  2. 采用这种方式,对象所需的资源在其生命周期内始终有效
//实现一个最简易的智能指针
template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	
    //对象析构时自动释放所管理的资源
	~smart_ptr()
	{
		cout << "delete " << _ptr << endl;

		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

此时,我们使用智能指针来代替裸指针,并让它发生除0错误:

#include <iostream>
#include "smart_ptr.h"
using namespace std;

int div()
{
	int a, b;
	cin >> a >> b;

	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}

	return a / b;
}

void func()
{
	smart_ptr<int> p1(new int);
	smart_ptr<int> p2(new int);

	cout << div() << endl;
}

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

	return 0;
}

image-20221108172706448

可以看到,刚才由于抛异常未能释放的资源现在可以正常释放.

📖2.2 智能指针的原理

我们所写的这个简单的智能指针smart_ptr还不能称其为智能指针,因为它还不具有指针的行为指针可以解引用,可以通过->去访问所指向空间内的内容,所以为了让它向指针一样,我们还需重载*->运算符.

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

	~smart_ptr()
	{
		cout << "delete " << _ptr << endl;

		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}

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

	T* get() const
	{
		return _ptr;
	}

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

但是这样的智能指针是有问题的,试一下它的拷贝?

image-20221112103233500

那么,怎么去解决这个问题呢?

所以,接下来,我们来介绍几种C++标准库里的智能指针,来探究如何解决此问题

🌠3. 常见智能指针

📖3.1 auto_ptr

C++98版本的库中就提供了auto_ptr智能指针,它解决了拷贝赋值的问题,但也还有一些不足

auto_ptr实现原理:管理权转移的思想,下面将简化的模拟实现auto_ptr,主要体现它的思想:

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

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

				delete _ptr;
				_ptr = nullptr;
			}
		}

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

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				//释放当前对象管理的资源
				if (_ptr)
					delete _ptr;

				//管理权转移
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
            
            return *this;
		}

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

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

从它的拷贝构造和赋值重载可以看出,它以资源管理权转移的方式解决拷贝的问题.

但是,这样也带来另一个问题:

int main()
{
	myPtr::auto_ptr<int> p1(new int);
	myPtr::auto_ptr<int> p2 = p1;

	//*p1 = 10;  p1已经没有对资源的管理权,不能再使用!

	return 0;
}

由于p1的资源管理权已经转移给了p2,那它自己就失去对资源的管理及使用权,造成p1悬空.

📖3.2 unique_ptr

unique_ptr是C++11才开始提供的一种智能指针,它相比于auto_ptr更靠谱.

unique的实现原理:简单粗暴的禁止拷贝

我们依然是模拟实现一个unique_ptr来理解它的原理:

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

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

            delete _ptr;
            _ptr = nullptr;
        }
    }

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

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

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

unique_ptr采用一种简单粗暴的方式来解决拷贝的问题:直接删除拷贝构造函数和赋值重载函数,禁止拷贝

📖3.3 shared_ptr

shared_ptr也是C++11开始提供的,它能够解决智能指针拷贝的问题,并且它不像unique_ptr那样直接禁止拷贝,它是支持拷贝的.

shared_ptr的原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源

  1. shared_ptr内部,给它所管理的资源维护了一份引用计数,用来记录该资源被几个对象共同管理(共享)
  2. 在对象被销毁时(调用析构函数),就说明自己不使用该资源了,对用的引用计数减1
  3. 如果引用计数减到0,就说明当前自己已经是最后一个使用该资源的对象,所以此时必须释放该资源
  4. 如果不是0.那就说明还有其他对象管理这份资源,此时只需将引用计数减减即可,不需要释放资源
template<class T>
class shared_ptr
{
    public:
    shared_ptr(T* ptr = nullptr)
        : _ptr(ptr)
        {
            //将引用计数初始化为1
        }
    
    ~shared_ptr()
    {
        if (_ptr)
        {
            cout << "delete " << _ptr << endl;

            delete _ptr;
            _ptr = nullptr;
        }
    }

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

    T* operator->() const
    {
        return _ptr;
    }
    private:
    T* _ptr;
    //维护一个引用计数
};

那么,这个引用计数,我们应该怎样去维护呢?

  1. 使用普通变量int _pCount

    显然不行,由于私有成员变量是两个对象独有的,假如我们有两个智能指针p1p2,对p1的引用计数的–不会影响p2

  2. 定义一个静态成员变量static int _pCount

    看似,好像可以,但是试一下如下场景:

    template<class T>
    class shared_ptr
    {
        public:
            shared_ptr(T* ptr = nullptr)
                : _ptr(ptr)
            {
                 //在构造函数中将引用计数初始化为1
                 _pCount = 1;
            }
    
            ~shared_ptr()
            {
                if (--_pCount == 0 && _ptr)
                {
                    cout << "delete " << _ptr << endl;
    
                    delete _ptr;
                    _ptr = nullptr;
                }
            }
    
            shared_ptr(const shared_ptr<T>& sp)
            {
                _ptr = sp._ptr;
    
                ++_pCount;
            }
    
            T& operator*() const
            {
                return *_ptr;
            }
    
            T* operator->() const
            {
                return _ptr;
            }
        private:
            T* _ptr;
            //维护一个引用计数
            static int _pCount;
    };
    
    template<class T>
    int shared_ptr<T>::_pCount = 0;
    
    int main()
    {
        //_pCount = 1
    	myPtr::shared_ptr<int> p1(new int);
        //_pCount = 2;
    	myPtr::shared_ptr<int> p2(p1);
    	
        //这一步又重新将_pCount置为1,导致最终只释放的一次资源
    	myPtr::shared_ptr<int> p3(new int);
    
    	return 0;
    }
    
  3. 定义一个指针成员变量int* _pCount

    template<class T>
    class shared_ptr
    {
        public:
        shared_ptr(T* ptr = nullptr)
            : _ptr(ptr)
            , _pCount(new int(1))
        {}
    
        void Release()
        {
            if (--(*_pCount) == 0 && _ptr)
            {
                cout << "delete " << _ptr << endl;
    
                delete _ptr;
                _ptr = nullptr;
    
                delete _pCount;
                _pCount = nullptr;
            }
        }
    
        ~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*() const
        {
            return *_ptr;
        }
    
        T* operator->() const
        {
            return _ptr;
        }
        private:
        T* _ptr;
        //维护一个引用计数
        int* _pCount;
    };
    

    这就是我们最终模拟实现出的shared_ptr.

📖3.4 shared_ptr的循环引用问题

shared_ptr的循环引用问题

struct ListNode
{
	ListNode(const int& val = int())
		: _next(nullptr)
		, _prev(nullptr)
		, _val(val)
	{}

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}

	myPtr::shared_ptr<ListNode> _next;
	myPtr::shared_ptr<ListNode> _prev;

	int _val;
};

int main()
{
	myPtr::shared_ptr<ListNode> p1(new ListNode(1));
	myPtr::shared_ptr<ListNode> p2(new ListNode(2));

	p1->_next = p2;
	p2->_prev = p1;

	return 0;
}

在上面的代码中,我们应该是有两份ListNode节点需要释放,运行程序:

image-20221112164835524

没有任何节点被释放.

image-20221112165831225

这便是shared_ptr的循环引用问题.

解决方案:在引用计数的场景下,把节点中的_prev_next改成weak_ptr就可以

📖3.5 weak_ptr

weak_ptr原理:

node1->_next = node2;
node2->_prev = node1;

//weak_ptr的_next和_prev不会增加node1和node2的引用计数

weak_ptr模拟实现

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

    weak_ptr(const shared_ptr<T>& sp)
    {
        _ptr = sp._ptr;
    }

    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        if (_ptr != sp.get())
        {
            _ptr = sp.get();
        }

        return *this;
    }

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

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

🌌4. 定制删除器

在我们写的智能指针的析构函数中,统一都使用delete来释放资源,但是,如果资源不是用new申请出来的呢?比如:new T[],malloc,所以我们就需要定制删除器来规范释放资源的方式.

我们使用shared_ptr来做演示:

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

template<class T, class Del = default_delete<T>>
class shared_ptr
{
    public:
    shared_ptr(T* ptr = nullptr)
        : _ptr(ptr)
            , _pCount(new int(1))
        {}

    void Release()
    {
        if (--(*_pCount) == 0 && _ptr)
        {
            cout << "delete " << _ptr << endl;

            Del del;
            del(_ptr);
            _ptr = nullptr;

            delete _pCount;
            _pCount = nullptr;
        }
    }

    ~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*() const
    {
        return *_ptr;
    }

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

    T* get() const
    {
        return _ptr;
    }
    private:
    T* _ptr;
    //维护一个引用计数
    int* _pCount;
};
//定制new T[]类型的删除器
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	myPtr::shared_ptr<int, DeleteArray<int>> p(new int[10]);

	return 0;
}

⛺5. C++11和boost智能指针的关系

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

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

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

相关文章

基于深度学习的宋词生成

《自然语言处理》课程报告 摘 要 宋词是一种相对于古体诗的新体诗歌之一&#xff0c;为宋代儒客文人智慧精华&#xff0c;标志宋代文学的最高成就。宋词生成属于自然语言处理领域的文本生成模块&#xff0c;当前文本生成领域主要包括基于语言模型的自然语言生成和使用深度学习…

RK3568平台开发系列讲解(安卓适配篇)Android 源码的 device 目录

🚀返回专栏总目录 文章目录 一、device 目录简介二、Android 产品配置各种变量沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍 Android 源码的 device 目录。 一、device 目录简介 Android 源码下的 device 目录是 Android 源码中对产品的描述文件夹,各…

STM32个人笔记-CAN总线通讯

笔记来源于STM32F103VET6&#xff0c;野火指南者&#xff0c;中文参考手册&#xff0c;HAL库开发手册和b站的野火指南者视频。观看过好多次了&#xff0c;但往往理解得不够全面&#xff0c;现记下小笔记&#xff0c;用来回顾。属于个人笔记。​​​​​ 大四实习那年记忆颇深…

基于Java+Springboot+Vue+elememt宠物用品商城系统设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取联系&#x1f345;精彩专栏推荐订阅&#x1f447;&#x1f…

K线形态识别_空方尖兵

写在前面&#xff1a; 1. 本文中提到的“K线形态查看工具”的具体使用操作请查看该博文&#xff1b; 2. K线形体所处背景&#xff0c;诸如处在上升趋势、下降趋势、盘整等&#xff0c;背景内容在K线形态策略代码中没有体现&#xff1b; 3. 文中知识内容来自书籍《K线技术分析》…

第九章 内置模块

目录 第九章 内置模块 1.math模块 1.查看&#xff1a; 2.常用方法&#xff1a; 2.random模块 1.查看&#xff1a; ​2.例&#xff1a; 3.random(a,b) 4.random.sample(range(0,20),10) 3.os模块与os.path模块 1.作用 2.目录 3.os模块与操作系统的关系 4.路径 5.判…

自我实现tcmalloc的项目简化版本

项目介绍 该项目是基于现代多核多线程的开发环境和谷歌项目原型tcmalloc的自我实现的简化版本,相比于本身就比较优秀的malloc来说,能够略胜一筹,因为其考虑了 性能,多线程环境,锁竞争和内存碎片的问题,主要利用了池化思想来管理内存分配,对于每个线程&#xff0c;都有自己的私…

鸿蒙开发套件全面升级,助力鸿蒙生态蓬勃发展

目录 1. 全场景分布式系统 2. HarmonyOS的超能力&#xff1a;ArkTS API万箭齐发 3.解锁“鸿蒙开发套件”的新技能 &#xff08;1&#xff09; 智能代码编辑器 &#xff08;2&#xff09;Hvigor编译构建 &#xff08;3&#xff09;热重载&#xff1a;向看直播一样查看运行…

MySQL : 彻底搞懂一条SQL的执行过程

整体流程 组件介绍 连接器 处理客户端的连接&#xff0c;一般处理我们这个命令&#xff0c;判断是否满足接入server的条件 mysql ‐h host[数据库地址] ‐u root[用户] ‐p root[密码] ‐P root查询缓存 在8.0之前&#xff0c;如果用户开启了查询缓存的开关&#xff0c;那么…

vue2.6 + ts 使用vuex

目录vue2.6 ts 使用vuex安装01&#xff1a;直接使用 store / index.ts的数据store / index.tsmain.ts001&#xff1a;同步mutation操作vuex数据与获取getters001&#xff1a;效果002&#xff1a;异步action、mutation操作vuex数据002&#xff1a;效果02&#xff1a;引入其他模…

CANoe 简介

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

Go 语言项目源码解析:定时任务库 cron

环境准备 首先我们将源码克隆&#xff08;Fork&#xff09;为自己的个人仓库&#xff0c;只需要在 GitHub 项目主页点击 Fork 按钮&#xff0c;然后输入项目名称点击确认即可。克隆完毕后&#xff0c;可以下载到本地&#xff0c;或者直接在科隆后的 GitHub 仓库主页上点击 Cre…

RabbitMQ

RabbitMQ 1.MQ引言 MessageQueue: 消息队列 模块之间的耦合度多高&#xff0c;导致一个模块宕机后&#xff0c;全部功能都不能用了&#xff0c;并且同步通讯的成本过高&#xff0c;用户体验差。 1.1什么是MQ MQ&#xff08;Message Queue&#xff09;消息队列&#xff0c;是基…

Android Studio App开发实战项目之广告轮播(附源码 可用于大作业)

需要图片集和源码请点赞关注收藏后评论区留言即可~~~ 电商App的首页上方&#xff0c;都在明显位置放了一栏广告条&#xff0c;并且广告条会轮播&#xff0c;非常吸引眼球&#xff0c;这种广告轮播的功能&#xff0c;为推广热门事物出力甚大。 轮播视频已上传至我的主页&#x…

【云原生】docker 搭建ElasticSearch7

前言 本篇演示如何基于docker环境快速搭建起es7的环境 安装es7.6 1、拉取镜像 docker pull elasticsearch:7.6.2 2、执行下面的命令进行安装 docker run -p 9200:9200 -p 9300:9300 -e "discovery.typesingle-node" -e ES_JAVA_OPTS"-Xms512m -Xmx512m"…

Android Studio App开发实战项目之计时器(附源码 简单易懂,适合新手学习)

运行有问题或需要源码请点赞关注收藏后评论区留言~~~ 一、Handler的延迟机制 活动页面的Java代码通常是串行工作的&#xff0c;而且App界面很快就加载完成容不得半点延迟&#xff0c;不过偶尔也需要某些控件时不时的动一下&#xff0c;好让界面呈现动画效果更加活泼&#xff0…

shiro框架04会话管理+缓存管理+Ehcache使用

目录 一、会话管理 1.基础组件 1.1 SessionManager 1.2 SessionListener 1.3 SessionDao 1.4 会话验证 1.5 案例 二、缓存管理 1、为什么要使用缓存 2、什么是ehcache 3、ehcache特点 4、ehcache入门 5、shiro与ehcache整合 1&#xff09;导入相关依赖&#xff0…

2019银川F,ccpc威海D - Sternhalma 2022

1401D - Maximum Distributed Tree 求每个边经过的次数&#xff0c;假设求u,v这条边的次数&#xff0c;边的左端是u这个集合一共有n-siz[v]个点&#xff0c;右端是v这个集合有siz[v]个端点&#xff0c;经过这条边的次数就是siz[v]*(n-siz[v]),然后再按照次数多的乘以大的质因数…

【附源码】Python计算机毕业设计汽车租赁管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Go 语言中的 Moduels 管理(Let‘s Go 三十四)

在 Go 1.11以前使用包管理一直被开发者所诟病。既然GOPATH这种包管理引起了一线开发者的一片骂声&#xff0c;所以&#xff0c;Go官方体恤一线开发者对GOPATH这种包管理的情绪&#xff0c;一直致力努力提供对一线开发者友好的包管理解决方法而奋斗。从最初的GOPATH到GO VENDOR&…