C++异常剖析
什么是异常?
在程序运行的过程中,我们不可能保证我们的程序百分百不出现异常和错误,那么出现异常时该怎么报错,让我们知道是哪个地方错误了呢?
C++中就提供了异常处理的机制。
一、异常处理的关键字
(1)throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
(2)catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
(3)try:try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块(至少要跟一个)。
当被try的括号包围的代码块(我们可称之为保护代码)出现异常时,就会抛出异常,catch块的代码就会捕获异常并执行catch包围的代码块。因为异常也可以有不同的种类,所以我们可以在try后面跟着多个catch块,捕获不同的异常,如下:
try
{
// 保护代码
}
catch( ExceptionName1 e1 )//第一种异常
{
// catch 块
}
catch( ExceptionName2 e2 )//第二种异常
{
// catch 块
}
catch( ExceptionName3 eN )//第三种异常
{
// catch 块
}
二、捕获异常
使用 throw 语句可以在代码块中的任何地方抛出异常。
throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
如下:
可以看到,我们抛出的是一串字符串,所以编译器报错的信息说异常类型是char(其实是const char*,但它简写了)。
实际我们编程的时候,我们肯定不希望编译器就只给我说异常类型是char*就完了。我们还希望把它打印出来,让我们看得到"出现了除数为0的错误",这就需要捕获异常。
#include <iostream>
using namespace std;
double division(double a, double b)
{
try
{
if (b == 0)
{
throw "出现了除数为0的错误";//throw关键字抛出错误类型 const char*
}
}
catch (const char* error_string)//捕获const char*类型的错误
{
cout << error_string << endl;//打印它
}
return (a / b);
}
int main()
{
cout << division(1, 0);
}
可见,有了try和catch后,程序不会在遇到错误时崩溃终止,而是会处理它,然后继续运行。
所以这个程序依然能输出计算结果inf(无限大),但是在此之前也打印了报错信息。
总之,利用异常处理机制,我们就可以在程序运行时处理异常,大大减少程序崩溃的概率。
你在生活中经常遇到软件闪退的问题,十有八九没有做好异常处理。
三、C++标准库自带的异常类型
下表是对上面层次结构中出现的每个异常的说明:
std::exception 该异常是所有标准 C++ 异常的父类。 std::bad_alloc 该异常可以通过 new 抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。 std::bad_typeid 该异常可以通过 typeid
抛出。 std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::logic_error 理论上可以通过读取代码来检测到的异常。
std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument 当使用了无效的参数时,会抛出该异常。
std::length_error 当创建了太长的std::string 时,会抛出该异常。
std::out_of_range 该异常可以通过方法抛出,例如 std::vector 和std::bitset<>::operator。
std::runtime_error 理论上不可以通过读取代码来检测到的异常。
std::overflow_error 当发生数学上溢时,会抛出该异常。
std::range_error 当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error 当发生数学下溢时,会抛出该异常。
四、异常规格说明
异常规格说明的目的是为了让函数使用者知道该函数可能抛出那些异常,可以再函数的声明中列出这个函数可能抛出的所有异常类型,例如:
viod fun() throw(A,B,C,D);
throw后面的括号内,必须写出会抛出的异常的类型,如果括号里面是空的,说明这个函数不会抛出任何异常。
如果你不声明throw(A,B,C,D),那么编译器就会认为这个函数什么异常都可能会抛出。
异常规范声明可以让编译器知道可能会抛出的异常有哪些,就可以提升编译速度和给编译器更多的优化空间,让你的代码性能更高,也可以让其他程序员了解你这段代码可能会产生的异常,提高代码可读性。
五、noexcept关键字
在C++11中新增了noexcept关键字以表示这个函数不会抛出某种异常。并且可以阻止异常的传播。
无条件的noexcept关键字
当我们声明的noexcept关键字无条件时,表示这个函数中所的所有代码都不会产生异常,如下:
void fun() noexcept; //C++11
void fun() noexcept(); //也可以写成这样,等价的
void fun() noexcept(...); //也可以写成这样,等价的
void fun() noexcept(true); //也可以写成这样,等价的
有条件的noexcept关键字
void fun(Type& x, Type& y) noexcept(noexcept(noexcept(fun1()),noexcept(fun2()));
//表示fun函数内会调用到的fun1函数和fun2函数都不会抛出异常,但不保证其他代码不会抛出异常
什么时候我们需要noexcept关键字?
使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上noexcept就能提高效率。
以下情形鼓励使用noexcept:
(1)移动构造函数(move constructor)
(2)移动分配函数(move assignment)
(3)析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。下面代码可以检测编译器是否给析构函数加上关键字noexcept。
(4)叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
最后强调一句,在不是以上情况或者没把握的情况下,不要轻易使用noexcept。
六、定义新的异常类型
只有C++标准库自带的异常类型肯定是不够用的,我们实际工作中还需要根据项目需求定义新的异常类型。
#include <iostream>
#include <exception>
using namespace std;
class DivisionZeroException :public exception//基于exception类定义新的异常类型除以0导致的异常类
{
public:
//这里重载了父类的虚函数what()
//throw ()的括号里面没有东西,这表示这个函数不会抛出任何异常
//const 是常量的关键字,常量在定义后无法被修改
const char* what() const throw ()
{
return "出现了除以0的错误\n";
}
};
double division(double a, double b)
{
try
{
if (b == 0)
{
DivisionZeroException e;
throw e;//throw关键字抛出错误类型DivisionZeroException
}
}
catch (DivisionZeroException e)//捕获const char*类型的错误
{
cout<<e.what();//打印什么异常了
}
return (a / b);
}
int main()
{
cout << division(1, 0);
}
出现了除数为0的错误
inf