C++智能指针

目录

1.引入

2.智能指针的原理和使用

2.1RAII

2.2原理

2.3std::auto_ptr

2.4std::unique_ptr

2.5std::shared_ptr

2.6循环引用

2.6.1代码演示

2.6.2分析过程

2.6.3分析原因

2.6.4避免问题

3.智能指针的总结

1.引入

当我们设计C++代码时,很容易遇到一种情况,即开辟内存之后因为操作不当,导致资源未完全释放,造成资源泄露。这便是内容泄露的内容,即造成程序未能释放已经不再使用内存的情况。

内存泄漏会导致内存浪费,并且若是长期运行的程序出现内容泄露的情况,会导致系统响应越来越慢,直到最终卡死。

所以为了更好的面对和处理内存泄问题,我们存在两种解决方案,分别为:事先预防,如智能指针等;或是事后纠错,如内存泄露检测工具。

我们本篇博客将重心置于事先预防内存泄漏中的智能指针模块,来对其进行概要的讲解。

2.智能指针的原理和使用

2.1RAII

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

当我们在构造对象时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后直到需要销毁对象时,调用析构函数进行资源释放。如此操作,我们便是将管理资源的责任交付给了一个具体的对象,对象的生命周期代表着资源的申请和销毁。

这样操作,我便不需要显式的释放资源(析构函数会自动被调用),并且对象所需要的资源将在其生命周期内始终有效。

我们可以根据上述思想来设计代码如下:

#include<iostream>

using namespace std;

