在上一篇中简单介绍了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下才会触发异常,linux下时可以正常跑过的。
auto_ptr是一种不太好的方法。在下一节中介绍scoped_ptr的使用
完,