ポインタの仕組みを習う2

ポインタ変数の宣言を深堀してみた

ポインタまじで全くわからないので超シンプルプログラムを書き、そこで疑問なことをジェミニ先生に聞いていきます。まず、ポインタ変数の宣言をしたら何が起こるか。ここを理解しないと初心者(私みたいなの)が次を理解するのは難しいです。

#include<stdio.h>
int main(void){
    int *p;
    return 0;
}

まず、int *p;でポインタ変数の宣言を書きます。pにはあえて何もいれず初期化しません。さて、ここで何が起きているかを知らねば次に進めません。

まずルールを知りましょう。

ジェミニ先生にある程度のルールを教えてもらいました!

ポインタ変数という「箱」の中に保存できるのは、どこまでいっても「メモリ上の住所(数値)」だけです。

ポインタは「住所」しか入りませんが、int *p という宣言は、「ここに入っている住所を訪ねたら、そこには int 型(4バイト)のデータがあるはずだ!」というルール(約束事)を含んでいます。

int *p;で16進数の住所を保存する場所がメモリ上に確保されます。

今主流の64ビットパソコンの場合、住所という『数値』を格納するための専用スペースは、8バイト必要なので8バイト確保されます。

Pの中身は8バイトを確保する前に入っていた何かしらのデータが入ってるんだよね?

そこには、8バイトを確保する直前までそこを使っていた、誰かのデータの残骸がそのまま残っています。

ちなみに8バイトならダブル型と一緒ですが、住所でなくダブルの数値が入ってることもあるの?

その通り、ダブルの数値がそのまま入っていることもあります
その数値が、無理やり「16進数の住所」として読み取られて表示されてしまうんです。

int二つ(8バイト)の中身がくっついて住所になってるってこともあるの?

まさに「2つの数値がくっついて1つの住所に見える」という、もっとエグい事件も起きています!

ここが重要!:数値が「住所」に化ける瞬間

確保したポインタ変数の中に入っているゴミデータは、もともとは住所ですらありません。

  • ある時は、誰かの「銀行の残高」だったかもしれない。
  • ある時は、ゲームの「スコア」だったかもしれない。
  • ある時は、さっき言ったように「引き裂かれた死体(バラバラの数値)」の組み合わせかもしれない。

それを、int *p; と宣言した瞬間に、プログラムが勝手に「よし、この8バイトに入っている数字を『住所』として読み取ろう!」と決めてしまうんです。

だから、表示された16進数の住所(00000045A1...)は、もともとは住所でも何でもない、ただの数値の残骸。それを信じてドアを開けるのは、デタラメに並んだ数字を地図だと思い込んで、全然知らない他人の家のドアを蹴破るのと同じくらい無茶なことなんです。

ポインタ変数宣言後のpはどうなっているのか見る

これを踏まえてプログラムを少し変えよう!ポインタ変数宣言後の中身を見に行こう!

#include<stdio.h>
int main(void){
    int *p;// ポインタ変数の宣言(int型を指すための「住所専用」の箱を確保)
    printf("Pの住所: %p\n", &p);
    printf("Pの中身: %p\n", (void *)p);
    printf("Pのサイズ: %zu バイト\n", sizeof(p));
    p = NULL;//変なところをいじらないように安全のためつけておきます
    return 0;
}

宣言後、変数pの住所と、変数pに入ってる中身のデータを16進数にしたもの、変数pのサイズは実行した結果こうなったよ。中身には、10進数に戻すと 「140兆番地」になっちゃうような変な数値が入っていました。

int *p;のほかの型でも宣言してみる

あとintで宣言するの気になってたからdoubleでも宣言してみて中身を見てみようかな。

#include<stdio.h>
int main(void){
    int *p;// ポインタ変数の宣言(int型を指すための「住所専用」の箱を確保)
    printf("Pの住所: %p\n", &p);
    printf("Pの中身: %p\n", (void *)p);
    printf("Pのサイズ: %zu バイト\n\n", sizeof(p));

    double *p2;
    printf("P2の住所: %p\n", &p2);
    printf("P2の中身: %p\n", (void *)p2);
    printf("P2のサイズ: %zu バイト\n", sizeof(p2));
    p = NULL;//変なところをいじらないように安全のためつけておきます
    p2 = NULL;//変なところをいじらないように安全のためつけておきます
    return 0;
}

double *p2;を宣言して同じように住所や中身やサイズを見てみました。

一番気になっていたポインタ変数のサイズですが、結果は両方8バイトでした。

ここでのintはpの型ではなく、pに入っている住所の訪ね先がint型(4バイト)だという宣言なのですね。

