[補足] new演算子例外の実験    (1999/04/13 初版)

1. メモリ領域はどれくらいまで確保できるか  以下では非常に大きな配列を動的に作成して、new演算子が例外を発生する実験を行 います。  実験はWindows 95 パソコンで、Borland C++ Builder 3 のコマンドライン・コンパ イラ bcc32.exe を使います。普通、動的メモリ領域は実メモリ(RAM)に作成され ますが、要求された領域が足りないとき Windows 95 ではハードディスクのスワップ・ ファイルを仮想メモリとして利用します。つまり可能なメモリ領域の大きさは、実メモ リとハードディスクの空き領域の大きさで決まります。  以下の例では、約500Mバイトまでの配列領域を作成することができます。このと きハードディスクの空き領域はスワップファイルで埋め尽くされます。そしてハードデ ィスク内にそれ以上の大きさの配列領域を作ろうとしたときに、new演算子は失敗して 例外を発生します。 [注意]この様な実験はシステムに及ぼす危険性が無いとはいえません。またプロ グラムの終了後、ハードディスクの中のスワップ・ファイル(win386.swp)が 大きいまま残っていて、ディスクの空きがほとんど無い状態になる場合がありま す(再起動すると通常のスワップ・ファイルのサイズに戻ります)。  そのようなときは、Windowsを終了させMS-DOSモードで再起動して、Windows ディレクトリにあるスワップ・ファイルWIN386.SWPをMS-DOSコマンドDELで削除 します。スワップ・ファイルはWindowsのMS-DOSプロンプトからは消すことはで きません。スワップ・ファイルは通常のときでもある程度のサイズを持っていま す。削除するのはそれが異常に大きくなった場合です。削除する前にはサイズを 確認してから行います。  次のプログラムは配列の大きさを入力して、整数型の配列を作成します。成功すると 作成された領域のバイト数を表示して、領域を解放し終了します。また失敗するとメッ セージを表示してプログラムを異常終了(アボート)します。
// except.cpp
// new演算子例外の実験プログラム(1)
#include <iostream.h>

int main()
{
    int *ptr, n;

    try{
        cout << "割り当てるメモリサイズを入力してください:"   
        << endl;
        cin >> n;
        ptr = new int[n];
        // 例外が発生すると、この下のコードは実行されない
        cout << "メモリサイズは: " << n * sizeof(int)
             << " バイトです。" << endl;

    }
    catch(bad_alloc){ // 例外 bad_alloc をここで受け取る
        cerr << "bad_alloc 例外を受け取りました。" << endl;  
        abort();
    }
    catch(...){ // それ以外の例外はここで受け取る
        cerr << "その他の例外を受け取りました。" << endl;  
        abort();
    }

    delete [] ptr;
    cout << "メモリを解放しました。" << endl;

    return 0;
}
[実行結果]                            ソースファイル  except.cpp
C:\Source>except                                   
割り当てる配列のサイズを入力してください:         
123456789                                          
メモリサイズは: 493827156 バイトです。            
メモリを解放しました。                             

C:\Source>except                                   
割り当てる配列のサイズを入力してください:         
1234567890                                         
bad_alloc 例外を受け取りました。                   

Abnormal program termination 
 プログラムは、2つの catch ブロックを用意して例外を捕捉するようになっていま す。  2つ目の catch(...)はすべての例外を捕捉します。つまり tryブロックで発生し た例外が bad_alloc なら1つ目のブロックで捕捉されます。それ以外の例外が発生す るとすべて2つ目のブロックで捕捉されます。  この場合 new演算子が送出する例外は標準C++規格の bad_alloc ですから、必ず1 つ目のcatchブロックで捕捉されます。  実行例では、2度プログラムを実行しています。最初はメモリ領域の作成に成功して います。プログラムの実行はcatchブロックが無視されて読み飛ばされます。  2度目の実行では、new演算子が失敗し bad_alloc例外が送出されます。例外の型が 一致する1つ目のcatchブロックに制御が移り、その中のコードが実行されます。  関数 abort()は、呼ばれると次のメッセージ Abnormal program termination を画面に出力して、プログラムを異常終了させます。  この様に現在のコンピュータシステムでは、充分大きなメモリ領域を使うことが可能 です。また使用可能なメモリ領域のおおよその大きさも予想できるので、その範囲内で の利用を前提にする限り、new演算子が例外を送出することは滅多に起こるものではな いといえます。

