[C++]——例外処理

序文:

  • 今回は例外処理 に関する知識を解説していきます!

目次

(1) C言語における従来のエラー処理方法

(2) C++例外の概念

(3) 異常な使用

1. 例外のスローとキャッチ

1️⃣ 例外のスローとマッチングの原則 

2️⃣ 関数呼び出しチェーンにおける例外スタックの拡張と一致原理

2. 例外の再スロー 

3. 非常に安全

4.仕様の異常

(4) C++標準ライブラリの例外システム

(5) 異常なメリットとデメリット

要約する


(1) C言語における従来のエラー処理方法

まず、C 言語で例外を処理する関連する方法を確認してみましょう。

  •  アサート、欠陥などの終了プログラム: ユーザーにとって受け入れがたいもの。メモリ エラーが発生すると、ゼロ除算エラーによってプログラムが終了します。

以下は、マクロを使用した例外処理の簡単なコード説明です。

#include <stdio.h>
#include <assert.h>


int divide(int num1, int num2) 
{
    assert(num2 != 0);  // 断言num2不等于0

    return num1 / num2;
}

int main() 
{
    int result = divide(10, 0);
    // 如果编译时定义了NDEBUG宏,assert会被禁用,否则会触发异常并终止程序执行

    return 0;
}

【説明する】

1. 上記のコードでは、関数を使用して 2 つの整数の除算演算を実装しています。関数内でマクロを使用すると、除数がゼロでないことを。ゼロの場合、例外がトリガーされ、プログラムの実行が終了します。

2. 関数内で関数を呼び出し、除数を 0 として渡します。コンパイル時にマクロが定義されていない場合 (つまり、デバッグ モードが有効になっていない場合)、例外がトリガーされ、プログラムの実行が終了します。マクロが定義されている場合、マクロは無効になり、例外はトリガーされず、プログラムは後続のコードの実行を続行します。

【出力表示】


  • エラー コード、欠陥が返されます。プログラマは対応するエラーを自分で見つける必要があります。たとえば、システム内の多くのライブラリのインターフェイス関数は、errno にエラー コードを入れることでエラーを表現します。

 以下は、エラー コードを返して例外を処理するコードの例です。

#include <stdio.h>
int divide(int num1, int num2, int* res)
{
    if (num2 == 0) 
    {
        return -1; // 返回错误码 -1 表示除数为零的异常情况
    }

    *res = num1 / num2;
    return 0; // 返回 0 表示成功
}

int main() 
{
    int num1 = 10, num2 = 0, res;
    int num = divide(num1, num2, &res);

    if (num != 0)
    {
        printf("Error: Divide by zero\n");
        // 处理错误的逻辑
    }
    else 
    {
        printf("Result: %d\n", res);
        // 处理正常情况的逻辑
    }

    return 0;
}

 【説明する】

  1. 関数内で関数を呼び出し、除数に 0 を渡します。関数が返すエラーコードは変数に格納されており、その値を判断することで正常か異常かを判断できます。0 に等しくない場合は、例外が発生したことを意味し、特定の状況に応じてエラー処理を実行できます。
  2. 返されたエラー コードが 0 の場合は除算が成功したことを意味し、変数を通じて計算結果を取得し、対応する通常の処理ロジックを実行できます。

【出力表示】

実際のC言語では基本的にエラーコードを返す方法でエラー処理を行っていますが、重大なエラーに対しては終了プログラムを使用する場合もあります。


(2) C++例外の概念
 

C++ では、例外はプログラムの実行時にエラーを処理するために使用されるメカニズムです。例外は、通常のプログラム フローを抜け出し、エラー情報を適切なハンドラーに渡して処理する方法を提供します。

次に、C++ 例外に関する概念をいくつか示します。

  1. 例外のスロー: 例外が発生した場合、throwステートメントを使用して例外をスローできます。throwステートメントには通常、基本型、クラス オブジェクト、またはポインターのいずれかである例外オブジェクトが含まれます。

  2. 例外のキャッチ: 例外がスローされた後、プログラムはtry-catchステートメント ブロックを使用して例外をキャッチして処理できます。tryブロックには例外が発生する可能性のあるコードが含まれており、catchブロックは例外をキャッチして処理するために使用されます。

  3. 例外ハンドラー: catchブロックは、例外を処理するために使用されるコードのブロックです。catchブロックでは、スローされた例外の種類に基づいて、対応する処理ロジックを実行できます。例外タイプを 1 つずつ順番に照合し、照合する処理ロジックを実行する複数のcatchブロックが存在する場合があります 。

ブロックが例外をスローした場合、例外をキャッチするメソッドは try および catch キーワードを使用します。
try ブロックには例外をスローする可能性のあるコードが配置されており、このコードを保護コードと呼びます。

  • try/catch ステートメントを使用するための構文は次のとおりです。
     
