C++进阶(8)智能指针

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

下面我们先分析一下下面这段程序有没有什么内存方面的问题?

提示一下:注意分析MergeSort函数中的问题。

cpp 复制代码
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

【问题分析】上面的问题分析出来我们发现有什么问题?

2. 内存泄漏

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

【内存泄漏的概念】内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。(内存是可重复利用的,用完不释放,那就是你不用别的进程也用不了)

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

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

cpp 复制代码
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;
}

【测试】

cout打印char*指针,会当作字符串打印,需要找'\0',过程中的随机值由于编码的原因会打印出屯屯屯、烫烫烫、......

要么使用printf,要么强转。

打印出来说明申请成功了。

但是没有释放内存,每运行1次,就泄露1GB的内存。

普通的程序内存泄露没有危害,因为程序是以进程的方式运行起来的,内存是以进程为单位分配空间的------(虚拟)进程地址。

实际要使用地时候按页为单位和实际物理内存进行映射。

进程正常结束的时候,进程的资源都会释放------程序内存泄露,但是进程正常结束,那没问题。


所以普通的内存泄漏被操作系统防范了。

但是有两种内存泄露有问题:

  1. 进程结束不了,内存一直在泄露。
  2. 进程长期运行(例如:服务器程序,不能轻易停服更新)。

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.1RAII

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

【原理·通过这个对象的生命周期来控制资源】

  • 在对象构造时获取资源 ,接着控制对资源的访问,++资源在对象的生命周期内始终保持有效。++
  • 对象生命周期结束,最后在****对象析构的时候,自动释放资源

借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
cpp 复制代码
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{
	}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}

private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	ShardPtr<int> sp1(new int);
	ShardPtr<int> sp2(new int);
	cout << div() << endl;
}
int main()
{
	try {
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

3.2 智能指针的原理

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

cpp 复制代码
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

std::auto_ptr文档

C++98版本的库中就提供了auto_ptr的智能指针。

下面演示的auto_ptr的使用及问题。


auto_ptr的实现原理:管理权转移的思想。

下面简化模拟实现了一份bit::auto_ptr来了解它的原理。

cpp 复制代码
// C++98 管理权转移 auto_ptr
namespace bit
{
	template<class T>
	class auto_ptr
	{
	public:
        //构造
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{
		}

        //支持拷贝构造
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)        //拷贝
		{
			sp._ptr = nullptr;    // 管理权转移
		}

        //赋值重载
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前管理的资源
				if (_ptr)
					delete _ptr;

				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;

                // ap对象管理的指针置空
				ap._ptr = NULL;
			}
			return *this;
		}

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

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
//int main()
//{
// std::auto_ptr<int> sp1(new int);
// std::auto_ptr<int> sp2(sp1); // 管理权转移
//
// // sp1悬空
// *sp2 = 10;
// cout << *sp2 << endl;
// cout << *sp1 << endl;
// return 0;
//}

【管理权转移】


3.4 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr。

unique_ptr文档

unique_ptr的实现原理:简单粗暴的防拷贝。

下面简化模拟实现了一份UniquePtr来了解它的原理。

cpp 复制代码
// 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

3.5.1 C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

std::shared_ptr文档

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

例如:比特老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。

【说明】

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共****享
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0 ,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0 ,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
cpp 复制代码
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
			, _pmtx(new mutex)
		{
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
			, _pmtx(sp._pmtx)
		{
			AddRef();
		}
		void Release()
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_pRefCount) == 0 && _ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pRefCount;
				flag = true;
			}
			_pmtx->unlock();
			if (flag == true)
			{
				delete _pmtx;
			}
		}
		void AddRef()
		{
			_pmtx->lock();
			++(*_pRefCount);
			_pmtx->unlock();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				Release();
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				_pmtx = sp._pmtx;
				AddRef();
			}
			return *this;
		}
		int use_count()
		{
			return *_pRefCount;
		}
		~shared_ptr()
		{
			Release();
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pRefCount;
		mutex* _pmtx;
	};

	// 简化版本的weak_ptr实现
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{
		}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{
		}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}
// shared_ptr智能指针是线程安全的吗?
// 是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的
// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
// 引用计数的线程安全问题,是智能指针要处理的
//int main()
//{
// bit::shared_ptr<int> sp1(new int);
// bit::shared_ptr<int> sp2(sp1);
// bit::shared_ptr<int> sp3(sp1);
//
// bit::shared_ptr<int> sp4(new int);
// bit::shared_ptr<int> sp5(sp4);
//
// //sp1 = sp1;
// //sp1 = sp2;
//
// //sp1 = sp4;
// //sp2 = sp4;
// //sp3 = sp4;
//
// *sp1 = 2;
// *sp2 = 3;
//
// return 0;
//}

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

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

【shared_ptr的线程安全分为两方面】

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
cpp 复制代码
// 1.演示引用计数线程安全问题,就把AddRefCount和SubRefCount中的锁去掉
// 2.演示可能不出现线程安全问题,因为线程安全问题是偶现性问题,
// main函数的n改大一些概率就变大了,就容易出现了。
// 3.下面代码我们使用SharedPtr演示,是为了方便演示引用计数的线程安全问题,
// 将代码中的SharedPtr换成shared_ptr进行测试,可以验证库的shared_ptr,发现结论是一样的。
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void SharePtrFunc(bit::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		bit::shared_ptr<Date> copy(sp);
		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n
		次,但是最终看到的结果,并一定是加了2n
		{
		unique_lock<mutex> lk(mtx);
		copy->_year++;
		copy->_month++;
		copy->_day++;
		}
	}
}
int main()
{
	bit::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 100000;
	mutex mtx;
	thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	cout << p.use_count() << endl;
	return 0;
}

std::shared_ptr的循环引用

cpp 复制代码
struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	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;
}

【循环引用分析】

  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成员,所以这就叫循环引用,谁也不会释放。
cpp 复制代码
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
struct ListNode
{
	int _data;
	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);
	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;
}

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这****个问题(ps:删除器这个问题我们了解一下)

cpp 复制代码
// 仿函数的删除器
template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
template<class T>
struct DeleteArrayFunc {
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	FreeFunc<int> freeFunc;
	std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
	DeleteArrayFunc<int> deleteArrayFunc;
	std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);


	std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });
	std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
		{fclose(p); });

	return 0;
}

【课堂演示】

java没有智能指针,因为java这些语言new出来的对象不需要手动释放,这些语言有"垃圾回收器",能自动识别一些动态申请出来的对象,然后自动释放。

但是也是有代价的,C/C++的秉持的理念就是自己管理内存,用的时候需要小心,用完记得释放。

但是有了异常之后,即使手动释放了,也可能直接跳过去造成内存泄露。

这样的场景就是在new和delete之间的代码抛异常了,这就需要中途捕获+释放资源。

若是中间有连续的抛异常(例如:多次new),那就需要很多捕获代码,这样的程序就不太简洁。

C++11就引入智能指针,核心是RAII------借助对象的生命周期去管理资源指针。

因为对象的构造和析构是自动调用的,获取到资源后就立即把指针交给对象的构造去管理。


【续·异常·智能指针的简单实现·智能指针的拷贝】

C++的编译器自动生成的拷贝构造,对内置类型完成浅拷贝,sp1和sp2的成员指针指向同一块空间

这里也不能实现成深拷贝,这里就是希望把同一块空间给sp2也去管理。
(传值返回生成临时对象)


C++98就有智能指针的概念------auto_ptr,都是按照RAII来实现的。

auto_ptr的文档介绍表明unique_ptr更好,虽然它不支持拷贝。

auto_ptr虽然支持拷贝,但会直接抢走被拷贝的对象管理的资源,让被拷贝的对象去管理空资源。

auto_ptr可以自动释放资源(通过析构函数)。

auto_ptr也支持拷贝,但拷贝完成后,sp1不再能管理它的资源。

auto_ptr拷贝时,管理权限转移,被拷贝悬空。


boost是C++的一个扩展库(不是标准库,标准库直接包含头文件就能用)。

C++中有一些语法不太成熟就纳入标准库了,比如auto_ptr,纳入C++标准库后不能轻易取消。

第三方库就没有不能轻易取消的限制,更灵活,新语法的加入更容易,于是很多语法就都加入了boost库看好不好用。C++11之前很多公司都会选择使用boost扩展库。

boost库中好用的语法就直接纳入C++标准库。




unique_ptr,不支持拷贝------delete了拷贝构造,防拷贝。

