2.8 動的配列  (1999/04/10 初版)

2.8.1 汎用的な関数  前のページで見たように、2次元配列を関数でやり取りするには、少なくとも配列 の後ろの添字サイズを指定する必要があります。例えば2次元配列 int mat[10][20]; を関数に渡すには、次のようなプロトタイプ void func(int (*a)[20], int row, int col); で宣言される関数が定義されている必要があります。関数は大きさが20の配列を指 すポインタaで受け取ります。  しかし、これでは関数に汎用性がありません。違うサイズの配列をこの関数に渡す ことができないからです。ここでは任意のサイズの2次元配列を扱える汎用的な関数 をつくります。

2.8.2 準備:ポインタを指すポインタ  まず準備として、これまでの知識を使った演習問題を考えましょう。上述の関数 func()で「int (*)[20]」と宣言されるポインタの代わりに、配列の大きさを指定 しなくても良いポインタを考えます。  ポインタ自体は変数の仲間の一つです。ですからポインタを指すポインタも定義す ることができます。ポインタを指すポインタは2つのアスタリスク(*)で宣言しま す。 int i = 10; int *ptr = &i; // intを指すポインタ int **pp = &ptr; // 「intへのポインタ」を指すポインタ ポインタppは整数型の変数 iを指すポインタ ptrのアドレスで初期化しています。つ まり ptrを指しています。これを図で描くと、 pp ----> ptr ----> i という関係になります。ppはポインタ ptrのアドレスを値に持つ変数です。すると、 間接参照 *ppは ptrの内容すなわち iのアドレスです。また*ptrは iの内容10なの で、**ppはiの内容10です( *ptr == *(*pp) == **pp と変形できますね)。従っ て2つの間接参照演算**で、変数 iを参照できます。  これを次のような変形を行って添字演算子で表現すると、 **pp == *pp[0] == pp[0][0] で変数 iを参照できることになります。これは整数型変数 iをあたかも1行1列の2 次元配列のように表現できることを示します。  そこで「2.5 2次元配列とポインタ」で扱った2次元配列を指すポインタの1つ、 ポインタの配列を思い出しましょう。これは、 const int ROW = 4; //行の次元 const int COL = 5; //列の次元 int a[ROW][COL] = { {11,12,13,14,15}, {21,22,23,24,25}, {31,32,33,34,35}, {41,42,43,44,45} }; という2次元配列に対して、 int *ptr[ROW]; // ポインタの配列(ポインタは行の個数分ある) で表されるものでした。この行の個数分のポインタ配列 ptr[0],...,ptr[ROW-1]を 指すポインタppを int **pp = &ptr[0]; と、ポインタ配列の先頭要素ptr[0]を指すように初期化します。このときポインタ配 列が行列aを指すように設定されているとして、次のような関係にあります。 ポインタを指  ポインタ    配列 すポインタ pp ----> ptr[0] ---> {11,12,13,14,15} (pp+1) ----> ptr[1] ---> {21,22,23,24,25} (pp+2) ----> ptr[2] ---> {31,32,33,34,35} (pp+3) ----> ptr[3] ---> {41,42,43,44,45} つまりポインタppについてのポインタ演算は、ポインタ配列の要素を指します。これら を間接参照するとpp[i]はptr[i]の値と同一で、さらにpp[i][j]は配列要素a[i][j]の 値を意味します。次のサンプルプログラムを見てください。
// lst02_12.cpp
// ポインタを指すポインタ変数
#include <iostream.h>

void print(int **, int, int); // 関数プロトタイプ宣言

int main()
{
    const int ROW = 4;  //行の次元
    const int COL = 5;  //列の次元

    int mat[ROW][COL] = { {11,12,13,14,15},
                          {21,22,23,24,25},
                          {31,32,33,34,35},
                          {41,42,43,44,45} };

    // ポインタ変数の配列
    int *ptr[ROW];
    for(int i = 0; i < ROW; i++)
        ptr[i] = mat[i];    //行の先頭アドレスを指す

    // ポインタを指すポインタ
    int **pp = &ptr[0];    // ポインタ配列の先頭を指す

    print( pp, ROW, COL ); // 関数呼び出し

    return 0;
}

