C++ 智能指针

问题引入

cpp 复制代码
int func1(int x)
{
	int y = 10;
	int* tmp = (int*)malloc(sizeof(int) * 2);
	if (x == 0)
		throw "func1_error";
	else
		return x + y;
	free(tmp);//抛异常造成异常安全问题,无法释放造成内存泄漏,
}

int main()
{
	try { int a=func1(0); }
	catch (const char* error)
	{
		cout << error << endl;
	}
	return 0;
}

内存泄漏:

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

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

智能指针的使用及原理

RAII(面试官喜欢问)

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

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

这种做法有两大好处:

不需要显式地释放资源。

采用这种方式,对象所需的资源在其生命期内始终保持有效。

核心:通过对象的生命周期管理资源的释放

发展历史

weak_ptr不增加share_ptr的引用计数,但是可以观察这个引用计数,不参与他的修改

使用RAII思想设计的Smart_sptr类

cpp 复制代码
template<class T>
class Smart_sptr
{
public:
	Smart_sptr(T* sptr=nullptr)
		:_ptr(sptr) 
	{}

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

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

private:
	T* _ptr;
};

struct Date
{
	int _year;
	int _month;
	int _day;
};


int main()
{
	//实现指针操作
	Smart_sptr<int> sp1(new int);
	*sp1 = 10;
	cout << *sp1 << endl;


	//管理资源
	Smart_sptr<Date> sp2(new Date);
	// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
	// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
	sp2->_year = 2018;
	sp2->_month = 1;
	sp2->_day = 1;
	cout << sp2->_year << " " << sp2->_month << " " << sp2->_day << endl;
	
	//测试生命周期
	cout << (*(Smart_sptr<int>(new int)) = 11) << endl;
	//执行完后立马析构,资源释放
	return 0;
}

将新创建的new int对象交给sp1,将new Date对象交给视2

通过类对象去控制new出来的对象属性,

cpp 复制代码
	int* tmp = new int;
	sp1 =tmp

难以理解可以直接将sp1是tmp的一个别名使用,依旧还是指针形式,存的是一块地址

结构体,对象通过 . 访问

结构体对象取地址 通过**->**访问:本质是sp2.operator->()->_day

std::auto_ptr

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

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

cpp 复制代码
#include <memory>
class Date
{
public:
	Date() { cout << "Date()" << endl; }
	~Date() { cout << "~Date()" << endl; }
	int _year;
	int _month;
	int _day;
};
int main()
{
	auto_ptr<Date> ap(new Date);
	auto_ptr<Date> copy(ap);
	// auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了copy()括号内的内容悬空
	// C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
	ap->_year = 2018;
	return 0;
}

std::unique_ptr

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

如何防拷贝参考:防拷贝文章

cpp 复制代码
int main()
{
	unique_ptr<Date> up(new Date);

	// unique_ptr的设计思路非常的粗暴-防拷贝,也就是不让拷贝和赋值。
	unique_ptr<Date> copy(ap);
	return 0;
}

std::shared_ptr

简单样例

cpp 复制代码
struct Date
{
	int _year;
	int _month;
	int _day;
};

int main()
{
	// shared_ptr通过引用计数支持智能指针对象的拷贝
	shared_ptr<Date> sp(new Date);
	//use_count()返回引用计数个数
	cout << "第一个对象sp管理new Date的引用计数:" << sp.use_count() << endl;

	shared_ptr<Date> copy(sp);
	cout << "新增一个对象后sp的引用计数:" << sp.use_count() << endl;
	cout << "第二个对象copy管理new Date的引用计数:" << copy.use_count() << endl;
	return 0;
}

图解

新申请的资源new Date其内部含有一个数据(引用计数)专门记录有多少对象能访问该资源

当sp对象管理new Date时,sp会让new Date的引用计数增加1

当copy对象管理new Date时,copy会让new Date的引用计数增加1

shared_ptr的原理:

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

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。

  2. 在对象被销毁时(也就是析构函数调用),说明自己不使用该资源了,对象的引用计数减1。

  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;

  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

模拟实现Shared_Ptr

初级版本

