IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++智能指针 -> 正文阅读

[C++知识库]C++智能指针

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 T>
	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<int> ap1 = new int(1);
	cout << *ap1 << endl;
}

我们可以发现它自己调用了析构函数去析构它管理的资源,满足了要求。
在这里插入图片描述
auto_ptr不好用的原因在于拷贝构造和赋值构造,后序讲的几个指针指针都是为了解决这个问题而出现的。

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

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

代码逻辑如下:

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

auto_ptr<T>& operator=(auto_ptr<T>& 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<T>&) = delete;
unique_ptr<T>& operator=(unique_ptr<T>&) = delete;

shared_ptr

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

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

在这里插入图片描述

template<class T>
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<T>& ap)
	{
		ptr = ap.ptr;
		count = ap.count;
		mtx = ap.mtx;
		AddRef();
	}

	shared_ptr<T>& operator=(shared_ptr<T>& 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<FILE> sp1(fopen("test.txt", "r"), [](FILE* ptr){fclose(ptr);});

循环引用

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

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

```cpp
int main()
{
	my::shared_ptr<ListNode> node1(new ListNode);
	my::shared_ptr<ListNode> 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 T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

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

		weak_ptr<T>& operator=(const shared_ptr<T>& 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<ListNode> next;
	my::weak_ptr<ListNode> prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

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

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-05-07 11:00:54  更:2022-05-07 11:02:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 5:56:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码