19 【C++11新特性】智能指针


前言

通过00【C++ 入门基础】前言得知,C++是为了解决C语言在面对大型项目的局限而诞生:

C语言面对的现实工程问题(复杂性、可维护性、可扩展性、安全性)

C语言面临的具体问题:

struct 数据公开暴露,函数数据分离,逻辑碎片化。(复杂性、安全性)
修改数据结构,如 struct 新增字段,可能导致大量相关函数需要修改。(可维护性)
添加新功能常需修改现有函数或结构体,易引入错误。(可扩展性)
资源(内存、文件句柄)需手动管理,易泄漏或重复释放。(安全性)

C++ 引入 new 和 delete,解决了C 语言动态内存分配机制对 面向对象特性(特别是构造/析构)缺乏支持以及类型不安全的问题。
但是即使如此,我们动态内存资源的申请和释放,还是需要手动,人脑没办法保证所new都有对应的delete;
且即使所有的new都有其对应的delete,在我们的上一节异常出来之后,delete语句也可能被跳过,而造成内存泄漏,所以智能指针的出现,是必要的。

智能指针主要是为了解决 ​手动管理动态内存带来的复杂性和潜在错误。


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

手动使用 new和 delete虽然灵活,但极易导致以下问题:

  1. ​内存泄漏​
    ​问题​:忘记调用 delete释放不再使用的内存。
void test() {
    int* ptr = new int(10); 
    // ...
	// 忘记调用 delete释放不再使用的内存。
}
  1. ​悬空指针
    问题​:释放内存后未将指针置空,后续误用已释放的内存。
int* ptr = new int(10);
delete ptr;
*ptr = 20; // 未定义行为!可能崩溃或数据损坏
  1. ​重复释放
    问题​:同一块内存被多次释放。
int* ptr = new int(10);
int* ptr2 = ptr;
delete ptr;
delete ptr2; // 未定义行为!重复释放
  1. ​异常栈展开​跳过
    问题​:代码在 new和 delete之间抛出异常,导致资源泄漏。正如上一节所说,虽然我们可以通过捕获不同类型的异常去判断,但是一个局部域中可能有很多个动态内存的申请和函数的调用,它们都有可能抛出异常,这样的代码出来也太丑陋了。
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.智能指针原理

智能指针是类,但是它 模拟指针的行为(和迭代器的原理一致),**采用RAII管理资源 **,所以涉及到 资源管理权限的转移、释放、共享问题,也就是智能指针类对象的拷贝问题。

2.1 RAII

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

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

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

2.2 简单模拟,理解原理

1. 我们将对资源的申请和释放,放在智能指针的构造和析构中,使用在栈上开辟的智能指针,去管理动态内存空间的申请和释放。
(栈上的智能指针对象,根据函数栈帧的申请和释放,会自动的调用其作用域中的临时智能指针对象的构造和析构函数,而构造和析构,又会去自动调用其函数体中对对应动态内存的申请和释放,这样就实现了自动对资源的控制。)

// 使用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()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

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

这样,如果我们将我们所有的内存的申请和释放,都交给RAII的智能指针管理,我们上面遇到的问题就都可以解决了:

  1. 内存泄漏:忘记调用 delete释放不再使用的内存。
  2. ​悬空指针: 释放内存后未将指针置空,后续误用已释放的内存。
  3. ​重复释放​:同一块内存被多次释放。
  4. 异常栈展开​跳过:代码在 new和 delete之间抛出异常,导致资源泄漏。
    异常是一种控制流转移机制,它的栈展开,在逻辑上是由内层向外层,在函数栈帧中查找对应的catch的异常解决语句;在 “物理上” 就是在虚拟进程地址空间的栈上,从低地址向高地址向上层层销毁函数调用链的函数栈帧的过程。
    而一个函数的栈帧在被销毁之前,又会调用其局部域中所有局部对象的析构函数,这样,就实现了利用函数的栈帧,和RAII原理,自动的对资源进行申请和释放。

2. 智能指针模仿指针的行为,指针可以解引用,也可以通过->去访问所指空间中的内容,因此:智能指针类、模板中需要将* 、->重载,让其像指针一样去使用,让我们可以通过智能指针,访问其管理的资源。

//智能指针
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<Date> sparray(new Date);
	// 需要注意的是这里应该是sparray.operator()->->_year = 2018;
	// 本来应该是sparray->->_year这里语法上为了可读性,编译器省略了一个->
	sparray->_year = 2018;
	sparray->_month = 1;
	sparray->_day = 1;
}

