ポインタ⑥(データ構造の構築) | Programming Place Plus C言語編 第36章

トップページC言語編

このページの概要

以下は目次です。


ポインタへのポインタ

ポインタが保持している情報はメモリアドレスです。また、ポインタ変数自身はメモリ上に置かれています。ということは、ポインタ変数を使って、ほかのポインタ変数を指し示すことは可能なのではないでしょうか?

結論からいって、これは可能です。このような、ポインタがポインタを指し示しているような使い方を、ポインタへのポインタ (pointer to pointer) と呼ぶことがあります。

「ポインタのポインタ」と表現されることが多いのですが、「~への~」とした方がイメージしやすいのではないかと思います。

ポインタ型は、指し示す先の型に「*」を付けたものでした。ポインタへのポインタの場合は、指し示す先にあるオブジェクトの型は「int*」のようなポインタ型です。よって、「*」をもう1つ追加して、「int**」とします。

変数として、ポインタへのポインタを宣言するときには、次のように書きます。

int** 変数名;

「*」の個数をさらに増やせば、「ポインタへのポインタへのポインタ」のようなこともできます。できるのですが、現実問題、ポインタへのポインタぐらいですでに十分複雑なので、これ以上追加すると手に負えません。

ポインタへのポインタに与える初期値はどうなるでしょう? ポインタを指し示さなければならないので、たとえば、次のようになります。

int num  = 100;
int* p   = #  // 変数num を指すポインタ
int** pp = &p;    // ポインタ変数p を指すポインタへのポインタ

通常のポインタであろうと、ポインタへのポインタであろうと、格納できるものはメモリアドレスであることに変わりはありません。しかし、変数pp は、型が int* のポインタ変数を指し示さなければなりませんから、たとえば、変数num のメモリアドレスを格納することは不適切です。ところが、もし格納しようとしてもコンパイルエラーにはなりません。

int num  = 100;
int* p   = #  // OK
int** pp = #  // OK

これは、基本的には間違っている可能性が高い行為なので、Visual Studio でも clang でも警告は出ますが、コンパイルは通ってしまいます。自分が何をしようとしているのか、きちんと理解する必要があります。もし、自信を持って、このような初期化や代入が正しいと言えるのであれば、キャストとコメントによって意図を示すと良いでしょう。

int** pp = (int**)#  // このキャストは問題ない

また、ポインタへのポインタが、何も指していない状態は、やはりヌルポインタで表現できます。

int** pp = NULL;

多重間接参照

ポインタへのポインタを間接参照することを考えてみます。

ポインタへのポインタを間接参照すると、そこにあるのは、やはりポインタです。ということで、そのポインタをさらに間接参照すると、ようやく一番先にあるオブジェクトに行き着きます。間接参照を繰り返すのであれば、間接演算子(*) を重ねて書けばよいです。このような行為を、多重間接参照 (multiple indirection) と呼ぶことがあります。

#include <stdio.h>

int main(void)
{
    int num  = 100;
    int* p   = &num;
    int** pp = &p;

    printf("%p\n", pp);   // pp が保持しているメモリアドレス(p のメモリアドレス)
    printf("%p\n", *pp);  // pp が指し示す先にある p が保持しているメモリアドレス(num のメモリアドレス)
    printf("%d\n", **pp); // pp が指し示す先にある p がさらに指し示す先にある値 (num の値)
}

実行結果:

00FFFBE0
00FFFBEC
100

多重間接参照も、「*」をさらに重ねて3段階以上にできます。しかし、やはり複雑になり過ぎ、手に負えないのでやめておくのが無難です。

多重間接参照を使う際には、間接参照のレベル(階数)を意識して考えると理解しやすいでしょう。つまり、「int**」として宣言された、ポインタへのポインタがあるとすれば、これは「*」の個数から、2 というレベルを持っていると考えます。そして、アドレス演算子(&) が登場するたびにレベルが +1 され、間接演算子(*) が登場するたびに -1 されます。

レベルが 0 のときには、メモリアドレスを保持していない通常の変数です。レベルが 1 以上であれば、それはメモリアドレスを保持しており、ポインタであることが分かります。

#include <stdio.h>