//RAII:将资源交给对象管理,做到资源的自动释放
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr() {
		if (_ptr) {
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

void Test() {
	//int* p = new int(10);
	//SmartPtr<int> sp(p);

	SmartPtr<int> sp(new int(10));
}

int main() {
	Test();//调用方法结束,自动调用析构函数释放资源
	return 0;
}

使用对象sp来对实例化的空间进行获取,sp的生命周期即代表着new实例化空间资源的申请和释放。

2.2原理

上述代码设计内容还不足以被称之为智能指针,我们需要在具体的类中加入*和->操作的重载,使类对象具有指针的行为。

template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	//对*和->进行重载,支持具体对象进行指针操作
	T& operator*() {
		return *_ptr;
	}
	T& operator->() {
		return _ptr;
	}
	~SmartPtr() {
		if (_ptr) {
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

所以智能指针的原理应该为:具有RAII的特性(思想),并且需要operator重载*和->,使具体对象具有指针的特性。

但是我们对于智能指针的封装仍存在一个较为明显的错误:浅拷贝问题。对于拷贝构造的问题,是涉及到资源管理的,所以需要我们去探究。

对于不同解决浅拷贝问题的方式,前人在设计过程中实现了不同版本的智能指针。

2.3std::auto_ptr

在C++98版本库中便提供了std::auto_ptr的智能指针(头文件为#include<memory>),其实现原理为:管理权转移的思想,我们来设计简单的代码,来理解std::auto_ptr原理。

如图,当我们使用ap1来拷贝构造ap2之后,std::auto_ptr会直接将ap1的资源转移给ap2,然后将ap1制空,不再访问ap1,这便是std::auto_ptr管理权转移的原理。

显然,std::auto_ptr并不能满足我们对于指针的使用,并不能很好的实现针赋值的情况。因此,我它只能作为一个失败的产品,C++标准中也建议我们在什么情况下都不要使用std::auto_ptr。

了解其原理之后,我们使用我们自己的代码,来对std::auto_ptr进行模拟实现:

namespace dake {
	template<class T>
	class auto_ptr {
	public:
		//RAII
		auto_ptr(T* ptr = nullptr) 
			: _ptr(ptr)
		{}
		~auto_ptr() {
			if (_ptr) {
				delete _ptr;
				_ptr = nullptr;
			}
		}

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

		//资源转移
		auto_ptr(auto_ptr<T>& ap)
			: _ptr(ap._ptr)
		{
			ap._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;
		}
	private:
		T* _ptr;
	};
}

2.4std::unique_ptr

C++11:std::unique_ptr的原理是简单粗暴的防止拷贝(禁止共享),具体使用过程我们不截图做解释,仅做简单的模拟实现供大家参考。

模拟实现的难点在于:设计一个不能被拷贝的类。具体的操作方法为:让该类不能调用拷贝构造函数和赋值运算符重载(拷贝只能发生在拷贝构造函数和赋值运算符当中)。我们将其设置为私有(防止在类外定义),且只声明不定义(不提供具体方法),便可完成无法拷贝的要求。

还有一种方法是C++11当中提出的,具体内容为:delete的拓展用法,我们只需要在默认成员函数后跟上=delete,表示编译器删除该默认的成员函数。这样我们便可以将拷贝构造函数和赋值运算符重载删除,即满足无法拷贝的要求。

具体的代码设计内容如下:

namespace dake {
	template<class T>
	class unique_ptr {
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
		~unique_ptr() {
			if (_ptr) {
				delete _ptr;
				_ptr = nullptr;
			}
		}
		T& operator*() { return *_ptr; }
		T& operator->() { return _ptr; }

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

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

很明显,这样直接粗暴的不处理拷贝也不是我们愿意看到的,这样操作是直接将发生拷贝赋值的操作报错,让我们在编译阶段处理问题。

2.5std::shared_ptr

C++11当中提供了更加健壮且支持拷贝的智能指针:std::shared_ptr。其原理是:采用计数的方式来实现多个对象之间的资源共享。

在std::shared_ptr内部,会为每一份资源都维护一份计数,用来记录该份资源被多少对象所共享。当某一对象生命周期结束时(调用析构函数),其对应的资源计数减1。

如果对应计数为0,则表示自己是最后一个使用该资源的对象,当自己的生命周期结束,必须对这份资源销毁;如果计数不是0,则表示仍存在其他对象在使用该资源,当自己的生命周期结束,不能释放该资源,否在其他对象会变成野指针。

由于std::shared_ptr具体实现代码较为复杂,所以我们在此只进行一个简单的模拟实现,仅供大家参考理解其原理。

模拟实现代码如下:

namespace dake {
	template<class T>
	class DFDef {
	public:
		void operator()(T*& ptr) {
			if (ptr) {
				delete ptr;
				ptr == nullptr;
			}
		}
	};
	template<class T,class DF = DFDef<T>>
	class shared_ptr {
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(nullptr)
			, _pMutex(nullptr)
		{
			if (_ptr) {
				_pcount = new int(1);
				_pMutex = new mutex();
			}
		}
		~shared_ptr() {
			Release();
		}
		T& operator*() { return *_ptr; }
		T& operator->() { return _ptr; }

		//解决浅拷贝:引用计数
		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr) 
			, _pcount(sp._pcount)
			, _pMutex(sp._pMutex){
			AddRef();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
			if (this != &sp) {
				//1.释放*this原有资源
				Release();
				//2.*this和sp共享资源和计数
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pMutex = sp._pMutex;
				AddRef();
			}
			return *this;
		}

	private:
		void Release() {
			_pMutex->lock();
			bool flag = false;//添加删除锁的标记
			if (_ptr && 0 == --(*_pcount)) {
				DF() (_ptr);//创建匿名对象调用默认销毁函数
				delete _pcount;
				_pcount = nullptr;
				flag = true;//对应锁资源也需要释放
			}
			_pMutex->unlock();
			if (flag) {
				delete _pMutex;
			}
		}
		void AddRef() {
			_pMutex->lock();
			++(*_pcount);
			_pMutex->unlock();
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pMutex;//定义锁来保护线程
	};
}

2.6循环引用

2.6.1代码演示

我们使用std::shared_ptr来对循环引用进行演示,具体代码设计如下:

#include<iostream>
#include<stdlib.h>
#include<memory>

using namespace std;


//采用shared_ptr来管理双向链表当中的节点来演示循环引用
struct ListNode {
	shared_ptr<ListNode> next;
	shared_ptr<ListNode> prev;
	int data;

	ListNode(int val = 0)
		: next(nullptr)
		, prev(nullptr)
		, data(val)
	{}
};


void TestCycle() {
	shared_ptr<ListNode> sp1(new ListNode(10));
	shared_ptr<ListNode> sp2(new ListNode(20));

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->next = sp2;
	sp2->prev = sp1;

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
}

int main() {
	TestCycle();
	_CrtDumpMemoryLeaks();
	exit(0);
}

然后让我们调试进入程序,在输出列表中可以看到很明显的内存泄露情况。

那么,是何种原因导致的内存泄露问题。答案是:循环引用。

2.6.2分析过程

智能指针当中的循环引用是指两个或多个智能指针相互引用,导致它们指向的对象无法被正确地释放。这种情况也称为资源泄漏,因为这些对象无法被垃圾收集器自动回收,最终会导致内存泄漏和程序崩溃。

具体的循环引用过程分析为:我们将代码中的智能指针sp1和sp2相互引用后,当二者对象生命周期结束,两份对象销毁时,又在互相使用对方中的成员。这边造成了要销毁sp1指向的next(为sp2)时,需要先销毁sp1节点,而sp1节点又受到prev管理,prev属于sp2成员。

在释放资源时,二者都告诉操作系统:若要销毁我,得先销毁对方。这便是循环引用的具体过程,我们无法决定先销毁时,也无法让二者同时销毁。因此,资源释放不当,出现内存泄漏。

2.6.3分析原因

出现智能指针循环引用的原因是一般是:两个或多个智能指针相互引用,即:智能指针A指向B,智能指针B又指向A;又或是智能指针与裸指针之间的相互引用,即:智能指针A 指向 B,智能指针B 又包含一个指向 A 的裸指针。

2.6.4避免问题

解决智能指针循环引用的方法主要有以下几种:

  1. 使用弱引用(weak_ptr):弱引用是一种不具有拥有权的智能指针,它指向的对象可以被释放。如果指向的对象被释放了,弱引用会自动变成空指针。因此,在出现循环引用的时候,可以使用弱引用来打破引用链,防止资源泄漏。

  2. 手动释放:在一些情况下,可以手动释放智能指针所指向的对象,然后再重置智能指针。这种做法需要开发者自己管理内存生命周期,比较麻烦,也容易出错。

  3. 限制资源的生命周期:通过设计良好的程序结构和生命周期管理,可以避免循环引用的发生。例如,使用单例模式、调整对象的依赖关系等。

3.智能指针的总结

C++智能指针是一种有力的工具,可以帮助我们来进行动态内存的管理,避免内存泄漏和悬空指针等问题。以下是C++智能指针的总结:

  1. C++智能指针是一个类,它封装了指针并提供了一些额外的功能。

  2. 最常用的C++智能指针是shared_ptr和unique_ptr。shared_ptr允许多个指向同一个对象的指针存在,并且在所有指向该对象的指针都离开作用域后自动删除该对象。unique_ptr则只允许一个指向对象的指针存在,并且在该指针离开作用域后自动删除该对象。

  3. 手动管理内存的指针可能会导致内存泄漏和悬空指针等问题。使用C++智能指针可以减少这些问题的出现。但是要注意的是,智能指针并不能完全避免这些问题,需要根据情况进行适当的判断和处理。

  4. 在使用C++智能指针时,需要注意的是避免循环引用的问题。循环引用可能会导致内存泄漏。

  5. C++智能指针可用于动态创建对象并在必要时自动删除对象,同时也可用于管理动态数组。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值