Introduction
Exceptions allow a function to throw an exception when it finds an error that it cannot handle, hoping that its caller can handle the problem directly or indirectly. With traditional error handling technology, when a problem that cannot be handled locally is detected:
1. Terminate the program (such as atol, atoi, enter NULL, a segmentation fault will occur, causing the program to exit abnormally, if there is no core file, the person looking for the problem Will definitely go crazy)
2. Return a value indicating an error (many system functions are like this, such as malloc, insufficient memory, allocation failure, return NULL pointer)
3. Return a legal value, leaving the program in some illegal state status (the most annoying thing, some third-party libraries are really like this)
4. Call a function that is prepared in advance for use in the event of an "error".
The first case is not allowed. The library for unconditionally terminating programs cannot be applied to programs that cannot crash. The second case is more commonly used, but sometimes inappropriate. For example, the error code returned is int, and the error value must be checked for each call. It is extremely inconvenient and can easily double the size of the program (but the logic must be accurately controlled. I think this Not a bad way). In the third case, it is easy to mislead the caller. If the caller does not check the global variable errno or check for errors in other ways, it will be a disaster, and this method does not work well in concurrent situations. As for the fourth situation, I think it is less used, and the callback code should not appear too often.
Using exceptions separates errors and processing. The library function throws an exception, and the caller catches the exception. The caller can know that an error occurred when calling the program function library, and handle it, and it is up to him whether to terminate the program. It's in the hands of the caller.
However, error handling is still a very difficult thing. The C++ exception mechanism provides programmers with a way to handle errors, allowing programmers to handle errors in a more natural way.
A practical introduction to exceptions
Suppose we write a program to convert two strings input by the user into integers and add the output. Generally we will write like this
char *str1 = "1", *str2 = "2"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
Suppose the user input is str1, str2, if str1 and str2 They are all integer type strings. This code can work normally, but the user's input may be incorrect and illegal characters may be entered. For example,
char *str1 = "1", *str2 = "a"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
The result is 1 at this time because atoi(str2) returns 0.
If the user input is like this:
char *str1 = "1", *str2 = NULL; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
then a segfault will occur in this code and the program will exit abnormally.
atoi I think is a relatively dangerous function. If in an important system, the caller does not know and passes in a NULL character, the program will exit abnormally, causing service interruption, or if an illegal character is passed in, the result will be 0. , the code continues to go on, it is really not easy to locate this problem in a complex system.
So the more appropriate way is for us to use exception handling to transform a safe atoi method called parseNumber.
class NumberParseException {}; bool isNumber(char * str) { using namespace std; if (str == NULL) return false; int len = strlen(str); if (len == 0) return false; bool isaNumber = false; char ch; for (int i = 0; i < len; i++) { if (i == 0 && (str[i] == '-' || str[i] == '+')) continue; if (isdigit(str[i])) { isaNumber = true; } else { isaNumber = false; break; } } return isaNumber; } int parseNumber(char * str) throw(NumberParseException) { if (!isNumber(str)) throw NumberParseException(); return atoi(str); }
NumberParseException in the above code is a custom exception class. When we detect that the incoming str is not a number, we will throw a number conversion exception and let the caller handle the error. This is better than passing in a NULL string. It is much better to cause a segmentation fault to end the program. The caller can catch this exception and decide whether to end the program, which is better than passing in a non-integer string and returning 0. The program will continue to execute silently when an error occurs.
So the code we wrote before can be transformed as follows:
char *str1 = "1", *str2 = NULL; try { int num1 = parseNumber(str1); int num2 = parseNumber(str2); printf("sum is %d\n", num1 + num2); } catch (NumberParseException) { printf("输入不是整数\n"); }
The result of this code is to print out "The input is not an integer". Assume that this code is running in a game statistics system, and the system needs to regularly collect statistics from a large number of files. The number of times a large number of users have entered game channel 1 and game channel 2. str1 represents the number of times they have entered game channel 1, and str2 represents the number of times they have entered channel 2. If exceptions are not used, when the input is NULL, the program will cause the entire system to crash. When the input is Illegal integers, all calculation results are wrong, and the program still "executes correctly" silently.
If the input is illegal, a NumberParseException will be thrown. Even if the caller does not consider that the input is illegal, for example:
char *str1 = "1", *str2 = "12,"; int num1 = parseNumber(str1); int num2 = parseNumber(str2); printf("sum is %d\n", num1 + num2);
Even if the caller is careless and does not catch the exception, a NumberParseException will be thrown while the program is running. If the program crashes, it will leave coredump file, the caller passes "gdb "Program name coredump file". If you check the stack when the program crashes, you will know that an illegal integer character appears while the program is running. Then he will quickly know the problem and will learn to change the above code to
char *str1 = "1", *str2 = NULL; try { int num1 = parseNumber(str1); int num2 = parseNumber(str2); printf("sum is %d\n", num1 + num2); } catch (NumberParseException) { printf("输入不是整数\n"); //打印文件的路径,行号,str1,str2等信息足够自己去定位问题所在 }
like this. When a problem occurs in the program, the caller can locate the problem. This is the error handling method of exception, which separates error discovery (parseNumber) and error handling (game statistics code)
Exception throwing is introduced here. and capture, as well as exception usage scenarios. Next, we will start to explain C++ exceptions step by step.
Description of exceptions
Function and the set of exceptions that may be thrown by the function are valuable as part of the function declaration, such as
void f(int a) throw (x2,x3);
表示f()只能抛出两个异常x2,x3,以及这些类型派生的异常,但不会抛出其他异常。如果f函数违反了这个规定,抛出了x2,x3之外的异常,例如x4,那么当函数f抛出x4异常时,
会转换为一个std::unexpected()调用,默认是调用std::terminate(),通常是调用abort()。
如果函数不带异常描述,那么假定他可能抛出任何异常。例如:
int f(); //可能抛出任何异常
带任何异常的函数可以用空表表示:
int g() throw (); // 不会抛出任何异常
捕获异常
捕获异常的代码一般如下:
try { throw E(); } catch (H h) { //何时我们可以能到这里呢 }
1.如果H和E是相同的类型
2.如果H是E的基类
3.如果H和E都是指针类型,而且1或者2对它们所引用的类型成立
4.如果H和E都是引用类型,而且1或者2对H所引用的类型成立
从原则上来说,异常在抛出时被复制,我们最后捕获的异常只是原始异常的一个副本,所以我们不应该抛出一个不允许抛出一个不允许复制的异常。
此外,我们可以在用于捕获异常的类型加上const,就像我们可以给函数加上const一样,限制我们,不能去修改捕捉到的那个异常。
还有,捕获异常时如果H和E不是引用类型或者指针类型,而且H是E的基类,那么h对象其实就是H h = E(),最后捕获的异常对象h会丢失E的附加携带信息。
异常处理的顺序
我们之前写的parseNumber函数会抛出NumberParseException,这个函数只是判断是否数字才抛出异常,但是没有考虑,但这个字符串表示的整数太大,溢出,抛出异常Overflow.表示如下:
class NumberParseException {}; class Overflow : public NumberParseException {};
假设我们parseNumber函数已经为字符串的整数溢出做了检测,遇到这种情况,会抛出Overflow异常,那么异常捕获代码如下:
char *str1 = "1", *str2 = NULL; try { int num1 = parseNumber(str1); int num2 = parseNumber(str2); printf("sum is %d\n", num1 + num2); } catch (Overflow) { //处理Overflow或者任何由Overflow派生的异常 } catch (NumberParseException) { //处理不是Overflow的NumberParseException异常 }
异常组织这种层次结构对于代码的健壮性很重要,因为库函数发布之后,不可能不加入新的异常,就像我们的parseNumber,第一次发布时只是考虑输入是否一个整数的错误,第二次发布时就考虑了判断输入的一个字符串作为整数是否太大溢出,对于一个函数发布之后不再添加新的异常,几乎所有的库函数都不能接受。
如果没有异常的层次结构,当函数升级加入新的异常描述时,我们可能都要修改代码,为每一处调用这个函数的地方加入对应的catch新的异常语句,这很让你厌烦,程序员也很容易忘记把某个异常加入列表,导致这个异常没有捕获,异常退出。
而有了异常的层次结构,函数升级之后,例如我们的parseNumber加入了Overflow异常描述,函数调用者只需要在自己感兴趣的调用场景加入catch(Overflow),并做处理就行了,如果根据不关心Overflow错误,甚至不用修改代码。
未捕获的异常
如果抛出的异常未被捕捉,那么就会调用函数std::terminate(),默认情况是调用abort,这对于大部分用户是正确选择,特别是排错程序错误的阶段(调用abort会产生coredump文件,coredump文件的使用可以参考博客的"学会用core dump调试程序错误")。
如果我们希望在发生未捕获异常时,保证清理工作,可以在所有真正需要关注的异常处理之外,再在main添加一个捕捉一切的异常处理,例如:
int main() { try { //... } catch (std::range_error) { cerr << "range error\n"; } catch (std::bad_alloc) { cerr << "new run out of memory\n"; } catch (...) { //.. } }
这样就可以捕捉所有的异常,除了那些在全局变量构造和析构的异常(如果要获得控制,唯一方式是set_unexpected)。
其中catch(...)表示捕捉所有异常,一般会在处理代码做一些清理工作。
重新抛出
当我们捕获了一个异常,却发现无法处理,这种情况下,我们会做完局部能够做的事情,然后再一次抛出这个异常,让这个异常在最合适的地方地方处理。例如:
void downloadFileFromServer() { try { connect_to_server(); //... } catch (NetworkException) { if (can_handle_it_completely) { //处理网络异常,例如重连 } else { throw; } } }
这个函数是从远程服务器下载文件,内部调用连接到远程服务器的函数,但是可能存在着网络异常,如果多次重连无法成功,就把这个网络异常抛出,让上层处理。
重新抛出是采用不带运算对象的throw表示,但是如果重新抛出,又没有异常可以重新抛出,就会调用terminate();
假设NetworkException有两个派生异常叫FtpConnectException和HttpConnectException,调用connect_to_server时是抛出HttpConnectException,那么调用downloadFileFromServer仍然能捕捉到异常HttpConnectException。
标准异常
到了这里,你已经基本会使用异常了,可是如果你是函数开发者,并需要把函数给别人使用,在使用异常时,会涉及到自定义异常类,但是C++标准已经定义了一部分标准异常,请尽可能复用这些异常,标准异常参考http://www.cplusplus.com/reference/std/stdexcept/
Although there are relatively few C++ standard exceptions, as a function developer, you should reuse C++ standard exceptions as much as possible. As a function caller, you can spend less time understanding your custom exception classes and better call the ones you developed. function.
Summary
This article only briefly introduces the basic usage of exceptions from the usage scenarios of exceptions. Some advanced exception usages are not listed. For detailed information, please refer to the exception handling of the C++ programming language by the father of C++.