cpp 复制代码
#include<string>
template<class T>
class Shared_Ptr
{
public:
	//构造
	Shared_Ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))//数组,开始初始化赋值为1
	{}

	//析构函数
	~Shared_Ptr()
	{
		//初始版本
		//if (_pcount == 0)
		//{
		//	delete _pcount;
		//	delete _ptr;
		//}
		//else
		//{
		//	--_pcount;
		//}

		//保证赋值重载能调用析构
		release();
	}

	//拷贝构造
	Shared_Ptr(const Shared_Ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		++(*_pcount);//拷贝后多一个对象管理,引用计数+1
	}


	//实现智能指针
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }

	//获得引用计数个数
	int use_count() { return *_pcount; }

	//返回类中原式指针
	T* get() { return _ptr; }

	void release()
	{
		//当引用计数为1时,不再让该指针管理资源,应该立马释放
		
		//如果是下面这样,当引用计数为1时,只让引用计数-1并没有释放
		//正常情况下引用计数为1时在调用release时就应该delete了
		//if (*_pcount != 0)或者改成!=1
		//{
		//	--(*_pcount);
		//}
		//else
		//{
		//	delete _pcount;
		//	delete _ptr;
		//}
		if (--(*_pcount) == 0)
		{
			delete _pcount;
			delete _ptr;
		}
	}

	//赋值重载--遇见想要多个对象管理时sp1=sp2
	Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp) 
	{
		//两种情况--1.自己赋值,2.两个不同的赋值
		if (sp._ptr!= _ptr)//不同
		{
			//释放sp1所管理的内容or减去sp1所管理内容的引用计数
			//从引用计数上保证sp1不在管理该块内容
			//调用析构,多套一层release函数
			release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		//else
		//{
		//	return *this;
		//	//相同返回对象本身
		//}
		全部完成后返回对象本身
		//return *this;
		//上述两个return可以合并成一个,在执行完不同对象的赋值后需要返回*this
		//相同时不进入if也返回*this
		return *this;
	}

private:
	int* _pcount;//引用计数个数
	T* _ptr;//管理指针
};

int main()
{
	Shared_Ptr<string> sp1(new string("test_smart_pointer"));
	Shared_Ptr<string> sp2(sp1);//测试拷贝
	cout << "sp1:" << sp1.get() << " _pcount " << sp1.use_count() << endl;//use_count()显示总引用计数
	cout << "sp2:" << sp2.get() << " _pcount " << sp2.use_count() << endl;
	cout << endl;
	Shared_Ptr<string> sp3(new string("test_assignment"));
	sp3 = sp3;//相同赋值
	cout << "sp3:" << sp3.get() << " _pcount " << sp3.use_count() << endl;
	sp1 = sp3;//不同赋值
	cout << "sp1:" << sp1.get() << " _pcount " << sp1.use_count() << endl;
	cout << "sp2:" << sp2.get() << " _pcount " << sp2.use_count() << endl;
	cout << "sp3:" << sp3.get() << " _pcount " << sp3.use_count() << endl;

	return 0;
}

结果:

开始时new string("test_smart_pointer")的地址为00B6F410,由智能指针sp1管理,又经过拷贝后sp2也能管理00B6F410,此时有两个智能指针管理该00B6F410地址所存资源,所以每一个智能指针的引用计数均为2

new string("test_assignment")创造的资源地址为:00B6F1D0,由智能指针sp3管理,引用计数为1,并且自我赋值后引用计数依旧为1。

因为是sp3所管理的资源赋值给sp1,所以sp1不在管理00B6F410而是管理00B6F1D0,并且此时sp1和sp3均管理00B6F1D0所以引用计数为2

此时管理00B6F410的只有sp2,引用计数为1
再加入测试代码检测sp2无管理内容时,引用计数个数

cpp 复制代码
	sp2.release();
	cout << "sp2:" << sp2.get() << " _pcount " << sp2.use_count()

此时引用计数为0

升级

新增接收任意删除器
解决不是new出来的对象通过智能指针管理

shared_ptr设计了一个删除器来解决这个问题

原代码测试

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;
	shared_ptr<int> sp1((int*)malloc(4), freeFunc);
	DeleteArrayFunc<int> deleteArrayFunc;
	shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);

	return 0;
}

改造模拟实现代码

针对有删除器的构造函数:

cpp 复制代码
	//构造--删除器函数的接收

	// 方法一://对重载函数的调用不明确
	//Shared_Ptr(T* ptr = nullptr, function<void(T*)> del= [](T* ptr) {delete ptr; })

	//方法二:
	template<class D>
	Shared_Ptr(T* ptr , D del)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{
	}

	方法三:
	//Shared_Ptr(T* ptr, function<void(T*)> del)
	//	:_ptr(ptr)
	//	, _pcount(new int(1))
	//	, _del(del)
	//{
	//}

方法三提供拓展思路,使用方法二

release函数的重写

