栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++智能指针

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++智能指针

文章目录
  • RAII
  • auto_ptr
  • unique_ptr
  • shared_ptr
    • use_count()
    • 删除器
  • 循环引用
    • weak_ptr

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源.也就是说对象会自己回收资源

有两个好处:

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

之前讲的lock_guard和unique_lock都是采用了RAII技术。

其实本质上就是把这些资源封装成一个类,创建对象的时候调用构造函数,等对象生命周期过了之后,自动调用析构函数,去销毁自己的资源。

智能指针必须满足的两点是:

  1. 使用RAII技术
  2. 能像指针一样使用该智能指针对象,因此要重载*和->两个符号
auto_ptr

auto_ptr是第一个出现的智能指针。
auto_ptr是c++98就出现了,但是并不好用,很多地方甚至明文规定禁止使用。它的逻辑大致如下:

namespace my
{
	template
	class auto_ptr
	{
	public:
		auto_ptr(T* _ptr = nullptr)
		{
			ptr = _ptr;
		}
		~auto_ptr()
		{
			delete ptr;
			ptr = nullptr;
			cout << "~auto_ptr()" << endl;
		}

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

		T* operator->()
		{
			return ptr;
		}
	private:
		T* ptr;
	};
}

int main()
{
	my::auto_ptr ap1 = new int(1);
	cout << *ap1 << endl;
}

我们可以发现它自己调用了析构函数去析构它管理的资源,满足了要求。

auto_ptr不好用的原因在于拷贝构造和赋值构造,后序讲的几个指针指针都是为了解决这个问题而出现的。

我们知道,如果只是单纯的浅拷贝,两个指针指向同一块空间,就会析构两次,肯定报错。

auto_ptr是这样解决问题的:如果发生了拷贝,就把第一个指针对空间的管理权给第二个指针。

代码逻辑如下:

auto_ptr(auto_ptr& ap)
{
	ptr = ap.ptr;
	ap.ptr = nullptr;
}

auto_ptr& operator=(auto_ptr& ap)
{
	if (this != &ap)
	{
		if (ptr) delete ptr;  把自己的资源释放
		ptr = ap.ptr;  自己指向别人的支援
		ap.ptr = nullptr;  管理权转移,赋值后指针悬空
	}
	return *this;
}

很明显这就有了一些问题:比如拷贝后会发生指针悬空的现象。但是使用者很可能不知道这是一个悬空的指针,依旧去对他解引用,这就会对空指针解引用,直接崩溃了程序。

这也是auto_ptr不允许使用的原因。

unique_ptr

C++11中开始提供更靠谱的unique_ptr.
unique_ptr面对拷贝和赋值的策略更加粗暴了。

就是直接禁止使用拷贝和赋值。

unique_ptr(unique_ptr&) = delete;
unique_ptr& operator=(unique_ptr&) = delete;
shared_ptr

shared_ptr才解决了拷贝和赋值的问题。它用的技术叫引用计数

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

template
class shared_ptr
{
public:
	shared_ptr(T* _ptr = nullptr)
	{
		ptr = _ptr;
		count = new int(1);
		mtx = new mutex;
	}
	~shared_ptr()
	{
		ReleaseRef();
	}

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

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

	shared_ptr(shared_ptr& ap)
	{
		ptr = ap.ptr;
		count = ap.count;
		mtx = ap.mtx;
		AddRef();
	}

	shared_ptr& operator=(shared_ptr& ap)
	{
		if (this != &ap)
		{
			ReleaseRef();
			ptr = ap.ptr;
			count = ap.count;
			mtx = ap.mtx;
			AddRef();
		}
		return *this;
	}
private:
	T* ptr;
	int* count;
	mutex* mtx;

	void AddRef()
	{
		if (count)
		{
			mtx->lock();
			(*count)++;
			mtx->unlock();
		}
	}

	void ReleaseRef()
	{
		mtx->lock();
		bool flag = true;
		if (-- (* count) == 0)
		{
			if (ptr)
			{
				cout << "delete" << endl;
				delete ptr;
			}
			delete count;
			flag = false;
		}
		mtx->unlock();	
		这里注意不能在unlock之前有析构锁的可能性,因此析构锁要放在最后析构
		if (flag == false) delete mtx;
	}
};


ps:加锁的原因是++和–不是原子的,并且count计数其实是临界资源。因此有线程安全问题,加锁。

另外:shared_ptr指向的资源有可能也是临界资源,但这个线程安全问题是用程序员来避免的,不关shared_ptr的事。(即使用普通指针也会有线程安全问题)

use_count()

std标准库中,shared_ptr有一个这个函数,是用来返回引用计数的。

删除器

对于shared_ptr指向的资源不一定是new出来的,然而上面我们写的删除ptr都是用的delete,万一shared_ptr指向了一个文件指针,用delete删除就会直接崩溃了。

因此我们要提供删除器。
这是库里面带有删除器的shared_ptr构造函数。

这个D可以是任何可调用对象,因此用lambda也行,写仿函数也行。

比如下面这种写法:

  1. 模板T别写成指针了
  2. 这里的lambda都要传参,因为没有东西给你捕获。
shared_ptr sp1(fopen("test.txt", "r"), [](FILE* ptr){fclose(ptr);});
循环引用

循环引用的后果是内存得不到正确的释放。
循环引用最简单的场景就是双链表。

struct ListNode
{
	int data;
	my::shared_ptr next;
	my::shared_ptr prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

```cpp
int main()
{
	my::shared_ptr node1(new ListNode);
	my::shared_ptr node2(new ListNode);
	node1->next = node2;
	node2->prev = node1;
}

上面代码计算机是这么执行的:

然后有些人是这么描述这个现象的:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

总结一下就是:要析构node1,就必须让count变成0,但是要让count变成0,就必须要先析构node1,才能去析构node1的成员。因此循环了。

weak_ptr

weak_ptr一般不会单独使用,它就是为了解决循环引用才出现的。

解决方法就是把ListNode成员的shared_ptr改成weak_ptr.weak_ptr不会增加引用计数。因此上述场景就不存在了。

weak_ptr的实现就是weak_ptr指向shared_ptr的那段空间。然后不增加引用计数即可。

template
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr& sp)
			直接拿指针
			:_ptr(sp.get())
		{}

		weak_ptr& operator=(const shared_ptr& sp)
		{
		    直接拿到shared_ptr的指针,就可以了
			_ptr = sp.get();
			return *this;
		}

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

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
struct ListNode
{
	int data;
	my::weak_ptr next;
	my::weak_ptr prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

写逻辑结构是循环的东西时,成员不要用shared_ptr,要用weak_ptr

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/862123.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号