【C++】如何解决智能指针的析构函数,如何实现shared_ptr的引用计数以及解决shared_ptr的循环引用

大家好,今天带来智能指针的析构函数问题,以及在面试中高频出现的模拟shared_ptr实现,及解决循环引用问题

在阅读本文前最好先初步了解智能指针是什么,基本作用和结构(大家可以去看看这篇博客)

【C++】 从RAII到智能指针unique_ptr-CSDN博客https://blog.csdn.net/lunar_coder/article/details/168327798?spm=1011.2415.3001.5331以及包装器,lambda等知识

一.智能指针的析构函数

错误示例

很多人都认为智能指针的析构函数就是下面这种方式实现的:

template<class T>

class Smart_pointer

{

public:

// ...

~Smart_pointer()

{

delete _ptr;

}

private:

T* _ptr;

// ...

};

指针的处理方式

其实不然,我们都知道指针的处理方式有很多种:

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
    int* p = new int();
    delete p;

    int* parr = new int[10];
    delete[] parr;

    FILE* pf = fopen("file", "w");
    fclose(pf);

    p = parr = nullptr;
    pf = nullptr;

    return 0;
}

正确方式

光用delete是不足以解决问题的,所以我们可以再添加一个智能指针的成员变量,这个成员变量就是一个仿函数,那么它的类型是什么呢?

function

详细参考文献

function - C++ Referencehttps://legacy.cplusplus.com/reference/functional/function/?kw=function

function<返回类型(形参类型)> type_name = 可调用对象;比如:

cpp 复制代码
#include<iostream>
#include<functional>
using namespace std;

int add(int a, int b)
{
    return a + b;
}

int main()
{
    function<int(int, int)> Add = add;
    cout << Add(10, 20) << endl;
    return 0;
}

在使用它之前要包含对应的头文件"functional",所以这个删除器的类型就是function<void(T*)>,同时我们要给_del一个lambda以作为默认析构函数

lambda

lambda基本结构:捕捉列表(形参)-> ret_type { 函数主题 }

其中返回类型可以省略

示例:

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
    auto add = [] (int a, int b) { return a + b; };
    cout << add(10, 20) << endl;
    return 0;
}

所以我们的基本框架就是:

cpp 复制代码
#include<iostream>
#include<functional>
using namespace std;

template<class T>
class Smart_pointer
{
public:
    // ...

    Smart_pointer(T* ptr)// 没有传析构函数
        :_ptr(ptr)
    {}

    template<class D>
    Smart_pointer(T* ptr, D del)// 传了析构函数
        :_ptr(ptr)
        ,_del(del)
    {}

    ~Smart_pointer()
    {
        _del(_ptr);
    }

private:
    T* _ptr;
    function<void(T*)> _del = [](T* ptr) { delete ptr; };// 默认析构函数
    // ...
};

我们可以自己模拟一下:

cpp 复制代码
#include<iostream>
#include<functional>
using namespace std;


template<class T>
class my_shared_ptr
{
public:
	explicit my_shared_ptr(T* ptr)
		:_ptr(ptr)
	{}

	template<class D>
	my_shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		,_del(del)
	{}

	~my_shared_ptr()
	{
		cout << "deletefunc: ";
		_del(_ptr);
	}

private:
	T* _ptr;
	function<void(T*)> _del = [](T* ptr) 
		{
			cout << "delete" << endl;
			delete ptr; 
		};
};

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


int main()
{
    my_shared_ptr<int> p(new int());

    my_shared_ptr<int> parr(new int[10], DeleteFunc1<int>());

    return 0;
}

可以看到也是很好解决了这个问题,还有删除器也可以作为类模板参数类型,库里的unique_ptr就是这样实现的,大家也可以根据这个思路去自己尝试一下

二.shared_ptr引用计数

我们上期博客讲了智能指针是什么及如何使用,也对比了各种智能指针的优缺点,shared_ptr相较于其他智能指针最大的优势就是能够实现多个指针一起管理一份资源,那么这是如何实现的呢?

其实是shared_ptr的成员变量里有一个自定义类型去统计共有多少智能指针管理这份资源,而我们**可以只用一个int*(pcount)指针来模拟,**多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则这时就可以析构资源了

cpp 复制代码
#include<iostream>
#include<functional>
using namespace std;

template<class T>
class my_shared_ptr
{
public:
    // ...

    int use_count()
    {
        return *_pcount;
    }

private:
    T* _ptr;
    int* _pcount;
    function<void(T*)> _del;
};

三.shared_ptr的循环引用问题

循环引用问题示例

我们来看这样一个结构

cpp 复制代码
#include<iostream>
#include<functional>
using namespace std;


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

	int _val;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
};

