RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源.也就是说对象会自己回收资源
有两个好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
之前讲的lock_guard和unique_lock都是采用了RAII技术。
其实本质上就是把这些资源封装成一个类,创建对象的时候调用构造函数,等对象生命周期过了之后,自动调用析构函数,去销毁自己的资源。
智能指针必须满足的两点是:
- 使用RAII技术
- 能像指针一样使用该智能指针对象,因此要重载*和->两个符号
auto_ptr
auto_ptr是第一个出现的智能指针。 auto_ptr是c++98就出现了,但是并不好用,很多地方甚至明文规定禁止使用。它的逻辑大致如下:
namespace my
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* _ptr = nullptr)
{
ptr = _ptr;
}
~auto_ptr()
{
delete ptr;
ptr = nullptr;
cout << "~auto_ptr()" << endl;
}
T& operator*()
{
return *ptr;
}
T* operator->()
{
return ptr;
}
private:
T* ptr;
};
}
int main()
{
my::auto_ptr<int> ap1 = new int(1);
cout << *ap1 << endl;
}
我们可以发现它自己调用了析构函数去析构它管理的资源,满足了要求。 auto_ptr不好用的原因在于拷贝构造和赋值构造,后序讲的几个指针指针都是为了解决这个问题而出现的。
我们知道,如果只是单纯的浅拷贝,两个指针指向同一块空间,就会析构两次,肯定报错。
auto_ptr是这样解决问题的:如果发生了拷贝,就把第一个指针对空间的管理权给第二个指针。
代码逻辑如下:
auto_ptr(auto_ptr<T>& ap)
{
ptr = ap.ptr;
ap.ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (ptr) delete ptr; 把自己的资源释放
ptr = ap.ptr; 自己指向别人的支援
ap.ptr = nullptr; 管理权转移,赋值后指针悬空
}
return *this;
}
很明显这就有了一些问题:比如拷贝后会发生指针悬空的现象。但是使用者很可能不知道这是一个悬空的指针,依旧去对他解引用,这就会对空指针解引用,直接崩溃了程序。
这也是auto_ptr不允许使用的原因。
unique_ptr
C++11中开始提供更靠谱的unique_ptr. unique_ptr面对拷贝和赋值的策略更加粗暴了。
就是直接禁止使用拷贝和赋值。
unique_ptr(unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(unique_ptr<T>&) = delete;
shared_ptr
shared_ptr才解决了拷贝和赋值的问题。它用的技术叫引用计数
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* _ptr = nullptr)
{
ptr = _ptr;
count = new int(1);
mtx = new mutex;
}
~shared_ptr()
{
ReleaseRef();
}
T& operator*()
{
return *ptr;
}
T* operator->()
{
return ptr;
}
shared_ptr(shared_ptr<T>& ap)
{
ptr = ap.ptr;
count = ap.count;
mtx = ap.mtx;
AddRef();
}
shared_ptr<T>& operator=(shared_ptr<T>& ap)
{
if (this != &ap)
{
ReleaseRef();
ptr = ap.ptr;
count = ap.count;
mtx = ap.mtx;
AddRef();
}
return *this;
}
private:
T* ptr;
int* count;
mutex* mtx;
void AddRef()
{
if (count)
{
mtx->lock();
(*count)++;
mtx->unlock();
}
}
void ReleaseRef()
{
mtx->lock();
bool flag = true;
if (-- (* count) == 0)
{
if (ptr)
{
cout << "delete" << endl;
delete ptr;
}
delete count;
flag = false;
}
mtx->unlock();
这里注意不能在unlock之前有析构锁的可能性,因此析构锁要放在最后析构
if (flag == false) delete mtx;
}
};
ps:加锁的原因是++和–不是原子的,并且count计数其实是临界资源。因此有线程安全问题,加锁。
另外:shared_ptr指向的资源有可能也是临界资源,但这个线程安全问题是用程序员来避免的,不关shared_ptr的事。(即使用普通指针也会有线程安全问题)
use_count()
std标准库中,shared_ptr有一个这个函数,是用来返回引用计数的。
删除器
对于shared_ptr指向的资源不一定是new出来的,然而上面我们写的删除ptr都是用的delete,万一shared_ptr指向了一个文件指针,用delete删除就会直接崩溃了。
因此我们要提供删除器。 这是库里面带有删除器的shared_ptr构造函数。 这个D可以是任何可调用对象,因此用lambda也行,写仿函数也行。
比如下面这种写法:
- 模板T别写成指针了
- 这里的lambda都要传参,因为没有东西给你捕获。
shared_ptr<FILE> sp1(fopen("test.txt", "r"), [](FILE* ptr){fclose(ptr);});
循环引用
循环引用的后果是内存得不到正确的释放。 循环引用最简单的场景就是双链表。
struct ListNode
{
int data;
my::shared_ptr<ListNode> next;
my::shared_ptr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
```cpp
int main()
{
my::shared_ptr<ListNode> node1(new ListNode);
my::shared_ptr<ListNode> node2(new ListNode);
node1->next = node2;
node2->prev = node1;
}
上面代码计算机是这么执行的: 然后有些人是这么描述这个现象的:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
总结一下就是:要析构node1,就必须让count变成0,但是要让count变成0,就必须要先析构node1,才能去析构node1的成员。因此循环了。
weak_ptr
weak_ptr一般不会单独使用,它就是为了解决循环引用才出现的。
解决方法就是把ListNode成员的shared_ptr改成weak_ptr.weak_ptr不会增加引用计数。因此上述场景就不存在了。
weak_ptr的实现就是weak_ptr指向shared_ptr的那段空间。然后不增加引用计数即可。
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
直接拿指针
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
直接拿到shared_ptr的指针,就可以了
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
struct ListNode
{
int data;
my::weak_ptr<ListNode> next;
my::weak_ptr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
写逻辑结构是循环的东西时,成员不要用shared_ptr,要用weak_ptr
|