智能指针(C++11)

智能指针的使用

问题

我们在平时写程序的时候,有些情况下不可避免地会遇见内存泄露的情况。内存泄露是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。例如下面这个例子,内存泄漏不易被察觉。

int div()
{
	int a, b;
	cin >> a >> b;
	if(b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* p = new int[10];
	cout << div() << endl;
	delete[] p;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

程序首先会调用func函数,当func函数中执行到“ cout << div() << endl ”这句代码时,就会跳转到div函数中依次执行,当我们把b设置为0时,这时就会抛异常。程序的执行流就直接跳转到主函数的catch块中执行,最终导致func函数中申请到的资源没有被释放,造成内存泄露。

解决方案一

我们可以利用异常的重新捕获来解决,在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* p = new int[10];
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete[] p;
		throw;
	}
	delete[] p;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

解决方案二

上述问题也可以使用智能指针进行解决

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _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> sp(new int[10]);
	//...
	cout << div() << endl;
	//...
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
  • 此外,为了让SmartPtr对象能够像原生指针一样使用,还需要对*->运算符进行重载。

这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。

智能指针的原理

实现智能指针时需要考虑以下三个方面的问题:

  1. 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
  2. *->运算符进行重载,使得该对象具有像指针一样的行为。
  3. 智能指针对象的拷贝问题。

 RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术。 简而言之就是把资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源

但是我们对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。

int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1); //拷贝构造

	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4(new int);
	sp3 = sp4; //拷贝赋值
	
	return 0;
}

这是由于编译器默认生成的拷贝构造函数和赋值重载函数对内置类型完成的是浅拷贝,若将一个SmartPtr对象原封不动地拷贝给另外一个SmartPtr对象,这时两个SmartPtr对象就指向同一块内存空间。当SmartPtr对象销毁时就会调用析构函数,这样就导致了同一块空间被析构了两次,程序因而会崩溃。

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

C++中的智能指针

auto_ptr

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。比如:

int main()
{
	// C++98 一般实践中,很多公司明确规定不要用这个
	auto_ptr<A> ap1(new A(1));
	auto_ptr<A> ap2(new A(2));

	auto_ptr<A> ap3(ap1);

	// 崩溃
	//ap1->_a++;
	ap3->_a++;

	return 0;
}

发生管理权转移后,再进行拷贝时就会把被拷贝对象的资源管理权转移给拷贝对象导致被拷贝对象悬空,这也就造成了很大的隐患,当访问被拷贝对象时程序就会造成程序崩溃。

auto_ptr的模拟实现

auto_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使auto_ptr对象具有像原生指针一样的行为。
  • 在拷贝构造函数中,通过传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

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

		T* operator->()
		{
			return _ptr;
		}
		//管理权转移
		auto_ptr(auto_ptr<T>& a)
			:_ptr(a._ptr)
		{
			a._ptr = nullptr;
		}

	private:
		T* _ptr;
	};

unique_ptr

unique_ptr是C++11中引入的智能指针,unique_ptr通过禁止拷贝的方式解决智能指针的拷贝问题,它是个简单粗暴的办法,这样做也能保证资源不会被多次释放。但是禁止拷贝的办法也不是一个万全的办法,因为有些场景就是要用到拷贝。

int main()
{
    unique_ptr<int> up1(new int(0));
    //std::unique_ptr<int> up2(up1);  //会报错
    return 0;
}

unique_ptr的模拟实现

unique_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使unique_ptr对象具有像原生指针一样的行为。
  • 用C++11的方式在拷贝构造和赋值重载函数后面加上=delete。
template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

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

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

		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
	private:
		T* _ptr;
	};

shared_ptr

shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。

  • 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
  • 当新增一个对象管理这块资源时,则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
  • 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}

	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	//private:

	int _a;
};

注:A类的具体内部结构如上述代码所示。

int main()
{
    a::shared_ptr<A> sp1(new A(1));
	a::shared_ptr<A> sp2(new A(2));
	a::shared_ptr<A> sp3(sp1);

	sp1->_a++;
	sp3->_a++;
	
	a::shared_ptr<A> sp4(sp2);
	a::shared_ptr<A> sp5(sp4);
    return 0;
}

其关系图如下图所示,其中红色方框代表的是引用计数

shared_ptr的模拟实现

shared_ptr的实现步骤如下:

  • 首先要在shared_ptr类中增加一个成员变量_pcount,代表引用计数。
  • 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理资源。
  • 在拷贝构造函数中,我们让传入的对象与之前管理资源的对象共同管理对应的资源,同时将该资源的引用计数加一。
  • 在赋值重载函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。

