プログラミング言語を学び始めると、たいていの教科書にはこう書いてある。
変数とは、値を入れる「箱」です。
最初のうちはこれで問題ない。int hp = 10; なら「hp という箱に 10 を入れた」でちゃんと動く。
でも、ポインタに入った瞬間このモデルが崩壊する。
「箱モデル」がポインタで壊れる瞬間
箱モデルだとこうなる。変数は箱で、箱の中に値が入っていて、ポインタは箱の場所を持っている。
ここまではまだ耐えられる。
でも問題はここから。
ポインタも「変数」なんだ。
int *p;
これも箱になる。じゃあその箱の中には何が入っているのか。住所(数値)だ。
すると世界はこうなる。
箱の中に住所が入っていて、その住所の先にも箱がある。
ここで多くの人の脳が軽くクラッシュする。「え、箱の中に箱の場所? じゃあ * って何?」となる。
三丁目の田中さん家で考える
ここで街のたとえ話に乗り換えよう。
街がある。それがメモリ全体だ。
三丁目1番地、三丁目2番地……と住所が並んでいる。それがメモリアドレスだ。
ある一軒家がある。
三丁目5番地に、田中さんが住んでいる。
家の中には貯金10万円がある。
Cで言うとこうなる。
- 三丁目5番地 =
0x3005(メモリアドレス) - 田中さん =
tanaka(変数名) - 家の中身(貯金10万円) =
100000(値)
ここで大事なことがある。
「tanaka」は家の中身ではない。「tanaka」は三丁目5番地の「表札」だ。
0x3005(三丁目5番地)
┌──────────┐
│ 100000 │ ← 貯金(値)
└──────────┘
↑
tanaka ← 田中さん家という表札(ラベル)
&tanaka はその家の住所(番地)だ。0x3005 という数値そのものだ。
ではポインタは何か?
ポインタはこうだ。「三丁目5番地」と書かれたメモ。それ自体も、どこか別の家に置いてある。
int *sato = &tanaka;
0x3005(三丁目5番地) 田中さん家(tanaka)
┌──────────┐
│ 100000 │ ← 貯金
└──────────┘
0x3008(三丁目8番地) 佐藤さん家(sato)
┌──────────┐
│ 0x3005 │ ← 「三丁目5番地」と書かれた紙
└──────────┘
佐藤さんは田中さんの住所を知っている。だから直接乗り込んで貯金を書き換えられる。
- 佐藤さん家にある紙を見る → 三丁目5番地と書いてある
- そこへ行く
- 田中さんの貯金を書き換える
*sato = 200000; //書き換える
直接侵入だ。合法的な。
佐藤さん家にある紙をたどって、田中さん家の中身を書き換えた。番地をたどっているだけ。
こう見ると、構造はシンプルで壊れない。
変数はラベルだった
箱という概念を一度捨てよう。
実行中のメモリというのは、ただの番地付きの連続した領域だ。何もない。ただ並んでいるだけ。
アドレス 中身(4バイト)
-------------------------------
0x3005 100000
ここにコードが関わる。
int tanaka = 100000;
これでやっていることは3つ。
- メモリ上のどこかに 4バイトの領域を確保する
- そこに「tanaka」という名前(ラベル)を貼る
- 100000 を書き込む
つまり、変数は「中身」じゃなく「場所につけた名前」だ。
&tanaka は「tanaka というラベルが貼られた場所の番地」。つまり 0x3005 という数値。
そしてポインタはその番地を保持するための変数、つまり別の場所に貼られた別のラベルに過ぎない。
0x3008 0x3005 ← tanaka の番地が入っている
↑
sato ← sato もただのラベル
*sato は「sato の中身(= 0x3005)を番地として扱って、そこの中身を触る」という操作。
番地をたどっているだけ。魔法は何もない。
値渡しとポインタ渡しは物理的に全然違う
街のたとえでもう一つ確認しよう。
int yamada = tanaka;
これは「三丁目9番地に、田中さん家と同じ10万円をコピーして別の家を建てる」という行為だ。
0x3005(三丁目5番地) 田中さん家(tanaka)
┌──────────┐
│ 100000 │
└──────────┘
0x3009(三丁目9番地) 山田さん家(yamada)
┌──────────┐
│ 100000 │ ← 同じ値だが、住所は違う
└──────────┘
住所は違う。中身は同じ。だから yamada を書き換えても tanaka は変わらない。
ポインタを渡すのと値を渡すのは、物理的に全然違う行為だ。
コードで確認する
#include
void kaizan(int *sato) {
*sato = 10000000; // 1000万に書き換える
}
int main(void) {
int tanaka = 100000; // 田中さんの貯金10万円
kaizan(&tanaka);
printf("現在の貯金は%d円\n", tanaka); // 10000000 と表示される
}
これをメモリの平面で追いかけるとこうなる。
① int tanaka = 100000; の時点
0x3005 100000
↑
tanaka
② kaizan(&tanaka) を呼ぶ時点
&tanaka = 0x3005 を関数に渡す。関数のスタック上に新しい領域ができる。
0x3005 100000 ← tanaka の実体(田中さん家)
0x3008 0x3005 ← sato(渡された番地を保持する別の家)
③ *sato = 10000000; の実行
CPUがやっていること:
-
sato(= 0x3008)の中身を読む →0x3005 -
0x3005に10000000を書く
0x3005 10000000 ← 書き換わった
0x3008 0x3005
④ main に戻る
tanaka は依然として 0x3005 のラベル。その中身は 10000000。だから printf は 10000000 を出す。
kaizan は tanaka という名前を一切知らない。
ただ「渡された番地の中身を書き換えた」だけ。田中さんの住所だけを知っていた。
コンパイル時と実行時は別の世界
もう一個重要なことがある。コードの上下=実行順ではない、という話だ。
kaizan は main より上に書いてある。でも tanaka はまだ存在していない。矛盾しないのか?しない。
コンパイル時にコンパイラがやることは「設計図の登録」だ。
-
kaizanはint*を受け取る関数として登録 - 型チェックをして「問題なし」と確認
この段階では実際のメモリ番地はまだ決まっていない。tanaka が 0x3005 になるかどうかも、まだ誰も知らない。
実行時に初めてメモリが動く。
-
mainがスタートする -
int tanaka = 100000;でスタックに領域が確保されてtanakaが0x3005になる -
kaizan(&tanaka)でその番地が渡される
さらに言うと、コンパイルが終わった後、tanaka という名前はバイナリから消える。CPUは番地しか見ない。
「tanaka」は人間のための便利装置だ。機械は 0x3005 しか知らない。
コードを書くとき、人間は名前の世界で考える。CPUは番地の世界で動く。ポインタはその境界にある概念だ。
まとめると
- 変数 = メモリ上の領域につけた名前(住所に貼られたラベル)
-
&tanaka= そのラベルが指す場所の番地(田中さん家の住所) - ポインタ = 番地を保持するための変数(「三丁目5番地」と書かれたメモを持つ別の家)
-
*sato= sato の中身を番地として、そこの中身を触る操作(メモを見て、その住所に侵入する) - 値渡し = 中身をコピーして別の家を建てる(住所は別、中身は同じ)
箱で考えると「箱の中に箱の場所がある」という謎構造になる。
街と住所で考えると「ただの住所録と番地たどり」になる。
コンピュータに箱は存在しない。あるのは連続した数値の並びと、それを指す番地だけだ。型は「このビット列をどう読むか」という約束に過ぎない。
この視点まで来ると、二重ポインタも、関数ポインタも、スタックとヒープの違いも、全部「番地遊び」として見えてくる。
世界は意外と単純だった。複雑に見えるのは、説明が箱モデルに寄りすぎているせいだったりする。