2.5 2次元配列とポインタ  (1999/03/28初版)

2.5.1 2次元配列をポインタ形式で扱う  2次元配列を考えます。はじめに調べたように2次元配列は配列の配列、つまり行ベク トル(横ベクトル)を要素にした配列でした。4×5の整数型2次元配列の場合、 int a[4][5]; では、各要素はそれぞれ要素数5(列の数)の配列 a[0], a[1]. a[2],a[3]となります。 メモリ配置を図に描くと次のようになります。
2次元配列のメモリ配置 添字が1つだけの a[i]は配列名を意味します(惑わされないでください)。従って各行 の先頭アドレスです。すると、配列を次のようにポインタのように書き表すことができま す。 a[i][j] は *( a[i] + j ) と同じ。 要素a[i][j]は、その行の先頭アドレスa[i]からjだけ後ろの位置にあります。そしてア ドレス (a[i] + j)の内容は間接参照演算、*(a[i]+j)で参照できるからです。さらに配 列a[i]は、*( a + i )と書けるので、次のような等号(同一性)が成り立ちます。 a[i][j] == *( a[i] + j ) == *( *(a+i) + j ), 以上をプログラムで確かめましょう。
// matrix03.cpp
// 4×5行列を2次元配列で表す
// 配列要素をポインタ形式に扱う

#include <iostream.h>

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

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


    cout << "\n行列の表示(1)\n";
    int i,j;
    for(i = 0; i < ROW; i++){
        for(j = 0; j < COL; j++)
            cout << " " << *( mat[i] + j); // mat[i][j]と同じ

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

    cout << "\n行列の表示(2)\n";
    for(i = 0; i < ROW; i++){
        for(j = 0; j < COL; j++)
            cout << " " << *( *( mat + i ) + j); // mat[i][j]と同じ

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

    return 0;
}

ソースファイル matrix03.cpp
[実行結果]
行列の表示(1)                 
 11 12 13 14 15 
 21 22 23 24 25 
 31 32 33 34 35 
 41 42 43 44 45 

行列の表示(2) 
 11 12 13 14 15 
 21 22 23 24 25 
 31 32 33 34 35 
 41 42 43 44 45 

2.5.2 2次元配列を指すポインタ(配列へのポインタ)  この2次元配列を指すポインタはどのような形になるでしょうか。このテーマはポイン タの学習で、もっとも難しくて悩ましい部類に入ります。でもそれを理解するための準備 は整っています。ここで何をやりたいのかを明確にしておきましょう。あるポインタ ptr が2次元配列を指しているときに、これを2次元配列とまったく同じように使いたい。配 列の要素をptr[i][j]という風に使うためには、ptrをどのように定義すればよいのかとい うことです。  いくつかの方法があります。その1つは、配列全体を指すポインタを定義することです。 それは次のように宣言します。 int (*ptr)[5]; これは、ptrが“要素数が5の整数型の配列”へのポインタであることを意味します。ポ インタptrは宣言(定義)で指定した大きさ(ここでは5)の配列全体を指します。これ を「配列へのポインタ」と呼びます。  この宣言で、括弧()は必要です。()がないとこれは別物の「ポインタの配列」(す ぐ後で現れます)になります。  配列へのポインタを使うと2次元配列が扱えます。 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)[COL]; // 配列へのポインタ ptr = a; // 行列の最初の行ベクトル(配列)を指す ここで、ptrの値は2次元配列の先頭つまり要素数5の整数配列a[0]の先頭アドレスで、 a[0]全体を指しています。図式的に書くと次のようになります。 ptr --> { 11, 12, 13, 14, 15 } このポインタの型は 'int (*)[5]'型です。  ポインタ演算 ptr + 1 は(int型のサイズ)×5=4×5=20バイト後ろのアドレス 値で、( ptr + 1 )が指しているのは2番目の配列{21, 22, 23, 24, 25 }全体ですか ら、*( ptr + 1 )は ptr[1]と同じで、それは配列a[1]を表します。  さて、ptr[1]は2行目の横並びの配列を意味します。ptr[1] + 3は行の先頭アドレス ptr[1]からint型の要素3個分先のアドレスになるので、値24が格納されている要素の アドレスとなります。  別の言い方をすると、ptr[1]は要素数5の整数型の配列です。従って、配列の要素は添 字演算ptr[1][3]で参照できます。これは、a[1][3]です。もしこれが理解しづらければ、 配列ptr[1]を別のシンボルXと置き換えて考えるとよいです。Xは配列で、その4番目の 要素はX[3]、これを元のシンボルに置き戻すと ptr[1][3]ということになります。  従って、*( ptr + i )はptr[i]と同じで、さらに配列a[i]と同じです。同様に要素 a[i][j]は *(ptr[i] + j)、および ptr[i][j]と同一です。  このように2次元配列を「配列へのポインタ」で指すことで、ポインタを元の配列と同 じように添字操作することができます。その仕組みは、ポインタは2次元配列を行単位で 指しているので、ポインタは各行の区切り場所の情報を持っているからです。1次元配列 (ベクトル)と2次元配列(行列)の対応をまとめると、 [1次元配列(ベクトル)] int vec[7];   は “int型”の配列である。 これに対応するポインタは、“int型”へのポインタ int *pV = vec;  であって、pV[i]でベクトルの要素を添字操作できる。   (ポインタpVはベクトルの要素がint型であることを知っているが、    ベクトルのサイズ7は知らない) [2次元配列(行列)] int mat[5][7];   は “int型の要素数が7の配列”の配列である。  これに対応するポインタは“int型の要素数が7の配列”へのポインタ int (*pM)[7] = mat;  であって、pM[i][j]で行列の要素を添字操作できる。   (ポインタpMは行列の要素が行ベクトル(横ベクトル)であることを知っているが、    行列の行のサイズ5は知らない) となります。この意味で、配列へのポインタは2次元配列と素直に対応します。