举例:例如将sp5赋值给sp1,那么sp1原来指向的那块资源的引用计数就要减一,否则的话就会内存泄漏,因为无论如何引用计数都减不到为0,那么这块空间也就不会释放。并且原先sp1指向的这块资源如果只有sp1在管理的话,那么就直接将其释放即可。还有一个细节问题,例如遇到“ sp1=sp1 ”自己给自己赋值这种情况的话,我们就直接返回*this即可。

  • 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放。
  • 对*和->运算符进行重载,使shared_ptr对象具有像原生指针一样的行为。
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);
	}

	// sp6 = sp6
	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;
};

这里有个问题,为什么引用计数要放在堆区呢? 

如果将引用计数设置为int类型时,那么每个对象都有自己独立的引用计数,而我们希望的是不同的对象管理同一份资源 。其次,shared_ptr中的引用计数count也不能定义成静态成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。

weak_ptr

虽然shared_ptr非常好用,看似完美无缺,但是它在某个场景中却有致命的缺陷。

我们先定义一个节点类,并且将_next和_prev成员变量的类型改为shared_ptr类型。

struct Node
{
	A _val;
	a::shared_ptr<Node> _next;
	a::shared_ptr<Node> _prev;
    ~Node()
    {
        cout<<"~Node"<<endl;
    }
};

 接着我们再将sp1和sp2相互链接,然后运行程序观察结果。

int main()
{
	// 循环引用
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->_next = sp2;
	sp2->_prev = sp1;

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	return 0;
}

我们发现这时程序运行结束后两个结点都没有被释放,但如果去掉链接节点时的两句代码中的任意一句,那么这两个节点就都能够正确释放,根本原因就是因为这两句链接节点的代码导致了循环引用

出现循环引用的原因

当以new的方式申请到两个Node节点并交给两个智能指针管理后,这两个资源对应的引用计数都为1。接着我们将这两个节点链接起来后,资源1当中的next成员与sp2一同管理资源2,资源2中的prev成员与sp1一起管理资源1,此时这两个资源对应的引用计数都被加到了2。

当出了main函数的作用域后,sp1和sp2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1

根据上图可知_prev管着左边的节点,_next管着右边的节点。当右边的节点析构时,_prev才会析构,而右边的节点是当_next析构它才会析构,而_next析构需要左边的节点析构才能析构。这时就形成了一个死循环,最终导致资源无法被释放。

而如果链接节点时只进行一个链接操作,那么当sp1和sp2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,当这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都会被释放了。这就是为什么只进行一个连接操作时这两个节点就都能够正确释放的原因。

针对上述问题,C++11引入了weak_ptr。其中我们最需要注意的一点是weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。

weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数

我们将_next和_prev的类型改为weak_ptr 

struct Node
{
	A _val;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
    ~Node()
    {
        cout<<"~Node"<<endl;
    }
};

接着再执行上述代码就会发现两个节点能正确释放,并且根据use_count链接前后两次打印的内容发现weak_ptr不会增加对应资源的引用计数

weak_ptr的模拟实现

weak_ptr的实现步骤如下:

  • 提供一个无参的构造函数,比如new Node这句代码就会调用weak_ptr的无参的构造函数。
  • 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr管理的资源。
  • 支持用shared_ptr对象赋值给weak_ptr对象,赋值时获取shared_ptr管理的资源。
  • 对*和->运算符进行重载,使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;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/504592.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

P28—P31:变量

P28-变量的定义 什么是变量&#xff1f; 从本质上来说&#xff0c;变量就是一块内存空间&#xff0c;而这块内存空间有数据类型、名字、字面值。变量包含三部分&#xff1a;数据类型、名字、字面值&#xff08;数据&#xff09;变量是内存中存储的基本单元。 数据类型的作用&a…

C/C++游戏编程实例-飞翔的小鸟

飞翔的小鸟游戏设计 首先需要包含以下库&#xff1a; #include<stdio.h> #include<windows.h> #include<stdlib.h> //包含system #include<conio.h>设置窗口大小&#xff1a; #define WIDTH 50 #define HEIGHT 16设置鸟的结构&#xff1a; struct …

8LS Three-phase Synchronous 电机Motors MAMOT2-ENG 安装调试接线等说明 146页

8LS Three-phase Synchronous 电机Motors MAMOT2-ENG 安装调试接线等说明 146页

什么是服务雪崩?什么是服务限流?

服务雪崩效应&#xff1a;因服务提供者的不可用而导致服务调用者的不可用&#xff0c;并且这种情况不断的衍生方法&#xff0c;从而导致整个系统崩溃的过程&#xff0c;就是服务雪崩效应。 解决方式&#xff1a; 熔断机制&#xff1a;当一个服务挂了&#xff0c;被影响的服务要…

Django(二)-搭建第一个应用(1)

一、项目环境和结构 1、项目环境 2、项目结构 二、编写项目 1、创建模型 代码示例: import datetimefrom django.db import models from django.utils import timezone# Create your models here.class Question(models.Model):question_text models.CharField(max_length2…

图解分布式定时器从零实现 | go语言(一)

