一 C处理错误的方式和缺陷
1.1 返回错误码
缺陷:
(1).错误码不好设置,比如:除0操作,就不好返回错误码。如果返回一个数字,可能会有两层含义,错误码或者处理结果。一般我们会定义函数返回值的类型,在规定成功和失败等不同情况返回的数字。
(2).需要程序员去查找错误码对应的含义。比如:很多系统接口函数都是把错误码放到全局变量errno中。
1.2 终止程序。比如:发生越界,除0,内存错误等,会直接终止程序。
缺陷:
(1).并不能明确知道是什么类型的错误,用户难以接收。
C++针对上面的不足,引入了异常的概念,不会终止程序,并且会将错误信息详细介绍。
示例代码:
int test()
{
int a, b;
cout << "Please inpt two numbers:" << endl;
cin >> a >> b;
if (b == 0)
throw bad_exception();
return a / b;
}
int main(int argc, char *argv[])
{
try {
test();
} catch (exception &e) {
cout << e.what() << endl;
}
return 0;
}
执行结果如下:
二 C++异常概念
异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。
当程序的某部分检测到一个它无法处理的问题,需要用到异常处理。此时,检测出问题的部分应该发出某种信号以表明程序遇到了故障,无法继续下去了,而且信号的发出方无须知道故障在何处得到解决。一旦发出异常信号,检测出问题的部分也就完成了任务。
如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。例如,如果程序的问题是输入无效,则异常处理部分可能会要求用户重新输入正确的数据;如果丢失了数据库连接,会发出警报信息。
异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:
(1)、throw表达式(throw expression),异常检测部分使用throw表达式来表示它遇到了无法处理的问题。我们说throw引发(raise)了异常。
(2)、try语句块(try block),异常处理部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句(catch clause)结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句"处理"异常,所以它们也被称为异常处理代码(exception handler)。
(3)、一套异常类(exception class),用于在throw表达式和相关的catch子句之间传递异常的具体信息。
2.1 throw语句
在C++中,我们通过抛出(throwing)一条表达式来引发(raised)一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码(handler)将被用来处理该异常。被选中的处理代码是在调用链中与抛出对象类型匹配的最近的处理代码。其中,根据抛出异常对象的类型和内容,程序的异常抛出部分将会告知异常处理部分到底发生了什么错误。
当执行一个throw时,跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到与之匹配的catch模块。该catch可能是同一个函数中的局部catch,也可能是位于直接或间接调用了发生异常的另一个函数中。控制权从一处转移到另一处,这有两个重要的含义:
(1)、沿着调用链的函数可能会提早退出。
(2)、一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
因为跟在throw后面的语句将不会再被执行,所以throw语句的用法有点类似于return语句:它通常作为条件语句的一部分或者作为某个函数的最后(或者唯一)一条语句。
示例代码:
int test()
{
int a, b;
if (b == 0)
throw "被除数不能为0";
return a / b;
}
2.2 try-catch语句块
try语句块的通用语法形式是:
try {
program-statements
} catch (exception-declaration) {
handler-statements
} catch (exception-declaration) {
handler-statements
} // ...
try语句块的一开始是关键字try,随后紧跟着一个块,这个块就像大多数时候那样是花括号括起来的语句序列。
紧跟着try块之后的是一个或多个catch子句。catch子句包括三部分:关键字catch、括号内一个(可能未命名的)对象的申明(称作异常申明,exception declaration)以及一个块。
当选中了某个catch子句处理异常之后,执行与之对应的块。catch一旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。
try语句块中的program-statements组成程序的正常逻辑,像其他任何块一样,program-statements可以有包括申明在内的任意C++语句。一如往常,try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问。
catch的说明:
- 当进入一个catch语句后,通过异常对象初始化异常声明中的参数。和函数的参数类似,如果catch的参数类型是非引用类型,则该参数是异常对象的一个副本,在catch语句内改变该参数实际上改变的是局部副本而非异常对象本身;相反,如果参数是引用类型,则和其他引用参数一样,该参数是异常对象的一个别名,此时改变参数也就是改变异常对象。
- catch的参数还有一个特性也与函数的参数非常类似;如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。此时,如果catch的参数是非引用类型,则异常对象将被切掉一部分,这与将派生类对象以值传递的方式传给一个普通函数差不多。另一方面,如果catch的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。
- 异常声明的静态类型将决定catch语句所能执行的操作,如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。
建议:
通常情况下,如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型。
2.3 异常的抛出和匹配规则
2.3.1 异常抛出说明
(1)、异常通过抛出对象而引发,对象的类型决定了应该激活哪个catch的处理代码。
(2)、被选中的处理代码的调用链是找到与该类型匹配且距离抛出异常位置最近的catch。
(3)、为了一次性捕获所有异常,我们使用省略号作为异常申明,这样的处理代码称为捕获所有异常(catch-all)的处理代码,形如catch(...)。一条捕获所有异常的语句可以与任意类型的异常匹配。catch(...)既能单独出现,也能与其他几个catch语句一起出现。如果catch(...)与其他几个catch语句一起出现,则catch(...)必须在最后的位置。出现在捕获所有异常语句后面的catch语句将永远不会被匹配。
2.3.2 异常匹配规则
(1)、首先检查throw是否在try语句块内部。如果是,在当前的函数栈中查找匹配的catch语句,如果查找到匹配的catch,则直接跳到匹配的catch语句块内部中执行。
(2)、若没有匹配的catch,则退出当前函数栈,在上一层函数中查找匹配的catch。
(3)、若到达main函数后仍没有查找到匹配的catch子句,程序将退出。因为异常通常被认为是妨碍程序正常执行的事件,所以一旦引发了某个异常,就不能对它置之不理。当找不到匹配的catch时,程序将调用标准库函数terminate,顾名思义,terminite负责终止程序的执行过程。
一个异常如果没有被捕获,则它将终止当前的程序。
(4)、找到匹配的catch后,程序流程会跳转到匹配的catch语句中,执行完成后,程序流程会沿着try-catch后面的语句中执行。
示例代码一:
int f3()
{
int a, b;
cout << "Please input two numbers:";
cin >> a >> b;
if (b == 0)
throw "被除数不能为0";
return a / b;
}
int f2()
{
int result = f3();
cout << "f2" << endl;
return result;
}
int f1()
{
int result = f2();
cout << "f1" << endl;
return result;
}
int main(int argc, char *argv[])
{
try {
// 可能出现异常的代码
cout << f1() << endl;
} catch (const char *err) { // 捕获异常
cout << err << endl;
} catch (...) {
cout << "unknown exception" << endl;
}
cout << "main" << endl;
return 0;
}
说明:
(1)、首先判断f3()中的throw是否在try-catch语句块中,若在,首先会在f3()中查找是否有匹配的catch
(2)、没有,在调用f3()的函数中,即f2()中查找是否有匹配的catch
(3)、没有,在调用f2()的函数中,即f1()中查找是否有匹配的catch
(4)、没有,在调用f1()的函数中,即main中查找是否有匹配的catch
(5)、如果还没有找到匹配的catch,将会调用标准库函数terminate终止程序;如果有直接跳到匹配的catch语句中执行
(6)、执行完catch后,程序流程会继续执行catch后面的语句,不会回到原来的地方执行。
执行结果如下:
示例代码二(中间匹配):
int f3()
{
int a, b;
cout << "Please input two numbers:";
cin >> a >> b;
if (b == 0)
throw "被除数不能为0";
return a / b;
}
int f2()
{
int result = f3();
cout << "f2" << endl;
return result;
}
int f1()
{
int result = 0;
try {
result = f2();
} catch (const char *err) {
cout << err << endl;
}
cout << "f1" << endl;
return result;
}
int main(int argc, char *argv[])
{
try {
// 可能出现异常的代码
cout << f1() << endl;
} catch (const char *err) { // 捕获异常
cout << err << endl;
} catch (...) {
cout << "unknown exception" << endl;
}
cout << "main" << endl;
return 0;
}
执行结果如下:
2.4 异常的重新抛出
有时,一个单独的catch语句不能完整地处理某个异常。在执行了某些矫正操作之后,当前的catch可能会决定有调用链更上一层的函数接着处理异常。一条catch语句通过重新抛出(rethrowing)的操作将异常传递给另一个catch语句。这里的重新抛出任然是一条throw语句,只不过不包含任何表达式:
throw;
空的throw语句只能出现在catch语句或catch语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空的throw语句,编译器将调用terminate。
一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。
很多时候,catch语句会改变其参数的内容。如果在改变参数的内容后catch语句重新抛出异常,则只有当catch异常申明是引用类型时我们对参数所作的改变才会被保留并继续传播:
catch (my_error &eObj) { // 引用类型
eObj.status = errCode::severrErr; // 修改了异常对象
throw; // 异常对象的status成员是severeErr
} catch (other_error eObj) { // 非引用类型
eObj.status = errCodes::badErr; // 只修改了异常对象的局部副本
throw; // 异常对象的status成员没有改变
}
示例代码:
int f2()
{
int a, b;
cout << "Please input two numbers:";
cin >> a >> b;
if (b == 0)
throw "被除数不能为0";
return a / b;
}
int f1()
{
int res = 0;
int *p = new int[10];
try {
res = f2();
} catch (const char *err) {
cout << err << endl;
// 需要将开辟的空间释放
delete[] p;
// 重新抛出异常
throw;
}
// 如果这里没有异常,不会执行catch的内容
// 这里还需要释放
delete[] p;
cout << "f1" << endl;
return res;
}
int main(int argc, char *argv[])
{
try {
cout << f1() << endl; // 可能会出现异常的代码
} catch (const char *err) { // 捕获异常
cout << err << endl;
} catch (...) {
cout << "unknown exception" << endl;
}
cout << "main" << endl;
return 0;
}
执行结果如下:
具体的代码流程如下所示:
2.5 自定义异常体系
在实际中,并不是我们想抛出什么异常就抛出什么异常,这样会导致异常不好捕捉,而一般会建立一个继承体系。
首先建立一个异常基类,派生类继承这个基类,定义出不同情况的异常。抛出异常后,只需要使用基类进行捕获即可。
基类相当于一个框架,派生类是具体的异常。然后抛异常只需抛派生类,捕获异常只需要捕获基类即可。
示例代码:
typedef enum {
NO_ERROR = -1,
SQL_ERROR = 0,
NET_ERROR = 1,
CACHE_ERROR = 2,
} error_e;
// 基类
// 异常
class MyException {
public:
MyException(const char *str = nullptr, int id = 0) :
_errmsg(str), _id(id)
{}
virtual void what() const = 0;
protected:
string _errmsg; // 错误信息
int _id; // 错误码
};
// 派生类
// 数据库异常
class SqlException : public MyException {
public:
SqlException(const char *str = nullptr, int id = 1) :
MyException(str, id)
{}
virtual void what() const {
cout << "Err_msg: " << _errmsg << endl;
cout << "Err_id: " << _id << endl;
}
};
// 网络异常
class NetException : public MyException {
public:
NetException(const char *str = nullptr, int id = 2) :
MyException(str, id)
{}
virtual void what() const {
cout << "Err_msg: " << _errmsg << endl;
cout << "Err_id: " << _id << endl;
}
};
// 缓存异常
class CacheException : public MyException {
public:
CacheException(const char *str = nullptr, int id = 3) :
MyException(str, id)
{}
virtual void what() const {
cout << "Err_msg: " << _errmsg << endl;
cout << "Err_id: " << _id << endl;
}
};
void exception_throw_func(int err) {
switch (err) {
case SQL_ERROR:
throw SqlException("Sql error", 1);
break;
case NET_ERROR:
throw NetException("Net error", 2);
case CACHE_ERROR:
throw CacheException("Cache error", 3);
default :
cout << "err_id out of range" << endl;
}
}
int main(int argc, char *argv[]) {
int err;
cout << "selete a exception to throw:";
cin >> err;
try {
exception_throw_func(err);
} catch (MyException &e) {
e.what();
} catch (...) {
cout << "unknown exception" << endl;
}
return 0;
}
2.6 C++标准库异常类
2.6.1 标准库异常类继承体系
说明:
2.6.2 标准库异常类声明
exception
// class声明路径: /usr/include/c++/4.8/exception
/**
* @brief Base class for all library exceptions.
*
* This is the base class for all exceptions thrown by the standard
* library, and by certain language expressions. You are free to derive
* your own %exception classes, or use a different hierarchy, or to
* throw non-class data (e.g., fundamental types).
*/
class exception {
public:
exception() _GLIBCXX_USE_NOEXCEPT { }
virtual ~exception() _GLIBCXX_USE_NOEXCEPT;
/** Returns a C-style character string describing the general cause
* of the current error. */
virtual const char* what() const _GLIBCXX_USE_NOEXCEPT;
};
bad_exception (exception -> bad_exception)
// class声明路径:/usr/include/c++/4.8/exception
/** If an %exception is thrown which is not listed in a function's
* %exception specification, one of these may be thrown.
*/
class bad_exception : public exception {
public:
bad_exception() _GLIBCXX_USE_NOEXCEPT { }
// This declaration is not useless:
// http://gcc.gnu.org/onlinedocs/gcc-3.0.2/gcc_6.html#SEC118
virtual ~bad_exception() _GLIBCXX_USE_NOEXCEPT;
// See comment in eh_exception.cc.
virtual const char* what() const _GLIBCXX_USE_NOEXCEPT;
};
bad_alloc (exception -> bad_alloc)
// class声明路径:/usr/include/c++/4.8/new
/**
* @brief Exception possibly thrown by @c new.
* @ingroup exceptions
*
* @c bad_alloc (or classes derived from it) is used to report allocation
* errors from the throwing forms of @c new.
*/
class bad_alloc : public exception {
public:
bad_alloc() throw() { }
// This declaration is not useless:
// http://gcc.gnu.org/onlinedocs/gcc-3.0.2/gcc_6.html#SEC118
virtual ~bad_alloc() throw();
// See comment in eh_exception.cc.
virtual const char* what() const throw();
};
bad_cast (exception -> bad_cast)
// class声明路径:/usr/include/c++/4.8/typeinfo
/**
* @brief Thrown during incorrect typecasting.
* @ingroup exceptions
*
* If you attempt an invalid @c dynamic_cast expression, an instance of
* this class (or something derived from this class) is thrown.
*/
class bad_cast : public exception {
public:
bad_cast() _GLIBCXX_USE_NOEXCEPT { }
// This declaration is not useless:
// http://gcc.gnu.org/onlinedocs/gcc-3.0.2/gcc_6.html#SEC118
virtual ~bad_cast() _GLIBCXX_USE_NOEXCEPT;
// See comment in eh_exception.cc.
virtual const char* what() const _GLIBCXX_USE_NOEXCEPT;
};
bad_typeid (exception -> bad_typeid)
// class声明路径:/usr/include/c++/4.8/typeinfo
/**
* @brief Thrown when a NULL pointer in a @c typeid expression is used.
* @ingroup exceptions
*/
class bad_typeid : public exception {
public:
bad_typeid () _GLIBCXX_USE_NOEXCEPT { }
// This declaration is not useless:
// http://gcc.gnu.org/onlinedocs/gcc-3.0.2/gcc_6.html#SEC118
virtual ~bad_typeid() _GLIBCXX_USE_NOEXCEPT;
// See comment in eh_exception.cc.
virtual const char* what() const _GLIBCXX_USE_NOEXCEPT;
};
logic_error (exception -> logic_error)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Logic errors represent problems in the internal logic of a program;
* in theory, these are preventable, and even detectable before the
* program runs (e.g., violations of class invariants).
* @brief One of two subclasses of exception.
*/
class logic_error : public exception {
string _M_msg;
public:
/** Takes a character string describing the error. */
explicit
logic_error(const string& __arg);
virtual ~logic_error() _GLIBCXX_USE_NOEXCEPT;
/** Returns a C-style character string describing the general cause of
* the current error (the same string passed to the ctor). */
virtual const char*
what() const _GLIBCXX_USE_NOEXCEPT;
};
domain_error (exception -> logic_error -> domain_error)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Thrown by the library, or by you, to report domain errors (domain in
* the mathematical sense). */
class domain_error : public logic_error {
public:
explicit domain_error(const string& __arg);
virtual ~domain_error() _GLIBCXX_USE_NOEXCEPT;
};
invalid_argument (exception -> logic_error -> invalid_argument)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Thrown to report invalid arguments to functions. */
class invalid_argument : public logic_error {
public:
explicit invalid_argument(const string& __arg);
virtual ~invalid_argument() _GLIBCXX_USE_NOEXCEPT;
};
length_error (exception -> logic_error -> length_error)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Thrown when an object is constructed that would exceed its maximum
* permitted size (e.g., a basic_string instance).
*/
class length_error : public logic_error {
public:
explicit length_error(const string& __arg);
virtual ~length_error() _GLIBCXX_USE_NOEXCEPT;
};
out_of_range (exception -> logic_error -> out_of_range)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** This represents an argument whose value is not within the expected
* range (e.g., boundary checks in basic_string).
*/
class out_of_range : public logic_error {
public:
explicit out_of_range(const string& __arg);
virtual ~out_of_range() _GLIBCXX_USE_NOEXCEPT;
};
runtime_error (exception -> runtime_error)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Runtime errors represent problems outside the scope of a program;
* they cannot be easily predicted and can generally only be caught as
* the program executes.
* @brief One of two subclasses of exception.
*/
class runtime_error : public exception {
string _M_msg;
public:
/** Takes a character string describing the error. */
explicit
runtime_error(const string& __arg);
virtual ~runtime_error() _GLIBCXX_USE_NOEXCEPT;
/** Returns a C-style character string describing the general cause of
* the current error (the same string passed to the ctor). */
virtual const char*
what() const _GLIBCXX_USE_NOEXCEPT;
};
range_error (exception -> runtime_error -> range_error)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Thrown to indicate range errors in internal computations. */
class range_error : public runtime_error {
public:
explicit range_error(const string& __arg);
virtual ~range_error() _GLIBCXX_USE_NOEXCEPT;
};
overflow_error (exception -> runtime_error -> overflow_error)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Thrown to indicate arithmetic overflow. */
class overflow_error : public runtime_error {
public:
explicit overflow_error(const string& __arg);
virtual ~overflow_error() _GLIBCXX_USE_NOEXCEPT;
};
underflow_error (exception -> runtime_error -> underflow_error)
// class声明路径:/usr/include/c++/4.8/stdexcept
/** Thrown to indicate arithmetic underflow. */
class underflow_error : public runtime_error {
public:
explicit underflow_error(const string& __arg);
virtual ~underflow_error() _GLIBCXX_USE_NOEXCEPT;
};
2.6.3 标准库异常类使用
示例程序:
#include <iostream>
#include <exception>
#include <stdexcept>
using std::cin;
using std::cout;
using std::endl;
using std::exception;
using std::overflow_error;
unsigned short add() {
unsigned short a, b;
cout << "Please input two numbers: ";
cin >> a >> b;
if (a + b > 65535)
// c++标准库中的异常类
throw overflow_error("overflow");
return a + b;
}
int main(int argc, char *argv[]) {
try {
add();
} catch (exception &e) {
cout << e.what() << endl;
} catch (...) {
cout << "unknown exception" << endl;
}
return 0;
}
执行结果如下: