欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (BingbingSuperEffort) - Gitee.comhttps://gitee.com/BingbingSuperEffort
系列文章推荐
目录
前言
前面的章节中我们学习了异常的使用方法,当程序抛出异常时,程序会直接跳转到最近的捕获区域进行异常的处理,这种处理方式保证了程序的不崩溃,只针对一个区域内的某种错误进行处理。这样的代码看似很正常,但往往会忽略掉某些内存的处理。例如我们在某个函数调用前向内存申请了部分空间,本来内存的释放逻辑是在函数调用之后进行完成,但是如果函数内部出现异常错误,函数将直接跳转,申请的内存无法释放,就造成了内存泄漏的风险。这种代码我们怎么处理呢?
1.为什么需要智能指针
首先我们先看一段代码:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
如代码所示,当new p1出现问题时,代码会抛出异常,空间无法开辟成功。当new p2出现错误时意味着p1已经被开辟,但是程序不会走到delete p1处,p1无法被释放,照成内存泄漏的风险。当调用函数出现异常,则p1,p2都无法进行内存的释放,依然存在内存泄漏的风险。
在当前的运行环境中,函数调用完毕后进程也就结束了,即便我们没有释放的内存也会在进程结束时一起归还给操作系统,因此这种内存泄漏并没有什么大的风险。 但是,服务器上的程序是一旦开机几乎不会停止的进程,如果我们的内存一直在泄漏,那么我们的机器将会越来越慢,最终因为内存不够而导致进程崩溃。
内存泄漏究竟是什么呢?内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。因此我们只是失去了对该内存的控制,并非内存的丢失。所以内存泄漏实际上是指针的丢失而并非内存的丢失。内存泄漏带来的危害是很可怕的,往往会照成操作系统,后台服务的崩溃。
照成内存泄漏的原因有可能是我们对申请的资源忘记释放,也有可能因为异常的跳转导致本该释放的内存没有办法释放。在C/C++程序中,我们往往关注两种方面的内存泄漏:
(1)堆内存泄露(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
(2)系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
内存泄漏非常的常见,我们通常采用两种方式避免内存泄漏,事前预防(智能指针)、事后检测(泄露工具)。
对于事前预防,我们需要对自己使用的内存进行即使的释放,养成良好的代码习惯,当遇到异常时,使用智能指针进行预防。事后检测的程序有很多,但有相当多的工具不靠谱。
因此我们发现智能指针就是来解决内存泄漏的问题的。
2.什么是智能指针
智能指针即RAII(Resource Acquisition Is Initialization,资源获得立即初始化)是一种利用对象生命周期来控制程序资源的简单技术。如最开始提出的例子,如果我们的指针能够像对象一样,出了作用域自己释放销毁,那么就不会出现内存泄漏的情况了。基于这一思想,智能指针孕育而生了。我们将指针托管给某个对象,在对象构造时获取资源,在对象析构时释放资源。这样我们的资源不用再手动进行释放,对象所需要的资源在生命周期内始终有效。
例如我们可以实现下面的类似代码:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* pt)
:_ptr(pt)
{}
~SmartPtr()
{
cout<<"delete "<<endl;
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
使用我们提供的类进行指针的托管,在开头提供的例子中,即便函数出现除0错误,指针的资源依旧能够得到释放,不会照成内存泄漏的风险。
但是上面的SmartPtr并不能称作智能指针,因为该类型的对象不能像正常的指针一样进行解引用操作,也不能使用箭头操作符。
接下来我们对其进行运算符重载:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* pt) :_ptr(pt) {}
~SmartPtr()
{
cout << "delete " << endl;
if (_ptr)
delete _ptr;
}
T& operator*() { return *(_ptr) ;}
T* operator->() { return _ptr ;}
private:
T* _ptr;
};
这样实现的智能指针能够像指针一样进行解引用操作,但是不能进行拷贝指针的操作。
对于拷贝出错的原因我们进行排查会发现进行拷贝后,两个指针指向了同一块区域,在对象进行销毁时,指针会释放两次,因此进行报错。 这就是典型的浅拷贝问题,但是我们又不能实现深拷贝的形式,因为普通的指针就是浅拷贝,两个指针指向的就是同一块区域。
那我们应该如何进行智能指针的设计呢?我们先看看库中的实现方式。
3.智能指针怎么用
智能指针并不是我们实现的这么简单,C++库中为我们提供了智能指针。C++98中库中提供了auto_ptr的智能指针。
3.1auto_ptr
在接口函数中,提供了能够显示释放的接口函数release和获得指针的get函数。auto_ptr可以支持拷贝,他是基于管理权进行转移的拷贝方式。
这是一种极不负责任的做法,auto_ptr将p1的指针直接转移给了p2,虽然不会造成析构两次的错误,但是如果我们想使用p1时就会出现野指针的问题。 总之来说,auto_ptr是一个极其失败的设计,尽量不要使用。
3.2unique_ptr
C++11中吸收了boost库的智能指针scope_ptr,设计了unique_ptr。unique_ptr的做法就是简单粗暴的防拷贝。
3.3shared_ptr
但是我们有时候确实需要指针的拷贝,那么该怎么办呢?C++11中还提供了可以进行拷贝的智能指针shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。每一次进行指针的拷贝,计数就会进行++操作,释放一次指针,计数就会进行--操作,直到最后一个指针被释放,计数变为0,调用delete进行空间的释放。
其中接口use_count是返回该空间有几个指针指向,unique则表示该指针是否唯一的指向一块空间。
3.4weak_ptr
weak_ptr是为了解决shared_ptr的循环引用问题而专门设计的一个指针,weak_ptr不会增加引用计数,只是进行指针的拷贝。weak_ptr不进行资源的管理。
什么是循环引用问题呢?当我们进行链表节点的搭建时,如果使用shared_ptr进行指针的管理就会出现下面的状况。
但是我们这样进行了管理,会发现指针无法进行释放了!
这就是典型的循环引用问题,在节点不连接的时候,n1的计数为1,n2的计数为1,当进程结束后,引用计数减为0,调用析构函数进行资源的释放。但是当n1的next指向n2时,n2节点就会有两个指针指向,一个是n2,一个是n1中的next。n2的prev类似,节点链接后,n1节点也有两个指针指向。此时进程结束,n1,n2被释放,计数减1变为1,next指向n2,prev指向n1。资源不会被清理,只有当next释放n2才会释放,n2释放prev才会释放,prev释放n1才会释放,n1释放next才会释放,此时形成了一个闭环。
为了解决这一问题,我们需要使用weak_ptr来进行管理,weak_ptr不会增加计数,因此进程结束,节点依旧是从1减到0然后释放。
4.模拟实现智能指针
接下来我们进行智能指针的模拟实现:
auto_ptr的实现:
我们要注意在赋值操作时我们需要释放掉原先的空间,避免内存空间泄漏。
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr) :_ptr(ptr) {}
auto_ptr(auto_ptr<T> &p) :_ptr(p._ptr)
{
p._ptr = nullptr;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
auto_ptr<T>& operator=(auto_ptr<T>& p)
{
if (this != &p)
{
if (_ptr)
{
delete _ptr;
}
_ptr = p._ptr;
p._ptr = nullptr;
}
}
T& operator*(){ return *_ptr;}
T* operator->(){ return _ptr;}
private:
T* _ptr = nullptr;
};
unique_ptr中不能进行拷贝和赋值,所以我们直接使用delete删除掉拷贝和赋值函数。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr) :_ptr(ptr) {}
~unique_ptr() { delete _ptr; }
unique_ptr(unique_ptr<T>& p) = delete;
unique_ptr<T> operator=(unique_ptr<T>& p) = delete;
T& operator*() { return *_ptr; }
T* operator->(){return _ptr; }
private:
T* _ptr = nullptr;
};
shared_ptr需要使用引用计数来进行拷贝和赋值,我们不能使用静态成员变量进行计数,因为静态成员变量在一个类中只有一份,不同类型的指针都会增加这个静态成员。所以我们采用初始化时多开辟空间存储计数的原理进行实现。
template<class T>
class shared_ptr
{
public:
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
shared_ptr(T* ptr)
:_ptr(ptr)
, _n(new int(1))
{}
shared_ptr(shared_ptr<T>& p)
:_ptr(p._ptr)
, _n(p._n)
{
(*_n)++;
}
void release()
{
if (--(*_n) == 0)
{
delete _ptr;
delete _n;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& p)
{
if (_ptr == p._ptr)
return *this;
release();
_ptr = p._ptr;
_n = p._n;
(*_n)++;
return *this;
}
~shared_ptr()
{
release();
}
bool unique()
{
if ((*_n) == 1)
return true;
else
return false;
}
int use_count()
{
return _n;
}
T* get()const
{
return _ptr;
}
private:
int* _n;
T* _ptr=nullptr;
};
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<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};