C++ 智能指针

C++ 智能指针

为什么要引入智能指针?

我们先来看一段代码:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;//p1抛异常,直接跳catch,没有问题
	cout << div() << endl; // div抛异常,调到catch,p1无法释放,资源泄露
	delete p1;
}
int main()
{
	try {
		Func();
	} catch (exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

new空间也有可能会抛出异常,对于p1如果抛出异常:没有问题,可以不管,直接到最外面去了。

而如果用户输入的除数为0,那么div函数就会抛出异常,跳到主函数的catch块中执行,但是别忘了,此时Func()中的申请的内存资源还没有释放!

对于这种情况,我们可以进行异常的重新捕获。

在func函数中对div函数中的抛出的异常重新捕获,将申请的内存进行释放,然后在将异常重新捕获:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;//p1抛异常,直接跳catch,没有问题
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		throw;
	}
	delete p1;
}
int main()
{
	try {
		Func();
	} catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

但是如果申请的不是上面的一块空间,而是更多呢?这时候就麻烦了,需要套很多,所以这时候智能指针就登场了,可以解决这个问题。

智能指针的概念及原理

概念

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

智能指针是一个类,在对象构造时调用构造函数获取资源,在对象生命周期内,保证资源不被释放,在对象生命周期结束时,编译器自动调用析构函数来释放资源。这就相当于,将管理资源的责任移交给了对象,这样即使程序抛出异常也不存在内存泄漏,因为捕获异常往往跳出函数体,执行流会离开对象的作用域,对象生命周期结束,编译器自动调用析构函数释放了资源。

采用智能指针管理资源,有如下优点:

  • 将资源管理的责任转移给智能指针对象,不用显示地释放资源,杜绝了异常安全问题。
  • 保证对象管理的资源在其生命周期内有效。

原理

智能指针,顾名思义,不能仅仅是用于资源管理,还应当具有指针的一般功能。因此,需要重载operator*、operator->函数(见下面的代码),用于访问指针指向的资源。

注意:C++标准库中的智能指针均不重载operator[]函数。

智能指针原理总结:

  • RAII,管理资源,对象创建时申请资源,对象生命周期结束时释放资源。
  • 具有像指针一样的行为:operator*、operator->。

我们可以自己实现一个最简单的智能指针:

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;
	}
	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
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);
	int* p2 = new int;
	SmartPtr<int> sp2(p2);

	cout << div() << endl;
}
int main()
{
	try {
		func();
	} catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

上面代码将申请到的内存交给了SmartPtr对象管理:

  • 在构造SmartPtr对象时,自动调用构造函数,将传入的需要管理的内存保存起来。

  • 在析构SmartPtr对象时,自动调用析构函数,将管理的内存空间进行释放。

  • SmartPtr还可以与普通指针一样使用,需对*和->以及[]进行运算符重载。

通过SmartPtr对象,无论程序是正常执行结束,还是因为某些中途原因进行返回,或者抛出异常等开始所面临的困境,只要SmartPtr对象的生命周期结束就会自动调用对应的析构函数,不会造成内存泄漏,完成资源释放。

智能指针对象拷贝问题

对于我们上面所实现的SmartPtr,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或者一个SmartPtr对象赋值给另一个SmartPtr对象,最终结果会导致程序崩溃:

void test()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1);//拷贝构造
	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4 = sp3;//赋值
}

编译器默认生成的拷贝构造函数对内置类型完成浅拷贝(值拷贝),sp1拷贝给sp2后,两个管理同一块空间,当sp1和sp2析构就会让这块内存空间释放两次。同理,编译器默认生成的赋值函数(operator=)对内置类型完成浅拷贝,把sp4赋值给sp3后,sp4与sp3都是管理原来sp3的空间,会析构两次,同时,原先sp4管理的内存没有释放。

在这里插入图片描述

单纯的浅拷贝会导致空间多次释放,因为根据智能指针解决该问题方式不同,所以有很多版本的智能指针。

我们要求智能指针具有一般的指针行为,因此,我们也就需要智能指针支持拷贝。但是,智能指针中的成员涉及到执行动态申请资源的指针,按照一般要求,应当进行深拷贝。

但是如果我们进行深拷贝,就会让两个智能指针指向不同的空间,但是我们所希望的是两个指针共同管理一块资源,因此我们就是要浅拷贝(值拷贝)。