int main()
{
	shared_ptr<ListNode> sp1(new ListNode());
	shared_ptr<ListNode> sp2(new ListNode());
	sp1->_next = sp2;
	sp2->_prev = sp1;
	return 0;
}

如果按照我们上面的思路的话:

当函数结束时,sp1被析构时,引用计数count--,变为1,代表该资源还有指针指向(sp2->prev),所以没有被释放,sp2被析构时同理被sp1->next指向,所以也未被释放;

这样两个资源都释放不了,从而导致内存泄漏

解决方式

这时我们把ListNode中的智能指针换成weak_ptr就解决了

原因:weak_ptr

weak_ptr不能独自管理资源,它只能由shared_ptr或weak_ptr初始化 ,它与shared_ptr的结构相似,只是在赋值重载和拷贝赋值时并不会增加引用计数

这就意味着当它和shared_ptr共同管理一份资源时,weak_ptr析构只代表weak_ptr不再管理这份资源,但shared_ptr析构代表着这块空间将要被释放

这样就很好的解决了循环引用的问题了

四.模拟实现shared_ptr与weak_ptr

cpp 复制代码
#include<iostream>
#include<functional>
using namespace std;


template<class T>
class my_shared_ptr
{
public:
	explicit my_shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

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

	my_shared_ptr(const my_shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
		,_del(sp._del)
	{
		++(*_pcount);
	}

	my_shared_ptr<T>& operator=(const my_shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			if (--(*_pcount) == 0)// 先将原指向地址的引用计数--
			{
				_del(_ptr);
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}

	T* get() const
	{
		return _ptr;
	}

	int use_count()
	{
		return *(_pcount);
	}

	~my_shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "deletefunc: ";
			_del(_ptr);
		}
	}

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

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

private:
	T* _ptr;
	int* _pcount;
	function<void(T*)> _del = [](T* ptr) 
		{
			cout << "delete" << endl;
			delete ptr; 
		};
};

template<class T>
class my_weak_ptr
{
public:

	my_weak_ptr() = default;

	my_weak_ptr(my_shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}

	my_weak_ptr<T>& operator=(my_shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

	my_weak_ptr(my_weak_ptr<T>& wp)
		:_ptr(wp._ptr)
	{}

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

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

private:
	T* _ptr = nullptr;
};

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

struct DeleteFunc2
{
	void operator()(FILE* pf)
	{
		cout << "fclose()" << endl;
		if (pf) fclose(pf);
	}
};

struct ListNode
{
	~ListNode()
	{
		delete _val;
		cout << "~ListNode()" << endl;
	}

	int* _val;
	my_weak_ptr<ListNode> _prev;
	my_weak_ptr<ListNode> _next;
};

int main()
{
	//shared_ptr<int> sp1(new int);
	//shared_ptr<int> sp2(sp1);
	//shared_ptr<int> sp3(new int);
	//cout << sp1.use_count() << endl;
	//cout << sp3.use_count() << endl;
	my_shared_ptr<ListNode> sp1(new ListNode());
	my_shared_ptr<ListNode> sp2(new ListNode());
	sp1->_next = sp2;
	sp2->_prev = sp1;
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	return 0;
}

注:shared_ptr在初始化构造的时候一定要显式化构造,因为智能指针的构造都是传一个指针,如果走隐式类型转化编译器就分辨不出到底是隐式类型转化为shared_ptr/unique_ptr或auto_ptr

以上就是本期博客的所有内容,感谢阅读

相关推荐
瑞克-桑切斯1 小时前
脚本陷入自循环的解决方法
1024程序员节
Java开发追求者1 小时前
windows快速安装docker
windows·docker·安装·1024程序员节
我叫洋洋1 小时前
用 Qoder(AI 编程助手)把 Cadence 16.6 从零配到能画板
人工智能·单片机·嵌入式硬件·电路·1024程序员节
ai_coder_ai1 小时前
自动化脚本最佳实践
1024程序员节
坐吃山猪1 小时前
Godot01-初体验
godot·1024程序员节
白白白小纯1 小时前
C++进阶.多态
c++
EnCi Zheng2 小时前
PostgreSQL 实战指南:安装配置、数据类型、索引选型、MVCC 事务与高可用备份全解析
数据库·sql·postgresql·索引·1024程序员节
choumin2 小时前
C++ std::bind的作用和用法
1024程序员节·std bind·函数参数绑定
流浪0012 小时前
计算机网络篇8:传输层——序列化与反序列化,TCP 面向字节流的报文边界由应用层自己切
网络协议·tcp/ip·计算机网络·1024程序员节·序列化反序列化·面向字节流·面向数据包