【初阶与进阶C++详解】第二十四篇:智能指针(内存泄漏+RAII+auto_ptr+unique_ptr+shared_ptr+weak_ptr+定制删除器)

🏆个人主页企鹅不叫的博客

​ 🌈专栏

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

💙系列文章💙

【初阶与进阶C++详解】第一篇:C++入门知识必备

【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件

【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)

【初阶与进阶C++详解】第四篇:类和对象中(类的六个默认成员函数)

【初阶与进阶C++详解】第五篇:类和对象下(构造+static+友元+内部类

【初阶与进阶C++详解】第六篇:C&C++内存管理(动态内存分布+内存管理+new&delete)

【初阶与进阶C++详解】第七篇:模板初阶(泛型编程+函数模板+类模板+模板特化+模板分离编译)

【初阶与进阶C++详解】第八篇:string类(标准库string类+string类模拟实现)

【初阶与进阶C++详解】第九篇:vector(vector接口介绍+vector模拟实现+vector迭代器区间构造/拷贝构造/赋值)

【初阶与进阶C++详解】第十篇:list(list接口介绍和使用+list模拟实现+反向迭代器和迭代器适配)

【初阶与进阶C++详解】第十一篇:stack+queue+priority_queue+deque(仿函数)

【初阶与进阶C++详解】第十二篇:模板进阶(函数模板特化+类模板特化+模板分离编译)

【初阶与进阶C++详解】第十三篇:继承(菱形继承+菱形虚拟继承+组合)

【初阶与进阶C++详解】第十四篇:多态(虚函数+重写(覆盖)+抽象类+单继承和多继承)

【初阶与进阶C++详解】第十五篇:二叉树搜索树(操作+实现+应用KVL+性能+习题)

【初阶与进阶C++详解】第十六篇:AVL树-平衡搜索二叉树(定义+插入+旋转+验证)

【初阶与进阶C++详解】第十七篇:红黑树(插入+验证+查找)

【初阶与进阶C++详解】第十八篇:map_set(map_set使用+multiset_multimap使用+模拟map_set)

【初阶与进阶C++详解】第十九篇:哈希(哈希函数+哈希冲突+哈希表+哈希桶)

【初阶与进阶C++详解】第二十篇:unordered_map和unordered_set(接口使用+模拟实现)

【初阶与进阶C++详解】第二十一篇:哈希应用(位图实现应用+布隆过滤器增删查优缺点+海量数据面试题)

【初阶与进阶C++详解】第二十二篇:C++11新特性(列表初始化+变量类型推到+右值引用+新增默认成员函数+可变模板参数+lambda表达式+包装器function_bind)

【初阶与进阶C++详解】第二十三篇:异常(异常抛出+异常捕获+异常优缺点)



💎一、内存泄漏

🏆1.内存泄漏概念

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

🏆2.内存泄漏危害

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

🏆3.避免内存泄漏

内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具

💎二、智能指针基本概念

🏆1.RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术,RAII是智能指针的指导思想。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做法有两大好处:

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

🏆2.智能指针概念

智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。

智能指针都只能管理单个对象的资源。

框架:智能指针类模板中都需要包含一个指针对象,构造函数和析构函数

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

		~SmartPtr()
		{
			cout << "delete" << _ptr << endl;
			delete _ptr;
			_ptr = nullptr;
		}
 	//直接打印地址
 	T* Get()
		{
			return _ptr;
		}
	private:
		T* _ptr;//指针对象
	};

💎三、智能指针的使用和原理

智能指针的使用跟普通指针类似,可以使用运算符“ * " 和 ” -> “去获得指向的对象,因此,我们就需要在类中重载” * " 和" -> "函数

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

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

当程序结束时,此时sp1和sp2指针被销毁时,对象sp1和sp2会自动调用析构函数去释放所指向的资源,这是智能指针特点。

int main ()
{
//下面两个释放完后会打印delete
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
}

