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

#include<studio.h>、メイン関数、リターンときて、ようやくポインタの仕組みまでこぎつけることができました。

ちなみに今使っているノートPCのCPUの型番の話をしてたのでそれにまつわるプログラムになっています。

#include <stdio.h>

int main() {
    int value = 3610; // あなたのCPUの型番です
    printf("i7-3610QMのメモリ住所を覗きます...\n");
    printf("住所: %p, 値: %d\n", (void *)&value, value);
    return 0;
}

実行結果はこうなりました。

上から読んできます

int value = 3610; // あなたのCPUの型番です

int型のvalueという変数を宣言して3610で初期化。

printf("i7-3610QMのメモリ住所を覗きます...\n");

画面に「i7-3610QMのメモリ住所を覗きます…」を表示して改行。ちなみにi7-3610QMは私のPCのCPUの型番で13年前の第3世代のCorei7です

    printf("住所: %p, 値: %d\n", (void *)&value, value);

わ、、わからん%p(void *)&valueがわからん。変数に「&」をつけるとその変数の住所だといってたっけ??でも(void *)これはなんだ・・・。ジェミニ先生に聞いてみよ。

ポインタの第一歩「変数がメモリのどこにあるか(住所)」の確認

このソースコード見てください。

このソースコード見てください。

そのソースコード、ポインタの第一歩である「変数がメモリのどこにあるか(住所)」を実際に確認する面白いコードですね!

内容を簡単に解説すると、こんなことをしています:

  1. int value = 3610;
    メモリの中に「3610」という値を入れるための箱(変数)を用意します。
  2. &value
    変数の名前の前に & をつけることで、値そのものではなく、その箱が置いてある「メモリ上の住所(アドレス)」を指します。
  3. %p と (void *)&value
    %p はアドレスを表示するための専用の書式です。実行するたびに(あるいはPCごとに)、0000007865BFF704 のような16進数の住所が表示されます。
実行した時のイメージ

「値(3610)」が「中身」で、「住所(%pの結果)」が「そのデータが保存されている場所」という関係がわかると、C言語の最難関と言われるポインタの理解がぐっと楽になります。

なるほど住所は16進数の数字(数字なのこれ?アルファベットもあるけど)がついてるんだね?実行すると変数valueの住所の数字が見れるんだ。

なるほど住所は16進数の数字(数字なのこれ?アルファベットもあるけど)がついてるんだね?実行すると変数valueの住所の数字が見れるんだ。

はい、その通りです!

「これ数字なの?」という疑問、鋭いですね。実はそれ、16進数(じゅうろくしんすう)という数え方なんです。

16進数ってなに?

私たちが普段使っているのは、0〜9までいったら桁が上がる「10進数」ですが、コンピュータの世界では「16までいったら桁が上がる」16進数がよく使われます。

  • 0〜9 までは数字。
  • 10〜15 までは足りないので、a, b, c, d, e, f というアルファベットを数字の代わりに使います。
    • a = 10
    • f = 15
  • その次は 10(16のこと)に桁が上がります。

なので、住所にアルファベットが混ざっていても、コンピュータにとっては立派な「数値(番号)」なんです。

なぜ住所は16進数なの?

2進数(0か1か)だと長すぎて人間には読めず、10進数だとコンピュータのキリの良い数字(2の倍数)と相性が悪いため、その中間の16進数が一番管理しやすいから採用されています。

実行するたびにその「住所」が変わるのも、OSが「空いてる場所はどこかな?」と毎回探して割り当てているからなんですよ。

次は、その住所を「別の変数に保存して、遠隔操作する」というポインタの魔法を試してみますか?

アドレスは数字でなく数値ね

アドレスは数字でなく数値ね

まさにその通りです!「数値」として扱うのが正解ですね。

単なる背番号のような「記号」ではなく、足したり引いたりできる計算可能なデータとしてメモリのアドレスは存在しています。

C言語では、この「住所という数値」を専用の変数(ポインタ変数といいます)に代入して、以下のような操作ができます。

  • 住所に1足す:隣のデータの住所へ移動する
  • 住所の中身を見る:その番号の場所に書き込まれている値を取り出す

「住所(数値)」さえわかれば、プログラム内のどこからでもそのデータにアクセスできるのがC言語の強力な(そして少し危険な)ところです。

アドレスは16桁なの?

住所: 000000376E9FF8DC, 値: 3610が実行結果でした。数えてみると16桁もあります!

アドレスは16桁なの?

今の主流である64bitのパソコンであれば、住所(メモリアドレス)をフルで表示すると16進数で最大16桁になります。

これにはコンピュータのデータの数え方が関係しています。

  • 2進数から16進数への変換:16進数の1桁は、2進数の4ビット(4桁)分をちょうど一文字で表せます。
  • 64bitマシンの計算 なので、64bitの住所を過不足なく表現するには16桁が必要になります。