try
{
    // 保护的标识代码
}catch( ExceptionName e1 )
{
    // catch 块
}catch( ExceptionName e2 )
{
    // catch 块
}catch( ExceptionName eN )
{
    // catch 块
}

【まとめ】

  1. 例外処理メカニズムを合理的に使用してプログラム内のエラーをキャッチして処理することにより、プログラムの堅牢性と保守性を高めることができます。
  2. 適切な例外処理により、コードがより明確で読みやすくなり、例外状況をより適切に処理してプログラムのフォールト トレランスを向上させることができます。

(3) 異常な使用

1. 例外のスローとキャッチ

C++ では、例外のスローと一致の原則は次の基本原則に従います。

1️⃣ 例外のスローとマッチングの原則
 

  1. スローされた例外:

    • プログラムで例外が発生した場合、throwステートメントを使用して例外をスローできます。
    • throw通常、ステートメントには例外オブジェクトが含まれます。例外オブジェクトには、プリミティブ型、クラス オブジェクト、またはポインタを指定できます。
  2. 例外一致:

    • catch例外の一致とは、スローされた例外のタイプに基づいて例外を処理するブロックを選択することを指します。
    • C++ 例外処理メカニズムは、例外タイプを処理できるブロックを見つけるために、ブロックtry内のブロックのタイプを照合しますcatchcatch
  3. 例外タイプの一致と継承関係:

    • C++ では、例外型が継承関係を形成できます。つまり、派生クラスの例外オブジェクトをcatch基本クラスのブロックでキャプチャできます。
    • 例外型に継承関係がある場合は、派生クラスのブロックを基本クラスのブロックより前にcatch配置する必要がありcatch、そうでない場合、派生クラスのブロックcatchは実行されません。
  4. 最適な例外処理:

    • catchC++ 例外処理メカニズムは、スローされた例外を処理するために最も一致するブロックを選択します。
    • 最も一致するブロックはcatch、スローされた例外の型またはその基本クラスの型を処理できるcatchブロック、つまり例外の型に最も近いブロックです。
  5. 一致しない例外の処理:

    • tryブロック内で例外がスローされ、その例外を処理するための一致するブロックが見つからない場合catch、例外は呼び出しスタックの上位に渡されます。
    • 例外が一致するcatchブロックによって処理されない場合、プログラムは最終的に実行を終了し、例外メッセージを出力する可能性があります。

【予防】

  1. 例外のスローとマッチングの原則は、catch例外を選択して処理するためにブロックをマッチングすることです。そのため、catchブロックの順序に注意してください。
  2. 一般に、例外が正しく処理され、対応する例外処理ロジックが実行されることを確認するには、特定の例外の型から始めて、次に基本クラスの型と一致させる必要があります。

2️⃣ 関数呼び出しチェーンにおける例外スタックの拡張と一致原理

関数呼び出しチェーンでは、例外スタック拡張一致原則は主に、例外の種類を一致させ、正しい例外処理コードを選択する方法を指定します。例外が発生すると、C++ ランタイム システムは、現在実行中の関数から開始してコール スタック内の関数呼び出しをチェックし、スローされた例外の種類に一致するブロックを見つけますcatch

例外スタックの拡張と一致の原則は次のとおりです。

  • 現在の関数のtryブロックを確認します。

    • 現在の関数にtryブロックが含まれている場合、ランタイム システムは一致するcatchブロックを探します。
  • 現在の関数のcatchブロックを確認します。

    • 現在の関数にスローされた例外のタイプに一致するブロックが含まれている場合catch、そのcatchブロックが実行されます。
    • 複数の一致するブロックが見つかった場合、例外を処理するためにcatch最も近い (最も近い)ブロックが選択されます。catch
  • catch現在の関数に一致するブロックがない場合:

    • 例外スタックは上位レベルの呼び出し関数に拡張されます。
    • 一致するcatchブロックが見つかるか、最上位の呼び出しスタックに到達するまで、手順 1 と 2 を繰り返します。
  • 呼び出しスタック全体で一致するcatchブロックが見つからない場合:

    • プログラムの実行が終了し、標準ライブラリ関数が呼び出されてterminate()プログラムが終了します。

 例外スタックの巻き戻しとマッチングに関する重要な注意事項:

  • 例外は、例外がスローされた順序ではなく、スタックが展開された順序で照合されます。
  • 派生クラスの例外オブジェクトは基本クラスのブロックでキャッチできるため、基本クラスのブロックはcatch派生クラスのcatchブロックの前に配置する必要がありますcatch
  • 一致するブロックのない関数で例外がスローされた場合catch、一致するcatchブロックが見つかるかプログラムが終了するまで、例外は呼び出しスタックに伝播されます。
  • 例外スタックの巻き戻しは関数とスレッドの境界を越えるため、これらの一致原則はマルチスレッド プログラムにも適用されます。

 

