- 在程序运行时经常碰到一些错误,例如年龄、身高不能为负,除数为0等,这些错误放到程序中如果不加以管制,程序就会崩溃。C++提供了异常机制,让我们能跟捕获运行时错误,给程序一次机会,给用户一个反馈。
- C++异常处理机制可以让我们捕获并进行处理错误,然后我们可以通过捕获后的判断,重新给程序指一条明路,或者在程序结束之前,做一些必要的工作,例如将错误写到日志等。
说到C++,C语言的处理错误方式也简单提一下:
assert宏
- 生产环境可能被禁用(会带来性能开销,并且不能对用户提供任何实际好处)编译器会优化掉。
- 开发环境可以用来检查条件,但是不提供详细的错误信息,并且终止程序。
- 只能用来检查布尔表达式,无法用于更复杂的逻辑和错误处理。
返回错误码
- 程序员要自己查找对应的错误,系统很多库函数也都是通过错误码(errno)表示错误
程序的错误大致可以分为三种,分别时语法错误、逻辑错误和运行时错误。
- 语法错误:在编译和链接阶段就能发现,必须全符合语法规则才能生成可执行代码。这种错误最简单。
- 逻辑错误:编码思路问题,执行的结果不是预定的,也可以通过调试解决。
- 运行时错误:程序运行期间发生的错误,如同除0,越界等。C++异常(Exception)机制就是为了解决这种错误。
异常:异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛异常,让函数的直接或间接调用者处理这个错误。
C++异常处理涉及三个关键字
- throw:出现问题,程序使用throw抛出异常
- catch:有抛出也得有捕获,catch关键字就是用来捕获异常。捕获的地方就是处理问题的地方。
- try:try块中的代码为保护代码,如果出错会激活特定的异常,后面跟着一个或多个catch块。
eg:语法
try{
//保护区,就是可能会出错的代码
}
catch(ExceptionName e1){
//出错后,通过异常处理程序捕获异常,在这里处理问题或者 再抛出
}
catch(ExceptionName e2){
//因为异常对象类型的多样,所以可以多个catch块捕获匹配的异常
}
catch(...){
//如果try块中出现异常,而前面的catch块也没有捕获,这是最后一道防线,所有类型都会在这里捕获,但不知道异常的错误原因。
}
注意: 可能有些不好理解,后面内容我会逐步介绍
简单的例子:
double Division(int a, int b)
{
if (b == 0)
throw "Division by zero condition!";
else
return (double)a / (double)b;
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "unknow exception" << endl;
}
return 0;
}
上例的函数调用链中栈的细节:
- 检查被throw是否在try块中,如果在并触发异常。
- 被throw抛出对象的类型和最近的catch块的参数进行匹配,匹配成功,则调到catch块中处理。没有匹配成功,则退出当前函数栈,在调用该函数的栈中继续匹配。
- 如果到main函数的栈依旧没有匹配,则终止程序(非正常结束)。但一般有catch(…)兜底。
- 如果匹配到catch子句处理完块中内容后,会沿着try,catch语句后继续执行
匹配原则
- throw抛出的对象类型与调用链中位置最近并且参数类型一样的catch匹配。
- throw抛出异常会生成一个异常对象的拷贝(因为抛出的对象可能是一个临时对象),这个拷贝的对象会被catch以后销毁。(类似函数传值返回)
- catch(…)可以捕获任意类型异常,但是不知道异常错误是什么。
- 匹配原则的例外:可以抛出派生类对象,使用基类捕获。(后面讲)
一个单独的catch语句不能完整的处理某个异常,再执行一些校正操作之后,可能会抛给调用链更上一层的函数接着处理异常。
这个简单的校正工作可能就是改变参数内容,但是这里又要注意是不是引用
eg:
//简单的校正操作
catch (my_error& eObj){ //引用类型
eObj.status = errCodes::severeErr; //修改了异常对象
throw;
}
catch (other_error eObj) { //非引用类型
eObj.status = errCodes::severeErr; //只修改了异常对象的局部副本
throw;
}
异常再抛出:
try
{
//保护代码
//...
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
throw;//异常再抛出,不含任何表达式
//注意异常进行抛出后,是直接跳过这个函数栈的后面部分,例如下面的打印语句就 不会在进行执行
cout << "unknow Exception" << endl;
}
目的是为了让使用者知道该函数抛出的异常类型。
异常说明:
//这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void func() 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 ();
//若无异常接口声明:表示这个函数可以抛出任何类型的异常
void func1()
C++11新增的noexcept,表示不会抛出异常
void recoup(int) noexcept; //不会抛出异常
void alloc(int) //可能抛出异常
注意: noexcept的位置也很特殊,因为const,final,override或者虚函数=0,都可以跟在函数后面,所以noexcept要在const及引用限定符之后,在final,override或者虚函数=0之前。
建议
- 不要在构造函数中抛出异常(可能导致对象未完成初始化)
- 不要在析构函数中抛出异常(可能导致资源泄漏)
- 不要在lock和unlock之间抛异常(死锁)(后面的博客讲)
实际很多公司会自己定义异常体系,规范管理。大家抛出的都是继承的派生类对象,捕获一个基类即可。
eg:服务器开发中使用的异常继承体系(例子)
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg;
int _id;
};
class SqlException : public Exception
{
public:
SqlException(const string& errmsg, int id, const string& sql)
:Exception(errmsg, id)
, _sql(sql)
{}
virtual string what() const
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _sql;
};
class CacheException : public Exception
{
public:
CacheException(const string& errmsg, int id)
:Exception(errmsg, id)
{}
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
class HttpServerException : public Exception
{
public:
SqlException(const string& errmsg, int id, const string& type)
:Exception(errmsg, id)
, _type(type)
{}
virtual string what() const
{
string str = "HttpServerException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type;
};
调用捕获:
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
//throw "xxxxxx";
}
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
void HttpServer()
{
// ...
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
CacheMgr();
}
int main()
{
while (true)
{
Sleep(500); //引用系统头文件
try {
HttpServer();
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
// 多态
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
注意: C++标准库的异常体系设计的不好用,所以一般都是定义自己的异常体系
异常总体而言利大于弊
异常的优缺点
优点:
- 异常对象定义好了,可以更加清晰准确的展示出错误的各种信息,更好的定位bug。
- 不需要层层返回检查并错误码,异常体系直接跳到catch捕获的位置
- 很多第三方库都包含异常,想要使用这些库也需要使用异常
- 部分函数使用异常更好处理,例如没有返回值的或者不便于返回的。
缺点:
- 执行流乱跳,导致跟踪调试时以及分析程序困难,很混乱
- 性能的开销
- C++标准库的异常体系定义的不好,大家各自定义异常体系,不统一