首页 > 编程语言 >【C++】学习笔记——智能指针

【C++】学习笔记——智能指针

时间:2024-08-02 15:59:28浏览次数:17  
标签:cout int auto 笔记 C++ shared ptr 指针

文章目录


二十一、智能指针

1. 内存泄漏

在上一章的异常中,我们了解到如果出现了异常,会中断执行流,跳转到catch处。但是这种情况非常不好,如果我们跳过了内存释放的代码,就会导致内存泄漏。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
虽然我们可以通过异常的再次抛出来解决,但是终究是比较麻烦。

如何避免内存泄露呢?

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。但这只是理想状态,仍有问题出现内存泄漏,需要智能指针来保障。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 使用内存泄漏工具检测。

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

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

RAII

RAII(Resource Acquisition Is Initialization)是一种 利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。我们可以使用对象来管理资源,在创建对象的时候获取资源,销毁对象的时候释放资源。

#include <iostream>
using namespace std;

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
	// 构造函数获取资源
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	// 析构函数释放资源
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "~SmartPtr()" << endl;
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		// 抛出个C++异常标准库里的异常类型
		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 技术。
这种做法有两大好处:①不需要显式地释放资源。②采用这种方式,对象所需的资源在其生命期内始终保持有效。

智能指针的原理

智能指针就是借助的 RAII 思想来实现的。但是上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。还得需要将* 、->重载下,才可让其像指针一样去使用。

#include <iostream>
using namespace std;

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
	// 构造函数获取资源
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

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

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

	// 析构函数释放资源
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "~SmartPtr()" << endl;
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		// 抛出个C++异常标准库里的异常类型
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	// 使其具有指针的行为
	*sp1 += 10;
	SmartPtr<pair<string, int>> sp3(new pair<string, int>);
	sp3->second = 1;
	sp3.operator->()->first = "hello";

	cout << div() << endl;
}

int main()
{
	try
	{
		Func();
	}
	// 使用异常标准库的基类获取
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

智能指针的特性:①RAII特性。②重载operator*和opertaor->,具有像指针一样的行为。

auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。需要注意的是,auto_ptr运行拷贝构造和赋值重载,但是 他会把旧的指针置空

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

int main()
{
	auto_ptr<int> sp1(new int);
	auto_ptr<int> sp2(sp1);


	*sp2 = 10;
	cout << *sp2 << endl;
	cout << *sp1 << endl;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
auto_ptr的模拟实现:

namespace my
{
	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 = nullptr;
			}
			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的特性,会将旧指针置空,所以一般都不会用这个。

unique_ptr

unique_ptr解决了auto_ptr的缺点,因为unique_ptr直接就是禁止拷贝构造以及复制重载。非常简单粗暴。
unique_ptr的模拟实现:

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

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

		// delete掉拷贝构造和复制重载
		unique_ptr(const unique_ptr<T>&sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
	private:
		T* _ptr;
	};
}

由于unique_ptr的特性,一个资源只能被一个指针所指向。

shared_ptr

unique_ptr虽然解决了auto_ptr的问题,但是限制太大了,如果非要多个指针指向同一块资源的话就没办法,于是C++又提供了新的智能指针——shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。最后一个指针释放资源。
那我们如何实现这个方法呢?怎么定义引用计数?使用局部变量吗?当然不可以,因为这样会导致每个对象里面都有自己独立的引用计数,失去了意义。静态变量吗?也不行。因为静态会导致类中只能存在1份,即只能对一个资源有效,多个资源就无法通过一个静态变量来管理。
那怎么办?我们可以和智能指针一样,构造时创建一个引用计数,析构时释放引用计数。
shared_ptr模拟实现:

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

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

		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝构造使引用计数+1
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			// 自己给自己赋值没意义
			if (_ptr != sp._ptr)
			{
				// 使原来的引用计数-1
				release();

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

				// 新的引用计数+1
				++(*_pcount);
			}

			return *this;
		}

		// 资源释放
		void release()
		{
			// 引用计数变成0就释放资源
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		// 引用计数变量
		int* _pcount;
	};
}

shared_ptr的循环引用

根据上面来看,shread_ptr似乎以及非常完善了,真的是这样吗?我们来看看下面这个场景:

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;

	ListNode(int data = 0)
		:_data(data)
	{}

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

int main()
{
	shared_ptr<ListNode> node1(new ListNode(10));
	shared_ptr<ListNode> node2(new ListNode(20));

	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;
}

在这里插入图片描述
我们发现引用数没错,但是并没有释放资源。这是为什么呢?因为node1和node2析构时,引用计数-1,但是分别还有node1->next以及node2->prev还指向两个节点,因此引用计数并没有变成 0。引用计数不是0就不会析构释放资源,这就是shared_ptr的循环引用问题。
在这里插入图片描述

weak_ptr

上面的shared_ptr循环引用的问题可以使用weak_ptr解决。weak_ptr并不会增加引用计数。并不是全部替换,节点本身都还是shread_ptr,但是节点的前驱指针和后继指针改成了weak_ptr。

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

