深入篇【C++】总结智能指针的使用与应用意义&&(auto_ptr/unique_ptr/shared_ptr/weak_ptr)底层原理剖析+模拟实现
- 智能指针的出现
- 智能指针的使用
- 应用意义/存在问题
- 智能指针原理剖析+模拟实现
- auto_ptr
- unique_ptr
- shared_ptr
- weak_ptr
智能指针的出现
首先我们要理解智能指针是什么。为什么要有智能指针。什么场景会用到智能指针。
首先我们知道C++的异常有很大的缺陷,那就是执行流会乱跳,这样就可能会造成内存泄露问题,比如在new和delete之间出现异常,那么就会出现资源没有释放,内存泄露。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
div();
}
int main()
{
try
{
func();
}
catch(const exception& e)//抛基类异常,用父类来接受
{
cout << e.what() << endl;
}
}
首先这是正常的使用异常处理,当div函数抛异常时,就会直接跳到catch捕获的地方,进行处理。
//异常的缺点:内存泄露,在new和delete之间抛异常
#include <vector>
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
vector<int>* p1 = new vector<int>;
div();
delete p1;
cout << "delete:" << p1 << endl;
}
//正常情况下,如果不抛异常,就不会内存泄露,但抛异常后,就会泄露,解决方法
//是再套一层异常判断,捕获的异常不处理,继续抛出(在抛出之前将资源释放)
int main()
{
try
{
func();
}
catch (const exception& e)//抛基类异常,用父类来接受
{
cout << e.what() << endl;
}
}
那如果是这样的场景呢,在new和delete之间如果div()抛异常了,那么开辟的空间p1就无法释放。最终会造成内存泄露的。
而想要处理这样的问题,就需要对div函数套一层异常判断,如果出现异常,先释放资源了,再将异常抛出给外面的捕获处理。
//异常的缺点:内存泄露,在new和delete之间抛异常
#include <vector>
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
vector<int>* p1 = new vector<int>;
//如果p1抛异常就 没有问题,没有资源的申请,所以也不需要释放
vector<int>* p2 = new vector<int>;
//如果p2抛异常,p1就内存泄露
vector<int>* p3 = new vector<int>;
//如果p3抛异常,p1和p2就内存泄露
//…………
//所以异常出现后,很容易造成内存泄露,有什么办法可以解决呢?
try
{
div();
}
catch (...)
{
delete p1;
cout << "delete" << p1 << endl;
throw;//再抛出
}
delete p1;
cout << "delete:" << p1 << endl;
}
//正常情况下,如果不抛异常,就不会内存泄露,但抛异常后,就会泄露,解决方法
//是再套一层异常判断,捕获的异常不处理,继续抛出(在抛出之前将资源释放)
int main()
{
try
{
func();
}
catch (const exception& e)//抛基类异常,用父类来接受
{
cout << e.what() << endl;
}
}
只不过这样做还是有缺陷,如果有很多个资源申请呢?或者有连续的资源申请呢?比如资源1申请,如果出现异常,那么就直接跳出去,如果资源2异常那么就需要对这个操作套一层异常处理,需要先将资源1的资源释放了,然后再重新抛异常给外面。如果资源3申请时出现异常呢?…………这样是不是每次申请资源时都需要套上异常处理呢?这样也太麻烦了吧!但你又不得不这样做,因为你要保证内存安全啊!
//异常的缺点:内存泄露,在new和delete之间抛异常
#include <vector>
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
vector<int>* p1 = new vector<int>;
//如果p1抛异常就 没有问题,没有资源的申请,所以也不需要释放
vector<int>* p2 = new vector<int>;
//如果p2抛异常,p1就内存泄露,需要给该步骤套上异常处理,释放p1然后再将异常抛出
vector<int>* p3 = new vector<int>;
//如果p3抛异常,p1和p2就内存泄露,需要给该步骤套上异常处理,释放p1,p2然后再将异常抛出
//…………
//所以异常出现后,很容易造成内存泄露,有什么办法可以解决呢?
try
{
div();
}
//如果div()抛异常,,p1,p2,p3都得释放
catch (...)
{
delete p1;
delete p2;
delete p3;
cout << "delete" << p1 << endl;
throw;//再抛出
}
delete p1;
cout << "delete:" << p1 << endl;
}
//正常情况下,如果不抛异常,就不会内存泄露,但抛异常后,就会泄露,解决方法
//是再套一层异常判断,捕获的异常不处理,继续抛出(在抛出之前将资源释放)
int main()
{
try
{
func();
}
catch (const exception& e)//抛基类异常,用父类来接受
{
cout << e.what() << endl;
}
}
这时,智能指针就出现了!
智能指针有三大特性:
1.RAII
2.可以像指针一样
3.存在拷贝问题。
一.RAII,是一种利用对象生命周期来控制程序资源的技术。
我们在对象构造的时候,将开辟的资源交给对象,那么这样在对象生命周期内,该资源一直存在,然后在该对象的析构函数里,进行释放资源。这样对象析构,资源也就释放了。
也就是我们将资源交给一个对象进行管理,当对象的生命周期还在时,资源就存在,当对象销毁时,资源就被释放,这样,我们就将管理资源的责任托管给了对象。
好处:
1.释放了双手,不需要我们显示的释放资源。就不用怕资源最后没有释放。
2.采用这种方式,对象所需的资源在其生命周期内始终保持有效。
二.智能指针,从指针二字我们就应该能意识到它是具备指针的特性的,而指针有哪些特性呢?
1.可以解引用。通过解引用访问资源。
2.可以使用→运算符来访问资源里的内容。
三.所以说智能指针本质上也是一个指针,那指针之间也是可以赋值,拷贝的。并且是值拷贝。
智能指针的使用
智能指针的实现其实很简单,智能指针底层就是封装着该类型的指针。原理(RAII)就是将申请的资源托管给一个对象管理,所以在对象构造时,将资源给对象即可。当对象析构时,就将资源释放。
然后还要实现*运算符重载和→运算符重载。
namespace tao
{
template<class T>
class smater_ptr
{
public:
smater_ptr( T* ptr)//将资源交给对象管理
:_ptr(ptr)
{}
~smater_ptr()//对象销毁时就将资源释放
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
那么我们就可以处理上面遗留的问题了:
#include <vector>
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
//vector<int>* p1 = new vector<int>;
如果p1抛异常就 没有问题,没有资源的申请,所以也不需要释放
//vector<int>* p2 = new vector<int>;
如果p2抛异常,p1就内存泄露
//vector<int>* p3 = new vector<int>;
如果p3抛异常,p1和p2就内存泄露
//…………
//所以异常出现后,很容易造成内存泄露,有什么办法可以解决呢?
smater_ptr<vector<int>> p1(new vector<int>);
//将资源给对象p1管理
smater_ptr<vector<int>> p2(new vector<int>);
smater_ptr<vector<int>> p3(new vector<int>);
smater_ptr<string> ps(new string("小陶来咯"));
div();
cout << *ps << endl;
cout << ps->size() << endl;
}
int main()
{
try
{
func();
}
catch (const exception& e)//抛基类异常,用父类来接受
{
cout << e.what() << endl;
}
}
这里再怎么抛异常都不会影响资源的释放,因为资源是被智能指针对象管理着,当对象销毁时,管理的资源肯定会释放的。
我们也不用担心连续的申请资源会出现异常的情况了。
应用意义/存在问题
一般正常使用智能指针,就可以避免大多数的内存泄露问题,但不排除乱用的。智能指针的应用能帮助我们很好的处理因为异常出现而导致的内存泄露问题。不过初期的智能指针还存在着问题,比如拷贝问题。C++98时期就已经存在智能指针auto_ptr.不过吐槽点很多,现在公司基本禁止使用auto_ptr.
随着C++的发展,又出现其他的智能指针比如:uniqe_ptr
,shared_ptr,weak_ptr(本质不是)。
那智能指针存在什么问题呢?我们来分析分析:
int main()
{
smater_ptr<string> sp1(new string("xioatao"));
smater_ptr<string> sp2(new string("xioyao"));
//存在这样的场景:
smater_ptr<string> sp2(sp1);
//指针之间的拷贝就应该是浅拷贝,我们不用写编译器会自动生成。
//但浅拷贝会出现什么问题呢?
//1.同一块资源被释放两次 2.内存泄露
return 0;
}
呐,这就是智能指针的拷贝问题,指针之间的拷贝肯定是值拷贝,因为是内置类型,不存在深拷贝。那浅拷贝我们不写,编译器生成的拷贝构造就是浅拷贝,所以我们就不用写了吗?
首先,我们要分析,确实是浅拷贝,但是如果指针浅拷贝了,就会出现这样的问题:①指向同一块的空间被释放两次②有一块空间没有释放,内存泄露。
那我们来看看C++库里是如何处理这些问题的。
智能指针原理剖析+模拟实现
auto_ptr
C++98库里提供的auto_ptr智能指针,处理这种问题的原理是:管理权转移。
什么叫管理权转移呢?就比如sp3(sp1),将sp1拷贝给sp3。也就是用sp1构造sp3。首先我们要明白,sp1是管理着一块资源的,sp3还没有实例化,没有管理资源。这里直接将sp1管理资源的权力转移给sp3。然后sp1就没有权力管理资源了也就是不需要管理资源了。不管理资源是如何做到的呢?直接将智能指针对象里面的指针置空即可。
namespace tao
{
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//sp3(sp1)
auto_ptr(auto_ptr<T>& p)
:_ptr(p._ptr)
{
//将资源转移后,被拷贝对象再置空
p._ptr = nullptr;
//如果不置空,就会释放两次
}
private:
T* _ptr;
};
这就是C++98提供的auto_ptr智能指针。这个版本被吐槽的很多,因为你将sp1对象管理资源的权转移后,sp1就悬空了。如果有人要再次访问sp1呢?就是那个不管理资源的智能指针。(里面的指针必须置空,不然就会释放两次。)
//我们写一个类,方便观察资源创建和释放
#include "smater_ptr.h"
class B
{
public:
B(int b = 0)
:_b(b)
{
cout << "b=0" << endl;
}
~B()
{
cout << this;
cout << "~B()" << endl;
}
private:
int _b;
};
//自定义类型构造会调用它的构造函数,和析构函数。
int main()
{
tao::auto_ptr<B> b1(new B(1));
tao::auto_ptr<B> b2(new B(2));
//这个是auto_ptr C++98时期就出现,但不好,很多公司严禁不给使这个
tao::auto_ptr<B> b3(b1);
//因为存在严重的不合理地方,当出现智能指针拷贝赋值的地方
//auto_ptr处理的方式是:直接转移资源的管理权。
//拷贝时,会把被拷贝对象的资源管理转移给拷贝对象。而被被拷贝对象就没有资源管理,直接置空
//存在问题:管理权转移后,再次访问被拷贝对象
//b1这个智能指针已经没有资源可以管理了,里面的指针直接置空了,不能再访问了
//b1->_b++;
b3->_b++;
}
unique_ptr
由于auto_ptr的设计太不合理,C++11中开始提供更靠谱的unique_ptr智能指针。
unique_ptr的原理其实很简单,四个字:简单粗暴。
直接不给拷贝,简单粗暴的防止拷贝。利用delete关键字,将函数定义为删除函数,不能使用。指针赋值的操作也被禁止。
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(unique_ptr<T>& p) = delete;//直接删除掉,不能拷贝
private:
T* _ptr;
};
shared_ptr
但是如果就是存在智能指针间的拷贝,那该怎么办呢?unique_ptr肯定不能使用了。
所以C++11又提供了一个可以允许拷贝的智能指针,那就是shared_ptr.
shared_ptr智能指针实现的原理是:<引用计数>
原理
①当有一个对象管理资源时,计数器就显示1.当有两个对象指向资源时,计数器就显示2.当有n个对象指向资源时,计数器就显示n。
②当指向资源的对象生命周期结束时,首先将该资源的上的计数器减减。然后判断计数器是否为0.如果不为0,说明还存在对象管理着资源。如果为0,就说明没有对象管理资源了,该资源就可以释放了。
那这个计数器应该如何设计呢?是设计成普通计数器就可以吗?还是设计成静态的呢?
如果设计成普通的计数器,就是在智能指针内部存一个计数器。这就表明每个对象都有一个计数器。这合理吗?
这肯定不合理啊,指向相同资源时如何进行计数呢?这样不能根据计数器来判断一个资源上有多少对象管理了。一个资源就一个计数器就可以了。那设计成静态计数器呢?
设计成静态计数器,如果只有一块资源的话,也不是不可以,但我们不知道会有多少资源啊,当有新的资源开辟后,该资源上应该只有一个对象管理,但使用静态计数器后,所有对象共享该计数器,就造成了该资源上的计数不对。
我们想要的计数器应该是要伴随着资源的申请而生成。当有一个资源生成被智能指针管理后,就会生成一个计数器,来计算该资源受管理的个数。当有两个资源时,就会有两个计数器,各计算各的,互不影响。所以这里的计数器应该是动态计数器。
当有资源申请时并给对象管理时(也就是调用对象的构造函数)时,就会动态生成一个计数器。
计数器实现完后,我们就可以利用引用计数来实现拷贝。比如用p2(p1),用p1拷贝构造p2.那么p2就要指向p1指向的资源,并且资源上的计数器要加加。这两个智能指针都管理着资源。不过当p2销毁时,资源并不会释放,计数器首先会减减,然后判断计数器是否为0,只有计数为0了,才可以将资源释放。
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;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr(shared_ptr<T>& ps)
:_ptr(ps._ptr)
, _pcount(ps._pcount)
//这个单纯指向没有再管理一个新的资源,所以不会生成计数器,只需要指向原来生成的计数器。
{
++(*_pcount);//然后原来的计数器加加即可,表面该资源上多了一个对象管理
}
T* get()const//获取_ptr
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
//动态计数器
};
shared_ptr除了支持智能指针间的拷贝,还支持指针间的赋值。那么赋值重载是如何实现的呢?
首先赋值是两个都已经存在的对象进行赋值。而拷贝构造是已存在的拷贝给不存在的对象。
假设两个存在的对象,各自都管理着一块资源,当两个对象进行赋值,会发生什么呢?
不过这里要注意两个细节,细节一就是赋值对象的计数器减减后,需要进行判断,是否为0.如果计数器为0了,就说明该资源上没有对象管理,那么该资源就可以释放了。如果不是0,那就没事。
细节二,就是自己给自己赋值的场景会有bug存在。
//shared_ptr的赋值运算符重载
shared_ptr<T>& operator=(const shared_ptr<T>& ps)
{
//自己给自己赋值的场景要避免
if (_ptr == ps._ptr)return *this;
//首先要对赋值对象管理的资源的计数器减减,要注意被赋值对象管理的资源是否只有一个智能指针控制
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = ps._ptr;
_pcount = ps._pcount;//正常转移指向即可
//被赋值对象的计数器++即可
(*ps._pcount)++;
return *this;
}
验证一下:
int main()
{
tao::shared_ptr<B> b1(new B(1));
tao::shared_ptr<B> b2(b1);
tao::shared_ptr<B> b3(b1);
tao::shared_ptr<B> b4(new B(2));
tao::shared_ptr<B> b5(b4);
b1 = b5;
}
weak_ptr
shared_ptr几乎已经完美了,既具有智能指针的特性,又允许拷贝和赋值。但还是具有缺点的,具有什么缺点呢?
该问题就是:循环计数
当出现循环计数时,shared_ptr是没有办法解决。什么叫循环计数问题呢?
当存在这种需求时:动态开辟的节点需要链接起来时。我们使用智能指针来管理资源会发生什么呢?
struct Node
{
B _val;
Node* _next;
Node* _prev;
};
int main()
{
tao::shared_ptr<Node> sp1(new Node);
tao::shared_ptr<Node> sp2(new Node);
//sp1->_next = sp2;
//类型不匹配,sp1->next的类型是Node而sp2的类型是shared_ptr类型所以这样无法链接起来。
// //所以Node节点里存的应该是shared_ptr类型的指针,这样才可以链接起来
}
我们发现无法链接起来,因为节点里next是Node*类型的,而sp2是shared_ptr类型的。所以为了能够链接起来,节点里存的应该是shared_ptr类型的next。
struct Node
{
B _val;
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
int main()
{
tao::shared_ptr<Node> sp1(new Node);
tao::shared_ptr<Node> sp2(new Node);
// //所以Node节点里存的应该是shared_ptr类型的指针,这样才可以链接起来
sp1->_next = sp2;
sp2->_prev = sp1;
}
这样两个节点就链接起来了。可是链接起来后就出现问题了:
我们发现两个智能指针管理的资源都没有释放,这是为什么呢?
C++中是如何解决循环计数的呢?C++11提供了weak_ptr专门用来处理shared_ptr出现的循环计数问题。
那么weak_ptr解决循环计数的原理是什么呢?
首先我们需要明白引起循环计数的原因是什么,要理解什么场景下会发生循环计数。
1.主要原因就是因为智能指针定义在节点的内部。
2.然后就是因为计数器要等于1时才可以释放资源。
就是因为内部的智能指针参与了资源的管理,导致计数器增加,外面管理的资源的对象销毁后资源也无法销毁,需要里面的智能指针对象销毁才可以销毁,而里面的对象销毁又需要资源先销毁才可以销毁。所以最主要原因就是内部的智能指针管理资源。
所以weak_ptr实现的原理就是让节点里面的智能指针不管理资源,就单纯的链接节点,不参与资源的管理,但可以访问资源。
这样计数器就不会增加,当外面的对象销毁,资源就会正常销毁。
所以正常操作应该是这样:
struct Node
{
B _val;
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
//weak_ptr不管理资源,不会增加计数器
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
sp1->_next = sp2;
sp2->_prev = sp1;
}
所以weak_ptr严格上来说不是智能指针,它不具备RAII特性。不管理资源。它主要是提供支持由shared_ptr类型转换成weak_ptr类型的拷贝构造和赋值。而不是管理资源。
template <class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
weak_ptr(const shared_ptr<T>& sp)//在类外无法访问到sp的保护成员_ptr,所以徐娅get函数来获取。
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr;
};
};