ポインタ配列

ポインタは、オブジェクト(または関数)の場所を示すためのオブジェクトです。つまり、型のサイズがわかっているので、他のサイズのわかっている型、たとえば基本型(charやintなど)と同じように、配列にすることができます。

ちょっと本題から逸れますが、「サイズがわかっている」というのは、コンパイラがプログラムをコンパイルする時点で、その型のオブジェクトが定義されたときに確保すべきメモリ中の領域の範囲を決定可能であるという意味であり、プログラマがその範囲の大きさ(すなわち型のサイズ)の具体的な値を知っているという意味ではありません。

ここでは、まずポインタ配列とはどのようなものかを示し、ポインタ配列を動的に確保する場合の注意点や、多次元配列と動的確保によって作り出した擬似的な多次元配列との違いなどを解説します。

ポインタ配列とは

たとえば、次の例ではchar型のオブジェクトが10個格納される配列を定義しています。

char charray[10];      /* char型オブジェクトの配列(要素数10) */

ポインタも同じように、配列を定義して使うことができます。

char *chr_ptr[10];     /* 「char型オブジェクトを指すポインタ型」のオブジェクトの配列(要素数10) */

このポインタの配列とは何なのでしょうか。2次元配列のことをポインタの配列だという人がたまにいますが、ちょっと違います。ポインタの配列は2次元配列などではなくあくまでもポインタ型の配列であり、上のように定義されたものは1次元配列です。メモリの確保のされ方からいって、いわゆる2次元配列とポインタ配列は全く違うものです。

ポインタ配列とメモリ領域

次のようなコードによって、ポインタ配列がどのようにメモリ上に確保されるかを確かめることができると思います。

#include <stdio.h>
#define N 5

int main(void)
{
    char *parray[N];
    int i;

    printf("ポインタのサイズは %d バイト\n", sizeof(char *));
    printf("先頭アドレスは %p\n", parray);

    for(i=0; i<N; i++) printf("%p\n", &parray[i]);

    return 0;
}

printf()の%p変換は、ポインタが保持している値を、出力装置が出力可能(便宜上、以後は表示と書きます)な文字列に変換します。「確かめることができると思います」と書いたのは、%pでの変換は処理系依存であり、表示することはできますが、必ずしも人間が読んで理解できる形式になっているという保証はないからです(現実問題としては、人間が読める形式でなければあまり役に立たないので、16進数で表示される処理系が多いと思います)。

私の環境では、次のような結果になりました。

ポインタのサイズは 4 バイト
先頭アドレスは 0012FF78
0012FF78
0012FF7C
0012FF80
0012FF84
0012FF88

ポインタのサイズとprintf()の%pの出力形式は、どちらも処理系に依存します。また先頭アドレスの値はたまたまこうなっただけです。%p変換により出力された値そのものにはあまり意味はなく、重要なのは出力された値と値との間の差です。

この処理系では、ポインタのサイズが4バイトなので、要素ひとつにつき4バイト必要で、それが配列として確保した5個隙間なく並んでいます。これがポインタ配列です。

ポインタ配列の動的確保

配列(メモリ領域)の動的確保

メモリの領域をプログラム中で動的に確保するには、通常はmalloc()を使います。次のコードは、char型のオブジェクトの配列を動的に確保する例です。

以降に示すコードは、本質的な部分の説明のため、実際的なプログラムでは本来必要となるエラー処理やメモリ開放処理を省略していることに注意してください。

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

#define N 5
int main(void)
{
    char *arr;
    int i;

    arr = malloc(N * sizeof(char));

    for(i=0;i<N;i++) printf(" %p\n", &arr[i]);

    return 0;
}

こういった例ではint型が使われることが多いのですが、int型のサイズとポインタ型のサイズが同じになっている処理系が多いので、混乱を避けるために、ここではchar型を用いています。

