【C++修炼之路】32.智能指针

news2024/11/15 23:54:18

在这里插入图片描述
每一个不曾起舞的日子都是对生命的辜负

智能指针

  • 一.为什么需要智能指针?
  • 二.智能指针解决new抛异常的示例
  • 三.智能指针的使用及其原理
    • 3.1 RAII
    • 3.2 SmartPtr存在的问题
    • 3.3 std::auto_ptr
    • 3.4 std::unique_ptr
    • 3.5 std::shared_ptr
      • std::shared_ptr的基本设计
      • std::shared_ptr的线程安全问题
      • std::shared_ptr的定制删除器
    • 3.6 std::weak_ptr
      • std::shared_ptr的循环引用问题
      • std::weak_ptr解决循环引用问题
  • C++11和boost中智能指针的关系

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

对于异常,捕获到后会进行跳转,如下:

#include<iostream>
using namespace std;

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

	return a / b;
}

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	int* p2 = new int[20];

	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		throw;
	}

	delete[] p1;
	delete[] p2;
}

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

	return 0;
}

对于上述代码,如果p1在new时异常,那么就会被main函数中的catch捕获,直接跳到最外面去,由于没有new成功就没有需要释放的,div抛异常,就会被Func中的catch捕获。那p1成功,p2抛异常,p2申请堆空间产生的异常就会直接被main中的catch捕获。而此时程序继续从main里向下运行,但是由于new是在堆里申请内存,即便跳转出函数,申请空间也不会随着函数栈帧的销毁而还给OS,所以就产生了内存泄漏。因此,为了避免这种情况的发生,就需要让p2申请内存失败之后不直接跳出函数,或者说起码等到p1释放空间再跳转出去,这样就给了p1释放空间的间隙避免了内存泄漏。

那么,就需要将在Func函数中继续嵌套一层try-catch,与p2进行匹配,如下代码所示:

#include<iostream>
using namespace std;

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

	return a / b;
}

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	int* p2 = nullptr;
	try
	{
		p2 = new int[20];
		try
		{
			cout << div() << endl;
		}
		catch (...)
		{
			delete[] p1;
			delete[] p2;
			throw;
		}
	}
	catch (...)
	{
		//...
	}
	delete[] p1;
	delete[] p2;
}

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

	return 0;
}

即再套一层try catch来对应p2的抛异常的情况,而在//…中就需要根据具体情况进行处理,如上就需要将p1内存释放,此外还要考虑div发生异常重新抛出的情况也需要在此进行处理。如果再new若干次,就需要嵌套若干个try catch,这很不现实。可见new本身的抛异常不能通过这样的方式去解决。为了防止抛异常时堆空间未能及时释放从而造成内存泄漏,引入了智能指针。

二.智能指针解决new抛异常的示例

#include<iostream>
using namespace std;

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

	return a / b;
}

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete[] _ptr;
		cout << _ptr << endl;
	}
private:
	T* _ptr;
};


void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	SmartPtr<int> sp1(p1);
	int* p2 = new int[20];
	SmartPtr<int> sp2(p2);

	cout << div() << endl;
}

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

	return 0;
}

image-20230630164909555

学过类和对象都知道析构函数会在栈帧释放前自动执行,也就是说对于上述代码,即便p2申请堆空间失败,跳到main中去,在跳出时Func的栈帧会被销毁,sp1的析构函数也会在栈帧销毁前执行,而sp1的析构函数中因含有释放p1的delete操作,此时就相当于自动的将sp1的空间释放,避免了内存泄漏问题。

那么,直接通过构造函数构造,省略掉p1和p2,会简单很多,但是Smartptr作为我们自己定义的类,不能像p1和p2一样解引用,因此如果想直接进行解引用可以考虑解引用的运算符重载:

#include<iostream>
using namespace std;

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

	return a / b;
}
template<class T>
class SmartPtr
{
public:
    //RAII
    //保存资源
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
    //释放资源
	~SmartPtr()
	{
		delete[] _ptr;
		cout << _ptr << endl;
	}
    //像指针一样
    T& operator*()
    {
        return *_ptr;
    }