但是值拷贝会存在对同一块空间多次释放的问题,对此,C++标准库中的智能指针auto_ptr和shared_ptr分别采用了管理权转移和引用计数的方法来解决问题,但一般会通过引用计数解决多次释放的问题。

智能指针拷贝问题总结:

  1. 即使涉及到动态申请内存,智能指针的拷贝也不应为深拷贝,应当是浅拷贝。
  2. 采用管理权转移(auto_ptr)或引用计数(shared_ptr)来解决同一块空间多次释放的问题。
  3. 一般都使用引用计数来解决多次释放问题,auto_ptr大部分情况下不使用。

C++ 中的智能指针

auto_ptr

auto_ptr是C++98的。

auto_ptr采用管理权转移的方法进行赋值和拷贝构造,保证了一个资源只有一个对象对其进行管理,这时候一个资源就不会被多个释放。

假设原先有一个auto_ptr对象p1,要通过p1构造p2,当拷贝构造完成后,用于拷贝构造传参的对象p1中管理资源的指针会被更改为nullptr,赋值也一样,假设p2=p1,p1中资源的管理权会转移给p2,p2原本的资源会被释放。

在这里插入图片描述

auto_ptr 的简单实现:

template <class T>
class auto_ptr
{
public:
	// 构造函数
	auto_ptr(T *ptr = nullptr)
		: _ptr(ptr)
	{
	}

	// 拷贝构造函数
	auto_ptr(auto_ptr<T> &ap)
		: _ptr(ap._ptr)
	{
		ap._ptr = nullptr; // 管理权转移
	}

	// 赋值函数
	auto_ptr<T> &operator=(auto_ptr<T> &ap)
	{
		if (_ptr != ap._ptr) // 自赋值检验
		{
			delete _ptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}

		return *this;
	}

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

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

	// 析构函数
	~auto_ptr()
	{
		delete _ptr;
	}

private:
	T *_ptr;
};

资源的管理权转移意味该对象不能在对原来管理的资源进行访问了,如果进行访问,会导致程序崩溃,很容易出现问题。采用管理权转移的方法进行智能指针拷贝是一种极不负责任的行为,auto_ptr已经被很多公司明令禁止使用,一般项目中也极少使用auto_ptr。

unique_ptr

unique_ptr是C++11中的智能指针,unique_ptr来的更直接:直接将拷贝构造和赋值禁止,保证资源不会被多次释放。

在这里插入图片描述

防止拷贝也不是解决问题的好办法。

unique_ptr的模拟实现:

  • 构造函数中获取资源,在析构函数中释放资源。对*和->运算符进行重载,使unique_ptr对象具有指针一样的行为。

  • C++98的方式是将拷贝构造函数和拷贝赋值函数声明为私有

  • C++11的方式就直接在这两个函数后面加上=delete,防止外部进行调用

template <class T>
class unique_ptr
{
public:
	// 构造函数
	unique_ptr(T *ptr = nullptr)
		: _ptr(ptr)
	{
	}

	// 使用delete关键字强行禁止拷贝构造函数和赋值函数的生成
	unique_ptr(const unique_ptr<T> &up) = delete;
	unique_ptr<T> &operator=(const unique_ptr<T> &up) = delete;

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

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

	// 析构函数
	~unique_ptr()
	{
		delete _ptr;
	}

private:
	T *_ptr;
};

shared_ptr

shared_ptr是C++11标准库中新纳入的智能指针,它通过引用计数的方法,较好的解决了拷贝和赋值的问题,是实际项目中最常用的智能指针。

每个被管理的资源有有一个对应的引用计数,这个引用计数记录当前有多少对象在管理这块资源。

为什么shared_ptr的引用计数是一个指针?
shared_ptr的引用计数定义成指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储对应的引用计数,如果其他对象也要管理这块资源,那么除了将这个资源给它,引用计数也要给它。此时管理同一个资源的多个对象访问到的就是同一个引用计数了,管理不同资源的对象访问到的就是不同的引用计数,符合我们的要求。

shared_ptr的引用计数可不可以是一个int?
shared_ptr的引用计数可不能直接定义成int类型的成员变量,如果是int类型那么每个shared_ptr对象都有自己的pcount成员变量,而当多个对象管理同一块资源时,这几个对象应该是用同一个引用计数!