少なくとも、ANSI C(1989年)以降の標準に準拠したCコンパイラなら、sizeof(char)の結果は確実に1です。それより前からそうだったことにも、10円くらいなら賭けられます。もし、ポインタ型のサイズがchar型のサイズと同じだったとしたら、上記のコードでわざわざchar型を用いた意味がなくなってしまうのですが、ここではポインタ配列とメモリ領域で述べたように、char型オブジェクトへのポインタ型のサイズが4である処理系を例として話を進めます。ポインタ型のサイズが1である処理系も、規格上はあり得なくもないような気がするのですが、現実問題としては、ほとんどすべての処理系で、1よりは大きいサイズになっていると思います。

また、ポインタが参照する先のオブジェクトの型によって、ポインタ自体のサイズが異なる(派生元の型によって派生型であるポインタ型のサイズが異なる、要はsizeof(int *)とsizeof(char *)の各々の結果が異なる)ような処理系もこれもまた規格上は(オブジェクトへのポインタ型のサイズが1の処理系よりは)あり得るような気もするのですが、広く普及しているほとんどの処理系では、どの型のオブジェクトを参照するためのポインタであっても同じサイズになることが多いと思います。

上のプログラムを実行すると、次のようになりました。

00B72CB4
00B72CB5
00B72CB6
00B72CB7
00B72CB8

もちろん先頭アドレスの値はたまたまこうなっただけで、値そのものが重要なのではありません。この領域はarr[0]からarr[4]まで、配列と同じような方法で、実体を指定し使うことができます。

確保した領域の先頭アドレスは00B72CB4で、そこから、少なくとも確保した数(ここでは5個)のchar型のオブジェクトが連続して入る領域が確保されます。ここで注意が必要なのは、確保された領域は、単なるメモリ内の範囲であって、その中にchar型のサイズに応じた境界といったような情報を持っているというわけではないということです(ただし、確保された領域の中にchar型のオブジェクトを適切に配置できることは保証されます)。malloc()は単に、「少なくとも指定された大きさの領域を割り当て、その領域の先頭へのポインタを返す」という仕事をするだけです。

それではなぜ配列と同じようにarr[0]やarr[1]のように配列添字演算子によって配列と同じように使えるかというと、malloc()の返却値をchar型へのポインタ変数に代入しているからです。配列添字演算子[]を用いたarr[1]のような指定は、ポインタ演算*(arr+1)の簡便記法に過ぎないので、「char型オブジェクトを指すためのポインタarrの指す位置からchar型オブジェクト1個分進めた場所に格納されているchar型のオブジェクト」を参照していることになります。

このページのサンプルコード中で、以前はmalloc()の返却値(型はvoid *)をchar *にキャストしていましたが、Cではvoid *はいかなるオブジェクト型へのポインタにも代入できるので、キャストは削除しました。

ポインタ配列の動的確保

ポインタ配列といっても、動的確保の考え方は同じです。ポインタ型のオブジェクトが指定した数だけ入る大きさのメモリ領域を確保します。ただし、実際の確保の仕方は次のようになります。ポインタの配列を動的に確保するには、ポインタへのポインタが必要になります。ややこしいのですが、理由を考えてみてください。

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

#define N 5
int main(void)
{
    char **arr;
    int i;

    arr = malloc(N * sizeof(char *));

    for(i=0; i<N; i++) printf("%p\n", &arr[i]);

    return 0;
}

結果は次のようになりました。上の節のchar配列を動的に確保した場合の結果と見比べてみてください。こちらは、プログラムを実行した処理系のポインタのサイズが4バイトだったので、ひとつの要素につき4バイトの領域が確保されています。

00B72CB4
00B72CB8
00B72CBC
00B72CC0
00B72CC4

"**"の理由

char型のオブジェクトのための領域を確保するときには、char型に対応するポインタ(「char型から派生したポインタ型のオブジェクト」即ち「char型オブジェクトを参照するためのポインタ型のオブジェクト」を以後便宜上こう書きます)を用いました。malloc()で領域を確保し、確保された領域の先頭アドレスをポインタに代入しています。これは特に不思議な点はないですね。では、char型に対応するポインタのための領域を確保するにはどうしたらいいのでしょうか。

