当前位置:首页 » 《随便一记》 » 正文

C/C++智能指针_水澹澹兮生烟

21 人参与  2021年11月20日 16:03  分类 : 《随便一记》  评论

点击全文阅读


目录

1.1RAII(资源获取几初始化)

 1.2auto_ptr

1.3unique_ptr

1.4shared_ptr

1.5weak_ptr


 我们在在动态开辟空间的时候,malloc出来的空间如果没有进行释放,那么回传在内存泄漏问题。或者在malloc与free之间如果存在抛异常,那么还是有内存泄漏安全。因此我们在这里引入了智能指针来对资源进行管理。(内存泄漏)

1.1RAII(资源获取几初始化)

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法的好处:

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

总结:RAII就是一种管理资源自动释放的一种机制,初步看来,他通过类将资源包装起来。在进行资源初始化时,巧妙地利用编译器会自动调用构造函数预计析构函数的特性,来完成对资源的自动释放。在构造方法中,将资源放入,让对象进行释放,在析构方法中,将资源释放掉。

#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T>
class Smartptr{
public:
	Smartptr(T* p = nullptr) :ptr(p){
	}
	~Smartptr(){
		if (ptr){//此时指针如果不为空且具有释放的权利的时候,则将其释放,且将owner重新职位false
			delete ptr;
			ptr = nullptr;
		}
	}
	//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载
	//重载*
	T& operator*(){
		return *ptr;
	}
	//他只能在指针指向的是对象或者是结构体的时候来使用
	T& operator->(){
		return ptr;
	}
	//某些情况下使用原生态指针
	T* get(){
		return ptr;
	}
private:
	T* ptr;//采用类进行指针管理
};
int main(){
	Smartptr<int> st1(new int);
	Smartptr<int> at2(st1);//此时调用拷贝构造函数,但是这个类里面没有,因此只能使用默认的拷贝构造
	//因此是浅拷贝
	return 0;
}

根据上面代码,我们先简单的模拟了一下智能指针发现了存在这一个致命的问题,如果当一个对象对另一个对象进行拷贝构造时,由于没有定义拷贝构造函数,那么就会使用到默认的拷贝构造函数,产生浅拷贝问题。又因为所有的智能指针都是一样的,那如何解决浅拷贝问题呢?我们在前面学习string类时,对浅拷贝的解决方式时使用深拷贝,但是在这里我们不能使用深拷贝,在string类中,因为其内部要存字符串,需要申请空间,而string类中的空间是自己申请与维护的,而智能指针的资源是用户提供的,如下图:

 智能指针不能申请资源只能提用户来管理资源,因此此处不能使用深拷贝的方式来解决问题。

 1.2auto_ptr

 资源完全转移

我们参考C++98版本的库中就提供了auto_ptr的智能指针是如何解决浅拷贝问题的。

namespace bite{
	template<class T>
	class auto_ptr{
	public:
		// RAII : 保证资源可以自动释放
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr){}
		~auto_ptr(){
			if (_ptr){
				delete _ptr;
				_ptr = nullptr;
			}
		}
		// 解决浅拷贝方式:资源转移
		// auto_ptr<int>  ap2(ap1)
		auto_ptr(auto_ptr<T>& ap)
			: _ptr(ap._ptr){
			ap._ptr = nullptr;
		}

		// ap1 = ap2;
		auto_ptr<T>& operator=(auto_ptr<T>& ap){
			if (this != &ap){
				// 此处需要将ap中的资源转移给this
				// 但是不能直接转移,因为this可能已经管理资源了,否则就会造成资源泄漏
				if (_ptr){
					delete _ptr;
				}
				// ap就可以将其资源转移给this
				_ptr = ap._ptr;
				ap._ptr = nullptr;   // 让ap与之前管理的资源断开联系,因为ap中的资源已经转移给this了
			}
			return *this;
		}
		// 对象具有指针类似的行为
		T& operator*(){
			return *_ptr;
		}
		T* operator->(){
			return _ptr;
		}
		T* Get(){
			return _ptr;
		}
	private:
		T* _ptr;
	};
}
int main(){
	auto_ptr<int> st1(new int);
	auto_ptr<int> at2(st1);
	return 0;
}

我们观察上述代码,虽然他解决了浅拷贝问题,但是他又引入了新的问题,。当对象拷贝或者赋值后,前面的对象就悬空了。它的缺陷就是当我们想访问或者修改st1对象的时候,代码会崩溃。

资源管理权限转移

 为了解决上面的问题有使用了转移资源管理权限的思想。

