当前位置: 首页 > article >正文

C++智能指针详解

一、智能指针简介

        智能指针是一个类似于指针的类,将指针交给这个类对象进行管理,我们就可以像使用指针一样使用这个类,并且它会自动释放资源。

        智能指针运用了 RAII 的思想(资源获得即初始化)。RAII 是指,用对象的生命周期来管理资源,类对象创建时拿到资源,析构时释放资源。

RAII 优点:

        1、不需要显式释放资源。

        2、在对象生命周期内,资源始终都是有效的。

简单的智能指针的示例:

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	// 像指针一样使用,重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	// 析构时释放资源
	~SmartPtr()
	{
		cout << "释放资源\n";
		if (_ptr)
			delete _ptr;
	}

private:
	T* _ptr;
};

void func()
{
	int* p1 = new int[10];
	int* p2 = new int[10];
	int* p3 = new int[10];

	delete[] p1;
	delete[] p2;
	delete[] p3;
}

     在上述代码中,指针 p1, p2, p3 在创建时都有可能出现异常,如果在 p1 创建时出现异常,那么我们只需要捕获;如果在 p2 创建时出现异常,那么我们除了捕获异常,还需要释放 p1;而如果在 p3 创建时出现异常,那么我们又要释放 p1 和 p2。

        要写多个 try catch,这会让我们的代码变得十分复杂,并且可能会有遗漏,造成内存泄漏。

        这时,智能指针的优势就体现出来了,只需要把指针交给智能指针进行管理,就能够在生命周期结束时自动释放。

用上面的简单的智能指针示例

void func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[10]);
	SmartPtr<int> sp3(new int[10]);
}

        在对象的生命周期结束后,会自动调用析构释放资源。我们就不需要写复杂的代码,也不用担心内存泄漏的问题了。

二、智能指针的拷贝问题

        智能指针的拷贝如果不写的话,默认生成的是浅拷贝。而浅拷贝会使同一份资源释放两次,运行会报错。

void func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(sp1);
}

这时候就有多种解决方案:

        1、auto_ptr

        将资源全部转给一方,将另一方置为空。(不靠谱,现在禁止使用了)

        2、unique_ptr

        拷贝有问题,干脆禁止拷贝。将拷贝封住,就可以了。(不需要拷贝的场景)

unique_ptr 的简单实现:

template<class T>
class Unique_Ptr
{
public:
	Unique_Ptr(T* ptr)
		:_ptr(ptr)
	{}
	// 像指针一样使用,重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	// 析构时释放资源
	~Unique_Ptr()
	{
		cout << "释放资源\n";
		if (_ptr)
			delete _ptr;
	}
	Unique_Ptr(const Unique_Ptr<T>& up) = delete;
	Unique_Ptr<T>& operator=(const Unique_Ptr<T>& up) = delete;
private:
	T* _ptr;
};

3、shared_ptr

        通过引用计数解决多次析构问题

        用一个引用计数表示当前共有多少对象在使用该指针,每次析构都减引用计数,当引用计数减到0,就释放资源。

        为什么引用计数不能为 int 和 静态 static int ?

        int:如果引用计数是 int ,当我们改了一个引用计数,其他的对象无法同步。

        如:有三个对象 sp1, sp2, sp3,如果sp3拷贝sp2,无法告知sp1,sp1 无法同步引用计数。

        static int:如果用静态的,整个类共用一个引用计数,无法区分shared_ptr 管理的多个指针的引用计数。

        如:sp1(new int(1)); sp2(new int(2)); sp1 和 sp2 的引用计数肯定是不同的,但用静态无法区分,因为它是整个类共有的。

因此,引用计数用指针或引用最佳。

shared_ptr 简单实现代码

template<class T>
class Shared_Ptr
{
public:
	// 引用计数初始为 1
	Shared_Ptr(T* ptr)
		:_ptr(ptr)
		,_count(new int(1))
	{}
	// 像指针一样使用,重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
    // 返回引用计数
    int use_count()
	{
		return *_count;
	}

	Shared_Ptr(const Shared_Ptr<T>& sp)
	{
		// 将资源拷贝过来,并 ++引用计数
		_ptr = sp._ptr;
		_count = sp._count;
		++(*_count);
	}

	Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
	{
		// 防自己给自己赋值
		if (sp._ptr != _ptr)
		{
			// 赋值会将原本的资源覆盖,因此要进行判断
			// 如果是最后一个对象,就析构释放,不是就 减减当前计数
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
			}

			// 拷贝资源,++拷贝的计数
			_ptr = sp._ptr;
			_count = sp._count;
			++(*_count);
		}

		return *this;
	}

	// 析构时释放资源
	~Shared_Ptr()
	{
		// 当引用计数减到 0,就释放资源
		if (--(*_count) == 0)
		{
			cout << "释放资源\n";
			delete _ptr;
			delete _count;
		}
		else
		{
			// 打印调试
			cout << "减减引用计数,当前引用计数为: " << *_count << endl;
		}
	}
