CPP_异常处理
1. 基础
处理错误有两种方式返回值和异常,C语言是典型的返回值凡是,C++包含两种。
返回值处理错误缺点:
- 程序员经常「忘记」处理错误返回值
- 每个可能产生错误的函数在调用后都需要判断是否有错误
- 与「真正的」返回值混用,需要规定一个错误代码(通常是0、-1或NULL)
异常处理错误的缺点:
- 使控制流变得复杂,难以追踪
- 开销相对较大,需要构建异常类
异常处理错误的优点:
- 错误信息丰富,便于获得错误现场
- 代码相对简短,不需要判断每个函数的返回值
C++异常特点:
- 异常的处理流程是完全独立的,throw 抛出异常后就可以不用管了,错误处理代码都集中在专门的 catch 块里。这样就彻底分离了业务逻辑与错误逻辑,看起来更清楚。
- 异常是绝对不能被忽略的,必须被处理。如果你有意或者无意不写 catch 捕获异常,那么它会一直向上传播出去,直至找到一个能够处理的 catch 块。如果实在没有,那就会导致程序立即停止运行,明白地提示你发生了错误,而不会“坚持带病工作”。
- 异常可以用在错误码无法使用的场合,这也算是 C++ 的“私人原因”。因为它比 C 语言多了构造 / 析构函数、操作符重载等新特性,有的函数根本就没有返回值,或者返回值无法表示错误,而全局的 errno 实在是“太不优雅”了,与 C++ 的理念不符,所以也必须使用异常来报告错误。
几个应当使用异常的判断准则:
- 不允许被忽略的错误;
- 极少数情况下才会发生的错误;
- 严重影响正常流程,很难恢复到正常状态的错误;
- 无法本地处理,必须“穿透”调用栈,传递到上层才能被处理的错误。
2. 异常使用
异常是程序在执行期间产生的问题。C++异常处理使用try...throw...catch...
异常通过类型来捕获
抛出异常
可以使用throw关键字在代码块中任何地方抛出异常。throw语句的操作数可以使任意的表达式,表达式的结果类型决定了抛出的异常的类型。
捕获异常
catch块跟在try块后面,用于捕获异常。通过在catch中指定异常类型,来捕获特定的异常。
try{
c[10] = 3;
cout <<"work done."<<endl;
throw 1;
}
catch(int exception){
if(exception == 1){
cerr<<"out of range."<<endl;
} else {
cerr<<"int other error."<<endl;
}
}catch(double){
cerr<<"other error."<<endl;
}
捕获所有异常(catch-all):为了一次性捕获所有异常,我们使用省略号作为异常声明,形如catch(...)。一条捕获所有异常的语句可以与任意类型的异常匹配。
异常捕获后,不会返回到异常发生的地方继续执行,而是执行捕获后的语句。
function-try
把整个函数体视为一个大 try 块,而 catch 块放在后面,与函数体同级并列。
void some_function()
try // 函数名之后直接写try块
{
...
}
catch(...) // catch块与函数体同级并列
{
...
}
异常对象
C++提供了一系列标准的异常,定义在
异常类提供了what()方法,返回异常产生的原因。
virtual const char* what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_NOTHROW;
异常对象(exception object)是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。
class my_exception : public std::runtime_error
{
public:
using this_type = my_exception; // 给自己起个别名
using super_type = std::runtime_error; // 给父类也起个别名
public:
my_exception(const char* msg): // 构造函数
super_type(msg) // 别名也可以用于构造
{}
my_exception() = default; // 默认构造函数
~my_exception() = default; // 默认析构函数
private:
int code = 0; // 其他的内部私有数据
};
注意 析构函数不应该跑出异常
析构函数总是会被执行,但是普通函数中负责释放资源的代码却可能被跳过,这一特点对于我们如何组织程序结构与重要影响。如果一个块分配了资源,并且在负责释放资源的代码前发生了异常,则释放资源的代码将不会执行。另一方面,类对象分配的资源将由类的析构函数负责释放。因此,如果我们使用类来控制资源的分配,就能确保无论函数正常结束还是遭遇异常,资源都能被正常释放。
出于栈展开可能使用析构函数的考虑,析构函数不应该抛出不能被它自身处理的异常。换句话说,如果析构函数需要执行某个可能爆出异常的操作,则该操作应该被放置在一个try语句块当中,并且在析构函数内部得到处理。
在实际的编程过程中,因为析构函数仅仅是释放资源,所以它不太可能抛出异常。所有标准库类型都能确保它们的析构函数不会引发异常。
注意 声明函数抛出异常
const char * what () const throw ()
{
//函数体
}
- const char * 表示返回值类型
- what 是函数名称
- () 是参数列表
- const 表示该成员函数不能修改成员变量
- throw() 是异常规格说明符。括号内写该函数可抛出的异常类型,这里面没有类型,就是声明这个函数不抛出异常,通常函数不写后面的就表示函数可以抛出任何类型的异常。
在 C++11 中,声明一个函数不可以抛出任何异常使用关键字 noexcept。
void mightThrow(); // could throw any exceptions.
void doesNotThrow() noexcept; // does not throw any exceptions.
下面两个函数声明的异常规格在语义上是相同的,都表示函数不抛出任何异常。
void old_stytle() throw();
void new_style() noexcept;
3. 示例
#include <iostream>
#include <string>
using namespace std;
void test()
{
string path = "/tmp/wang";
char *str = (char *)"STR";
if(!path.empty()){
// throw string("can't open " + path);
throw std::runtime_error(static_cast<string>(str) + " can't open");
}
cout <<"After exception" << endl;
}
int main()
{
try{
test();
} catch(const char* msg){
cerr <<"Char*: "<< msg << endl;
} catch(string &str){
cerr << "STR: "<<str <<endl;
} catch(exception& e){
cerr << "Exception: "<< e.what() << endl;
}
cout << "main over" <<endl;
return 0;
}
------------
Exception: STR can't open
main over