【C++】智能指针

      • 智能指针的使用及原理
        • 智能指针的使用
        • 智能指针原理
      • C++中的智能指针
        • std::auto_ptr
        • std::unique_ptr
        • std::shared_ptr
          • std::shared_ptr的基本设计
          • std::shared_ptr 的线程安全问题
          • std::shared_ptr的定制删除器
        • std::weak_ptr
          • std::shared_ptr的循环引用问题
          • std::weak_ptr解决循环引用问题

智能指针的使用及原理

智能指针的使用

智能指针可以很好的帮助我们解决内存泄漏问题

内存泄漏是指因为疏忽或错误,造成程序结束后,没有释放已经不在使用的内存情况。比如:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}

	return a / b;
}

void func()
{
	int* ptr = new int;
	//...
	cout << div() << endl;
	//...
	delete ptr;
}

int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

执行上述代码时,如果用户输入的除数为0,那么div函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func函数中申请的内存资源没有得到释放。

解决方法一:利用异常的重新捕获解决

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}

	return a / b;
}

void func()
{
	int* ptr = new int;
	//...
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete ptr;
		throw;
	}
	//...
	delete ptr;
}

int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

【C++】智能指针_第1张图片

解决方法二:利用智能指针解决

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr):_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

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

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}

	return a / b;
}

void func()
{
	SmartPtr<int> sp(new int);
	//....
	cout << div() << endl;
	//....
}

int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

【C++】智能指针_第2张图片
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
  • 此外,为了让SmartPtr对象能够像原生指针一样使用,还需要对*和->运算符进行重载。

这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。

智能指针原理

智能指针原理

实现智能指针的时候要注意以下几点:

  1. 在对象构造时获取资源,在对资源析构时释放资源,利用对象的生命周期来控制程序的资源,即RALL特性。
  2. *->运算符进行重载,使得该对象具有像指针一样的行为。
  3. 智能指针对象的拷贝问题。

什么时RALL

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术。

为什么要解决智能指针对象的拷贝问题

对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。比如:

void func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1);//拷贝构造


	SmartPtr<int> sp3 = sp1;//拷贝赋值
	//....
	cout << div() << endl;
	//....
}

【C++】智能指针_第3张图片
导致报错的原因是因为多次释放的问题。

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

C++中的智能指针

std::auto_ptr

管理权转移

auto_ptr是C++98中引入的智能指针,它的作用就是将指针所指向的管理权转移了,转移到了另一个指针目标上,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。

但是有一个不好的地方就是,当转移管理权后,原先的数据就不可以再次使用了,通俗的说就是悬空了,在进行操作就会报错

模拟实现auto_ptr

简易版的auto_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使auto_ptr对象具有指针一样的行为。
  • 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
  • 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。
class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		// 管理权转移
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		
		auto_ptr& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				delete _ptr;       //释放自己管理的资源
				_ptr = ap._ptr;    //接管ap对象的资源
				ap._ptr = nullptr; //管理权转移后ap被置空
			}
			return *this;
		}


		T& operator*()
		{
			return *_ptr;
		}

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

	void test_auto()
	{
		auto_ptr<int> ap1(new int(1));
		auto_ptr<int> ap2(ap1);

		*ap1 = 1; // 管理权转移以后导致ap1悬空,不能访问
		*ap2 = 1;
	}

但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题,很多公司也都明确规定了禁止使用auto_ptr。

【C++】智能指针_第4张图片

std::unique_ptr

防拷贝

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。比如:

int main()
{
	std::unique_ptr<int> up1(new int(0));
	//std::unique_ptr up2(up1); //error
	return 0;
}

但防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。

unique_ptr的模拟实现

简易版的unique_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使unique_ptr对象具有指针一样的行为。
  • 用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式在这两个函数后面加上=delete,防止外部调用。
