初步认识智能指针
为什么需要智能指针
先看以下代码
cpp
#include<iostream>
int div()
{
int a, b;
std::cin >> a >> b;
if (b == 0)
{
throw std::invalid_argument("除0错误");
}
return a / b;
}
void func()
{
int* p1 = new int;
int* p2 = new int;
std::cout << div() << std::endl;
delete p1;
delete p2;
}
int main()
{
try
{
func();
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
当在func
中没有捕捉异常时,此时没有执行到后面的delete
语句,整个func函数就已经全部退出了,但是由于p1
和p2
所指向的空间是在堆区申请的,它们并没有被释放,但是当程序返回到main
函数后,也无法再次访问到申请的空间资源,这里就造成了内存泄漏的问题。
一个长期运行的程序出现内存泄漏,影响很大,比如操作系统或者后台服务等,出现内存泄漏后会导致响应越来越慢,最终卡死。想要在C++中避免内存泄漏的问题,智能指针是一个很好的选择
智能指针的使用及基本原理
RAII
RAII(Resource Acquisition is initialization)是一种利用对象生命周期来控制程序资源 的技术。在对象构造时获取资源 ,让资源的访问在该对象的生命周期内始终有效,最后在对象析构时释放资源 。就相当于是将管理一份资源的责任交给了一个对象。
将RAII思想运用到刚才的代码中
cpp
#include<iostream>
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~smart_ptr()
{
std::cout << "~smart_ptr()" << std::endl;
if (_ptr)delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a = 2, b = 0;
std::cout << "a=" << a << " " << "b=" << b << std::endl;
if (b == 0)
{
throw std::invalid_argument("除0错误");
}
return a / b;
}
void func()
{
smart_ptr<int>p1(new int);
smart_ptr<int>p2(new int);
std::cout << div() << std::endl;
}
int main()
{
try
{
func();
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
这里看到,即使是中间发生了异常,申请到的资源可以随着对象生命周期的结束而自动释放。作为一个指针,还需要实现解引用和->访问所知空间中的内容,使其拥有普通指针一样的行为
std::auto_ptr
C++98的库中就提供了auto_ptr的智能指针,auto_ptr的实现原理是:管理权转移思想。下面我用代码来演示以下
在以上代码中,先用p1管理申请出来的int大小的空间,然后让p2也来管理这块空间,最后通过p1来查看空间的内容,发现已经无法查看了,这是因为在用p2拷贝p1时,p1就将所申请空间的使用权交给了p2,自己所维护的指针置空。
以下就是我简单模拟实现的auto_ptr,帮助理解它的原理
cpp
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)delete _ptr;
}
auto_ptr(const auto_ptr<T>& ptr)
:_ptr(ptr._ptr)
{
ptr = nullptr;//管理权转移
}
auto_ptr& operator=(const auto_ptr<T>& ptr)
{
if (this != &ptr)
{
if (_ptr)
{
delete _ptr;
}
//管理权转移
_ptr = ptr._ptr;
ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
std::unique_ptr
C++11中提供了相较于std::auto_ptr
更为靠谱的std::auto_ptr
std::unique_ptr
的实现原理就是:简单粗暴的防止拷贝
cpp
template<class T>
class unique_ptr
{
public:
unique_ptr(T* p)
:_ptr(p)
{}
~unique_ptr()
{
if (_ptr)
delete _ptr;
}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T> operator=(const unique_ptr<T>&p) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
std::shared_ptr
C++11中还提供了靠谱且支持拷贝的shared_ptr
shared_ptr支持拷贝的原理就是通过引用计数的方法来实现多个shared_ptr对象之间共享资源
- shared_ptr内部都维护一个计数器,用来记录所管理的资源一共被几个对象所共享
- 对象在销毁 时(调用析构函数),就说明自己已经不在管理该资源了,这时,计数器里的值减一
- 如果计数为0,说明当前对象是最后一个指向该资源的对象 ,就需要该对象承担释放资源的任务
- 如果不是0,就说明除了自己还有其他对象在使用该份资源 ,就不能释放该资源,否则其他对象就找不到该资源了
std::shared_ptr的循环引用问题
cpp
#include<iostream>
#include<memory>
struct Node
{
int _val = 0;
std::shared_ptr<Node>_next;
std::shared_ptr<Node>_prev;
Node(int val = 0, std::shared_ptr<Node>next = nullptr, std::shared_ptr<Node>prev = nullptr)
:_val(val)
, _next(next)
, _prev(prev)
{}
~Node()
{
std::cout << "~Node()" << std::endl;
}
};
void test1()
{
std::shared_ptr<Node>node1(new Node);
std::shared_ptr<Node>node2(new Node);
std::cout << node1.use_count() << std::endl;
std::cout << node2.use_count() << std::endl;
node1->_next = node2;
node2->_prev = node1;
std::cout << node1.use_count() << std::endl;
std::cout << node2.use_count() << std::endl;
}
int main()
{
test1();
return 0;
}
可以看到在test1函数执行结束之后,两个指针所指向的空间并没有释放,下面就来分析以下原因
要解决这个问题,可以将结点中的_prev
和_next
改成weak_ptr
就可以了。原理就是node1->_next = node2;
和node2->_prev = node1;
使用weak_ptr
并不会增加node1
和node2
内部的引用计数。
weak_ptr
不支持RAII,并不直接参与资源管理(所以不支持使用一个普通指针构造对象)。但是weak_ptr
内部也会存在一个引用计数,这个计数的作用就是为了防止在shared_ptr
已经被释放的情况下,weak_ptr
再去访问被释放的资源(当内部计数器为0的时候就说明该资源已经被释放了)
std::shared_ptr删除器
如果shared_ptr
指向的资源并不是在堆区开辟的(new出来的空间),那么在析构时就不能直接delete。这个时候就需要在构造对象时传入一个自定义的"删除器",这个删除器就是一个仿函数
以下面为例
这里直接在构造的时候传入一个lambda表达式,这样shared_ptr在析构时就直接调用这个lambda表达式来管理对应的资源
std::shared_ptr的简单模拟实现
cpp
#include<atomic>
namespace lsh
{
template<class T>
struct DefaultDeleter
{
void operator()(T* p)
{
delete p;
}
};
struct spControlBlock
{
std::atomic<long> refcnt;
spControlBlock()
:refcnt(1)
{}
spControlBlock(spControlBlock&&) = delete;
void inref()
{
refcnt.fetch_add(1);
}
void decref()
{
if (refcnt.fetch_sub(1) == 1)//相当于(refcnt--) == 1
{
delete this;
}
}
long cntref()
{
return refcnt.load();
}
virtual ~spControlBlock() = default;
};
template<class T,class Deleter>
struct spControlBlockImpl :spControlBlock
{
T* _ptr;
Deleter _del;
spControlBlockImpl(T* ptr)
:_ptr(ptr)
{}
spControlBlockImpl(T* ptr, Deleter del)
:_ptr(ptr)
, _del(del)
{}
virtual ~spControlBlockImpl() override
{
//delete _ptr;
_del(_ptr);
}
};
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _cb(new spControlBlockImpl<T, DefaultDeleter<T>>(ptr))
{}
template<class Deleter>
shared_ptr(T* ptr, Deleter del)
: _ptr(ptr)
, _cb(new spControlBlockImpl<T, Deleter>(ptr, del))
{}
shared_ptr(const shared_ptr& that)
:_ptr(that._ptr)
, _cb(that._cb)
{
_cb->inref();
}
~shared_ptr()
{
_cb->decref();
}
long use_count()
{
return _cb->cntref();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
spControlBlock* _cb;
};
}
在模拟实现中,我创建了一个类spControlBlock
来对计数器进行封装,由于智能指针还可能运行在多线程环境下,所以内部对于计数器的操作必须是原子的,所以我将计数器定义为std::atomic<long>
类型,
spControlBlockImpl
负责删除的设计主要是为了分离内存管理和对象删除的逻辑。通过这种设计将删除逻辑与引用计数管理分开,使得 spControlBlock
只关注引用计数,而spControlBlockImpl
负责处理资源的销毁细节