C++:智能指针

1.引入使用场景

在没有智能指针之前,我们一般用new申请空间以后,需要手动delete,释放这一块空间

#include<iostream>
using namespace std;

int main()
{
    pair<string,string>*p1=new(pair<"insert","插入");
    pair<string,string>*p2=new(pair<"sort","排序");
    
    delete p1;
    delete p2;
    return 0;
}

在程序正常按流程运行时,只要我们记住delete,那么就不会产生内存泄漏的问题。但如果由于抛异常等场景可能导致程序没法按流程正常运行,导致空间无法得到释放。例如以下场景:

#include<iostream>
#include<string>

using namespace std;

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}


void f()
{
	pair<string, string>* p1 = new pair<string, string>;
	pair<string, string>* p2 = new pair<string, string>;
	pair<string, string>* p3 = new pair<string, string>;
	pair<string, string>* p4 = new pair<string, string>;

	try 
	{
		div();
	}
	catch (...)
	{
		throw;
	}

	delete p1;
	cout << "delete:" << p1 << endl;

	delete p2;
	cout << "delete:" << p2 << endl;

	delete p3;
	cout << "delete:" << p3 << endl;

	delete p4;
	cout << "delete:" << p4 << endl;
	
}

int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

如果程序正常进行的话,p1到p4都会正常析构

如下图所示:

但如果出现异常,它们都无法得到析构,就会导致内存泄漏,如下图运行结果的输出:

可见没有任何析构的信息,因为异常被捕获导致程序直接跳转,原本应该被析构的空间无法得到释放,导致内存泄漏。

像上面类似的无法我们手动释放或者很难我们手动释放的场景还有很多,因此,智能指针应运而生

2.智能指针的特性

2.1.RAII

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

基本概念:

资源获取即初始化 - 将资源的生命周期与对象的生命周期绑定:

  • 构造函数中获取资源

  • 析构函数中释放资源

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。

所以智能指针实际上是一个对象,我们把资源交给它管理,正确使用它可以帮助我们规避大部分情况下的内存泄漏问题

2.2.具有像指针一样的行为

在智能指针对象里了重载operator*和opertaor->,具有像指针一样的行为。在下文的对库里的智能指针模拟实现时我会展现出来

3.C++库中的几个智能指针

接下来我会解释C++库里提供的四个智能指针并且通过我对它们的模拟实现来更好的解释它们各自的特性(注:在我的实践中,我将它们放在自己创建的命名空间Smartptr下避免与库中的冲突)

3.1.auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份auto_ptr来了解它的原理

template<class T>
class auto_ptr
{
public:
	auto_ptr(T*ptr)
		:_ptr(ptr)
	{}
	
	~auto_ptr()
	{
		delete _ptr;
	}
	//管理权转移,被拷贝的智能指针被置空
	auto_ptr(auto_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
//如果不置空,两个或多个智能指针共同管理一块空间导致该空间被多次析构,会导致程序崩溃
	}

	auto_ptr<T>& operator=(auto_ptr<T>& sp)
	{
		if (this != &sp)
		{
			delete _ptr;
			_ptr = sp._ptr;
			sp._ptr = nullptr;//同样也是管理权转移
		}
		return *this;
	}

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

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

可见如果相同类型的auto_ptr如果彼此进行赋值的时候,会导致管理权转移,被赋值的智能指针悬空,再对其解引用就会导致错误。因此,由于auto_ptr的巨大缺陷,我们在实践中不应该使用它

3.2.unique_ptr

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份unique_ptr来了解它的原 理

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

	~unique_ptr()
	{
		delete _ptr;
	}

	//防拷贝
	unique_ptr(unique_ptr<T>& sp) = delete;

	unique_ptr<T>& operator=(unique_ptr<T>& sp) = delete;

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

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


private:
	T* _ptr;
};

unique_ptr把拷贝构造函数和赋值重载函数设置成“已删除的函数”使得无法赋值(或者将这两个函数设置成私有,简单粗暴,如果没有赋值的需要可以用unique_ptr

3.3.shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

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

4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

以下是模拟实现:

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}

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

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

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

	int use_count()const
	{
		return *_pcount;
	}

	T* get()const
	{
		return _ptr; 
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr == sp._ptr)
			return *this;
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
		}
		return *this;
	}

private:
	T* _ptr;
	int* _pcount;
//作为引用计数使用,为一块由多个智能指针共同管理的空间它统计被多少智能指针共同管理
//,直到计数为零再析构这块空间
};

