【C++】智能指针&&RALL&&实现shared_ptr

个人主页zxctscl
专栏 【C++】【C语言】【Linux】【数据结构】【算法】
如有转载请先通知

上次分享到 【C++】C++中异常及异常的使用,有了异常,就很难控制,那么该怎么办呢?这时候就来看看智能指针了。

1. 为什么需要智能指针?

在异常中提到的下面这段代码,可能会出现p1申请空间时候出现异常,p2申请空间也可能会出现异常,所以p1释放时候要捕获,Division时候也可能会抛异常,就得把p1、p2释放掉。

示例代码:

double Division(int a, int b)
{
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
	int* p1 = new int[10];
	int* p2 = nullptr;
	try
	{
		p2 = new int[20];
		try
		{
			int len, time;
			cin >> len >> time;
			cout << Division(len, time) << endl; // throw
		}
		catch (...)
		{
			delete[] p1;
			cout << "delete:" << p1 << endl;

			delete[] p2;
			cout << "delete:" << p2 << endl;

			throw;  // 捕获什么抛出什么
		}
	}
	catch (...)
	{
		delete[] p1;
		cout << "delete:" << p1 << endl;

		throw;
	}

	delete[] p1;
	cout << "delete:" << p1 << endl;

	delete[] p2;
	cout << "delete:" << p2 << endl;
}

C++就借助构造函数和析构函数来帮助解决上面问题。
构造一个SmartPtr类把指针保存起来,再析构的时候就对这个指针进行释放。

class SmartPtr
{
public:
	SmartPtr(int * ptr)
		:_ptr(ptr)
	{}

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

private:
	int* _ptr;
};

只要生命周期结束就可以调用析构函数释放。


int main()
{
	SmartPtr sp1(new int[10]);
	retuen 0;
}

此时它就能释放,构造函数保存资源,析构函数释放资源:
在这里插入图片描述

C++11中确定不抛异常就加上noexcept
在这里插入图片描述

2. 内存泄漏

2.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	// 2.异常安全问题
	int* p3 = new int[10];
	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	delete[] p3;
}

2.2 内存泄漏分类(了解)

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

2.3 如何检测内存泄漏(了解)

在linux下内存泄漏检测: linux下几款内存泄漏检测工具
在windows下使用第三方工具: VLD工具说明
其他工具: 内存泄漏工具比较

2.4 如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
    总结一下:
    内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具

3. 智能指针的使用及原理

3.1 RAII

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

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效**,最后在对象析构的时候释放资源**。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
(1)不需要显式地释放资源。
(2)采用这种方式,对象所需的资源在其生命期内始终保持有效。

上面示例代码p1申请空间时候出现异常,p2申请空间也可能会出现异常,有了SmartPtr,直接不定义p1,直接定义一个SmartPtr sp1,同样再定义一个SmartPtr sp2

void Func()
{
	SmartPtr sp1(new int[10]);
	SmartPtr sp2(new int[20]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

对比之下,左边嵌套捕获、释放,而右边就没有:
在这里插入图片描述

测试一下代码:

double Division(int a, int b)
{
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

class SmartPtr
{
public:
	SmartPtr(int* ptr)
		:_ptr(ptr)
	{}

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

private:
	int* _ptr;
};

void Func()
{
	SmartPtr sp1(new int[10]);
	SmartPtr sp2(new int[20]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}




int main()
{
	/*SmartPtr sp1(new int[10]);*/

	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}

	return 0;
}

会正常释放,就不用担心忘记释放而导致抛异常了:
在这里插入图片描述

目前SmartPtr只能管理整型,那么将它改为模板,它有时候会用到它原生指针,还能重载operator*、重载operator->operator[]

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

	~SmartPtr()
	{
		delete[] _ptr;
		cout << "delete:" << _ptr << endl;
	}
		T* get()
	{
		return _ptr;
	}

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

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}

private:
	T* _ptr;
};

void Func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

3.2 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

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;
};
int main()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 10
		cout << *sp1 << endl;
	SmartPtr<int> sparray(new Date);
	// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
	// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
	sparray->_year = 2018;
	sparray->_month = 1;
	sparray->_day = 1;
}

总结一下智能指针的原理:

  1. RAII特性
  2. 重载operator*和opertaor->,具有像指针一样的行为。

3.3 std::auto_ptr

在示例代码中:

double Division(int a, int b)
{
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

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

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

	T* get()
	{
		return _ptr;
	}

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

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}
private:
	T* _ptr;
};

void Func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}


int main()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(sp1);

	int* p1 = new int[10];
	int* p2 = p1;

	return 0;
}

