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

C++智能指针讲解及模拟实现

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->来使该类有指针的行为,能够像指针一样使用。

第一种智能指针:auto_ptr

1、auto_ptr的使用

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

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

2、auto_ptr的问题

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

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

3、问题的解决

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

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

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

4、auto_ptr的模拟实现

template<class T>
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<T>& my_ap)
	{
  
		_ptr = my_ap._ptr;
		my_ap._ptr = nullptr;
	}
	my_auto_ptr<T>& operator=(my_auto_ptr<T>& my_ap)
	{
  
		if(this != &my_ap)
		{
  
			delete _ptr;
			_ptr = my_ap._ptr;
			my_ap._ptr = nullptr;
		}
		return *this;
	}
};

第二种智能指针unique_ptr

1、unique_ptr的使用

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

std::unqiue_ptr<int> up1(new int);

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

2、unique_ptr的模拟实现

tempalte<class T>
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<T>& my_up) = delete;
	my_unique_ptr<T>& operator=(my_unique_ptr<T>& my_up) = delete;
	
	T& operator*()
	{
  
		return *_ptr;
	}
	T* operator()
	{
  
		return _ptr;
	}
private:
	T* _ptr;
};

3、unique_ptr的问题

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

第三种智能指针shared_ptr

1、shared_ptr的使用

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

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

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

2、shared_ptr的模拟实现

template<class T>
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<T>& my_sp)
		:_ptr(my_sp._ptr)
		,_count(my_sp._count)
	{
  
		AddRef()
	}
	my_shared_ptr<T>& operator=(const my_shared_ptr<T>& 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> _b;
};
struct B
{
  
	shared_ptr<A> _a;
};
int main()
{
  
	shared_ptr<A> asp(new A);
	shared_ptr<B> bsp(new B);
	asp->_b = bsp;
	bsp->_a = asp;
}


此时我们可以看到,_a和asp两个shared_ptr都指向A对象的空间;_b和bsp两个shared_ptr都指向B对象的空间。
所以_a\asp的引用计数都为2;_b\bsp的引用计数也为2。
我们接着看,当出作用域调用析构函数时,asp和bsp析构,调用析构函数,因为最初引用计数为2,所以调用析构函数只将引用计数-1,所以此时A对象、B对象空间的引用计数为1,空间不会delete释放。A对象B对象空间不释放,就不会调用A、B的析构函数,既不会释放成员变量_a和_b指向的空间,所以引用计数一直保持为1,所以就会造成内存泄漏(堆泄露)。
A、B两个对象中,A中成员变量指向B、B中成员变量指向A,导致双方的空间都依赖于对方是否释放。这种就形成了”循环引用“(类似于死锁的循环等待问题)。

(2)循环引用举例2

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

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

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

第四种智能指针weak_ptr

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> _b;
};
struct B
{
  
	weak_ptr<A> _a;
};
int main()
{
  
	shared_ptr<A> asp(new A);
	shared_ptr<B> bsp(new B);
	asp->_b = bsp;
	bsp->_a = asp;
}
struct ListNode
{
  
	weak_pre<ListNode> _next;
	weak_ptr<ListNode> _prev;
	int _val;
};
int main()
{
  
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
}

2、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(const weak_ptr<T>& wp)
		:_ptr(wp._ptr)
	{
  }
	weak_ptr<T>& operator(const shared_ptr<T>& sp)
	{
  
		_pre = sp.get();
		return *this;
	}
	T& operator*()
	{
  
		return *_ptr;
	}
	T* operator->()
	{
  
		return _ptr;
	}
private:
	T* _ptr;
};

shared_ptr中定制删除器

库中实现的智能指针,统一的操作就是对指针进行delete,但是如果交付给智能指针管理的指针指向一个new出来的数组(要delete [],而不是delete),或者是FILE*的指针(要close,而不是delete)等情况,就会出现错误。

默认的删除方式只适合new出来的指向单个对象的指针。

所以我们就要根据智能指针中不同的内容来定制不同的析构方式(定制删除器)。

在库中,D del就是删除器。在构造时候 传入一个可调用对象 作为删除器

举例

template<class T>
struct DeleteArray
{
  
	void operator()(const T* ptr)
	{
  
		delete[] ptr;
	}
};
void fun()
{
  
	//函数对象
	std::shared_ptr<int,DeleteArray<int>> arrSptr(new int[10],DeleteArray<int>());
	//lambda表达式(使用包装器或者decltype)
	std::shared_ptr<FILE*,std::function<void(FILE*)>> fileSptr(fopen("test.txt","w"),[](FILE* ptr){
  fclose(ptr);});
}

shared_ptr中增加定制删除器的功能

template<class T, class D>
class my_shared_ptr
{
  
	void AddRef()
	{
  
		++(*_count);
	}
	void ReleaseRef()
	{
  
		if(--(*_count) == 0)
		{
  
			if(_ptr)
			{
  
				_del(_ptr);
			}
			delete _count;
		}
	}
public:
	my_shared_ptr(T* ptr,D del)
		:_ptr(ptr)
		,_count(new int(1))
		,_del(del)
	{
  }
	~my_shared_ptr()
	{
  
		ReleaseRef()
	}
	my_shared_ptr(const my_shared_ptr<T>& my_sp)
		:_ptr(my_sp._ptr)
		,_count(my_sp._count)
	{
  
		AddRef()
	}
	my_shared_ptr<T>& operator=(const my_shared_ptr<T>& 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;
	D _del;
};