char型に対応するポインタを用いて、char型に対応するポインタのための領域の動的確保を行うとします。すると、重大な矛盾が生じます。

たとえば上の例でarrの型がchar **ではなくchar *であったとして、arr = malloc(N * sizeof(char *));のようにしたとしましょう。確かにこれでchar型に対応するポインタが5個収まるだけの大きさを持った領域を確保することはできます。しかし、ポインタarrはあくまでも「char型に対応する」ものであって、「char型に対応するポインタに対応する」ポインタではありません

ここで、ポインタの性質のひとつを思い出してください。

ポインタ変数は、対応する型のサイズ単位で値が増減する変数である。

これを確かめるために、上のプログラムコードではchar **で定義されていたポインタ変数arrを、char *に変えてみました。

/* malloc()の返却値を誤った型のポインタに代入する例 */
#include <stdio.h>
#include <stdlib.h>

#define N 5
int main(void)
{
    char *arr;
    int i;

    arr = malloc(N * sizeof(char *));

    for(i=0; i<N; i++) printf("%p\n", &arr[i]);

    return 0;
}

何度も述べるように、char型のサイズは1バイトです。つまり、char型に対応するポインタ変数の値は1(バイト)を変化の最小単位として増減します。一方、ポインタ型のサイズは処理系に依存します。このプログラムを実行した処理系では、4バイトでした。

00B72CB4
00B72CB5
00B72CB6
00B72CB7
00B72CB8

この処理系では、ポインタを1個格納するには、4バイトの領域が必要です。しかし、上の実行結果を見るとわかるように、まるで要素1個につき1バイトの配列であるかのように扱われています。arr[1]として「ポインタ配列の2番目の要素」を指定したつもりが、「ただの2バイト目」ということになってしまうのです。これは、char *で宣言されたポインタの被参照型(参照されるオブジェクトの型)がcharであり、そのポインタはsizeof(char)の単位で値が増減するということです。つまり、上の結果として00B72CB4という値が入っているポインタarrをarr++;と1回インクリメントすると、00B72CB5になるということです。もちろん、このようなポインタ配列のできそこないを使用したプログラムは正しく動作しません。

したがってchar *を実体とし、sizeof(char *)の単位で値が増減するポインタは、char **で宣言されなければなりません。ということで、ポインタ配列を動的に正しく確保するためには、ポインタへのポインタを使わなければならないのです。

コラム Cという言語において確かなこと

Cの仕様を学び始めて間もないプログラマが天を仰ぎ、神に訴えました。

「おお、言語の神よ! 私はわからない。このCという言語は、標準においてまで曖昧で未定義で処理系依存ばかりで私を惑わせる。神よ、私に確かなことを教えたもう給え!」

天から声が降り注ぎました。

『――sizeof(char)の結果は1である。』

プログラマは天を仰いだ姿勢のまま続きを待ちましたが、いくら待ってみても、それ以上の言葉が届くことはありませんでした。そうしているうちに首が痛くなってきたので、彼は神託を受けることを諦め、自らの手で処理系の仕様を事細かに調べることにしました。

…まぁ実際には、標準で規定されていて確かなことは他にももう少しありますが、それはさておき、このプログラマは間違っています。彼がすべきなのは、標準で処理系依存とされているような不確かなことを事細かに調べることではなく、不確かなことに依存しない手法を学ぶことです。このページの解説では、説明の都合上、sizeof(char *)の結果の具体的な値(この例の処理系では4)を用いていますが、大胆にいえば、普通のプログラムではsizeof(char *)の結果の実際の値なんてプログラム作成者にとっては知ったこっちゃありません。上述のポインタ配列を動的確保するコード例のように、型のサイズが必要な場合にはsizeof文の結果を渡せば良いだけです。

ところで、sizeof(char)の結果が1であることは言語の神に誓って確かなのですが、1バイトって何ビットでしょうか。8ビット? いやいや、実は、それもまた処理系に依存するんですよ。決まっているのは、少なくとも8ビット以上の2進表現であることだけです。9ビットだろうが10ビットだろうが、処理系がそれを1バイトとして扱うと決めたら1バイトなのです。符号ビットの有無とかも処理系に依存します。つまり、1バイトで表現できる値の範囲は処理系によって違います

