当前位置: 首页 > news >正文

C++的RAII思想以及在智能指针上的应用

文章目录

    • 何为RAII?
      • 内存泄漏的场景
    • SmartPtr
    • C++98的失败设计:auto_ptr
    • unique_ptr
    • shared_ptr
      • 循环引用
    • weak_ptr
    • 定制删除器

何为RAII?

RAII的百度百科介绍
在这里插入图片描述
一般情况下,C++申请资源后都需要手动释放资源,一旦忘记资源的释放就会造成内存泄漏,为了解决内存泄漏问题,C++引入了RAII机制。

RAII是一种利用对象的声明周期来控制资源释放的技术。比如一个局部对象,出了作用域就被销毁,RAII利用这一特性将资源与对象绑定在一起,当局部对象释放时,绑定在其身上的资源也要被释放。

内存泄漏的场景


int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void func()
{
	int* p1 = new int[10]; 

	int* p2 = new int[10]; 
	int* p3 = new int[10]; 
	int* p4 = new int[10]; 

	try
	{
		div();
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		delete[] p3;
		delete[] p4;

		throw;
	}

	delete[] p1;
	delete[] p2;
	delete[] p3;
	delete[] p4;
}

int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
		// ...
	}

	return 0;
}

main函数调用func函数并捕获其中的异常,func函数申请资源后调用div函数并捕获其中的异常,div函数可能抛出异常。当资源被申请后,调用div函数时发生了异常,func函数就能捕获其异常,并释放之前申请的资源。

但程序还有问题,如果func函数只申请一次资源,申请失败时,main函数捕获申请失败的异常,这没有问题,但上面的func函数申请了四次资源,假如前三次申请资源成功,第四次申请资源失败,异常被抛出并被main函数捕获,但前三次申请的资源没有被释放,造成了资源泄漏。总不能在main函数中释放资源,p1到p4都是局部对象,出了作用域就销毁了,main函数想释放资源也不知道资源在哪。而使用RAII的思想就能解决这样的问题。

当申请资源时,就构造一个对象,在对象的声明周期内,资源能被正常使用,对象析构时,资源也随之释放。

SmartPtr

SmartPtr类的定义

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr() { cout << "delete:" << _ptr << endl; delete _ptr; _ptr = nullptr; }

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

private:
	T* _ptr;
};

SmartPtr类管理一个指针,使用SmartPtr类对象时可以像指针一样使用,支持*,->操作,特殊的是SmartPtr类对象在析构时会将管理的指针上的空间释放,也就是申请资源的生命周期和SmartPtr类对象的生命周期一样,对象生命结束,资源的生命也结束。

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void func()
{
	SmartPtr<int> p1(new int);
	SmartPtr<int> p2(new int);
	SmartPtr<int> p3(new int);
	SmartPtr<int> p4(new int);
}

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

	return 0;
}

在这里插入图片描述
尽管没有显式地写出delete释放申请的资源,但SmartPtr在析构时顺便清理了资源。

总结一下智能指针的特点:1.不用显式地写出析构函数。2.资源在智能指针的生命周期中始终有效。3.可以像指针一样的使用。4.最重要的是:具有RAII特性

C++98的失败设计:auto_ptr

上面的SmartPtr还存在着问题:拷贝构造对象和赋值时,多个SmartPtr指向同一块空间,当SmartPtr析构时便会造成资源的多次释放,导致程序崩溃。所以针对这个问题,智能指针有很多版本,C++98只有一个智能指针版本auto_ptr,auto_ptr对于拷贝问题的解决方案是:管理权的转移,比如将p1拷贝给p2,也就是将p1对于资源的管理权转移给了p2,即将p1的指针置空,后面的代码也就不能使用p1对象。这个智能指针被很多人诟病,许多公司明确要求不能使用auto_ptr指针,因为管理权转移导致了原指针不能使用,相比后续的智能指针,auto_ptr确实是个失败的设计。

auto_ptr的模拟实现

template <class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}
	auto_ptr(auto_ptr<T>& p)
	{
		_ptr = p._ptr;
		p._ptr = nullptr;
	}
	auto_ptr<T>& operator=(auto_ptr<T>& p)
	{
		_ptr = p._ptr;
		p._ptr = nullptr;
	}

	~auto_ptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}


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

private:
	T* _ptr;
};

unique_ptr