int main(void)
{
    int num  = 100;         // どこも指し示していないので レベル0
    int* p   = &num;        // レベル0 の num に &演算子。レベルが +1 されて 1。あるいは int* なのでレベル 1
    int** pp = &p;          // レベル1 の p に &演算子。レベルが +1 されて 2。あるいは int** なのでレベル 2


    printf("%p\n", &pp);  // レベル2 の pp に対する &演算子。
                          // レベルが +1 されて 3。ポインタなので %p で出力

    printf("%p\n", pp);   // レベル2 の pp。
                          // ポインタなので %p で出力

    printf("%p\n", *pp);  // レベル2 の pp に対する間接演算子。
                          // レベルが -1 されて 1。まだポインタなので %p で出力

    printf("%d\n", **pp); // レベル2 の pp に対して2つの間接演算子。
                          // レベルが -2 されて 0。ポインタではないので %d で出力
}

実行結果:

006FF8A8
006FF8B4
006FF8C0
100


文字列へのポインタ

要素が文字列の配列を考えてみます。文字列を表現するには、char[] のような配列型とする方法と、char* のようなポインタ型とする方法とがありました(第32章)。ここではポインタによる方法を使ってみます。

const char* messages[] = {
    "OK!",
    "Error!",
    "Hello, World!",
    "Thank you!",
    "Good Bye!"
};

文字列リテラルを使って初期化を行うので、const型修飾子を付けて const char* で扱っています。要素の型が const char* の配列なので、変数名の後ろには [] があり、初期値も複数あります。初期値は5個ありますから、要素数は 5 ということになり、結果、messages の型は const char*[5] です。

さて、式の中で配列が現れたとき、暗黙的にポインタに変換されるのでした(第32章)。では、配列 messages の場合は、どんな型に変換されるのでしょうか? int[5] なら int* になるというルールですから、const char*[5] なら const char** です。

このように、ポインタで表現された文字列を要素とする配列を使うと、わりと簡単に思いがけず、ポインタへのポインタが登場します。ポインタへのポインタを使う機会として多いのは、この場面かもしれません。

実例として、この messages という配列の各要素を出力する関数を作るとしたら、次のようになります。

#include <stdio.h>

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

void print_message(const char** messages, size_t max);

int main(void)
{
    const char* messages[] = {
        "OK!",
        "Error!",
        "Hello, World!",
        "Thank you!",
        "Good Bye!"
    };


    print_message(messages, SIZE_OF_ARRAY(messages));
}

/*
    メッセージを一覧表示
    引数:
        messages:   表示するメッセージへのポインタを保持するポインタ配列。
        max:        messages に含まれる要素数。
*/
void print_message(const char** messages, size_t max)
{
    for (size_ t i = 0; i < max; ++i) {
        puts(messages[i]);
    }
}

実行結果:

OK!
Error!
Hello, World!
Thank you!
Good Bye!

print_message関数は、引数を const char*[5] ではなく、const char** として受け取ります。第33章で説明したように、関数の仮引数に限っては、[] を使っても * を使っても同じ意味ですから、次のようにしても問題ありません。

void print_message(const char* messages[], size_t max);

ここで、レベルの話を考えてみましょう。print_message関数の中では、「messages[i]」のようにして、要素の1つにアクセスしています。添字演算子の意味は、「*(messages + i)」のような表現と同じことでした(第32章)。ですから、添字演算子を使ったときには、間接演算子を使ったときと同様に、レベルが -1 されると考えられます。

messages は「const char**」なのでレベル 2。「messages[i]」とすると、-1 されてレベル 1 です。ですから、まだポインタ(const char*)であることが分かります。puts関数の仮引数もまた、const char*型ですから、そのまま渡せます。


多次元配列

これまでに登場した配列は、メモリ上に、要素が連続的に並んだものでした。これは図にすると、次のようになります(数字は添字です)。

配列のイメージ

ここで、各要素がそれぞれ配列だったらどうなるでしょうか? 図で書くと、次のようになると考えられます。

二次元配列のイメージ

横方向に要素が並んでいましたが、それぞれの要素から縦方向にも配列が伸びたという状態です。結果、表のような形になるとイメージできます。このような配列は、二次元配列 (two-dimensional array) と呼ばれます。これに対して、最初の配列は、一次元配列 (one-dimensional array) です。二次元以上になった配列を、多次元配列 (multi-dimensional array) と呼びます。