そろそろうんざりしてきましたか? でも、良い知らせもあります。標準ヘッダファイル<limits.h>の存在です。limits.hでは、処理系が扱える整数型の範囲がマクロとして定義されています。たとえば、プログラム中で1バイトのビット幅が必要な場合(あまりないと思いますが)、CHAR_BITで定義された値が使用できます。intの最小値と最大値なら、INT_MINとINT_MAXが定義済みなので、必要に応じてこれらのマクロを使えば良いのです。

このように、プログラム作成者が型のサイズや表現範囲の具体的な値を知る必要など、基本的には全くないものと考えてください。稀に例外として、整数型の特定のビットが立っているかどうかをフラグにするAPIとかライブラリ関数とかがある場合もありますが、そういった場合はその関数の説明に渡すべき型のビット幅も含めてちゃんと書いてあるはずです。

再び、天から声が降り注ぎました。

『――汝の手にした標準ヘッダファイル<limits.h>と<float.h>に祈るが良い』

それは、処理系のすべてを知り深淵を究めるため、プログラマがマシン語の世界へと旅立ってしまった後のことでした。

擬似的な多次元配列

多重ポインタを使ってmalloc()などの関数による領域の動的確保を繰り返すことで、擬似的な多次元配列を実現できます。この擬似的な多次元配列と通常の多次元配列の違いを説明します。

通常の多次元配列で確保されるメモリ領域

次のコードは、2次元配列として「char型オブジェクトの配列(要素数3)」を要素とする配列(要素数3)を定義しています。Cの多次元配列の概念はこのような「配列型」の配列であり、いわゆる「多次元」の配列ではないという意見もありますが、便宜上、ここではこのような構文で定義された配列型のオブジェクトのことを多次元配列と称します。

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

#define N 3
int main(void)
{
    char arr[N][N];
    int i, j;

    printf("arr の先頭アドレスは %p\n", arr);
    for(j=0; j<N; j++){
        printf("arr[%d]:%p ", j, arr[j]);
        for(i=0; i<N; i++){
            if(i) printf("--------------- ");
            printf("&arr[%d][%d]:%p\n", j, i, &arr[j][i]);
        }
    }
    return 0;
}

ある処理系では、実行結果は次のようになりました。例によって値そのものにはあまり意味はありません。隣接する値の間の差に注目してください。多次元配列はこのように連続したメモリ領域に確保されます。したがって、arr[1][0]は、arr[0][3]のように指定することもできます。

arr の先頭アドレスは 0012FF80
arr[0]:0012FF80 &arr[0][0]:0012FF80
--------------- &arr[0][1]:0012FF81
--------------- &arr[0][2]:0012FF82
arr[1]:0012FF83 &arr[1][0]:0012FF83
--------------- &arr[1][1]:0012FF84
--------------- &arr[1][2]:0012FF85
arr[2]:0012FF86 &arr[2][0]:0012FF86
--------------- &arr[2][1]:0012FF87
--------------- &arr[2][2]:0012FF88

擬似的な多次元配列で確保されるメモリ領域

同じように、char型オブジェクトの配列(要素数3)を3個、動的に確保するにはどうすれば良いでしょうか。次のコードを見てください。

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