たとえば次の例です。

 

次に、コードを通して詳しく理解しましょう。

double Division(int a, int b) 
{
    if (b == 0)
        throw "Division by zero condition!";
    else
        return ((double)a / (double)b);
}

void Func() 
{
    int len, time;
   cin >> len >> time;
   cout << Division(len, time) << endl;
}

int main() 
{
    try {
        Func();
    }
    catch (const char* errmsg) {
        cout << errmsg << endl;
    }
    catch (...) {
        cout << "unknown exception" << endl;
    }

    return 0;
}

出力表示:

 

 【説明する】

  1. 例外処理メカニズムを使用して、発生する可能性のあるゼロ除算例外をキャッチして処理します。ゼロによる除算が発生すると、文字列定数例外がスローされ、catch (const char* errmsg)ブロックによってキャッチされます。
  2. 他のタイプの例外が発生した場合、それらはcatch (...)ブロックによって捕捉され、対応する処理ロジックが実行されます。

 

2. 例外の再スロー
 

C++ では、例外の再スローにより、catchキャッチされた例外をブロック内で処理して再スローできるため、高レベルの例外処理コードで例外をさらに処理できるようになります。throw例外はステートメントを使用して再スローできます。

例外の再スローを使用するコード例を次に示します。

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}
void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
	// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
	// 重新抛出去。
	int* array = new int[10];
	try {
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw;
	}
	// ...
	cout << "delete []" << array << endl;
	delete[] array;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	return 0;
}

【説明する】

  1. 関数内でFuncゼロ除算例外が発生した場合、例外をキャッチし、削除したarray情報を出力します。その後、array例外が解放され ( used delete[])、throwステートメントを使用して例外が再スローされます。このようにして、例外は上位レベルのコードに渡されます。
  2. 関数内ではmain一番外側のブロックで例外をキャッチしcatch、例外情報を出力します。
  3. Func関数内で例外を再スローし、例外をキャッチする前後に例外を解放することでarray例外が上位レベルに渡される前に関連リソースが確実に解放されるようにすることができます。

【まとめ】

例外を再スローすることで、例外をキャッチした場所で適切に処理でき、同じ例外の処理を継続したり、上位レベルのコードで他の操作を実行したりできます。このメカニズムにより、柔軟性とエラーの上方伝播が実現します。 


3. 非常に安全

  • コンストラクターは、 オブジェクト の構築と初期化を完了しますコンストラクターで例外をスローしないことが最善です。そうしないと、オブジェクトが不完全であるか、完全に初期化されていない可能性があります。
  • デストラクターは主にリソースのクリーンアップを完了します。デストラクターで例外をスローしないことが最善です。そうしないと、リソース リーク(メモリ リーク、ハンドルが閉じられていないなど) が発生する可能性があります。
  • C++ の例外はリソース リークにつながることがよくあります。たとえば、new と delete で例外がスローされてメモリ リークが発生し、ロックとロック解除の間に例外がスローされてデッドロックが発生します。C++ では、上記の問題を解決するために RAII がよく使用されます。スマート ポインターのこのセクションで説明されています

4.仕様の異常

 C++ では、例外仕様は、関数がスローする可能性のある例外を関数宣言で指定する方法です。例外仕様を関数の一部として含めて、関数がスローする可能性のある例外のタイプを識別できます。具体的には、例外仕様は、関数がスローできる例外タイプのリストを指定します。

C++98\03 では、例外仕様はthrow()宣言を使用します。例えば:

void foo() throw(int, std::exception);

【説明する】

  1. 上記のコードは、関数が型と型の例外fooをスローする可能性があることを示しています。intexception
  2. 関数が例外仕様にリストされていない別の例外タイプをスローした場合、プログラムはunexpectedその関数を呼び出します。これにより、デフォルトでterminate呼び出されたプログラムが終了します。

他の例を以下の画像に示します。 

// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);

// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);

// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();

C++11 では、例外仕様に代わる、より柔軟で安全な例外処理メカニズム (例外仕様) が導入されまし例外仕様では、noexceptキーワードを使用して、関数が例外をスローできるかどうかを指定します。

noexceptキーワードを使用した関数は、「noexcept関数」または「例外をスローしない関数」と呼ばれます。これらには、次のような重要な用途と利点があります。

  1. パフォーマンスの最適化: コンパイラーは、noexcept明示的な約束に基づいていくつかの最適化を行うことができます。
  2. 例外の伝播: 例外を処理すべきではないコンテキストに例外が伝播することを回避します。

noexcept使用例をいくつか示します。

void myFunction() noexcept {
  // 函数体,不会抛出异常
}

void anotherFunction() {
  // 函数体,可能会抛出异常
}