正確にいえば、C言語の多次元配列は、配列の配列 (array of array) と呼ぶほうが適切ですが、多次元配列と呼ばれることが多いので、今後も多次元配列と表現します。

二次元 “以上” と書いたように、三次元以上にすることが可能です。たとえば、三次元配列であれば、次のようなイメージになるでしょう。

三次元配列のイメージ

これもやはり、4次元、5次元・・・と増やすことが可能ですが、ほとんどの人は4次元以上をイメージすることができないはずです(現実世界が3次元ですから)。実際、まともなイメージ図は描けませんし、4次元以上の配列は避けるのが無難です。

多次元配列は、次のように宣言します。

int array[5][6];     // 二次元配列
int array[3][4][4];  // 三次元配列

次元が増えるたびに、要素を特定するための添字の個数も増えていきます。初期値を与える場合は、分かりやすさのために、次元ごとに { } で囲むように書きます。

int array[5][6] = {
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5}
};
int array[3][4][4] = {
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    }
};

実際には、{ } は1組だけあれば認識されますが、分かりづらくなるかもしれません。

int array[5][6] = {
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5,
    0, 1, 2, 3, 4, 5
};
int array[3][4][4] = {
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3,
    0, 1, 2, 3
};

また、要素数の指定を省略して、コンパイラに要素数を判断させることができるのは、一番左側の [] だけです。

int array[][6] = {
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5}
};
int array[][4][4] = {
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    },
    {
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3},
        {0, 1, 2, 3}
    }
};

多次元配列になっても、メモリ上で、要素が連続的に並ぶ性質は変わりません。

#include <stdio.h>

int array[5][6] = {
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5},
    {0, 1, 2, 3, 4, 5}
};

int main(void)
{
    for (int i = 0; i < 5; ++i) {
        for (int j = 0; j < 6; ++j) {
            printf("%p ", &array[i][j]);
        }
        printf("\n");
    }
}

実行結果:

00F68000 00F68004 00F68008 00F6800C 00F68010 00F68014
00F68018 00F6801C 00F68020 00F68024 00F68028 00F6802C
00F68030 00F68034 00F68038 00F6803C 00F68040 00F68044
00F68048 00F6804C 00F68050 00F68054 00F68058 00F6805C
00F68060 00F68064 00F68068 00F6806C 00F68070 00F68074

要素を順番にアクセスする必要がある場合には、できるだけ、メモリアドレスも連続するような順番で行う方が、処理効率が高まる可能性があります。ですから、二次元配列の全要素を順番にアクセスするときには、以下のように書くのではなく、

for (int i = 0; i < 6; ++i) {
    for (int j = 0; j < 5; ++j) {
        array[j][i] = i * j;
    }
}

以下のように書いた方が良いといえます。

for (int i = 0; i < 5; ++i) {
    for (int j = 0; j < 6; ++j) {
        array[i][j] = i * j;
    }
}

つまり、array[i][j][k] のように添字を指定する場合なら、後ろにある添字の方が速くカウントが進んでいき、手前側にある添字のカウントの方が遅れて進む方が良いということです。

二次元配列を関数に渡すことを考えてみます。

式の中であらわれた配列は、暗黙的にポインタに変換されるのでした。二次元配列ならどうなるでしょうか? たとえば、int[][] であれば、int** でなりそうに思えますが、そうはなりません。

暗黙的に変換できるのは、「配列」から「ポインタ」だけです。「配列の配列」から一気に「ポインタへのポインタ」に変換されることはないのです。したがって、int[][] は int[] を指すポインタに変換されます。

これはかなり分かりづらいので、二次元配列のイメージ図で考えてみます。

二次元配列のイメージ

図中の矢印は、「配列」を指し示すポインタを考えたときに、あり得そうな3種類の方法を表しています。つまり、以下の3通りです。

  1. 二次元配列全体を指すような矢印をイメージする
  2. 横方向(行方向)に、どこか1行を指す矢印をイメージする
  3. 縦方向(列方向)に、どこか1列を指す矢印をイメージする

配列の配列を、(一次元)配列を指すポインタに変換したときのイメージとして正しいのは2番です。

1番は、二次元配列全体を指しているので、int[] を指すポインタのイメージには合いません。3番は、要素のメモリアドレスが連続的に並ばないので、これでは配列ではなくなってしまいます。

