[C++] 智能指针

Posted 哦哦呵呵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++] 智能指针相关的知识,希望对你有一定的参考价值。

1. 为什么需要智能指针?

  我们在使用指针时一般都需要向内存申请一块内存空间进行使用,但是如果忘记对该块空间进行释放,就造成了内存泄漏
  并且如果在申请内存的使用时间,程序中有异常处理,并且抛出了异常,那么这个过程中没有进行空间的释放,同样造成了内存泄漏。

内存泄漏的危害

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死

  所以为了避免上述情况的出现,引入了只能指针,使用智能指针进行资源的管理,就可以避免上述问题。

2. RAII (资源获取及初始化)

  RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

  巧妙利用编译器会自动调用构造函数以及析构函数的特性,来完成资源的自动释放。实际就是定义一个指针类,使用其构造函数与析构函数进行资源的管理。

构造函数: 在对象构造时获取资源,控制对资源的访问使之在对象的生命周期内始终保持有效。
析构函数: 释放资源

优点

  • 不需要显式地释放资源
  • 采用这种方式,对象所需的资源在其生命周期内始终保持有效

2.1 RAII方式的原理

注意: 只能指针的类中,构造函数中不能申请空间,而是由用户在外部自己申请空间,传递到只能指针的类中去管理该资源。
  智能指针只是将资源管理起来,找一个合适的时机去释放。

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr(){
		if(_ptr)
			delete _ptr;
	}
	// 让该类对象具有指针类似的操作就可以了
	T& operator*(){
		return *ptr;
	}
	//  -> 只能指针指向对象或者结构体的这些场景中
	T* operator->(){
		return ptr;
	}
private:
	T* _ptr; // 采用类将指针管理起来
};
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	// 将tmp指针委托给了sp对象,给tmp指针找了一个可怕的女朋友!天天管着你,直到你go die^^
	SmartPtr<int> sp(tmp);
	// _MergeSort(a, 0, n - 1, tmp);
	
	// 这里假设处理了一些其他逻辑
	vector<int> v(1000000000, 10);
	// ...
}

  如上只是实现了资源的管理,但是该类不能使用指针独有的操作,所以需要重载指针的一些操作符。

2.2 重大问题

  上述代码中没有提供拷贝构造函数,那么如果进行拷贝时,会调用编译器提供的默认拷贝构造函数。智能指针不能申请资源,只能替用户管理资源,因此不能使用深拷贝来代替浅拷贝

如何解决: C++标准库中提供了,最优解,下面会介绍到。

2.3 智能指针原理

所以智能指针的原理: RAII + operator*() / operator->() + 解决浅拷贝问题

RAII: 能够保证资源可以被自动释放
operator()/operator->()*: 可以保证对象能够按照指针的方式来运行
解决浅拷贝的方式:可能保证资源不被释放多次而引起代码崩溃的问题

3. C++98 auto_ptr(不要用)

RAII + operator*() / operator->() + 解决浅拷贝问题

3.1 解决浅拷贝的方式一

auto_ptr: 解决浅拷贝使用的是资源转移

当发生拷贝或赋值时,将被拷贝对象中的资源转移给新对象,然后让被拷贝对象与资源断开联系

但是同时只能有一个指针被使用,另外一个指针就没有指向了

// 解决浅拷贝方式:资源转移
// auto_ptr<int>  ap2(ap1)
auto_ptr(auto_ptr<T>& ap)
	: _ptr(ap._ptr)
{
	ap._ptr = nullptr;
}

// ap1 = ap2;
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
	if (this != &ap)
	{
		// 此处需要将ap中的资源转移给this
		// 但是不能直接转移,因为this可能已经管理资源了,否则就会造成资源泄漏
		if (_ptr)
		{
			delete _ptr;
		}

		// ap就可以将其资源转移给this
		_ptr = ap._ptr;
		ap._ptr = nullptr;   // 让ap与之前管理的资源断开联系,因为ap中的资源已经转移给this了
	}

	return *this;
}

3.2 解决浅拷贝的方式二

资源管理权限转移

如果发生了拷贝或者赋值,不断开原来指针与资源块的联系,而是将资源的释放权限交给新指针,但是会造成野指针问题,因为新指针一旦析构,就会释放资源,而老指针还指向那块空间

4. C++11 unique_ptr

RAII + operator*() / operator->() + 解决浅拷贝问题

浅拷贝解决方式: 资源独占

禁止拷贝,一份资源只能让一个对象管理,对象间不能共享资源。