private:
	T* _ptr; // 指针
	int* _count; // 引用计数
};

        上述代码中存在线程安全问题,引用计数需要加锁保护!

多线程测试代码 测试记得把打印的调试信息注释掉

// 测试线程安全:拷贝 n 个对象
// 测试记得把打印的调试信息注释掉
void ThreadRoute(Shared_Ptr<int>& sp, int n, mutex& mtx)
{
	for (int i = 0; i < n; ++i)
	{
		Shared_Ptr<int> test(sp);
	}
}

void TestSharedThreadSafe()
{
	Shared_Ptr<int> sp(new int(1));
	mutex mtx;
	int n = 10000;
	// 因为不清楚内部实现,多线程的引用要使用库函数 ref
	thread t1(ThreadRoute, ref(sp), n, ref(mtx));
	thread t2(ThreadRoute, ref(sp), n, ref(mtx));

	t1.join();
	t2.join();
    
}

        多线程版 shared_ptr 实现,在修改引用计数时,加锁保护

// 多线程
template<class T>
class Shared_Ptr
{
public:
	// 引用计数初始为 1
	Shared_Ptr(T* ptr)
		:_ptr(ptr)
		, _count(new int(1))
		, _pmtx(new mutex)
	{}
	// 像指针一样使用,重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

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

	void AddCount()
	{
		// 对公共资源 引用计数加加,加锁保护
		unique_lock<mutex> lock(*_pmtx);
		++(*_count);
	}

	void DelCount()
	{
		// 对公共资源 引用计数减减,加锁保护
		unique_lock<mutex> lock(*_pmtx);
		--(*_count);
	}

	// 返回管理的指针
	T* Get()
	{
		return _ptr;
	}

	// 返回引用计数 
	int use_count()
	{
		return *_count;
	}

	// 拷贝构造
	Shared_Ptr(const Shared_Ptr<T>& sp)
	{
		// 将资源拷贝过来,并 ++引用计数
		_ptr = sp._ptr;
		_count = sp._count;
		_pmtx = sp._pmtx;

		// 将锁拿到后再 用锁保护,++引用计数
		AddCount();
	}

	Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
	{
		// 防自己给自己赋值
		if (sp._ptr != _ptr)
		{
            // 释放原本资源
			release();

			// 拷贝资源,++拷贝的计数
			_pmtx = sp._pmtx;
			_ptr = sp._ptr;
			_count = sp._count;

			AddCount();
		}

		return *this;
	}
	// 释放资源
	void release()
	{
		unique_lock<mutex> lock(*_pmtx);
		// 当引用计数减到 0,就释放资源
		if (--(*_count) == 0)
		{
			// cout << "释放资源\n";
			// 释放锁之前 解锁
			lock.unlock();
			delete _ptr;
			delete _count;
			delete _pmtx;
		}
		else
		{
			// 打印调试
			// cout << "减减引用计数,当前引用计数为: " << *_count << endl;
		}
	}

	// 析构时释放资源
	~Shared_Ptr()
	{
		release();
	}
private:
	T* _ptr; // 指针
	int* _count; // 引用计数
	mutex* _pmtx; // 锁
};

三、shared_ptr 的循环引用问题

        当存在类里面有智能指针互相指向时,就会出现循环引用问题。

 

        因此,官方给 shared_ptr 配了一个小弟:weak_ptr

        weak_ptr 不是常规的智能指针,它具有以下特点

  • 它不支持 RAII

  • 支持像指针一样使用

  • 专门设计出来解决循环引用问题

        核心:weak_ptr 支持用 shared_ptr 构造,它不会加加引用计数。

        测试循环引用的代码:

struct ListNode
{
    // 双向链表
	Shared_Ptr<ListNode> _prev;
	Shared_Ptr<ListNode> _next;
	// 析构
	~ListNode()
	{
		cout << "释放节点\n";
	}
};

void CirculaReferenceProblem()
{
	Shared_Ptr<ListNode> n1(new ListNode);
	Shared_Ptr<ListNode> n2(new ListNode);
	n1->_next = n2;
	n2->_prev = n1;
}

        weak_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)
	{
		_ptr = sp.Get();

		return *this;
	}

	// 像指针一样使用,重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

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

        解决方案:在内部的互相引用处,用 weak_ptr 就可以了

struct ListNode
{
    // 在内部的互相引用处,用 weak_ptr 就可以了
	Weak_Ptr<ListNode> _prev;
	Weak_Ptr<ListNode> _next;

	~ListNode()
	{
		cout << "释放节点\n";
	}
};

void CirculaReferenceProblem()
{
	Shared_Ptr<ListNode> n1(new ListNode);
	Shared_Ptr<ListNode> n2(new ListNode);
	n1->_next = n2;
	n2->_prev = n1;
}