boost作为C++的一个开源库,承担着C++新功能的开发,如果boost库中有好用的设计出现,C++的标准库便会将好用的设计引入,出现在下一次的更新中。unique_ptr最初就是boost库中的scoped_ptr,C++的标准库汲取scoped_ptr中的精华并设计出了unique_ptr。unique_ptr解决智能指针拷贝问题的方案是:禁止拷贝

unique_ptr的模拟实现

template <class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	unique_ptr(const unique_ptr<T>& p) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;

	~unique_ptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
		_ptr = nullptr;
	}


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

private:
	T* _ptr;
};

在拷贝构造函数和赋值重载函数后加上delete,表示不能调用该函数,并且不能定义该函数。unique用这种简单粗暴的方式解决智能指针的拷贝问题

shared_ptr

shared_ptr允许拷贝智能指针,其采用了计数的方式进行拷贝,只有一个指针指向资源时,计数为1,两个指针指向,计数为2,以此类推。对象析构时,只要将计数减一,如果减一后的计数为1,说明该对象是指向资源的最后一个对象,需要完成资源的释放。

注意:shared_ptr中又存储了一个指向_count的指针,在拷贝构造后,两对象的_ptr指向同一块空间,需要将_count的值+1,达到计数增加的效果。

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		: _ptr(ptr)
		, _pCount(new int(1))
	{}
	shared_ptr(const shared_ptr<T>& p)
	{
		_ptr = p._ptr;
		_pCount = p._pCount;
		*_pCount++;
	}
	void Release()	
	{
		if (--(*_pCount) == 0) // 当计数为0,需要释放pCount
		{
			if (_ptr) // 如果_ptr为空,只要释放pCount
			{
				delete _ptr;
				_ptr = nullptr;
			}
			cout << "~shared_ptr()" << endl;
			delete _pCount;
			_pCount = nullptr;
		}
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& p)
	{
		// 指向资源不同时,使两指针指向同一资源,并且计数增加
		if (_ptr != p._ptr) // 当指向资源相同时,没有必要进行赋值
		{
			Release(); // 先释放该指针之前指向的空间

			_ptr = p._ptr;
			_pCount = p._pCount;
			* _pCount++;
		}
	}

	~shared_ptr()
	{
		Release();
	}

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

private:
	T* _ptr;
	int* _pCount;
};

循环引用

shared_ptr还存在一个问题:循环引用。假设现在有Node这样一个类,有三个成员,两个分别指向前后节点的智能指针,一个保存数据的成员变量_val。当申请了Node类型的资源后,用shared_ptr托管,此时的智能指针可以正常的释放资源

struct Node
{
	Node()
		: _prev(nullptr)
		, _next(nullptr)
		, _val(0)
	{}
	myPtr::shared_ptr<Node> _prev;
	myPtr::shared_ptr<Node> _next;
	int _val;
};

int main()
{
	myPtr::shared_ptr<Node> p1(new Node);
	myPtr::shared_ptr<Node> p2(new Node);

	// p1->_next = p2;
	// p2->_prev = p1;
	return 0;
}

在这里插入图片描述
但是当p1指针的_next指向p2,p2的_prev指向p1后,资源就不能正常释放,产生了内存泄漏
在这里插入图片描述
分析一下前后两段代码,当两智能指针没有产生连接时,p1和p2的计数都为1,释放资源正常。但它们产生连接后,p1的_next也是一个智能指针,将_next指向p2,相当于shared_ptr的一次赋值,此时的p2计数为2,p2的_prev指向p1后,p1的计数也为2。当main函数指向完,p1和p2的生命周期结束,自动调用其析构函数,但其析构函数是根据计数决定是否析构,两指针的计数都为2,析构后只是将计数减小为1,并没有释放资源。

此时要释放p1的资源,就要释放p2的_prev,_prev作为Node类的成员,只有Node类释放时才会被释放,所以现在要释放p2,而要释放p2就要释放p1的_next,要释放_next就要释放p1,这就回到了开头的问题。对于循环引用的问题,需要使用weak_ptr来解决。

weak_ptr

weak_ptr是解决shared_ptr循环引用问题的关键,用shared_ptr作为Node的前后指针类型,使得Node的前后指针也参与了资源的管理,weak_ptr作为弱指针,即不参与资源的管理,只是用来指向资源,类似于容器里的迭代器