// 配列要素をプリントする関数の定義
void print(int **a, int row, int col)
{
    for(int i = 0; i < row; i++){
        for(int j = 0; j < col; j++)
            cout << " " << a[i][j];

        cout << endl;  //行の終わりに改行
    }
}

ソースファイル lst02_12.cpp
[実行結果]
 11 12 13 14 15                        
 21 22 23 24 25                        
 31 32 33 34 35                        
 41 42 43 44 45
この様にポインタを指すポインタ変数を使って、配列を指すことができます。注意す べきことは、間接的に配列を指していることです。配列を直接指しているのは別のポ インタであること、しかも配列構造をしたポインタであることです。ppが指している のはポインタptr[0]ですから、ppのアドレスの足し算 (pp+1), (PP+2),..はポイン タの配列、ptr[1],ptr[2],.. のアドレスを移動する演算だからです。この様に、 2段階で設定する必要があります。

2.8.3 動的メモリ割り当て  いよいよ本題です。C/C++ではデータを格納するメモリ領域を、必要なときに確保し 不要なときに解放するといった、メモリ割り当てを動的に行うことができます。  動的とは実行時にメモリ領域を割り当てるということです。これまでの変数は宣言 によってメモリ領域が割り当てられていました。これらはすべてコンパイルのときに 決まっています。例えば配列は、定数を使ってそのサイズを宣言しなければなりませ んでした。そのため実行時に必要な大きさを標準入力で与えて配列を使用するという 様なことはできません。しかし、動的にメモリ割り当てを行うことで可能になります。  標準Cのライブラリ関数 malloc() がこの機能を備えています。これは未使用のメ モリ(ヒープメモリといいます)に指定した大きさのメモリ領域を確保して、ポイン タを返します。関数malloc()はヘッダファイル <stdlib.h> (一般ユーティリティ・ ライブラリ)の中で次のように定義されています。 void *malloc(size_t size); これは大きさが size バイトのメモリ領域を確保し、そのポインタを返します。戻り 値型 (void *) は総称ポインタで、あらゆる型を指すことができます。また、メモリ 領域の確保に失敗するとヌルポインタ(0あるいは NULL)を返します。  大きさが100の整数型配列を動的配列として確保するには、 int n = 100; // 配列の大きさ int *ptr = (int *)malloc( n * sizeof(int)); if(ptr == 0){ cout << "メモリ領域の確保に失敗しました。\n"; exit (1); } というコードを書きます。関数 malloc() は(int型のサイズ)×100バイトの領域 を確保して先頭アドレスをポインタ ptr に渡します。  これを見ると動的配列は、いわば名前の無い配列と言えます。名前の代わりにポイン タを使っています。これはポインタの強力な使い方の1つです。この様なことはポイン タ無しではできません(これまでの話ではポインタはどちらかといえば配列の代用に過 ぎませんでした)。  ここで配列の大きさを変数でも指定できることに注目しましょう(配列では定数でな いとダメでした)。sizeof(int)を使うのは、int型は処理系によっては2バイトや4 バイトの場合があるからです。バイト数を数値で直接与えることもできますが、この方 が確実でプログラムの移植性を良くします。  またC++ではポインタを受け取る際に(void *)型から(int *)型へキャスト(型変 換)する必要があります。一方、標準Cでは要りません。  3行目のif文は戻り値をチェックしてエラー処理を行います。もし未使用のメモリが 足らないときは領域確保に失敗します。そのときはエラー・メッセージを表示してプロ グラムを終了させます。  関数malloc()が作成したメモリ領域を解放するには、関数free()を使います。 free(ptr); ptr = 0; // こうすると解放されたメモリをうっかり使用することを防げる 関数free()はポインタが指す領域を解放します。解放された領域は未使用のメモリとし て別のプログラムから使用できるようになります。free()はポインタ変数を削除するの でなく、それが指している領域であることに注意しましょう。  関数malloc()とfree()は必ずペアで使います。もしfree()を忘れると、メモリ領域 は未使用メモリに返されずにそのまま残ります。このことが繰り返されると、使用可能 なメモリが使い尽くされてしまいます(メモリリークといいます)。また解放されたメ モリ領域をポインタで使用すると、別のプログラムで使われているメモリを使用してし まうことになります。これらは、予期しない結果やプログラムのクラッシュを招きます。 そのためエラー処理は大切です。 【演習問題 2.11】ポインタを指すポインタを使って、2次元配列を動的に作成する  プログラムを考えてみなさい。もし難しければ、以下を読んだ後、再び挑戦してみ  てください。( 回答プログラム ex02_11.cpp