    T& operator[](size_t pos)
    {
        return _ptr[pos];
    }
private:
	T* _ptr;
};
void Func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);
    *sp1 = 10;
    sp1[0]--;
    cout << *sp1 << endl;
	cout << div() << endl;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

image-20230701155156109

这样,就与指针没有什么区别,还能防止因为异常造成的内存泄漏的问题。也可以看出运算符重载解决的问题很多。

上述的Smartptr大致可以分成两个部分:

  • RAII:此Smartptr构造函数和析构函数的设计思想
  • 重载:像指针一样。

RAII是什么?

三.智能指针的使用及其原理

3.1 RAII

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

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

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

3.2 SmartPtr存在的问题

对于智能指针,RAII+像指针一样 的思想使其具有很实用的功能,但在拷贝上却存在一定的问题:

int main()
{
   
    SmartPtr<int> sp1(new int);
    SmartPtr<int> sp2 = sp1;
    return 0;
}

image-20230701163908409

sp2的值与sp1相同,当函数结束,就会调用析构,由于指向同一个位置,导致被析构两次,就会出现错误。

很明显,可以采用计数器的方式解决,深拷贝不符合指针赋值的初衷。

智能指针在C++库中已有现成的可以使用,比如auto_ptr, weak_ptr, share_ptr, unique_Ptr等,这些针对上述拷贝的问题都有不同的方法解决:

3.3 std::auto_ptr

std::auto_ptr文档

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

auto_ptr的实现原理:管理权转移的思想–>对象悬空

image-20230701164812021

image-20230701164828659

因为为了防止拷贝后析构两次的错误,auto_ptr把管理权转移了,所以如果在经过这样的操作之后,将ap1再进行赋值就会出现空指针的错误。

简易版的auto_ptr的实现步骤如下:

  1. 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。

  2. 对*和->运算符进行重载,使auto_ptr对象具有指针一样的行为。

  3. 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。

  4. 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。

#pragma once
#include<iostream>
using namespace std;
namespace cfy
{
	template<class T>
	class auto_ptr
	{
	public:
		//保存资源
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//释放资源
		~auto_ptr()
		{
			delete[] _ptr;
			cout << _ptr << endl;
		}

		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;
		}

		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T& operator[](size_t pos)
		{
			return _ptr[pos];
		}
	private:
		T* _ptr;
	};
}

int main()
{
	cfy::auto_ptr<int> ap1(new int);
	cfy::auto_ptr<int> ap2 = ap1;
	(*ap1)++;
	(*ap2)++;
	return 0;
}

image-20230701172228566

由此可见,auto_ptr是不完备且不好用的智能指针。(很多公司明确要求不能使用)

3.4 std::unique_ptr

unique_ptr实现原理:防拷贝

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。比如:

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

但防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。

简易版的unique_ptr的实现步骤如下:

  1. 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  2. *->运算符进行重载,使unique_ptr对象具有指针一样的行为。
  3. 用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式在这两个函数后面加上=delete,防止外部调用。

代码如下:

template<class T>
class unique_ptr
{
    public:
    //RAII
    unique_ptr(T* ptr = nullptr)
        :_ptr(ptr)
        {}
    ~unique_ptr()
    {
        if (_ptr != nullptr)
        {
            cout << "delete: " << _ptr << endl;
            delete _ptr;
            _ptr = nullptr;
        }
    }
    //可以像指针一样使用
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
    //防拷贝
    unique_ptr(unique_ptr<T>& up) = delete;
    unique_ptr& operator=(unique_ptr<T>& up) = delete;
    private:
    T* _ptr;//管理的资源
};

3.5 std::shared_ptr

std::shared_ptr的基本设计

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

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

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

比如:

int main()
{
	cfy::shared_ptr<int> sp1(new int(1));
	cfy::shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	cout << sp1.use_count() << endl; //2

	cfy::shared_ptr<int> sp3(new int(1));
	cfy::shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	cout << sp3.use_count() << endl; //2
	return 0;
}

说明一下: use_count成员函数,用于获取当前对象管理的资源对应的引用计数。

shared_ptr的模拟实现