その通り!まさにポインタ学習最大の「アハ体験」ですね。

その理解をブログで強調すると、読者の「モヤモヤ」を一気に晴らすことができます。整理するとこういうことです。

ポインタ宣言 int *p; の「役割分担」
  • *p の部分:自分のスペック
    「俺は住所専用のメモ帳だぜ!サイズはたっぷり8バイトあるから、どんな巨大な部屋番号でも書き込めるぜ」という宣言。
  • int の部分:訪ね先のスペック
    「でも、その住所のドアを開けた先に住んでいるのはイントさん(int型・4バイト)だ。だからドアを開けたときは、4バイト分だけ読み書きしてくれよな」というガイド。

なぜ「訪ね先の型」が必要なのか?

でもなんで訪問先の型がひつようなの?

メモリ管理人は冷酷なので、住所だけ教えても「どこからどこまでがその人の荷物か」を教えてくれません。

もし int という指定がないと、ドアを開けた瞬間に、
「えっ、ここから1バイトだけ読むの? それとも100バイト分が住人の持ち物なの?」
と迷ってしまい、隣の部屋の住人まで巻き込んでデータを破壊してしまいます。

だから、「住所録(8バイト)」の中に、「訪ね先は4バイト(int)だよ」というルールをセットで持たせているんですね。

訪ね先は4バイトだから4バイト分の中身を見てきたらいいよという指定だったんだね

型は「探索範囲」の指定

  • int *p; なら、「住所へ行ったら、そこから4バイト分をガサ入れしてこい!」という命令。
  • double *p; なら、「住所へ行ったら、そこから8バイト分をガサ入れしてこい!」という命令。

多くの人が「int *p」の int を見て「4バイトの箱ができる」と勘違いしてしまいますが、ポインタ変数自体のサイズは8バイトです。(64びっとPCの場合)

intで宣言したのにdoubleの値が入ってる変数の住所渡しちゃったらどうなるか気になったのでプログラム作って実行してみたよ

int *p;
double a = 1.23; 
p=&a;
printf("住所%pの中身は%dです。\n",(void *)p,*p);//あえてintで表示
return 0;
kadai2.c: In function 'main':
kadai2.c:5:6: error: assignment to 'int *' from incompatible pointer type 'double *' [-Wincompatible-pointer-types]
    5 |     p=&a;   //aの住所をpに入れる
      |      ^

実行したらエラーが出て怒られました。型があってないと先へ進めないことがわかりましたのでちゃんと合わせていきたいと思います。

そのエラーメッセージこそが、C言語が「ポインタの型」を厳しく管理している証拠です。

  • assignment to 'int *' from incompatible pointer type 'double *'
    (意訳:int * 用の箱に、全然ちがう double * という種類の住所を入れようとしてるよ!ダメだよ!)

コンパイラは、このまま実行するとデータが壊れることがわかっているので、親切に止めてくれたわけです。

ちなみにpは8バイトだけどPCに積んであるメモリを増やすほど住所も増えるでしょ?8バイトという少ない領域でどんどんメモリを増設しても対応できるの?

結論から言うと、8バイト(64ビット)あれば、現代のPCどころか未来のスパコンでもお釣りがくるくらいのメモリを扱えます。

なぜ「たった8バイト」で大丈夫なのか、数字で見てみると納得できるはずです。

8バイトで表現できる「住所」の数

8バイトは64ビットなので264通りの住所を表現できます。これを具体的な数字にすると……
約1844京(けい)通り です。

メモリ容量に直すと?

1つの住所に1バイトのデータがあるとすると、8バイトのポインタが指し示せる最大容量は:
160億ギガバイト (GB)
今の一般的なパソコンがメモリ16GB〜32GBくらいですから、今のPCの10億倍くらいメモリを増設しても、8バイトのポインタなら余裕で住所を書き込めるということになります。

昔は4バイト(32ビット)だった

一世代前の32bitパソコンでは、ポインタは4バイトでした。
4バイトで表現できるのは約42億通り、つまり 4GB まで。
「昔のPCでメモリを4GBより多く積んでも意味がなかった(認識しなかった)」のは、ポインタという住所録の箱が4バイトしかなくて、それ以上の番地を書き込めなかったからなんです。

なので、「8バイトへの進化」は、コンピュータの世界ではとてつもない余裕を手に入れた革命だったんですね。

余計な心配はしなくてよかったですね・・・。

つぎはようやく、int型の変数から、ポインタを使って中身を書き換えるという、ポインタ本来の便利な使い方に進んでみようと思います。