template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

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

		// C++11思路:语法直接支持
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;


		// 防拷贝
		// 拷贝构造和赋值是默认成员函数,我们不写会自动生成,所以我们不需写
		// C++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以再加一条,声明为私有
	private:
		//unique_ptr(const unique_ptr& up);
		// unique_ptr& operator=(const unique_ptr& up);
	private:
		T* _ptr;
	};

C++11
【C++】智能指针_第5张图片
C++98
【C++】智能指针_第6张图片

std::shared_ptr
std::shared_ptr的基本设计

shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。

  • 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数可以记录当前有多少个资源一起共享
  • 当新增一个对象一起共享此资源时,引用进行++,当一个对象退出不在管理此资源时进行–。
  • 当它的引用减为0时,开始进行释放。

通过这种方法,我们的智能指针就可以进行赋值拷贝和拷贝构造了,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。比如:

int main()
{
	cl::shared_ptr<int> sp1(new int(1));
	cl::shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	cout << sp1.use_count() << endl; //2

	cl::shared_ptr<int> sp3(new int(1));
	cl::shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	cout << sp3.use_count() << endl; //2
	return 0;
}

shared_ptr的模拟实现

简易版的shared_ptr的实现步骤如下:

  • 在shared_ptr中,比起unique_ptr新增加了一个引用计数count,用来记录有多少个对象一起共享资源。
  • 当进行构造的时候,我们需要将引用计数count初始化为1,表示当前只有一个对象进行共享。
  • 在拷贝构造中,不仅要进行资源共享,count也要进行共享,并且要进行++操作
  • 在赋值拷贝中,要先对当前资源的共享引用计数进行–(如果count=0,则进行释放),–完成后,再将要传入的对象进行赋值并且进行资源共享,引用计数还有进行++。
  • 在析构函数中,将管理资源对应的引用计数–,如果减为0则需要将该资源释放。
  • 对*和->运算符进行重载,使shared_ptr对象具有指针一样的行为。

初级模拟实现shared_ptr

template<class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr) :_ptr(ptr), count(new int(1))
	{}

	~Shared_ptr()
	{
		if (--(*count) == 0)
		{
			if (_ptr != nullptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
			delete count;
			count = nullptr;
		}
	}

	Shared_ptr(const Shared_ptr<T>& sp):_ptr(sp._ptr),count(sp.count)
	{
		(*count)++;
	}

	Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
	{
		//管理同一块空间的对象之间无需进行赋值操作
		//if (&sp != this)
		//当sp1 = sp2已经赋值过一次后,他们就共享了资源了,如果再次对他们进行一次sp1 = sp2就没有意义了
		if (_ptr != sp._ptr)
		{
			if (--(*count) == 0)//将管理的资源对应的引用计数--
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				delete count;
			}
			_ptr = sp._ptr;       //与sp对象一同管理它的资源
			count = sp.count; //获取sp对象管理的资源对应的引用计数
			(*count)++;         //新增一个对象来管理该资源,引用计数++
		}
		return *this;
	}

	//获取引用计数
	int use_count()
	{
		return *count;
	}
	//可以像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;//管理的资源
	int* count;//管理的资源对应的引用计数
};

【C++】智能指针_第7张图片
【C++】智能指针_第8张图片

为什么引用计数需要存放在堆区?

首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。
【C++】智能指针_第9张图片
其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。
【C++】智能指针_第10张图片
而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。

这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
【C++】智能指针_第11张图片
但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。

std::shared_ptr 的线程安全问题

shared_ptr的线程安全问题

因为我们的引用计数的方法是共享的,当多个线程进行同一时间访问时,就会有线程安全问题,主要就是++和–不是原子操作,所以我们在这里要给他们进行加锁,使其变为原子操作。
比如下面代码中用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁。比如:

void func(Shared_ptr<int> sp,int n)
{
	for (size_t i = 0; i < n; i++)
	{
		Shared_ptr<int> copy(sp);
	}
}
int main() 
{
	Shared_ptr<int> p = new int(1);
	const size_t n = 100000;
	thread t1(func, p, n);
	thread t2(func, p, n);

	t1.join();
	t2.join();

	cout << p.use_count() << endl; //预期:1
	return 0;
}

【C++】智能指针_第12张图片
在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作。

加锁解决线程安全问题

要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例。

  • 同样,互斥量也是每个线程共享的,所以它也必须和引用计数一样,放到堆上。
  • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数交给当前对象管理之外,还需要将对应的互斥锁也交给当前对象。
  • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,由于互斥锁也是在堆区创建的,因此还需要将对应的互斥锁进行释放。
template<class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr) :_ptr(ptr), count(new int(1)),_pmutex(new mutex)
	{}

	void Addcount()
	{
		_pmutex->lock();
		++(*count);
		_pmutex->unlock();
	}

	void Release()
	{
		_pmutex->lock();
		bool deleteFlag = false;
		if (--(*count) == 0)
		{
			if (_ptr != nullptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
			delete count;
			count = nullptr;
			deleteFlag = true;
		}
		_pmutex->unlock();
		if (deleteFlag)
		{
			delete _pmutex;
			_pmutex = nullptr;
		}
	}

	~Shared_ptr()
	{
		Release();
	}


	Shared_ptr(const Shared_ptr<T>& sp):_ptr(sp._ptr),count(sp.count),_pmutex(sp._pmutex)
	{
		Addcount();
	}

	Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
	{
		//管理同一块空间的对象之间无需进行赋值操作		
		//if (&sp != this)
		//当sp1 = sp2已经赋值过一次后,他们就共享了资源了,如果再次对他们进行一次sp1 = sp2就没有意义了
		if (_ptr != sp._ptr)//
		{
			//if (--(*count) == 0)//将管理的资源对应的引用计数--
			//{
			//	cout << "delete: " << _ptr << endl;
			//	delete _ptr;
			//	delete count;
			//	delete _pmutex;
			//}
			Release();
			_ptr = sp._ptr;       //与sp对象一同管理它的资源
			count = sp.count; //获取sp对象管理的资源对应的引用计数
			_pmutex = sp._pmutex;
			Addcount();         //新增一个对象来管理该资源,引用计数++

		}
		return *this;
	}

	//获取引用计数
	int use_count()
	{
		return *count;
	}
	//可以像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}

private:
	T* _ptr;//管理的资源
	int* count;//管理的资源对应的引用计数
	mutex* _pmutex;
};

void func(Shared_ptr<int> sp,int n)
{
	for (size_t i = 0; i < n; i++)
	{
		Shared_ptr<int> copy(sp);
	}
}
int main() 
{
	Shared_ptr<int> p = new int(1);
	const size_t n = 100000;
	thread t1(func, p, n);
	thread t2(func, p, n);

	t1.join();
	t2.join();

	cout << p.use_count() << endl; //预期:1
	return 0;
}

【C++】智能指针_第13张图片
注意:

  • 在release函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后释放需要释放互斥锁资源
std::shared_ptr的定制删除器

定制删除器的用法

当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是以new[]的方式申请到的空间,或管理的是一个文件指针。比如:

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10]);   //error
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error

	return 0;
}

这时当智能指针对象的生命周期结束时,再以delete的方式释放管理的资源就会导致程序崩溃,因为以new[]的方式申请到的内存空间必须以delete[]的方式进行释放,而文件指针必须通过调用fclose函数进行释放。

这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:

template <class U, class D>
shared_ptr (U* p, D del);

参数说明:

  • p:需要让智能指针管理的资源。
  • del:接受仿函数类型,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。

因此当智能指针管理的资源不是以new的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器。比如:

template<class T>
struct DelArr
{
	void operator()(const T* ptr)
	{
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	});

	return 0;
}

