C++异常
-
为什么要用c++的异常处理机制?
在我们平时写C语言程序时,经常出现会出现一些错误,有些是我们可以预测到的,这些我们可以通过返回错误码,或者设置回调函数打印错误信息等方法处理。但还有些程错误是我们不好预测的,如断错误,这时产生错误我们没有处理就会终止程序,然而我们经常运行程序后遇到错误不希望终止,如我们进程使用一些垃圾软件会闪退,这就是因为他门的程序异常没有处理好,所以遇到预测范围外的错误时,程序就会直接终止。所以为了解决这种情况,在C++里面就提出了异常处理机制,当一个函数无法处理产生的错误时,就抛出异常,让函数的调用者直接或者间接处理从错误。
-
C++异常处理的抛出与捕获
当 throw 抛出异常时,就会暂停当前函数,先去当前函数里寻找同类型的catch异常捕获,如果当前函数没有,就释放当前函数的栈帧,去上一层函数里去找catch,这里是按照就近原则寻找catch,如果没有找到则退出程序。这个过程称为==栈展开==。
-
C++异常处理注意的地方
1.代码中抛出异常对象后,会在函数调用链里与该对象类型匹配且离抛出异常处最近的catch匹配(==就近原则==)。
2.异常抛出后,会打乱程序执行流,会释放局部存储对象,因此在调试含异常的代码比较难调试。
3.异常抛出的类型必须和catch捕捉的类型必须匹配才可捕获,只有以下三种情况例外:
- 允许非const对象到const对象的转换
- ==允许派生类到基类的转换==,这一点很重要,因为库里的exception就用到了这条原理。
- 将数组转换为指向数组的指针,将函数转换为指向函数的指针。
4.(==异常的再次抛出==)catch可以将收到的异常处理后再次抛给外层调用链,再由外层catch进一步处理。
5.记住!不要在析构函数里和构造函数里抛异常。分别可能会导致对象数据清理不完全,造成内存泄漏。可能会造成对象初始化不完全,在别人使用时可能调用都野指针。
- ## C++的库里面提供了一种==exception==类
这个异常类有什么作用呢?
在写一些项目时,会分工不同的人来合作,那么有三个人来实现三个不同的功能的接口,我要调用他们三个人设计的接口,可是他们都使用了异常,但我又不知道他们抛出的异常都有什么类型,就算知道,在大型项目里也要常设计好多种类型的异常,我难道要每种类型设置一个catch吗?那怎么办呢?
class exception {
public:
exception() throw();
exception(const exception&) throw();
exception& operator= (const exception&) throw();
virtual ~exception() throw();
virtual const char* what() const throw();
}
exception是一个抽象类基类,他们三个人在设计异常对象时可以使异常对象继承这个类,并重写基类里的what函数,what是用来展示异常原因的。那么我调用他们的接口就可以直接catch(exception&e),用基类类型就可以接收所有的子类异常了,通过e.what()就可以展示出异常信息,是不是很方便,如果不明白看下面栗子。
看下面模拟项目如何使用exception
#include<iostream>
#include<string>
#include<windows.h>
using namespace std;
class Exception
{
public:
Exception(const string& msg, int id)
:_errMsg(msg)
, _errId(id)
{}
virtual string what()=0;
protected:
string _errMsg;
int _errId;
};
class connectException : public Exception//连接数据库异常
{
public:
connectException(const string& Msg, int id)
:Exception(Msg , id)
{}
virtual string what()
{
string msg = "【数据库错误:】";
msg += _errMsg;
return msg;
}
};
class getinfoException : public Exception//获取数据异常
{
public:
getinfoException(const string& Msg, int id)
:Exception(Msg, id)
{}
virtual string what()
{
string msg = "【获取数据数据错误:】";
msg += _errMsg;
return msg;
}
};
class echoException : public Exception//响应异常
{
public:
echoException(const string& Msg, int id)
:Exception(Msg, id)
{}
virtual string what()
{
string msg = "【响应错误:】";
msg += _errMsg;
return msg;
}
};
void connectsql()
{
if (rand() % 5 == 0) //这里是模拟函数产生异常,以下雷同
{
throw connectException ("数据库连接错误", 1);
}
if (rand() % 3 == 0)
{
throw connectException ("网络错误",2);
}
cout << "【OK 数据库连接成功】" << endl;
}
void getinfo()
{
if (rand() % 2 == 0)
{
throw getinfoException("获取数据失败", 101);
}
if (rand() % 3 == 0)
{
throw getinfoException("数据处理错误", 102);
}
cout << "【OK 获取客户端信息成功】" << endl;
}
void echoclient()
{
if (rand() % 2 == 0)
{
throw echoException("响应错误", 202);
}
if (rand() % 3 == 0)
{
throw echoException("数据丢失", 203);
}
cout << "【OK 响应客户端成功】" << endl;
}
class HTTPserver
{
public:
void start()
{
while (1)
{
try
{
connectsql();
Sleep(1000);
getinfo();
Sleep(1000);
echoclient();
Sleep(1000);
}
catch (Exception& e)
{
cout << e.what() << endl;
}
}
}
};
int main()
{
HTTPserver h;
h.start();
return 0;
}
C++异常的优点与缺点
优点:
1.可以清晰的展示出错误原因,不像返回错误码那么模糊
2.许多第三方库使用异常,因此容易与这些结合使用
3.在测试框架里使用比较方便
缺点:
1.会打断执行流,函数有可能不在该返回的地方返回,这样使得代码的管理和调试困难
2.异常安全要使用RAII和不同编码实践。加大了代码量,需要大量的支持。C++异常处理
异常处理
C++的异常机制为程序员提供了一种可以更加自然处理异常的方法。
好处:使用异常可以使错误和处理分开来,由库函数抛出异常,由调用者捕获并处理异常,是否终止异常就由调用者决定。
异常的处理结构
try{ //可能引发异常的代码 } catch(ErrorType error){ //异常处理 } catch(...){ //省略号代表可以接受任何类型的异常 }
抛出异常
使用
throw 异常对象
语句两个含义:
沿着调用链的函数将提早退出,类似于return语句。
一旦程序开始执行异常处理代码,沿着调用链创建的对象(局部对象,**对于动态分配的变量将不会销毁)将被销毁。
栈展开:栈展开过程沿着潜逃函数的调用链不断查找,知道找到与异常相匹配的catch字句为止或未找到catch子句,则退出主函数。
重新抛出异常
当捕获了一个异常,当当前无法处理时,我们可以先处理此时可以完成的处理,然后再次调用
throw;
抛出异常。当catch异常声明语句是引用,在catch子句中对异常对象修改的内容才能被保留并继续传播
catch(error &err){ throw; }
异常对象
throw语句中的表达对象必须是完全类型的。
如果语句是类类型的,则必须包含一个可访问的析构函数和一个可访问的拷贝构造函数或移动构造函数。
如果语句时数组类型或函数类型,则表达式将被转换成与之对应的指针类型,注意:抛出一个局部对象的指针是错误的
当抛出了一条表达式时,该表达式的静态编译类型决定了异常对象的类型。
捕获异常
catch语句的异常声明类似于函数的声明
可以忽略捕获形参的名字
声明的类型必须是完全类型,可以是引用,指针,还可以加上
const
异常的匹配规则比正常的转换规则相比,受到了更多限制
必须正确处理捕获异常的顺序,派生类的catch子句应该位于基类的catch子句之前。
如果未捕获异常,将会调用函数
std::terminate()
,默认情况时调用abort,可以调用set_terminate()来设置终止函数,参数是一个函数指针,类型是:void (*terminate)()。try{ throw A(); }catch(B b){ //什么时候可以被捕获呢 }
A和B时相同的类型。
B是A的直接或间接基类,此时B会丢失A的部分信息。
A,B是指针或引用,与普通类型的引用和指针规则一致。
函数抛出异常的描述(C++11已经取消该方案)
可以将可能抛出的异常集合作为函数声明和定义的一部分,如果函数违反了这个规定,将会转换为一个
std::unexpected()
调用,默认是调用std::terminate()
,通常 是调用abort()
。int a();//表示可以抛出任何异常 int b() throw();//表示不会抛出任何异常 int c() throw(x1,x2);//表示会抛出x1,x2类型以及其派生类类型的异常
示例
#include <iostream> #include <string> #include <exception> using namespace std; class MyError{ public: MyError(string err):error(err){} string what(){return error;} ~MyError() throw(){} private: string error; }; void funCall(){ throw MyError("zhainankl"); } int main(){ try{ funCall(); }catch(MyError e){ cout<<"MyError e:"; cout<<e.what()<<endl; }catch(...){ cout<<"..."<<endl; } return 0; }
其他
throw和throw new的区别
throw exception()
表示编译器会自动建立一个一场对象,并且由编译器负责清理对象所占内存
throw new exception()
表示抛出一个由用户建立的异常对象的指针,并且在catch子句中声明为exception *
,需要由用户自己处理异常对象的内存。建议使用
throw exception()
析构函数中不能抛出异常
- 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
- 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
- 析构函数的异常不能被抛出析构函数之外,需要在析构函数内部进行处理。
throw抛出的对象
如果throw抛出一个对象,那么无论catch中用什么类型接收(引用或其他),在传递到catch之前都会构造一个临时对象,也就是说至少经历了一次对象的复制,因此要求异常类型必须可复制的,并且该临时对象的类型与异常对象的静态类型是一致的,也就是说,如果throw抛出的是一个指向子类对象的父类引用,那么会发生分割现象,即只有子类对象中的父类部分会被抛出,抛出对象的类型也是父类类型。