2.8.4 new演算子とdelete演算子  C++にはメモリ領域を動的に割り当てる機能として、new演算子とdelete演算子が あります。new演算子で領域を割り当て、delete演算子でそれを解放します。これら は関数 malloc() と free() の組み合わせよりも、優れた機能を持ちます。その違い は、malloc()-free() は関数ですが、new-deleteの方はC++言語機能に組み込まれ た演算子であることです。  関数 malloc()はデータの領域を作成するだけですが、new演算子はデータ型の領域 を作成するときに初期化も行います。new演算子は、 double *ptr; ptr = new double; の様にnewの後にデータ型を指定することで、そのデータ型分のメモリ領域を割り当て てポインタ(アドレス値)を返します。この形式を見ると分かるように、new演算子は 関数 malloc()と違って sizeof()演算子を使う必要がありません。  このとき次のように、 ptr = new double(1.2345); と初期値を与えることで、ptrが指すdouble型の領域が値1.2345で初期化されます。  この様な基本型を動的に割り当てることは意味がありませんが、後のテーマで取りあ げるユーザー定義型のクラス・オブジェクトを作成するときに強力な働きをします。 また、new演算子はクラスのコンストラクタを呼び出し、delete演算子はデストラク タを呼び出します。  動的に割り当てたメモリ領域を解放するには、次のようにdelete演算子でポインタ ptrが指すメモリ領域を解放します。 delete ptr;  一方配列領域を作成するには、データ型の後に括弧[]を使って大きさを指定します。 ptr = new double[10]; で大きさ10のdouble型の配列領域が作成されます。これを解放するときは次のよう にします。 delete [] ptr; []の位置に注意しましょう。new演算子で配列のメモリ領域を作成したときはdelete 演算子に添字演算子[]が必要です。  もしこれをただのdeleteで解放した場合、ptr が指している配列の最初の要素のメ モリ領域だけが解放され、残りの部分は解放されません。 [注]new演算子とdelete演算子は多重定義されています。単一のデータ型に対しては、 演算子new()とdelete()を使います。配列には演算子new[]()とdelete[]()を使い ます。つまり、配列に対してdeleteを呼び出すと、配列であることを認識してくれま せん。  以下では、このnew演算子とdelete演算子を使って、2次元配列を動的に作成しま す。int型へのポインタを指すポインタppを考え、ppが指すメモリ領域の配置を下の 図の様に割り当てます。
行列の動的メモリ割り当て 各データ(整数値またはアドレス)のメモリ領域を四角形で表しています。隣接してい る四角形はメモリ領域が連続して配置していることを意味します。ppが指すメモリ領域 を2段階でつくります。まず行を表すポインタ配列の領域を作成し、次に各行ごとに行 ベクトル(横ベクトル)の領域をつくります。 int **pp; pp = new int*[ROW]; // 行を作る for(int i = 0; i < ROW; i++) // 列を作る pp[i] = new int[COL]; 2行目でnew演算子は行の個数分のポインタ配列のメモリ領域を作成し、その先頭アドレ スを返します。  それから3行目のfor文で、各行のポインタが指す、大きさが列の数の配列(行ベクト ル)のデータ領域を作成します。これで ポインタを2次元配列のようにpp[i][j]で操作 することができます。  そしてこれを解放するときは、逆の順に解放してゆきます。 for(int i = 0; i < ROW; i++) delete [] pp[i]; // 列を解放 delete [] pp; // 行を解放  次の例は2次元配列を動的に作成し、それを関数に渡します。
// matrix06.cpp
//行列を動的に作成して関数に渡す
#include <iostream.h>


void print(int **, int, int);

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

    int i,j;
    // 領域の動的確保
    int **mat = new int*[row];  // 行を作る      
    for(i = 0; i < row; i++)    // 列を作る
        mat[i] = new int[col];

    // 行列の値を設定
    for(i = 0; i < row; i++)
        for(j = 0; j < col; j++)
            mat[i][j] = 10*(i+1) + (j + 1);

    print(mat, row, col);

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

    return 0;
}