sp1和sp2指向的同一块空间,当sp2被销毁时,它会调用它的析构函数去delete该资源对象,当sp1被销毁时,也会去调用它的析构函数去释放sp1所指向的资源.所以,当程序结束时,sp2被先被销毁,同时释放sp2所指向的资源,然后sp1被销毁,也去释放该资源对象,那么如下的资源对象同时被释放两次,所以程序就会被崩溃掉。(资源对象被释放后,如果再去释放该资源,程序就会崩溃)

int main ()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1);
}

不能使用原生的拷贝构造函数和赋值重载函数,并且定义的拷贝构造函数和赋值重载函数需要考虑只能释放一次资源对象。

💎四、auto_ptr

auto_ptr是c++98版本库中提供的智能指针,该指针解决上诉的问题采取的措施是管理权转移的思想,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。

如果auto_ptr调用拷贝构造函数或者赋值重载函数后,如果再去使用原来的对象的话,那么整个程序就会崩溃掉(因为原来的对象被设置为nullptr)。

准委员会建议:什么情况下都不要使用auto_ptr,包含头文件memory

int main ()
{
	auto_ptr<int> sp1(new int);
	auto_ptr<int> sp2(sp1);//sp1置为空
}

auto_ptr的拷贝构造函数和赋值重载函数的实现

template<class T>
class auto_ptr
	{
	public:	

		// sp2(sp1)
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;
		}
	  auto_ptr<T> operator=(auto_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				_ptr = sp._ptr;
				sp._ptr = nullptr;
			}

			return _ptr;
		}
	private:
		T* _ptr;
	};

💎五、unique_ptr

unique_ptr是c++11版本boost库中提供的智能指针(后面share_ptr和weak_ptr也是这个库中),它直接将拷贝构造函数和赋值重载函数给禁用掉,因此,不让其进行拷贝和赋值。

int main()
{
	unique_ptr<int> up1(new int);
	//bit::unique_ptr<int> up2(up1);//不能调用unique_ptr的拷贝构造
	unique_ptr<int> up2(new int);
	//up1 = up2;//不能调用unique_ptr的拷贝构造
}

unique_ptr的拷贝函数和赋值重载函数

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

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}
 	//直接返回地址
		T* get()
		{
			return _ptr;
		}
		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

	private:
		T* _ptr;
	};

💎六、shared_ptr

🏆1.shared_ptr的基本概念

shared_ptr是c++11版本库中的智能指针,shared_ptr允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,因此是程序不会崩溃掉。

🏆2.shared_ptr的原理

shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源

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

🏆3.shared_ptr的实现

赋值过程有以下三种情况:

  • ptr1=ptr1;智能指针自己给自己赋值,不做处理
  • ptr2=ptr1;如果ptr1和ptr2指向同一块空间,不做处理
  • ptr2=ptr1;如果ptr2和ptr1指向的空间不一样,如果ptr2原本就有指向的空间,此时就需要将ptr2的引用计数减1,如果此时ptr2引用计数变为0,则需要释放ptr2指向资源,然后将ptr2指向ptr1指向的资源,且ptr1的引用计数加1

🏆4.shared_ptr的循环引用

假设我们要使用定义一个双向链表,如果我们想要让创建出来的链表的节点都定义成shared_ptr智能指针,那么也需要将节点内的_pre和_next都定义成shared_ptr的智能指针。

  • 当创建出node1和node2智能指针对象时,引用计数都是1.
  • 当node1的next指向node2所指向的资源时,node2的引用计数就+1,变成2,node2的pre指向noede1所指向的资源时,node1的引用计数+1,变成2.
  • 当这两个智能指针使用完后,调用析构函数,引用计数都减1,都变成1,由于引用计数不为0,所以node1和node2所指向的对象不会被释放。
  • 当node1所指向的资源释放需要当node2中的_prev被销毁,就需要node2资源的释放,node2所指向的资源释放就需要当node1中的_next被销毁,就需要node1资源的释放。因此node1和node2都有对方的“把柄”,这两个就造成循环引用现象,最终这node1和node2资源就不会进行释放。
struct ListNode
{
	shared_ptr<ListNode> _next = nullptr;
	shared_ptr<ListNode> _prev = nullptr;
};

