浅谈智能指针shared_ptr

智能指针的引入

由于C++语言没有自动内存回收机制,每次new出来的内存都要手动delete。

  • 忘记delete
  • 二次释放
  • 异常导致程序过早退出,没有执行delete
    用智能指针便可以有效缓解这类问题

理解智能指针

1.从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装。因此智能指针实质是一个对象,行为表现的却像一个指针。

2.每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,析构函数减少引用计数(如果引用计数减至0,则删除基础对象)。

3.智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。

智能指针的简单实现

template<typename T>
class CSmartPtr
{
public:
	// 构造函数
	CSmartPtr(T *ptr = nullptr)
		:mptr(ptr){}
	~CSmartPtr()
	{
		delete mptr;
	}
	
	
	T& operator*() { return *mptr; }
	const T& operator*()const { return *mptr; }
	T* operator->() { return mptr; }

	
private:
	T *mptr;  
};

int main()
{
	CSmartPtr<int>  ptr(new int(10));
	return 0;
}

由于ptr是栈上的智能指针对象,无论是函数正常执行结束还是异常结束,栈上的对象都会自动调用它的析构函数,保证释放资源。

仔细分析上述代码可以发现,我们没有给智能指针实现,拷贝构造函数,如果执行下面的语句,就会调用默认的拷贝构造函数进行浅拷贝,这样就会导致同一个资源析构两次,这是严重的错误。

CSmartPtr<int>  ptr1(ptr);

需要解决的问题

  • 浅拷贝问题
  • 多个智能指针指向同一个对象时,如何保证资源不被重复释放

解决方法

记录资源的引用计数类

class RefCnt
{
public:
	// 给资源添加引用计数
	void add(void *ptr)
	{
		auto it = mrefCntMap.find(ptr);
		if (it != mrefCntMap.end())
		{
			it->second++;
		}
		else
		{
			// make_pair(ptr,1);
			mrefCntMap.insert({ ptr, 1 });
		}
	}
	// 给资源减少引用计数
	void del(void *ptr)
	{
		auto it = mrefCntMap.find(ptr);
		if (it != mrefCntMap.end())
		{
			if (--(it->second) == 0)
			{
				mrefCntMap.erase(it);
			}
		}
	}
	// 返回指定资源的引用计数
	int get(void *ptr)
	{
		auto it = mrefCntMap.find(ptr);
		if (it != mrefCntMap.end())
		{
			return it->second;
		}
		return 0;
	}
private:
	// 一个资源void* 《=》 计数器 int
	unordered_map<void*, int> mrefCntMap;
};

自定义的智能指针

template<typename T, typename D = Deletor<T>>
class CSmartPtr
{
public:
	// 构造函数
	CSmartPtr(T *ptr = nullptr)
		:mptr(ptr)
	{
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}
	}
	~CSmartPtr()
	{
		mrefCnt.del(mptr);
	}
	CSmartPtr(const CSmartPtr<T> &src)
		:mptr(src.mptr)
	{
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}
	}
	
   CSmartPtr<T>& operator=(const CSmartPtr &src)
	{
		if (this == &src)
			return *this;

		mrefCnt.del(mptr);
		if (0 == mrefCnt.get(mptr))
			delete mptr;

		mptr = src.mptr;
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}

		return *this;
	}
	
	// 指针常用运算符重载函数
	T& operator*() { return *mptr; }
	const T& operator*()const { return *mptr; }
	T* operator->() { return mptr; }


private:
	T *mptr;  
	static RefCnt mrefCnt;
};
RefCnt CSmartPtr<T>::mrefCnt;

但是该程序还存在一个问题, C++STL库里面容器的增加,删除不是线程安全

删除器

一般情况下,我们都用智能指针是用来管理动态内存的,其实智能指针是用来管理资源的,资源很多,动态内存只是资源的一种,比如说我们可以用智能指针来管理文件,那么我们就不能用智能指针默认的删除器了,如果要管理文件的话最后是fclose,而不是delete,所以我们就必须自己定义一个删除器。