简易版的shared_ptr的实现步骤如下:

  1. 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数。
  2. 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理这个资源。
  3. 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++。
  4. 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数–(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。
  5. 在析构函数中,将管理资源对应的引用计数–,如果减为0则需要将该资源释放。
  6. *->运算符进行重载,使用shared_ptr对象具有指针一样的行为。

代码如下:

#pragma once
#include<iostream>
using namespace std;
namespace cfy
{
	template<class T>
	class shared_ptr
	{
	public:
		//RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
			delete _pcount;
			_pcount = nullptr;
		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		shared_ptr& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//管理同一块空间的对象之间无需进行赋值操作
			{
				if (--(*_pcount) == 0)//将管理的资源对应的引用计数--
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;       //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				(*_pcount)++;         //新增一个对象来管理该资源,引用计数++
			}
			return *this;
		}

		//获取引用计数
		int use_count()
		{
			return *_pcount;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;	 //管理的资源
		int* _pcount;//管理的资源对应的引用计数
	};
}

为什么引用计数需要存放在堆区?

首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。

如下图:

image-20230707165537339

其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。

如下图:

image-20230707170605840

而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。

这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。

如下图:

image-20230707170958003

但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。

std::shared_ptr的线程安全问题

shared_ptr的线程安全问题

当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题。

比如下面代码中用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁。比如:

void func(cfy::shared_ptr<int>& sp, size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		cfy::shared_ptr<int> copy(sp);
	}
}
int main()
{
	cfy::shared_ptr<int> p(new int(0));
	const size_t n = 1000;
	thread t1(func, p, n);
	thread t2(func, p, n);
	t1.join();
	t2.join();
	cout << p.use_count() << endl;//预期:1
	return 0;
}

在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作。

加锁解决线程安全问题

要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例。

  • 在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建。
  • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数交给当前对象管理之外,还需要将对应的互斥锁也交给当前对象。
  • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,由于互斥锁也是在堆区创建的,因此还需要将对应的互斥锁进行释放。
  • 为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成AddRef函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成ReleaseRef函数,这样就只需要对AddRef和ReleaseRef函数进行加锁保护即可。

代码如下:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
namespace cfy
{
	template<class T>
	class shared_ptr
	{
	private:
		//++引用计数
		void AddRef()
		{
			_pmutex->lock();
			(*_pcount)++;
			_pmutex->unlock();
		}
		//--引用计数
		void ReleaseRef()
		{
			_pmutex->lock();
			bool flag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr != nullptr)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
				flag = true;
			}
			_pmutex->unlock();
			if (flag == true)
			{
				delete _pmutex;
			}
		
		}
	public:
		//RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}
		~shared_ptr()
		{
			ReleaseRef();
		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			AddRef();
		}

		shared_ptr& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//管理同一块空间的对象之间无需进行赋值操作
			{
				ReleaseRef();
				_ptr = sp._ptr;       //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				AddRef();       //新增一个对象来管理该资源,引用计数++
			}
			return *this;
		}

		//获取引用计数
		int use_count()
		{
			return *_pcount;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;	 //管理的资源
		int* _pcount;//管理的资源对应的引用计数
		mutex* _pmutex;//管理的资源对应的互斥锁
	};
}

说明一下:

  • 在ReleaseRef函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后释放需要释放互斥锁资源。
  • shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证。

std::shared_ptr的定制删除器

定制删除器的用法

当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是以new[]的方式申请到的空间,或管理的是一个文件指针。比如:

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10]);   //error
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error

	return 0;
}

这时当智能指针对象的生命周期结束时,再以delete的方式释放管理的资源就会导致程序崩溃,因为以new[]的方式申请到的内存空间必须以delete[]的方式进行释放,而文件指针必须通过调用fclose函数进行释放。

这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:

template <class U, class D>
shared_ptr (U* p, D del);

参数说明:

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。

因此当智能指针管理的资源不是以new的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器。比如:

template<class T>
struct DelArr
{
	void operator()(const T* ptr)
	{
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	});

	return 0;
}

定制删除器的模拟实现

