C++进阶--C++11智能指针

news2024/11/17 2:47:09

目录

  • 一、智能指针的使用及原理
    • 1.1 什么是智能指针
    • 1.2 智能指针的发展历史
    • 1.3 智能指针的使用
      • 1.3.1 内存泄漏问题
      • 1.3.2 利用异常的重新捕获解决
      • 1.3.3 利用智能指针解决
    • 1.4 智能指针的原理
      • 1.4.1 需要考虑的问题
      • 1.4.2 为什么要解决智能指针对象的拷贝问题
  • 二、C++中的智能指针
    • 2.1 std::auto_ptr
      • 2.1.1 管理权转移
      • 2.1.2 auto_ptr的模拟实现
    • 2.2 std::unique_ptr
      • 2.2.1 unique_ptr的防拷贝
      • 2.2.2 unique_ptr的模拟实现
    • 2.3 std::shared_ptr
      • 2.3.1 std::shared_ptr的引用计数
      • 2.3.2 std::shared_ptr的模拟实现
      • 2.3.3 引用计数需要存放堆区
      • 2.3.4 std::shared_ptr的线程安全问题
      • 2.3.5 std::shared_ptr的定制删除器
      • 2.3.6 定制删除器的模拟实现
    • 2.4 std::weak_ptr
      • 2.4.1 std::shared_ptr的循环引用问题
      • 2.4.2 std::weak_ptr解决循环引用问题
      • 2.4.3 std::weak_ptr的模拟实现
  • 三、C++11和boost中智能指针的关系

一、智能指针的使用及原理

1.1 什么是智能指针

  智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。
  动态分配的资源,交给一个类对象去管理,当类对象生命周期结束时,自动调用析构函数释放资源。
  RAII是resource acquisition is initialization的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。

这样做有两大好处

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

1.2 智能指针的发展历史

  1、C++98中产生了第一个智能指针auto_ptr
  2、C++boost给出了更加实用的scope_ptr和shared_ptr和weak_ptr
  3、C++11引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的是boost中的scoped_ptr。并且这些智能指针的实现是参照boost中实现的。

1.3 智能指针的使用

1.3.1 内存泄漏问题

  内存泄漏是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。

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

  执行上述代码时,如果用户输入的除数为0,那么div函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func函数中申请的内存资源没有得到释放。

1.3.2 利用异常的重新捕获解决

  对于这种情况,我们可以在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。

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

1.3.3 利用智能指针解决

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);
	//...
	cout << div() << endl;
	//...
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

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

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

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

1.4 智能指针的原理

1.4.1 需要考虑的问题

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

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

1.4.2 为什么要解决智能指针对象的拷贝问题

  对于当前实现的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;
}

原因

  • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次。
  • 编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。

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

二、C++中的智能指针

2.1 std::auto_ptr

2.1.1 管理权转移

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

int main()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap2 = 10;
	//*ap1 = 20; //error

	std::auto_ptr<int> ap3(new int(1));
	std::auto_ptr<int> ap4(new int(2));
	ap3 = ap4;
	return 0;
}

  但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题。

2.1.2 auto_ptr的模拟实现

namespace zl
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

		//管理权转移
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

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

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

	private:
		T* _ptr;
	};


	void test_auto()
	{
		auto_ptr<int> ap1(new int(1));
		auto_ptr<int> ap2(ap1);

		*ap1 = 1;    //管理权转移以后导致ap1悬空,不能访问(存在被拷贝对象悬空的问题)
		*ap2 = 1;
	}
}

2.2 std::unique_ptr

2.2.1 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;
}

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

2.2.2 unique_ptr的模拟实现

namespace zl
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

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

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

		//C++11思路:语法直接支持
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

		//防拷贝
		//拷贝构造和赋值是默认成员函数,我们不写会自动生成,所以我们不需写
		//C++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以再加一条,声明为私有

	private:
		//unique_ptr(const unique_ptr<T>& up);
		//unique_ptr<T>& operator=(const unique_ptr<T>& up);

	private:
		T* _ptr;
		int* _pcount;
	};


	void test_unique()
	{
		unique_ptr<int> up1(new int(1));
		unique_ptr<int> up2(up1);
	}
}

2.3 std::shared_ptr

2.3.1 std::shared_ptr的引用计数

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

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

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

2.3.2 std::shared_ptr的模拟实现