2.5.3 2次元配列を指すポインタ(ポインタの配列)  もう1つのアイデアは、「ポインタの配列」を使う方法です。2次元配列をポインタで 添字操作するためには、行の区切りの情報が必要です。そのために各行の先頭を指すポイ ンタ変数を行の数だけ用意して、この集合を配列として扱います。つまりポインタの配列 を考えます。 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[1], ptr[2], ptr[3]はint 型へのポインタです。つまり各要素 ptr[i]の型は (int *)型のポインタです。  2次元配列の各行の先頭要素のアドレスをセットすることで2次元配列を指すことがで きます。 for(int i = 0; i < ROW; i++) ptr[i] = a[i]; //各行(1次元配列)の先頭アドレスをセット ptr[i]はa[i]の先頭要素a[i][0]のアドレスを指しています。つまり指しているのはint 型のデータです。  たとえばptr[2]は2次元配列の3行目の先頭アドレスですから、ptr[2]+3 は3行目の 先頭アドレスからint型の要素3個分先のアドレスになるので、*( ptr[2] + 3 ) は ptr[2][3]と書けます。これは a[2][3]と同じ中身の値34です。従って、ptr[i][j]と a[i][j]は同一です。  前の「配列へのポインタ」と、ここでの「ポインタの配列」との違いに注意しましょう。 配列へのポインタの定義は、 型名 (*ポインタ名)[ 要素数(列数) ]; で()が必要です。一方ポインタの配列は、 型名 *ポインタ名 [ 要素数(行数) ]; となります。「配列へのポインタ」は配列全体を指す変数で、メモリ内にはアドレスを記 憶する領域が1つだけ確保されます。「ポインタの配列」は、ポインタ配列の要素数(こ の場合は行の数)分の領域が確保されます。  もっとも大きな違いは、「配列へのポインタ」が2次元配列を指すとき列の数は固定さ れていますが、「ポインタの配列」は列の数(行ベクトルのサイズ)が固定されている必 要がありません。次のように不揃いな要素(行ベクトル)を指すことができます。 int *ptr[3]; // ポインタ配列 ptr[0] ---> {1, 2, 3, 4, 5, 6} ptr[1] ---> {7 } ptr[2] ---> {8, 9 } これは、文字列の処理で使われます。  次のプログラムは、「配列へのポインタ」と「ポインタの配列」の使い方を示していま す。
// matrix04.cpp
//4×5行列を2元配列で表す
// 行列へのポインタ