2. new演算子が失敗したときにヌルポインタを返すようにする  new 演算子がメモリ割り当てに失敗したときの動作をプログラムでカスタマイズする ことができます。  関数 set_new_handler()で、例外の型(ハンドラ)を変更したり、関数を呼び出すよ うにできます。例えば、set_new_handler() の引数にユーザ定義の関数名を指定してお くと、newが失敗するとその関数が呼び出されます(後述)。  C++ Builder のコマンドコンパイラでは、引数に0(ゼロ)を指定すると、例外を送 出しない従来バージョンの new演算子として動作します。つまり、newが失敗するとヌ ルポインタを返します。それを示したのが次のプログラムです。
// except1.cpp
// new演算子例外の実験プログラム(2)
// newが失敗すると例外でなくヌルポインタを返す
#include <iostream.h>

int main()
{
    set_new_handler(0); // newが失敗するとヌルポインタを返す      
    int *ptr, n;

    cout << "割り当てるメモリサイズを入力してください:"
         << endl;
    cin >> n;
    ptr = new int[n];
    if(ptr == 0){
        cerr << "メモリ割り当てに失敗しました。" << endl;
        abort();
    }
    cout << "メモリサイズは: " << n * sizeof(int)
         << " バイトです。" << endl;


    delete [] ptr;
    cout << "メモリを解放しました。" << endl;

    return 0;
}
[実行結果]                         ソースファイル  except1.cpp

C:\Source>except1                                            
割り当てる配列のサイズを入力してください:                   
1234567890                                                   
メモリ割り当てに失敗しました。                               

Abnormal program termination 
この場合のエラーチェックは、C言語の関数 malloc()と同じ流儀、if文を使ってい ます。  この他に、プログラムの正しさをチェックするアサーションの用法があります。こ れは次のように書かれて assert(条件); 条件の評価が偽なら、診断結果が出力されて実行はアボートされます。
// except2.cpp
// new演算子例外の実験プログラム(3)
// アサーション
#include <iostream.h>
#include <assert.h>

int main()
{
    set_new_handler(0); // newが失敗するとヌルポインタを返す
    int *ptr, n;

    cout << "割り当てるメモリサイズを入力してください:" << endl;       
    cin >> n;
    ptr = new int[n];
    assert(ptr != 0); // アサーション

    cout << "メモリサイズは: " << n * sizeof(int)
         << " バイトです。" << endl;


    delete [] ptr;
    cout << "メモリを解放しました。" << endl;

    return 0;
}
[実行結果]                            ソースファイル  except2.cpp

C:\Source>except2                                             
割り当てる配列のサイズを入力してください:                    
1234567890                                                    

Assertion failed: ptr != 0, file except2.cpp, line 15         

Abnormal program termination 
診断結果に注目しましょう。また前のif文を使ったサンプル except1.cpp と比べる と、条件が反対であることに注意しましょう。つまり assert(ptr != 0); // アサーション では成り立つべき条件、すなわち「ヌルポインタでない」ことが保証されているかをテ ストします。つまりアサーションとは、プログラムの正しさを assert(主張する)と いうことです。こちらの方が、if文を使うより簡単です。1行ですみます。

3. newの例外処理をどのように行うべきか  ここで示したサンプルは、大変簡単な例外処理を行っています。つまりメモリ領域の 確保に失敗すると直ちにプログラムを終了させます。配列を作ることができなければ、 それ以上実行するわけにはゆきません。あまり上品なやり方ではありませんが、妥当な 方法と言えます。  これは例外処理を何もしないときの、処理系が用意したデフォルトの動作と同じです。 catchブロックを用意していない場合は、new演算子が失敗すると例外は捕捉されませ ん。その場合は関数 terminate() が呼び出され、そしてこの関数はデフォルトでは、 関数 abort() を呼び出します。  つまりnewが例外を送出するコンパイラを使っている場合、何もしなくても最低限の エラー処理(プログラムの終了)は行ってくれます。この場合エラーメッセージを表示 できないだけです。  この意味では、次のようなコードを書くことも悪くはないでしょう。 int *ptr = new int[n]; assert(ptr != 0); 2行目のコードは、newが例外を送出するコンパイラを使っている場合は、実行される (アサーションが偽になる)ことはありません。しかし、ヌルポインタを返すコンパイ ラを使う場合の用心のために不要なコードとはいえません。例えば、Visual C++ のコ マンドライン・コンパイラはヌルポインタを返すようになっています。ヌルポインタを 返す場合はエラー処理を明示的に行ってプログラムを終了させる必要があります。  では、2次元配列の場合はどうでしょうか。「2.8 動的配列」で扱った最後のサンプ ルプログラム matrix07.cpp をもう一度考えましょう。  行列を構成するために(行数+1)個の動的配列を作成します。このときnew演算子 が失敗すると、既に領域を確保した配列があります(最初に失敗しなければ)。このよ うなときは例外処理でアボートする前に、既存の領域を解放しておくべきでしょう。  次のプログラムは、領域確保に失敗した配列を識別するために変数countを用意して、 これを使って、それまでに作成した領域を解放します。
// matrix08.cpp
//行列の例外処理
//例外が発生した場合、既に作成した領域を解放する