使用场景: 只能应用于资源被一个对象管理,并且资源不会共享的场景中。

如何实现
  让编译器不生成默认的拷贝构造以及赋值运算符的重载。

C++11中: delete关键字,可以用其修饰默认的成员函数,表明编译器不会生成修饰的成员函数。
C++98中: 将拷贝构造函数以及赋值运算符重载只声明不定义,并且把访问权限设置为私有。

只声明不定义,没有将权限设置为private,仍然可以在类外进行定义。

4.1 管理的资源多样如何释放

  管理的资源可能是从堆上申请的空间,也可能是文件指针等等

  定制删除器: 此处的释放方式不能写死,应该按照资源的不同类型对应不同的释放方式。

定义只能指针类时,模板参数加上资源释放的方式,用户自定义资源释放的类,自己指定释放资源的方法。在初始化智能指针时,就该把资源的释放方式确定下来,在初始化模板参数时就传入释放资源的类。

4.2 模拟实现

// 负责释放new资源
template<class T>
class DFDef
{
public:
	void operator()(T*& ptr)
	{
		if (ptr)
		{
			delete ptr;
			ptr = nullptr;
		}
	}
};

// 负责:malloc的资源的释放
template<class T>
class Free
{
public:
	void operator()(T*& ptr)
	{
		if (ptr)
		{
			free(ptr);
			ptr = nullptr;
		}
	}
};

// 关闭文件指针
class FClose
{
public:
	void operator()(FILE* & ptr)
	{
		if (ptr)
		{
			fclose(ptr);
			ptr = nullptr;
		}
	}
};


namespace test
{
	// T: 资源中所放数据的类型
	// DF: 资源的释放方式
	// 定制删除器
	template<class T, class DF = DFDef<T>>
	class unique_ptr
	{
	public:
		/
		// RAII
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
			{
				// 问题:_ptr管理的资源:可能是从堆上申请的内存空间、文件指针、malloc空间...
				// delete _ptr; // 注意:此处的释放资源的方式不能写死了,应该按照资源类型不同找对应的方式释放
				// malloc--->free
				// new---->delete
				// fopen--->fclose关闭
				DF df;
				df(_ptr);
			}
		}
		// C++11: 可以让编译器不生成默认的拷贝构造以及赋值运算符重载---delete
		// 在C++11当中,delete关键字的功能扩展:释放new申请的空间  用其修饰默认成员函数,表明:编译器不会生成了
		 unique_ptr(const unique_ptr<T,DF>&) = delete;  // 表明:编译器不会生成默认的拷贝构造函数
		 unique_ptr<T,DF>& operator=(const unique_ptr<T,DF>&) = delete;// 表明:编译器不会生成默认的赋值运算符重载

	private:
		unique_ptr(const unique_ptr<T,DF>&);
		unique_ptr<T,DF>& operator=(const unique_ptr<T,DF>&);

	private:
		T* _ptr;
	};

	// 用户在外部可以对方法进行定义---在unique_ptr的类中如果将该权限设置为private的
	//template<class T>
	//unique_ptr<T>::unique_ptr(const unique_ptr<T>& up)
	//{}
}

5. C++11 shared_ptr

多个对象之间可以共享资源

5.1 如何解决浅拷贝

  采用引用计数的方式解决浅拷贝

引用计数: 实际是一个整型的空间,记录着资源的对象的个数。释放资源时,判断有没有其它对象在使用资源,没有的话就释放掉资源。

5.2 实现原理

拷贝时:

新对象要与之前的对象共享所有资源,并且计数器进行++

释放时:

1.先检测资源是否还存在
2.存在的话,先对计数器–,检测计数器当前是否为0

  • =0: 当前对象是最后使用该资源的对象,需要将资源以及计数器进行释放。
  • !=0: 还有其他对象在使用资源,当前对象不需要释放,如果释放了就会造成其它对象变成野指针。

5.3 多线程使用时的问题

  上述操作在单线程下不会出现问题,但是在多线程中有多个执行流。
  多个线程共享一份资源,多个线程在结束时需要将其管理的资源释放掉,多个线程可能会同时释放同一份空间,计数器的操作不是原子性的,所以多个线程判断当前资源都还有人在使用,导致资源没有释放,而引起内存泄漏。

   智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的

解决方案

1.加锁,保证计数器的操作是安全的
2.把计数器改为原子操作

C++11中的share_ptr,自身是线程安全的,标准库中是按照原子类型变量实现的 atomic_int