sp2去拷贝构造sp1,会导致析构两次,也就是浅拷贝的问题:
在这里插入图片描述
模拟指针的行为,是不能深拷贝的,期望的是浅拷贝,想要sp1和sp2一起管理资源。

拷贝问题怎么解决呢?
早期解决用了std::auto_ptr
std::auto_ptr文档

C++98版本的库中就提供了auto_ptr的智能指针。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理:

double Division(int a, int b)
{
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

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

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

	T* get()
	{
		return _ptr;
	}

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

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}
private:
	T* _ptr;
};

void Func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}


int main()
{
	auto_ptr<int> sp1(new int(1));
	// C++98 转移管理权,sp1对象悬空
	auto_ptr<int> sp2(sp1);

	

	return 0;
}

拷贝构造的时候就转移资源,
在这里插入图片描述
C++98 转移管理权,sp1对象悬空,如果在不知道这个特性时候使用,就会崩溃:
在这里插入图片描述
这个内部最主要就是:

auto_ptr(auto_ptr<T>& sp)
	:_ptr(sp._ptr)
{
	// 管理权转移
	sp._ptr = nullptr;
}

Boost库里面
在这里插入图片描述
C++11拿其中scoped_ptr和shared_ptr和weak_ptr,并将scoped_ptr改名为unique_ptr。

在memory中
在这里插入图片描述

管理权限转移,被拷贝对象悬空,有风险

3.4 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr
unique_ptr文档
在这里插入图片描述
unique_ptr不支持拷贝构造:
在这里插入图片描述

在这里插入图片描述

class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	unique_ptr<A> sp1(new A);
	unique_ptr<A> sp2(sp1);
	
	return 0;
}

它不支持拷贝构造:
在这里插入图片描述

可以获取到它原生指针:
在这里插入图片描述
还可以做条件判断,如果成员变量是公有的就能用operator->:
在这里插入图片描述
多个对象就能用operator[]:
在这里插入图片描述

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理。

// C++11库才更新智能指针实现
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace bit
{
	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;
		}
		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
	private:
		T* _ptr;
	};
}
//int main()
//{
// /*bit::unique_ptr<int> sp1(new int);
// bit::unique_ptr<int> sp2(sp1);*/
//
// std::unique_ptr<int> sp1(new int);
// //std::unique_ptr<int> sp2(sp1);
//
// return 0;
//}
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源

3.5 std::shared_ptr

shared_ptr支持拷贝构造
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
std::shared_ptr文档
在这里插入图片描述
在这里插入图片描述
不支持隐式类型转换:
在这里插入图片描述

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	shared_ptr<A> sp1(new A);
	shared_ptr<A> sp2(sp1);

	/*shared_ptr<A> sp3 = make_shared<A>(1, 1);*/

	cout << sp1.use_count() << endl;
	{
		shared_ptr<A> sp3(sp1);
		cout << sp1.use_count() << endl;
	}

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


	return 0;
}

用了引用计数;
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
make_shared构造一个智能指针
在这里插入图片描述

make_shared构造一个智能指针来管理A,来返回一个对象:
在这里插入图片描述

3.5.1 实现shared_ptr

3.5.1.1 构造+拷贝构造+析构

要想实现shared_ptr最主要的就是实现引用计数。
如果用一个成员变量,是不行的,因为当p1和p2管理相同的资源,每个对象里面都有一个引用计数,就不合适。
如果用静态或者全局成员变量,也不可行,当sp1和sp2管理同一个资源时候没有什么问题;但是当再有一个sp3的时候,也需要一个计数。所以不能单只有一个计数,而静态成员是属于所有对象的,就不能再记录sp3的。
在这里插入图片描述

我们想要的是一个资源就伴随一个计数。可以开多个计数,管理同一个资源的技术就能用它们的指针指向这个计数,就能找到这个计数,就不用存到每一个对象里面。
在这里插入图片描述
所以这里计数成员变量用指针来实现:

	template<class T>
	class Share_ptr
	{
	public:

	private:
		T* _ptr;
		int* _pcount;
	};

在这里插入图片描述
shared_ptr构造时就给计数开一个空间,用来计数。
当sp2和sp1共用一个空间时候,计数的指针指向的资源就加加

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

		//sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._pcount)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}

	private:
		T* _ptr;
		int* _pcount;
	};


#include"Share_ptr.h"

int main()
{
	bit::shared_ptr<A> sp1(new A(1,1));
	bit::shared_ptr<A> sp2(sp1);

	bit::shared_ptr <A> sp3(new A(2,2));

	return 0;
}

