【C++】智能指针详解

这篇具有很好参考价值的文章主要介绍了【C++】智能指针详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

今天我们来讲一下c++中的智能指针。

1. 智能指针初识

1.1 什么是智能指针

智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。

动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源


1.2 智能指针发展历史

  1. C++ 98 中产生了第一个智能指针auto_ptr
  2. C++boost给出了更加实用的scoped_ptr 和 shared_ptr 和 weak_ptr
  3. C++ 11 引入了unquie_ptr 和 shared_ptr 和 weak_ptrt .需要注意的是,unique_ptr对应的是boost中的scoped_ptr。并且这些智能指针的实现是参照boost中的实现的。

1.3 为什么需要智能指针

在之前的博客 【C++】异常 中我提到过一个叫做异常的重新抛出的场景:

我们再把那个例子讲一下:


void File()
{
	string filename;
	cin >> filename;
	FILE* fout = fopen(filename.c_str(), "r");
	if (fout == nullptr) {
		string errmsg = "打开文件失败:";
		errmsg += filename;
		errmsg += "->";
		errmsg += strerror(errno);
		Exception e(errno, errmsg);
		throw e;
	}
	char ch;
	while ((ch = fgetc(fout))!=EOF) {
		cout << ch;
	}
	fclose(fout);
}