さて、問題なのは、2番のイメージに沿ったポインタは、どんな型で表現されるのかという点です。「int[]*」でしょうか? それとも「int*[]」でしょうか? どちらでもなく、このイメージ図の場合ならば、「int (*)[6]」が正解です。

この謎めいた表現を理解する考え方はいろいろありますが、たとえばこう考えてみましょう。

先ほどの図で、2番の矢印は、二次元配列のどこか1つの行を、一次元配列とみなして、その配列を指しているのです。ここで、ポインタに対するインクリメントの挙動を考えてみます。

int型のポインタに対して ++演算子を適用すると、int1個分の大きさ(sizeof(int)) だけ先へ移動するのでした(第32章)。同じように考えると、1行分の配列を指しているポインタをインクリメントしたら、次の行へ移ると考えられます。つまり、「sizeof(int) * 1行に含まれている要素数」だけ加算される訳です。

この計算を実現するには、「1行に含まれている要素数」という情報をきちんと持っていないといけません。「int**」のような型になってしまったら、要素数が分かりません。「int (*)[6]」という型で、[6] の部分が、「1行に含まれている要素数(言い換えると列の個数)」です。

要するに、「int (*)[6]」は、「int型の要素を 6個持った一次元配列を指すポインタ型」と解釈すればよいということです

ここまでを踏まえて、実際のプログラムを見てみましょう。

#include <stdio.h>

#define ARRAY_COL_NUM    6      // 配列の列の数
#define ARRAY_ROW_NUM    5      // 配列の行の数

void print_array(const int (*array)[ARRAY_COL_NUM], int row, int col);

int main(void)
{
    int array[ARRAY_ROW_NUM][ARRAY_COL_NUM];


    // 全要素へ値を格納
    for (int i = 0 ; i < ARRAY_ROW_NUM; ++i) {
        for (int j = 0 ; j < ARRAY_COL_NUM; ++j) {
            array[i][j] = i * 10 + j;
        }
    }

    // 要素を出力
    print_array(array, ARRAY_ROW_NUM, ARRAY_COL_NUM);
}

/*
    二次元配列の要素を出力。
    引数:
        array:      二次元配列の先頭メモリアドレス。
        row:        列の数。
        col:        行の数。
*/
void print_array(const int (*array)[ARRAY_COL_NUM], int row, int col)
{
    for (int i = 0 ; i < row; ++i) {
        for (int j = 0; j < col; ++j) {
            printf("%02d ", array[i][j]);
        }
        printf("\n");
    }
}

実行結果:

00 01 02 03 04 05
10 11 12 13 14 15
20 21 22 23 24 25
30 31 32 33 34 35
40 41 42 43 44 45

print_array関数の仮引数に注目してください。「const int (*array)[ARRAY_COL_NUM]」という複雑な表現になっていますが、ARRAY_COL_NUM は、二次元配列の列の個数ですから、先ほどの説明どおりです。const は、関数内で書き換えないことを示すためのものです。

なお、( ) が必要であることに注意してください。( ) がないと、意味が変わります。

int (*array)[6];  // int型で要素数6 の配列 へのポインタ
int* array[6];    // int型へのポインタ の配列で要素数は 6


動的な二次元配列

今度は、動的に二次元配列を作ることを考えます。

動的な二次元配列を作るには、2つの考え方があります。1つには、1つの次元ごとに順番に形作っていく方法です。これは次のように書けます。

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_COL_NUM    6      // 配列の列の数
#define ARRAY_ROW_NUM    5      // 配列の行の数

int main(void)
{
    // 二次元配列を動的に確保
    int** array = calloc(ARRAY_ROW_NUM, sizeof(int*));
    for (int i = 0; i < ARRAY_ROW_NUM; ++i) {
        array[i] = calloc(ARRAY_COL_NUM, sizeof(int));
    }

    // 二次元配列の要素を出力
    for (int i = 0; i < ARRAY_ROW_NUM; ++i) {
        for (int j = 0; j < ARRAY_COL_NUM; ++j) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }

    // メモリ解放
    for (int i = 0; i < ARRAY_ROW_NUM; ++i) {
        free(array[i]);
    }
    free(array);
}

実行結果:

0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0

