Heim > Artikel > Backend-Entwicklung > C++-Ausnahmebehandlung
Einführung
Ausnahmen ermöglichen es einer Funktion, eine Ausnahme auszulösen, wenn sie einen Fehler findet, den sie nicht behandeln kann, in der Hoffnung, dass ihr Aufrufer das Problem direkt oder indirekt lösen kann. Wenn die herkömmliche Fehlerbehandlungstechnologie ein Problem erkennt, das nicht lokal behandelt werden kann:
1 Beenden Sie das Programm (z. B. atol, atoi, geben Sie NULL ein). Es tritt ein Segmentierungsfehler auf, der dazu führt, dass das Programm abnormal beendet wird, falls vorhanden ist keine Kerndatei, Leute, die nach Problemen suchen, werden definitiv verrückt)
2. Geben Sie einen Wert zurück, der einen Fehler anzeigt (viele Systemfunktionen sind so, wie z. B. Malloc, unzureichender Speicher, Zuordnungsfehler, NULL-Zeiger zurückgeben)
3 .Gibt einen zulässigen Wert zurück und versetzt das Programm in einen illegalen Zustand (das Ärgerlichste ist, dass einige Bibliotheken von Drittanbietern dies wirklich tun)
4. Rufen Sie eine vorbereitete Funktion auf verwendet werden, wenn ein „Fehler“ auftritt.
Der erste Fall ist nicht zulässig. Die Bibliothek zum bedingungslosen Beenden von Programmen kann nicht in Programmen verwendet werden, die nicht abstürzen können. Der zweite Fall wird häufiger verwendet, ist jedoch manchmal ungeeignet. Beispielsweise ist der zurückgegebene Fehlercode int und der Fehlerwert muss bei jedem Aufruf überprüft werden. Dies ist äußerst unpraktisch und kann die Größe des Programms (aber der Logik) leicht verdoppeln muss genau kontrolliert werden. Ich denke, das ist kein schlechter Weg. Im dritten Fall ist es leicht, den Aufrufer in die Irre zu führen, wenn er die globale Variable errno nicht überprüft oder auf andere Weise auf Fehler prüft, ist dies eine Katastrophe, und diese Methode funktioniert in gleichzeitigen Situationen nicht gut. Was die vierte Situation betrifft, denke ich, dass sie weniger verwendet wird und der Rückrufcode nicht zu oft erscheinen sollte.
Die Verwendung von Ausnahmen trennt Fehler und Verarbeitung, und der Aufrufer erkennt, dass beim Aufruf der Programmfunktion ein Fehler aufgetreten ist, und kann ihn behandeln liegt in der Hand des Anrufers.
Die Fehlerbehandlung ist jedoch immer noch eine sehr schwierige Sache. Der C++-Ausnahmemechanismus bietet Programmierern eine Möglichkeit, mit Fehlern umzugehen, sodass Programmierer Fehler auf natürlichere Weise behandeln können.
Eine Einführung in praktische Anomalien
Angenommen, wir schreiben ein Programm, um zwei vom Benutzer eingegebene Zeichenfolgen in ganze Zahlen umzuwandeln, sie zu addieren und auszugeben. Im Allgemeinen würden wir so schreiben
char *str1 = "1", *str2 = "2"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
Angenommen, der Benutzer gibt str1 und str2 ein. Wenn str1 und str2 beide Zeichenfolgen vom Typ Integer sind, kann dieser Code normal funktionieren, die Eingabe des Benutzers ist jedoch möglicherweise falsch und es können ungültige Zeichen eingegeben werden, z. B.
char *str1 = "1", *str2 = "a"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
Das Ergebnis ist zu diesem Zeitpunkt 1, da atoi(str2) 0 zurückgibt.
Wenn die Benutzereingabe wie folgt lautet:
char *str1 = "1", *str2 = NULL; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
dann tritt in diesem Code ein Segfault auf und das Programm wird abnormal beendet.
atoi ist meiner Meinung nach eine relativ gefährliche Funktion. Wenn der Aufrufer in einem wichtigen System ein NULL-Zeichen übergibt, ohne es zu wissen, wird das Programm abnormal beendet, was zu einer Dienstunterbrechung führt, oder es werden illegale Zeichen übergeben. , das Ergebnis gibt 0 zurück und der Code wird fortgesetzt. Es ist nicht einfach, dieses Problem in einem komplexen System zu lokalisieren.
Der passendere Weg ist also, dass wir die Ausnahmebehandlung verwenden, um eine sichere Atoi-Methode namens parseNumber umzuwandeln.
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 im obigen Code ist eine benutzerdefinierte Ausnahmeklasse. Wenn wir feststellen, dass es sich bei der eingehenden Zeichenfolge nicht um eine Zahl handelt, lösen wir eine Nummernkonvertierungsausnahme aus und lassen den Anrufer den Fehler behandeln als Es ist viel besser, eine NULL-Zeichenfolge zu übergeben und einen Segmentierungsfehler zu verursachen, der das Programm beendet. Der Aufrufer kann diese Ausnahme abfangen und entscheiden, ob das Programm beendet werden soll, was besser ist, als eine nicht ganzzahlige Zeichenfolge zu übergeben und 0 zurückzugeben. Das Programm läuft stillschweigend weiter, wenn ein Fehler auftritt.
Der Code, den wir zuvor geschrieben haben, kann also wie folgt umgewandelt werden:
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"); }
Das Ergebnis dieses Codes ist die Ausgabe „Die Eingabe ist keine Ganzzahl“. Wenn das System in einem Spielstatistiksystem ausgeführt wird, muss es regelmäßig zählen, wie oft eine große Anzahl von Benutzern Spielkanal 1 und Spielkanal 2 aus einer großen Anzahl von Dateien betreten. str1 stellt die Häufigkeit dar, wie oft sie Spielkanal 1 betreten str2 stellt die Häufigkeit dar, mit der sie Kanal 2 betreten. Wenn keine Ausnahme verwendet wird und die Eingabe ein NULL-Programm ist, führt dies zum Absturz des gesamten Systems. Wenn die Eingabe eine unzulässige Ganzzahl ist, sind alle Berechnungsergebnisse falsch Das Programm wird stillschweigend immer noch „korrekt ausgeführt“.
Die Eingabe ist unzulässig und es wird eine NumberParseException ausgelöst, auch wenn der Anrufer die Eingabe nicht für unzulässig hält, zum Beispiel:
char *str1 = "1", *str2 = "12,"; int num1 = parseNumber(str1); int num2 = parseNumber(str2); printf("sum is %d\n", num1 + num2);
Auch wenn der Anrufer nachlässig ist und dies nicht tut Fangen Sie die Ausnahme ab, das Programm löst aus. Wenn eine NumberParseException auftritt, stürzt das Programm ab und hinterlässt eine Coredump-Datei. Der Aufrufer übergibt „gdb „Programmname Coredump-Datei“. Wenn Sie den Stapel überprüfen, wenn das Programm abstürzt, werden Sie feststellen, dass während der Ausführung des Programms ein unzulässiges Ganzzahlzeichen erscheint. Dann wird er das Problem schnell erkennen und lernen, den obigen Code in
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等信息足够自己去定位问题所在 }Auf diese Weise kann der Aufrufer das nächste Mal, wenn ein Problem im Programm auftritt, das Problem lokalisieren. Auf diese Weise werden Ausnahmen behandelt, indem Fehlererkennung (parseNumber) und Fehlerverarbeitung (Spielstatistikcode) getrennt werden.
Hier stellen wir das Auslösen und Abfangen von Ausnahmen sowie die Verwendungsszenarien von Ausnahmen vor. Beschreibung von Ausnahmen
Funktionen und Funktionen, die sie auslösen können, sind als Teil der Funktionsdeklaration wertvoll, z. B.
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/
Obwohl es relativ wenige C++-Standardausnahmen gibt, sollten Sie als Funktionsentwickler so oft wie möglich C++-Standardausnahmen verwenden. Als Funktionsaufrufer können Sie weniger Zeit damit verbringen, Ihre benutzerdefinierten Ausnahmeklassen zu verstehen und Ihre entwickelten Funktionen besser aufzurufen .
Zusammenfassung
Dieser Artikel stellt lediglich die grundlegende Verwendung von Ausnahmen aus den Verwendungsszenarien von Ausnahmen vor. Detaillierte Informationen finden Sie in der Programmiersprache C++ Vater von C++.