C语言处理错误的方式
传统的错误处理机制: 1.终止程序缺陷:用户难以接受。 2.返回错误码:缺陷:需要自己去查错误码对应的解释。 C语言一般使用错误码来处理错误,部分情况下才会用终止情况处理处理严重的错误。
C++异常
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数直接或者间接的调用处理这个问题。 1.throw:当问题出现时,程序会抛出异常。通过关键字throw完成。 2.catch:在处理异常问题的地方,通过异常处理程序捕获异常。catch关键字可用于捕获异常,可以有多个catch捕获异常。 3.try:try块中的代码表示将被激活的特定异常,它后面会使用一个或者多个catch块。 如果一个块炮捶个异常,捕获方法会使用try和catch。try中防止可能抛出的异常代码,try块的代码被称为保护代码。 语法规则:
try
{
//保护的标识代码
}
//catch块
catch(e1)
{
//...
}
//catch可以有多个
异常的使用
异常抛出和捕获
异常的抛出和匹配原则
1.异常是通过抛出对象而引发的,该对象的类型决定了应该激活catch的哪个处理代码。 2.被选中的处理代码是调用链中的与该对象类型匹配并且抛出异常位置最近的的哪一个。 3.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的对象可能是一个临时对象,所以会出现一个拷贝对象,这个拷贝的临时对象会在被catch后销毁(处理类似于函数的传值返回)。 4.catch{...}可以捕获任意类型的异常 5.实际中抛出和捕获异常的匹配原则有个例外,并不是什么类型都完全匹配,可以抛出派生类对象,使用基类捕捉。
在函数调用链中异常栈展开匹配
1.首先检查throw本身是否在try块的内部,如果是再查找匹配的catch‘语句。如果有匹配的,则调到catch的地方进行处理。 2.没有匹配的catch则退出当前栈,继续在调用函数的栈中查找匹配的catch。 3.如果daodamain函数的栈,依旧没有匹配的,就终止程序(沿着调用链查找匹配的catch子句的过程称为栈展开) 4.找到匹配的catch语句并处理后,会沿着catch字句后面继续执行。
代码展示:
#include<iostream>
using namespace std;
double Div(double a, double b)
{
if (b == 0)//抛异常
{
throw "除数不能为0";
}
return b / a;
}
void Func()
{
int a, b;
cin >> a>>b;
cout << Div(a, b) << endl;
}
int main()
{
try {
Func();
}
catch (const char* err)
{
cout << err << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
return 0;
}
//打印:除数不能为0
异常的重新输入
可能单个的catch不能完全处理一个异常,再进行一些矫正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。 示例:
#include<iostream>
using namespace std;
double Div(double a, double b)
{
if (b == 0)//抛异常
{
throw "除数不能为0";
}
return b / a;
}
void Func()
{
int* arr = new int[10];
//当出现除0错误的时候,arr就没有处理。
//这里波或异常后,不直接处理异常,而是抛出去,让外部链函数处理
try {
int a, b;
cin >> a >> b;
cout << Div(a, b) << endl;
}
catch (...)
{
cout << "delete[]" << arr << endl;
delete[] arr;//有异常,释放arr防止内存泄漏
throw;//重新抛出,没加啥参数,是因为不知道是啥异常,反正抛出就对了
}
//....
cout << "delete[]" << arr << endl;
delete[] arr;
}
int main()
{
try {
Func();
}
catch (const char* err)
{
cout << err << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
return 0;
}
异常安全
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或者没有完全初始化。 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏的风险。 C++经常会导致内存泄漏的问题,智能指针能有效解决此类问题。
异常规范
1.异常规范说明目的是为了让函数使用者知道该函数可能抛出的异常有哪些。可以在函数的后面接throw(类型),列出这个函数可能抛出的异常类型。 2.函数的后面接throw(),表示函数不抛异常。 3.若无异常接口声明,则此函数可以不抛出任何类型的异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
C++标准库的异常体系
exception 类的直接派生类:
异常名称 | 说 明 |
---|---|
logic_error | 逻辑错误。 |
runtime_error | 运行时错误。 |
bad_alloc | 使用 new 或 new[ ] 分配内存失败时抛出的异常。 |
bad_typeid | 使用 typeid 操作一个 NULL [指针],而且该指针是带有虚函数的类,这时抛出 bad_typeid 异常。 |
bad_cast | 使用 dynamic_cast 转换失败时抛出的异常。 |
ios_base::failure | io 过程中出现的异常。 |
bad_exception | 这是个特殊的异常,如果函数的异常列表里声明了 bad_exception 异常,当函数内部抛出了异常列表中没有的异常时,如果调用的 unexpected() 函数中抛出了异常,不论什么类型,都会被替换为 bad_exception 类型。 |
logic_error 的派生类:
异常名称 | 说 明 |
---|---|
length_error | 试图生成一个超出该类型最大长度的对象时抛出该异常,例如 vector 的 resize 操作。 |
domain_error | 参数的值域错误,主要用在数学函数中,例如使用一个负值调用只能操作非负数的函数。 |
out_of_range | 超出有效范围。 |
invalid_argument | 参数不合适。在标准库中,当利用string对象构造 bitset 时,而 string 中的字符不是 0 或1 的时候,抛出该异常。 |
runtime_error 的派生类:
异常名称 | 说 明 |
---|---|
range_error | 计算结果超出了有意义的值域范围。 |
overflow_error | 算术计算上溢。 |
underflow_error | 算术计算下溢。 |
对expection类实现自己的异常类
int main()
{
try{
vector<int> v(10, 5);
// 这里如果系统内存不够也会抛异常
v.reserve(1000000000);
// 这里越界会抛异常
v.at(10) = 100;
}
catch (const exception& e) // 这里捕获父类对象就可以
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0; }
异常的优缺点
优点
1.异类对象定义好了,相比错误码的方式可以准确清晰的站输出各种错误的信息,甚至是包含战队的信息,这样可以更好的定位程序的bug 2.返回错误码的传统方式有个巨大的问题,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。 3.很多第三方库包含异常。 4.不分寒暑使用异常更好处理,不方便使用错误码的情况。
缺点
1.异常会导致程序的执行流乱跳,比较混乱。并且运行时出错抛异常就会乱跳,这使得调试分析时比较困难。 2.异常会有一些性能的开销。 3.C++没有垃圾回收机制,资源需要自己管理。 4.C++标准库的异常体系一般,导致自定义对象的各自的一场体系,非常的混乱。 5.异常尽量规范使。
总结:异常是利大于弊。