この方法の場合、ポインタへのポインタを使用します。最初の calloc関数malloc関数でも構いません)で、必要な行数分のメモリ領域を確保しています。このときに確保するメモリ領域は、この後、一次元配列を作るための場所なので、int型ではなくて、int のポインタ型 (int*) のためのものであることに注意してください。

また、calloc関数の戻り値を受け取るポインタ変数 array は、int* ではなく int** でなければなりません。確保しようとしているメモリ領域は、前述のとおり int* のためのものですから、この領域を指すポインタは int** です。

calloc関数の戻り値の型は void* です。レベルの考え方でいえばレベル 1 ですが、実際には int** というレベル 2 のポインタを意味しています。レベルが合わないのは多くの場合、何かが間違っていると考えられますが、ここは自身を持って合っていると言わなければならない場面です。ただ、voidポインタへは、あらゆるポインタ型を代入できるため、キャストは不要です。

このように、voidポインタである void* は、レベルの差すら無視した使い方を強いられるので、ここは気を付けなければなりません。

確保されたそれぞれの行ポインタに対して、今度は、その行を構成する要素のための領域を確保します。今度は、各要素は int型ですので、calloc関数には int型の大きさを指定すればよいです。

もう1つの方法は、行も列もまとめて1つの一次元配列とみなしてしまう方法です。

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_COL_NUM    6      // 配列の列の数
#define ARRAY_ROW_NUM    5      // 配列の行の数

int main(void)
{
    // 行と列をまとめた一次元配列を動的に確保
    int* array = calloc(ARRAY_ROW_NUM * ARRAY_COL_NUM, sizeof(int));

    // 一次元配列の要素を出力
    for (int i = 0; i < ARRAY_ROW_NUM; ++i) {
        for (int j = 0; j < ARRAY_COL_NUM; ++j) {
            printf("%d ", array[i * ARRAY_COL_NUM + j]);
        }
        printf("\n");
    }

    // メモリ解放
    free(array);
}

実行結果:

0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0

こちらの方法では、確保と解放はすっきりと分かりやすいと思いますが、これだとあくまでも一次元配列です。なぜこれでもいいのかというと、各要素のメモリアドレスが連続的であるという特徴は、どちらの考え方でも変わらないからです。

各要素へアクセスする際に添字を工夫して、二次元配列のように見せかけます。たとえば、1行目の 4列目の要素は、以下のように書きたいところですが、

array[1][4];

これはできないので、代わりに次のように書きます。

array[1 * ARRAY_COL_NUM + 4];

二次元配列の場合の各要素のメモリアドレスの並びを思い出してください。このように「行番号×列の個数+列番号」とすれば、目的の要素の位置を割り出せます。

1つ目の方法では、malloc関数や free関数の呼び出し回数が多くなるため、実行効率もメモリの使用効率も、あまり良いものではありません。その意味では、2つ目の方法の方が、malloc関数も free関数も 1回ずつで済むという利点があります。他方、要素にアクセスするたびに、添字を計算で求めなければならない点では、2つ目の方法の方が非効率です。

どちらの方法もよく使われているものなので、両方とも理解しておきたいところです。


練習問題

問題① 次のプログラムの誤りを修正してください。

#include <stdio.h>

int main(void)
{
    char str[81];
    char* p_str = str;
    char** pp_str = &p_str;

    puts("80文字以内の文字列を入力してください。");
    fgets(**pp_str, sizeof(str), stdin);
    printf(str);
}

問題② 二次元配列を使って、九九表を作成・出力するプログラムを作成してください。

問題③ 動的な二次元配列の各要素のメモリアドレスを調べるプログラムを作成してください。

問題④ 「配列を指し示すポインタ」が欲しいと考えました。これは可能ですか? どうすれば実現できるでしょうか?

問題⑤ 次のプログラムは正しいですか? 正しければ実行結果を答えてください。間違っていれば、どう間違っているか指摘してください。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int* value;
    int** ptr = &value;

    value = malloc(sizeof(int));
    *value = 100;

    printf("%d\n", *value);
    printf("%d\n", **ptr);

    free(value);
}

問題⑥ リバーシの盤面を表現する二次元配列を作り、ゲーム開始時点の状態を表現するように初期化してください。初期化処理は1つの関数にまとめ、また状態を確認できるような出力関数を作成してください。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第35章 ポインタ⑤(動的メモリ割り当て))

次の章へ (第37章 ポインタ⑦(関数ポインタ))

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る