C++言語解説:2-6.例外処理
2002-08-01 |
[C++ Index Top] [Prev] [Next] |
[概要] エラー処理の基本となる例外処理について学習します。C++の例外処理は多種有り ますが、ここでは標準的処理を対象とします。 [構成]・例外処理の基礎 * try, throw, catch * 派生クラスのcatch * 全てをcatch * 投入型制限 * 再投入 ・newとdeleteの例外処理 * newの例外処理 * newとdeleteのオーバーロード * 脱線:auto_ptr ●例外処理の基礎 ◆try, throw ,catch ・C++は実行時のエラーを操作するために、例外処理(exception handling)の機能を持 っています。これにより例外エラー発生時に処理ルーチンを自動的に呼び出すこと ができるようになります。 ・C++の例外処理は、try, throw, catchのキーワードで構成されます。一般的な構成 は以下のようになります。 [List1.tryとcatchの構成] ┌───────────────────────────────────┐ try { //実行コードブロック //型1〜型Nの例外をthrowする可能性がある } catch(型1 仮引数){ //例外処理ブロック1 } catch(型2 仮引数){ //例外処理ブロック2 } : : catch(型N 仮引数){ //例外処理ブロックN } └───────────────────────────────────┘ ・tryブロックにはエラーが監視されるべき実行コードを記述します。監視する部分は 単一ステートメントでも、main()関数を含むプログラム全体でも構いませんが、実 行時エラー処理を行う目的において、後者のみのtry〜catchで済むことは無いでし ょう。 ・そして、エラー(例外)はtryブロックからthrowされます。言い換えると、throwは指 定型の例外を生成します。生成された例外型を処理するcatchブロックが存在する場 合、該当ルーチンで例外処理が実行されます。 ・throwされた例外型を処理するcatch文が存在しない場合「プログラム異常終了」処 理ハンドラのterminate()関数が呼び出されます。terminate()関数が扱うエラー処 理関数はコンパイラによって独自定義できますが、デフォルトはabort()関数を呼び 出します。 ・それではList2に例外処理の例を示します。 [List2.例外処理の例] ┌───────────────────────────────────┐ #include <iostream> using namespace std; int main() { int i_a; cout << "int例外(0), char例外(1) : "; cin >> i_a; try { if (i_a==0) throw 100; //int例外throw else if (i_a==1) throw 'A'; //char例外throw else if (i_a==2) throw "String"; //char*例外throw cout << "例外が起きなかった" << endl; } catch(int i_except){ cout << "Catch int例外" << endl; } catch(char c_except){ cout << "Catch char例外" << endl; } cout << "catchブロック終了" << endl; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] C:>prog int例外(0), char例外(1) : 0 Catch int例外 catchブロック終了 C:>prog int例外(0), char例外(1) : 1 Catch char例外 catchブロック終了 C:>prog int例外(0), char例外(1) : 2 Abnormal program termination ← abort()関数が実行された C:>prog int例外(0), char例外(1) : 3 例外が起きなかった catchブロック終了 └───────────────────────────────────┘ ・List2では、int型とchar型のエラーハンドラを用意しています。0/1の入力により、 int/charの例外がthrowされた段階でtryブロックの実行が終了し、「例外が起きな かった」は表示されていません。 ・2を入力すると文字列(char*)例外をthrowしますが、char*に対するエラーハンドラ は用意していないので、terminate()→abort()関数が呼び出されています。 ◆派生クラスのcatch ・派生クラスをcatchする場合には注意が必要です。ポリモーフィズムの観点から見れ ばわかるのですが、基本クラスの例外は派生クラスの例外を含みます。 ・従って、派生クラスの例外をcatchしたい場合、派生クラスのエラーハンドラを、基 本クラスのエラーハンドラよりも先に記述する必要があります。List3に失敗例を示 します。 [List3.派生クラスの例外処理(失敗例)] ┌───────────────────────────────────┐ #include <iostream> using namespace std; class Base { public: virtual void Exe() { cout << "Base\n"; return; } }; class Derived : public Base { public: void Exe() { cout << "Derived\n"; return; } }; int main() { Base base; Derived derived; int i_a; cout << "Base例外(0), Derived例外(1) : "; cin >> i_a; try { if (i_a) throw derived; //Derived例外 else throw base; //Base例外 } catch(Base base){ cout << "Catch Base例外" << endl; } catch(Derived derived){ cout << "Catch Derived例外" << endl; } cout << "catchブロック終了" << endl; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] C:\Tmp>prog Base例外(0), Derived例外(1) : 0 Catch Base例外 catchブロック終了 C:\Tmp>prog Base例外(0), Derived例外(1) : 1 Catch Base例外 <-------- Baseのハンドラが呼ばれている!! catchブロック終了 └───────────────────────────────────┘ ・ちなみにbcc32で、List3をコンパイルしたところ 「'Derived' のハンドラが直前のハンドラ 'Base' で隠されている」 と警告が出ました。List3の不具合を完全に言い当てています。 ・ではList4に成功例を示します。 [List4.派生クラスの例外処理(成功例)] ┌───────────────────────────────────┐ // // BaseとDerivedについてはList3と同様 // int main() { Base base; Derived derived; int i_a; cout << "Base例外(0), Derived例外(1) : "; cin >> i_a; try { if (i_a) throw derived; //Derived例外 else throw base; //Base例外 } catch(Derived derived){ cout << "Catch Derived例外" << endl; } catch(Base base){ cout << "Catch Base例外" << endl; } cout << "catchブロック終了" << endl; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] C:\Tmp>prog Base例外(0), Derived例外(1) : 0 Catch Base例外 catchブロック終了 C:\Tmp>prog Base例外(0), Derived例外(1) : 1 Catch Derived例外 catchブロック終了 └───────────────────────────────────┘ ◆全てをcatch ・全ての例外をcatchする以下の表現が存在します。 catch(...){・・・} ・大抵は特定の型のエラーハンドラを記述後、取りこぼしたエラーの処理を行うため に使用します。List5に例を示します。 [List5.catch(...)の例] ┌───────────────────────────────────┐ // //List2にcatch(...)を追加 // #include <iostream> using namespace std; int main() { int i_a; cout << "int例外(0), char例外(1) : "; cin >> i_a; try { if (i_a==0) throw 100; //int例外throw else if (i_a==1) throw 'A'; //char例外throw else if (i_a==2) throw "String"; //char*例外throw cout << "例外が起きなかった" << endl; } catch(int i_except){ cout << "Catch int例外" << endl; } catch(char c_except){ cout << "Catch char例外" << endl; } catch(...){ cout << "Catch 全ての例外" << endl; } cout << "catchブロック終了" << endl; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] C:>prog int例外(0), char例外(1) : 0 Catch int例外 catchブロック終了 C:>prog int例外(0), char例外(1) : 2 Catch 全ての例外 <---------- char*の例外を捕捉している catchブロック終了 └───────────────────────────────────┘ ◆投入型制限 ・関数が外部へ投入する例外の型を制限することができます。例外型制限は以下のよ うに記述します。 戻値型 関数名(引数リスト) throw(型リスト) {・・・} ・throw型リスト以外の例外を投入すると、unexpected()関数が呼ばれます。デフォル トでは、この関数もabort()を呼び出します。unexpected()が呼び出す関数もコンパ イラ側でユーザ定義することができます。 [List6.投入型制限の例] ┌───────────────────────────────────┐ #include <iostream> using namespace std; void Throw_Except() throw(int, char); int main() { try { Throw_Except(); cout << "例外が起きなかった" << endl; } catch(int i_except){ cout << "Catch int例外" << endl; } catch(char c_except){ cout << "Catch char例外" << endl; } catch(...){ cout << "Catch 全ての例外" << endl; } cout << "catchブロック終了" << endl; return 0; } void Throw_Except() throw(int, char) { int i_a; cout << "int例外(0), char例外(1) : "; cin >> i_a; if (i_a==0) throw 100; //int例外throw else if (i_a==1) throw 'A'; //char例外throw else if (i_a==2) throw "String"; //char*例外throw return; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] C:>prog int例外(0), char例外(1) : 0 Catch int例外 catchブロック終了 C:>prog int例外(0), char例外(1) : 2 Abnormal program termination <---- catch(...)も捕捉しない └───────────────────────────────────┘ ・List6では、2が入力されると必ずunexpected()が実行されます。bcc32では throw 式が例外指定に違反している(関数 Throw_Except() throw(int,char) ) の警告がコンパイル時に出ました。 ◆再投入 ・catchブロック(エラーハンドラ)から例外を再投入(rethrow)することができます。 このときは例外オブジェクトを指定せずに、そのままthrowと記述します。このとき throwするものは該当catchが捕捉した例外オブジェクトです。 ・これは関数の中でローカルなtry〜catchブロックがあり、その中でcatchした例外の ローカル処理を施した後、再度関数の外へ再投入するようなケースで使用します。 ・これにより、エラー処理を適当な分担を持たせながら、複数のハンドラへ分散させ ることができます。 ・尚再投入時に投入すべきオブジェクトが存在しない場合、terminate()関数が呼び出 されます。 ●newとdeleteの例外処理 ◆newの例外処理 ・1-7章(他のデータ型と演算子)でnewとdelete演算子を学習しましたが、このときは 「newによるメモリ確保は成功」という前提で説明を行いました。もちろん実際は失 敗することも有り得ます。 ・最新のC++では、new失敗時に例外が投入されます。これは「古いC++は違った」とい う意味です。これからC++を使用するのであれば、例外方式を理解すべきと考えます。 ・newはメモリ確保に失敗するとbad_alloc例外を投入します。bad_alloc例外を使用す るには<new>ヘッダが必要です。ではList7に例を示します。 [List7.newの例外処理] ┌───────────────────────────────────┐ #include <iostream> #include <new> using namespace std; int main() { double* dp; int i_num; cout << "確保数 :"; cin >> i_num; try { dp = new double[i_num]; cout << "確保成功" << endl; } catch(bad_alloc ba) { cout << "確保失敗" << endl; exit(EXIT_FAILURE); } delete[] dp; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] C:>prog 確保数 :1000 確保成功 C:>prog 確保数 :250000000 確保失敗 └───────────────────────────────────┘ ◆newとdeleteのオーバーロード ・newとdeleteは演算子ですから、当然オーバーロードすることができます。しかし 2-2章(演算子オーバーロード)で説明しなかった理由ですが、newの標準的動作とし て確保失敗時にbad_allocを投げる必要があったからです。 ・オーバーロード版のnewは、意図的にオーバーロード関数内で定義をしない限り bad_alloc例外を投げません。確保失敗時にはnullポインタが返ります。 ・このままだと、malloc()やfree()(*1)と何も違わない印象を持ちますが、決定的に 違う点があります。それは new / newのオーバーロード --------> コンストラクタを呼ぶ delete / deleteのオーバーロード --> デストラクタを呼ぶ ことです。 ・もう1点ですが、配列が呼ばれると、つまりnew[] / delete[]が呼ばれると、各オブ ジェクトの確保数分、コンストラクタ又はデストラクタが自動的に呼ばれます。プ ログラマが特に意識する点はありません。 ・それではList8にnewとdeleteのオーバーロード例を示します。ここでは、オーバー ロードのnewと通常のnewが混乱するのを避けるために、メモリ確保そのものは malloc()とfree()関数で行っています。 [List8.newとdeleteのオーバーロード] ┌───────────────────────────────────┐ #include <iostream> #include <new> using namespace std; class Sample { public: Sample() { cout << "コンストラクタ\n"; return; } ~Sample() { cout << "デストラクタ\n"; return; } void* operator new(size_t st_size); void* operator new[](size_t st_size); void operator delete(void* p_mem); void operator delete[](void* p_mem); }; void* Sample::operator new(size_t st_size) { void* p_mem; cout << "--- new実行 ---\n"; p_mem = malloc(st_size); if (!p_mem) { bad_alloc ba; throw ba; //bad_alloc例外の投入 } return p_mem; } void* Sample::operator new[](size_t st_size) { void* p_mem; cout << "--- new[]実行 ---\n"; p_mem = malloc(st_size); if (!p_mem) { bad_alloc ba; throw ba; //bad_alloc例外の投入 } return p_mem; } void Sample::operator delete(void* p_mem) { cout << "--- delete実行 ---\n"; free(p_mem); return; } void Sample::operator delete[](void* p_mem) { cout << "--- delete[]実行 ---\n"; free(p_mem); return; } int main() { Sample* p_new1; Sample* p_new2; try { p_new1 = new Sample; } catch(bad_alloc ba){ cout << "失敗1\n"; exit(1); } delete p_new1; try { p_new2 = new Sample[2]; //ここを変えると失敗例が見える } catch(bad_alloc ba){ cout << "失敗2\n"; exit(1); } delete[] p_new2; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] --- new実行 --- コンストラクタ デストラクタ --- delete実行 --- --- new[]実行 --- コンストラクタ コンストラクタ デストラクタ デストラクタ --- delete[]実行 --- └───────────────────────────────────┘ ・この実行結果から、演算子関数とコンストラクタ/デストラクタが動作する順番を確 認して下さい。 ◇脱線:auto_ptr ・newとdeleteが伴うオブジェクトを扱うために コピーコンストラクタ 代入演算子オーバーロード 等を2-1,2-2章で学習してきました。これらは暗黙のコピーや、暗黙の代入演算によ り解放すべきメモリ領域が2重指定されることが要因でした。 ・しかしながら、メモリ容量的に厳しいオブジェクトを使用する場合、従来のように コピーコンストラクタや代入演算子関数で「別領域」を確保できるものでしょうか。 ・この場合、C++ではテンプレートクラスのauto_ptrを使用します。auto_ptrの特徴は コピー時に所有権が移る スコープ終了時にdeleteする といった点でしょう。尚auto_ptrを使用するには<memory>ヘッダが必要です。 ・auto_ptrは以下の書式で使用します。 ┌───────────────────────────────────┐ (1)newで確保したオブジェクトを指す場合 auto_ptr<型名> ポインタ名(new 型名); (2)何も指さない場合 auto_ptr<型名> ポインタ名; └───────────────────────────────────┘ List9に所有権が移る例を示します。 [List9.auto_ptr使用例(1):所有権が移る] ┌───────────────────────────────────┐ #include <iostream> #include <memory> #include <new> using namespace std; class Sample { int mi; public: Sample(int i_num){ mi = i_num; cout << mi; cout << " :コンストラクタ\n"; return; } ~Sample(){ cout << mi; cout << " :デストラクタ\n"; return; } }; int main() { auto_ptr<Sample> p_sample1(new Sample(1)); auto_ptr<Sample> p_sample2; cout << "auto_ptrコピー前\n"; p_sample2 = p_sample1; //auto_ptrの代入 cout << "auto_ptrコピー後\n"; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] 1 :コンストラクタ auto_ptrコピー前 auto_ptrコピー後 1 :デストラクタ └───────────────────────────────────┘ ・この結果からわかるように、コンストラクタとデストラクタは1回ずつ実行されてい ます。つまりp_sample1が指していたオブジェクトが、p_sample2へ所有権委譲され ています。そしてp_sample2のスコープ終了時にデストラクタが実行されています。 ・これが通常のポインタであれば、p_sample1とp_sample2は同じオブジェクトを指し 必ずdeleteを行う必要から、どちらのポインタをdeleteするべきか、注意を払わな ければなりません。 ・auto_ptrの場合、deleteはオブジェクトのスコープが失われた時点で実行されます。 List10に例を示します。 [List10.auto_ptr使用例(2):スコープ終了時にdelete] ┌───────────────────────────────────┐ // // List9と同様 // int main() { auto_ptr<Sample> p_sample1(new Sample(1)); auto_ptr<Sample> p_sample2(new Sample(2)); cout << "auto_ptrコピー前\n"; p_sample2 = p_sample1; //auto_ptrの代入 cout << "auto_ptrコピー後\n"; return 0; } └───────────────────────────────────┘ ┌───────────────────────────────────┐ [出力結果] 1 :コンストラクタ 2 :コンストラクタ auto_ptrコピー前 2 :デストラクタ auto_ptrコピー後 1 :デストラクタ └───────────────────────────────────┘ ・p_sample2 = p_sample1が実行された段階で、p_sample2にSample(1)オブジェクトの 所有権が移り、上書きされたSample(2)オブジェクトのスコープが終了するので Sample(2)のデストラクタが実行されます。 ・このように、auto_ptrを使用することで、newとdeleteに伴うコーディングミスを減 らすことができるようになります。使いこなせれば非常に便利な機能でしょう。 ・ただし注意点として、auto_ptrは自らがdeleteを行う機能(破壊的コピーセマンティ クス-destructive copy semantics)を持っているため、STLのコンテナ要素等には適 用できません。 ・STLについては、本テキストでは範囲外としていますが、今後違う場にて取り扱いた いと思っています。 (*1)malloc()はC言語で動的メモリを確保する関数です。戻り値はvoid*ポインタです。 free()は引数のポインタが指すメモリを解放する関数です。 [Revision Table] |Revision |Date |Comments |----------|-----------|----------------------------------------------------- |1.00 |2002-08-01 |初版 |1.01 |2002-08-08 |リンク追加 [end] |
Copyright(C) 2002 Altmo
|
[C++ Index Top] [Prev] [Next] |