这么看来,shared_ptr似乎是最符合我们使用场景的智能指针了,事实也确实如此,在大部分情况下它也确实能帮助我们对资源进行正常释放。但如果在循环引用的场景下,shared_ptr也会发生内存泄漏的问题

struct Node
{
	int _val;

	Smartptr::shared_ptr<Node> _next;
	Smartptr::shared_ptr<Node> _prev;

	Node(int val=0)
		:_val(val)
		,_prev(nullptr)
		,_next(nullptr)
	{}
	
};

int main()
{
	/*Node* n1 = new Node;
	Node* n2 = new Node;*/
	////...

	//delete n1;
	//delete n2;

	// 循环引用
	Smartptr::shared_ptr<Node> sp1(new Node);
	Smartptr::shared_ptr<Node> sp2(new Node);

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->_next = sp2;
	sp2->_prev = sp1;

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	return 0;
}

以下是图示:

那么怎么解决这个问题呢?这就要请我们最后一个智能指针,weak_ptr出场了

3.4.weak_ptr

template<class T>
class weak_ptr//无RAII特性,由shared_ptr初始化,用来解决shared_ptr里的循环引用问题
{
public:
	weak_ptr()
		:_ptr(nullptr)
	{}

	weak_ptr(shared_ptr<T>&sp)
		:_ptr(sp.get())//因为sp._ptr是私有成员,要么把weak_ptr写成shared_ptr的友元,或者像这样用公有get函数获取_ptr
	{}

	~weak_ptr()
	{
		delete _ptr;
	}

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

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

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

private:
	T* _ptr;
};

注意:weak_ptr不是RAII智能指针,专门用来解决shared_ptr循环引用问题,weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理

我们把上面发生循环引用的代码改一下

struct Node
{
	int _val;
	
	/*Smartptr::shared_ptr<Node> _next;
	Smartptr::shared_ptr<Node> _prev;*/

	Smartptr::weak_ptr<Node>_next;
	Smartptr::weak_ptr<Node>_prev;
};

这样就能够解决循环引用的问题,所以当我们用shared_ptr,同时意识到某处用shared_ptr可能导致循环引用的问题时,我们就可以用weak_ptr

4.补充

我们上面实现的shared_ptr还和库里的有一点区别,也有点小问题,如下面的场景:

int main()
{
    Smartptr::shared_ptr<int>sp1(new int[10]);
    Smartptr::shared_ptr<int>sp2((int*)malloc(sizeof(int));
    Smartptr::shared_ptr<FILE>sp3(fopen("test.cpp", "r");
    return 0;
}

我模拟实现的shared_ptr并不能正确处理上述的场景,那怎么办呢?

这时,就要用到定制删除器这个工具了

在库里的shared_ptr我们可以手动传定制删除器来进行对应正确的对资源的处理

template<class T>
struct Free
{
 void operator()(T* ptr)
 {
     free(ptr);
 }
};
template<class T>
struct DeleteArray
 {
 void operator()(T* ptr)
 { 
     delete[] ptr; 
 }
};

int main()
{
     Free<int> freeFunc;
     std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
     DeleteArray<int> deleteArrayFunc;
     std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
     std::shared_ptr<int> sp4(new int[10], [](int* p){delete[] p; });
     return 0;
}

删除器除了用仿函数,也可以传函数指针,lambda表达式

以下是我增加了删除器的shared_ptr的模拟实现

template<class T>
class shared_ptr
{
public:
    //在构造函数这里新增模板参数,用来传删除器
	template<class D>
	shared_ptr(T* ptr, D del=_del)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			_del(_ptr);
			delete _pcount;
		}
	}

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

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

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

	int use_count()const
	{
		return *_pcount;
	}

	T* get()const
	{
		return _ptr;
	}


	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr == sp._ptr)
			return *this;
		{
			if (--(*_pcount) == 0)
			{
				_del(_ptr);
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
		}
		return *this;
	}

private:
	T* _ptr;
	int* _pcount;//作为引用计数使用,为一块由多个智能指针共同管理的空间来统计被多少智能指针共同管理,直到计数为零再析构这块空间
	function<void(T*)>_del = [](T* ptr) {delete ptr; };
//增加一个包装器类型的成员变量,用于接收删除器,使得析构函数中可以顺利使用删除器释放资源
};

5.小结

正确使用智能指针使得资源可以得到释放,避免内存泄漏的发生,是一个很有用的工具

(感谢您能看到这,如果有问题可以留言讨论指出,感谢!!)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值