// 提供一个默认的删除器,默认删除的是堆内存资源
template<typename T>
class Deletor
{
public:
	void operator()(T *ptr) // 一元函数对象
	{
		delete ptr; // 默认删除的是堆内存资源
	}
};
// 自定义的智能指针 T资源的类型   D删除器的类型
template<typename T, typename D = Deletor<T>>
class CSmartPtr
{
public:
	// 构造函数
	CSmartPtr(T *ptr = nullptr, const D &d=D())
		:mptr(ptr), mdeletor(d)
	{
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}
	}
	~CSmartPtr()
	{
		if (0 == mrefCnt.get(mptr))
		{
			cout << "释放默认的堆内存资源" << endl;
			mdeletor(mptr); // 通过删除器来释放资源
		}
	}
	CSmartPtr(const CSmartPtr<T> &src)
		:mptr(src.mptr)
	{
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}
	}
	CSmartPtr<T>& operator=(const CSmartPtr &src)
	{
		if (this == &src)
			return *this;

		mrefCnt.del(mptr);
		if (0 == mrefCnt.get(mptr))
			delete mptr;

		mptr = src.mptr;
		if (mptr != nullptr)
		{
			mrefCnt.add(mptr);
		}

		return *this;
	}


	// 指针常用运算符重载函数
	T& operator*() { return *mptr; }
	const T& operator*()const { return *mptr; }
	T* operator->() { return mptr; }

	
private:
	T *mptr;  // FILE *pf;    delete pf;   fclose(pf);
	D mdeletor;
	static RefCnt mrefCnt;
};
template<typename T, typename D>
RefCnt CSmartPtr<T, D>::mrefCnt;

int main()
{
	// 智能指针  管理资源 =》  堆内存 文件
	CSmartPtr<int> ptr2(new int);

	class FileDeletor// 文件类型删除器
	{
	public:
		void operator()(FILE *pf) const
		{ 
			cout << "释放文件资源" << endl;
			fclose(pf); 
		}
	};
	CSmartPtr<FILE, FileDeletor> ptr(fopen("data.txt", "w+"));

	// mdeletor(mptr);                     
	// CSmartPtr<FILE> ptr3(fopen("data1.txt", "w+"), [](FILE *pf)->void { cout << "xxx" << endl; });
	// delete p;
	
	unique_ptr<int> ptr3(new int);

	unique_ptr<FILE, FileDeletor> ptr4(fopen("d", "w+"));
	unique_ptr<FILE> ptr5(fopen("dd", "w+")});

	return 0;
}

能否在堆上直接定义智能指针??

CSmartPtr<int>  *ptr=new CSmartPtr<int> (new int);
//delete ptr;

ptr虽然是智能指针类型,但实质上就还是一个裸指针,需要我们手动进行delete释放资源,智能指针的意义也就不复存在。

智能指针的使用

智能指针包括在C++11之前的auto_ptr,以及C++11版本之后提供的shared_ptr,unqueue_ptr,weak_ptr,包含在头文件 < memory > 中

shared_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类对象,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误的

  • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。

  • 访问智能指针包含的裸指针则可以用 get() 函数

  • 由于智能指针是一个对象,要判断智能指针的裸指针是否为空,需要这样判断:if (obj.get())

  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存

  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。

  • 智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。

shared_ptr 的操作


make_shared()函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向对象的shared_ptr。与只能指针一样,make_shared也定义在头文件memory中。

当要使用make_shared时,必须指定想要创建的对象的类型。

//p1指向一个值为10的int的shared_ptr
shared_ptr<int> p1=make_shared<int> (20);

//p2指向一个值为"555"的string
shared_ptr<string> p2=make_shared<string> (3,'5');

//p3指向一个值初始化的int,即,值为0
shared_ptr<int> p3=make_shared<int> ();

当然,我们通常用auto定义一个对象来保存make_shared的结果 ,这种方式较简单:

p4指向一个动态分配的空vector

 auto p4=make_shared<vector<int>> ();

shared_ptr的拷贝和赋值

当进行拷贝或赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。

auto ptr1=make_shard<int>(10); //ptr1指向的对象只有ptr1一个引用者
auto ptr2(ptr1);  //ptr1和ptr2指向相同的对象,此对象有两个引用者