#include <iostream.h>

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

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


    cout << "もとの行列 mat[4][5]:2元配列(配列の配列)\n";
    cout << "サイズ: " << sizeof(mat) << endl;
    int i,j;
    for(i = 0; i < ROW; i++){
        for(j = 0; j < COL; j++)
            cout << " " << mat[i][j];
        cout << endl;	//行の終わりに改行
    }

    // 配列へのポインタ
    int (*ptr1)[COL];
    ptr1 = &mat[0];    //行列の先頭アドレスで初期化

    cout << "\nポインタ (*ptr1)[5]:配列へのポインタ\n";
    cout << "ポインタのサイズ: sizeof(ptr1) = "
         << sizeof(ptr1) <<  endl;
    cout << "ポインタが指しているサイズ: sizeof(*ptr1) = "
         << sizeof(*ptr1) <<  endl;
    for(i = 0; i < ROW; i++){
        for(j = 0; j < COL; j++)
            cout << " " << ptr1[i][j];
        cout << endl;    //行の終わりに改行
    }

    // ポインタ変数の配列
    int *ptr2[ROW];
    for(i = 0; i < ROW; i++)
        ptr2[i] = &mat[i][0]; //行の先頭アドレスで初期化

    cout << "\nポインタ *ptr2[4]:ポインタ変数の配列\n";
    cout << "配列ポインタのサイズ: sizeof(ptr2) = "
         << sizeof(ptr2) << endl;
    cout << "要素ポインタのサイズ: sizeof(ptr2[0]) = "
         << sizeof(ptr2[0]) <<  endl;
    cout << "要素ポインタが指しているサイズ: sizeof(*ptr2[0]) = "
         << sizeof(*ptr2[0]) <<  endl;
    for(i = 0; i < ROW; i++){
        for(j = 0; j < COL; j++)
            cout << " " << ptr2[i][j];
        cout << endl; //行の終わりに改行
    }

    return 0;
}

ソースファイル matrix04.cpp
[実行結果]
もとの行列 mat[4][5]:2元配列(配列の配列)         
サイズ: 80 
 11 12 13 14 15 
 21 22 23 24 25 
 31 32 33 34 35 
 41 42 43 44 45 

ポインタ (*ptr1)[5]:配列へのポインタ 
ポインタのサイズ: sizeof(ptr1) = 4 
ポインタが指しているサイズ: sizeof(*ptr1) = 20        
 11 12 13 14 15 
 21 22 23 24 25 
 31 32 33 34 35 
 41 42 43 44 45 

ポインタ *ptr2[4]:ポインタ変数の配列 
配列ポインタのサイズ: sizeof(ptr2) = 16 
要素ポインタのサイズ: sizeof(ptr2[0]) = 4 
要素ポインタが指しているサイズ: sizeof(*ptr2[0]) = 4   
 11 12 13 14 15 
 21 22 23 24 25 
 31 32 33 34 35 
 41 42 43 44 45 
繰り返しになりますが、それぞれのポインタのサイズ(メモリ領域の大きさ)と、ポイン タが指しているサイズ(データのサイズ)の違いに注意しましょう。 【演習問題 2.6】上のプログラムを、double型のデータ型に書き換えなさい。  ポインタのサイズはいくらになるか考え、実行結果を確かめなさい。 【演習問題 2.7】上のプログラムで、配列へのポインタ ptr1を次のように初期化すると  エラーになります。理由を考えなさい。 int (*ptr1)[COL] = &mat[0][0]; // 先頭要素のアドレスで初期化

2.5.4 補足:2次元配列を(int *)型のポインタで指す  整数型の2次元配列は、メモリ内でリニアに並んでいるわけですから、これを1次元配 列と見なして(int *)型のポインタで指すこともできるはずです。次のプログラムを見て ください。
// lst02_06.cpp
//4×5行列を2元配列で表す
// (int *)型のポインタを使う

#include <iostream.h>

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

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


    int *ptr;
    ptr = &mat[0][0];  //先頭要素のアドレスで初期化

    for(int i = 0; i < ROW; i++){
        for(int j = 0; j < COL; j++)
            cout << " " << ptr[ i * COL + j];
        cout << endl; //行の終わりに改行
    }

    return 0;
}

ソースファイル lst02_06.cpp
[実行結果]
 11 12 13 14 15                   
 21 22 23 24 25                   
 31 32 33 34 35                   
 41 42 43 44 45
ポインタは整数型へのポインタで、行の区切りに関する情報を持ちません。そのために 各要素を参照するためには、 ptr[ i * COL + j ] または、*( ptr + i * COL + j ) と、行と列の位置を計算して与える必要があります。あまり賢い方法ではありません。

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

Copyright(c) 1999 Yamada,K