4ビット(16進数で1桁)×16=64ビット(16進数で16桁)ということだね。

その通り!完璧な理解です。

計算式にするとまさにこうなります:
4ビット(24=16通り) × 16桁 = 64ビット

この「4ビットで1桁」というキリの良さが、プログラミングで16進数が愛される最大の理由です。

実際に見える桁数が少ないこともある理由

プログラムを実行したときに「16桁もないよ?」と思うことがあるかもしれません。それには以下の理由があります:

  1. 上の桁が「0」だと省略される
    例えば、住所が 0000000000401000 だった場合、画面には 401000 とだけ表示されることがあります。
  2. 昔の環境(32bit)の場合
    少し古いパソコンや設定だと32bitで動くため、その場合は  で、最大8桁になります。

見た目の例

  • フル表示(16桁)0x00007ffe6b3a21a8 (0xは16進数だよという印です)
  • 省略表示0x7ffe6b3a21a8 や 401000

あなたの手元の環境で実行したとき、何桁表示されましたか?

実行結果「住所: 000000376E9FF8DC, 値: 3610 」やけに0が多いなって思っちゃった

その感覚、大正解です!まさに「やけに0が多い」のには理由があります。

表示された 000000376E9FF8DC を見ると、確かに頭に 0が6つ も並んでいますね。これは、「64ビット(16桁)という巨大な住所の部屋があるけれど、今はまだ若い番号の場所を使っている」という状態だからです。

なぜ0がたくさん並ぶのか?
  1. 器(うつわ)がデカいから
    今のパソコン(64bit)は、理論上264という天文学的な数の住所を扱えます。16桁フルで使うと、ものすごい量のメモリを管理できるのですが、普通のプログラムが使う範囲では、まだ「上の桁」まで番号が埋まることはまずありません。
  2. ホテルの部屋番号に例えると
    「1億号室まである超巨大ホテル」に泊まったとき、あなたの部屋番号が「00003610号室」だったようなものです。上の桁の「0000」は、「まだそこまで部屋を使ってないよ」という空きスペースを意味しています

私の実行結果には16進数を表す「0x」ついてないね

それは鋭い観察眼ですね!

実は、C言語の printf("%p", ...) で住所を表示するとき、「0x」が付くかどうかはコンパイラ(プログラムを動かす道具)の種類や設定によって変わるんです。

なぜ「0x」が付かないことがあるの?
  1. コンパイラの違い:
    Windowsでよく使われる「Visual Studio (MSVC)」などは、デフォルトで 0x を付けずに大文字の16進数だけを表示することがあります。逆に、MacやLinuxで標準的な「GCC」などは、親切に 0x7ffe... と付けてくれることが多いです。
  2. 書式のルール:
    %p という書き方は「その環境で一番標準的なアドレスの書き方で出してね」という命令なので、OSやツールのご作法に任されている部分があるんです。
「0x」の意味をおさらい

もし自分で「これは16進数だよ!」と明示したいときは、コードの中で数値の頭に 0x を書くのがC言語のルールです。

  • 10 → 10進数の「じゅう」
  • 0x10 → 16進数の「じゅう」(10進数だと16)

お手元の画面に 000000376E9FF8DC と出ているなら、それはコンピュータが「余計な飾り(0x)抜きで、生の住所(数値)を教えるね!」と言っている状態ですね。

(void *)&valueこの変数valueの住所の前につく「(void *)」はどういう意味なの?

(void *)&valueこの変数valueの住所の前につく「(void *)」はどういう意味なの?

それはめちゃくちゃいい質問ですね!初心者だとスルーしがちな、とても大事なポイントです。

結論から言うと、それは「どんな型のデータが入っているか気にせず、とにかく『住所』として扱ってね!」という「型変換(キャスト)」の命令です。

なぜ (void *) が必要なの?

C言語の printf で %p を使って住所を表示するとき、ルールとして「汎用的な住所(void型のポインタ)」を渡すことになっているからです。

  1. 本来の姿: &value は、中身が int(整数)なので「整数用の住所(int *)」というラベルが付いています。
  2. 型変換(キャスト): (void *) を付けることで、「中身が整数かどうかは一旦置いといて、ただの『住所』というデータとして見てね」と、無理やりラベルを貼り替えています。
もし (void *) を書かなかったら?

最近のコンパイラ(プログラムを翻訳する道具)は賢いので、書かなくても気を利かせて表示してくれることが多いです。

でも、厳密なルール(C言語の規格)では%p には void * を渡すべき」と決まっています。真面目なコンパイラだと、「型が違うよ!」と警告(Warning)を出してくることもあります。

void って何?

プログラミングで void は「空(から)」とか「決まっていない」という意味です。

  • int *:整数の住所
  • char *:文字の住所
  • void *何でもありの住所(住所界のフリーサイズ)