void myFunction2() noexcept(true) {
  // 与上面的 myFunction 等效,不会抛出异常
}

void myFunction3() noexcept(false) {
  // 与 anotherFunction 等效,可能会抛出异常
}

//不会抛出异常
thread (thread&& x) noexcept;

【予防】

  1. C++11 では、noexceptキーワードを関数タイプの一部として使用して、関数が例外をスローするかどうかを示すことができます。
  2. C++17 以降では、noexcept例外をスローするかどうかを動的に決定する関数式がサポートされています。これにより、特定の状況において例外の仕様がより柔軟かつ動的になります。

(4) C++標準ライブラリの例外システム

C++ には、プログラムで使用できる、 で定義された一連の標準例外が用意されています。これらは、
次のように親子クラス階層で編成されます。
 

 

 

: 実際には、例外クラスを継承して独自の例外クラスを実装できます。しかし実際には、多くの企業が上記のような独自の例外継承システムを定義しています。それは、C++ 標準ライブラリの設計が使いにくいからです。
 

int main()
{
	try {
		vector<int> v(10, 5);
		// 这里如果系统内存不够也会抛异常
		v.reserve(1000000000);
		// 这里越界会抛异常
		v.at(10) = 100;
	}
	catch (const exception& e) // 这里捕获父类对象就可以
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

(5) 異常なメリットとデメリット

C++ 例外の利点:

  • 1. 例外オブジェクトが定義されると、エラー コード メソッドと比較して、さまざまなエラー情報を明確かつ正確に表示できます。また、スタック コール情報も含めることができるため、プログラムのバグをより適切に見つけることができます。
  • 2. エラー コードを返す従来の方法の大きな問題は、関数呼び出しチェーンにおいて、深い関数がエラーを返した場合、層ごとにエラーを返さなければならず、最も外側の層だけがエラーを取得できることです。詳細は以下で説明します。
// 1.下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,
ServerStart再返回给main函数,main函数再针对问题处理具体的错误。
// 2.如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因
为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
int ConnnectSql()
{
	// 用户名密码错误
	if (...)
		return 1;
	// 权限不足
	if (...)
		return 2;
}
int ServerStart() {
	if (int ret = ConnnectSql() < 0)
		return ret;
	int fd = socket()
		if(fd < 0)
		return errno;
}
int main()
{
	if (ServerStart() < 0)
		...
		return 0;
}
  • 3. 多くのサードパーティ ライブラリには、boost、gtest、gmock やその他の一般的に使用されるライブラリなどの例外が含まれているため、それらを使用する場合も例外を使用する必要があります。
  • 4. コンストラクタには戻り値がないため、エラーコードを使用して処理するのは不便であるなど、例外を使用した方が処理が容易な関数もあります。たとえば、T& 演算子のような関数の場合、pos が制限を超えると、例外を使用するかプログラムを終了することしかできず、戻り値でエラーを示す方法はありません。

C++ 例外の欠点:

  • 1. 例外によりプログラムの実行フローが大幅にジャンプし、非常に混沌とした状態になります。また、実行時にエラーがスローされるとランダムにジャンプします。これにより、プログラムの追跡、デバッグ、分析が困難になります。
  • 2. 例外にはパフォーマンスのオーバーヘッドが伴います。もちろん、最新のハードウェアの速度が速いため、この影響は基本的に無視できます。
  • 3. C++ にはガベージ コレクション機構がないため、リソースは独自に管理する必要があります。例外を除き、メモリ リークやデッドロックなどの異常なセキュリティ問題が非常に簡単に発生します。これには、リソース管理の問題を処理するために RAII を使用する必要があります。学習コストは高くなります。
  • 4. C++ 標準ライブラリの例外システムは明確に定義されていないため、誰もが独自の例外システムを定義することになり、非常に混乱を招きます。
  • 5. 可能な限り標準化された例外を使用してください。そうでない場合、結果は悲惨なものになります。例外が意のままにスローされると、外側の層に捕らえられたユーザーは悲惨な結果になります。したがって、例外仕様には次の 2 つのポイントがあります。 1. スローされる例外タイプは基本クラスから継承されます。2. 関数が例外をスローするかどうか、およびどのような例外をスローするかは、 func() throw(); を使用して標準化されます。

要約する

以上が C++11 の例外に関する知識のすべてです。続いて、この記事の簡単なレビューです!

  1. 一般的に言えば、例外の利点は欠点を上回るため、エンジニアリングでは例外を使用することを依然として推奨しています。
  2. また、OO 言語では基本的に例外を使用してエラーを処理しており、これが一般的な傾向であることもわかります。
     

この記事はここまでです。見てくださった皆様、応援してくださった皆様、ありがとうございました!

おすすめ

転載: blog.csdn.net/m0_56069910/article/details/132540415