C++?智能指针!!!
一、引言
在平常的写代码过程中,难免会遇到new完空间不释放的情况,甚至有时候我们记得释放内存,但是代码中抛出的异常改变了程序的运行,导致delete没有成功,这些情况造成的内存泄漏非常让人头疼, 所以我们希望new出的空间可以自动释放,今天我们将要一起学习的智能指针也就应运而生了
二、智能指针的相关概念及原理
1、RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术
我们将借助对象,在对象构造的时候申请资源,在对象出作用域的时候借助对象的析构函数释放资源,这样做有两个好处:
(1).不需要显示的释放资源
(2).在对象的生命周期内资源一直有效
要想实现这样的功能,非常简单:
//实现RAII
template<class T>
class smart_ptr
{smart_ptr(T* ptr):_ptr(ptr){}~smart_ptr(){delete _ptr;}//成员变量T* _ptr = nullptr;
};
上面的代码就可以实现RAII的功能,在ptr传入时,smart_ptr完成构造,在出smart_ptr作用域时,ptr所指向的空间被自动释放,但是这时候的smart_ptr还不能被称为一个指针,因为它还不能像指针一样使用
2、让smart_ptr能够像指针一样被使用
事实上想让smart_ptr可以像指针一样被使用也很简单,重载operator*和operator->即可:
T& operator*()
{return *_ptr;
}
T* operator->()
{return _ptr;
}
三、四种智能指针的实现
注:上面所实现的简单smart_ptr存在一个显著的问题:当遇到拷贝的情景时,程序就会出bug,这也很好理解,当多个智能指针指向同一块空间时,会在出作用域时对这块空间多次释放,这就导致程序出bug,所以下面的四种智能指针都给出了自己的解决方法
1、auto_ptr
auto_ptr给出的解决方案是管理权的转移,也就是说,当发生赋值时,赋值方将指向空间的管理权交给被赋值方,而自己置空:
class auto_ptr
{auto_ptr(T* ptr):_ptr(ptr) {}~auto_ptr(){if(_ptr)delete _ptr;}auto_ptr(auto_ptr& ptr):_ptr(ptr._ptr){ptr._ptr = nullptr;}autp_ptr& operator=(auto_ptr& ptr){_ptr = ptr._ptr;ptr._ptr = nullptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//成员变量T* _ptr = nullptr;
};
auto_ptr的出现的确解决了赋值相关的问题,但是这种管理权转移的方法同样存在着很严重的隐患,就是悬空指针的问题,当一个auto_ptr被用于给其它指针赋值时,它就不再管理一块空间,这之后如果再对它进行使用就会出现问题,当然,如果可以在这之后保证不再使用它,那么auto_ptr是可以被使用的,但是要注意,在平常的工作中还是不建议使用auto_ptr,甚至很多企业禁止使用它
2、unique_ptr
unique_ptr给出的解决方案是简单粗暴的,既然赋值相关的情景会导致smart_ptr出bug,那么就简单粗暴的禁止拷贝构造和赋值运算符重载:
class unique_ptr
{unique_ptr(T* ptr):_ptr(ptr) {}~unique_ptr(){if (_ptr)delete _ptr;}unique_ptr(unique_ptr& ptr) = delete;unique_ptr& operator=(unique_ptr& ptr) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//成员变量T* _ptr = nullptr;
};
这种方式是简单粗暴的,如果此时的情景完全不会涉及到赋值相关,那么我们很推荐选择unique_ptr,但是事实是不会涉及到复制相关的情景微乎其微,所以我们还需要更好的解决办法
3、shared_ptr
顾名思义,shared_ptr是支持赋值的,它给出的解决方案是记录管理某一块空间的指针数量,如果此时出作用域的是最后一个,那么释放,其余情况不进行释放,显然,这种处理方式是目前最好的:
template<class T>
struct shared_ptr
{shared_ptr(T* ptr):_ptr(ptr){_pcount = new int(1);}~shared_ptr(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}}//拷贝构造函数shared_ptr<T>(const shared_ptr<T>& ptr):_ptr(ptr._ptr), _pcount(ptr._pcount){(*_pcount)++;}//赋值运算符重载shared_ptr<T>& operator=(const shared_ptr<T>& ptr){if (_ptr == ptr._ptr){return *this;}if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = ptr._ptr;_pcount = ptr._pcount;(*_pcount)++;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//成员变量T* _ptr = nullptr;int* _pcount = nullptr;
};
上面的实现过程中,重点就是赋值运算符重载和拷贝构造函数的实现,通过动态的储存管理某块空间指针的个数从而实现了需要释放空间时释放,不需要释放时进行其它操作,看起来这种处理方式已经天衣无缝了,真的是这样吗?我们一起来看下面一段代码:
struct Node
{int x = 0;bea::shared_ptr<Node> prev = nullptr;bea::shared_ptr<Node> next = nullptr;
};int main()
{bea::shared_ptr<Node> ptr1(new Node);bea::shared_ptr<Node> ptr2(new Node);ptr1->next = ptr2;ptr2->prev = ptr1;return 0;
}
上面这段代码运行起来看起来是没有什么问题,智能指针也可以正常使用,但是当我们仔细分析这段代码时,就会发现一些问题:
当将要释放空间时,会出现下面这样一个奇怪的循环:
想要释放ptr1,就要释放prev
想要释放prev,就要释放ptr2
想要释放ptr2,就要释放next
想要释放next,就要释放ptr1
......
为什么会出现上面的问题呢?关键就是shared_ptr采取了计数的方式,只有最后一个管理该空间的智能指针才能释放空间
上面的问题就是shared_ptr的死穴,也就是被称为循环引用的问题,事实上,在使用智能指针时我们要尽量避免循环引用的情况出现,正确的使用shared_ptr就可以很好的解决空间泄露的问 题,但是,总会有不可避免的出现循环引用的问题,这时候该怎么办呢?继续看下去吧!
4、weak_ptr
顾名思义,weak_ptr被称为弱指针,