当前位置: 首页 > news >正文

【C++11】智能指针

🦄个人主页:修修修也

🎏所属专栏:C++

⚙️操作环境:Visual Studio 2022


目录

为什么需要智能指针

智能指针的使用及其实现原理

RAII

auto_ptr

简介

实现

unique_ptr

简介

实现

shared_ptr

简介

实现

weak_ptr

简介

实现

结语


为什么需要智能指针

        C++没有垃圾回收的机制,必须通过我们手动的去动态申请并释放资源。也就是说, 我们new出的资源, 必须在后面不使用之后将其delete掉, 否则就会造成内存泄漏。

        我在【C++】动态内存管理中详细介绍过内存泄漏的危害, 并提供了几种防止内存泄漏的方式, 其中最重要的一点就是要严格遵守谁申请谁释放原则, 这样就可以避免大量的内存泄漏场景, 一般而言,只要内存遵行了先申请后释放的原则, 那么100%是不可能内存泄漏的, 但是当C++引入异常这一特性的时候, 一切都变得不一样了...

        我们来看这段代码, 分析一下下面三种情况程序会出现什么问题:

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

void Func()
{
    // 1、如果p1这里new 抛异常会如何?
    // 2、如果p2这里new 抛异常会如何?
    // 3、如果div调用这里又会抛异常会如何?
    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抛异常,剩下的两种情况都会导致不同程度的内存泄漏:

        那么是不是有异常的时候我们只能对着在执行流里乱窜的异常说:"太好了孩子们,是异常,我们没救了😅"。当然不是!!!乱世之中, 咱们的救世主悄然登场了:


智能指针的使用及其实现原理

        没错,智能指针就是救我们与水火之中的救世主, 那他的原理是什么呢?我们先来了解RAII思想:

RAII

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

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

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

auto_ptr

简介

        借助RAII思想, 我们的初代智能指针auto_ptr闪亮登场, 只是可惜这个初代目不太经打, 刚刚登场, 就已领了盒饭, 以至于后面被很多企业禁止使用, auto_ptr的主要败笔在于, 它在拷贝构造和赋值时使用了管理权转移的思想, 即把b赋值/拷贝构造给a时, 会直接把b的指针给a, 然后b自己置空。这样会导致b指针后续处于一个悬空状态, 后续如果继续不慎使用b则会造成空指针解引用的问题。而且在b生命周期结束后他会去析构一个空指针, 这完全就是一个非法的内存访问操作, 所以不可避免的会导致程序崩溃。如果b指针赋值后不置为空, 那么后续又会出现多重析构的问题。这导致了auto_ptr面世没多久就遭到了"封杀", 实在是可惜。但是智能指针并不因此挫折就销声匿迹了, 有此前车之鉴, 后续大佬们又在auto_ptr的基础上开发出了更加安全, 实用的智能指针。

实现

        以下是auto_ptr的简单模拟实现代码:

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

		//管理权转移,把ap.ptr的管理权转让给_ptr,然后ap.ptr自己置空
		auto_ptr(auto_ptr<T>& other)
			: _ptr(other.ptr)
		{
			other._ptr = nullptr;
		}

		~auto_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		// 赋值运算符重载,转移所有权
		auto_ptr& operator=(auto_ptr& other) 
		{
			if (this != &other) //检查是否自己给自己赋值
			{
				delete _ptr;	//先析构(放弃)自己原有的指针管理权
				_ptr = other.ptr;	//再拿到新的指针管理权
				other.ptr = nullptr;	//被转移对象自己置空
			}
			return *this;
		}

	private:
		T* _ptr;
	};
}

unique_ptr

简介

        auto_ptr的惨痛教训还历历在目, 于是C++痛定思痛, 推出了修补版本的unique_ptr。unique_ptr的想法是, 既然auto_ptr的赋值和拷贝构造会导致安全问题, 那么我把这两个操作禁用了不就ok了, 于是我们的二代智能指针就这样登场了。

实现

        unique_ptr在实现上和auto_ptr几乎一模一样, 但是对于拷贝构造函数和赋值运算符重载函数则是直接"封掉":

namespace test
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//禁止拷贝构造
		unique_ptr(const unique_ptr& other) = delete;
		//禁止赋值
		unique_ptr& operator=(const unique_ptr& other) = delete;

	private:
		T* _ptr;
	};
}

shared_ptr

简介

        unique_ptr的改进为我们提供了相对安全的智能指针的方案, 但是由于他的实现思想的限制, 导致该智能指针在应用上有诸多限制, 例如不可以多个指针管理同一份资源, 也不可以赋值更改管理的资源。为了解决这一问题, 大佬们又出研发了一个史诗级的智能指针, 就是shared_ptr。

        shared_ptr借助了引用计数的思想, 支持多个智能指针管理同一份资源, 允许拷贝构造和赋值, 是通过为一份资源维护一份引用计数来实现的, 该引用计数记录了当前同时管理这份资源的智能指针数, 如果其中一个智能指针超出了生命周期要销毁资源,那么会先判断它是否是当前唯一管理这份资源,即引用计数是否为1, 如果是则析构释放资源, 如果不是那么只会将引用计数-1, 不会真实的去销毁资源。相应的,遇到拷贝构造和赋值则会相应的给引用计数+1。图示如下:

         shared_ptr可以说是智能指针的中流砥柱了, 但是即便如此, 它在某些场景中还是会出现一些无解的问题, 即在某些情况下会出现循环引用问题:

        首先我们可能会遇到使用智能指针管理链表资源的场景:

        然后我们需要将这两个链表结点连接起来:

        看起来好像没一点问题, 但是当我们要释放这两个结点的时候问题就出现了:

实现

        shared_ptr的实现比前面两个稍复杂一点, 引入引用计数后, 我们需要在构造,析构,拷贝构造和赋值函数中对引用计数做相应的处理, 但只要清楚了引用计数的原理,实现起来也非常简单:

namespace test
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
        //如果引用计数为1才释放资源,否则只减引用计数
		~shared_ptr()
		{
			if (_ptr)
			{
				if (*_pcount == 1)
				{
					delete _ptr;
					delete _pcount;
				}
				else
				{
					(* _pcount)--;
				}
			}	
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//拷贝构造
		shared_ptr(const shared_ptr& other)
			:_ptr(other._ptr)
			,_pcount(other._pcount)
		{
			(*_pcount)++;
		}
		//赋值
		shared_ptr& operator=(const shared_ptr& other)
		{
            //判断自己给自己赋值
			if (_ptr == other._ptr)
			{
				return *this;
			}

            //处理赋值对象管理的原资源
			if (*_pcount == 1)
			{
				delete _ptr;
				delete _pcount;
			}
			else
			{
				(*_pcount)--;
			}

			//把新资源给赋值对象,同时增加引用计数
			_ptr = other._ptr;
			_pcount = other._pcount;
			++(*_pcount);

			return *this;
		}
        T* get()
        {
            return _ptr;
        }

	private:
		T* _ptr;
		int* _pcount;
        //用动态资源来管理引用计数,不能用普通类型,因为会导致每个类各自有一个
        //也不能用静态成员, 因为不能修改
	};
}

weak_ptr

简介

        weak_ptr是一种弱引用智能指针, 它是为配合 shared_ptr 而引入的,主要用于解决 shared_ptr 可能出现的循环引用问题。他不是RAII智能指针, 他不增加引用计数, 可以访问资源, 不参与资源释放的管理。

实现

        因为weak_ptr不参与资源的管理,所以实现的时候非常简单, 析构函数, 拷贝构造函数和赋值运算符重载都不需要实现, 系统默认生成的就可以使用, 但是除此之外, weak_ptr还要支持用shared_ptr来构造和赋值, 所以我们实现这两个函数:

namespace test
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr(T* ptr)
			:_ptr(ptr)
		{}

		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//要支持用shared_ptr构造
		weak_ptr(const shared_ptr<T>& other)
			:_ptr(other._ptr)
		{}

		//支持用shared_ptr赋值
		weak_ptr& operator=(const shared_ptr<T>& other)
		{
			_ptr = other.get();
			return *this;
		}

	private:
		T* _ptr;
	};
}

结语

希望这篇关于 C++智能指针 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【C++11】左值引用、右值引用、移动语义和完美转发

【C++】STL标准模板库容器set

【C++】模拟实现二叉搜索(排序)树

【C++】模拟实现priority_queue(优先级队列)

【C++】模拟实现queue

【C++】模拟实现stack

【C++】模拟实现list

【C++】模拟实现vector

【C++】标准库类型vector

【C++】模拟实现string类

【C++】标准库类型string

【C++】构建第一个C++类:Date类

【C++】类的六大默认成员函数及其特性(万字详解)

【C++】什么是类与对象?



实际就是把动态开辟的资源交给智能指针来管理.

相关文章:

  • 第十六届蓝桥杯Java b组(试题C:电池分组)
  • LabVIEW 程序持续优化
  • [react]Next.js之自适应布局和高清屏幕适配解决方案
  • 2025SQCTF赛题复现
  • 泰勒公式的深入研究
  • 【信息系统项目管理师】高分论文:论信息系统项目的整合管理(旅游景区导游管理平台)
  • Laravel 使用通义灵码 - AI 辅助开发提升效率
  • 【vue】双向绑定
  • git安装(windows)
  • 一文读懂WPF系列之控件模版数据模板
  • LeetCode LCR157 套餐内商品的排列顺序
  • Java基础关键_037_Java 常见新特性
  • 深度解析Redis过期字段清理机制:从源码到集群化实践 (二)
  • OSPF单区域配置实验
  • 软件测试之单元测试详解
  • [LVGL] 使用lvgl自带的链表函数
  • CSV文件中的中文乱码--UTF-8 with BOM
  • DeepSeek 与开源:肥沃土壤孕育 AI 硕果
  • react/vue中前端多图片展示页面优化图片加载速度的五种方案
  • 高德地图 JS-SDK 实现教程
  • 第1现场 | 印巴停火次日:当地民众逐渐恢复正常生活
  • 浙江公开征集涉企行政执法问题线索,包括乱收费、乱罚款等
  • “电竞+文旅”释放价值,王者全国大赛带火赛地五一游
  • 武汉旅游体育集团有限公司原党委书记、董事长董志向被查
  • 当我们提起拉动消费时,应该拉动什么消费?
  • 刘元春在《光明日报》撰文:以法治护航民营经济高质量发展