#include <iostream.h>

int main()
{
    int row, col; // 行と列の大きさ
    cout << "行の大きさを入力してください: ";
    cin  >>  row;
    cout << "列の大きさを入力してください: ";
    cin  >> col;

    int i,j;
    // 領域の動的確保
    int **mat; // ポインタを指すポインタ

	int count = -1; // 例外が発生した場所を識別する
    try{
        mat = new int*[row];        // 行を作る
        for(i = 0; i < row; i++){   // 列を作る
            count = i;
            mat[i] = new int[col];
        }
    }
    catch (bad_alloc){

        // ここにエラー処理コードを書く
        cout << "メモリ割り当てに失敗しました。\n";

        if(count >= 0){ // 既に作成した領域を解放する
            cout << count + 1 << " 行ベクトル\n";
            while(--count >= 0) delete [] mat[count];
            delete [] mat;
        }
        else
            cout << "行のポインタ配列\n";

        abort(); // 異常終了させる
    }

    cout << row << " 行 " << col << " 列の行列を作成しました。";

    //領域の解放
    for(i = 0; i < row; i++)
        delete [] mat[i];   // 列を解放
    delete [] mat;          // 行を解放

    return 0;
}
[実行結果]                                 ソースファイル  matrix08.cpp
C:\Source>matrix08 
行の大きさを入力してください: 123 
列の大きさを入力してください: 1234567 
101 行ベクトルでメモリ割り当てに失敗しました。              

Abnormal program termination 
これで、new演算子で確保した領域はdelete演算子で確実に解放して終了させること ができます。  しかし、よく考えるとこれでも完全ではないことに気付きます。現在作成中の2次元 配列については、これで処理できますが、プログラムの中で別の配列領域を既に作成し ていた場合はどうすることもできません。  関数ライブラリとして行列作成を提供する場合は、アボートして強制的に終了させる ことが次善の策といえます。

4. 例外処理で関数を呼び出す方法  例外処理は、3つのキーワードtrythrowcatchを使います。throwは送出する 例外を決めるキーワードです。次の例は、例外が発生したときにデフォルトの動作では なく、ユーザ定義の関数 out_of_memory() を呼び出します。そしてこの関数で例外の 型を throw(投げる)ようにします。
// except3.cpp
// new演算子が失敗したときの動作を定義する
#include <iostream.h>

void out_of_memory()
{
    cerr << "メモリ割り当てに失敗しました。" << endl;
    throw bad_alloc();
}

int main()
{
    set_new_handler(out_of_memory);

    int *ptr, n;

    cout << "割り当てるメモリサイズを入力してください:" << endl;  
    cin >> n;
    ptr = new int[n];

    cout << "メモリサイズは: " << n * sizeof(int)
         << " バイトです。" << endl;


    delete [] ptr;
    cout << "メモリを解放しました。" << endl;

    return 0;
}
[実行結果]                                 ソースファイル except3.cpp

C:\Source>except3 
割り当てる配列サイズを入力してください:                      
1234567890 
メモリ割り当てに失敗しました。 

Abnormal program termination 
tryブロックの中で、問題が発見されると関数 set_new_handler()で次のように set_new_handler(out_of_memory); でインストールされた関数 out_of_memory()が呼び出されます。C/C++ では関数の引 数に関数を渡す場合、関数名はその関数コードを指すポインタとして扱われます。  呼び出された関数は、エラーメッセージを表示して、bad_alloc 例外を送出します。 throwで投げた例外 bad_alloc は catch(bad_alloc) ブロックが見つかれば、そこ で捕捉されます。見つからなければ、関数 terminate() が呼び出さて終了します。

| 目次 | 戻る |

Copyright(c) 1999 Yamada,K