weak_ptr的定义

template <class T>
class weak_ptr
{
public:
	weak_ptr(T* ptr)
		:_ptr(ptr)
	{}
	weak_ptr(weak_ptr<T>& p)
	{
		_ptr = p._ptr;
	}
	weak_ptr<T>& operator=(weak_ptr<T>& p)
	{
		_ptr = p._ptr;
		return *this;
	}
	weak_ptr<T>& operator=(shared_ptr<T>& p)
	{
		_ptr = p.get();
		return *this;
	}

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

private:
	T* _ptr;
};

// Node的前后指针类型改为weak_ptr	
struct Node
{
	Node()
		: _prev(nullptr)
		, _next(nullptr)
		, _val(0)
	{}
	myPtr::weak_ptr<Node> _prev;
	myPtr::weak_ptr<Node> _next;
	int _val;
};

在这里插入图片描述

定制删除器

上面模拟实现的智能指针中的析构函数,都是用delete进行释放资源。但这就有一个问题,如果资源需要用delete[]析构,或者不是new的,而是malloc的,又或者是一个文件指针,需要用fclose进行关闭,这时的析构函数就不能写死delete释放资源了,需要用不同的方式进行释放资源,但默认的释放方式为delete,C++提供了定制删除器进行特定资源的释放,名字看着唬人,其本质就是一个仿函数。

DefaultDelete为默认的删除器,执行的是delete语句,将unique_ptr改造,加一个新的模板参数Delete,接收默认值,使用者可以根据资源的不同传入相应的删除器。析构函数释放资源时,用Delete类构造一个对象,利用该对象对()的重载释放资源。

template <class T>
struct DefaultDelete
{
	void operator()(T* p)
	{
		delete p;
		cout << "delete:" << p << endl;
	}
};

template <class T, class Delete = DefaultDelete<T>>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	unique_ptr(const unique_ptr<T>& p) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;

	~unique_ptr()
	{
		Delete del;
		//cout << "delete:" << _ptr << endl;
		del(_ptr);
		_ptr = nullptr;
	}


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

private:
	T* _ptr;
};

在这里插入图片描述
库中的unique_ptr也实现了定制删除器,但shared_ptr用于底层结构实现不同,定制删除器作为实参在其构造函数的参数被传入。
在这里插入图片描述
所以,在使用shared_ptr的删除器时,需要将对象传入,而不是类型,而传入对象,可以用lambda表达式代替。
在这里插入图片描述
在这里插入图片描述

相关文章:

  • Linux-进程和计划任务管理⭐
  • OpenCV实现霍夫变换
  • 【Python】使用Python连接ClickHouse进行批量数据写入
  • 关于TrAXFilter类在动态加载的利用思考以及如何无视构造器获取对象
  • 磨损对输送带生产效率的影响
  • ansible安装教程
  • word embedding
  • stable diffusion webUI之赛博菩萨【秋葉】——工具包新手安裝与使用教程
  • 搭建xorbits容器集群,大规模数据去重利器
  • 【黑马程序员】2、TypeScript介绍_黑马程序员前端TypeScript教程,TypeScript零基础入门到实战全套教程
  • Eclipse是如何创建web project项目的?
  • Go命令源码文件
  • #include <iostream> 和#include <iostream.h>
  • 【硬件开源电路】STM32G070RBT6开发板
  • 登录页面案例
  • Hudi源码|bootstrap源码分析总结(写Hudi)
  • JavaEE——网络通信基础
  • Odoo | 页面视图的跳转逻辑
  • tf.name_scope
  • 【让你从0到1学会c语言】程序环境和预处理指令
  • 什么是CMMI能力成熟度模型?企业为什么要做?
  • 嵌入式 Linux 入门(十、Linux 下的 C 编程)
  • 【附源码】计算机毕业设计SSM培训中心管理系统
  • S2B2C模式有何优势?S2B2C电商系统赋能皮革企业渠道,提升供应链管理效率
  • cdq分治 学习笔记
  • MES是生产成功的因素
  • 通过ssh远程登录linux的原理过程和配置免密登录
  • 软考:信息安全工程师4(系统安全)
  • Hadoop中的Yarn的Tool接口案例、Yarn 案例实操(四)
  • 【C++】STL——string(两万字详解)
  • 浅谈Linux下的redis攻击
  • 【C++】类和对象(中)(万字详解)