shared_ptr支持拷贝。


【unique_ptr的使用】

支持移动构造,禁用了拷贝构造。

重载operator bool,可以让自定义类型隐式类型转换成内置类型。

在后面讲解类型转换的时候会具体说明。

get获取管理的的指针。

get_delete获取删除器。

release可以在智能指针没析构之前主动释放管理的指针。


接收new[]的指针,但是unique_ptr的底层默认使用delete释放,不匹配,只能用delete[]去释放。

解决方案一就是使用定制删除器。

智能指针管理new[],需要给它传定制删除器------delete[]。

给一个可调用对象------仿函数、函数指针、lambda,或者包装了这三个的包装器。

给到D,底层会用D(代表delete)去对管理的指针进行释放。



智能指针也能用来管理fopen的文件指针。

但是智能指针管理资源,底层的释放操作默认是delete。

所以智能指针管理文件指针,需要给它传定制删除器------fclose。

【结论】

  • 凡是需要手动释放的资源,都可以交给智能指针,同时把定制删除方式交给它,它就可以帮你自动释放资源。

应对new[]的解决方案二是:unique_ptr的特化版本。

当第一个模版参数是T[]的时候,就会实例化出这个特化的版本,里面的析构就是delete[]。

up2先定义最后析构,up3中间定义中间析构,up4最后定义最先析构。


【shared_ptr的使用】

shared_ptr的模版参数不支持定制删除器。

但是可以传T[]。

如果是管理文件指针,就需要在构造函数传递定制删除器。

由于引用计数需要使用一小块内存去存储,所以如果大量使用shared_ptr是有内存碎片的效率问题、风险问题等,所以可以在构造的时候给一个内存池Alloc alloc。
(绝大多数场景不需要)


make_shared是一个可变参数模版,会返回对应的shared_ptr。

把构造shared_ptr的参数给make_shared,然后在底层new一个由这些参数构造的T类型的对象,然后把这个指针交给智能指针管理,最后返回这个智能指针。

【使用make_shared的好处】

由于底层引用计数和智能指针对象是分离的,有一个智能指针,就需要一个引用计数块,大量地使用shared_ptr就会有大量的小块内存,就有效率和内存碎片的问题。

使用make_shared会把引用计数和T类型的对象放到一起------在new对象的时候在它前面多开几个字节存储用于引用计数的计数值,这样就不会出现需要管理小块内存的问题。


【shared_ptr的基本实现】

【最简智能指针】

先来搭一下最基本的智能指针。

cpp 复制代码
namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{
		}
		
		//析构
		~shared_ptr()
		{
			if (_ptr)
				delete _ptr;
		}

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

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

	private:
		T* _ptr;
	};
}

使用测试:


【拷贝构造】

增加拷贝构造,就需要引用计数。

不能用成员变量int _count的方式来引用计数。

int _count就是每个shared_ptr对象有一个自己的计数值,这是不合理的。

sp1和sp2共同管理同一块资源,那么它们对这块资源的管理者计数值应该是同一个,而不能各自有一个计数值------完全控制不住。

也不能用静态成员变量static int _count的方式来引用计数。

初始化给1,拷贝就加加,析构就减减。

静态成员属于这个类,所有对象都能用,那sp1和sp2管理同一个资源没问题。

但是sp3和它们不管理同一个资源,那sp3也应该有自己的一个计数空间,不能和sp1、sp2共享同一个计数空间。

这里的计数count不能随着对象走,而应该随着资源走,一个资源就应该配一个计数。

  • 计数值应该开在堆上,用一个指针指向这一个计数,一个对象析构的时候,就对这个指针指向的空间进行减减运算;
  • 减到0才对管理的资源进行析构;

make_shared就是开一个资源的空间的同时多开几字节给计数值,减少内存碎片。

【默认构造】智能指针指向空,计数值值为1,析构的时候delete nullptr。


学了多线程之后,还应注意,引用计数需要加锁。


【析构函数】

测试:

sp3先定义,最先析构,引用计数减到0,直接析构,析构掉管理的空间和计数值的空间;

sp2再析构,引用计数减到1;

sp1最后析构,引用计数减到0,析构掉管理的空间和计数值的空间。


【get、use_count】
cpp 复制代码
T* get() const
{
	return _ptr;
}

