为什么要智能指针
通过普通指针,我们能轻松访问到数据的地址。但是,我们往往会忘记在合适的地方释放它。那么有没有一种方法,来辅助我们释放指针呢?答案是有的,就是接下来要介绍的智能指针。
栈空间和堆空间
我们先引入两个概念,栈空间和堆空间。简单来说,栈空间由系统分配和释放,堆空间由程序猿申请和释放。我们可以利用栈的特性来帮助释放指针。
class HelpPtr
{
public:
HelpPtr() {}
~HelpPtr() {}
};
int main()
{
HelpPtr ptr;
return 0;
}
先看上面的代码,在main函数中,我们申明了一个局部变量。根据栈空间的特性,在函数返回后,ptr是不是就自动释放了呢,释放时会调用析构函数。发散一下,如果ptr中保存了一个指针,在析构函数中将指针delete,我们没有主动调用delete,HelpPtr帮我们做了。
class A
{
public:
A() {}
};
class HelpPtr
{
public:
HelpPtr(A* pA)
{
m_pA = pA;
}
~HelpPtr()
{
delete m_pA;
}
private:
A* m_pA = nullptr;
};
int main()
{
A* pA = new A();
HelpPtr ptr(pA);
return 0;
}
再看上面的代码,将第一部分代码稍作变形,将指针A保存到HelpPtr中,即使没有在main函数中调用delete,妈妈再也不担心内存泄漏了。
以下内容只会介绍C++11之后的智能指针,C++11之前的std::auto_ptr已经被摒弃,另外例如非官方大神实现的scoped_ptr智能指针,这里都不做介绍。
独享智能指针(unique_ptr)
std::unique_ptr是一个独享型的智能指针,它不允许其他的智能指针共享其内部的指针。
独享智能指针帮助我们管理释放指针,它不允许其他智能指针共享其内部的指针。为实现这种效果,我们应该把独享类的拷贝构造函数和赋值函数都隐藏起来。当然,为了这个独享类能将其他类包装成智能指针,必须使用模板类的方式来实现。
//unique.h
#pragma once
template<typename T>
class UniquePtr
{
public:
UniquePtr(T* p = nullptr) :m_p(p){}
~UniquePtr()
{
if (m_p) delete m_p;
}
T* operator->()
{
return this->m_p;
}
T& operator*()
{
return *(this->m_p);
}
T* get()
{
return this->m_p;
}
protected:
UniquePtr(const UniquePtr<T>& ptr) {}
UniquePtr<T>& operator=(const UniquePtr<T>& ptr) {}
private:
T* m_p = nullptr;
};
#pragma once
#include <iostream>
#include "unique.h"
using namespace std;
class A
{
public:
A() {}
};
int main()
{
A* pA = new A();
UniquePtr<A> APtr(pA);
UniquePtr<A> A2Ptr = APtr; //报错
return 0;
}
上面的代码编译时,会报如下错误。因为我们实现的是独享指针,那么就不能将智能指针赋值给其他的智能指针,这其实就是scope_ptr的实现。
C++11后形成了std::unique_ptr智能指针,基本和scope_ptr相同,不过它们之间也有一些细微差别。就是unique_ptr可以对右值进行转移,对右值转移这是啥意思呢?说明白了就是提供了一种特殊方法可以将unique_ptr赋值给另一个unique_ptr,被转移后的unique_ptr也就不能再处理之前管理的指针了。
//unique.h
#pragma once
template<typename T>
class UniquePtr
{
public:
UniquePtr(T* p = nullptr) :m_p(p){}
~UniquePtr()
{
if (m_p) delete m_p;
}
T* operator->()
{
return this->m_p;
}
T& operator*()
{
return *(this->m_p);
}
T* get()
{
return this->m_p;
}
UniquePtr(UniquePtr<T>&& ptr) noexcept
: m_p(ptr.m_p)
{
ptr.m_p = nullptr;
}
UniquePtr& operator=(UniquePtr<T>&& ptr) noexcept
{
if (this != &ptr)
{
m_p = ptr.m_p;
ptr.m_p = nullptr;
}
return *this;
}
protected:
UniquePtr(const UniquePtr<T>& ptr) {}
UniquePtr<T>& operator=(const UniquePtr<T>& ptr) {}
private:
T* m_p = nullptr;
};
#pragma once
#include <iostream>
#include "unique.h"
using namespace std;
class A
{
public:
A() {}
};
int main()
{
A* pA = new A();
UniquePtr<A> APtr(pA);
//UniquePtr<A> A2Ptr = APtr; //报错
UniquePtr<A> A2Ptr = std::move(APtr);
return 0;
}
上面main函数中的第四行调用的是拷贝构造函数,由于该函数不是public属性,所以调用该行时会失败。而第五行会调用移动构造函数,因为我们已经实现了移动构造函数,所以该行可以编译成功。在运行时,当APtr移动给A2Ptr 后,APtr也就失去了对原指针的控制权,这在代码中也有体现就是将 ptr.m_p设置为nullptr了。
上面就是C++11标准中的unique_ptr的实现,这样一分析下来也是蛮简单的对吧。
共享智能指针(shared_ptr)
虽然unique_ptr已经很好用了,但有时候我们还是需要多个智能指针管理同一块堆内存空间。有个不错的解决方案—引用计数法。其基本原理是当有多个智能指针指对同一块堆空间进行管理时,每增加一个智能指针引用计数就增1,每减少一个智能指针引用计数就减少。当引用计数减为0时,就将管理的堆空间释放掉。
看文字可能比较抽象,直接上代码:
//shared.h
#pragma once
template<typename T>
class SharedPtr
{
public:
SharedPtr(T* p = nullptr) :m_p(p), m_pRefCnt(new int(1)) {}
SharedPtr(const SharedPtr<T>& ptr)
{
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(*m_pRefCnt);
}
SharedPtr<T>& operator=(const SharedPtr<T>& ptr)
{
if (this != &ptr)
{
releaseRef(); //接受另外一个智能指针时,应该将自身的先释放掉
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(*m_pRefCnt);
}
return *this;
}
~SharedPtr()
{
releaseRef();
}
T* operator->()
{
return this->m_p;
}
T& operator*()
{
return *(this->m_p);
}
T* get()
{
return this->m_p;
}
int refCnt()
{
return *m_pRefCnt;
}
private:
void releaseRef()
{
if (--(*m_pRefCnt) == 0)
{
delete m_p;
delete m_pRefCnt;
}
}
private:
T* m_p = nullptr;
int* m_pRefCnt = nullptr;
};
#pragma once
#include <iostream>
#include "shared.h"
using namespace std;
class A
{
public:
A() {}
};
int main()
{
A* pA = new A();
SharedPtr<A> APtr(pA);
SharedPtr<A> A2Ptr = APtr; //不会报错
return 0;
}
从结果中我们可以看到创建APtr时引用计数为 1,将APtr赋值给A2Ptr时引用计算都为2。当main程序结束后首先释放A2Ptr ,其引用计数都减1。再释放APtr,引用计数减为0,当引用计数为0时,释放堆空间。这样的智能指针还是非常棒的,我们再也不怕内存泄漏了!
可惜的是,我们高兴的有点早,当指针循环指向的时候,还是会出现内存泄漏的情况。例如:
struct Node
{
SharedPtr<Node> _prev;
SharedPtr<Node> _next;
~Node()
{
std::cout << "delete :" <<this<< std::endl;
}
};
SharedPtr<Node> ptrCur(new(Node));
SharedPtr<Node> ptrNext(new(Node));
ptrCur->_next = ptrNext;
ptrNext->_prev = ptrCur;
这里大家可以自行分析一下释放顺序,有助于大家进一步理解引用计数,这里就不分析内存泄漏的原因了。
弱智能指针(weak_ptr)
为了解决这个bug,C++11提供了一个辅助指针类型weak_ptr。实际上weak_ptr不能单独称为一个智能指针,它必须与shared_ptr一起使用,起到辅助share_ptr的作用。我们来看看它是如何解决上述问题的吧。
在之前的shared.h上修改,直接上代码:
#pragma once
struct Counter
{
int sRef = 0; //存放SharedPtr引用计数
int wRef = 0; //存放WeakPtr的引用计数
};
template<typename T>
class WeakPtr;
template<typename T>
class SharedPtr
{
public:
SharedPtr(T* p = nullptr) :m_p(p), m_pRefCnt(new Counter())
{
if (p != nullptr)
{
m_pRefCnt->sRef = 1;
}
}
SharedPtr(const SharedPtr<T>& ptr)
{
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(m_pRefCnt->sRef);
}
SharedPtr(const WeakPtr<T>& ptr)
{
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(m_pRefCnt->sRef);
}
SharedPtr<T>& operator=(const SharedPtr<T>& ptr)
{
if (this != &ptr)
{
releaseRef(); //接受另外一个智能指针时,应该将自身的先释放掉
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(m_pRefCnt->sRef);
}
return *this;
}
~SharedPtr()
{
releaseRef();
}
T* operator->()
{
return this->m_p;
}
T& operator*()
{
return *(this->m_p);
}
T* get()
{
return this->m_p;
}
int refCnt()
{
return m_pRefCnt->sRef;
}
private:
void releaseRef()
{
if (--(m_pRefCnt->sRef) == 0)
{
delete m_p;
if(m_pRefCnt->wRef == 0)
{
delete m_pRefCnt;
m_pRefCnt = nullptr;
}
}
}
private:
T* m_p = nullptr;
Counter* m_pRefCnt = nullptr;
friend class WeakPtr<T>;
};
template<typename T>
class WeakPtr
{
public:
WeakPtr(){}
WeakPtr(const WeakPtr<T>& ptr)
{
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(m_pRefCnt->wRef);
}
WeakPtr(const SharedPtr<T>& ptr)
{
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(m_pRefCnt->wRef);
}
WeakPtr<T>& operator=(const WeakPtr<T>& ptr)
{
if (this != &ptr)
{
releaseRef(); //接受另外一个智能指针时,应该将自身的先释放掉
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(m_pRefCnt->wRef);
}
return *this;
}
WeakPtr& operator=(const SharedPtr<T>& ptr)
{
releaseRef();
m_p = ptr.m_p;
m_pRefCnt = ptr.m_pRefCnt;
++(m_pRefCnt->wRef);
return *this;
}
~WeakPtr()
{
releaseRef();
}
SharedPtr<T> lock()
{
return SharedPtr<T>(*this);
}
bool expried()
{
if (m_pRefCnt && m_pRefCnt->sRef > 0)
return false;
return true;
}
private:
void releaseRef()
{
if (m_pRefCnt)
{
--(m_pRefCnt->sRef);
if (m_pRefCnt->wRef < 1 && m_pRefCnt->sRef < 1)
{
m_pRefCnt = nullptr;
}
}
}
private:
T* m_p = nullptr;
Counter* m_pRefCnt = nullptr;
};
#pragma once
#include <iostream>
#include "shared.h"
using namespace std;
struct Node
{
WeakPtr<Node> _prev;
WeakPtr<Node> _next;
~Node()
{
std::cout << "delete :" << this << std::endl;
}
};
int main()
{
SharedPtr<Node> cur(new(Node));
SharedPtr<Node> next(new(Node));
cur->_next = next;
next->_prev = cur;
return 0;
}
以上就是WeakPtr的实现以及SharedPtr的改造,从中我们可以看到,SharedPtr与我们之前的SharedPtr区别并不是很大,主要做了以下三点修改:
1、以前只有一个计数器,然在变成了两个,一个是SharedPtr本身的计数,另一个是WeakPtr的计数;
2、是增加了一个参数为WeakPtr引用的拷贝构造函数;
3、m_ptr和m_pRefCnt的释放都是在SharedPtr中完成的,WeakPtr不做具体的释放工作。
WeakPtr是新增加的弱指针,它是配合SharedPtr使用的,自己并不能单独使用。WeakPtr也包含m_ptr和m_pRefCnt两个成员,但它更多是是引用,对它们没有创建和释放权。另外在WeakPtr中会对Counter对象的wRef操作,也就是说多个WeakPtr指向同一个堆空间时,它仅操作Counter中的wRef。
因此,对于我们之前的SharedPtr形成环后导致的内存泄漏可以通过WeakPtr对其进行改造,这样内存泄漏的问题就迎刃而解了。
同样的,上面main函数的释放过程,大家自己分析。