#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T>
class autoptr{
public:
	autoptr(T* p = nullptr) :ptr(p), owner(true){
	}
	~autoptr(){
		if (ptr && owner){//此时指针如果不为空且具有释放的权利的时候,则将其释放,且将owner重新职位false
			delete ptr;
			owner = false;
		}
	}
	//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载
	//重载*
	T& operator*(){
		return *ptr;
	}
	//他只能在指针指向的是对象或者是结构体的时候来使用
	T& operator->(){
		return ptr;
	}
	//某些情况下使用原生态指针
	T* get(){
		return ptr;
	}
	//因此在这里解决浅拷贝问题
	//资源管理权限的转移
	autoptr(autoptr<T>& p) :ptr(p.ptr), owner(p.owner){
		p.owner = false;
	}
	T& operator=(autoptr<T>& p){//赋值运算符的重载
		if (this == p){
			//首先判断是否是自己给自己复制
			return p;
		}
		if (ptr && owner){
			//如果此时ptr不为空且具有权限,那么此时就将现在的资源释放掉,顺便拿到p的权限
			delete ptr;
			ptr = p.ptr;
			owner = p.owner;
			p.owner = false;
		}
	}
	//某些情况下使用原生态指针
private:
	T* ptr;//采用类进行指针管理
};
int main(){
	autoptr<int> st1(new int);
	autoptr<int> at2(st1);//此时调用拷贝构造函数,但是这个类里面没有,因此只能使用默认的拷贝构造函数
	//因此是浅拷贝
	return 0;
}

 如上面代码,当发生拷贝构造或者赋值时,将被拷贝对象中资源转移给新对象,然后让被拷贝对象与资源断开联系,这样就解决了一块空间被多个对象使用而造成程序崩溃问题。但是在这里存在着致命缺陷。再对st1进行拷贝后将其的指针赋值为空,导致了st1对象悬空,通过st1对象访问资源就会出现问题,会造成野指针,使代码崩溃。因此要在这里说明什么情况下对不要使用auto_ptr。

1.3unique_ptr

 上面的问题都是因为发生了拷贝构造然后造成的,因此unique_ptr在这里采用的方式是禁止拷贝。也就是说,一份资源只能被一个对象来进行管理,对象之见不能共享资源(资源独占)。解决浅拷贝方式--资源独占,防止拷贝,在这里有两种方案,第一种:C++98中的方案,将拷贝构造函数以及赋值运算符重载方法只进行声明不进行定义,并且将其权限给成私有的,这样就防止其被拷贝。第二种:C++11种的方案:可以让编译器不生成默认的拷贝构造以及赋值运算符delete,delete关键字它的扩展功能就是从堆上进行释放资源,用其修饰默认的构造函数,表明编译器不会生成了。


#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<calss T>
class DF_new{
public:
    void operatr()(T*& ptr){
        if(ptr){
            delete ptr;
            ptr = nullptr;
        }
    }
};
template<calss T>
class DF_free{
public:
    void operatr()(T*& ptr){
        if(ptr){
            free(ptr);
            ptr = nullptr;
        }
    }
};
//关闭文件指针
template<calss T>
class DF_close{
public:
    void operatr()(FILE*& ptr){
        if(ptr){
            fclose(ptr);
            ptr = nullptr;
        }
    }
};
//T:资源中所放的数据的类型
//DF:资源的释放方式
template<class T,class DF = DF_new<T>>//DF释放的方式
class uniqueptr{
public:
	uniqueptr(T* p = nullptr) :ptr(p){
	}
	~uniqueptr(){
		if (ptr){
            //对于ptr管理的资源,有可能是从堆上申请的内存空间,文件指针,malloc空间...
            //因此他在释放的是否是要进行考虑的,是不同的,解决的方式就是对这个类再加上一个模板参数列表即可
            
			ptr = nullptr;
		}
	}
	//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载
	//重载*
	T& operator*(){
		return *ptr;
	}
	//他只能在指针指向的是对象或者是结构体的时候来使用
	T& operator->(){
		return ptr;
	}
	//某些情况下使用原生态指针
	T* get(){
		return ptr;
	}
	//解决浅拷贝方式--资源独占,防止拷贝,在这里有两种方案
	//第一种:C++98中的方案:
private:
	uniqueptr(const uniqueptr<T,DF>&);
	uniqueptr<T&>operator=(const uniqueptr<T,DF>&);
	//第二种:C++11中的方案:可以让编译器不生成默认的拷贝构造以及赋值运算符--delete
	uniqueptr(const uniqueptr<T,DF>&) = delete;
	//表明编译器不会生成默认的赋值运算符重载
	uniqueptr<T,DF>& operator=(const uniqueptr<T,DF>&) = delete;
private:
	T* ptr;//采用类进行指针管理
};

在这里说明一下为什么在C++98中对其拷贝构造函数与赋值运算符重载只进行定义,不声明不定义,且将其权限给成私有的。如果没有将其设置为私有的,那么用户就会在外部对其方法进行定义。

unique_ptr指针适用于资源被一个对象管理并且不会被共享。他的缺陷就是多个对象中资源无法进行共享,因此使用到了shared_ptr指针。

1.4shared_ptr

共享指针,对个对象之间可以共享资源。在这里采用引用计数的方式来进行浅拷贝的。引用计数实际上就是一个整形空间,记录使用资源的对象的个数,在释放之前,让最后一个使用资源的的对象来进行释放。

#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T,class DF = DF_new<T>>
class sharedptr{
public:
	sharedptr(T* p = nullptr) 
		:ptr(p)
		,p_count(nullptr){
			if(ptr){//此时只有当前建好的一个对象在使用该份资源
				p_count = new int(1);
			}
	}
	~sharedptr(){
		if (ptr && 0 == --(*count)){
			DF df;
			df(ptr);
			delete p_count;
			p_count = nullptr;
		}
	}
	//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载
	//重载*
	T& operator*(){
		return *ptr;
	}
	//他只能在指针指向的是对象或者是结构体的时候来使用
	T& operator->(){
		return ptr;
	}
	//某些情况下使用原生态指针
	T* get(){
		return ptr;
	}
	//用户可能需要获取引用计数
	int use_count()const{
		return *p_count;
	}
	//解决浅拷贝方式,引用计数
	sharedptr(const sharedptr<T,DF>& sp)
			:ptr(sp.ptr)
			,p_count(sp.p_count){
				if(ptr){
					++(*p_count);
				}
	}
	sharedptr<T,DF>& operator=(const sharedptr<T,DF>& sp){
		if(this != &sp){
			//在sp共享之前,需要将之前的资源进行释放
			if(ptr && 0 == --*(p_count)){
				//如果此时之前的内容只有他一个进行管理,那么直接进行释放
				DF df;
				df(ptr);
				delete p_count;
			}
			//this就可以与sp进行共享了
			ptr = sp->ptr;
			p_count = sp->p_count;
			if(p_count){
				p_count++;
			}
		}
		return *this;
	}

private:
	T* ptr;//采用类进行指针管理
	int* p_count;//指向的是使用资源的对象的个数
};

释放的操作:先检测是否有资源,有资源即是pcount>=1,先给计数器进行-1操作,然后检测计数器是否为0,如果是0,则说明当前对象是最后使用资源的对象,,需要将资源以及计数空间进行释放,当为非0的时候,说明还有其他对象在使用资源,当前资源不需要释放。

我们观察上面的代码,可以判断吹他在单线程下是没有出现问题的,但是在多线程下可能是有问题的。多线程下有多个执行流,CPU也是多核的,多个线程同时往下执行,假设现在连个线程中的智能指针共享的是同一份资源,两个线程结束时,需要将其管理的资源释放掉。也有情况下,线程同事进行判断,使得最后导致资源没有进行释放,而引起资源泄漏。因此,在遇到共享的资源,变量等等之类的,需要考虑多线程环境下的安全性。因此最常见的方式是对其进行加锁。在这里进行加锁,是为了保证自身的安全性。


#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T,class DF = DF_new<T>>
class sharedptr{
public:
	sharedptr(T* p = nullptr) 
		:ptr(p)
		,p_count(nullptr)
		,mutex(new mutex){
			if(_ptr){
				p_count = new int(1);
			}
	}
	~sharedptr(){
		reldef();
	}
	//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载
	//重载*
	T& operator*(){
		return *ptr;
	}
	//他只能在指针指向的是对象或者是结构体的时候来使用
	T& operator->(){
		return ptr;
	}
	//某些情况下使用原生态指针
	T* get(){
		return ptr;
	}
	//用户可能需要获取引用计数
	int use_count()const{
		return *p_count;
	}
	//解决浅拷贝方式,引用计数
	sharedptr(const sharedptr<T,DF>& sp)
			:ptr(sp.ptr)
			,p_count(sp.p_count)
			,_pmutex(sp._pmutex){
				Addref();
	}
	sharedptr<T,DF>& operator=(const sharedptr<T,DF>& sp){
		if(this != &sp){
			//在sp共享之前,需要将之前的资源进行释放
			reldef();
			//this就可以与sp进行共享了
			ptr = sp->ptr;
			p_count = sp->p_count;
			_pmutex = sp._pmutex;
			Addref();
		}
		return *this;
	}
private:
	void Addref(){//对加法进行处理
		if(!ptr) return;
		_pmutex->lock();
		++(*p_count);
		_pmutex->unlock();
	}
	//此时我们还需要判断锁是否需要释放
	void reldef(){//对减法进行处理
		if(ptr) return;		
		bool isdelete = false;
		_pmutex->lock();
		if (ptr && 0 == --(*count)){
			DF df;
			df(ptr);
			delete p_count;
			p_count = nullptr;
			//当资源释放完毕后,对其进行标记
			isdelete = true;
		}
		_pmutex->unlock();
		if(isdelete){
			delete(_pmutex);
		}
	}
private:
	T* ptr;//采用类进行指针管理
	int* p_count;//指向的是使用资源的对象的个数
	mutex* _pmutex;//加上锁的原因是要保证在这里引用计数的操作是原子性的
};