一个资源有一个计数,需要共管这个资源的对象就加加这个计数就行:
在这里插入图片描述

析构:把计数置为0,再释放资源:

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				//最后一个管理的对象释放资源
				delete _ptr;
			}
		}

use_count()来获取*_pcount:

		int use_count()
		{
			return *_pcount;
		}

就能看到获得资源数量:
在这里插入图片描述
在这里插入图片描述

3.5.1.2 赋值

加入将sp3赋值给sp1,不能直接将sp1释放,是希望sp1和sp3共同管理资源,sp1的资源是和sp2共用的。
在这里插入图片描述
在赋值里面就相等于调析构,但是不能真的去调用析构,就可以重新写一个release()减减计数,如果减到0就释放:

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//最后一个管理的对象释放资源
				delete _ptr;
			}
		}
		
		~shared_ptr()
		{
			release();
		}

如果只有sp1就释放;如果sp1的资源还有其他共用,就减减计数,再让sp3共管同一块资源,再加加计数:

		//sp1=sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			this->release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);

			return *this;
		}

测试一下:
此时sp1计数就变成了4
在这里插入图片描述
就是这样的关系图:
在这里插入图片描述

在这里插入图片描述
如果自己给自己赋值,就会出现值丢失的情况,因为sp6只有一个对象管理,一赋值,就先调用release(),就直接释放了:
在这里插入图片描述
解决方法,先判断是不是自己给自己赋值

		//sp1=sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				this->release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}

			return *this;
		}

3.5.1.3 shared_ptr类代码

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}

		//sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}


		void release()
		{
			if (--(*_pcount) == 0)
			{
				//最后一个管理的对象释放资源
				delete _ptr;
			}
		}

		//sp1=sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				this->release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}

			return *this;
		}

		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

3.5.2 make_shared

make_shared可以减少内存碎片,它不仅仅会构造对象还会构造计数。
在这里插入图片描述

3.5.3 线程安全

3.5.3.2 std::shared_ptr的线程安全问题

std::shared_ptr的线程安全问题
通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

创建一个链表,再创建两个线程,想要在链表里面插入东西。
先用func函数来实现链表插入,再调用来创建线程:

void func(bit::shared_ptr<list<int>> sp, int n)
{
	for (int i = 0; i < n; i++)
	{
		sp->emplace_back(i);
	}
}

int main()
{
	bit::shared_ptr<list<int>> sp1(new list<int>);

	thread t1(func, sp1, 1000000);
	thread t2(func, sp1, 2000000);

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

	cout << sp1->size() << endl;

	return 0;
}

有时候程序会崩溃:
在这里插入图片描述
此时需要给程序加锁:
定义一个全局锁mutex mtx;

mutex mtx;

// 智能指针对象本身拷贝析构是线程安全的
// 底层引用计数加减是线程安全的
// 指向的资源访问不是线程安全的
void func(bit::shared_ptr<list<int>> sp, int n)
{
	for (int i = 0; i < n; i++)
	{
		mtx.lock();
		sp->emplace_back(i);
		mtx.unlock();
	}
}

int main()
{
	bit::shared_ptr<list<int>> sp1(new list<int>);
	

	thread t1(func, sp1, 1000000);
	thread t2(func, sp1, 2000000);

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

	cout << sp1->size() << endl;

	return 0;
}

在这里插入图片描述

但如果在func里面每次拷贝一下:

void func(bit::shared_ptr<list<int>> sp, int n)
{

	for (int i = 0; i < n; i++)
	{
		bit::shared_ptr<list<int>> copy1(sp);
		bit::shared_ptr<list<int>> copy2(sp);
		bit::shared_ptr<list<int>> copy3(sp);

		mtx.lock();
		sp->emplace_back(i);
		mtx.unlock();
	}
}

时不时就会程序崩溃:
在这里插入图片描述
此时保护了emplace_back,两个线程不能同时往list里面去插入数据。但是func里面的拷贝不是线程安全,就说明拷贝构造不是线程安全。
为什么拷贝构造不是线程安全呢?
是因为引用计数不是线程安全。

当在list里面插入数据
在这里插入图片描述
测试结果显示,计数不同。
在这里插入图片描述
线程1有自己的copy1、copy2、copy3,线程2也有自己的copy1、copy2、copy3。

对比之下库里面的就没有问题:
在这里插入图片描述

在这里插入图片描述

3.5.3.2 线程安全下shared_ptr类实现

此时问题出现在:引用计数不是线程安全,t1和t2都去调用自己的copy1、copy2、copy3。两个线程都在不断的加加减减这个计数,这个计数就不是安全的
在这里插入图片描述
那么怎么修改呢?
把_pcount类型改为atomic<int>*,其他地方就不用改:

#pragma once
#include<atomic>

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new atomic<int>(1))
		{}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		// sp1 = sp3;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				this->release();

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

				++(*_pcount);
			}

			return *this;
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				// 最后一个管理的对象,释放资源
				delete _ptr;
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		atomic<int>* _pcount;
	};
}

3.5.4 循环引用

3.5.4.1 什么是循环引用

实现链表,在两个节点p1和p2互相指向的时候会出现循环引用

struct Node
{
	std::shared_ptr<Node> _next;
	std::shared_ptr<Node> _prev;

	int _val;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};


int main()
{
	std::shared_ptr<Node> p1(new Node);
	std::shared_ptr<Node> p2(new Node);

	p1->_next = p2;
	p2->_prev = p1;


	return 0;
}

在这里插入图片描述

这时候就会出现内存泄漏,没有调用析构函数,那两个节点没有释放。
在这里插入图片描述
为什么没有释放呢?
在管理节点时候会有计数,如果没有p1和p2互相指向的时候,是正常的。
在这里插入图片描述
如果p1和p2互相指向的时候,此时计数是2:
在这里插入图片描述
释放时候,如果左边节点什么时候释放由右边节点_prev管理着;右边节点什么时候释放由左边节点_next管理着,释放逻辑就是一个死循环。
在这里插入图片描述

循环引用分析:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
    在这里插入图片描述
3.5.4.2 weak_ptr

官方解决循环引用的方法提供了weak_ptr
在这里插入图片描述

weak_ptr不支持RAII,不单独管理资源,它专门用来辅助解决shared_ptr的循环引用
它的构造支持无参构造,
在这里插入图片描述

将上面代码std::shared_ptr改成std::weak_ptr

struct Node
{
	/*std::shared_ptr<Node> _next;
	std::shared_ptr<Node> _prev;*/

	std::weak_ptr<Node> _next;
	std::weak_ptr<Node> _prev;

	int _val;

	~Node()
	{
		cout << "~Node()" << endl;
	}
};


int main()
{
	std::shared_ptr<Node> p1(new Node);
	std::shared_ptr<Node> p2(new Node);

	cout << p1.use_count() << endl;
    cout << p2.use_count() << endl;

	p1->_next = p2;
	p2->_prev = p1; 

	return 0;
}

此时测试的代码就没有问题,关键就是weak_ptr中引用计数没有增加,引用计数还是1,不直接参与资源的释放
在这里插入图片描述

weak_ptr本质赋值或者拷贝时,只指向资源,但是不增加shared_ptr的引用计数。

它也能访问到引用计数:
在这里插入图片描述
在这里插入图片描述

3.5.5 定制删除器(了解)

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题(ps:删除器这个问题我们了解一下)
在这里插入图片描述
定制删除器
如果传了删除器,就用删除器去删,没有就用delete去删除
在这里插入图片描述
设计一个仿函数

template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};

代码测试:

class A
{
public:
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{}

	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a1 = 1;
	int _a2 = 1;
};

template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};

 //定制删除器
int main()
{
	//std::shared_ptr<A[]> sp1(new A[10]);
	bit::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
	bit::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());
	bit::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });

	bit::shared_ptr<A> sp4(new A);

	return 0;
}

传一个 FreeFunc 匿名对象,会回掉删除器,来进行对应的释放:
在这里插入图片描述
那么定制删除器如何在shared_ptr类中传导呢?
构造函数中传导之后,析构怎么办呢?此时构造函数中加了一个_del的成员变量,去接受传导过来的删除器对象,再用_del进行释放。

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

加一个包装器functional,在模板里面指定可调用包装的可调用对象和返回值类型,同时为了解决单个对象不支持删除器,就在定义成员变量时候给个缺省值:

function<void(T*)> _del = [](T* ptr) {delete ptr;

在这里插入图片描述

3.5.6 定制删除器下实现shared_ptr的代码

shared_ptr.h文件中

#pragma once
#include<atomic>
#include<functional>

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new atomic<int>(1))
		{}

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

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		// sp1 = sp3;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				this->release();

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

				++(*_pcount);
			}

			return *this;
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				// 最后一个管理的对象,释放资源
				//delete _ptr;
				_del(_ptr);

				delete _pcount;
			}
		}

		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		atomic<int>* _pcount;

		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};
}

4. C++11和boost中智能指针的关系

在这里插入图片描述

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptrshared_ptrweak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptrshared_ptrweak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

有问题请指出,大家一起进步!!!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zxctscl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值