shared_ptr的引用计数可不可以是一个静态变量?
shared_ptr的引用计数不能定义成静态的成员变量,如果是静态成员变量,那么是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象都是用同一个引用计数!

当一个资源的引用计数为0时那么就说明已经没有对象在管理这块资源了,这时候就可以进行释放了。

引用计数的方式能够支持多个对象一起管理一个资源,也就支持智能指针的拷贝,只有当资源的引用计数减为0时才会释放,保证了同一个资源不会被多次释放。

shared_ptr的常用接口:

接口函数功能
shared_ptr(T* ptr = nullptr, Del del = Delete())构造函数,del为定制删除器,是一个仿函数对象,用于不同情况下的资源释放操作
shared_ptr(shared_ptr& sp)拷贝构造函数
shared_ptr& operator=(shared_ptr& sp)赋值运算符重载函数
T& operator*()解引用操作符重载函数
T* operator->()成员访问操作符重载函数
T* get()获取shared_ptr内部管理资源的指针
long int use_count()获取引用计数(当前智能指针管理的资源被多少智能指针共同管理)
bool unique()判断当前智能指针管理的资源是否只有它本身在管理(引用计数是否为1)
  • shared_ptr内部有一个成员变量long int* _pcount,它指向一块存储引用计数的空间,当进行拷贝构造时,引用计数+1,即:++(*_pcount)。
  • 进行赋值操作(sp2 = sp1)时,首先应当检查自赋值,如果是自赋值直接返回*this即可。如果不是自赋值,那么首先将sp2的引用计数-1,如果sp2的引用计数-1后变为了0,那么就释放sp2的资源,然后赋予sp2管理sp1管理的资源的权限,sp2和sp1共用一个引用计数,引用计数+1。
  • 调用析构函数时,先让引用计数-1,如果此时引用计数变为0,就释放资源。

在这里插入图片描述

shared_ptr的模拟实现:

template <class T, class Del = Delete<T>>
class shared_ptr
{
public:
	// 构造函数
	shared_ptr(T *ptr = nullptr)
		: _ptr(ptr), _pcount(new long int(1))
	{
	}

	// 拷贝构造函数
	shared_ptr(shared_ptr<T> &sp)
		: _ptr(sp._ptr), _pcount(sp._pcount)
	{
		++(*_pcount); // 引用计数+1
	}

	// 赋值函数
	shared_ptr<T> &operator=(shared_ptr<T> &sp)
	{
		if (_ptr == sp._ptr) // 自赋值检查
		{
			return *this;
		}

		// this的引用计数-1,并判断是否需要释放资源
		if (--(*_pcount) == 0)
		{
			Del del;
			del(_ptr);
			delete _pcount;
		}

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);

		return *this;
	}

	// 指针获取函数
	T *get()
	{
		return _ptr;
	}

	// 引用计数获取函数
	long int use_count()
	{
		return *_pcount;
	}

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

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

	bool unique()
	{
		return *_pcount == 1;
	}

	// 析构函数
	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			Del del;
			del(_ptr);
			delete _pcount;
		}
	}

private:
	T *_ptr;		   // 指向动态申请空间的指针
	long int *_pcount; // 引用计数
};

小细节:引用计数需要存放在堆区。

shared_ptr线程安全问题

我们上面模拟实现的shared_ptr其实还存在着线程安全的问题,管理同一个资源的多个对象共享引用计数,多个线程可能会同时对同一个个引用计数进行加或减,而自增或自减都不是原子操作,所以需要通过加锁对引用计数进行保护。

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

void test_shared_ptr()
{
	int n = 1000;
	shared_ptr<int> sp1(new int(1));

	thread t1([&]()
			  {
			for (int i = 0; i < n; i++)
			{
				hwc::shared_ptr<int> sp2(sp1);
			} });

	thread t2([&]()
			  {
			for (int i = 0; i < n; i++)
			{
				hwc::shared_ptr<int> sp3(sp1);
			} });

	t1.join();
	t2.join();
	cout << sp1.use_count() << endl;
}

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

但是实际上每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作。

加锁解决shared_ptr线程安全问题:

通过加锁让引用计数的++、–操作变成原子操作,对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数封装。

这里只使用加锁,shared_ptr加锁版本:

template <class T>
class shared_ptr
{
public:
	// RAII
	// 保存资源
	shared_ptr(T *ptr)
		: _ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
	{
	}

