知识的学习在于点滴记录,坚持不懈;知识的学习要有深度和广度,不能只流于表面,坐井观天;知识要善于总结,不仅能够理解,更知道如何表达!
智能指针介绍
学习C++的人,一直在接触裸指针,一边感受着它的强大,一边感受着它的坑爹。当然,坑不坑爹在于开发者,指针本身近乎完美,但奈何用的人比较猥琐,给自己埋下无数的坑,还哭喊着指针不好用,那么今天要介绍的智能指针可以释放大家在使用裸指针时的一些压力,当然智能指针无法替代裸指针的全部功能。
裸指针到底有什么不好,写过一些C++代码的人应该都能知道,比如下面的原因:
- 忘记释放资源,导致资源泄露(常发生内存泄漏问题)
- 同一资源释放多次,导致释放野指针,程序崩溃
- 明明代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间return掉了,导致释放资源的代码未被执行到,懵
- 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到,懵
总之,智能指针的智能二字,主要体现在用户可以不关注资源的释放,因为智能指针会帮你完全管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异常,资源在到期的情况下,一定会进行释放。
C++11库里面,提供了带引用计数的智能指针和不带引用计数的智能指针,这篇文章主要介绍它们的原理和应用场景,包括auto_ptr,scoped_ptr,unique_ptr,shared_ptr,weak_ptr。
自己实现智能指针
为了更好的理解C++库中智能指针的原理,我们首先需要自己实现一个简单的智能指针,窥探一下智能指针的基本原理,就是利用栈上的对象出作用域会自动析构这么一个特点,把资源释放的代码全部放在这个析构函数中执行,就达到了所谓的智能指针。对比下面的两块代码:
- 使用裸指针
int main()
{
int *p = new int;
/*其它的代码...*/
/*
如果这里忘记写delete,或者上面的代码段中程序return掉了,
没有执行到这里,都会导致这里没有释放内存,内存泄漏
*/
delete p;
return 0;
}
- 使用智能指针
template<typename T>
class CSmartPtr
{
public:
CSmartPtr(T *ptr = nullptr) :mptr(ptr) {
}
~CSmartPtr() {
delete mptr; }
private:
T *mptr;
};
int main()
{
CSmartPtr<int> ptr(new int);
/*其它的代码...*/
/*由于ptr是栈上的智能指针对象,不管是函数正常执行完,还是运行过程中出现
异常,栈上的对象都会自动调用析构函数,在析构函数中进行了delete
操作,保证释放资源*/
return 0;
}
上面这段代码就是一个非常简单的智能指针,主要用到了这两点:
1)智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
2)利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源
所以,智能指针一般都是定义在栈上的,曾经有一个面试问题,问“能不能在堆上定义智能指针?”,如这段代码CSmartPtr *p = new CSmartPtr(new int);大家应该能看出来,这里定义的p虽然是智能指针类型,但它实质上还是一个裸指针,因此p还是需要进行手动delete,又回到了最开始裸指针我们面临的问题。
当然,智能指针要做到和裸指针相似,还得提供裸指针常见的*和->两种运算符的重载函数,使用起来才真正的和裸指针一样,代码扩充如下:
template<typename T>
class CSmartPtr
{
public:
CSmartPtr(T *ptr = nullptr) :mptr(ptr) {
}
~CSmartPtr() {
delete mptr; }
T& operator*() {
return *mptr; }
const T& operator*()const {
return *mptr; }
T* operator->() {
return mptr; }
const T* operator->()const {
return mptr; }
private:
T *mptr;
};
int main()
{
CSmartPtr<int> ptr(new int);
*ptr = 20;
cout << *ptr << endl;
return 0;
}
上面的这个智能指针,使用起来就和普通的裸指针非常相似了,但是它还存在很大的问题,看下面的代码:
int main()
{
CSmartPtr<int> ptr1(new int);
CSmartPtr<int> ptr2(ptr1);
return 0;
}
这个main函数运行,代码直接崩溃,问题出在默认的拷贝构造函数做的是浅拷贝,两个智能指针都持有一个new int资源,ptr2先析构释放了资源,到ptr1析构的时候,就成了delete野指针了,造成程序崩溃。所以这里引出来智能指针需要解决的两件事情:
- 怎么解决智能指针的浅拷贝问题
- 多个智能指针指向同一个资源的时候,怎么保证资源只释放一次,而不是每个智能指针都释放一次,造成代码运行不可预期的严重后果
我们一起看看C++库中提供的智能指针是怎么解决上面提到的问题的。
不带引用计数的智能指针
C++库中提供的不带引用计数的智能指针主要包括:auto_ptr,scoped_ptr,unique_ptr,下面一一进行介绍。
- auto_ptr
先浏览一下auto_ptr的主要源码,如下:
template<class _Ty>
class auto_ptr
{
// wrap an object pointer to ensure destruction
public:
typedef _Ty element_type;
explicit auto_ptr(_Ty * _Ptr = nullptr) noexcept
: _Myptr(_Ptr)
{
// construct from object pointer
}
/*这里是auto_ptr的拷贝构造函数,
_Right.release()函数中,把_Right的_Myptr
赋为nullptr,也就是换成当前auto_ptr持有资源地址
*/
auto_ptr(auto_ptr& _Right) noexcept
: _Myptr(_Right.release())
{
// construct by assuming pointer from _Right auto_ptr
}
_Ty * release() noexcept
{
// return wrapped pointer and give up ownership
_Ty * _Tmp = _Myptr;
_Myptr = nullptr;
return (_Tmp);
}
private:
_Ty * _Myptr; // the wrapped object pointer
};
从auto_ptr的源码可以看到,只有最后一个auto_ptr智能指针持有资源,原来的auto_ptr都被赋nullptr了,考虑如下代码:
int main()
{
auto_ptr<int> p1(new int);
/*
经过拷贝构造,p2指向了new int资源,
p1现在为nullptr了,如果使用p1,相当于
访问空指针了,很危险
*/
auto_ptr<int> p2 = p1;
*p1 = 10;
return 0;
}
上面的程序,如果用户不了解auto_ptr的实现,代码就会出现严重的问题。记得曾经一个面试题问过“auto_ptr能不能使用在容器当中?”,看下面的代码描述:
int main()