分析一下下面这段程序有没有什么内存方面的问题? 前面在异常的博客中,我们分析了下图一的代码Func函数中如果div()函数抛异常则程序会直接跳到主函数的catch捕获程序部分,然后接着主函数catch捕获程序部分往后执行代码,那么Func函数中new的空间没有执行后面的delete释放空间代码会造成内存泄漏。 因此在异常的博客中我们的处理办法是在Func函数中增加try catch捕获拦截异常,将delete释放空间操作处理完之后再把异常抛出去,如下图二所示。![]()
在异常的博客中new开辟一次空间上图二所示的方式是可以的,但上图二所示的代码两次new开辟空间,如果第二次new开辟空间开辟失败抛了异常,例如上面代码int* p2 = new int开辟失败报异常会直接跳到主函数的catch捕获程序部分,然后接着主函数catch捕获程序部分往后执行代码,那么Func函数中代码int* p2 = new int往后的代码都没有运行,因此第一次new开辟空间后也没有delete释放空间,会造成内存泄漏。
那么如何解决上面的问题呢?c++提出了智能指针的解决方案。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。 注:RAII技术是智能指针实现的指导思想。 如下图所示,我们使用智能指针类来解决本博客第一节的问题,这样即使Func函数中第二次new开辟空间失败即SmartPtr
sp2(new int)中new int开辟空间失败抛异常,程序会直接跳到主函数的catch捕获程序部分,然后接着主函数catch捕获程序部分往后执行代码,此时Func函数栈帧已经销毁,sp1智能指针通过系统自动调用智能指针类的析构函数将sp1智能指针指向的空间进行了释放,解决了内存泄漏的问题。 // 使用RAII思想设计的SmartPtr类 template
class SmartPtr { public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}private:T* _ptr; }; int div() {int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b; } void Func() {//使用方法一int* p1 = new int;SmartPtr sp1(p1);//使用方法二SmartPtr sp2(new int);cout << div() << endl; } int main() {try {Func();}catch (const exception& e){cout << e.what() << endl;}return 0; }
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此AutoPtr模板类中还得需要将operator*函数和operator->函数重载一下,才可让其像指针一样去使用。总结一下智能指针的原理: 1. RAII特性。 2. 重载operator*和opertaor->,具有像指针一样的行为。template
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; }; struct Date {int _year;int _month;int _day; }; int main() {SmartPtr sp1(new int);*sp1 = 10;cout << *sp1 << endl;SmartPtr sparray(new Date);// 需要注意的是这里应该是sparray.operator->()->_year = 2018;// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->sparray->_year = 2023;sparray->_month = 1;sparray->_day = 1; }
智能指针的拷贝问题:
智能指针类模拟的是原生指针的行为,因此智能指针类的拷贝应该是浅拷贝。
智能指针类正常的浅拷贝会出现问题,原智能指针和拷贝对象智能指针都指向同一块内存空间,析构时调用智能指针类的析构函数,两个智能指针会调用两次析构函数分别释放,指向的内存空间会释放两次,系统报错。
问题:容器的迭代器也是模拟原生指针的行为,拷贝的时候也是浅拷贝,为什么智能指针浅拷贝会出错而迭代器不会出错?
答:容器的迭代器类中,迭代器类的析构函数不负责节点的释放,节点属于容器由容器类的析构函数释放,迭代器生命周期结束调用析构函数时不会释放空间,不会报错。对于智能指针,智能指针类的析构函数要负责释放资源,所以智能指针生命周期结束调用析构函数时,如果两个智能指针指向同一空间会连续释放就会报错。
为了解决智能指针拷贝的问题,STL官方库中给出了auto_ptr、unique_ptr、shared_ptr和weak_ptr四种智能指针来解决,下面我们详细介绍这四种智能指针。
auto_ptr文档介绍:auto_ptr - C++ Reference (cplusplus.com)
auto_ptr的实现原理:管理权转移的思想,被拷贝的对象悬空,如下图所示,被拷贝对象无法再使用。注: 1.很多公司明确要求不能使用auto_ptr。 2.auto_ptr在标准库的
中,因此要使用auto_ptr需要#include C++98版本的库中就提供了auto_ptr的智能指针。下面简化模拟实现了一份bit::auto_ptr来了解它的原理。
SmartPtr.h文件:
// C++98 管理权转移 auto_ptr namespace bit {template
class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr & sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;}auto_ptr & operator=(auto_ptr & 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;}; }
boost库:Boost是为c++标准库提供扩展的一些C++程序库的总称。boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称。boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的“准”标准库。
C++11标准库库才更新智能指针实现,C++11出来之前,boost库出了比标准库中auto_ptr更好用的scoped_ptr、shared_ptr、weak_ptr,C++11标准库将boost库中智能指针精华部分吸收了过来,产生了unique_ptr、shared_ptr、weak_ptr智能指针。
unique_ptr文档介绍:unique_ptr - C++ Reference (cplusplus.com)
unique_ptr的实现原理:简单粗暴的防拷贝,即不让拷贝,如下图所示,如果拷贝编译就报错。![]()
注:
1.标准库中unique_ptr的原型是boost库中的scoped_ptr。 2.unique_ptr在标准库的中,因此要使用unique_ptr需要#include 下面简化模拟实现了一份UniquePtr来了解它的原理。 SmartPtr.h文件: // C++11 防拷贝 unique_ptr namespace bit {template
class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(const unique_ptr & sp) = delete;unique_ptr & operator=(const unique_ptr & sp) = delete;private:T* _ptr;}; } 注:
1.防拷贝的方法是拷贝函数只声明不实现,但是仅仅只声明不实现是不行的,不排除一些老六在外面自己实现,如下图的①所示。为了防止这种情况,c++98的方法是将拷贝函数声明设置为私有,这样类外面就无法调用(即使老六自己实现了在类外面也无法调用拷贝函数),如下图②所示,c++11的方法是直接在拷贝函数声明的后面加一个=delete即可,这样拷贝函数不能再被调用(即使老六自己实现了在任何地方也都无法调用拷贝函数),如下图的③所示。![]()
2.赋值运算符重载函数与拷贝构造函数相同,要做到防赋值,即如果赋值编译就报错。
shared_ptr文档介绍:shared_ptr - C++ Reference (cplusplus.com)
shared_ptr的实现原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如实验课老师在上完课之后都会通知,让最后走的学生记得把门锁下。
原理详细解释:shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
注:
1.标准库中shared_ptr的原型是boost库中的shared_ptr。 2.shared_ptr在标准库的中,因此要使用shared_ptr需要#include 下面简化模拟实现了一份UniquePtr来了解它的原理。
SmartPtr.h文件:
注:
1.
上一篇:Word文档内容如何添加双删除线
下一篇:MATLAB-二维线性插值运算