	// 释放资源
	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T> &sp)
		: _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx)
	{
		_pmtx->lock(); // t1,t2
		++(*_pcount);
		_pmtx->unlock();
	}

	void Release()
	{
		bool flag = false;
		_pmtx->lock();
		if (--(*_pcount) == 0)
		{
			delete _pcount;
			delete _ptr;
			flag = true;
		}
		_pmtx->unlock();
		if (flag == true)
			delete _pmtx;
	}

	// sp1 = sp1;
	// sp1 = sp2;//sp2如果是sp1的拷贝呢?
	shared_ptr<T> &operator=(const shared_ptr<T> &sp)
	{
		if (_ptr != sp._ptr) // 资源地址不一样
		{
			Release();
			_pcount = sp._pcount;
			_ptr = sp._ptr;
			_pmtx = sp._pmtx;

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

		return *this;
	}

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

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

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

	int use_count()
	{
		return *_pcount;
	}

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

在shared_ptr类中新增加互斥锁成员变量,让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的就是不同的互斥锁,所以互斥锁也在堆区创建,调用拷贝构造和拷贝赋值时,应当将对应的资源交给对方对象管理,同时还要将对应的互斥锁交给当前对象。同理,当资源的引用计数减为0时,除了需将对应的资源和引用计数进行释放,由于互斥锁在堆区创建,所以也需要将互斥锁进行释放

flag作用:当引用计数减到0时需要释放互斥锁,但是不能在临界区直接进行释放,因为后面还要解锁,所以可以通过flag去标记,判断解锁后是否释放互斥锁资源。

注意:

  1. shared_ptr本身是线程安全的(拷贝和析构时,引用计数++,–是线程安全的),不需要保证管理的资源的线程安全问题。
  2. shared_ptr管理资源的访问不是线程安全的,需要用的地方自行保护。

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]); 
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"));
	return 0;
}

new[]申请内存空间必须以delete[]方式进行释放,文件指针要fclost进行释放。这时通过定制删除器来控制资源的释放。

在这里插入图片描述

说明:

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

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

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

// 定制删除器
template <class T>
struct DeleteArray
{
	void operator()(const T* ptr)
	{
		delete[] ptr;
		cout << "delete []" << ptr << endl;
	}
};
int main()
{
	std::shared_ptr<int> sp1(new int[10],DeleteArray<int>());
	std::shared_ptr<string> sp2(new string[10],DeleteArray<string>());
    
	return 0;
}

我们是直接用一个类来模拟实现shared_ptr的,不能将删除器的类型设置为构造函数的参数模板,因为删除器不是在构造函数中调用,而是在Release中进行调用,所以需要多加一个成员变量把删除器保存下来,定义这个成员变量需要指定删除器的类型,所以模拟实现的时候不能将删除器的类型设置为构造函数的模板参数。

给shared_ptr增加一个模板参数,构造shared_ptr对象需要指定删除器的类型。增加一个支持删除器的构造函数,在构造对象把删除器保存下来,可以设置一个默认的删除器,如果用户定义shared_ptr对象没有传入,默认以delete的方式进行释放:

template <class T>
class deault_delete
{
public:
	void operator()(T *ptr)
	{
		delete ptr;
	}
};

template <class T, class D = deault_delete<T>>
class shared_ptr
{
public:
	// RAII
	// 保存资源
	shared_ptr(T *ptr = nullptr)
		: _ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)

	{
	}

	// 释放资源
	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T> &sp)
		: _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx)
	{
		_pmtx->lock(); // t1,t2
		++(*_pcount);
		_pmtx->unlock();
	}

	void Release()
	{
		bool flag = false;
		_pmtx->lock();
		if (--(*_pcount) == 0)
		{
			delete _pcount;
			// delete _ptr;
			_del(_ptr);
			flag = true;
		}
		_pmtx->unlock();
		if (flag == true)
			delete _pmtx;
	}

	// sp1 = sp1;
	// sp1 = sp2;//sp2如果是sp1的拷贝呢?
	shared_ptr<T> &operator=(const shared_ptr<T> &sp)
	{
		if (_ptr != sp._ptr) // 资源地址不一样
		{
			Release();
			_pcount = sp._pcount;
			_ptr = sp._ptr;
			_pmtx = sp->_pmtx;

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

		return *this;
	}

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

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

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

	int use_count() const
	{
		return *_pcount;
	}

	T *get() const
	{
		return _ptr;
	}

private:
	T *_ptr;
	int *_pcount;
	mutex *_pmtx;

	D _del;
};