cpp 复制代码
	void release()
	{
		//当引用计数为1时,不再让该指针管理资源,应该立马释放
		
		//如果是下面这样,当引用计数为1时,只让引用计数-1并没有释放
		//正常情况下引用计数为1时在调用release时就应该delete了
		//if (*_pcount != 0)或者改成!=1
		//{
		//	--(*_pcount);
		//}
		//else
		//{
		//	delete _pcount;
		//	delete _ptr;
		//}

		if (--(*_pcount) == 0)
		{
			if(_del)//仅验证function对象_del有true or false的属性
			{
				cout << "_del有可调用内容" << endl;
			}
			delete _pcount;
			//delete _ptr;
			_del(_ptr);
		}

	}

注意当引用计数为1时是按0判断还是按照1判断

引用计数为1时释放,说明此时只有一个智能指针管理该资源,--后立马能释放

难以理解就将0换成1,如下

cpp 复制代码
		if ((*_pcount)-- == 1)
		{
			if (_del)//仅验证function对象_del有true or false的属性
			{
				cout << "_del有可调用内容" << endl;
			}
			delete _pcount;
			//delete _ptr;
			_del(_ptr);
		}
		

提供的新的成员变量

cpp 复制代码
private:
	int* _pcount;//引用计数个数
	T* _ptr;//管理指针
	//用包装器接收删除函数,不需要返回值,操作T* _ptr
	//写在成员函数上,默认_del被初始化成[](T* ptr) {delete ptr; },函数内部可以调用_del进行释放
	function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

function<void(T*)> _del = [](T* ptr) {delete ptr; };的包装器的使用值得反复研究

删除器内容

cpp 复制代码
template<class T>
struct DelArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

struct ListNode
{
	int val;
	weak_ptr<ListNode> next;
	weak_ptr<ListNode> prev;

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

测试删除器

cpp 复制代码
	Shared_Ptr<ListNode> sp4(new ListNode[10], DelArray<ListNode>());
	Shared_Ptr<ListNode> sp5(new ListNode[10], [](ListNode* ptr) {delete[] ptr; });
	Shared_Ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
cpp 复制代码
	    if (--(*_pcount) == 0)
		{
			if(_del)
			{			
				_del(_ptr);
				cout << "_del有可调用内容" << endl;
			}
			else
			{
				delete _ptr;
				cout << "_del无可调用内容" << endl;
			}
			delete _pcount;
		}

该段代码仅提供思路参考,面对function对象有无可调用对象的参考

重载调用不明确

方法一://造成重载调用不明确

方法一在使用Shared_Ptr传递一个T*类型的对象时,

cpp 复制代码
Shared_Ptr(T* ptr = nullptr)

Shared_Ptr(T* ptr = nullptr, function<void(T*)> del= [](T* ptr) {delete ptr; })

这两个构造函数是都能接收的,构造二在接收时默认有一个缺省值,构造一二都会调用

cpp 复制代码
情况1:void fun(int a = 1) { cout << a << endl; }

情况2:void fun(int a = 1, int b = 2) { cout << a << " " << b << endl; }

int main(){
	//fun(1);//报错,对重载函数的调用不明确
	return 0;
}

此时fun(1)能调用情况1的fun,也能调用情况2的fun,调用1时,a接收内容。调用2时,a接收内容,b使用缺省值。这两种情况都是能被调用的,造成调用不明确

使调用重载明确方法:

cpp 复制代码
//方法1:这样的修改使得构造函数的重载更加明确,因为现在有两个不同的参数类型
template<class D>
void fun(int a , D b=1 ) { cout << a << " " << b << endl; }
//方法2:
void fun(int a, int b) { cout << a << " " << b << endl; }

这里class D为了让两个函数重载的更加明显,避免编译器又分不清

引入D后,迫使fun函数调用时必须给第二个参数传参,才能调用void fun(int a , D b=1 ),避免因固定类型的缺省导致重载调用不明确

缺陷

cpp 复制代码
Shared_Ptr<ListNode> sp4(new ListNode[10], DelArray<ListNode>());
Shared_Ptr<ListNode> sp5(new ListNode[10], [](ListNode* ptr) {delete[] ptr; });

sp4->next=sp5;
sp5->prev=sp4;

造成循环引用

sp4->next=sp5;

sp5->prev=sp4;

使用时应避免出现相互交叉的情况

形成闭合回路的情况会造成 循环引用

相关推荐
捕鲸叉19 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer23 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq25 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
wheeldown1 小时前
【数据结构】选择排序
数据结构·算法·排序算法
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷2 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程2 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
观音山保我别报错2 小时前
C语言扫雷小游戏
c语言·开发语言·算法