「とにかく住所を表示したいだけだから、中身の型(intとかcharとか)はどうでもいいよ!」という時に、この (void *) が活躍します。

汎用的な住所って意味が分からなかった

「汎用的な住所」という言葉、少し分かりにくかったですね。すみません!

簡単に言うと、(void *) は「中身が何であれ、ただの『番地(数字)』として扱うよ」という印です。

専用の住所」と「汎用的な住所」の違い

例えば、あなたが「荷物」を運ぶと想像してみてください。

  • 専用の住所 (int *)
    「ここは『リンゴ(int)』が入っている箱の住所ですよ」という情報付きのメモです。
  • 汎用的な住所 (void *)
    「中身が何かは知らないけれど、とにかく『3番地』ですよ」という、数字だけのメモです。
なぜ printf では「汎用的」にするの?

printf の %p という機能は、「中身がリンゴだろうが、ミカンだろうが、とにかく『番地(数字)』を表示する」のがお仕事です。

もし int *(リンゴ用)のまま渡すと、printf は「えっ、これはリンゴ専用のメモだよね? 私はただの数字として表示したいだけなんだけど、本当にこれでいいの?」と慎重になって、警告(エラーの一歩手前)を出すことがあります。

そこで、(void *) と書くことで、「中身の情報(int)は捨てて、ただの『番地』というデータにしてから渡すね!」と、printf が扱いやすい形に変換してあげているんです。

まとめると
  • int * = 「中身が整数」という情報を持った住所
  • void * = 「中身は不明」だけど、とにかく場所だけを示す住所(汎用的)

「住所を表示するだけ」なら中身の情報はいらないので、この (void *) を使って「ただの住所」に変身させている、というわけです。

あーーわかった中身の型がdoubleでもintでもcharでも必要なのは住所だけってことね!ちなみにvalueは最初にint型と定義してるのにint * じゃないんだね

あーーわかった中身の型がdoubleでもintでもcharでも必要なのは住所だけってことね!ちなみにvalueは最初にint型と定義してるのにint * じゃないんだね

その通り!「中身が何であれ、住所という『数値』さえあれば表示できる」という理解、完璧です!

「なぜ int * じゃないの?」という疑問についても、実はすごく深いところに気づきましたね。

1. 内部的には int * なんです

プログラムの中で &value と書いた瞬間、それは自動的に int 型の変数の住所(int *)」 というラベルが付いた状態になっています。

2. なぜわざわざ (void *) に書き換えるのか?

それは、printf の %p という機能の「受け入れ口」が void * 専用に設計されているから です。

【C言語の (void ) をアパートで例えると】

メモリを巨大なアパートだと想像してみてください。

  • int * や double * (入居者情報付きの住所メモ)
    「101号室のイントさん(int型)宅」
    「202号室のダブルさん(double型)宅」
    というように、部屋番号と住人の名前がセットになったメモです。
  • printf の %p (住所を表示する受付窓口)
    この窓口には、ひとつの厳しいルールがあります。
    「入居者が誰かは(intでもdoubleでも)気にしません。とにかく純粋な『部屋番号』だけの形式で出してください」
  • そこで (void ) の出番!
    入居者情報付きのメモから、住人の名前を隠して「部屋番号」だけにする
    目隠しシール*のような役割です。これを使うことで、窓口(%p)に正しく住所を渡せるようになります。

3. もし int * のまま渡したら?

実は、現代のほとんどのコンパイラでは、int * のまま渡しても 「あ、これ int の住所だけど、どうせ表示するだけだから勝手に void * として扱うね」 と気を利かせて、正しく表示してくれます。

でも、C言語の厳格なルール(標準規格)では、
%p を使うときは、必ず void * 型に変換して渡しなさい」
と決められているんです。だから、教科書や丁寧なコードでは、行儀よく (void *)value と書くのが習慣になっています。

ちなみに、これは printf 関数だけの特別ルールなの? %p を使うときは全部 (void *) にしなきゃいけないの?

ちなみに、これは printf 関数だけの特別ルールなの? %p を使うときは全部 (void *) にしなきゃいけないの?

はい、基本的には%p を使うなら (void *) とセット」と覚えてOKです!

厳密には printf だけのルールではありません。以下の2つの理由から (void *) を付けるのが正解です。

  1. 「表示の専門窓口」だから
    %p という窓口(書式指定子)は、「どんな型の情報もいらないから、とにかく『純粋な住所データ(void *)』として渡してね」という設計になっています。
  2. C言語の公式ルール(マナー)
    最近のコンパイラは賢いので、(void *) をサボっても気を利かせて表示してくれます。でも、C言語の公式なルール(規格)では「%p には void * を渡すこと」と決まっています。

ようやくこの短いソースコードの意味をすべて理解することができました!次はポインタ変数で中身を入れ替えるとかいうのに挑戦。