C++智能指针

内存泄漏

分类
  • 堆内存泄漏(Heap leak)
    堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的free / delete / delete[]删掉,假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak;
  • 系统资源泄漏
    指程序使用系统分配的资源,比如套接字、文件描述符、管道等,在使用完之后没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定等;
检测

智能指针原理

RAII
  • 概念:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术;
  • 原理:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,借此我们实际上把管理一份资源的责任托管给了一个对象,这种做法有两大好处:
    • 不需要显式地释放资源;
    • 采用这种方式,对象所需的资源在其生命期内始终保持有效;
简单实现
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr(){
		if(_ptr)
			delete _ptr;
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
private:
	T* _ptr;
};
struct Date{
	int _year;
	int _month;
	int _day;
};
void main(){
	SmartPtr<int> sp1(new int);
	//在上面的类中实现了*运算符的重载
	*sp1 = 10
	cout << *sp1 <<endl;
	SmartPtr<int> sparray(new Date);
	
	//在上面的类中实现了->运算符的重载
	sparray->_year = 2018;
	sparray->_month = 1;
	sparray->_day = 1;
	
	SmartPtr<int> sp(new int);
	//假如说这里差产生了异常,导致程序提前退出没有执行到下面的释放代码,但是sp的生命周期结束了,所以也会释放开辟的空间
	delete sp;
}

auto_ptr

概念
  • 自动指针:在这个智能指针中允许指针之间的互相拷贝或赋值,例如:构造了一个对象 A 来管理 *ptr 这片空间,此时利用对象 A 拷贝了一个 B,那么 A 将会失去对 *ptr 这片空间的管理权,转而交给 B 来管理,这样的做法是为了防止多个对象来管理同一片资源,造成多次释放的问题;
  • 问题:由于存在管理权转移,所以在进行拷贝或者赋值后,原对象所管理的空间指针就失效了,此时再使用原对象进行操作就会出错,因此实际中很多公司明确规定了不能使用auto_ptr
