Menu

C のポインタと配列:減衰、arr[i] = *(arr+i)、そして sizeof の罠

C では、配列名は使われるほとんどの場所で先頭要素へのポインタに変わります。この減衰、arr[i] が文字どおり *(arr+i) である理由、配列のサイズを関数へ別途渡さなければならない理由、そして配列へのポインタとポインタの配列の違いを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

配列とポインタは C では別物ですが、ほとんどの場合は同じように振る舞います。その「ほとんどの場合」こそがこの話題をつかみどころのないものにしています。配列をポインタとして扱うコードはたいてい動いてしまい、違いが効いてくる 2 か所のどちらかに行き当たった日に、黙って間違った答えを返すのです。

このページではその境界線を正確に引きます。

配列の減衰:すべての土台にある 1 つのルール

ほとんどすべての式で、配列名は自動的に先頭要素へのポインタに変換されます。これを**減衰(decay)**と呼びます。arr&arr[0] になり、型は int * です。

配列名と先頭要素のアドレスは、まったく同じものが表示されます。それが減衰の働きであり、& なしで配列をポインタに代入できる理由です。

減衰が起こらない場所はちょうど 3 つです。sizeof のオペランドであるとき、& のオペランドであるとき、そして文字列リテラルで文字配列を初期化するときです。それ以外の場所では減衰すると考えてください。

arr[i] は文字どおり *(arr + i)

C 標準は添字演算子をポインタ演算の言葉で定義しています。a[b]*(a + b) を意味します。添字付けは独立した機能ではなく、略記なのです。

ここから 2 つの帰結が出てきます。第一に、ポインタにも添字を付けられます。p[2]*(p + 2) であり、ポインタを受け取る関数でも見慣れた arr[i] の構文が使えるのはそのためです。第二に、2[arr] はコンパイルが通ります。*(2 + arr)*(arr + 2) と同じアドレスだからです。クイズ以外で書く人はいませんが、添字付けが「本当に」ポインタ演算なのかという疑問に決着を付けてくれます。

配列を関数に渡す

減衰のせいで、関数が配列を受け取ることは決してありません。受け取るのはポインタです。

この例から取り出しておく価値のある点が 3 つあります。

int arr[100] のサイズは説明書き以上のものではありません。 コンパイラは仮引数を int *arr に書き換え、本当に 100 個渡したかどうかは決してチェックしません。

長さは別途運ばなければなりません。 関数はアドレスを持っているだけで、データがどこまで続くのかを知りません。配列を受け取る C の関数がほぼ例外なく個数を受け取るのはこのためです。memcpy(dst, src, n)fread(ptr, size, count, f)qsort(base, nmemb, size, cmp) などです。文字列が例外なのは、自前の終端マーカー '\0' を持ち歩いているからにすぎません。

ポインタを渡すのは安価で、データは共有されます。 配列のコピーは作られないので、関数は呼び出し側の要素を変更できます。多くの場合、それこそが望みのものです。

print_all のように、読み取り専用の仮引数には const int * と印を付けましょう。意図が記録され、うっかりした書き込みをコンパイラが捕まえてくれます。

sizeof の罠

ここが、どの C プログラマも一度は引っかかるバグです。sizeof は減衰が起こらない 2 か所のうちの 1 つなので、配列が宣言された場所では本当の配列サイズを返し、配列が減衰した場所ではポインタのサイズを返します。

main の中では sizeof data は 40 バイトで、長さは 10 と計算されます。inspect の中では sizeof arr はポインタのサイズ(64 ビットマシンで 8)になり、「長さ」は 2 と出ます。コードは同じに見えるのに、静かに間違っています。

ここから導かれるルールはこうです。配列の長さを計算するのは、その配列が宣言されたスコープの中だけにする。 そして、そこから先へ持ち回りましょう。

配列の & は型が違う

減衰が起こらないもう 1 つの場所が & 演算子です。&arr は先頭要素ではなく配列全体へのポインタです。どちらも同じアドレスを保持しますが、違うのは「1 を足す」の意味です。

開始アドレスは同じ、歩幅は別です。arr の型は int * で 1 要素ずつ進み、&arr の型は int (*)[5] で配列全体ずつ進みます。これが 2 次元配列の 1 行を渡せるようにしている仕組みで、多次元配列 で扱います。

配列へのポインタとポインタの配列

似て見えて正反対の意味を持つ、2 つの宣言です。

int *p[5];     // int へのポインタ 5 個の配列
int (*q)[5];   // int 5 個の配列へのポインタ

[]* より優先順位が高いので、int *p[5] は「p はポインタの配列である」と解析されます。int (*q)[5] のかっこは * を先に結び付けさせ、「q は配列へのポインタである」となります。

int *p[5]                     int (*q)[5]

 p[0] -> [ int ]               q -> [ int | int | int | int | int ]
 p[1] -> [ int ]                     5 個分の連続した 1 ブロック
 p[2] -> [ int ]
 p[3] -> [ int ]
 p[4] -> [ int ]
 5 つの別々のアドレス

よく使うのはポインタの配列のほうで、すでに使ったことがあるはずです。char *argv[] は文字列へのポインタの配列で、コマンドライン引数 1 つにつき 1 個です。長さの異なる文字列のリストを、領域を無駄にせずに保持する方法でもあります。

(*q)[0] に注目してください。ポインタを参照外しして配列を得てから、添字を付けています。ここでもかっこは必須で、理由は同じ優先順位の話です。

配列とポインタが本当に違うところ

この短い表を頭に入れておけば、混乱は消えます。

配列 int arr[10]ポインタ int *p
正体メモリに並んだ 10 個の intアドレスを 1 つ保持する変数
sizeof40 - ブロック全体8 - ポインタだけ
代入可能か不可:arr = x; はエラー可:p = x; は問題ない
& が返す型int (*)[10]int **
メモリの出どころ配列の宣言とともに確保指した先ならどこでも

配列名はアドレスを保持する変数ではありません。それ自体が記憶領域であり、アドレスは必要に応じて計算されます。だからこそ代入できないのです。

よくある質問

C で配列とポインタは同じものですか?

違います。ただし、配列名はほとんどすべての式で先頭要素へのポインタに減衰するため、混同しやすいのです。配列そのものはサイズの分かった要素のブロックであり、ポインタは 1 つのアドレスを保持する単一の変数です。違いが表に出るのは sizeof& の 2 か所です。

なぜ関数の中では sizeof(arr) が間違った答えを返すのですか?

その仮引数が配列ではないからです。void f(int arr[]) は黙って void f(int *arr) に書き換えられるので、sizeof(arr) は元の配列ではなくポインタ(ほとんどのマシンで 8 バイト)を測ります。長さは別の仮引数として渡しましょう。

int *p[5] と int (*p)[5] の違いは何ですか?

int *p[5] は int へのポインタ 5 個の配列です。int (*p)[5] は int 5 個の配列への単一のポインタです。かっこは * を先に名前へ結び付けます。かっこがなければ、優先順位の高い [] が勝ちます。

arr[i] は本当に *(arr + i) と同じですか?

同じです。標準は a[b]*(a + b) として定義しています。奇妙に見える i[arr] がコンパイルされて動くのもそのためです。加算は交換可能なので *(i + arr) は同じ要素になります。実際のコードで書いてはいけませんが、このルールを証明してくれます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める