struct ListNode
{
	int _data;
	// 这里替换成不会增加引用计数的 weak_ptr
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;

	ListNode(int data = 0)
		:_data(data)
	{}

	~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;
}

在这里插入图片描述
这样,只有被shared_ptr指向的节点才会增加引用计数。

删除器

智能指针的释放都是使用 delete 来释放的,与 delete 匹配的是 new,如果不是new出来的对象如何通过智能指针管理呢?比如malloc,或者new[]等等,这样的若是使用delete来释放资源就会出现大问题!该怎么办呢?其实shared_ptr设计了一个删除器来解决这个问题。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <memory>
using namespace std;

// 仿函数的删除器
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<int> sp4(new int[10], [](int* p){delete[] p; });
	std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); });

	return 0;
}

在这里插入图片描述
只要在定义的时候在后面跟上删除器(删除的方式)就可以使用了。


未完待续

标签:cout,int,auto,笔记,C++,shared,ptr,指针
From: https://blog.csdn.net/m0_69828905/article/details/140819007

相关文章

  • 【C++】学习笔记——特殊类的设计
    文章目录二十二、特殊类的设计1.请设计一个类,不能被拷贝2.请设计一个类,只能在堆上创建对象3.请设计一个类,只能在栈上创建对象4.请设计一个类,不能被继承5.请设计一个类,只能创建一个对象(单例模式)未完待续二十二、特殊类的设计1.请设计一个类,不能被拷贝拷贝......
  • 【C++庖丁解牛】C++特殊类设计 | 单例模式
    ......
  • Pytorch笔记|小土堆|P10-13|transforms
    transforms对图像进行改造最靠谱的办法:根据help文件自行学习transforms包含哪些工具(类)以及如何使用————————————————————————————————————自学一个类时,应关注:1、如何使用各种工具(类)的使用思路:创建对象(实例化)——>传入参数,调用函数(如有__......
  • 让C++通过JNI来callback回调kotlin侧代码
    kotlin一次通过jni调用C++侧,然后C++侧多次回传数据让C++通过JNI来callback回调kotlin侧代码1.定义Kotlin接口在Kotlin中定义一个用于接收C++回调的接口:interfaceDataCallback{funonDataReceived(data:String)}2.定义C++侧的JNI方法在C++侧,实现接收指令和......
  • Qt C++ 调用 Python 之 PyObject* 数据类型转换
    整数:PyLong_FromLong和PyLong_AsLong类型检查函数:PyLong_Check()intcppInt=42;//C++整数转换为Python整数对象PyObject*pyInt=PyLong_FromLong(cppInt);//Python整数对象转换为C++整数longcppIntFromPy=PyLong_AsLong(pyInt);Py_DECREF(pyInt)......
  • 【笔记】计数选讲:容斥、LGV、集合幂级数、GF 2024.8.2
    今天写的很乱。[HEOI2013]SAO容斥。因为我们已经知道父亲\(<\)儿子时的情况(\(n!/\prod_isiz_i\),也适用于森林),那么儿子\(<\)父亲的情况就容斥掉,无限制的就当作那条边不存在。树上背包,记录当前节点为根的连通块大小和容斥系数的积。*[ECFinal23A]DFSOrder4转写为:统计多......
  • c++ 从txt读取数据gnss转化为enu数据
    https://github.com/Dongvdong/gnss_ecef_enu_txt_yaml  测试文件config.yaml#==============##CameraModel##==============#Camera.name:EH2022leftmonocularCamera.setup:monocularCamera.model:perspectiveCamera.fx:1220Camera.fy:1220Camera.cx:......
  • 基于SpringBoot的智能购房推荐系统-09040(免费领源码)可做计算机毕业设计JAVA、PHP、爬
    Springboot智能购房推荐系统摘 要近年来随着我国经济的高速发展,房地产业也随之蓬勃发展,尤其是最近国家新出台的房改政策。鼓励居民购房,这对房产公司无疑是一个极好的发展势头。尤为重要的是,近几年随着信息技术和电子商务的快速发展,许多企业都开发了自己房产信息软件。智......
  • Pytorch笔记|小土堆|P7-8|Tensorboard数据可视化
    Tensorboard数据可视化TensorBoard是一个可视化工具,它可以用来展示网络图、张量的指标变化、张量的分布情况等。它通过运行一个本地服务器,来监听6006端口(可更改)。在浏览器发出请求时,分析训练时记录的数据,绘制训练过程中的图像当前环境下安装:pipinstalltensorboardSummaryWrit......
  • Python基础学习笔记(一)
    文章目录一、下载Python二、变量三、数据类型四、运算符五、语句六、容器类型七、函数function八、常用API九、面向对象类的创建:创建对象:实例成员:实例方法:类成员:静态方法:十、三大特征:封装、继承、多态十一、六大原则:Python基础学习笔记(二)一、下载Python官网:https......