四、定制删除器

        有的时候我们使用 new [] 开辟空间或传入的是文件指针,就可以定制删除器来指定使用 delete [] 或fclose() 删除。

        定制删除器就是传入一个可调用对象(仿函数或lambda或函数指针),在释放时调用。

        改变:

        1、成员加一个 function 包装的删除器,构造函数添加删除器模版

        2、release() 中删除改为用定制删除器删除

添加定制删除器

template<class T>
class Shared_Ptr
{
public:
	Shared_Ptr()
		:_ptr(nullptr)
		, _count(new int(1))
		, _pmtx(new mutex)
	{}

	// 引用计数初始为 1
	Shared_Ptr(T* ptr)
		:_ptr(ptr)
		, _count(new int(1))
		, _pmtx(new mutex)
	{}
	
	// 定制删除器
	template<class D>
	Shared_Ptr(T* ptr, D del)
		:_ptr(ptr)
		, _count(new int(1))
		, _pmtx(new mutex)
		, _del(del)
	{}
	// 像指针一样使用,重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

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

	void AddCount()
	{
		// 对公共资源 引用计数加加,加锁保护
		unique_lock<mutex> lock(*_pmtx);
		++(*_count);
	}

	void DelCount()
	{
		// 对公共资源 引用计数减减,加锁保护
		unique_lock<mutex> lock(*_pmtx);
		--(*_count);
	}

	// 返回管理的指针
	T* Get() const
	{
		return _ptr;
	}

	// 返回引用计数 
	int use_count()
	{
		return *_count;
	}

	// 拷贝构造
	Shared_Ptr(const Shared_Ptr<T>& sp)
	{
		// 将资源拷贝过来,并 ++引用计数
		_ptr = sp._ptr;
		_count = sp._count;
		_pmtx = sp._pmtx;

		// 将锁拿到后再 用锁保护,++引用计数
		AddCount();
	}

	Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
	{
		// 防自己给自己赋值
		if (sp._ptr != _ptr)
		{
			// 释放原本资源
			release();

			// 拷贝资源,++拷贝的计数
			_pmtx = sp._pmtx;
			_ptr = sp._ptr;
			_count = sp._count;

			AddCount();
		}

		return *this;
	}
	// 释放资源
	void release()
	{
		unique_lock<mutex> lock(*_pmtx);
		// 当引用计数减到 0,就释放资源
		if (--(*_count) == 0)
		{
			// cout << "释放资源\n";
			// 释放锁之前 解锁
			lock.unlock();
			// delete _ptr;
			// 改为用定制删除器删除
			_del(_ptr);
			delete _count;
			delete _pmtx;
		}
		else
		{
			// 打印调试
			// cout << "减减引用计数,当前引用计数为: " << *_count << endl;
		}
	}

	// 析构时释放资源
	~Shared_Ptr()
	{
		release();
	}
private:
	T* _ptr; // 指针
	int* _count; // 引用计数
	mutex* _pmtx; // 锁

	function<void(T*)> _del = [](T* ptr) {
		cout << "默认 delete\n";
		delete ptr;
	};
};

  测试代码

template<class T>
struct DeleteArr
{
	void operator()(T* ptr)
	{
		cout << "delete[] ptr";
		delete[] ptr;
	}
};

void TestDeletor()
{
    // 如果不传定制删除器,运行会报错
	Shared_Ptr<ListNode> sp(new ListNode[10], DeleteArr<ListNode>());
}

        到此结束,感谢大家观看♪(・ω・)ノ


http://www.kler.cn/a/440845.html

相关文章:

  • 3D Gaussian Splatting for Real-Time Radiance Field Rendering-简洁版
  • 05、SpringMVC全注解开发
  • 音频声音太小怎么调大?调大音频声音的几种方法
  • STM32-笔记1-点亮led灯
  • sqlilabs靶场二十一关二十五关攻略
  • app的测试范围以及web和app的测试区别
  • 搭建Tomcat(二)--反射的应用
  • 详细描述一下 Elasticsearch 更新和删除文档的过程。
  • 在VScode中对R语言进行环境配置
  • 沈剑-架构师训练营
  • Mongodb 集群搭建
  • 项目二十三:电阻测量(需要简单的外围检测电路,将电阻转换为电压)测量100,1k,4.7k,10k,20k的电阻阻值,由数码管显示。要求测试误差 <10%
  • k8s+rancher配置滚动发布更新时服务不可用
  • STM32--IO引脚复用
  • 留学论文Introduction辅导:论文开头introduction怎么写
  • 19. 【.NET 8 实战--孢子记账--从单体到微服务】--记账模块--收支记录
  • OpenShift 4 - 多云管理(2) - 配置多集群观察功能
  • 【0368】Postgres内核 清除所有旧的 relcache cache files ( 11 )
  • JS进阶-面向对象-搭建网站-HTML与JS交互
  • Typescript安装