はじめに
例外を使用すると、関数が処理できないエラーを見つけたときに、呼び出し元が直接的または間接的に問題を処理できることを期待して、例外をスローできます。従来のエラー処理テクノロジでは、ローカルで解決できない問題が検出された場合:
1. プログラムを終了します (atol、atoi、NULL を入力すると、コア ファイルがない場合はセグメンテーション違反が発生し、プログラムが異常終了します)。 、問題を探している人は間違いなく発狂します)
2. エラーを示す値を返します(malloc、メモリ不足、割り当て失敗、NULL ポインタの戻りなど、多くのシステム関数がこれに似ています)
3.正当な値を返し、プログラムを何らかの不正な状態にします (最も迷惑なことですが、一部のサードパーティ ライブラリは実際にこのようなものです)
4. 「エラー」の場合に使用するために事前に準備された関数を呼び出します。
最初のケースは、プログラムを無条件で終了するためのライブラリは、クラッシュできないプログラムには適用できません。 2 番目のケースはより一般的に使用されますが、場合によっては不適切です。たとえば、返されるエラー コードは int であり、呼び出しごとにエラー値をチェックする必要があります。これは非常に不便で、プログラムのサイズが簡単に 2 倍になる可能性があります。正確に制御する必要があります。これは悪い方法ではないと思います)。 3 番目のケースでは、呼び出し元がグローバル変数 errno をチェックしない、または他の方法でエラーをチェックしないと、呼び出し元を誤解させる可能性があり、この方法は同時実行状況ではうまく機能しません。 4 番目の状況については、あまり使用されていないと思います。コールバック コードはあまり頻繁に出現しないはずです。
例外を使用すると、ライブラリ関数が例外をスローし、呼び出し元はプログラム関数ライブラリの呼び出し時にエラーが発生したことを認識して処理することができます。プログラムを終了するのは呼び出し側の権限です。
しかし、エラー処理は依然として非常に難しいものです。C++ 例外メカニズムはプログラマにエラーを処理する方法を提供し、プログラマがより自然な方法でエラーを処理できるようにします。
例外の実践的な紹介
ユーザーが入力した 2 つの文字列を整数に変換し、出力を追加するプログラムを作成するとします。一般的には次のように記述します
char *str1 = "1", *str2 = "2"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
ユーザー入力が str1、str2 であるとします。 str2 これらはすべて整数型の文字列です。このコードは正常に動作しますが、ユーザーの入力が間違っている可能性があり、たとえば
char *str1 = "1", *str2 = "a"; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
atoi(str2) が 0 を返すため、結果は 1 になります。
ユーザー入力が次のような場合:
char *str1 = "1", *str2 = NULL; int num1 = atoi(str1); int num2 = atoi(str2); printf("sum is %d\n", num1 + num2);
このコードではセグメンテーション違反が発生し、プログラムは異常終了します。
atoi は、重要なシステムで、呼び出し側がそれを知らずに NULL 文字を渡した場合、プログラムが異常終了してサービスが中断されたり、不正な文字が渡された場合、その結果が発生する、比較的危険な関数だと思います。は 0. となり、コードは継続して実行されます。複雑なシステムでこの問題を特定するのは実際には簡単ではありません。
したがって、より適切な方法は、例外処理を使用して、parseNumber と呼ばれる安全な atoi メソッドを変換することです。上記のコードの
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 は、受信した str が数値ではないことを検出すると、数値変換例外をスローし、呼び出し元にエラーを処理させます。これは、NULL 文字列を渡すよりも優れています。セグメンテーション違反を発生させてプログラムを終了させる方が、呼び出し元がこの例外をキャッチしてプログラムを終了するかどうかを決定できるため、整数以外の文字列を渡して 0 を返すよりも優れています。プログラムは引き続き実行されます。エラーが発生した場合はサイレントで実行されます。
したがって、前に書いたコードは次のように変換できます:
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"); }
このコードの結果は、「入力は整数ではありません」と出力されます。このコードがゲーム統計システムで実行されていると仮定します。多数のファイルから統計を定期的に収集する必要があります。多数のユーザーがゲーム チャンネル 1 とゲーム チャンネル 2 に入場した回数を表します。str1 はゲーム チャンネル 1 に入場した回数を表し、str2 はその回数を表します。例外が使用されていない場合、入力が NULL の場合、プログラムはシステム全体をクラッシュさせます。入力が不正な整数の場合、すべての計算結果は間違っていますが、プログラムは依然として静かに「正しく実行」されます。 。
入力が不正である場合、呼び出し元が入力が不正であると考えていない場合でも、たとえば次のようになります。
char *str1 = "1", *str2 = "12,"; int num1 = parseNumber(str1); int num2 = parseNumber(str2); printf("sum is %d\n", num1 + num2);
呼び出し元が不注意で例外をキャッチしなかった場合でも、NumberParseException がスローされます。プログラムの実行中にスローされます。プログラムがクラッシュすると、コアダンプ ファイルが残され、呼び出し元は「gdb」を渡します。 「プログラム名コアダンプファイル」。プログラムがクラッシュしたときにスタックをチェックすると、プログラムの実行中に不正な整数文字が表示されていることがわかります。その後、問題をすぐに認識し、上記のコードを
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等信息足够自己去定位问题所在 }
こんな感じで、次に プログラム内で問題が発生した際に、呼び出し元が問題箇所を特定するための、エラー検出(parseNumber)とエラー処理(ゲーム統計コード)を分離した例外のエラー処理メソッドです
例外スローが導入されています。次に、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++ プログラミング言語の例外処理を参照してください。 。