int main()
{
	// 循环引用
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	//输出1
	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	node1->_next = node2;
	node2->_prev = node1;
	// use_count(): 返回智能指针对象的引用计数
    //输出2
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	return 0;
}

weak_ptr解决shared_ptr的循环引用

weak_ptr对象指向shared_ptr对象时,不会增加shared_ptr中的引用计数,因此当node1销毁掉时,则node1指向的空间就会被销毁掉,node2类似,所以weak_ptr指针可以很好解决循环引用的问题。

所以在定义双向链表或者在二叉树等有多个指针的时候,如果想要将该类型定义成智能指针,那么结构体内的指针需要定义成weak_ptr类型的指针,防止循环引用的出现。

weak_ptr不能单独管理资源,必须配合shared_ptr一块使用,解决shared_ptr中存在的 循环引用问题

struct ListNode
{
 //不支持用指针初始化,只能拷贝和shared_ptr初始化
	weak_ptr<ListNode> _next ;
	weak_ptr<ListNode> _prev;
};

int main()
{
	// 循环引用
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	//输出1
	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	node1->_next = node2;
	node2->_prev = node1;
	// use_count(): 返回智能指针对象的引用计数
 //输出1
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	return 0;
}

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)
	{
		if (_ptr != sp.get())
		{
			_ptr = sp.get();
		}

		return *this;
	}

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

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

public:
	T* _ptr;
};

🏆5.定制删除器

出现问题

我们如果在动态内存中创建出一个数组,用一个unique_ptr对象去指向该数组,当unique_ptr使用完后,就会去调用析构函数,由于unique_ptr默认的删除方式是 delete ptr,后面没有带方括号,那么程序就会崩掉。

同理,如果我们打开一个了文件,返回一个文件指针,让一个unique_ptr对象去指向该文件,那么在调用析构函数的时候就不能采用delete方法,而是使用fclose()函数去关闭该文件

如果用malloc开辟了一段空间,那么也因该用free释放空间

class Date
{
public:
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main ()
{
//下面程序会报错
	unique_ptr<Date> up1(new Date[10]);
	unique_ptr<Date,> up3((Date*)malloc(sizeof(Date)* 10));
	unique_ptr<FILE> up4((FILE*)fopen("Test.cpp", "r"));
}

解决办法

下面是官方对unique_ptr定义,除了对应的指针外,还有一个带默认值的仿函数,所以对于上面情况,我们要写出对应的仿函数

template <class T, class D = default_delete<T>> class unique_ptr;

下面是对unique_ptr改造(添加了默认定制删除器)和具体的实现过程

template<class T>
struct default_delete
{
	void operator()(T* ptr)
	{
		delete ptr;
	}
};

template<class T, class D = default_delete<T>>
class unique_ptr
{
public:
	// RAII思想
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		if (_ptr)
		{
			D del;
			del(_ptr);
			_ptr = nullptr;
		}
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T* get()
	{
		return _ptr;
	}

	unique_ptr(const unique_ptr<T>& sp) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

private:
	T* _ptr;
};

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout <<"delete[]"<<ptr << endl;

		delete[] ptr;
	}
};

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

		free(ptr);
	}
};

struct Fclose
{
	void operator()(FILE* ptr)
	{
		cout << "fclose" << ptr << endl;

		fclose(ptr);
	}
};

int main ()
{
 //传类型
	unique_ptr<Date> up1(new Date);
	unique_ptr<Date, DeleteArray<Date>> up2(new Date[10]);
	unique_ptr<Date, Free<Date>> up3((Date*)malloc(sizeof(Date)* 10));
	unique_ptr<FILE, Fclose> up4((FILE*)fopen("Test.cpp", "r"));
}

注意:shared_ptr定制删除器只能在构造函数传参支持,unique_ptr定制删除器只在类的模板参数支持

下面是shared_ptr演示

int main ()
{
//传对象
shared_ptr<Date> sp2(new Date[10], DeleteArray<Date>());
}

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

penguin_bark

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

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

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

打赏作者

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

抵扣说明:

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

余额充值