作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:gitee✨
作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
今天我们来讲解一个好理解,有好用的知识点–智能指针。智能指针有很多种,但是每种都有相同之处,一会按顺序介绍的哦都是每一种的补充,他的作用是解决没有办法再回头来释放的空间,导致会内存泄漏,所以需要指针指针来解决这个问题,话不多说,我们开始进入正文讲解
再以前是没有智能指针的东西,那为什么现在设计出来了看代码:
void f()
{
pair<string, string>* p1 = new pair<string, string>;//可能会出现错误抛异常
fun();
delete p1;
}
int main()
{
f();
return 0;
}
以前我们申请空间就释放空间,中间有啥都无所谓,肯定没有问题的,但是异常出来之后,就会导致fun出现异常,这时候就会影响执行流,可能会释放不到p1,这样就导致内存泄漏,
**那我们的java有没有智能指针呢??**答案是没有的,他有垃圾回收器,可以自动回收内存,那C++为什么不搞呢??原因就是c++程序是直接跑在os上的,而Java先再虚拟机上跑,而虚拟机再os上跑,多了一层,所以C++想要直接去搞垃圾回收器是一件很耗成本的事,有便宜就有付出,所以再一些追求性能的程序,大部分都选择C++比如游戏,遥控器需要及时响应的。并且C++程序和os是直接互通的
有了异常来说我们来看下面的代码:
(1)
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
pair<string, string>* p1 = new pair<string, string>;//可能会出现错误抛异常
try
{
div();//正常执行逻辑
}
catch (...)//接收任意类型的异常
{
cout << "delete:" << p1 << endl;
delete p1;//先释放
throw;//抛出去
}
}
int main()
{
try
{
f();
}
catch (const exception& e)//捕获抛出来的异常
{
cout << e.what() << endl;
}
return 0;
}
大家仔细看我们的f函数里面有一块动态申请的空间,如果div里面抛异常的,我们再抛出去之前就给释放就可以解决问题。也是可以解决问题的。
这就是由于异常那个出现导致执行流的变化。
(2)将上面的f()函数改成下面这样看看,其余不变。
void f()
{
pair<string, string>* p1 = new pair<string, string>;//可能会出现错误抛异常
pair<string, string>* p2 = new pair<string, string>;
try
{
div();//正常执行逻辑
}
catch (...)//接收任意类型的异常
{
cout << "delete:" << p1 << endl;
delete p1;//先释放
throw;//抛出去
}
cout << "delete:" << p1 << endl;
delete p1;
cout << "delete:" << p2 << endl;
delete p2;
}
我给大家来分析一下,再申请的时候也可能出现申请失败,概率很小但有可能,此时p1申请失败,那下面的代码就不会执行,退出函数,也不存申请内存释放问题,万一p1申请成功,执行申请p2的时候出现了异常,退出了函数,那么刚申请的p1就没有办法释放,有人说给p2也加一个异常处理:
void f()
{
pair* p1 = new pair;//可能会出现错误抛异常
pair* p2 = new pair;//可能会出现错误抛异常
try//是p2的异常捕获
{
;//正常执行逻辑
}
catch (...)//接收任意类型的异常
{
cout << "delete:" << p1 << endl;
delete p1;//先释放
throw;//抛出去
}
try//是div的异常捕获
{
div();//正常执行逻辑
}
catch (...)//接收任意类型的异常
{
cout << "delete:" << p1 << endl;
delete p1;//先释放
throw;//抛出去
}
cout << "delete:" << p1 << endl;
delete p1;
cout << "delete:" << p2 << endl;
delete p2;
}
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死
所以针对可能造成内存泄漏的空间,我们不可能每一个都要捕获一下异常,所以针对这个问题我们要想办法解决,这才引入了智能指针
相信通过上面的例子,大家应该明白C++11要搞出智能指针了吧,接下来我将带大家来学习智能指针。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效
我们抛出异常就意味结束了此函数,想要解决释放,将指针写成一个类,自动调用析构函数,就可以了,来看代码:
class smartptr
{
public:
smartptr(pair<string, string>* sp)
:_ptr(sp)
{}
~smartptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
private:
pair<string, string>* _ptr;
};
为了能适应任何类型,我们将智能指针设计成模板的形式
template<class T>
class smartptr
{
public:
smartptr(T* sp)
:_ptr(sp)
{}
~smartptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
private:
T* _ptr;
};
xdh::smartptr<pair<string, string>> p1 = new pair<string,string>;
xdh::smartptr<pair<string, string>> p2 = new pair<string, string>;
难道智能指针这样就可以了??
我们要达到像普通指针一样完成下面的操作:
所以我们要实现*和->,这个再迭代器那一块已经孰能生巧了,再类里面去实现那一下就好了
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
按照正常情况我们的智能指针的内容讲解的差不多了,但是我们不能学到这里就结束了,这只是自己造轮子,简单的实现了,我们还要学习库里面给我们提供的智能指针,这就要来讲讲智能指针的发展史了
我们会介绍四款智能指针,auto_ptr,unique_ptr,shared_ptr,weak_ptr,这四个我都会带大家去使用以及模拟实现一下,让大家更好的理解智能指针。
我们上面一开始自己实现的智能指针还有一些问题解决不了,那就是下面的场景:
int main()
{
xdh::smartptr<string> sp1(new string("xxxxx"));
xdh::smartptr<string> sp2(new string("yyyyy"));
sp1 =sp2;
return 0;
}
我们的第一款智能指针是C++98搞出来叫auto_ptr,但是这个太坑了,也会大部分公司禁止使用的,为什么会这么说他呢,他有悬空
我们来使用一下看看:
void f()
{
auto_ptr<string> p1(new string("111"));
auto_ptr<string> p2(p1);
auto_ptr<string> p3(new string("111"));
auto_ptr<string> p4(new string("111"));
p4 = p3;
}
我们的auto_ptr再实现拷贝和赋值的时候就会使得被拷贝的对象发生悬空,如果再对被拷贝对象操作,就会出现问题,不使用就没事。但是这种再开发中就不被允许,万一再前面使用了被拷贝对象,后面悬空了,想看看值就会出现问题,他的实现原理很见到,就是把资源给别人,把自己置空就可以了。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份xdh::auto_ptr来了解它的原
理
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
拷贝构造里面就很简单,把指针交给新对象,自己置空,几乎不会自己给自己拷贝构造的,再赋值里面我们就要注意,自己给自己赋值,就不应该置空,所以加了一个判断,这有点像移动构造,把我的资源给你,但是移动构造是将将亡值的资源给你,而这里是左值,我可能还要使用,给你我自己就会出现问题,所以这就是auto_ptr设计坑的所在之处
最上面说过赋值后可能会造成析构两次的问题,那么unique_ptr就是直接暴力的禁止赋值。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//C++11防拷贝:delete
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
private:
T* _ptr;
//C++98:只声明不实现+私有化
unique_ptr(unique_ptr<T>& ap);
unique_ptr<T>& operator=(unique_ptr<T>& ap);
};
接下来讲解的智能指针才是重点
为什么会出现析构两次,就会因为同一块空间有两个对象共享,结束后每个对象都会自动调用自购函数进行释放,所以shared_ptr采取的思想就是当一块空间只有一个指向的时候才进行释放,所以需要使用一个计数来达到想要的效果
库里面的使用:
shared_ptr<string>p1(new string("111"));
shared_ptr<string>p2(new string("111"));
shared_ptr<string>p3(p1);
shared_ptr<string>p4(new string("111"));
p4 = p2;
shared_ptr<string>p5(p4);
我们使用一个静态成员作为计数行不行??答案是不行的,只有地址相同的对象才共享一块地址,静态变量是属于类,也是属于所有类对象的,所以对于这两块空间都是公用一个变量的。我们要达到每块空间有自己的计数变量,所以采用引用计数
template<class T>
class shared_ptr
{
public:
// RAII
// 像指针一样
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))//自己创建一个对象的计数初始为1
{}
shared_ptr(const shared_ptr<T>& sp)//拷贝构造,指向同一块空间,计数++
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
private:
T* _ptr;
int* _pcount;
}
我们来看看析构函数怎么搞的:
~shared_ptr()
{
if (--(*_pcount) == 0)//就很自己了就可以被释放
{
delete _ptr;
cout << "delete:" << _ptr << endl;
delete _pcount;
}
}
我们来看看赋值函数怎么搞的:
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
拷贝对象可能就自己指向那块空间了,被拷贝后,那块空间就没人指向了,所以需要判断将其释放
自己给自己赋值
p2=p2;这是直接自己给自己赋值
p2=p4;这是间接自己给自己赋值
都叫自己给自己赋值
shared_ptr最难的就在赋值那里面,要注意判断,通过第二个判断间接把自己的引用也减少一个,这才是关键
我们来看一个场景,是shared_ptr解决不了的
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << " ~A()" << endl;
}
private:
int _a;
};
class Node
{
public:
A _val;
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
void f()
{
shared_ptr<Node> p1(new Node);
shared_ptr<Node> p2(new Node);
p1->_next = p2;
p2->_prev = p1;
}
我们运行程序会发现没有释放,原因就是下面这幅图:
造成这样的主要原因就是计数变量的增加导致结点不能释放,我们的weak_ptr就是来解决这个问题的,他的出现不增加计数,我们来看看使用:
这样就可以解决问题了。那这么一开有点类型不匹配的感觉,说明weak_ptr里面肯定有使用shared_ptr的构造函数,把shared_ptr的资源拿过来操作,但是不做释放也不增加计数,就是借此过度一下,让其赋值成功,间接控制没有增加计数。
我们来模拟实现一下:
template<class T>
class weak_ptr
{
public:
// RAII
// 像指针一样
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{
}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
~weak_ptr()
{
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
也同样的解决我们的问题,我们再将结束打印出来看看,再shared_ptr里面写一个use_count函数
int use_count()
{
return *_pcount;
}
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
weak_ptr就是专门用来解决shared_ptr这个场景的,没有其他作用,几乎大部分场景shared_ptr就足够了
定制删除其只针对shared_ptr去定制了,因为他的使用范围最广,我们来回顾一下shared_ptr的析构:
~shared_ptr()
{
if (--(*_pcount) == 0)//就很自己了就可以被释放
{
delete _ptr;
cout << "delete:" << _ptr << endl;
delete _pcount;
}
}
我们看到这种释放就写死了,只能这样去申请空间了
xdh::shared_ptr p1(new A);
如果我想这样去申请,就会出现问题,因为需要delete[]去释放。
xdh::shared_ptr p1(new A[10]);
有了上面的问题我们才引入了定制删除器。
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
shared_ptr<A> sp1(new A[10], DeleteArray<A>());
return 0;
}
没使用之前是有九个都没有释放使用之后都释放了,我们接下来自己来写一个定制删除器
//template
template<class T>
class shared_ptr
{
public:
// RAII
// 像指针一样
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))//自己创建一个对象的计数初始为1
{}
template<class D>
shared_ptr(T* ptr, D del)//这里面传进来析构函数不能使用,这不是类的模板参数,当然也可以直接再类外面弄一个模板参数
: _ptr(ptr)
, _pcount(new int(1))//自己创建一个对象的计数初始为1
,_del(del)
{}
shared_ptr(const shared_ptr<T>& sp)//拷贝构造,指向同一块空间,计数++
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
T* get()const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del;//本来就要传仿函数进来,而且也不止类型,刚好知道返回值和参数,可以使用包装器
};
xdh::shared_ptr<A> sp2(new A[5], [](A* ptr) { delete[] ptr; });//使用lambda表达式也行
到这里我们的定制删除器就讲解结束,大家下来好好的去看看。
今天讲解内容非常多,但是非常使用,他是以后你在公司可以避免事故的一个重要东西,因为内存泄漏是一个非常不好的问题,而智能指针可以很好的避免这样的问题出现,所以还是希望大家多去了解了解,再智能指针这环节还有一些知识点没讲到,博主后面都会补充到的,这篇我们就讲到这里,我们下篇再见