实现
template<class T>
class AutoPtr{
public:
	AutoPtr(T* ptr = NULL)
		: _ptr(ptr)
	{}
	~AutoPtr(){
		if(_ptr)
			delete _ptr;
	}
	// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
	// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
	AutoPtr(AutoPtr<T>& ap)
		: _ptr(ap._ptr)
	{
		ap._ptr = NULL;
	}
	AutoPtr<T>& operator=(AutoPtr<T>& ap){
		// 检测是否是自己给自己赋值,这里检测的是对象中管理的空间是否相同,这样能避免所有多余的操作,如:两个对象的地址不同,但是管理的是同一片空间,这也就不用赋值了
		if(this->_ptr != ap._ptr){
			// 释放当前对象中资源
			if(_ptr)
			delete _ptr;
			// 转移ap中资源到当前对象中
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	//两个指针运算符的重载
	T& operator*() {return *_ptr;}
	T* operator->() { return _ptr;}
private:
	T* _ptr;
};
int main(){
	AutoPtr<Date> ap(new Date);
	AutoPtr<Date> copy(ap);
	//这里拷贝后,ap对象对于空间的管理权就丢失了,导致ap对象为nullptr,此时再通过ap对象访问资源时就会出现问题。
	//ap->_year = 2018;
	return 0;
}

unique_ptr

概念
  • 唯一指针:由于上面的auto_ptr存在管理权转移的问题,所以在这个指针中直接delete了拷贝、赋值接口,简单粗暴的解决了管理权的这个问题,但是这又使得这个指针的操作过于简单,不够完善;
实现
template<class T>
class UniquePtr{
public:
	UniquePtr(T * ptr = nullptr)
		: _ptr(ptr)
	{}
	~UniquePtr(){
		if(_ptr)
		delete _ptr;
	}
	T& operator*() {return *_ptr;}
	T* operator->() {return _ptr;}
private:
	// C++98防拷贝的方式:只声明不实现+声明成私有
	UniquePtr(UniquePtr<T> const &);
	UniquePtr & operator=(UniquePtr<T> const &);
	// C++11防拷贝的方式:delete
	UniquePtr(UniquePtr<T> const &) = delete;
	UniquePtr & operator=(UniquePtr<T> const &) = delete;
private:
	T * _ptr;
};

shared_ptr

概念
  • 共享指针:auto_ptr存在不安全问题,unique_ptr又过于简单,所以就出现了shared_ptr,该指针是通过引用计数的方式来实现多个shared_ptr对象之间共享资源;
    1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享;
    2. 在对象被销毁时 (也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一;
    3. 如果引用计数是 0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
    4. 如果不是 0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象所管理的指针就成野指针了;
安全问题
  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++ 或 --,这个操作不是原子的,可能会导致引用计数错乱,进而导致资源未释放或者程序崩溃的问题,所以只能指针中引用计数 ++ 或 – 是需要加锁的(或者设置成原子属性),以此来保证引用计数的操作是线程安全的;
  2. 智能指针管理的对象存放在堆上,两个线程中管理同一片资源的对象同时去访问资源,会导致线程安全问题,这个需要外部加锁来实现;
实现
  • 需要注意的一点是,当进行对象之间的赋值时,需要关注原来资源的情况,例如要将 sp2 对象的内容赋值给 sp1,那么就意味着 sp1 不再管理它原来的那片资源,所以需要对原来的那片资源计数减一,如果减一之后为 0,就需要释放原来的那片资源,然后再进行赋值操作;
template <class T>
class SharedPtr{
public:
	SharedPtr(T* ptr = nullptr)
		: _ptr(ptr)
		, _pRefCount(new int(1))
		, _pMutex(new mutex)
	{}
	//析构对象时需要计数减一
	~SharedPtr() { Release();}
	//拷贝对象时需要计数加一
	SharedPtr(const SharedPtr<T>& sp)
		: _ptr(sp._ptr)
		, _pRefCount(sp._pRefCount)
		, _pMutex(sp._pMutex)
	{
		AddRefCount();
	}
	// sp1 = sp2
	SharedPtr<T>& operator=(const SharedPtr<T>& sp){
		//如果两个对象所管理的是同一片资源,则就不需要赋值
		if (_ptr != sp._ptr){
			// 释放管理的旧资源
			Release();
			// 共享管理新对象的资源,并增加引用计数
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			_pMutex = sp._pMutex;
			AddRefCount();
		}
		return *this;
	}
	T& operator*() {return *_ptr;}
	T* operator->() {return _ptr;}
	int UseCount() {return *_pRefCount;}
	T* Get() { return _ptr; }
	//计数加一
	void AddRefCount(){
		// 加锁或者使用加1的原子操作
		_pMutex->lock();
		++(*_pRefCount);
		_pMutex->unlock();
	}
private:
	//计数减一
	void Release(){
		bool deleteflag = false;
		// 引用计数减1,如果减到0,则释放资源
		_pMutex.lock();
		if (--(*_pRefCount) == 0){
			//释放资源和计数空间
			delete _ptr;
			delete _pRefCount;
			deleteflag = true;
		}
		_pMutex.unlock();
		//如果资源被释放了,锁也就不需要了,也需要被释放
		if(deleteflag == true)
			delete _pMutex;
	}
private:
	int* _pRefCount; // 引用计数
	T* _ptr; // 指向管理资源的指针
	mutex* _pMutex; // 互斥锁
};
循环引用
问题

在这里插入图片描述

  1. node1 和 node2 两个智能指针对象分别指向 n1、n2 两个资源,此时两个资源的引用计数变成 1;
  2. 资源 n1 的 _prve 智能指针对象指向 n2,资源 n2 的 _next 指向 n1,两个资源的引用计数变成 2;
  3. node1 和 node2 析构,n1、n2 两个资源的引用计数减到 1,但是 n1 的 _prve 还指向 n2,n2 的 _next 还指向 n1;
  4. 而 _next 和 _prve 属于两个资源 n1、n2 的成员,n1 销毁时,_prve 才会析构,n2 销毁时,_next 才会析构;
  5. 但是两个资源因为有对象进行管理呢,计数至少为 1,所以不会被释放,这样导致的结果就叫循环引用,谁也不会释放;
解决
  • weak_ptr:弱引用指针,我们可以把资源中的shared_ptr修改为weak_ptr,当weak_ptr类型的对象指向一片资源时,资源的计数不会加一;
struct ListNode{
	//此时再使用_prve、_next指向node1、node2时,node1、node2的引用计数不会增加
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode(){ cout << "~ListNode()" << endl; }
};
int main(){
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	
	node1->_next = node2;
	node2->_prev = node1;
	
	return 0;
}
删除器
  • 一片空间可以由malloc / new / new[]等多种方式申请创建出来,我们也需要使用对应的方式free / delete / delete[]等进行析构,但是智能指针内部并不知道用户传入的空间是由什么方式开辟出来的,所以需要我们用户显式的传入释放空间所需的删除器;
  • 我们可以选择是在显示实例化对象时传入删除器,此时传入的是一个类型,然后在类中创建仿函数对象成员变量来进行删除;也可以在构造函数中传入删除器,此时传入的是一个仿函数对象用来初始化类中的删除器成员变量;
template <class T>
struct DeleteDel{
	void operator()(T* ptr){
		delete ptr;
	}
};

template <class T>
struct FreeDel{
	void operator()(T* ptr){
		free(ptr);
	}
};

template <class T>
struct DeleteArrDel{
	void operator()(T* ptr){
		delete[] ptr;
	}
};
template <class T, class Del = DeleteDel<T>>
class SharedPtr{
public:
	//删除器有默认参数,如果是用new开辟的单个空间,可以不传入删除器
	SharedPtr(T* ptr = nullptr, Del del = Del())
		: _ptr(ptr)
		, _pRefCount(new int(1))
		, _pMutex(new mutex)
		, _del(del)
	{}
	//析构对象时需要计数减一
	~SharedPtr() { Release();}
	//拷贝对象时需要计数加一
	SharedPtr(const SharedPtr<T>& sp)
		: _ptr(sp._ptr)
		, _pRefCount(sp._pRefCount)
		, _pMutex(sp._pMutex)
		, _del(sp._del)
	{
		AddRefCount();
	}
	// sp1 = sp2
	SharedPtr<T>& operator=(const SharedPtr<T>& sp){
		//如果两个对象所管理的是同一片资源,则就不需要赋值
		if (_ptr != sp._ptr){
			// 释放管理的旧资源
			Release();
			// 共享管理新对象的资源,并增加引用计数
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			_pMutex = sp._pMutex;
			AddRefCount();
		}
		return *this;
	}
	T& operator*() {return *_ptr;}
	T* operator->() {return _ptr;}
	int UseCount() {return *_pRefCount;}
	T* Get() { return _ptr; }
	//计数加一
	void AddRefCount(){
		// 加锁或者使用加1的原子操作
		_pMutex->lock();
		++(*_pRefCount);
		_pMutex->unlock();
	}
private:
	//计数减一
	void Release(){
		bool deleteflag = false;
		// 引用计数减1,如果减到0,则释放资源
		_pMutex.lock();
		if (--(*_pRefCount) == 0){
			//释放资源和计数空间
			_del(_ptr); // 使用删除器来清理资源
			delete _pRefCount;
			deleteflag = true;
		}
		_pMutex.unlock();
		//如果资源被释放了,锁也就不需要了,也需要被释放
		if(deleteflag == true)
			delete _pMutex);
	}
private:
	int* _pRefCount; // 引用计数
	T* _ptr; // 指向管理资源的指针
	mutex* _pMutex; // 互斥锁
	Del _del; // 删除器
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值