天天看点

C++智能指针简介

写在前面:

在很多的场景下我们都需要进行动态的申请内存,但是动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时候我们会忘记了释放内存,这样的情况会产生内存泄漏的问题,这种问题也比较难以发现,因为现象不会立马出现,而多次释放一块内存就会很容易导致崩溃问题。

而C++为了更容易、更安全的使用动态内存,为我们提供了智能指针的概念,通过指针指针来管理对象,其行为比较像指针,主要负责自动释放所指向的对象。

智能指针头文件 <memory>

一、智能指针的原理

说到智能指针就不得不提一个叫 RALL(Resource Acquisition Initlalization) 直译过来就是资源获取初始化

它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

借此,我们实际上把管理一份资源的责任托管给了一个对象(听到没,把你托管给你的对象,然后你的对象一直管你管到你被析构了为止)。

这种做法有两大好处:

1.不需要显式地释放资源。

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

示例 :RALL 思想

1.1 动态申请的内存忘记释放

for (int i = 0; i <= 10000000; ++i)  
    {  
        int *ptr = new int[3];  
        ptr[0] = 1;  
        ptr[1] = 2;  
        ptr[2] = 3;  
        //delete ptr;     //未释放资源
    } 
           

这样造成的后果就是内存泄露(危险动作切勿模仿)

我们都知道类的构造函数与析构函数,在对象生成时自动调用其构造函数,在对象的生命周期结束时自动调用其析构函数,那么如果我使用一个这样的机制来管理我动态申请的对象,那么这样就再也不怕忘记释放资源了。

示例:

#include"SmartPtr.h"
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

void Test1()
{
		int *ptr = new int[3];
		SmartPtr<int> sp(ptr);
		ptr[0] = 1;
		ptr[1] = 2;
		ptr[2] = 3;
		//delete ptr;     //未释放资源
}
int main()
{
	Test1();
	cout << "hahhah" << endl;
	system("pause");
	return 0;
}
           
C++智能指针简介

这里可以看到,我们定义的sp与ptr地址是相同的,那么两个指针所指向的内容也是相同的,通过智能指针 sp 的管理实现资源的自动释放,当然手动释放了 ptr 指针也没有问题,因为两者生命周期一致,ptr 被释放了 sp 自然也被释放了,不存在多次非法释放内存问题。

这样的一个SmartPtr 类的确可以体现智能指针的思想,但是这里的 SmartPtr还不具有一个指针的行为,我们都知道指针是支持解引用或者 -> 的方式来访问所指向的空间的内容,因此再次基础之上再对 * 和 -> 进行重载即可,这便是智能指针的大概的实现原理的实现理念。

二、auto_ptr、unique_ptr 、scoped_ptr 、shared_ptr 、weak_ptr 简介

2.1 auto_ptr

auto_ptr 在C++ 98 版本的库中就提供了该智能指针

auto_ptr的实现原理:管理权转移的思想。

该指针的大致是实现思路为:

#include <memory>
class Date
{
public:
 Date() { cout << "Date()" << endl;}
 ~Date(){ cout << "~Date()" << endl;}
 int _year;
 int _month;
 int _day;
};
int main()
{
 auto_ptr<Date> ap(new Date);
 auto_ptr<Date> copy(ap);
 // auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
 // 所以auto_ptr 的缺陷非常严重、现在很多时候都不建议使用这种智能指针
 ap->_year = 2018;
 return 0;
}
           
C++智能指针简介
C++智能指针简介

这是因为auto_ptr 管理权转移的思想,auot_ptr 中的 赋值操作大致思路为:

AutoPtr<T>& operator=(AutoPtr<T>& ap)
 {
 // 检测是否为自己给自己赋值
 	if(this != &ap)
 	{
 		// 释放当前对象中资源
 		if(_ptr)
 		delete _ptr;
 	
 		// 转移ap中资源到当前对象中
 		_ptr = ap._ptr;
 		ap._ptr = NULL;
 }
 	return *this;
 }
           

可以看到当我们再去访问已经被释放的指针指向的空间时就会出现崩溃的问题。

2.2 unique_ptr

因为前车之鉴,C++ 11 开始提供更靠谱的 unique_ptr

俗话说在哪里跌打就在哪里站起来,unique_ptr 直接非常的暴力 杜绝拷贝和赋值,可以说 unique_ptr 就是 auto_ptr 的 2.0 修复版本

C++智能指针简介

具体的实现原理大概为:

1.将该接口只声明、不实现。并将其声明为私有 这是C98 版本的实现方法

2.C++ 11 是直接将其删除

2.3 scoped_ptr

scoped_ptr是一个类似于auto_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确的删除。

scoped_ptr同时把拷贝构造函数和赋值操作都声明为私有的,禁止对智能指针的复制操作,保证了被它管理的指针不能被转让所有权。

所以scoped_ptr 和 unique_ptr 其实实现原理是一样的。

2.4 shared_ptr

上面的直接干掉了指针的赋值和拷贝的操作肯定是不满足一些场景需求的,因此C++ 提供了更靠谱且支持拷贝的 shared_ptr

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

    针了。

大致实现思路:

void Release()
 {
 	bool deleteflag = false;
	 // 引用计数减1,如果减到0,则释放资源
	 _pMutex.lock();//保证线程安全
	 if (--(*_pRefCount) == 0)
	 {
		 delete _ptr;
		 delete _pRefCount;
 		deleteflag = true;
	 }
	 _pMutex.unlock();
 
 	if(deleteflag == true)
 	{
    	delete _pMutex;
    }
 }
private:
 		int* _pRefCount; // 引用计数
 		T* _ptr; // 指向管理资源的指针 
 		mutex* _pMutex; // 互斥锁
           

shared_ptr 通过引用计数的方法来确保资源的合理释放、同时还考虑到了线程安全的问题,使多个线程同时访问也可以保证线程安全。

但是shared_ptr 还是存在一个循环引用的问题

循环引用:

  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

    成员,所以这就叫循环引用,谁也不会释放。

就像这样:

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
	//最后谁也不会释放 还是会造成内存泄露问题
	return 0;
           

这样的问题如何解决呢?

接下来就要引入 weak_ptr 的概念了

2.5 weak_ptr

weak_ptr 使一种不控制所指向对象生存生存期的智能指针,它所指向由一个shared_ptr 管理的对象。将一个weak_ptr 绑定到一个shared_ptr,但是其不会改变 shared_ptr 的引用计数,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被销毁。

即使有weak_ptr 指向对象,该对象还是会被释放。

因此该智能指针的创建需要一个shared_ptr 来初始化它。

示例:

auto p = make_shared<int>(0);
weak_ptr<int> wp(p);
           

而shared_ptr 循环引用的缺陷也可以被解决

struct ListNode
{
	 int _data;
	 weak_ptr<ListNode> _prev;
	 weak_ptr<ListNode> _next;
	 ~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
 	shared_ptr<ListNode> node1(new ListNode);
	 shared_ptr<ListNode> node2(new ListNode);
	 node1->_next = node2;
	 node2->_prev = node1;
	 return 0;
}
           

继续阅读