/tmp

雑記帳.

配列はポインタと同じものか

この記事は「こう考えたら自分なりに分かりやすかったな」というメモである. 実際の処理系の実装や実際の動作とは必ずしも一致しない.

配列とポインタの違いについて解説しているサイト ポインタ虎の巻~初期化文字列の2つの定義 を読んでいて面白かったので, 自分なりの理解をまとめた. 少し学んだ人ならすぐに気づくようなことなのかもしれないが.

通常, 配列名はその先頭要素のアドレスを指す. そのおかげで普段、型T []をもつ配列の 名前T *のように扱うことができる. 統一的に扱えるし, ポインタなら関数に渡すのも簡単. うまい仕組みだ.

では配列は, 実は先頭要素のアドレスを保持するポインタと等価なのか. 当然実際はそうではない. よく知られているように

char cha[4];
assert(&cha == cha);

は成功する. 型が違う (char (*)[4] <-> char *) ので警告は出るかもしれないが, 値は等しい. これはchaが通常のポインタならば有り得ない. &chaはそのポインタ変数そのもののアドレスになるからである.

ところで変数とは何か. 値を入れる箱である. では変数名とは何か. 入れる箱を識別するためのタグである. ではメモリの特定の領域に特定の名前がついていて, 名前でその領域にアクセスできるのか. そんなはずはない. メモリにはただ通し番号が振られているだけだ.

関数内で宣言したいわゆるローカル変数は, そのスタックフレームに領域を確保される. C言語関数辞典 - C言語用語集 スタックフレーム (stack frame) の図が見やすくて分かりやすい. コンパイラはローカル変数名とスタック上の位置を対応付けて持っている. あるローカル変数に対して読み書きするとき, コンパイラはその変数に対応する位置を取得して, 「取得した位置のメモリに対して読み書きするようなプログラム」を出力する. ここで変数名は消えさる. 出力されるプログラムはそのローカル変数の位置をベースポインタからの相対的距離で表しているわけだが, まあ, 実行段階なのでアドレスと同一視しよう.

変数名 -> アドレス -> 実体, という流れを踏む点で, 変数名によって実体を間接的に参照しているように見える. 通常の代入文では見えないので, ここでは見えない間接参照と呼ぼう. そして, アドレス演算子を使わなければ変数名に結び付いたアドレスを見ることはない. だから変数自体のアドレスのことを見えないアドレスと呼ぶことにする. 見えないアドレスとは呼んでも, 別に普通にアドレス演算子を適用すれば見えるのだが. つまり, 逆に言えばアドレス演算子見えない間接参照を抑制する 働きであるとも考えられる.

言ってることがよく分からないと思うので, たとえば次のような場合.

int c = 10;
c;              // (1)
*(int *)0x1111; // (2)

値を捨てているが気にしない. 仮に &c == 0x1111 とする. (1) においてcを評価しているが, これは (2) と同値である. ユーザーは (1) の評価で明示的なデリファレンスも何もしないが, やってることはデリファレンスだと言いたかった. このデリファレンスを以って間接参照と呼んでいる.

話を戻す. 結論から言うと, 配列名が先頭アドレスになるという仕様は, いろいろな変数があるなかで配列名に限っては自動的にデリファレンスされないということだと理解できる. アドレス演算子を適用していないのに見えないアドレスが見えるということだ.

普通の変数名を評価するとその中身が返る. 先の例のように見えない間接参照をする. 単体の配列名が例外で, 見えない間接参照をしないのだ. そして見えないアドレスをそのまま返してくる. 逆に「Cで見えている世界」のうちで考えるなら, 何もしていないのにアドレス演算子を適用していると言い換えてもよい.

さて, ここまでくるとポインタとは根本的に違うものだと分かる. ポインタは, 所詮変数である. そのポインタ用に別途領域を確保し, その中身として何らかのアドレスを明示的に初期化あるいは代入する. ポインタ変数を評価すると, 見えない間接参照をして, ポインタ変数の中身を返す. 普通の変数と何の違いもない. でもたまたま型がアドレスを保持するためのものだったので, 評価結果はアドレスになる. それに対して配列名は, 見えない間接参照をしないで, つまり中身へ辿らず, そのまま見えないアドレスを返してくる.

ポインタのそれと違い配列の先頭アドレスを保存するための領域があるわけでもないし, その領域の値を間接参照しているわけでもない. 配列そのものに対して代入ができないのも納得である.

もう &cha == cha は簡単だ. まず右辺のchaは見えない間接参照を行わず見えないアドレスがそのまま出てくる. 左辺は, 配列名がアドレスに変換されるルールの例外で, ここでのchaは配列そのものだ. 少々ややこしいが, 配列そのものは普通の変数同様に見えない間接参照をすると みなせば どうだろうか. 具体的にどういうことだ... と考える必要はない. どうせアドレス演算子を適用するのである. 普通の変数と同様, アドレス演算子を適用した結果, 見えない間接参照が抑制され, 見えないアドレスがそのまま返るのだ. 結局, 右辺も左辺も同じになる.

おもしろい.