虽然shared_ptr在这里是可以避免拷贝构造带来的错误,但是他自身也有缺陷。在使用shared_ptr时可能会引起循环引用。什么是循环引用呢?我们先举个例子。

#incldue<memory>
struct ListNode{
	shared_ptr<ListNode*> next;
	shared_ptr<ListNode*> prve;
	int data;
	shared(int x):next(nullptr),prev(nullptr),data(x){
		cout<<"ListNode(int)"<<this<<endl;
	}
	~ListNode(){
		cout<<"~ListNode():"<<this<<endl;
	}
};
void Looptest(){
	//将两个节点分别交给智能指针来管理
	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(){
	Looptest();
}

当shared_ptr管理的资源在相互指向的时候,我们看上面代码的运行情况:在结果中,我们发现运行时并未出现调用析构函数的结果,在这里没有释放掉资源,因此会引起资源泄露问题。也就是说,循环引用是指两个对象之间形成了环路,在智能指针shared_ptr中存在这个问题,他的引用计数不为0。也就是两份资源分别等待对方先进行释放,最后导致了内存泄漏。处理这种现象十分简单,只需要只使用一个weak_ptr即可。

1.5weak_ptr

weak_ptr的实现原理是使用了引用计数进行实现的,他不可以进行资源的管理,唯一的作用就是配合shared_ptr解决循环引用的问题。


#incldue<memory>
struct ListNode{
	weak_ptr<ListNode*> next;
	weak_ptr<ListNode*> prve;
	int data;
	shared(int x):next(nullptr),prev(nullptr),data(x){
		cout<<"ListNode(int)"<<this<<endl;
	}
	~ListNode(){
		cout<<"~ListNode():"<<this<<endl;
	}
};
void Looptest(){
	//将两个节点分别交给智能指针来管理
	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(){
	Looptest();
}



我们看上面的代码,此时析构函数执行了,并没有发生引用循环。

question:为什么weak_ptr可以解决循环引用?

原因是在他的引用计数上。如上图代码,我们进行分析:

 在标准库中,weak_ptr的引用计数维护了两份,由图可知,当开始执行时,use=weak=1;此时在执行sp1->next=sp2,因为sp1->next的类型是一个weak_ptr,因此此时的sp2的引用计数的weak++,再执行sp2->prve=sp1,因为sp2->prve的类型也是一个weak_ptr,因此此时的sp1的引用计数weak++;此时sp1指向空间中的计数use=1,weak=2,sp2指向的资源空间的计数也是一样。

现在要对资源进行释放。首先释放sp2,因为sp2的类型是一个shared_ptr,use--等于0,说明此时资源是可以进行释放的,因此就要对对象内部的每一个资源进行释放掉,sp2->prev是weak_ptr类型,将其销毁,那么左面资源的中的引用计数weak--,然后sp2->prve与sp1断开,next指针也销毁掉了,因此此时的节点也销毁掉了,所以sp2的pcount与资源的引用计数断开,右面的资源的引用计数weak--。

现在进行释放sp1,因为sp1的类型是一个shared_ptr,use--等于0,说明此时资源是可以进行释放的,因此就要对对象内部的每一个资源进行释放掉,sp1->next是weak_ptr类型,将其销毁,那么右面资源的中的引用计数weak--,此时右面的引用计数的weak=0,因此就可以将右面资源的引用计数进行释放;左面资源的prve指针此时也销毁了,此时节点进行销毁,所以sp1的pcount与资源的引用计数断开,左面的weak--等于0,此时将左面的资源的引用计数进行销毁。

总结:当一个资源被shared_ptr共享时,use++;当一个资源被weak_ptr共享时,weak++。且只有shared_ptr可以独立的管理资源。

question:unique_ptr与shared_ptr能否可以管理一块连续空间?

可以。如果要管理里一段连续的空间,我们必须自己实现删除器,operator()(T*&ptr){delete[] ptr;ptr=nullptr;}。但是没有什么意义,对于连续空间,一般是不会直接交给智能指针进行管理的,因为在STL中已经有了vector。


点击全文阅读


本文链接:http://zhangshiyu.com/post/31093.html

指针  资源  拷贝  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1