int use_count() const
{
	return *_pcount;
}

【赋值重载】

sp1 = sp3的示意图:

析构函数是可以显式调用的:

但是不建议这样写,因为析构函数默认是和构造函数匹配着使用,这样会破坏这种匹配关系。

可以写一个子函数,复用一下这段逻辑:

cpp 复制代码
//子函数
void release()
{
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
		
        _ptr = nullptr;
		_pcount = nullptr;
	}
}
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    //1.先把自己管理的资源释放了,也不一定是释放,是计数值减一
	//this->~shared_ptr();
    release();

    //2.拷贝对方的资源
	_ptr = sp._ptr;
	_pcount = sp._pcount;

	//3.计数值自增【容易忽略的点】
	++(*_pcount);
}

	return *this;
}
//析构
~shared_ptr()
{
	//if (--(*_pcount) == 0)
	//{
	//	delete _ptr;
	//	delete _pcount;
    //
	//	_ptr = nullptr;
	//	_pcount = nullptr;
	//}
    release();
}

测试:

可以调试观察引用计数值的变化。

智能指针管理的资源不一都是生命周期到了再释放,赋值的过程中也有可能会释放。


还要注意一下避免自己给自己赋值。

由于sp3管理的资源只有一个引用计数,在sp3拷贝给sp3之前,sp3管理的空间就已经被释放了。

除了这种直观的自己给自己赋值,sp1 = sp2也算是自己给自己赋值,这种情况下就是效率问题了,完全没必要进行赋值操作,直接返回就可以了。

所以赋值重载要避免自己给自己赋值:

cpp 复制代码
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	//避免自己给自己赋值
	if (_ptr != sp._ptr)
	{
		//1.先把自己管理的资源释放了,也不一定是释放,是计数值减一【容易错的点------没这步会造成内存泄露】
		//this->~shared_ptr();
		release();

		//2.拷贝对方的资源
		_ptr = sp._ptr;
		_pcount = sp._pcount;

		//3.计数值自增【容易忽略的点】
		++(*_pcount);
	}

	return *this;
}

【移动构造】

不是深拷贝的类都不需要写移动构造、移动赋值。

拷贝构造比移动构造并没有多多少消耗。

这里的拷贝构造走的还是浅拷贝,只是增加了控制计数值的操作。


之前的移动构造是用于:拷贝一棵树如果用拷贝构造,就需要拷贝每一个结点,然后原树再释放每一个结点,这个代价就太大了;所以改为直接把树根交给你,就能极大地提升性能。


所以shared_ptr的传值返回没问题,只是中间生成临时对象,计数值自增,用完临时对象销毁,计数值再自减。


【shared_ptr的完善】

【定制删除器】

需要一个支持传递定制删除器的构造函数版本。

cpp 复制代码
//函数模版------D:删除器(仿函数对象、lambda表达式)
template<class D>
shared_ptr(T* ptr, D del)
	: _ptr(ptr)
	, _pcount(new int(1))
	, _del(del)
{
}

这个删除器要给到release函数使用,不能直接delete _ptr了。

由于是成员函数之间的信息传递,所以需要增加一个成员变量_del。

但是问题是这个_del是什么类型?

如果像unique-ptr一样把这个删除器给成类模版参数就很方便,但是这里就不太方便。

成员变量_del不能定义成D _del,因为D是给函数模版的,整个类用不了这个D类型。

由于D是一个可调用对象类型,所以成员变量_del可以给成包装器类型。

(注:下图少了一个分号)

function也是一个仿函数,里面重载了operator(),这个operator()会去调用可调用对象。

没有可调用对象,那就会报错。

缺省值可以给一个lambda表达式,默认使用delete来释放管理的资源。

每个成员变量的初始化都是走构造函数的初始化列表,默认构造没给删除器,初始化列表的_del就可以使用声明缺省值来初始化。


测试:


【循环引用】

shared_ptr本身还有一个缺陷------循环引用。

标准库的std::shared_ptr 的构造函数加了 explicit(显式)修饰,不能进行隐式转换。

只能调构造。

如果想让结点1指向结点2,会发现指向不了:

因为n2是智能指针对象,n1->_next是原生指针。

这时候就会考虑把_next和_prev的类型变成智能指针,以支持赋值操作。