5.4 缺陷- 循环引用

struct ListNode
{
	shared_ptr<ListNode> next;
	shared_ptr<ListNode> prev;
	//weak_ptr<ListNode> next;
	//weak_ptr<ListNode> prev;
	int data;

	ListNode(int x)
		//: data(x)
		: next(nullptr)
		, prev(nullptr)
		, data(x)
	{
		cout << "ListNode(int):" << this << endl;
	}

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

void TestLoopRef()
{
	shared_ptr<ListNode> sp1(new ListNode(10));
	shared_ptr<ListNode> sp2(new ListNode(20));

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

	sp1->next = sp2;
	sp2->prev = sp1;

	cout << sp1.use_count() << endl;    // 2
	cout << sp2.use_count() << endl;    // 2
}

上述代码导致的后果就是内存泄漏,释放资源时没有调用析构函数。

什么是循环引用

函数中sp1 ,sp2之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针sp1 ,sp2析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类种里面的shared_ptr 指针; 改为weak_ptr 指针; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放

5.5 weak_ptr

weak_ptr的作用,配合shared_ptr解决循环引用问题。weak_ptr不能独立管理资源
不控制对象生命周期,指向一个shared_ptr管理的对象,只是提供了对管理对象的一个访问手段。weak_ptr设计的目的为了配合shared_ptr解决循环引用,导致无法释放内存空间的问题。weak_ptr的构造和析构不会造成引用计数的增加或者减少

原理

实际上shared_ptr维护了两块计数器,一份用来记录shared_ptr的使用次数,一份用来记录weakptr的使用次数。weak_ptr指向资源,shared_ptr的use计数器不懂,weak计数器+1

销毁时

当一个资源被shared_ptr类型的对象共享时,给use+1
当一个资源被weak_ptr类型的对象时,给weak+1
销毁时: 先给use-1,确认资源能够释放,再给weak-1,确认资源能释放

5.5 代码实现

#include <mutex>

namespace test
{
	// shared_ptr: 自身才是安全的---加锁:为了保证shared_ptr自身的安全性
	template<class T, class DF = DFDef<T>>
	class shared_ptr
	{
	public:
		//
		// RAII
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pCount(nullptr)
			, _pMutex(new mutex)
		{
			if (_ptr)
			{
				// 此时只有当前刚刚创建好的1个对象在使用该份资源
				_pCount = new int(1);
			}
		}

		~shared_ptr()
		{
			Release();
		}

		/
		// 具有指针类似的行为
		T& operator*()
		{
			return *_ptr;
		}

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

		T* Get()
		{
			return _ptr;
		}

		//
		// 用户可能需要获取引用计数
		int use_count()const
		{
			return *_pCount;
		}

		///
		// 解决浅拷贝方式:采用引用计数
		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pCount(sp._pCount)
			, _pMutex(sp._pMutex)
		{
			AddRef();
		}

		shared_ptr<T, DF>& operator=(const shared_ptr<T, DF>& sp)
		{
			if (this != &sp)
			{
				// 在和sp共享之前,this先要将之前的状态清空
				Release();

				// this就可以和sp共享资源以及计数了
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMutex = sp._pMutex;
				AddRef();
			}

			return *this;
		}

	private:
		void AddRef()
		{
			if (nullptr == _ptr)
				return;

			_pMutex->lock();
			++(*_pCount);
			_pMutex->unlock();
		}

		void Release()
		{
			if (nullptr == _ptr)
				return;

			bool isDelete = false;
			_pMutex->lock();

			if (_ptr && 0 == --(*_pCount))
			{
				// delete _ptr;
				DF df;
				df(_ptr);
				delete _pCount;
				_pCount = nullptr;
				isDelete = true;
			}

			_pMutex->unlock();

			if (isDelete)
			{
				delete _pMutex;
			}
		}
	private:
		T* _ptr;        // 用来接收资源的
		int* _pCount;   // 指向了引用计数的空间---记录的是使用资源的对象的个数
		mutex* _pMutex; // 目的:保证对引用计数的操作是安全的
	};
}

以上是关于[C++] 智能指针的主要内容,如果未能解决你的问题,请参考以下文章

C++智能指针(3.30)

指向外部托管(例如:Python)资源的 C++ 智能指针?

c++智能指针介绍_再补充

C++中的智能指针

Android系统的智能指针(轻量级指针强指针和弱指针)的实现原理分析

c++使用裸指针与智能指针返回数组详解