菜鸟笔记
提升您的技术认知

c 智能指针讲解及模拟实现-ag真人游戏

c 四种智能指针

1、裸指针中可能存在的问题

裸指针是指未经类封装的原生指针。在工程项目中,如果使用裸指针不规范或者书写代码逻辑时候不仔细,那么就有可能产生各种错误、异常现象。
(1)malloc出来的空间,如果没有及时释放,就会造成内存泄漏
(2)异常安全问题:如果malloc和free之间存在异常,那么发生异常时,就有可能无法释放空间,造成内存泄漏。这种问题称为“异常安全问题”。
(3)当要delete一个指针指向的空间时,不方便判断该指针指向的是一个数组还是一个单独的对象,所以使用“delete”还是"delete[]"不方便确定。
(4)无法判断一个不为nullptr的指针是否为悬挂指针。
(5)还有可能出现多次释放空一块空间的情况

面对这些问题,gc(垃圾回收机制)是可以有效解决的,但是c 并没有垃圾回收机制,而是提出了一种思想raii。

2、raii思想

rall (resource acquisition ls initialization)是c 之父bjarne stroustrup提出的,翻译为资源获取即初始化。使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字互斥量,文件句柄等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。

(1)raii原理

符合raii的资源一般要经历三个步骤:
获取资源——使用资源——销毁资源
其实在c 中,类对象就很好体现了raii这一原理。具体如下
获取资源(构造)——使用资源——销毁资源(析构)
我们都知道,当我们创建一个类对象的时候,编译器就会自动调用构造函数,当对象出了所在作用域,编译器就会自动调用该类的析构函数。无论是构造函数还是析构函数,都不需要我们自己手动调用,这样就避免了我们忘记初始化,忘记销毁对象的不好的事情发生。
这样看来,避免忘记析构是不是也对应着我们想要避免忘记free/delete指针

类对象符合raii的举例

class a
{
  
public:
	a()
	{
  
		cout << "construct the object" << endl;
	}
	~a()
	{
  
		cout << "destroy the object" << endl;
	}
};
void fun()
{
  
	//创建一个对象
	a a = a();
	//fun调用结束,a出了作用域,自动调用其析构函数
}
int main()
{
  
	fun();
	return 0;
}

代码运行结果

(2)整个rali过程总结为四个步骤:

a.设计一个类封装资源
b.在构造函数中初始化
c.在析构函数中执行销毁操作
d.使用时定义一个该类的对象

3、c 内存泄漏

(1)堆内存泄露

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生heap leak(堆内存泄露)。

(2)系统资源泄露

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

从比较简单的层面来看,智能指针是raii(resource acquisition is initialization,资源获取即初始化)机制对普通指针进行的一层封装。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。
c 中总共有四种智能指针:
auto_ptr
unique_ptr
shared_ptr
weak_ptr
其中,auto_ptr 在 c 11已被摒弃,在c 17中已经移除不可用。

总结:智能指针是将原生指针(裸指针)封装成类,通过构造、析构函数来实现资源的即使释放,避免内存泄漏和多次释放空间等非法行为发生(raii特性)。同时还需要重载operator*和operator->来使该类有指针的行为,能够像指针一样使用。

1、auto_ptr的使用

在c98中,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。

std::auto_ptr ap1(new int);
std::auto_ptr ap2(ap1);

2、auto_ptr的问题

(1)拷贝问题:编译器对auto_ptr默认生成的拷贝构造和复制重载会对指针进行”浅拷贝“,这个时候对指针进行delete时候就会对同一块空间delete多次,就会发生错误。

这个时候就有人说,那我就深拷贝。但事实是,我们需要的就是浅拷贝,想一下指针赋值的场景下,我们的目的就是让多个指针指向相同的空间,即指针内容相同。所以只能指针去赋值或者拷贝也要进行浅拷贝。

3、问题的解决

(1)c 98中对于auto_ptr的拷贝问题,ag真人游戏的解决方案是管理权转移
管理权转移:指针浅拷贝以后,将被赋值指针指向nullptr。

auto_ptr sp1 = new auto_ptr(new int);//管理权在sp1手中
auto_ptr sp2(sp1);//sp1拷贝构造sp2后,sp1中管理的指针指向nullptr了,此时sp2中指针指向sp1创建时开辟的空间。

<注>:这个时候,sp1已经悬空了,sp1指向空,如果这个时候再去对其中的指针解引用,就会触发空指针异常。

4、auto_ptr的模拟实现

template
class my_auto_ptr
{
  
private:
	t* _ptr;
public:
	//构造和析构实现raii特性
	my_auto_ptr(t* ptr)
		:_ptr(ptr)
	{
  }
	~my_auto_ptr()
	{
  
		if(_ptr)
		{
  
			delete _ptr;
			_ptr = nullptr;
		}
	}
	//*和->的重载实现指针行为的模拟
	t& operator*()
	{
  
		return *_ptr;
	}
	t* operator()
	{
  
		return _ptr;
	}
	//赋值 拷贝实现管理权转移
	my_auto_ptr(my_auto_ptr& my_ap)
	{
  
		_ptr = my_ap._ptr;
		my_ap._ptr = nullptr;
	}
	my_auto_ptr& operator=(my_auto_ptr& my_ap)
	{
  
		if(this != &my_ap)
		{
  
			delete _ptr;
			_ptr = my_ap._ptr;
			my_ap._ptr = nullptr;
		}
		return *this;
	}
};