定制删除器的实现问题:

  • C++标准库中实现shared_ptr时是分成了很多个类的,因此C++标准库中可以将删除器的类型设置为构造函数的模板参数,然后将删除器的类型在各个类之间进行传递。
  • 但我们是直接用一个类来模拟实现shared_ptr的,因此不能将删除器的类型设置为构造函数的模板参数。因为删除器不是在构造函数中调用的,而是需要在ReleaseRef函数中进行调用,因此势必需要用一个成员变量将删除器保存下来,而在定义这个成员变量时就需要指定删除器的类型,因此这里模拟实现的时候不能将删除器的类型设置为构造函数的模板参数。
  • 要在当前模拟实现的shared_ptr的基础上支持定制删除器,就只能给shared_ptr类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。最好在设置一个默认的删除器,如果用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源。

代码如下:

namespace cfy
{
    //默认的删除器
	template<class T>
	struct Delete
	{
		void operator()(const T* ptr)
		{
			delete ptr;
		}
	};
	template<class T, class D = Delete<T>>
	class shared_ptr
	{
	private:
		void ReleaseRef()
		{
			_pmutex->lock();
			bool flag = false;
			if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
			{
				if (_ptr != nullptr)
				{
					cout << "delete: " << _ptr << endl;
					_del(_ptr); //使用定制删除器释放资源
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
				flag = true;
			}
			_pmutex->unlock();
			if (flag == true)
			{
				delete _pmutex;
			}
		}
		//...
	public:
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmutex(new mutex)
			, _del(del)
		{}
		//...
	private:
		T* _ptr;        //管理的资源
		int* _pcount;   //管理的资源对应的引用计数
		mutex* _pmutex; //管理的资源对应的互斥锁
		D _del;         //管理的资源对应的删除器
	};
}

这时我们模拟实现的shared_ptr就支持定制删除器了,但是使用起来没有C++标准库中的那么方便。

  • 如果传入的删除器是一个仿函数,那么需要在构造shared_ptr对象时指明仿函数的类型。
  • 如果传入的删除器是一个lambda表达式就更麻烦了,因为lambda表达式的类型不太容易获取。这里可以将lambda表达式的类型指明为一个包装器类型,让编译器传参时自行进行推演,也可以先用auto接收lambda表达式,然后再用decltype来声明删除器的类型。
template<class T>
struct DelArr
{
	void operator()(const T* ptr)
	{
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	//仿函数示例
	cfy::shared_ptr<ListNode, DelArr<ListNode>> sp1(new ListNode[10], DelArr<ListNode>());

	//lambda示例1
	cfy::shared_ptr<FILE, function<void(FILE*)>> sp2(fopen("test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
		});

	//lambda示例2
	auto f = [](FILE* ptr) {
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	};
	cfy::shared_ptr<FILE, decltype(f)> sp3(fopen("test.cpp", "r"), f);

	return 0;
}

3.6 std::weak_ptr

std::shared_ptr的循环引用问题

循环引用问题

shared_ptr的循环引用问题在一些特定的场景下才会产生。比如定义如下的结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放。

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

现在以new的方式在堆上构建两个结点,并将这两个结点连接起来,在程序的最后以delete的方式释放这两个结点。比如:

int main()
{
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;

	node1->_next = node2;
	node2->_prev = node1;
	//...
	delete node1;
	delete node2;
	return 0;
}

上述程序是没有问题的,两个结点都能够正确释放。为了防止程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点分别交给两个shared_ptr对象进行管理,这时为了让连接节点时的赋值操作能够执行,就需要把ListNode类中的next和prev成员变量的类型也改为shared_ptr类型。比如:

struct ListNode
{
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
	node2->_prev = node1;
	//...

	return 0;
}

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

当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。如下图:

image-20230707193704201

将这两个结点连接起来后,资源1当中的next成员与node2一同管理资源2,资源2中的prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。如下图:

image-20230707193812369

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

image-20230707193856239

循环引用导致资源未被释放的原因:

  • 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
  • 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。

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

std::weak_ptr解决循环引用问题

解决循环引用问题

weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。

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

将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。比如:

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	//...
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。

weak_ptr的模拟实现

简易版的weak_ptr的实现步骤如下:

  1. 提供一个无参的构造函数,比如刚才new ListNode时就会调用weak_ptr的无参的构造函数。
  2. 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
  3. 支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源。
  4. 对*和->运算符进行重载,使weak_ptr对象具有指针一样的行为。

代码如下:

namespace cfy
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		weak_ptr& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr; //管理的资源
	};
}

说明一下: shared_ptr还会提供一个get函数,用于获取其管理的资源。

C++11和boost中智能指针的关系

  1. C++98中产生了第一个智能指针auto_ptr。
  2. C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。
  3. C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。

说明一下:boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方案。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

海明码(汉明码)原理及其计算方法

海明码&#xff08;汉明码&#xff09;是一种利用奇偶性检测和纠正错误的编码方法。在传输和储存数据时&#xff0c;可能会发生传输错误或数据损坏。海明码通过在数据中添加冗余位来检测错误并进行纠正&#xff0c;提高了传输和存储数据的可靠性。 基本原理&#xff1a;将原始…

【数据结构复习】汉诺塔:快速回忆汉诺塔问题

题目 题解 众所周知这是一个递归问题&#xff08; 我们只需要注意两点&#xff1a;①什么时候退出递归 ②怎么从n-1推出n ①什么时候退出递归&#xff1a;很明显啦&#xff0c;n1的时候&#xff0c;我们直接把A位置的唯一盘子移到C上&#xff0c;大功告成。 ②怎么从n-1推出n…

Vision Pro销售策略曝光,面罩/头带/屈光镜片加大零售难度

彭博社Mark Gurman再次发布了关于苹果Vision Pro的销售策略&#xff0c;以及零售方面的难题。 一、销售计划和策略 1&#xff0c;2024年初先在美国部分门店销售&#xff0c;仅线下购买&#xff0c;线上暂不开放。购买方式是先线上预约&#xff08;可能要提供面部扫描图、眼镜…

Python 利用opencv识别某象旋转验证码,识别率达95%以上

本期介绍某象旋转验证码识别,识别的思想其实与上篇文章识别滑动还原验证码相似,也是借鉴过来的,但是旋转验证码更加复杂,实现起来稍加困难,下面来看一下,原始数据集和识别之后数据集。 原始数据集: 将圆图旋转成功之后的数据集: 注意:我这里仅仅抓取了几十张作为数据…

c++使用回调函数

前言 回调函数的使用场景&#xff0c;当内部逻辑不知道用户的类型时&#xff0c;让用户自己提供对应数据类型的函数。 代码 #include<iostream> using namespace std;/// <summary> /// 万能打印函数。用户调用 /// </summary> /// <param name"d…

操作系统第六章之进程同步

操作系统第六章之进程同步 参考资料来源竞争同步临界区问题标准解决方案Peterson solution 硬件同步TestAndSet指令Swap介绍 信号量SemaphoresCritical Section of n n n Processes 信号量的实现Semaphore Implementation死锁和饥饿 经典同步问题有限缓冲问题生产者进程的结构…

笔记2(DCL指令)

用处&#xff1a;用来管理数据库 用户&#xff0c;控制数据库的访问 权限 DCL – 用户管理 1.查询用户 use mysql; select * from user; 2.创建用户 create user 用户名主机名 identified by 密码; 例如&#xff1a; create user xiaojielocalhost identified by 4620; 创建…

可重入函数、volatile关键字、SIGCHLD信号

目录 一、可重入函数 二、volatile关键字 三、SIGCHLD信号 一、可重入函数 以一个链表头插为例子 main函数调用insert函数像一个链表head中插入节点node1&#xff0c;插入操作分为两步&#xff0c;刚做完第一步的时候因为硬件中断使进程切换到内核&#xff0c;再次回到用户…

Visual studio 2015下载安装以及缺包提示的处理方法

最近要加入的比赛团队需要用到Visual studio 2015&#xff0c;百度后找到很多资源&#xff0c;自己也转到了百度网盘。中英文都有&#xff0c;需要的可以下载。 链接&#xff1a;https://pan.baidu.com/s/12gpVwXfQxfdkXub-IwhWFw?pwds325 提取码&#xff1a;s325 --来自百…

linux安装交叉编译环境

1、安装系统自带的交叉编译环境 sudo apt install gcc-arm-linux-gnueabihf sudo apt install g-arm-linux-gnueabihf 2、选择特定交叉编译器版本 版本路径&#xff1a;Linaro Releases 当前选择版本&#xff1a;4.9 、64位的版本

ArcGIS栅格影像数据处理

ArcGIS栅格影像数据处理 文章目录 ArcGIS栅格影像数据处理1. 栅格影像数据坐标系转换2. 栅格数据16bit转8bit3. 栅格数据波段变换参考链接 1. 栅格影像数据坐标系转换 点击【ArcToolbox】>【数据管理工具】>【投影和变换】>【栅格】>【投影栅格】。 2. 栅格数据16…

Jetpack compose——深入了解recomposition的工作原理

一、compose怎么 实现响应式编程的 Jetpack Compose 是 Android 的现代 UI 工具包&#xff0c;它使用 Kotlin 语言的声明式 UI 模式来简化 UI 开发。在这种模式中&#xff0c;你只需描述 UI 应该如何根据应用的状态进行显示&#xff0c;而 Compose 会在状态发生变化时自动更新…

【Linux】oh-my-zsh终端配置

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍oh-my-zsh终端配置。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&am…

Spring Boot发送QQ邮件

Spring Boot发送QQ邮件 1. 创建Spring Boot项目2. 引入发邮件的starter3. 必要配置4. 编写邮件内容5. 测试其他&#xff1a; Spring Boot简单引入一个包就能轻松发邮件&#xff0c;仅需5分钟就能实现 1. 创建Spring Boot项目 略 2. 引入发邮件的starter <dependency>&l…

学习在外部Python脚本中运行Houdini的Python接口(hou模块)

0. 目标 学习在外部Python脚本&#xff08;而非Houdini编辑器内部&#xff09;使用 hou 。 主要参考Houdini官方文档 Command-Line Scripting 中的【Accessing hou from a Regular Python Shell】部分。我将要点记录在下&#xff1a; 1. 将Houdini的dll加入搜索路径 为了能…

FPGA实验五:信号发生器设计

目录 一、实验目的 二、设计要求 三、实验代码 1.代码原理分析 2.代码设计思路 3.IP核的设计与配置 四、实验结果及分析 1、引脚锁定 2、仿真波形及分析 &#xff08;1&#xff09;关于波形一些指标的介绍 &#xff08;2&#xff09;对波形转换功能的验证 &#xf…

【LeetCode】225. 用队列实现栈

225. 用队列实现栈&#xff08;简单&#xff09; 思路 要使用一个队列来实现栈的功能。 具体来说&#xff0c;实现了以下几个方法&#xff1a; push(int x)&#xff1a;将元素 x 入栈&#xff0c;即将元素 x 插入到队列的末尾。pop()&#xff1a;弹出栈顶元素&#xff0c;即将队…

空天遥感守护自然资源底线,擦亮生态底色

上期介绍了航天宏图采用“空天地”一体化监测监管手段&#xff0c;打造基于“SAR卫星耕地变化遥感智能解译”、 基于“遥感大模型的耕地分类技术”等智慧耕地保护解决方案&#xff0c;本篇文章主要讲述“遥感如何助力自然保护地监测与保护”和“如何助力山水林田湖草沙生态修复…

Spanner: Google的全球分布级数据库----论文摘要

Spanner中一个新奇的time api揭示了时钟的不确定性。该api及其实现对于支持外部一致性&#xff08;外部观察一致性&#xff09;以及一系列强力的特性至关重要&#xff0c;这些特性包括&#xff1a;对过去版本数据的无阻塞读&#xff08;对于历史数据的读不加锁&#xff0c;且不…

密码学入门——DES与AES

文章目录 参考书目一、编码与异或1.1 编码1.2 异或 二、DES与三重DES三、AES 参考书目 图解密码技术&#xff0c;第三版 一、编码与异或 1.1 编码 计算机的操作对象并不是文字&#xff0c;而是由0和1排列而成的比特序列。无论是文字、图像、声音、视频还是程序&#xff0c;…