namespace zl
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_pmtx(new mutex)
		{}

		template<class D>
		shared_ptr(T* ptr ,D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new mutex)
			,_del(del)
		{}

		~shared_ptr()
		{
			Release();
		}

		void Release()
		{
			_pmtx->lock();

			bool deleteFlag = false;

			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					//cout << "delete:" << _ptr << endl;
					//delete _ptr;

					//删除器进行删除
					_del(_ptr);
				}
				
				delete _pcount;

				deleteFlag=true;

				//delete _pmtx;       //如何解决?
			}
			_pmtx->unlock();

			if (deleteFlag)
			{
				delete _pmtx;
			}
		}

		void AddCount()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}

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

		//sp1=sp4
		//sp1=sp1
		//sp1=sp2
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (--(*_pcount) == 0)
			{
				/*cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;*/

				Release();
			}

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp._pmtx;

			AddCount();

			return *this;
		}

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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}

	private:
		T* _ptr;
		//static int* _count;    //静态这种方案是不行的,因为属于所有对象,多个对象可能多个资源,每个资源应该配对一个引用计数
		int* _pcount;
		mutex* _pmtx;

		//包装器
		function<void(T*)> _del = [](T* ptr) {
			cout << "lambda delete:" << ptr << endl;
			delete ptr;
		};
	};
}

2.3.3 引用计数需要存放堆区

在这里插入图片描述
  首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。
  其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。
  而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。
  这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
  但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。

2.3.4 std::shared_ptr的线程安全问题

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

namespace zl
{
	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))
			, _pmutex(new mutex)
		{}
		~shared_ptr()
		{
			ReleaseRef();
		}
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmutex(sp._pmutex)
		{
			AddRef();
		}
		shared_ptr& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
			{
				ReleaseRef();         //将管理的资源对应的引用计数--
				_ptr = sp._ptr;       //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				_pmutex = sp._pmutex; //获取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只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证。

2.3.5 std::shared_ptr的定制删除器

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

2.3.6 定制删除器的模拟实现

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

	//lambda示例1
	zl::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);
	};
	zl::shared_ptr<FILE, decltype(f)> sp3(fopen("test.cpp", "r"), f);

	return 0;
}

2.4 std::weak_ptr

2.4.1 std::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。
在这里插入图片描述
  将这两个结点连接起来后,资源1当中的next成员与node2一同管理资源2,资源2中的prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。
  当出了main函数的作用域后,node1和node2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1。

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

  • 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
  • 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。
      而如果连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因。

2.4.2 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不会增加管理的资源对应的引用计数。

2.4.3 std::weak_ptr的模拟实现

namespace zl
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

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

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

		T* get() 
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

三、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/1438500.html

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

相关文章

锁(二)队列同步器AQS

一、队列同步器AQS 1、定义 用来构建锁或者其他同步组件的基础框架&#xff0c;它使用了一个int成员变量表示同步状态&#xff0c;通过内置的FIFO队列来完成资源获取线程的排队工作。是实现锁的关键。 2、实现 同步器的设计是基于模板方法模式的&#xff0c;也就是说&#…

Excel——分类汇总

1.一级分类汇总 Q&#xff1a;请根据各销售地区统计销售额总数。 第一步&#xff1a;排序&#xff0c;我们需要根据销售地区汇总数据&#xff0c;我们就要对【销售地区】的内容进行排序。点击【销售地区】列中任意一个单元格&#xff0c;选择【数据】——【排序】&#xff0c…

Linux自有服务—防火墙和计划任务

Linux常用自有服务有NTP时间同步服务、firewalld防火墙服务和crond计划任务服务&#xff0c;NTP在上一篇中讲过&#xff0c;这次主要来说一下防火墙firewalld与计划任务的相关内容。如下。 一、Linux中防火墙firewalld 1、什么是防火墙 防火墙&#xff1a;防范一些网络攻击…

找不到concrt140.dll无法继续执行程序的多种解决方法

concrt140.dll文件的丢失可能会对Windows操作系统产生一系列显著的影响。作为系统运行过程中不可或缺的一部分&#xff0c;concrt140.dll是Microsoft Visual C Redistributable Package中包含的重要动态链接库文件&#xff0c;它为应用程序提供了关键的并发运行时支持。一旦该文…

Spring Boot 001 环境配置以及初始化项目

知识储备 后端&#xff1a;JavaSE, SSM&#xff08;SpringSpringMVCMyBatis&#xff09; 前端&#xff1a;HTML, CSS, Javascript 环境准备 JDK17下载 Java Downloads | Oracle 安装方式 JDK17在Windows安装以及环境变量配置&#xff08;超详细的教程&#xff09;_jdk17安装…

网络原理HTTP/HTTPS(1)

文章目录 HTTP抓包工具FIddler**HTTP请求****HTTP响应** 认识URLURL encode认识"方法"(method)1.GET方法使用Fiddler观察GET请求 2.POST方法使⽤Fiddler观察POST⽅法 经典面试题&#xff1a;GET和POST有啥区别请求报头&#xff08;header&#xff09;HostContent-Len…

Java+SpringBoot:构建稳定高效的计算机基础教学平台

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

2021年通信工程师初级 实务 真题