1、unique_ptr的使用

禁止拷贝(复制重载 拷贝构造),这样就不存在auto_ptr中析构两次的问题。也就不需要管理权转移的方法。

std::unqiue_ptr up1(new int);

<注>:不能进行拷贝构造和赋值操作

2、unique_ptr的模拟实现

tempalte
class my_unique_ptr
{
  
public:
	my_unique_ptr(t* ptr)
		:_ptr(ptr)
	{
  }
	~my_unique_ptr()
	{
  
		if(_ptr != nullptr)
		{
  
			delete _ptr;
			_ptr = nullptr;
		}
	}
	//禁用拷贝
	my_unique_ptr(my_unique_ptr& my_up) = delete;
	my_unique_ptr& operator=(my_unique_ptr& my_up) = delete;
	
	t& operator*()
	{
  
		return *_ptr;
	}
	t* operator()
	{
  
		return _ptr;
	}
private:
	t* _ptr;
};

3、unique_ptr的问题

unique_ptr是一种十分暴力的方式,我直接禁用operator=和拷贝构造,这样就根本无法使用,从根本上解决了拷贝带来的问题。但是太暴力,毕竟有些情况下确实需要拷贝。所以c 库又采用了另一种机制引用计数
使用引用计数的只能指针叫做shared_ptr,将在后续继续介绍。

1、shared_ptr的使用

使用引用计数机制,统计指向同一块空间的shared_ptr的个数。增加一个,计数器就 1,减少一个计数器就-1。shared_ptr出作用域调用析构函数时,只有当计数器数值为0时,才回去调用delete去释放空间;否则只对计数器进行–操作。

#include 
#include 
using namespace std;
int main()
{
  
	shared_ptr sp1(new int);
	shared_ptr sp2(sp1);
	//use_count()可以统计出管理同一块空间的shared_ptr个数
	cout << sp2.use_count() << endl;
   return 0;
}

2、shared_ptr的模拟实现

template
class my_shared_ptr
{
  
	void addref()
	{
  
		  (*_count);
	}
	void releaseref()
	{
  
		if(--(*_count) == 0)
		{
  
			if(_ptr)
			{
  
				delete _ptr;
			}
			delete _count;
		}
	}
public:
	my_shared_ptr(t* ptr)
		:_ptr(ptr)
		,_count(new int(1))
	{
  }
	~my_shared_ptr()
	{
  
		releaseref()
	}
	my_shared_ptr(const my_shared_ptr& my_sp)
		:_ptr(my_sp._ptr)
		,_count(my_sp._count)
	{
  
		addref()
	}
	my_shared_ptr& operator=(const my_shared_ptr& my_sp)
	{
  
	//防止自己给自己赋值时候,不能使用
	//if(this != & my_sp)这种方式赋值
		if(_ptr != my_sp._ptr)
		{
  
			releaseref()
			_ptr = my_sp._ptr;
			_count = my_sp._count;
			addref()
		}
		return *this;
	}
	t& operator*()
	{
  
		return *_ptr;
	}
	t* operator->()
	{
  
		return _ptr;
	}
	size_t use_count() const
	{
  
		return *_count;
	}'
	const t* get() const
	{
  
		return _ptr;
	}
private:
	t* _ptr;
	int* _count;
};

引用计数需要注意的问题:
(1)int _count:
(2)static int _count;
(3)int* _count;

3、shared_ptr的问题

shared_ptr也是解决了指针使用的绝大部分问题,但是还是在有些场景下会出现问题。shared_ptr出现的问题时:循环引用,接下来具体解释一下什么叫循环引用。

(1)循环引用举例1

示例1

struct b;
struct a
{
  
	shared_ptr _b;
};
struct b
{
  
	shared_ptr

死锁的循环等待问题)。

(2)循环引用举例2

在双向链表中,定义_next和_prev两个指针,指向前后节点

struct listnode
{
  
	shared_ptr _next;
	shared_ptr _prev;
	int _val;
};
int main()
{
  
	shared_ptr node1(new listnode);
	shared_ptr node2(new listnode);
	node1->_next = node2;
	node2->_prev = node1;
}

和情况1相同。node1空间的释放依赖于node2空间释放,node2空间释放依赖于node1空间释放。形成循环引用现象,导致两部分空间都不释放。

1、weak_ptr的使用

weak_ptr并不像前三种智能指针一样单独使用。weak_ptr是为了解决循环引用问题而产生的,所以weak_ptr是配合shared_ptr来使用的。不参与资源的管理,只是来访问内容。
weak_ptr可以使用shared_ptr来构造或者将shared_ptr赋值给weak_ptr
weak_ptr不会改变引用计数

上述两种示例的修改

struct b;
struct a
{
  
	weak_ptr _b;
};
struct b
{
  
	weak_ptr
网站地图