定制删除器的模拟实现

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

	template<class D>
	Shared_ptr(T* ptr, D del)
		: _ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
		, _del(del)
	{}

	~Shared_ptr()
	{
		Release();
	}

	void Release()
	{
		_pmtx->lock();

		bool deleteFlag = false;

		if (--(*_pcount) == 0)
		{
			if (_ptr)
			{
				//cout << "delete:" << _ptr << endl;
				//delete _ptr;

				// 删除器进行删除
				_del(_ptr);
			}

			delete _pcount;
			deleteFlag = true;
		}

		_pmtx->unlock();

		if (deleteFlag)
		{
			delete _pmtx;
		}
	}

	void AddCount()
	{
		_pmtx->lock();

		++(*_pcount);

		_pmtx->unlock();
	}

	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pmtx(sp._pmtx)
	{
		AddCount();
	}

	// sp1 = sp4
	// sp1 = sp1;
	// sp1 = sp2;
	Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			Release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp._pmtx;

			AddCount();
		}

		return *this;
	}

	T& operator*()
	{
		return *_ptr;
	}

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

	T* get() const
	{
		return _ptr;
	}

	int use_count()
	{
		return *_pcount;
	}

private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;

	// 包装器
	function<void(T*)> _del = [](T* ptr) {
		cout << "lambda delete:" << ptr << endl;
		delete ptr;
	};
};

//  定制删除器 -- 可调用对象
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "void operator()(T* ptr)" << endl;
		delete[] ptr;
	}
};

//void test_shared_deletor()
//{
//	std::shared_ptr spa1(new Date[10], DeleteArray());
//	std::shared_ptr spa2(new Date[10], [](Date* ptr){
//		cout << "lambda delete[]"<
//		delete[] ptr; 
//	});

//	std::shared_ptr spF3(fopen("Test.cpp", "r"), [](FILE* ptr){
//		cout << "lambda fclose" << ptr << endl;
//		fclose(ptr);
//	});
//}



int main() 
{
	Shared_ptr<int> p = new int(10);

	Shared_ptr<FILE> spF3(fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "lambda fclose:" << ptr << endl;
		fclose(ptr);
		});
	return 0;
}

【C++】智能指针_第14张图片

std::weak_ptr
  • 他不是常规的智能指针,不支持RAII
  • 支持像指针一样
  • 专门设计出来,辅助解决shared_ptr的循环引用问题
  • weak_ptr可以指向资源,但是他不参与管理,不增加引用计数
std::shared_ptr的循环引用问题

shared_ptr的循环引用问题在一些特定的场景下才会产生。比如定义如下的结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放。

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

现在以new的方式在堆上构建两个结点,并将这两个结点连接起来,在程序的最后以delete的方式释放这两个结点。比如:

int main()
{
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;

	node1->_next = node2;
	node2->_prev = node1;
	//...
	delete node1;
	delete node2;
	return 0;
}

上述程序是没有问题的,两个结点都能够正确释放。为了防止程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点分别交给两个shared_ptr对象进行管理,这时为了让连接节点时的赋值操作能够执行,就需要把ListNode类中的next和prev成员变量的类型也改为shared_ptr类型。比如:

struct ListNode
{
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
	node2->_prev = node1;
	//...

	return 0;
}

这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用。

当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。如下图:
【C++】智能指针_第15张图片
将这两个结点连接起来后,资源1当中的next成员与node2一同管理资源2,资源2中的prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。如下图:
【C++】智能指针_第16张图片
当出了main函数的作用域后,node1和node2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1。如下图:
【C++】智能指针_第17张图片
循环引用导致资源未被释放的原因:

  • 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
  • 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。
  • 而如果连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因。
std::weak_ptr解决循环引用问题

解决循环引用问题

weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。

  • weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。

将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。比如:

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	//...
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

weak_ptr的模拟实现

namespace cl
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		weak_ptr& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr; //管理的资源
	};
}

你可能感兴趣的:(c++,c++,java,算法)