소개
예외를 사용하면 함수가 처리할 수 없는 오류를 발견했을 때 호출자가 문제를 직접 또는 간접적으로 처리할 수 있기를 바라면서 예외를 발생시킬 수 있습니다. 기존 오류 처리 기술이 로컬에서 처리할 수 없는 문제를 감지한 경우:
1. 프로그램을 종료합니다(예: atol, atoi, NULL 입력). 분할 오류가 발생하여 프로그램이 비정상적으로 종료됩니다. 코어 파일이 아니니 문제 찾는 사람은 분명 미쳐버릴 겁니다
2. 오류를 나타내는 값을 반환합니다(malloc, 메모리 부족, 할당 실패, NULL 포인터 반환 등 많은 시스템 기능이 이와 같습니다).
3 .합법적인 값을 반환하여 프로그램을 불법적인 상태로 만듭니다. (가장 짜증나는 점은 일부 타사 라이브러리가 실제로 이 작업을 수행한다는 것입니다.)
4. "오류"가 발생할 때 사용됩니다.
첫 번째 경우는 무조건 프로그램 종료를 위한 라이브러리를 사용할 수 없습니다. 두 번째 경우가 더 일반적으로 사용되지만 때로는 부적절합니다. 예를 들어 반환된 오류 코드는 int이며 각 호출마다 오류 값을 확인해야 합니다. 이는 매우 불편하고 프로그램 크기를 쉽게 두 배로 늘릴 수 있습니다. 정확하게 제어해야 합니다. 나쁜 방법은 아니라고 생각합니다. 세 번째 경우 호출자가 전역 변수 errno를 확인하지 않거나 다른 방법으로 오류를 확인하지 않으면 호출자를 오도하기 쉽습니다. 이는 재앙이 되며 이 방법은 동시 상황에서는 제대로 작동하지 않습니다. 네 번째 상황은 활용도가 낮고, 콜백 코드도 너무 자주 등장하면 안 될 것 같아요.
예외를 사용하면 오류와 처리가 분리되며, 호출자는 예외를 catch하고 프로그램 함수 라이브러리 호출에서 오류가 발생했는지 여부를 알 수 있습니다. 호출자의 손에 있습니다.
그러나 오류 처리는 여전히 매우 어려운 작업입니다. C++ 예외 메커니즘은 프로그래머에게 오류를 처리할 수 있는 방법을 제공하여 프로그래머가 보다 자연스러운 방식으로 오류를 처리할 수 있도록 해줍니다.
실질적인 이상 현상 소개
사용자가 입력한 두 문자열을 정수로 변환하고 더해 출력하는 프로그램을 작성한다고 가정해 보겠습니다.
char *str1 = "1", *str2 = "2"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
사용자가 str1, str2를 입력했다고 가정합니다. str1과 str2가 모두 정수형 문자열인 경우 이 코드는 정상적으로 작동하지만 사용자의 입력이 잘못되어
<와 같이 잘못된 문자가 입력될 수 있습니다. 🎜>char *str1 = "1", *str2 = "a"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
char *str1 = "1", *str2 = NULL; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
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); }
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"); }
char *str1 = "1", *str2 = "12,"; int num1 = parseNumber(str1); int num2 = parseNumber(str2); printf("sum is %d\n", num1 + num2);
이렇게 하면 다음에 프로그램에 문제가 발생할 때 호출자가 문제를 찾을 수 있도록 오류 발견(parseNumber)과 오류 처리(게임 통계 코드)를 분리하여 처리하는 방식입니다.
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等信息足够自己去定位问题所在 }
여기에서는 예외 발생 및 잡기와 예외 사용 시나리오를 소개합니다. 다음으로 C++ 예외에 대해 단계별로 설명하겠습니다.
예외 설명
이를 발생시킬 수 있는 함수 및 함수는
과 같은 함수 선언의 일부로 유용합니다.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/
C++ 표준 예외는 비교적 적지만 함수 개발자는 C++ 표준 예외를 최대한 재사용해야 합니다. 함수 호출자는 사용자 정의 예외 클래스를 이해하는 데 소요되는 시간을 줄이고 개발된 함수를 더 잘 호출할 수 있습니다. .
요약
이 문서에서는 예외 사용 시나리오에서 예외의 기본 사용법을 간단히 소개합니다. 일부 고급 예외 사용법은 나열되지 않습니다. 자세한 내용은 C++ 프로그래밍 언어를 참조하세요. C++ 예외 처리의 아버지.