介绍
智能指针基于RAII的思想设计。
RAII:利用对象生命周期来控制程序资源。在对象构造时获取资源,在对象析构时释放资源,则不再需要显式释放资源,同时在对象的生命周期内,对该资源的访问始终有效。即将管理资源的责任托管给了对象。
智能指针本质上就是使用对象对裸指针进行封装,在析构函数中delete该指针指向的内存,同时重载该对象的*和->运算符模拟指针的操作。则在对象生命周期结束时,会自动调用其析构,即自动释放掉其管理的资源。
简单的智能指针实现如下(注:文章中代码不够严谨,仅用于表达设计原理):
cpp
template <class T>
class smart_ptr
{
public:
smart_ptr(T *ptr) : _ptr(ptr) {}
~smart_ptr(){ delete _ptr; }
T &operator*(){ return *_ptr; }
T *operator->(){ return _ptr; }
private:
T *_ptr;
};
上述简单的智能指针的问题在于:多个智能指针指向同一份资源可能出现资源被多次释放问题。如:
cpp
smart_ptr<int> p1(new int);
smart_ptr<int> p2=p1; // p1和p2指向同一份资源
针对不同的解决方法则有了多种类型的智能指针:
- 管理权转移-C++98的auto_ptr
- 防拷贝-C++11的unique_ptr
- 引用计数的共享拷贝-C++11的shared_ptr
auto_ptr
auto_ptr在C++98中被提出,其只是简单的将源指针置为nullptr来达到转移的效果,即:
cpp
auto_ptr(auto_ptr<T>& p)
{
_ptr=p._ptr;
p._ptr=nullptr;
}
但是该设计并不好,在C++11中被弃用。
unique_ptr
其简单粗暴,直接就不提供拷贝构造和赋值拷贝,达到防拷贝的效果。即
cpp
unique_ptr(unique<T>& p)=delete;
unique_ptr<T>& operator=(unique_ptr<T>& p)=delete;
shared_ptr
指向同一资源的多个shared_ptr共同管理一份引用计数,每个shared_ptr对象则持有该引用计数的指针。
cpp
T *_ptr;
int *_count; // 引用计数
shared_ptr(T *ptr) : _ptr(ptr), _count(new int(1)) {}
~shared_ptr(){ release(); }
shared_ptr(const shared_ptr<T> &p) : _ptr(p._ptr), _count(p._count){ ++(*_count); }
shared_ptr<T> &operator=(const shared_ptr<T> &p)
{
if (this != &p)
{
release();
_ptr = p._ptr;
_count = p._count;
++(*_count);
}
return *this;
}
void release()
{
if (--(*_count) == 0)
{
delete _ptr;
_ptr=nullptr;
delete _count;
_count=nullptr;
}
}
shared_ptr的线程安全问题
上述的shared_ptr当在不同线程中同时进行拷贝构造、赋值拷贝、析构,极有可能会导致其*_count出错。
故:引入互斥锁,保证并发安全。
cpp
T *_ptr;
int *_count;
std::mutex *_mtx; // 互斥锁
void add_ref_count()
{
_mtx->lock();
++(*_count);
_mtx->unlock();
}
void release()
{
_mtx->lock();
bool need_delete = false;
if (--(*_count) == 0)
{
delete _ptr;
_ptr = nullptr;
delete _count;
_count = nullptr;
need_delete = true;
}
_mtx->unlock();
if (need_delete)
{
delete _mtx;
_mtx = nullptr;
}
}
weak_ptr
shared_ptr的循环引用问题
考虑以下场景:
cpp
struct ListNode
{
int val;
shared_ptr<ListNode> prev;
shared_ptr<ListNode> next;
};
void fun()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->next=node2;
node2->prev=node1;
}
由于ListNode中的前置和后置的shared_ptr,node1和node2的引用计数都为2,当fun函数结束析构node1和node2时,其引用计数变为1,并不会释放指向的对应资源。
为了辅助shared_ptr解决该循环引用场景,引入了weak_ptr。
weak_ptr
该指针本质上就是指针的封装,但并不控制指向对象的生命周期,并不符合RAII,或者严格上来说其并不是智能指针。
weak_ptr也不会影响shared_ptr的引用计数。
cpp
template <class T>
class weak_ptr
{
public:
weak_ptr()=default;
weak_ptr(const shared_ptr<T>& p):_ptr(p.get()){}
weak_ptr<T> operator=(const shared_ptr<T>& p)
{
_ptr=p.get();
return *this;
}
private:
T* _ptr;
};
则上述的循环引用可使用weak_ptr解决:
cpp
struct ListNode
{
int val;
weak_ptr<ListNode> prev;
weak_ptr<ListNode> next;
};
void fun()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->next=node2;
node2->prev=node1;
}
删除器
上述的智能指针都只是可以自动释放new出来的对象,智能指针封装的并非对象指针,而是如:malloc返回的指针,或是FILE*等资源,则需要手动传入一个删除器仿函数。
通过定制删除器可以定制智能指针中对资源的释放。
cpp
template<class T>
class freer
{
void operator()(T* p){ free p; }
};
int main()
{
// 第二个参数传入仿函数
shared_ptr<int> a((int*)malloc(sizeof(int)*10),freer<int>());
}