参考 https://zhuanlan.zhihu.com/p/600380258 https://xie.infoq.cn/article/aaa353c9df6641eb1b09e6f36 https://www.luozhiyun.com/archives/458 前言 在许多业务场景中,我们需要使用定时器来执行一些定期任务或操作。以下是一些常见的使用场景: 订单管理 当订单一直处于未…

数据结构——lesson12排序之归并排序

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

C++2D原创我的世界1.00.3版本上市!!!

我很郁闷&#xff0c;为什么就是整不了昼夜交替啊喂&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 虽然这看上去很简单&#xff0c;但做起来要我命&#xff01;&#xff01;&#xff01; 优化过后总共1312行&#xff0c…

Linux:内核源代码角度看文件和Socket

文章目录 文件和Socket 文件和Socket 在之前写的网络服务&#xff0c;它们的本质其实就是一个进程&#xff0c;而对于每一个打开的文件来说&#xff0c;都要有一个自己对应的文件描述符&#xff0c;其中会默认打开对应的012&#xff0c;作为标准输入标准输出标准错误&#xff…

数据结构——lesson13排序之计数排序

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

如何简化多个 if 的判断结构

多少算太多&#xff1f; 有些人认为数字就是一&#xff0c;你应该总是用至少一个三元运算符来代替任何单个 if 语句。我并不这样认为&#xff0c;但我想强调一些摆脱常见的 if/else 意大利面条代码的方法。 我相信很多开发人员很容易陷入 if/else 陷阱&#xff0c;不是因为其…

ThreadLocal的基本使用

一、ThreadLocal的介绍 ThreadLocal 是 Java 中的一个类&#xff0c;它提供了线程局部变量的功能。线程局部变量是指每个线程拥有自己独立的变量副本&#xff0c;这些变量在不同的线程中互不影响。ThreadLocal 提供了一种在多线程环境下&#xff0c;每个线程都可以独立访问自己…

PS从入门到精通视频各类教程整理全集,包含素材、作业等(4)

PS从入门到精通视频各类教程整理全集&#xff0c;包含素材、作业等 最新PS以及插件合集&#xff0c;可在我以往文章中找到 由于阿里云盘有分享次受限制和文件大小限制&#xff0c;今天先分享到这里&#xff0c;后续持续更新 PS人物数码照片处理技法视频教程 https://www.…

武汉星起航:一站式跨境电商服务引领者,专业高效助力客户出海

武汉星起航电子商务有限公司&#xff0c;坐落于华中地区的商业核心地带——湖北武汉&#xff0c;自公司成立以来&#xff0c;便以提供一站式跨境电商服务为核心发展&#xff0c;致力于为广大客户提供专业、高效、全面的出海解决方案。凭借5对1服务体系、ERP软件授权、中转仓服务…

二、分布式事务

目录 二、分布式事务2.1 什么是分布式事务2.2 分布式事务产生的背景2.3 分布式事务产生的场景2.4 分布式事务理论4.1 CAP理论4.2 Base理论 5、分布式事务的解决方案 二、分布式事务 2.1 什么是分布式事务 一组操作会产⽣多个数据库session会话 此时就会出现分布式事务 2.2 分…

游戏软件出现d3dcompiler_47.dll缺失怎么修复,亲测的六种有效方法推荐

D3DCompiler47.dll是DirectX SDK中的一个重要组件&#xff0c;它提供了将HLSL&#xff08;High-Level Shading Language&#xff09;着色器编译为可执行代码的功能。通过使用D3DCompiler47.dll&#xff0c;开发人员可以将复杂的着色器代码转换为可以在GPU上高效运行的机器代码&…

黑马点评项目笔记 II

基于Stream的消息队列 stream是一种数据类型&#xff0c;可以实现一个功能非常完善的消息队列 key&#xff1a;队列名称 nomkstream&#xff1a;如果队列不存在是否自动创建&#xff0c;默认创建 maxlen/minid&#xff1a;设置消息队列的最大消息数量 *|ID 唯一id&#xff1a;…

Vue系列-el挂载

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>el:挂载点</title> </head> <body&g…

作业 二维数组-定位问题

图形相似度 描述 给出两幅相同大小的黑白图像&#xff08;用0-1矩阵&#xff09;表示&#xff0c;求它们的相似度。 说明&#xff1a;若两幅图像在相同位置上的像素点颜色相同&#xff0c;则称它们在该位置具有相同的像素点。 两幅图像的相似度定义为相同像素点数占总像素点数…

P87 4.1 C++ FOR 与Delphi FOR 的区别

输出x, sin(x), cos(x), tan(x)的值。已知X0&#xff0c;10&#xff0c; 20&#xff0c;180。 我用Delphi编写了程序&#xff1a; 第10行出现 给FOR 循环变量赋值i错误。 C中是可以的&#xff0c; 详见&#xff1a;delphi循环的一个小知识_assignment to for-loop variable…