3. 智能指针是管理资源的方式,它又是类,所以又涉及到资源管理权限的转移、释放、共享问题,针对这个问题的方案,也是我们的C++标准模板库的来时路,我们在下文中逐渐揭晓大佬们的心路(设计)历程。


3.智能指针的使用

3.1 std::auto_ptr(管理权转移,失败设计)

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

#include <memory> // 包含auto_ptr(需旧编译器支持)
struct Date
{
	int _year;
	int _month;
	int _day;
};
int main() {
    // 创建auto_ptr并分配资源
    std::auto_ptr<Date> ptr1(new Date);
    ptr1->_year; // 正常使用

    // 所有权转移:ptr1将资源交给ptr2后自动置空
    std::auto_ptr<Date> ptr2 = ptr1;
    ptr2->_year; // 有效
    //ptr1->_year; // 危险!ptr1已是空指针

    return 0; // ptr2离开作用域,资源自动释放
}

auto_ptr,可以通过类的拷贝构造函数,将资源的管理权限转移给另一个指针的拷贝。

auto_ptr的实现原理:在模拟指针RAII管理资源的基本智能指针前提下,实现对资源的管理权转移的思想,下面简化模拟实现了一份我们自己的auto_ptr来了解它的原理:

// C++98 管理权转移 auto_ptr
namespace myPtr
{
	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._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()
{
	myPtr::auto_ptr<int> sp1(new int);
	myPtr::auto_ptr<int> sp2(sp1); // 管理权转移

	// sp1悬空
	*sp2 = 10;
	cout << *sp2 << endl;
	//cout << *sp1 << endl;  //这里会打印报错.
	return 0;
}

auto_ptr可以实现资源的转移,这样会造成一个指针的悬空,这样的话又会造成我们上面分析过的一些问题:

“2. ​悬空指针: 释放内存后未将指针置空,后续误用已释放的内存。”
“3. ​重复释放​:同一块内存被多次释放。”

我们智能指针本来就是为了解决这些问题而设计的,这样无疑是本末倒置的。所以auto_ptr没有意外的被大家诟病了。
指针本质是一个管理资源的地址,它本来就可以做管理资源的转移;我们的智能指针类是对指针的模仿,适当的模仿可以满足基本指针功能的使用前提下实现RAII,过度的模仿会适得其反,这和之前讨论的,面向对象编程对现实世界的过度描述,可能出现了副作用一样,多重验证了这句话:程序世界没有银弹。

结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。

3.2 std::unique_ptr(禁止类拷贝,禁止管理权转移、共享)

C++11中开始提供更靠谱的unique_ptr(最早是在boost中出来的)。

#include <memory> 
using namespace std;
struct Date
{
	int _year;
	int _month;
	int _day;
};
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
int main()
{
	unique_ptr<Date> Dp1(new Date);
	//unique_ptr<Date> Dp2(Dp1);        // 尝试引用已删除的函数
	//unique_ptr<Date> Dp3 = Dp1;       // 尝试引用已删除的函数
	return 0;
}

unique_ptr对于资源管理权限复制的解决方案更加暴力,直接不允许拷贝,如何让一个类不能拷贝呢?
我们知道: 对象的确定性生命周期框架——从诞生(构造)到复制传递(拷贝/移动),最终到消亡(析构),确保所有对象行为可预测、资源可管控,这是C++高效性与安全性的基石。

所以,我们只要将其复制和拷贝等函数禁止掉就可以了。

unique_ptr的实现原理:在模拟指针RAII管理资源的基本智能指针前提下,实现对资源的管理权转移的直接禁止,这样直接避免了指针悬空等问题,下面简化模拟实现了一份我们自己的unique_ptr来了解它的原理:

namespace myPtr
{
	template<class T>
	class unique_ptr
	{
	public:
		// RAII
		// 像指针一样
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{
		}
	
		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	
		T& operator*()
		{
			return *_ptr;
		}
	
		T* operator->()
		{
			return _ptr;
		}
	