文章目录 一、第1章 现代通信网概述&#xff0c;通信网的定义。第10章 通信业务&#xff0c;普遍服务原则10.2.4 通信行业的发展趋势&#xff08;六化&#xff09; 二、第2章 传输网SDH帧结构SDH线路保护倒换&#xff0c;“11 保护”和“1:1保护”波长值λc/f&#xff0c;中心频…

YOLO部署实战(2):使用OpenCV优化视频转图片流程并设置帧数

在计算机视觉和图像处理领域&#xff0c;OpenCV是一个强大的开源库&#xff0c;它为处理图像和视频提供了丰富的工具和功能。本文将介绍如何使用OpenCV将视频文件转换为一系列图片&#xff0c;并演示如何通过设置转换的帧数来优化这一过程。 1 Win10配置OpenCV 在Windows操作…

常用ES技巧二

文章目录 一、Object.entries()和Object.fromEntries()1.1、Object.entries()1.2、Object.fromEntries() 二、Symbol类型和Symbol属性三、WeakMap和WeakSet四、Promise.allSettled()五、BigInt六、Array.of和Array.from七、.at和.flat八、总结九、最后 一、Object.entries()和O…

解决计算机“缺失ffmpeg.dll”报错?修复ffmpeg.dll文件方案

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“ffmpeg.dll丢失”。ffmpeg.dll是FFmpeg多媒体框架中的一个重要组件&#xff0c;它负责处理音频和视频的编解码。当打开某些软件时&#xff0c;如果系统找不到该文件&#xff0c;就会出现这…

C++入门学习(二十五)do-while循环

do { // 代码块&#xff0c;至少会执行一次 } while (条件); 对比一下while和do-while循环&#xff1a; 因为while循环先判断条件&#xff0c;所以数字10直接就没有进入for循环里&#xff0c;卡在了判断条件这一步&#xff0c;所以就没有输出数据&#xff1b; do-while循环是…

2023年全国职业院校技能大赛软件测试赛题第2套

2023年全国职业院校技能大赛 软件测试赛题第2套 赛项名称&#xff1a; 软件测试 英文名称&#xff1a; Software Testing 赛项编号&#xff1a; GZ034 归属产业&#xff1a; 电子与信息大类 …

【数据结构与算法】堆 / 堆排序 / TopK问题(Heap)

文章目录 1.堆2.C语言实现堆2.1 堆结构与基本操作2.2 其它辅助操作2.3 堆的基本操作2.3.1 插入2.3.2 删除 3. 堆排序4. TopK5. 所有代码 1.堆 堆总是一棵完全二叉树&#xff0c;而完全二叉树更适合使用**顺序结构&#xff08;数组&#xff09;**存储&#xff0c;完全二叉树前h…

蓝桥杯省赛无忧 课件92 行列式

01 什么是行列式 02 行列式的性质 03 高斯消元求行列式

linux之wsl2安装远程桌面

0. 安装后的效果 1. wsl中打开terminal并安装库 sudo apt-get purge xrdp sudo apt install -y xrdp sudo apt install -y xfce4 sudo apt install -y xfce4-goodies 2.优化显示 sudo sed -i s/max_bpp32/#max_bpp32\nmax_bpp128/g /etc/xrdp/xrdp.ini sudo sed -i s/xserverbp…

Linux下的多线程

前面学习了进程、文件等概念&#xff0c;接下里为大家引入线程的概念 多线程 线程是什么&#xff1f;为什么要有线程&#xff1f;线程的优缺点Linux线程操作线程创建线程等待线程终止线程分离 线程间的私有和共享数据理解线程库和线程id深刻理解Linux多线程&#xff08;重点&a…

【Linux】gdb调试与make/makefile工具

目录 导读 1. make/Makefile 1.1 引入 1.2 概念 1.3 语法规则 1.4 示例 2. Linux调试器-gdb 2.1 引入 2.2 概念 2.3 使用 导读 我们在上次讲了Linux编辑器gcc\g的使用&#xff0c;今天我们就来进一步的学习如何调试&#xff0c;以及makefile这个强大的工具。 1. mak…

Hadoop3.x基础(4)- Yarn

来源&#xff1a;B站尚硅谷 目录 Yarn资源调度器Yarn基础架构Yarn工作机制作业提交全过程Yarn调度器和调度算法先进先出调度器&#xff08;FIFO&#xff09;容量调度器&#xff08;Capacity Scheduler&#xff09;公平调度器&#xff08;Fair Scheduler&#xff09; Yarn常用命…

《数电》理论笔记-第2章-组合逻辑电路

一&#xff0c;集成门电路 1TTL门电路 TTL门电路中双极型三极管构成,它的特点是速度快、抗静电能力强集成度低、功耗大&#xff0c; 目前广泛应用于中、小规模集成电路中。 TTL门电路有 74 (商用) 和 54 (军用) 两大系列&#xff0c;每个系列中又有若干子系列。 2 CMOS门电路 …