在讲解之前,先讲述一种RAII思想.
目录
RAII
智能指针原理
auto_ptr
auto_ptr的介绍
auto_ptr的实现
unique_ptr
unique_ptr的介绍
unique_ptr实现
weak_ptr
weak_ptr的介绍
weak_ptr的实现
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命周期内始终保持有效
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
}
private:
T* _ptr;
};
这样我们最后便不再需要显式地释放资源了.
我们创建对象时,可以直接用匿名对象创建,也可以利用已有的指针创建.
int main()
{
int* ptr1 = new int;
SmartPtr<int> sp1(ptr1);
SmartPtr<int> sp2(new int);
}
这样即使在过程中抛异常,对象资源也会正常释放.
智能指针原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因为SmartPtr类中还得需要将* 、->重载下,才可让其像指针一样去使用.
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
这样才算是智能指针.
总结满足是智能指针必须有两个条件(原理):
1.RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。
是不是满足这两个条件就万事大吉了呢,也不能算是,下面将讲解四种常见的智能指针。
auto_ptr
auto_ptr的介绍
auto_ptr在C++98中就已经存在了,库中也有对应的函数.
class A
{
public:
A()
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 0;
int _a2 = 0;
};
int main()
{
auto_ptr<A> ap1(new A);
return 0;
}
这段代码没有任何问题,包括运行起来也能正常释放资源.
但是智能指针存在一个严重的问题:拷贝问题.
我们如果用我们刚才写的SmartPtr类进行拷贝:
SmartPtr<A> sp1(new A);
SmartPtr<A> sp2(ptr1);
这样会引发一个我们之前经常说的问题:同一块资源被析构两次.
sp2先析构一次,sp1再次析构这时就会出错.说到底还是因为浅拷贝问题.
那么我们改成深拷贝不就可以了吗?
答案也是不能,因为深拷贝就违背了我们的功能需求.
智能指针也是模拟原生指针的行为,我们本身的目的就是把资源交给对象管理。
其实我们仔细想一下,我们把sp1托给sp2,不也就是相当于让sp1和sp2共同管理这份资源吗.
所以我们这里要的就是浅拷贝!
这里和迭代器类似,迭代器浅拷贝没问题的原因是:不管迭代器中资源的释放.
那么库里的auto_ptr是如何解决的呢?
先来说结论:是转移了资源管理权.
一开始在ap1还没有赋值给ap2时,资源还在ap1中.
当执行完第二条语句:
我们发现ap1被悬空了,资源管理交给了ap2.
这是一种极不负责的行为,当后面有人想在解引用ap1中的内容时,便会报错.
这算是设计的一种很大的缺陷吧,很多公司也禁止使用auto_ptr.
auto_ptr的实现
namespace hyx {
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
cout << "~auto_ptr()" << endl;
delete _ptr;
}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
cout << "auto_ptr Delete()" << endl;
//将原来的指针置空
ap._ptr = nullptr;
}
//以ap1 = ap2为例
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
//如果当前ap1存在,则释放掉ap1
if (_ptr)
{
cout << "operator= Delete()" << endl;
delete _ptr;
}
//将ap2的资源转移给ap1
_ptr = ap._ptr;
//将ap2的指向置为空
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
unique_ptr
unique_ptr的介绍
unique_ptr对于拷贝问题设计思路非常简单粗暴:禁止拷贝和赋值.只适用于不需要拷贝的场景.
例如下面这段代码:
unique_ptr<A> up1(new A);
unique_ptr<A> up2(up1);//错误,unique_ptr不允许拷贝
unique_ptr<A> up3 = up2;//错误,unique_ptr不允许赋值
除了拷贝,其它方面和别的智能指针作用几乎是一样的.正常用即可.
unique_ptr实现
不能拷贝和赋值,我们直接将这两个函数设为delete即可,这是C++11中的特性.
而在C++98中,我们只能把它设为私有(防止有人可以在外部实现),并且只声明不实现(防止编译器自己生成).
namespace hyx {
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << " unique_ptr" << endl;
if (_ptr)
delete _ptr;
}
//C++11 将两个函数设为delete
unique_ptr (unique_ptr <T>& ap) = delete;
unique_ptr <T>& operator=(unique_ptr <T>& ap) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//C++98防拷贝的方式:私有+只声明不实现
//private:
// unique_ptr(unique_ptr <T>& ap);
// unique_ptr <T>& operator=(unique_ptr <T>& ap);
private:
T* _ptr;
};
}
shared_ptr
shared_ptr的介绍
那如果我们就是想要拷贝呢?这个时候shared_ptr便登场了.
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr.
那它是怎么支持的呢?浅拷贝还是深拷贝还是别的方法呢?
看下面这段代码
class A
{
public:
A()
{}
~A()
{ }
int _a1 = 0;
int _a2 = 0;
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2(sp1);
sp1->_a1++;
sp1->_a2++;
cout << sp2->_a1 << " " << sp2->_a2 << endl;
}
我们把sp1拷贝给了sp2,然后让sp1的两个成员分别+1,然后观察sp2的情况:
发现sp2两个成员也都+1,这就说明它们是共享这块资源的.
这不就是浅拷贝吗,那为什么不会出现析构多次出错的情况?
shared_ptr内部其实是采用引用计数的方式来实现的.
具体是怎么做的呢?
有一个计数器count,每当有新的对象来管理时,计数器便++,而每当有对象释放时,这个计数器便--,直到最后一个对象析构时,即count=1时,此时再把这块空间资源释放掉。如此便很好解决了问题.
比如刚才的sp1和sp2共同管理那一块空间资源,此时count=2,当sp2释放时,count--,此时count=1,说明还有其它对象也在管理这块空间,不要清理释放这块资源,所以直接count--即可.
sp1释放时,count=0,说明没有对象再管理这块空间了,此时便清理释放掉这块空间即可.
shared_ptr实现
刚才是我们的想法,但是具体要怎么实现呢?
我们把计数器_count放在哪呢?
如果直接加到类成员里面去,那么每个对象都会有一份独立的_count,这样就无法再进行计数了.
我们想要的_count一定是共享的,那我们把它设置成static可不可以?
答案也是不可以的.看下面的场景就知道了:
test::shared_ptr<A> sp1(new A);
test::shared_ptr<A> sp2(sp1);
test::shared_ptr<A> sp3(sp1);
其中test域是static修饰的_count,主要目的是为了演示.
这样目前看起来没有问题,sp1,sp2,sp3各管理一份,此时_count=3,没任何问题.
但如果此时我又加了一句这样的代码呢?
test::shared_ptr<int> sp4(new int);
此时我们期望的应该是重新生成一份_count,然后sp4再管理。
但按刚才说的static那种方法,_count会直接++,会让_count=4,这样肯定就错误了.
因为当sp4想释放资源时,由于_count并不为0,会导致sp4的资源不能正常释放.
而按常理来说,sp4和前面的1,2,3管理的并不是同一块资源,sp4不应该和他们公用计数器.此时sp4的_count=0理应被释放的.
这就是static修饰所带来的问题.也是为什么不能用static修饰的原因:所有资源都只有一个计数.
我们想要的是每个资源都有一个计数.
这里直接说正确的设计思路:在类内部加了一个指针.int* _pCount;
我们什么时候知道资源 来了呢?构造函数!
来一个新资源我们就new一个,这样就能保证每一份资源都能有计数了.
拷贝构造的时候,不仅把要把_ptr拷贝,也要把_pCount拷贝一份,同时让(*_pCount)++.
析构时,每次先--(*pCount),如果此时_pCount==0说明已经没有对象管理这块资源了,此时再释放掉这块资源.
比较需要注意且麻烦的是 赋值重载运算符.
1.以sp1=sp5为例,由于sp1要管理sp5指向的空间资源,此时应该先把sp1的计数--,如果sp1的计数为0,说明sp1是最后一个对象,需要释放掉当前的资源和计数,然后再指向sp5.
2.还有防止自己给自己赋值,以及管理相同资源的对象互相赋值,注意不要写this==&sp,因为这样只能解决前者,不能解决后者问题
namespace hyx
{
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pCount(new int(1))
{}
~shared_ptr()
{
if (--(*_pCount) == 0)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pCount(sp._pCount)
{
(*_pCount)++;
}
//以sp1=sp5为例.
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//防止自己给自己赋值,以及管理相同资源的对象互相赋值,注意不要写this==&sp,因为这样只能解决前者,不能解决后者问题
if (_ptr == sp._ptr)
{
return *this;
}
//由于sp1要管理sp5指向的空间资源,此时应该先把sp1的计数--,如果sp1的计数为0,
//说明sp1是最后一个对象,需要释放掉当前的资源和计数,然后再指向sp5
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
}
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pCount;
};
}
我们测试一下下面代码:
int main()
{
hyx::shared_ptr<A> sp1(new A);
hyx::shared_ptr<A> sp2(sp1);
hyx::shared_ptr<A> sp3(sp1);
hyx::shared_ptr<int> sp4(new int);
hyx::shared_ptr<A> sp5(new A);
hyx::shared_ptr<A> sp6(sp5);
}
一共创建了3份资源,所以最后也应该析构3次.
这样完美符合了我们的预期.
weak_ptr
weak_ptr的介绍
shared_ptr看起来很完美了,但是还存在一些小问题:循环引用.
看如下代码
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
return 0;
}
我们画图来演示问题所在:
循环引用分析:
1. node1和node2两个智能指针对象分别指向对应的结点,此时引用计数都是1.
2. node1的_next指向node2,node2的引用计数变为2,node2的_prev指向node1,node1的引用计数变成2。
3. node1和node2析构,引用计数分别减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
4.也就是说node1中_next析构了,node2就释放了;node2中_prev析构了,node1就释放了.
5.但是_next属于node的成员,node1释放了,_next才会析构,而node1由node2中的_prev管理,只有node2释放了,_prev才会释放,而node2又有node1的_next管理,形成了循环,所以这就叫循环引用,谁也不会释放。
为解决循环引用问题,便引用了weak_ptr,但weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源.
设计出来就是为了解决shared_ptr循环引用问题的.
_prev和_prev是weak_ptr时,它不参与资源释放管理,但是可以访问和修改资源.但是不增加计数,就不存在循环引用的问题了.
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
node1和node2的引用计数
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
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(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
public:
T* _ptr;
};
这样,智能指针的部分就讲解的差不多了.
其中,shared_ptr是最重要的一个智能指针,如何实现的一定要牢记!