本博客代码在vs2022底下调试与实现
目录
为什么需要智能指针?
因为我们很容易对于指针管理的资源忘记释放,这就很容易造成资源泄漏
RAII
提到智能指针,就不得不提一下RAII,这是一种思想
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。
说白了,就是把资源进行包装,利用析构函数的特性,来帮助我们释放资源
auto_ptr
auto_ptr的实现
#pragma once
namespace czz {
template <class T>
class auto_ptr {
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _isOwner(false)
{
if (_ptr != nullptr) {
_isOwner = true;
}
}
~auto_ptr() {
if (_ptr != nullptr && _isOwner == true) {
delete _ptr;
_ptr = nullptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
//解决资源释放的问题:资源转移
auto_ptr(const auto_ptr<T>& ap)
:_ptr(ap._ptr)
, _isOwner(ap._isOwner) {
ap._isOwner = false;
}
auto_ptr<T>& operator=(const auto_ptr<T>& ap) {
if (this != &ap) {
if (_ptr != nullptr && _isOwner == true) {
delete _ptr;
}
_ptr = ap._ptr;
_isOwner = ap._isOwner;
ap._isOwner = false;
}
return *this;
}
private:
T* _ptr;
//_isOwner有两个用处,判断资源是否存在,以及此对象是否拥有释放权;
mutable bool _isOwner;
};
}
对auto_ptr的研究
先谈谈历史,auto_ptr是c++98里面提出的概念,在后来的修正中改过一次,但是最后又改回去了.后来标准库给的建议是:任何情况下都不建议使用.
那既然改来改去,就表示有问题嘛,而且还没有得到解决,所以才建议任何情况下都不建议使用.
98版auto_ptr(上面代码)的问题
我们知道,一个类,要有拷贝构造和赋值运算符重载,那对于包装的指针,浅拷贝肯定不行,因为它涉及到了资源的管理,如果两个对象用一个资源,那一个释放资源以后,另一个怎么办?所以98版解决这个问题的办法就是资源转移,就是拷贝构造或者赋值运算符重载的时候,把前一个对象的资源交给后一个对象,但这也有一个问题:两个指针不能同时指向同一个资源.
后来修正的auto_ptr的问题
修正,自然就是解决前面的问题,那是怎么修正的呢,就是按照我上面的代码,也就是大家都能使用,但只有一个有资格释放,但是这下问题更大了,可能直接就造成野指针了,比如前面已经把资源释放了,后面又用指向这个资源的其他的指针,所以c++11又改回去了.那c++11到底搞了什么,让人们放弃使用auto_ptr呢?往下看.
unique_ptr
unique_ptr实现
简单的让大家看个思想
#pragma once
namespace czz {
template <class T>
class unique_ptr {
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr() {
if (_ptr != nullptr) {
delete _ptr;
_ptr = nullptr;
}
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
相信大家也能看懂,就是直接不允许实现拷贝构造和赋值运算符重载,这个没什么好说的,好用,唯一可惜的就是一份资源只能由一个指针管理
shared_ptr
这个是为了解决上面的那个问题而实现的,对于资源问题,也就是涉及到拷贝构造和赋值运算符重载,它采用的就是引用计数,先看代码
shared_ptr的实现
#pragma once
#include <mutex>
namespace czz {
template<class T>
struct DFDef {
void operator()(T*& ptr) {
if (ptr) {
delete ptr;
ptr = nullptr;
}
}
};
template <class T, class DF = DFDef<T>> //可能资源不是new的,所以需要别的方法去释放
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr), _pRefCount(nullptr), _pMutex(nullptr) {
if (_ptr) {
_pRefCount = new int(1);
_pMutex = new std::mutex;
}
}
~shared_ptr() {
Release();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
shared_ptr(const shared_ptr<T, DF>& sp)
:_ptr(sp._ptr), _pRefCount(sp._pRefCount) {
_pMutex = new std::mutex;
AddRef();
}
shared_ptr<T, DF>& operator=(const shared_ptr<T, DF>& sp) {
if (_ptr != sp._ptr) {
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = new std::mutex;
AddRef();
}
return *this;
}
size_t use_count()const {
return *_pRefCount;
}
T* Get() {
return _ptr;
}
private:
void AddRef() {
if (_pRefCount != nullptr) {
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
}
int SubRef() {
_pMutex->lock();
--(*_pRefCount);
_pMutex->unlock();
return *_pRefCount;
}
void Release() {
if (_ptr != nullptr && SubRef() == 0) {
DF()(_ptr);
delete _pRefCount;
delete _pMutex;
_ptr = nullptr;
_pRefCount = nullptr;
}
}
private:
T* _ptr;
int* _pRefCount;
std::mutex* _pMutex;
};
}
shared_ptr的问题
我们举个栗子:
对于一个双向链表的一个节点,我们可以把里面它本身和它里面的两个指向前(prev)后(next)的指针都改成shared_ptr,现在有一个链表有两个节点,A节点的next指向B节点,B节点的prev指向A节点,那现在不要B节点了,它的引用计数减1,但因为A节点的next还指向这B节点的东西,所以B节点的资源不能直接释放,同理,A节点也一样,而且推导下去,你会发现,这种情况下,无论如何都无法释放这两个资源
解决问题的办法
weak_ptr:这个就是专门解决shared_ptr的,也很简单,就是只有主资源采用shared_ptr,其他的采用weak_ptr就行了,在上面的例子里面,对于节点本身,我们用shared_ptr管理,节点里面的如prev和next,我们采取weak_ptr就可以了,至于原理,大概说一下,就是引用计数其实不是一个,而是两个,一个管理shared_ptr,一个管理weak_ptr