auto p=make_shared<int>(11);  //p指向的int只有一个引用者

p=q;   //给p赋值,令它指向另一个地址,递增q指向的对象的引用计数,递减p原来指向的对象的引用计数,p原来指向的对象已没有引用者,会自动释放

//此例中我们分配了一个int,将其指针保存在p中。接下来,我们将一个新值赋予p。在此情况下,p是唯一指向此int的shared_ptr,在把q赋给p的过程中,此会自动释放。

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。

shared_ptr的析构函数会递减它所指对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。

boost::shared_ptr的线程安全问题

shared_ptr的底层实现原理是引用计数,关于这个计数是否线程安全呢??

首先通过原子操作解决了引用计数的线程安全问题, 但是智能指针指向的对象的线程安全问题,智能指针没有做任何的保证.

因为 shared_ptr 有两个数据成员,读写操作不能原子化.使得多线程读写同一个 shared_ptr 对象需要加锁。

多线程无保护读写 shared_ptr 可能出现的 race condition

shared_ptr循环引用导致内存泄露

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	shared_ptr<B> _ptrb;

	void testA() { cout << "A类中很强大的一个方法实现!" << endl; }
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_ptr<A> _ptra;
	// operator*  operator->

	void func()
	{
		// promote 提升 弱 -》 强(引用计数 1)
		// shared_ptr   operator== !=  nullptr
		shared_ptr<A> sp =  _ptra.lock();
		if (sp != nullptr)
		{
			// 提升成功了(说明资源还在)
			sp->testA();
		}
		else
		{
			// 提升失败了(说明资源已经不存在了)
		}
	}
};
int main()
{
	// 用引用计数解决问题,有一个bug
	shared_ptr<A> pa(new A());
	shared_ptr<B> pb(new B());

	pa->_ptrb = pb;
	pb->_ptra = pa;

	cout << pa.use_count() << endl;
	cout << pb.use_count() << endl;

	pb->func();

	return 0;
}

在程序退出前,A的引用计数为2,B的计数也为2,退出时,shared_ptr所作操作就是简单的将计数减1,如果为0则释放,显然,这个情况下,引用计数不为0,于是造成A和B的内存得不到释放,导致内存泄露。
在这里插入图片描述

一般来讲, 解除这种循环引用 有下面有三种可行的方法( 参考 ):
1 . 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
2 . 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。
3 . 使用弱引用的智能指针打破这种循环引用。

虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr。

强智能指针和弱智能指针

  • shared_ptr 强(引起资源引用计数的改变)智能指针

  • weak_ptr 弱(不会引起资源引用计数的改变)智能指针

  • weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。没有重载*和->但可以使用lock获得一个可用的shared_ptr对象。

  • 弱智能指针 相当于是监测强智能指针的,它不能直接指向资源,要想引用资源,必须提升为强智能指针。 是线程安全的智能指针。

  • 强智能指针的引用计数是资源的引用个数

  • 弱智能指针的引用计数是有多少个观察者

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	weak_ptr<B> _ptrb;//改为弱智能指针

	void testA() { cout << "A类中很强大的一个方法实现!" << endl; }
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	weak_ptr<A> _ptra;//改为弱智能指针
	// operator*  operator->

	void func()
	{
		
		// shared_ptr   operator== !=  nullptr
		shared_ptr<A> sp =  _ptra.lock();//// promote 提升 弱 -》 强(引用计数 1)
		if (sp != nullptr)
		{
			// 提升成功了(说明资源还在)
			sp->testA();
		}
		else
		{
			// 提升失败了(说明资源已经不存在了)
		}
	}
};
int main()
{
	// 用引用计数解决问题,有一个bug
	shared_ptr<A> pa(new A());
	shared_ptr<B> pb(new B());

	pa->_ptrb = pb;
	pb->_ptra = pa;

	cout << pa.use_count() << endl;
	cout << pb.use_count() << endl;

	pb->func();
	return 0;
}

建议定义对象的时候使用强智能指针(shared_ptr),其他引用的地方使用弱智能指针(weak_ptr)

猜你喜欢

转载自blog.csdn.net/qq_43313035/article/details/88943600