double Division(int a, int b)
{
	if (b == 0) 
	{
		string errmsg = "Division by zero condition!";
		Exception e(100, errmsg);
		throw e;
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	int* p = new int[100];
	int len, time;
	cin >> len >> time;
	try {
		cout << Division(len,time) << endl;
		File();
	}
	catch (...)
	{
		//捕获之后,不是要处理异常,异常由最外层同一处理
		//这里捕获异常只是为了处理内存泄漏的问题
		delete[]p;
		throw; 
	}
	delete[]p;
}

int main()
{

	try {
		Func();
	}
	catch (const Exception& e) {
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的 析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。

但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。

所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题

3. 智能指针原理

3.1 RALL

RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。

借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

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

我们可以借助RALL思想来写一个简单的 智能指针:

#include<iostream>
using namespace std;

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

int main()
{
	int* a = new int(1);
	SmartPtr<int> sp(a); //将a 指针委托给sp对象管理

	SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理
}

3.2 智能指针的分类

上面的SmartPtr 还不可以被称为智能指针,因为它还不具有指针的行为与性质。

指针可以解引用,也可以通过->去访问所指向的空间中的内容,因此智能指针还需要将 *,->重载。

除此之外,如果我们使用了 拷贝或者赋值操作,就会发生浅拷贝的问题,由于二者指向同一块空间,所以在析构的时候也会析构两次,造成错误。

【C++】智能指针详解
所以,为了解决以上问题,C++提供了几种设计方案实现的智能指针,我们下面来一一讲解。


C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景。

3.2.1 auto_ptr

在C++98版本的库种,提供了 auto_ptr 的智能指针:

我们使用一下std::auto_ptr:

class Date
{
public:
	Date()
		:_year(0),_month(0),_day(0)
	{}
	~Date(){}

	int _year;
	int _month;
	int _day;

};

int main()
{
	auto_ptr<Date>ap(new Date);

	//拷贝构造
	auto_ptr<Date>copy(ap);
	
	ap->_year = 2022;
}

我们发现报错了,发生了非法访问。

这就是auto_ptr 的弊病,当我们使用对象拷贝或者赋值之后,之前的那个对象就被置空(如下图)

【C++】智能指针详解
在拷贝或者赋值的过程种,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。

虽然这种方法确实解决了 浅拷贝的问题,但是十分局限性也很大,这也就导致了,我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。

由于C++98种提供的这个智能指针问题明显,所以在实际工作种哼多公司是明确规定了不能使用auto_ptr的。


那么auto_ptr具体是如何实现的呢?很简单.

template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr=nullptr)
			:_ptr(ptr)
		{}
		
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr; //管理权转移
		}

		auto_ptr<T>& operator = (auto_ptr<T>& ap)
		{
			if (this != *ap) {
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		~SmartPtr()
		{
			if (_ptr)delete _ptr;
		}
		T& operator *()
		{
			return *_ptr;
		}
		T* operator ->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

3.2.2 unique_ptr

在C++11中,C++11y引入了unique_ptr.

unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。

对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。


我们可以尝试实现一下:

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

		//防拷贝
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator = (unique_ptr<T>& ap) = delete;

		~SmartPtr()
		{
			if (_ptr)delete _ptr;
		}

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

3.2.3 shared_ptr

3.2.3.1 shared_ptr 原理

C++中还提供了shared_ptr。

shared_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。

我们可以测试使用一下:
【C++】智能指针详解


那么shared_ptr的原理是什么?

我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。

再具体一点:

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指
    针了。

3.2.3.2 shared_ptr 的模拟实现

我们可以实现一个简单的shared_ptr:

template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T*ptr =nullptr)
			:_ptr(ptr),_pcount(new int(1))
		{}
		//拷贝构造
		shared_ptr(const T& sp)
			_ptr(sp._ptr),_pcount(sp._pcount)
		{
			++(*_pcount);
		}
		//赋值拷贝
		shared_ptr<T>& operator = (shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) {
				if (--(*_pcount) == 0){
					delete _pcount;
					delete _ptr;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}
		T& operator *()
		{
			return *_ptr;
		}

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


		~shared_ptr()
		{
			if (--(*_pcount) == 0 && _ptr) {
				delete _pcount;
				delete _ptr;
			}
		}
	private:
		T* _ptr;
		int* _pcount;
	};

我们把 这个 计数器 建在堆上,这样就可以保证各个对象之间保持同步同时计数正确。

-拷贝构造

【C++】智能指针详解

  • 赋值拷贝

赋值拷贝需要注意两点:

  1. 在被赋值之前的对象需要将自己析构,也就是放弃当前资源的管理权,然后再去被赋值,取得新的管理权。
  2. 避免自己对自己赋值,按照1中的机制,如果自己对自己赋值,会造成无谓的操作,或者误析构资源。

【C++】智能指针详解
其他写法:

		shared_ptr<T>& operator=(shared_ptr<T> sp)
		{
			swap(_ptr, sp._ptr);
			swap(_pcount, sp._pcount);
			return *this;
		}

但是,此时我们的shared_ptr 还面临着 线程安全的问题。

这里我们需要保障的是对于 计数器的 ++ 和 – 造成的线程不安全。对于资源的线程安全问题,这不是智能指针保证的部分。


	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
		{}

		void add_ref()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}
		void release_ref()
		{
			bool flag = false;
			_pmtx->lock();
			if (--(*_pcount) == 0 && _ptr) {
				delete _pcount;
				delete _ptr;
				flag = true;
				cout << "释放资源:" << _ptr << endl;
			}
			_pmtx->unlock();
			if (flag)delete _pmtx;
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx)
		{
			add_ref();
		}
		shared_ptr<T>& operator = (const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) {
				if (--(*_pcount) == 0){
					delete _pcount;
					delete _ptr;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				add_ref();
			}
			return *this;
		}
		T& operator *()
		{
			return *_ptr;
		}

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

		T* get()
		{
			return _ptr;
		}
		int use_count()
		{
			return *_pcount;
		}


		~shared_ptr()
		{
			release_ref();
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};
3.2.3.3 定制删除器

不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。

为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete
【C++】智能指针详解

这里举两个例子


	template<class T>
	struct DeleteArray
	{
		void operator()(T* ptr)
		{
			delete[]ptr;
		}
	};
	
	void test_deletor()
	{
		DeleteArray<string>da; //使用仿函数定制
		std::shared_ptr<string>s2(new string[10], da);

		std::shared_ptr<string>s3((string*)malloc(sizeof(string)),
		 [](string* ptr) {free(ptr); }); //使用lamdba 定制
	}

如果我们也想自己是想一下呢?

当然是可以的,但是由于我们的实现比库中的简单很多(库中使用多个类),所以我们难以通过传参的方式来定制删除器,我们增加一个模板参数,通过向模板传参来达到相同的目的。

std的框架设计底层用一个类专门管理资源计数,所以它们可以在构造函数传参,把删除器类型传递给专门管理资源的这个类。而我们是一体化的。

template<class T>
	struct DefaultDel
	{
		void operator ()(T* ptr)
		{
			delete ptr;
		}
	};
	
	template<class T,class D=DefaultDel<T>> //增加模板参数
	class shared_ptr
	{
	public:
		explicit shared_ptr(T* ptr = nullptr)
			:_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
		{}

		void add_ref()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}
		void release_ref()
		{
			bool flag = false;
			_pmtx->lock();
			if (--(*_pcount) == 0 && _ptr) {
				//定制化删除
				D del;
				del(_ptr);
				
				delete _pcount;
				flag = true;
				cout << "释放资源:" << _ptr << endl;
			}
			_pmtx->unlock();
			if (flag)delete _pmtx;
		}

		shared_ptr(const shared_ptr<T,D>& sp)
			:_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx)
		{
			add_ref();
		}
		shared_ptr<T,D>& operator = (const shared_ptr<T,D>& sp)
		{
			if (_ptr != sp._ptr) {
				if (--(*_pcount) == 0){
					delete _pcount;
					delete _ptr;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				add_ref();
			}
			return *this;
		}
		T& operator *()
		{
			return *_ptr;
		}

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

		T* get()
		{
			return _ptr;
		}
		
		int use_count()
		{
			return *_pcount;
		}
		
		~shared_ptr()
		{
			release_ref();
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};

此时我们要这样使用:


	template<class T>
	struct DeleteArray
	{
		void operator()(T* ptr)
		{
			delete[]ptr;
		}
	};
	
	void test_deletor()
	{
	 	//使用lamdba 定制
		DeleteArray<string>da; 
		std::shared_ptr<string,DeleteArray<string>>s2(new string[10);

		 //使用lamdba 定制
		auto ffree =  [](string* ptr) {free(ptr); };
		std::shared_ptr<string,decltype(ffree)>s3((string*)malloc(sizeof(string)));
		
		auto fclose =  [](FILE* ptr) {fclose(ptr); };
		std::shared_ptr<string,decltype(ffcolse)>s3(fopen("test.cpp","r"));
	}

3.2.4 weak_ptr

虽然 shared_ptr 确实已经是一个不错的设计了,但是没有“十全十美”的东西,在一些特别的场景之下shared_ptr 也无能为力:

  • shared_ptr 的循环引用
    我们看下面的场景,我们运行发现,两个节点n1.n2 都没有析构。
    【C++】智能指针详解

为什么会发生这种情况呢?
【C++】智能指针详解

在出了作用域之后,首先把 n1,n2 两个对象析构,此时两边计数器均减为1,那么左边节点资源什么时候析构呢, 当n2->prev析构,也就是当右边节点资源析构,那么右边节点资源什么时候析构呢,当n1->_next析构,也就是当左边节点资源析构…我们发现,此时形成了一个类似于“死锁”的情况。


此时我们就要使用 weak_ptr 来解决 循环引用。

weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的,为什么这么说呢,我们可以看看它的构造函数:

我们只能使用 wek_ptr或者 shared_ptr 去初始化它。
【C++】智能指针详解

我们在会产生循环引用的位置,把shared_ptr换成weak_ptr。 weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。

【C++】智能指针详解

我们可以自己实现一个weak_ptr:文章来源地址https://www.toymoban.com/news/detail-441392.html


	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(shared_ptr<T>& sp)
			:_ptr(sp.get()),_pcount(sp.use_count())
		{}
		weak_ptr(weak_ptr<T>& sp)
			:_ptr(sp._ptr), _pcount(sp._pcount)
		{}
		weak_ptr& operator = (shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			_pcount = sp.use_count();
			return *this;
		}
		weak_ptr& operator = (weak_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		int use_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		int* _pcount;

	};

到了这里,关于【C++】智能指针详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】智能指针(RAII)详解

      我们在上篇文章中(异常处理详解)提到了 RAII 。那么本篇文章会对此进行详解。重点是智能指针的详解。其中会讲解到 RAII 思想、auto_ptr、unique_ptr、shared_ptr、weak_ptr、循环引用问题 。希望本篇文章会对你有所帮助。 文章目录 一、为什么需要智能指针 二、智能指针的使用

    2024年02月10日
    浏览(37)
  • C++智能指针shared_ptr详解

    C++智能指针shared_ptr是一种可以自动管理内存的智能指针,它是C++11新增的特性之一。与传统指针不同,shared_ptr可以自动释放所管理的动态分配对象的内存,并避免了手动释放内存的繁琐操作,从而减少了内存泄漏和野指针的出现。 shared_ptr是一个模板类,通过引用计数器实现

    2023年04月22日
    浏览(41)
  • C++智能指针shared_ptr使用详解

    shared_ptr 是一个共享所有权的智能指针,允许多个指针指向同一个对象。 ​ shared_ptr 使用 引用计数 ,每一个shared_ptr的拷贝都指向相同的内存。每使用它一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,释放所指向的堆内存。shared_ptr内部的引用计数是安

    2024年02月07日
    浏览(42)
  • 如何在C++中将int类型的变量转换为string类型呢?今天我们就来介绍两种方法。

    如何在C++中将int类型的变量转换为string类型呢?今天我们就来介绍两种方法。 第一种方法是使用C++11标准引入的std::to_string()函数。这个函数可以将数字类型的变量转换为对应的字符串类型。下面是一个使用示例: 上面的代码将整型变量num转换为字符串类型,并输出到控制台

    2024年02月08日
    浏览(58)
  • 【C++ 观察者模式 思想理解】C++中的观察者模式:松耦合设计与动态交互的艺术,合理使用智能指针观察者

    在进入技术细节之前,理解观察者模式(Observer Pattern)的基本概念和它在现代编程中的重要性是至关重要的。 观察者模式是一种设计模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。在C++中,这个

    2024年01月24日
    浏览(56)
  • C++中智能指针(unique_ptr、shared_ptr、weak_ptr)详解

    C++中的智能指针是一种 RAII(资源获取即初始化)机制的实现, 它可以在对象不再需要时自动释放相关资源。 智能指针通过封装指针对象并提供一些额外的功能,如 引用计数、自动内存管理、避免内存泄漏等, 使得指针使用更加安全和方便。 在C++中,有三种主要类型的智能

    2023年04月11日
    浏览(40)
  • C++11中的智能指针unique_ptr、shared_ptr和weak_ptr详解

    目录 1、引言 2、什么是智能指针? 3、在Visual Studio中查看智能指针的源码实现 4、独占式指针unique_ptr 4.1、查看unique_ptr的源码实现片段 4.2、为什么unique_ptr的拷贝构造函数和复制函数被delete了?(面试题) 4.3、使用unique_ptr独占式智能指针的实例 5、共享式指针shared_ptr  5.1、查

    2024年02月08日
    浏览(44)
  • 今天,我终于学懂了C++中的引用

    本篇博客全站热榜排名:2 Hello,大家好,今天我们就来聊聊有关C++中的引用知识📖 回忆一下我们曾经在写C语言的时候因为指针所引发的种种难题,特别是对于【两数交换】的时候因为函数内部的概念不会引发外部的变化,使得我们 需要传入两个需要交换数的地址 ,在函数

    2023年04月08日
    浏览(71)
  • 今天来认识一下无聊的spark和scala基础理知识

    spark: 首先了解什么是spark。 Spark是一种快速、通用、可扩展的大数据分析引擎。 --spark的发展史 spark的主要优点 1.快速 一般情况下,对于迭代次数较多的应用程序,Spark程序在内存中的运行速度是Hadoop MapReduce运行速度的100多倍,在磁盘上的运行速度是Hadoop MapReduce运行速度的

    2024年03月19日
    浏览(46)
  • 【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针

    【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针 大家好,今天是【重学C++】系列的第二讲,我们来聊聊C++的智能指针。 在上一讲《01 C++如何进行内存资源管理》中,提到了对于堆上的内存资源,需要我们手动分配和释放。管理这些资源是个技术活,一不小心,就会导致内

    2024年02月05日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包