#define N 3
int main(void)
{
    char **arr;
    int i,j;

    arr = malloc(N * sizeof(char *)); /* ポインタ配列を確保 */
    /* 配列の要素それぞれにつき、メモリ領域を確保 */
    for(i=0; i<N; i++) arr[i] = malloc(N * sizeof(char));

    printf("char *, char **のサイズは %d, %d\n", sizeof(char *), sizeof(char **));
    printf("arr の先頭アドレスは %p\n", arr);

    for(j=0; j< N;j++) printf("&arr[%d]:%p,arr[%d]:%p\n", j, &arr[j], j, arr[j]);
    for(j=0; j<N; j++){
        printf("arr[%d]:%p ", j, arr[j]);
        for(i=0; i<N; i++){
            if(i) printf("---------------- ");
            printf("&arr[%d][%d]:%p\n", j, i, &arr[j][i]);
        }
    }
    return 0;
}
char *,char **のサイズは 4, 4
arr の先頭アドレスは 00B72CB0
&arr[0]:00B72CB0,arr[0]:00B72CC0
&arr[1]:00B72CB4,arr[1]:00B72CD0
&arr[2]:00B72CB8,arr[2]:00B72CE0
arr[0]:00B72CC0 &arr[0][0]:00B72CC0
--------------- &arr[0][1]:00B72CC1
--------------- &arr[0][2]:00B72CC2
arr[1]:00B72CD0 &arr[1][0]:00B72CD0
--------------- &arr[1][1]:00B72CD1
--------------- &arr[1][2]:00B72CD2
arr[2]:00B72CE0 &arr[2][0]:00B72CE0
--------------- &arr[2][1]:00B72CE1
--------------- &arr[2][2]:00B72CE2

実行結果は上のようになりました。

    arr = malloc(N * sizeof(char *));

の部分で、まずchar型に対応したポインタが3個収まるだけの領域が確保されます。arrは先頭アドレスが0x00B72CB0で、arr[0]からarr[2]までの3個の要素を持つポインタ配列と捉えることができます。ただし、この時点では領域が確保されただけで、各要素の値は不定なので、このままでは何の役にも立ちません。より正確にいえば、少なくともsizeof(char *)が3個連続して収まる大きさを持つ領域の使用権を得たといったところでしょうか。

    for(i=0; i<N; i++) arr[i] = malloc(N * sizeof(char));

その後、3バイトの領域を確保し確保された領域の先頭アドレスをポインタに代入するという作業を、arr[0]からarr[2]までの各要素について行います。領域の確保が成功すると、ポインタ配列arrのそれぞれの要素の値は、確保された領域の先頭アドレスになります。

このように、多重ポインタを用いてまずポインタ配列を確保し、配列の各要素であるポインタを用いて領域を確保することで、配列添字演算子[]を使ってアクセスする限りにおいては、多次元配列と同じように扱えるメモリ領域を確保することができます。しかし、通常の多次元配列のような連続した領域が確保されるわけではありません。

実行結果を見るとわかるように、擬似的な多次元配列の場合、確保に使った多重ポインタの参照先が、配列の先頭を指すわけではありません。このポインタは、ポインタ配列のために最初に確保した領域の先頭を示しているだけです。

この例の擬似的な2次元配列の場合、ポインタ配列の各要素は、領域の先頭のオブジェクトを参照するためのポインタになっているので、arr[1][2]のような指定は、「ポインタ配列の2番目のポインタ型のオブジェクトが保持している値を先頭とする領域の」「3番目の要素」という意味になります。見た目には通常の2次元配列の指定方法と同じになりますが、実際の領域の確保のされ方は全く違うことに注意してください。ただ、配列添字演算子を使ってアクセスしたいという要求を満たせば良いというだけなら、上記の点に注意すれば、充分に役に立つこともあるかと思います。

領域の開放に関する注意

上記のコードでは省略していますが、実際のプログラムではほとんどの場合、不要になった領域は開放しておく必要があります。上の擬似的な多次元配列に対して、次のようにしたくなるかもしれません。

    free(arr); /* 領域は開放されるが…各要素(別の領域の先頭を指すポインタ)の行方は? */

arrが保持している値の示す場所を先頭とする領域を開放する必要は確かにあるのですが、その前にarrの各要素についてfree()を呼び出し、それぞれのポインタの指す各領域を開放しておく必要があります。

    for(i=0; i<N; i++) free(arr[i]);
    free(arr);

各要素に対してfree()を呼び出す前に最初に確保したarrの領域を開放してしまうと、各要素として格納されているポインタにアクセスする手段が永遠に失われ、メモリリークとなります。


最終更新日: 2009年4月17日 領域の開放に関する注意を追加


Already Exists