异常
C语言其实有两种处理错误的办法:
- 1.一个是终止程序,如assert
- 2. 程序返回错误码,由程序员处理
C++兼容C,自然以上处理机制,一旦出错,比如一个小错,就终止是很不合理。比如服务器。返回错误码呢,并不直观,处理起来十分的麻烦,同时不同错误码对应也很麻烦。
C++处理错误的方式:
异常:一个函数发现出现自己无法处理的错误时,就抛出异常,让直接调用或者间接调用去处理这个错误。
* throw:抛出异常就是通过这个关键字实现。throw + 对象
(可以是自定义类,也可以时内置类型)
* catch :捕获throw抛出得对象,但是要求类型匹配。
catch(const typename e){}
* try :在try代码块里面函数,其得throw才会别识别,否则就无法识别。其后面跟 不定个数 catch,便可捕捉里面发生得异常。
try {} <br/>catch(){}
可以借助下面代码加深理解:
try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
异常抛出与捕获
- 异常由抛出对象引发,对象类型决定激活哪个catch的代码,且激活的时距离try最近的那个匹配的 catch
- 抛出异常对象后会生成一个异常对象的拷贝,很有可能时因为抛出的是一个临时对象,生成了一个拷贝对象。
- catch(…)可以捕获任意类型的异常,但是不知道异常的错误类型。一般就标识未知异常
- 一般来说,抛出与捕获是需要匹配的,哪怕是一个是const,一个不是都不行,但是抛出派生类,可以用基类接受。
运行下面的代码,打断点调试下,对除0函数里面的注释定义的取消,看看效果。
#include <bits/stdc++.h>
using namespace std;
//经过下面的测试,可以发现,异常是一层一层栈帧的寻找,找不到就退出当前栈帧,当前定义的局部变量也会销毁,其后续的代码也不再执行
//实际上到main函数都没找到接受异常的,总体就是异常无法处理,程序就会退出。
class A
{
public:
~A()
{
cout<<"~A\n";
}
};
double Exception_divise_zero(int a,int factor)
{
if(factor==0)
{
// const string s ="Division by zero condition";
// char s[100] = "Division by zero condition";
char* s = "Division by zero condition";
throw (char*)s;//抛出什么类型,接受什么类型,但是基类和派生类之间,基类接受派生类是可行的
//如这里是const char*,那接受就必须是const char* ,少了const或者用const string都不行
}
else
{
return (double)a/factor;
}
}
void CallDivision()
{
A b;//但是可以发现,在一层层处理当前栈帧的时候,会销毁这时候定义的局部变量
Exception_divise_zero(10,0);
A a;//经过测验可以发现,抛异常之后,之后的代码不会再执行了
}
void Test_Exception(){
try
{
CallDivision();
}
catch(const string s)
{
std::cerr<<"const"<<s<<'\n';
}
catch(string s)
{
std::cerr<<s<<'\n';
}
catch(char* e)
{
std::cout << e<< '\n';
}
catch(const char* e)
{
std::cerr <<"const "<< e<< '\n';
}
catch(...)//可以接受任何异常,下面一般写未知异常,这样的话,就能避免直接杀死程序
{
cerr<<"unkown Exception \n";
}
} ```
结论:如果你运行过看到了现象,**抛出异常之后,函数是跳转级别的,有点像C语言的goto的感觉,直接回“跳过”好几个函数栈帧,当然实际不是这么回事,上面的代码证明,抛出异常之后呢,会结束当前函数栈帧,但是该做的局部变量释放那些都会处理。**
如果你解除了注释,抛出了字符数组的s,你会发现接受之后发现打印时乱码。
这个原因是因为,就是验证局部变量是否被消除,其实就是因为定义在里面的变量被释放罢了。
**分别注释catch,我们发现,const对普通非const是可以接受的,这意味着,你想抛出非const,就不要把const放在前面。**,当然实践里面,推荐不要搞这些易混淆的操作,害人害己。
下面是父类接受子类:
```cpp
class MyExeception{
protected:
int _eid;
string _errorMsg;
public:
MyExeception(int eid,string eMsg)
:_eid(eid)
,_errorMsg(eMsg)
{
}
virtual string what()const
{
return "eid::"+to_string(_eid)+"\n"+"eMsg::"+_errorMsg;
}
};
class net_serve_exceoptin: public MyExeception{
string Mysign;
public:
net_serve_exceoptin(int eid,string eMsg)
:MyExeception(eid,eMsg)
,Mysign("网络错误")
{
;
}
};
int Test()
{
throw net_serve_exceoptin(1,"net error");
return 0;
}
// int main()
// {
// while(1)
// {
// try
// {
// Test();
// }
// catch(MyExeception& e)
// {
// std::cerr << e.what() << '\n';
// }
// catch (...)
// {
// cout << "Unkown Exception" << endl;
// }
// }
// return 0;
// }
下面来看异常的缺陷:
double Division(int a,int factor)
{
if(factor==0)
{
throw "Division by zero condition";
}
else
{
return (double)a/factor;
}
}
void Exception_Newdelete()
{
int *arr = new int[10];
//Division(10,0);//这个Division的函数,抛出一场之后,由于当前函数没有,接受的,后面代码不执行,显然会导致内存错误
//----------------------------------------------------
//解决方法一,重新抛出,但是解决的问题十分受限,本质并没有解决完.倘若有调用多个函数,各自又都开辟后续的空间在该处异常之后释放,显然会导致大量的问题
try{
Division(10,0);
}
catch(const char* e)
{
delete[]arr;
arr = nullptr;
//重新再次抛出
throw "Division error";
}
catch(...)//捕获什么异常,抛出什么异常
{
delete[]arr;
arr = nullptr;
throw;
}
//因此另外的解决方法就是智能指针
delete[] arr;
}
void Exception_get()
{
try{
Exception_Newdelete();
}
catch(const char* e)
{
cerr<<e<<endl;
}
}
运行上面的代码,很容易就发现,指针申请的空间由于抛出异常,没有释放,这导致了内存泄漏。这就是异常带来的问题。解决方案就是智能指针。
智能指针
首先了解下RAII思想。
RAII(Resource Acquisition Is Initialization):利用对象的生命周期来控制程序的资源。
而智能指针,就是基于这个特点去设计的。
简单来说:设计一个类,使其实例化对象的时候,获取资源;析构的时候,释放资源。
我们知道,C++支持重载运算符,所以只要重载*和->就可以解决像指针的使用。
其采用的是转移控制权的思想,指针要赋值拷贝,原指针呢,就会被置为空。 这其实不好,万一使用者不注意,就会产生解引用空指针等问题造成一系列问题。
2.unque_ptr:这是C++后来的另外一种设计,禁止赋值和拷贝,这样就不会有指针悬空的情况,但是对某些情况就无法处理。
下面是简化的模拟实现:
template<class T>
class unique_ptr_1{
public:
unique_ptr_1(T* p)
:ptr(p)
{
}
unique_ptr_1(const unique_ptr_1& a) = delete;
unique_ptr_1 operator=(const unique_ptr_1& a) = delete;
T& operator*()
{
if(ptr)
{
return *ptr;
}
else
{
throw "Ref nullptr";
}
}
T* operator->()
{
return ptr;
}
~unique_ptr_1()
{
delete[] ptr;
}
private:
T* ptr;
};
3.share_ptr:这种指针就是用一个计数的办法,存储有多少个指向对应空间的指针,用这个来解决不能赋值的问题。
template <class T>
class sharePtr{
typedef sharePtr<T> Self;
T* m_ptr;
int* count;
function<void(T*)> _del= [](T*ptr)mutable->void{delete ptr;};//解决申请数组空间释放的问题
public:
T* get()
{
return m_ptr;
}
int Usecount()
{
return *count;
}
sharePtr(T* p)
:m_ptr(p)
,count(new int(1))
{
}
template<class Del>
sharePtr(T* p,Del del )
:m_ptr(p)
,count(new int(1))
,_del(del)
{
}
//可以直接写这个含缺省参数取顶替上面两个,这样就不需要在声明那里给默认值
// template<class Del>
// sharePtr(T* p,Del del = [](T*ptr)mutable->void{delete ptr;})
// :m_ptr(p)
// ,count(new int(1))
// ,_del(del)
// {
// }
sharePtr()
:m_ptr(nullptr)
,count(new int(1))
{
}
Self operator=(const Self& ptr)
{
if(this ==&ptr)
{
return *this;
}
release();
m_ptr = ptr.m_ptr;
count = ptr.count;
(*count)++;
return *this;
}
sharePtr(const Self& ptr)
:m_ptr(ptr.m_ptr)
,count(ptr.count)
{
if(count && *count>0)
{
++*count;
}
}
T* operator->()
{
return m_ptr;
}
T& operator*()
{
return *m_ptr;
}
~sharePtr()
{
release();
}
void release()
{
if(--*count ==0)
{
_del(m_ptr);
delete count;
count = nullptr;
m_ptr = nullptr;
}
}
};
有这个指针,基本解决了90%的场景,但是呢有一个特殊的场景没法解决。循环引用
目前没有方便的解决办法,C++里面提供的方案就是对意识到是循环引用的情况,就使用另外一种指针。
weakPtr。让节点里面是weakPtr,让节点申请的使用sharePtr就可以解决问题了。当然,需要实现sharePtr到WeakPtr的构造。
C++11就是如此实现的。