c++智能指针
文章目录
前言
智能指针是C++ 中的一种数据类型,用于自动管理动态分配的内存。
一、为什么要学习智能指针?
这时就该掏出上篇博客的代码了,没看过的可以看看,或者认真阅读下面代码。
double Division(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
int* array = new int[10];
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
cout << "delete []" << array << endl;
delete[] array;
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (...)
{
}
return 0;
}
大家看一下这个代码,如果在Division函数中抛异常,就会发生执行流的跳跃,我们在Func函数中申请的空间(array指向的空间)就不会得到释放,也就会发生内存泄漏,而学习智能指针就是为了解决类似问题,不要着急我们一步一步来。
二、内存泄漏
2.1、 什么是内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
2.2、内存泄漏的危害
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
三、智能指针的使用及原理
3.1、RAII
RAII是一种利用对象生命周期来控制程序资源的简单技术。简单来说就是把申请的空间资源交给一个自定义类型的对象,然后利用对象,销毁时自动调用析构函数的性质,将所申请的空间资源释放。
示例:
template<class T>
class SmartPtr//创建自定义类型对象
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete[] _ptr;
cout << "delete[] " << _ptr << endl;
}
private:
T* _ptr;
};
double Division(int a, int b)
{
if (b == 0)
{
throw invalid_argument("Division by zero condition!");
}
return (double)a / (double)b;
}
void Func()
{
int* array = new int[10];//这里先申请空间,再传递,是为了大家好理解
SmartPtr<int> sp1(array);
//SmartPtr<int> sp1(new int[10]);一般直接写成这种形式
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
上面的代码,我们定义了一个SmartPtr< int> 类型的对象,将指向申请空间的array用于初始化该类型对象,当程序抛异常时(执行流跳跃时),随着Func函数的栈帧,程序自动调用sp1对象的析构函数,而在析构函数的内部执行delete[] _ptr;将申请空间释放。这样内存泄漏就解决了。
3.2、智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可
以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其
像指针一样去使用。
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
在c++的发展过历程中,智能指针出现了很多版本,接下来我一一讲解
大家先仔细看下面的问题:
在我们编写程序的过程中拷贝和赋值操作是少不了的,智能指针也不例外
void Func()
{
SmartPtr<int> sp1( new int[10];);
SmartPtr<int> sp2 = sp1;//将sp1拷贝给sp2
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
这时sp1中的_ptr指针和sp2中的_ptr指针指向了统一块空间,这也就意味这,当sp1对象和sp2对象销毁时,我们所申请的空间将被释放两次,这是很经典的野指针问题了!!!那么库中实现的智能指针是怎么来解决这块问题的呢?下面我们一起来看一下
四、智能指针的发展
4.1、auto_ptr
auto_ptr作为在c++98时期就出现的老大哥,其实现是比较简单的。它解决这块问题的方法叫做管理权转移.
struct auto_ptr
{
auot_ptr(T* ptr)
:_ptr(ptr)
{}
//如sp2(sp1)
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
T& operator *()
{
return *_ptr
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
可以看到上面的赋值拷贝,直接将sp1对象中_ptr指针指向的内容拷贝给sp2对象中的_ptr指针,再将sp1对象中的_ptr指针制空,就是将sp1对我们申请空间管理的权限交给了sp2,这个行为我们称作管理权转移.
缺点:
被转移对象(这里指sp1)来不及释放,如果有人不小心去用sp1访问,会有空指针问题。
我们接着来看下面是怎么解决的。
4.2、unique_ptr
对于上面问题c++11又给我们提供了unique_ptr智能指针,它的想法是,既然你auto_ptr在拷贝是会出现下面的问题,那么我不让你拷贝不就解决了吗?看到这里你可能又有疑问了,别急我们慢慢看。
那么不让unique_ptr类拷贝,我们不给它提供拷贝函数、赋值拷贝就行了吗?当然不是,大家想想,如果有人在类外面实现它的拷贝函数呢?那该如何来实现呢,其实很简单,我们只需要利用delete关键字就可以了。
class unique_ptr
{
public:
// RAII
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete->" << _ptr << endl;
delete _ptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// C++11
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
这种简单粗暴的方法叫做防拷贝,但是在上面我们说了,在有些场景下拷贝是无可替代的,所以这里这样解决,也不一个很好的方法。
4.3、shared_ptr
shared_ptr则是通过引用计数的方式,来解决智能指针的拷贝问题。
- shared_ptr会在其内部,给它所维护的每一份资源都配置了一份计数,用这份计数来记录这份资源(空间)被几个对象所维护。
- 在对象被销毁时(调用析构函数时)就说明该对象,不再维护那块资源,计数就减一。
- 当引用计数等于0时,就说明自己是最后一个维护这块资源的对象,这时释放这块资源的任务就是它的了。
- 如果计数不为0,就说明还有其他的对象维护这块资源,此时就不用释放资源,这样就规避了野指针问题,也解决了智能指针不能拷贝问题。
class share_ptr
{
public:
share_ptr()
:_ptr(nullptr)
,_pount(new size_t(1))
{}
~share_ptr()
{
--(*_pount);
if (*_pount == 0)
{
delete _ptr;
delete _pount;
}
_ptr = nullptr;
}
share_ptr(const share_ptr<T>& sp)
{
_ptr = sp._ptr;
_pount = sp._pount;
++(*_pount);
}
share_ptr<T>& operator=(share_ptr<T>& sp)
{
if (_ptr == sp._ptr)//防止自己给自己赋值或维护同一块资源的对象之间相互赋值
return *this;
--(*_pount);
if (*_pount == 0)//判断该对象刚开始维护的资源,是否需要释放
{
delete _ptr;
delete _pount;
}
_ptr = sp._ptr;
_pount = sp._pount;
++(*_pount);
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
size_t* _pount;
};
shared_ptr使用引用计数的方式即解决了资源泄漏问题,又满足了,我们对于对象拷贝的需求,可以说是相当不错了。智能指针发展到这个版本,可以说是比较成熟的了,但是shared_ptr又带来了一个新的问题。
4.3.1、shared_ptr的循环引用问题
为了营造出所需场景,将代码稍微修改一下。
namespace ltn {
template<class T>
class share_ptr
{
public:
share_ptr(T* ptr=nullptr)
:_ptr(ptr)
, _pount(new size_t(1))
{}
~share_ptr()
{
--(*_pount);
if (*_pount == 0)
{
delete _ptr;
delete _pount;
}
_ptr = nullptr;
}
share_ptr(const share_ptr<T>& sp)
{
_ptr = sp._ptr;
_pount = sp._pount;
++(*_pount);
}
share_ptr<T>& operator=(share_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
--(*_pount);
if (*_pount == 0)
{
delete _ptr;
delete _pount;
}
_ptr = sp._ptr;
_pount = sp._pount;
++(*_pount);
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
size_t* _pount;
};
struct ListNode
{
share_ptr<ListNode> _next;
share_ptr<ListNode> _prve;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
}
int main()
{
ltn::share_ptr<ltn::ListNode> n1(new ltn::ListNode);//将申请空间交给share_ptr维护
ltn::share_ptr<ltn::ListNode> n2(new ltn::ListNode);
n1->_next = n2;
//n2->_next = n1;
return 0;
}
可以看到,这里空间正常释放,我们来分析一下,它是如何释放空间的:
我将他们的关系画出来了,有点丑大家凑合着看
接下来的分析是重点哈:当程序执行结束时,首先n1调用析构函数(为什么是这个顺序自己复习哈),引用计数减减变为0,因此n1所维护的空间释放,这时_next对象调用析构函数,它所维护的空间的引用计数减减变为1,接着到n2调用析构函数了,n2的引用计数减减变为0,释放n2维护的空间。申请空间完美释放。
大家将上面代码仔细分析一下,只需要看主函数中的变化即可
int main()
{
ltn::share_ptr<ltn::ListNode> n1(new ltn::ListNode);//将申请空间交给share_ptr维护
ltn::share_ptr<ltn::ListNode> n2(new ltn::ListNode);
n1->_next = n2;
n2->_next = n1;
return 0;
}
这里空间为什么没有释放呢?这就是我们要说的循环引用问题了!!!
接下来我们使用调试的方法,深入内部来看一下,这块的问题
我们接着向下看
接下来我们讲解这里为什发生循环引用问题。
开始先调用n1的析构函数,引用计数减减变为1,再调用n2的析构函数,引用计数减减变为1。我们会发现这时,如果左边结点想要释放,就需要维护左边空间的引用计数减为0,然而要是左边结点引用计数减为0,就要将右边结点内部维护左边空间的对象销毁,那么怎么才能使这个对象销毁呢?这时就需要将右边空间释放,要使右面空间释放,就要使维护右面空间的引用计数减为0,然而要使它为零,就要将左边空间释放。这时就进入了一种循环,这个就叫做循环引用问题。
对于循环引用问题,库里给它提供了一个私人助理weark_ptr。
4.4、weak_ptr
我们将引起循环引用部分换成库里的,空间就可以正常释放,那么库里的weak_ptr是2如何解决这里的问题的呢?
首先我们要清楚,循环引用是因为要维护空间内部对象相互赋值导致的,说白了,就是增加引用计数导致的,所以这里我们需要一个智能指针,它不增加额外的引用计数,但是它可以与shared_ptr相互赋值。
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())//get函数用来获取share_ptr内部指针变量。因为指针变量私有
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
总结
到这里我们就介绍完了,由于篇幅较长,又都是手敲,可能会有打字错误,请大家见谅!!