		// ap3(ap1)
		// 管理权转移
		// 防拷贝
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
	private:
		T* _ptr;
	};
}
int main()
{
	myPtr::unique_ptr<Date> Dp1(new Date);
	//myPtr::unique_ptr<Date> Dp2(Dp1);        // 尝试引用已删除的函数
	//myPtr::unique_ptr<Date> Dp3 = Dp1;       // 尝试引用已删除的函数
	return 0;
}

C++98中我们实现让一个默认成员函数不被编译器自动生成、不被调用的方式:只声明,不实现,并且放到私有。
C++11的方式:用delete去声明该函数不可使用(删除)。

3.3 std::shared_ptr(管理权共享)

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

#include <memory> 
using namespace std;
int main()
{
	std::shared_ptr<int> sp1(new int);
	std::shared_ptr<int> sp2 = sp1;
	std::shared_ptr<int> sp3;
	sp3 = sp2;
	return 0;
}

shared_ptr允许管理权限的共享,允许多个智能指针管理同一个资源。
但是这样不就又会出现指针悬空或者重复释放的问题了吗?并不会,看下面原理。

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:
老师晚上在下班之前都会通知,让最后走的学生记得把门关上。

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

shared_ptr的实现原理:在模拟指针RAII管理资源的基本智能指针前提下,使用引用计数的方式,实现对资源的管理权共享,这样直接避免了指针悬空等问题,又可以实现权限共享,下面简化模拟实现了一份我们自己的shared_ptr来了解它的原理:

namespace myPtr
{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		// 像指针一样
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{
		}
	
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}
	
		T& operator*()
		{
			return *_ptr;
		}
	
		T* operator->()
		{
			return _ptr;
		}
	
		// sp3(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}
	
		// sp1 = sp5
		// sp6 = sp6
		// sp4 = sp5
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr == sp._ptr)
				return *this;
	
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
	
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
	
			return *this;
		}
	
		int use_count() const
		{
			return *_pcount;
		}
	
		T* get() const
		{
			return _ptr;
		}
	
	private:
		T* _ptr;
		int* _pcount;
	};
}

实际上shared_ptr的在库中的实现是有一个控制块结构,它就像我们上面的_pcount的思想一样,类模板实例化出不同类,就有不同的控制块结构,一个类的不同对象就共享控制块:

// 控制块的基本结构
struct ControlBlock {
    std::atomic<long> use_count;    // 引用计数(原子操作)
    std::atomic<long> weak_count;   // 弱引用计数
    void* managed_object;           // 被管理对象的指针
    Deleter deleter;                // 删除器
    Allocator allocator;            // 分配器(可选)
};

它其实就是多搞了个类,其指针作为shared_ptr成员,将一些需要实现的功能细节(删除器、强计数、弱计数、内存分配)都放在这个类里面了。

  1. 这种方式将资源的生命周期管理(智能指针管理)与资源本身解耦,让被管理的资源专注于业务功能,shared_ptr类专注于生命周期管理。
  2. 和上面_pcount的思想一样,实现实例化出的类的不同对象共享访问。

3.3.1 shared_ptr循环引用问题

#include <memory> 
#include <iostream> 
using namespace std;

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;   //1
	cout << node2.use_count() << endl;   //1
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;   //2
	cout << node2.use_count() << endl;   //2
	//最后没有调用析构函数.
	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属于节点的成员,而节点又是智能指针node1管理的资源,只有node1释放了,_next才会析构,而节点又由_prev管理,_prev属于另一个节点的成员,所以这就叫循环引用,谁也不会释放。
    在这里插入图片描述

shared_ptr,在模拟指针RAII管理资源的基本智能指针前提下,使用引用计数的方式,实现对资源的管理权共享。
但是存在循环引用的问题,这可以认为是shared_ptr的一个缺陷 — 循环引用导致的内存泄漏。
为了补这个缺陷,又搞出了weak_ptr。

3.3.2 weak_ptr(不参与shared_ptr的引用计数,解决循环引用问题)

weak_ptr不是RAII的智能指针,它是专门用来解决shared_ptr循环引用问题的。
它甚至不可以使用指针参数构造,但是它却支持使用shared_ptr去构造:
在这里插入图片描述

#include <memory> 
#include <iostream> 
using namespace std;
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;   //1
	cout << node2.use_count() << endl;   //1
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;   //1
	cout << node2.use_count() << endl;   //1
	//~ListNode()
	//~ListNode()
	return 0;
}

那么它是如何解决我们shared_ptr循环引用的问题的呢?
很简单,它不会增加我们shared_ptr的引用计数,它不是RAII智能指针,它不参与资源的管理,只是可以访问资源。

weak_ptr做成员分析:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的weak_ptr _next指向node2,node2的weak_ptr _prev指向node1,它们不参与引用计数,所以就算指向对方,shared_ptr中的引用计数还是1。
  3. 当我们函数结束,栈帧被销毁,我们的两个智能指针node1和node2相继销毁,并调用其析构函数,其中判断它们其中的shared_ptr的引用计数变为0,节点正常销毁,节点的成员_next、_prev也就正常销毁。

weak_ptr的实现原理:它并不是管理资源的RAII智能指针,它只是常量模拟指针,使得我们可以通过它正常访问其指向的资源,weak_ptr只能使用weak_ptr或者shared_ptr初始化,但是它并不参与shared_ptr的引用计数,下面简化模拟实现了一份我们自己的weak_ptr来了解它的原理:

namespace myPtr
{
	//简化版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;
	};
}

3.3.3 shared_ptr的线程安全问题

shared_ptr的线程安全分为两方面:

  1. shared_ptr智能指针中引用计数,可能同时被多个线程操作,会有线程安全问题。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会有线程安全问题。

1. shared_ptr智能指针中引用计数是否是线程安全的。
是的,引用计数的加减是加锁保护的,这由标准库自己维护,我们无需在意。

2. shared_ptr智能指针管理的资源是否是线程安全的。
引用计数的线程安全问题,是智能指针要处理的,而指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了。

通过下面的程序我们来测试shared_ptr管理的资源的线程安全问题:

#include <memory> 
#include <iostream> 
#include <mutex> 
using namespace std;
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void SharePtrFunc(std::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		std::shared_ptr<Date> copy(sp);
		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
		{
			//unique_lock<mutex> lk(mtx);    //当注释掉这行,我们智能指针管理的资源没有枷锁,将不具备原子性,最终的值有随机性:(_year输出: 198533  _month输出: 199769  _day输出: 199759)
			copy->_year++;
			copy->_month++;
			copy->_day++;
		}
	}
}
int main()
{
	std::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;        //get是shared_ptr中的一个成员函数,作用是取到智能指针管理资源的地址。 输出:005646A0
	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;       //输出:200000
	cout << p->_month << endl;      //输出:200000
	cout << p->_day << endl;        //输出:200000
	cout << p.use_count() << endl;  //输出:1

	return 0;
}

可见我们上面自己实现的shared_ptr还是有问题,因为我们并没有考虑多线程情况的线程安全问题。
考虑线程安全实现一下:

namespace myPtr
{
	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;
	};
}

4.删除器

这里还有一个问题,我们之前的代码(shared_ptr、unique_ptr),在其智能指针的析构函数中都是写死的直接delete对于的资源,那如果我们是new或者malloc出来一个数组给智能指针去管理呢?

struct Date
{
	Date()
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
int main()
{ 
	std::shared_ptr<Date> sp1(new Date[10]);
	//输出:
	//Date()
	//Date()
	//Date()
	//Date()
	//Date()
	//Date()
	//Date()
	//Date()
	//Date()
	//Date()
	//~Date()
	return 0;
}

new会自动调用类的构造,然后返回一个指针,被我们的智能指针管理起来,但是智能指针之中,析构函数中对管理的资源的释放,是写死的delete,所以它只会释放new出空间的头部的Date对象,其它的对象就出现了内存泄漏了。

库里面的解决办法就是:定制删除器

我们要控制一个模版类内释放资源的规则。
我们想在外部控制内部的比较/规则,那就很容易想到我们的老朋友:仿函数。
没错我们的定制删除器就是写一个仿函数传入shared_ptr,等到我们资源要释放的时候,shared_ptr智能指针会自动的调用这个仿函数并将管理的资源的指针传入。

其实不止仿函数,只要是可调用对象都可以作为智能指针的删除器(lambda、仿函数、函数指针)。

我们来看看shared_ptr构造函数的一个函数重载:

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

我们来试试自己定制删除器然后传入库中的shared_ptr :

template<class T>
class DeleteType
{
public:
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};
template<class T>
void deleteFUNC(T* ptr)
{
	delete[] ptr;
}
int main()
{
	shared_ptr<Date> Dp1(new Date[10],DeleteType<Date>());
	//shared_ptr<Date> Dp2((Date*)malloc(sizeof(Date)),[](Date* ptr){free ptr;});
	//shared_ptr<Date> Dp3(new Date[10],deleteFUNC<Date>);
	return 0;
}
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()

这样就可以正常的删除我们的资源了。

智能指针管理的资源当然也不一定是动态内存,又可能是一个打开的文件或者是其他的资源,所以定制删除器是不可或缺的:

#define _CRT_SECURE_NO_WARNINGS
int main()
{
	shared_ptr<FILE> Fp(fopen("test.txt", "r"), [](FILE* ptr) {fclose(ptr); });
	cout << Fp.get() << endl;
	char buffer[256];
	while (fgets(buffer, sizeof(buffer), Fp.get()) != nullptr) {
		std::cout << buffer;
	}
	return 0;
}
//输出:
//011EAC58
//你好

4.1 分析删除器

在这里插入图片描述
C++文档中对shared_ptr的构造函数的介绍可见,我们传入删除器类型,是传给其构造函数的,即class D类型参数,它是类的构造函数的参数模板,而不是整个类的参数模板。

那么既然如此,也是只有构造函数可以使用这个类型参数,我们的整个类应该如何使用传入的模板呢?

传入的是一个可调用对象,所以我们就可以用包装器function<void*(U*)> del;去存它,然后整个类就可以获取其类型,在最后智能指针销毁析构的时候调用这个删除器可调用对象,并执行定制化的删除操作。
库中:实际shared_ptr是有一个控制块的,删除器的包装器实际存储在这个控制块中。

这里可以用包装器存的原因之一就是:
我们可以确定我们传入的仿函数的返回值一定为void,还能确定它的参数一定是U*的。

也正是因为我们传入删除器的底层原理是通过包装器去存的,所以其实所有的可调用类型都可以用作删除器。

注意:
我们的unique_ptr也有定制删除器,但是它通常是在类模版处给的:
在这里插入图片描述

这是因为​unique_ptr和 shared_ptr的不同设计目标所决定的:

  1. unique_ptr追求极致的运行时效率和无额外开销。​​
  • 零运行时开销(EBO - Empty Base Optimization)​​:这是最关键的原因。当删除器是一个无状态的函数对象​(例如 std::default_delete,它没有成员变量)时,通过将其作为模板参数,编译器可以利用空基类优化(EBO)​。这意味着这个删除器不会占用 unique_ptr对象的任何额外内存。unique_ptr的大小通常就等于一个原始指针的大小。

    // 自定义的无状态删除器
    struct MyDeleter {
        void operator()(MyClass* ptr) {
            std::cout << "Deleting with MyDeleter\n";
            delete ptr;
        }
    };
    
    std::unique_ptr<MyClass, MyDeleter> p1(new MyClass);
    std::cout << "Size of unique_ptr with custom deleter: " << sizeof(p1) << std::endl; // 大概率输出 8 (64位系统下指针大小)
    
    // 对比:有状态的删除器(例如lambda捕获了变量)
    int some_capture = 42;
    auto lambda_deleter = [&](MyClass* ptr) { delete ptr; std::cout << some_capture; };
    // 这样的删除器无法作为模板参数直接使用,除非包装成 std::function,但那会有开销。
    
  • 类型安全与内联优化​:由于删除器的类型在编译期就确定了,编译器可以毫无困难地进行内联优化。调用删除器的代码就像直接调用一个普通函数一样快,没有任何间接调用(如通过函数指针)的开销。

  • ​代价:类型刚性​:这是这种设计的主要代价。​删除器类型是 unique_ptr类型的一部分。这意味着两个拥有不同删除器类型的 unique_ptr是两种完全不同的类型,它们不能互相赋值,也不能放在同一个 vector<std::unique_ptr>中。

  1. ​shared_ptr追求更大的灵活性和动态分配能力。
  • 类型擦除与灵活性​:shared_ptr的设计目标之一是灵活性。无论你传入什么类型的删除器(函数指针、lambda表达式、函数对象等),这些类型信息都不会成为 shared_ptr类型的一部分。所有的 shared_ptr都是同一种类型,可以放在同一个容器里,互相赋值。
    这是通过类型擦除技术实现的。shared_ptr在内部(通常是在控制块中)存储一个指向删除器的基础类指针,而具体的删除器信息被存储在一个派生类对象中。调用时通过虚函数机制来分派到正确的删除器。

  • 动态分配开销​:因为删除器是运行时绑定的,并且需要与引用计数等一起存储在控制块中(这个控制块通常是动态分配的),所以它必然带来一些额外开销:

    • 内存开销​:shared_ptr的大小通常是两个指针(一个指向对象,一个指向控制块)。
    • 运行时开销​:调用删除器需要一次间接调用(通过指针),可能无法像 unique_ptr那样容易被内联。
  • 一致的生命周期管理​:删除器与引用计数一起存储在控制块中,这意味着删除器的生命周期与它所管理的对象完全绑定。当引用计数降为0时,控制块会正确地调用存储的删除器来销毁对象,然后销毁自身(包括删除器)。这保证了安全性。


总结

三种智能指针都是模拟指针行为,采用RAII管理资源:

  1. auto_ptr:管理权转移(拷贝后原指针悬空,有安全隐患)
  2. unique_ptr:防拷贝(简单粗暴,适用于独占资源)
  3. shared_ptr:引用计数(支持拷贝共享资源,需注意循环引用问题)
  4. weak_ptr:配合shared_ptr解决循环引用问题

智能指针提高了内存管理的安全性,但仍需根据场景选择合适的类型。实际应用中,shared_ptr使用较多,但要注意线程安全和循环引用问题。


本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点 😃。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值