void print(int **a, int row, int col)
{
    cout << "行列の表示\n";

    for(int i = 0; i < row; i++){
        for(int j = 0; j < col; j++)
            cout << " " << a[i][j];

        cout << endl;  //行の終わりに改行
    }
}

ソースファイル matrix06.cpp
[実行結果]
行の大きさを入力してください: 5              
列の大きさを入力してください: 6 
行列の表示 
 11 12 13 14 15 16 
 21 22 23 24 25 26 
 31 32 33 34 35 36 
 41 42 43 44 45 46 
 51 52 53 54 55 56
行列の大きさをキーボードから入力し、そのサイズで2次元配列を動的に作成します。 このプログラムでは、以前の固定サイズの配列は姿を消し、ポインタが主役となってい ます。さらにポインタはあたかも配列のように振る舞います。  ここまでくると、プログラムを機能ごとに分割し汎用関数の集まりとして仕立て上げ ることは容易でしょう。


2.8.5 例外処理  new演算子がメモリ領域の確保に失敗するとどうなるでしょう。以前のC++の仕様で はヌルポインタ(0またはNULL)を返しましたが、現在の標準C++(ANSI/ISO C++) は例外処理を行うようになっています。  例外とは予想外の例外的な状況(エラー条件)をいいます。実行時に起こる0(ゼロ) による割り算などがその例です。例外処理はこの様な例外的な状況を検出し実際にエラ ーが発生する前に適切な処理(例えばエラー条件から復帰してプログラムの実行が可能 となるようにする)を行います。  標準C++の規格ではnew演算子が失敗すると、bad_alloc 例外(エラーの識別子)が 送出されます。  次のサンプルコードは、2次元配列の領域確保に簡単な例外処理を追加したものです。 上のプログラム matrix06.cpp の領域確保の部分をこのコードで差し替えたプログラム は、Borland C++ Builder 3、4 のコマンドライン・コンパイラ bcc32.exeでコンパイ ルできます。
// サンプル・プログラム matrix07.cpp の一部 

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

    try{ // 例外を起こしそうなコードをこのブロックに入れる     
        mat = new int*[row];        // 行を作る 
        for(i = 0; i < row; i++)    // 列を作る 
            mat[i] = new int[col]; 
    } 
    catch (bad_alloc){ // 例外をここで受け取る 

        // ここにエラー処理コードを書く 
        cout << "メモリ割り当てに失敗しました。\n"; 
        abort(); // 終了させる 
    }

ソースファイル matrix07.cpp
 ここで行っている例外処理は2つのキーワードtry(例外のテスト)とcatch(例外 の捕捉)からなる2種類のプロックで構成されます。  例外を起こす可能性のあるコードをtryブロックの括弧{}の中に入れます。そして、 tryブロックの直後に1つ以上のcatchブロックを置きます。各catchブロックには、 捕捉する例外の型を指定します。  tryブロックの中で例外が発生すると、制御はcatchブロックに移ります。発生した 例外がcatchブロックの1つのパラメータの型と一致すれば、その catchブロックの コードが実行されます。逆に例外が発生しないときは、catchブロックは無視され実行 されません。  また一致するcatchブロックが無いときは、関数 terminate() が呼び出されます。 この関数はデフォルトでは、関数 abort() を呼び出します。abort() はプログラムを 直ちに終了させます。すなわちプログラムをアボート(流産)させる関数です。  サンプルコードでは、newが失敗すると例外 bad_alloc が捕捉され、エラーメッセ ージを表示して、関数 abort() でプログラムを終了させます。 (注)関数 abort() はC標準ライブラリのヘッダファイル stdlib.h 内で定義されて います。  new演算子のエラー処理の扱いはコンパイラに依存します。古いコンパイラではヌル ポインターを返すかもしれません。また例外を送出する場合でも、コンパイラ独自の方 法を採っている場合もあります。  例えば C++ Builderの以前のバージョン Borland C++ 5.0 では、標準C++の規格 である bad_alloc 例外ではなく、xalloc 例外を送出します。  また、Microsoft Visual C++ (ver 6.0) のコマンドライン・コンパイラではデフ ォルトでは、ヌルポインタを返します。  少し詳しいことは、次の補足ページ  new演算子の例外処理の実験 をご覧ください。

| 目次 | 前のページ | 次のページ |

Copyright(c) 1999 Yamada,K