单向指向不会出问题。但是互相指向就会出问题------再把n2的前驱链上n1。

new的两个结点没有释放,程序就结束了------内存泄露。


【示意图】

单向指向不会出问题。


双向指向就会出问题:以n2后定义先析构为例。

  • n2析构,这块空间依旧存在,_prev依旧存在。
  • 那当n1析构的时候,n1这块空间就不会被析构掉了,因为有_prev指向着。
  • 最后这两块空间内部的成员互相指向对方,空间保持着存活。
  • 但是外界拿不到这两块空间的地址了。

类似于"三角债"。

其实只需要0.000000001元就能还完A欠B的100万,因为A还B,B还C,C还A,A永远有下一个0.000000001元用于还B。重复循环 1万亿 次,就能还完。

这里就是谁也回收不了空间,都得等对方先回收空间,才能带动自己这块空间回收。


为了解决这个问题,C++标准库(其实最先是boost库)专门搞出来了一个weak_ptr。

weak_ptr使用shared_ptr进行构造的时候,不增加引用计数。

所以链表节点的指针类型可以给weak_ptr:

weak_ptr的特点就是当你用shared_ptr赋值重载、拷贝构造的时候,不会增加引用计数。

weak_ptr能达到指向shared_ptr指向的那块空间的同时,不增加引用计数。


weak_ptr不增加引用计数,不代表它没有引用计数。

STL库的weak_ptr有引用计数,也可以获取这个值:

这是为了应对:weak_ptr和shared_ptr指向同一块资源的时候,当shared_ptr的生命周期到了,而weak_ptr的周期没到,比如weak_ptr在shared_ptr的外层局部域。


expired成员函数能判断这块空间是否有效------获取引用计数值来判断。

lock成员函数能避免其他指针释放后使得weak_ptr悬空,提前lock一下能增加一个shared_ptr来管理这块空间,这个shared-ptr和weak_ptr生命周期一致,这样在weak_ptr的生命周期内,这块空间不会被释放。

weak_ptr没重载operator*、operator->,只支持管理,不支持访问,只能配合shared_ptr使用。

使用weak_ptr要小心指向的资源已经过期了,因为weak_ptr没释放的时候,它指向的资源可能已经被释放了。


可以看到使用shared_ptr,循环引用的时候没有调用Node的析构,产生了内存泄露。

必须造一个weak_ptr,这里只简单实现,不加引用计数,不考虑过期。

测试:


增加计数才能判断过期。

但是增加计数,那shared_ptr释放的时候,就要判断有没有weak_ptr。

如果有,那就只能释放_ptr成员,不能释放_pcount成员,否则weak_ptr拿到的就是一个野指针。

那这里计数值的这个空间也需要一个类似计数的方式去管理,看shared_ptr释放,有没有其他的智能指针管理着这块计数值的这个空间。

所以库里面的计数不是一个简单的指针,而是封装给一个类去管理。


【特化】

boost库里面还设计了shared_array,scoped_array,用于new[]和delete[]的场景。

C+=11没有,因为提供了特化版本。


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

  1. C++ 98 中产生了第一个智能指针auto_ptr。
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
相关推荐
無限進步D2 小时前
蓝桥杯赛前刷题
c++·算法·蓝桥杯·竞赛
小贾要学习2 小时前
【Linux】应用层自定义协议与序列化
linux·服务器·c++·json
CoderCodingNo2 小时前
【GESP】C++二级真题 luogu-B4497, [GESP202603 二级] 数数
开发语言·c++·算法
ss2732 小时前
致Java初学者的一封信
java·开发语言
We་ct2 小时前
LeetCode 50. Pow(x, n):从暴力法到快速幂的优化之路
开发语言·前端·javascript·算法·leetcode·typescript·
阿里嘎多学长2 小时前
2026-04-12 GitHub 热点项目精选
开发语言·程序员·github·代码托管
EnCi Zheng2 小时前
P2G-Python字符串方法完全指南-split、join、strip、replace的Python编程利器
开发语言·python
爱学习的小囧2 小时前
VCF 9 实验室网络部署全攻略:从硬件连接到配置实操
开发语言·网络·php
weixin_395772472 小时前
计算机网络学习笔记】初始网络之网络发展和OSI七层模型
笔记·学习·计算机网络