前言
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
try-catch
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。
catch语句匹配被抛出的异常对象时,如果catch语句的参数是引用型,则该参数直接引用到异常对象上;如果catch语句的参数是传值的,则拷贝构造一个新的对象作为catch语句的参数的值。语句结束时,先析构catch的参数对象,再析构throw语句抛出的异常对象。
catch语句匹配异常对象时,规则很严格,不会做隐式类型转换。除了非const到constg、派生类到基类的转换、数组和函数类型转换对应的指针类型。
再catch语句中可以使用不带表达式的throw语句将捕获的异常重新抛出,让外层catch可以再次捕获。本层其他catch不能再次捕获。
throw;
被重新抛出的的异常对象,与当前catch形参无关,比如派生类的异常对象被catch的基类形参捕获,再次抛出时,任然是派生类的异常对象。但是如果在本层catch的形参是引用类型,是可能在catch中修改异常对象的。
throw
throw是一个C++关键字,与其后的表达式构成了throw语句,语法上类似于return语句。throw语句必须被包含在try块之中,可以是被包含在调用栈的外层函数的try中。所以throw关键字的作用就是主动抛出异常。
执行throw语句时,其表达式的运算结果作为对象被复制构造为一个新的对象,放在内存的特殊位置(既不是堆、也不是栈中,Windows上是放在“线程信息块TIB”中)。这个新的对象由匹配且最接近的try-catch捕获。
由于throw语句会进行一次副本拷贝,所以异常对象支持拷贝构造。
如果catch中的形参是引用类型,则异常对象只会执行一次拷贝构造。如果形参是非引用的,有n层非引用的catch就执行n+1次拷贝构造函数。
C++异常标准
C++ 提供了一系列标准的异常,定义在
std::exception:该异常是所有标准 C++ 异常的父类。
std::bad_alloc:该异常可以通过 new 抛出。
std::bad_cast:该异常可以通过 dynamic_cast 抛出。
std::bad_exception:这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid:该异常可以通过 typeid 抛出。
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:当发生数学下溢时,会抛出该异常。
可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:
#include
#include
#include
struct MyException : public std::exception
{
MyException(const char* p) : log(p)
{
std::cout << "constructor:" << this << std::endl;
}
MyException(const MyException& obj) : log(obj.log)
{
std::cout << "copy constructor:" << this << std::endl;
}
~MyException()
{
std::cout << "destructor" << this << std::endl;
}
void show_log() const throw ()
{
std::cout << log << this << std::endl;
}
const char * what() const throw ()
{
return log.c_str();
}
private:
std::string log;
};
int fun(int m, int n)
{
if (0 == n)
{
throw MyException("Division by zero condition!");
}
return m / n;
}
int main()
{
try
{
fun(1, 0);
}
catch (MyException e)
{
e.show_log();
}
}
结果:
其中what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。
noexcept
noexcept关键字告诉编译器,函数不会发生异常,这有利于编译器对程序做更多优化。如果运行时,noexcept函数向外抛出了异常(函数内部捕获异常并完成处理不算),程序就调用std::terminate()函数,该函数内部会调用std::abort()终止程序。
建议使用noexcept的场景:
- 移动构造函数(move constructor)
- 移动分配函数(move assignment),就是使用move的赋值运算符重载
- 析构函数(destructor),析构函数默认会设置noexcept
- 叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
前面例子的fun加上noexcept,则再执行时会直接中断,不会抛出异常
int fun(int m, int n) noexcept
{
if (0 == n)
{
throw MyException("Division by zero condition!");
}
return m / n;
}
还可以使用有条件的noexcept,比如把上面的noexcept改成noexcept(false),就会正常抛出异常
构造函数初始化列表的异常机制
构造函数没有返回值,所以应该用异常来报告发生的问题。构造函数抛出异常就意味着该构造函数没有执行完,所以其对应的析构函数不会被自动调用,因此构造函数应该先析构所有已初始化的基对象、成员对象,再抛出异常。
myClass::myClass(type1 pa1)
try: _myClass_val (初始化值)
{
/*构造函数的函数体 */
}
catch ( exception& err )
{
/* 构造函数的异常处理部分 */
};
析构函数被期望不向函数外抛出异常。析构函数抛出异常时,将直接调用terminator()系统函数终止程序。如果一个析构函数内部抛出了异常,就应该在该析构函数内部捕获、处理了该异常,不能让异常被抛出析构函数之外。
后记
处理各种可能发生的异常,除了抛出异常,我们还可以用返回err_code的方法来处理。
选择返回error_code还是抛异常,可以参考boost asio的设计,一个函数提供两个接口,返回error_code的和抛异常的。返回error_code的适合即时解决问题,抛异常的适合自己不处理交由别人解决。逻辑复杂的可以用状态机管理,比如boost meta state machine。
使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升,移动平台对包体大小很敏感,所以移动平台慎用!!!