如果传入的删除器是仿函数,那么在构造shared_ptr对象要指明仿函数的类型。如果传入的删除器是Lambda表达式,那么就很麻烦了,Lambda表达式类型难以获取,毕竟我们模拟实现的shared_ptr肯定是使用起来没有C++标准库中的那么方便。

// 定制删除器
template <class T>
struct DeleteArray
{
	void operator()(const T* ptr=nullptr)
	{
		delete[] ptr;
		cout << "delete []" << ptr << endl;
	}
};
int main()
{
	hwc::shared_ptr<ListNode> n1(new ListNode);
	hwc::shared_ptr<ListNode, DeleteArray<ListNode>> sp1(new ListNode[10]);
	return 0;
}

shared_ptr的循环引用问题

在绝大部分情况下,shared_ptr能够解决智能指针赋值造成的多次析构问题,也不会引发内存泄漏。但是,下面的代码展现了一种特殊情况,定义一个Node节点,其中包含两个shared_ptr成员_prev和_next。在主函数中实例化出两个shared_ptr<Node>对象n1和n2,n1的_next指向n2,n2的_prev指向n1,n1和n2相互指向对方,这样就属于循环引用,会造成n1和n2的资源释放失败,引发内存泄漏问题。

struct Node
{
	int _val;
	std::shared_ptr<Node> _prev;
	std::shared_ptr<Node> _next;
 
	~Node()
	{
		std::cout << "~Node()" << std::endl;
	}
};
 
int main()
{
	std::shared_ptr<Node> n1(new Node);
	std::shared_ptr<Node> n2(new Node);
 
	n1->_next = n2;
	n2->_prev = n1;
 
	return 0;
}

循环引用的成因如下:

  1. 构造对象n1和n2,引用计数为1,然后n1->_next = n2、n2->_prev = n1后,引用计数变为2。
  2. 先后由n2和n1调用析构函数,引用计数变为1。
  3. 此时,n1和n2的资源还都没有释放,n1的_next依旧指向n2,n2的_prev依旧指向n1。
  4. n1释放,就需要n2的_prev成员释放,n1释放,就需要n1的_next成员释放。但是,只有对象本身析构,它的成员才会析构,因此n1和n2彼此制约对方的析构,最终n1和n2的资源都无法释放,造成了内存泄漏。

在这里插入图片描述

在这里插入图片描述

怎么解决这个问题?

为了避免循环引用,可以把Node节点中的_next和_prev成员变量的类型改为weak_ptr<Node>,weak_ptr是C++标准库中的比较特殊的一个“智能指针”,允许使用shared_ptr对象来构造weak_ptr对象,但是,weak_ptr不增加引用计数,不参与资源的申请和释放,从严格意义上讲,weak_ptr不算是智能指针。

weak_ptr

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

可以使用shared_ptr对象来构造weak_ptr对象,但是不能直接使用指针来构造weak_ptr对象,在weak_ptr中,也没有operator*函数和operator->成员函数,不具有一般指针的行为,因此,weak_ptr严格意义上并不是智能指针。

weak_ptr的模拟实现:

template <class T>
class weak_ptr
{
public:
	// 默认构造函数
	weak_ptr()
		: _ptr(nullptr)
	{
	}

	// 拷贝构造函数
	weak_ptr(weak_ptr<T> &wp)
		: _ptr(wp._ptr)
	{
	}

	// 采用shared_ptr构造
	weak_ptr(shared_ptr<T> &sp)
		: _ptr(sp.get())
	{
	}

	// 赋值函数
	weak_ptr<T> &operator=(weak_ptr<T> &wp)
	{
		_ptr = wp._ptr;
	}

	// 通过shared_ptr对象赋值
	weak_ptr<T> &operator=(shared_ptr<T> &sp)
	{
		_ptr = sp.get();
	}

private:
	T *_ptr;
};

weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数,至此解决了shared_ptr的循环引用问题。

struct ListNode
{
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;

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

void test_shared_ptr2()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);

	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

在这里插入图片描述

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

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

C++98中产生了第一个智能指针auto_ptr。

C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。

C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。

C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值