在上一篇中简单介绍了C++中的异常机制,我们了解到异常会导致执行流的乱跳现象,这样的话就会引起一些问题,如下:
#include <iostream> #include <stdio.h> #include <vector using namespace std; void func() { int *p1=new int[10]; vector<int> v1; v1.at(0);//这里触发异常 cout<<"释放p1所指向资源"<<endl; delete[] p1; } void test() { try { func(); } catch(exception &e) { cout<<e.what()<<endl; } } int main() { test(); return 0; }
很明显上面的代码中因为异常导致执行流的乱跳,并没有正常释放资源,不仅是上面的问题进场对于这样的问题C++中是这样解决的
C++中引出了RAII(Resource Acquisition Is Initialization,直译为资源分配即初始化)
定义一个类来封存资源的分配的释放,即在这个类的构造函数中完成资源的分配和初始化,在这个类的析构函数中完成资源的清理,这样可以保证资源的正确初始化和释放。
就如上面的情况,可以有下面实现方法:
#include <iostream> #include <stdio.h> #include <vector> using namespace std; template<class T> class AutoPtr { public AutoPtr(T* ptr):_ptr(ptr)//构造函数来保存 {} ~AutoPtr()//析构函数来负责释放 { delete _ptr; cout<<"~AutoPtr"<<endl; } private: T * _ptr; }; void func() { AutoPtr<int> p1(new int[10]);//这里的对象p1负责我们堆上开辟的空间的保存和释放 vector<int> v1; v1.at(0);//这里触发异常 //即使抛异常了,只要出作用域就一定会调用p1的析构函数,将资源正确释放} void test() { try { func(); } catch(exception &e) { cout<<e.what()<<endl; } } int main() { test(); return 0; }
上面的方法我们正常调用了析构函数,而我们在析构函数中进行了资源的释放
这里是用智能指针的来实现的(智能的管理指针所指空间的保存和释放)
智能指针--只是RAII的一种实践,可以理解为下面两层含义:
1.RAII ,构造函数保存资源,析构函数释放资源
2.像指针一样(可以解引用)
我们将上面的代码进行完善近似模拟实现C++标准库中的auto_ptr
自己重载出operator*()和operator->()(->是为了给结构体使用的)
#include <iostream> #include <stdio.h> #include <vector> using namespace std; template<class T> class AutoPtr { public: AutoPtr(T* ptr):_ptr(ptr)//构造函数来保存 {} ~AutoPtr()//析构函数来负责释放 { delete _ptr; cout<<"~AutoPtr"<<endl; } T & operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T * _ptr; }; struct AA { int _a; int _b; }; void func() { AutoPtr<AA> p1(new AA);//这里的对象p1负责我们堆上开辟的空间的保存和释放 (*p1)._a=20; p1->_b=30; cout<<(*p1)._a<<endl; cout<<p1->_b<<endl; // vector<int> v1; // v1.at(0);//这里触发异常 // //即使抛异常了,只要出作用域就一定会调用p1的析构函数,将资源正确释放 } void test() { try { func(); } catch(exception &e) { cout<<e.what()<<endl; } } int main() { test(); return 0; }
分析上面的两个重载函数
(*p1)._a=20其实可以写成是p1.operator*()._a=10
p1->_b=30原生形式应该是p1.operator->()->_b=30,这样的话应该这样用,p1->->_b=30
但是编译器为了增强可读性,规定p1->_b=30才是正确形式
再分析上面我们实现的代码,若是遇到这样的情况,
AutoPtr<AA> p1(new AA);//这里的对象p1负责我们堆上开辟的空间的保存和释放 AutoPtr<AA> p2(p1);//用p1对象拷贝构造p2对象我们上面没有实现拷贝构造函数,那么默认的拷贝构造是一个浅拷贝,这里会释放两个对象,就会调用两次析构函数,就会对同一块空间进行释放两次,程序崩溃。
那么我们这里就会想到两种解决方法
1.采用深拷贝
2.采用引用计数管理释放
我们这里的AutoPtr对象是用来管理资源的保存和释放的,假如采用采用深拷贝的方法,那就是说我们每个管理同一个指针的AutoPtr对象里面的指针确不同的,那么通过解引用改变其中一个,另外一个不可见,这里很明显不是我们的想要的情景。那么这里就采用引用计数的方法来实现。
其实在C++98标准库中是如何是采用了一种不是很好的处理方法,采一种管理权转移的方法,如下实现:
#include <iostream>#include <stdio.h>#include <vector>using namespace std;template<class T>class AutoPtr{public: AutoPtr(T* ptr):_ptr(ptr)//构造函数来保存 {} AutoPtr(AutoPtr<T> & p1):_ptr(p1._ptr) { p1._ptr=NULL; } ~AutoPtr()//析构函数来负责释放 { delete _ptr; cout<<"~AutoPtr()"<<endl; }
AutoPtr<T> & operator=(AutoPtr<T> &p1) { if(_ptr!=p1._ptr)//防止自己给自己拷贝 { _ptr=p1._ptr; p1._ptr=NULL;//将管理权转移给要拷贝的对象,自己置空 }
return *this; } T* GetPtr() { return _ptr; } T & operator*() { return *_ptr; } T* operator->() { return _ptr; }private: T * _ptr;};struct AA{ int _a; int _b;};void func(){ AutoPtr<AA> p1(new AA);//这里的对象p1负责我们堆上开辟的空间的保存和释放 (*p1)._a=20; AutoPtr<AA> p2(p1); if(p1.GetPtr())//这样的话每次访问时都必须要判断以下是否为空,否则可能会导致崩溃 { cout<<(*p1)._a<<endl; } if(p2.GetPtr()) { cout<<(*p2)._a<<endl; } // vector<int> v1; // v1.at(0);//这里触发异常 // //即使抛异常了,只要出作用域就一定会调用p1的析构函数,将资源正确释放}void test(){ try { func(); } catch(exception &e) { cout<<e.what()<<endl; }}int main(){ test(); return 0;}
这种实现方法其实是新的版本种的解决方法,再旧的版本中,是这样实现的:(用一个owner标志来表明是否为所有者)
#include <iostream>#include <stdio.h>#include <vector>using namespace std;template<class T>class AutoPtr{public: AutoPtr(T* ptr):_ptr(ptr),_owner(true)//构造函数来保存 {} AutoPtr(AutoPtr<T> & p1):_ptr(p1._ptr) { _owner=true; p1._owner=false; } ~AutoPtr()//析构函数来负责释放 { if(_owner)//析构时,必须是自己的才能释放 { delete _ptr; cout<<"~AutoPtr()"<<endl; } }
AutoPtr<T> & operator=(AutoPtr<T> &p1) { if(_ptr!=p1._ptr)//防止自己给自己拷贝 { _ptr=p1._ptr; p1._owner=false; _owner=true; }
return *this; } T* GetPtr() { return _ptr; } T & operator*() { return *_ptr; } T* operator->() { return _ptr; }private: T * _ptr; bool _owner;};struct AA{ int _a; int _b;};void func(){ AutoPtr<AA> p1(new AA);//这里的对象p1负责我们堆上开辟的空间的保存和释放 (*p1)._a=20; AutoPtr<AA> p2(p1); AutoPtr<AA> p3(p2); cout<<(*p1)._a<<endl;//这里的三个对象都可以进行访问 cout<<(*p2)._a<<endl; cout<<(*p3)._a<<endl;}int main(){ func(); return 0;}
那么上面两种方法哪种更好一些呢,可以肯定的说,在两种都不怎么好的方法中非要选一个的话,其实是第一种更好一些,
因为第二种存在一个这样的问题,当所属者owner先释放是时候,因为其他的对象里面并没有进行访问限制,那么当你再进行访问的时候,其实就是野指针了,对一块已经释放的空间进行读时不一定会出错,进行写时就会崩溃了。例如:
#include <iostream>#include <stdio.h>#include <vector>using namespace std;template<class T>class AutoPtr{public: AutoPtr(T* ptr):_ptr(ptr),_owner(true)//构造函数来保存 {} AutoPtr(AutoPtr<T> & p1):_ptr(p1._ptr) { _owner=true; p1._owner=false; } ~AutoPtr()//析构函数来负责释放 { if(_owner)//析构时,必须是自己的才能释放 { delete _ptr; cout<<"~AutoPtr()"<<endl; } } AutoPtr<T> & operator=(AutoPtr<T> &p1) { if(_ptr!=p1._ptr)//防止自己给自己拷贝 { _ptr=p1._ptr; p1._owner=false; _owner=true; }
return *this; } T* GetPtr() { return _ptr; } T & operator*() { return *_ptr; } T* operator->() { return _ptr; }private: T * _ptr; bool _owner;};struct AA{ int _a; int _b;};void func1(AutoPtr<AA> p)//这里也是会进行拷贝构造一个对象p(owner){ cout<<(*p)._a<<endl;}//这里函数出作用域,p对象就会被释放,当owner被释放后,其他的就会出现问题void func(){ AutoPtr<AA> p1(new AA);//这里的对象p1负责我们堆上开辟的空间的保存和释放 (*p1)._a=20; AutoPtr<AA> p2(p1); AutoPtr<AA> p3(p2); func1(p3);//这里将owner做为参数传过去,这里进行值传递 (*p1)._a=30; cout<<(*p2)._a<<endl; cout<<(*p3)._a<<endl;}int main(){ func(); return 0;}
这里因为编译器不处理的不同,在vs下才会触发异常。
总之auto_ptr是一种不太好的方法。在下一节中介绍scoped_ptr的使用
完,