C言語のきほん|アドレスとポインタの関係

見えないメモリの住所が見えてくると、C言語はぐっとおもしろくなる。

C言語を学んでいると、変数に値を入れる、配列にデータを並べる、といった操作にはだんだん慣れてきます。けれども、そこで次に大切になってくるのが、その値がメモリのどこに置かれているのかという視点です。

C言語では、値そのものだけでなく、値が保存されている場所もとても重要です。
その「場所」を表すのがアドレスであり、そのアドレスを扱うための仕組みがポインタです。

最初は、ポインタという言葉を聞くだけで少し身構えてしまうかもしれません。
見えないメモリを相手にするので、難しそうに感じるのも自然なことです。ですが、考え方をひとつずつ整理していけば、ポインタは決して特別な魔法ではありません。

たとえば、変数は「値を入れる箱」とよく説明されます。
その箱には中身だけでなく、置かれている場所があります。
C言語では、その場所の情報を使って箱を見つけたり、別の場所から箱の中身を操作したりできます。

このときに登場するのが、次の3つの考え方です。

用語意味たとえ
変数の中に入っている中身箱の中身
アドレスその変数が置かれている場所箱の住所
ポインタアドレスを覚えている変数住所を書いたメモ

この関係がわかると、なぜ scanf で変数に & を付けるのか、なぜ関数に値ではなくアドレスを渡すことがあるのか、といったことが一気につながって見えてきます。

つまり、ポインタは難しい文法として覚えるより、値のある場所をたどる仕組みとして理解するのがコツです。

ここでは、「アドレスとポインタの関係」をやわらかく整理しながら、
ポインタとは何か、なぜ必要なのか、そしてどう使うのかを丁寧に見ていきます。
図や表も交えながら、頭の中にイメージが残る形で理解を深めていきましょう。

ポインタとは何か

ポインタとは、メモリのアドレスを記憶する変数のことです。

通常の変数は、たとえば整数そのものを保存します。
一方でポインタ変数は、整数や文字そのものではなく、どこにそのデータがあるかという場所の情報を保存します。

たとえば、次のように考えるとイメージしやすくなります。

  • 普通の変数
    → 数値そのものを持っている
  • ポインタ変数
    → 数値が置かれている場所を持っている

この違いはとても大切です。

たとえば変数 score に 80 が入っているとします。
このとき、score には 80 という値があります。
さらに、score はメモリ上のどこかに置かれているので、score にはアドレスもあります。

そして、そのアドレスを保存したものがポインタです。

もの
変数 score の値80
変数 score のアドレスたとえば 0x1000
score のアドレスを保存するポインタ0x1000 を記憶する変数

つまりポインタは、変数そのものを持つのではなく、その変数へたどり着くための手がかりを持つ存在です。

ポインタをリモコンで考える

ポインタは、よく「リモコン」にたとえられます。
このたとえはとてもわかりやすいです。

テレビ本体の前まで行かなくても、リモコンがあれば離れた場所から操作できます。
同じように、変数そのものを直接扱わなくても、ポインタがあればその変数の場所をたどって操作できるわけです。

ここで大事なのは、ポインタ自身が値そのものではないことです。
ポインタはあくまで対象へアクセスするための情報を持っています。

たとえば、部屋の中にテレビがあり、リモコンにはそのテレビを操作するための機能があるとします。
この関係をC言語に置き換えると、次のようになります。

たとえC言語での対応
テレビ本体変数や配列
テレビのある場所アドレス
リモコンポインタ
リモコンで操作するポインタ経由で参照・変更する

このように考えると、ポインタは「難しい記号」ではなく、離れた場所にあるデータへ届くための道具だと理解しやすくなります。

図:ポインタは変数の住所を覚える

この図では、左側に変数 box があり、その中に値 25 が入っています。
そしてその変数には、たとえば 1000番地のようなアドレスが割り当てられています。
右側にはポインタ p があり、その中には 1000 というアドレスが保存されています。

ここでのポイントは、p の中に 25 が入っているのではなく、25 が入っている変数の場所が入っていることです。
つまり、ポインタは値を直接持つのではなく、値のある場所を指しているのです。

なぜポインタが必要なのか

ここで、こんな疑問が出てくるかもしれません。

変数をそのまま使えばいいのに、なぜわざわざポインタを使うのか。

この疑問はとても自然です。
実際、簡単なプログラムではポインタを使わなくても書ける場面はたくさんあります。

それでもC言語でポインタが重要なのは、別の場所にあるデータを柔軟に扱えるからです。

たとえば、関数Aで用意した変数を、関数Bで変更したい場面があります。
このとき、値だけを渡すと、関数Bはそのコピーしか受け取れません。
しかし、アドレスを渡せば、関数Bは元の変数そのものを操作できます。

この違いは、C言語の関数を理解するうえで非常に重要です。

渡すもの関数の中で起こること
元の変数のコピーを受け取る
アドレス元の変数そのものにアクセスできる

つまりポインタは、単なる知識ではなく、関数どうしでデータを正しく受け渡すための土台でもあるのです。

別の関数から操作できるしくみ

ポインタの便利さは、関数をまたいでデータを扱うときによくわかります。

たとえば、main 関数の中で作った変数を、別の関数で書き換えたいとします。
このときに、ただ整数の値を渡すだけでは、別の関数の中で変更しても元の変数は変わりません。

けれども、その変数のアドレスを渡せば、別の関数はその場所をたどって元の値を書き換えられます。

これは、まさに「隣の部屋の機器をリモコンで操作する」イメージに近いです。
ポインタは、今いる場所とは別の場所にあるデータへ手を伸ばすための仕組みだと考えると理解しやすいでしょう。

図:関数にアドレスを渡すと元の変数を変更できる

この図では、main 関数の中にある変数 number が 10 を持っています。
そのアドレスを、別の関数 update に渡します。
update 関数は受け取ったアドレスを使って、main 関数の変数 number の中身を直接 20 に変更します。

この図で見てほしいのは、関数間で値がコピーされているのではなく、同じ変数の場所を共有して操作している点です。
ここにポインタの大きな役割があります。

scanf とポインタの関係

C言語の学習を進めていると、すでに scanf を使ったことがあるはずです。
そのとき、多くの場合は次のように書いてきたと思います。

scanf("%d", &num);

ここで付いている & は、変数 num の値を渡しているのではなく、変数 num のアドレスを渡していることを意味します。

なぜアドレスを渡すのでしょうか。
それは、scanf が入力された値を、呼び出し元の変数に書き込む必要があるからです。

もし値だけを渡したら、scanf はどこに書き込めばよいかわかりません。
でもアドレスが渡されれば、「この場所に入力値を入れればいい」と判断できます。

このしくみは、ポインタの考え方そのものです。

書き方意味
numnum に入っている値
&numnum が保存されている場所
scanf("%d", &num)num の場所を scanf に教える

つまり、scanf はポインタの実用例としてとても身近な存在です。
ポインタは特別な世界の話ではなく、すでに使っていた機能の中に自然に登場しているのです。

サンプルプログラムで見る

ここでは、元の例とは別の、もっとシンプルなプログラムでポインタの基本を確認してみましょう。
整数型の変数のアドレスをポインタに入れ、そのポインタを使って値を変更する例です。

ファイル名:11_3_1.c

#include <stdio.h>

int main(void)
{
    int temperature = 26;     /* 室温を表す変数 */
    int *ptr = &temperature;  /* temperatureのアドレスを保存するポインタ */

    printf("変更前の室温: %d度\n", temperature);
    printf("ptrが指しているアドレス: %p\n", (void *)ptr);

    *ptr = 30;  /* ポインタを使ってtemperatureの値を変更する */

    printf("変更後の室温: %d度\n", temperature);

    return 0;
}

プログラムの読み取り方

このプログラムでは、まず temperature という整数型の変数を用意し、26 を代入しています。

int temperature = 26;

この時点で、temperature はメモリ上のどこかに保存されています。
その場所にはアドレスがあります。

次に、temperature のアドレスをポインタ ptr に代入しています。

int *ptr = &temperature;

ここでのポイントは2つあります。

記述意味
int *ptrptr は int 型の値を指すポインタ
&temperaturetemperature のアドレスを取り出す

つまりこの1行で、ptr は temperature の住所を覚えたことになります。

そのあとで、次の文があります。

*ptr = 30;

ここでの * は、宣言のときの * とは役割が少し違います。
この * は、ptr が指している先の値を表します。

つまり、*ptr = 30; は
「ptr が指している先にある値を 30 に書き換える」
という意味になります。

ptr は temperature を指しているので、結果として temperature の値が 30 に変わります。

実行イメージ

実際のメモリのイメージを、簡単な図として表すと次のようになります。

要素中身
temperature26
temperature のアドレスたとえば 0x7ffeeabc
ptr0x7ffeeabc
*ptrtemperature の値そのもの

このあと *ptr = 30; を実行すると、変化するのは ptr の中身ではなく、ptr が指している先の値です。

実行前実行後
temperature = 26temperature = 30
ptr = temperature のアドレスptr = 同じアドレスのまま

ここがとても大切です。
ポインタを使って値を書き換えても、ポインタ自身が別の場所を指すようになるわけではありません。
あくまで、指している先の中身を変更しているのです。

* と & の役割を整理する

ポインタでは、特に * と & の意味を混同しやすいです。
ここで一度きれいに整理しておきましょう。

記号役割意味
&アドレスを取り出す&temperaturetemperature の住所を取得する
*指している先の値を表す*ptrptr が指す先の値を表す

この2つはセットで登場することが多いです。

たとえば、

ptr = &temperature;

は、temperature の住所を ptr に入れるという意味です。

一方で、

*ptr = 30;

は、ptr が指す先の値を 30 にするという意味です。

この流れを言葉でつなげると、次のようになります。

  1. &temperature で temperature の住所を取り出す
  2. その住所を ptr に保存する
  3. *ptr で、その住所にある実際の値へアクセスする

この3段階の流れがわかると、ポインタの基本はかなり見通しがよくなります。

よくあるつまずきポイント

ポインタを学び始めたときに、多くの人が混乱しやすいポイントがあります。
ここで先に整理しておくと、理解が安定しやすくなります。

ポインタは値そのものを保存しているわけではない

ptr の中に入っているのは、temperature の値 26 ではありません。
入っているのは、temperature が置かれている場所の情報です。

*ptr と ptr は別物

ptr はアドレスです。
*ptr は、そのアドレスの先にある値です。
この2つを同じものとして読んでしまうと、急にわかりにくくなります。

アドレスは環境によって表示が変わる

プログラムを実行すると、アドレスの値は毎回同じとは限りません。
そのため、アドレスの数値そのものを覚える必要はありません。
「ある変数の場所を表している」と理解すれば十分です。

もう一歩やさしく考えるための整理

ポインタを難しく感じる最大の理由は、一度に2つの世界を見ないといけないからです。
ひとつは値の世界、もうひとつは場所の世界です。

  • 値の世界
    26、30、100 などの中身を見る
  • 場所の世界
    その中身がどこにあるかを見る

普通の変数だけを使っているときは、値の世界だけでもある程度プログラムが書けます。
でもC言語では、場所の世界を扱えることが強みになります。
その入口がポインタです。

ですから、ポインタが難しいと感じたら、
「何の値が入っているか」
ではなく
「何の場所を覚えているか」
という見方に切り替えると、かなり理解しやすくなります。

図:& と * の関係

この図では、変数 data があり、その中に 50 が入っています。
data には 3000番地というアドレスがあります。
ポインタ p には 3000 が入っています。

そして、&data は data の住所である 3000 を表し、
*p は 3000番地にある中身、つまり 50 を表します。

この図を見ると、& は場所を取り出す働き、* はその場所にある中身を見る働きだと整理しやすくなります。

scanf が理解できるとポインタも見えてくる

ここまで読むと、scanf でなぜ & を付けていたのかが、かなりはっきりしてきたのではないでしょうか。

scanf は、入力された値を呼び出し元の変数へ書き込む必要があります。
そのためには、どの変数へ書き込むかを知る必要があります。
そこで渡しているのが変数のアドレスです。

つまり、scanf は内部で「受け取ったアドレスの先に値を書き込む」処理をしています。
これはまさに、ポインタの考え方そのものです。

これまで何気なく使っていた &num には、
この変数の場所に書き込んでください
という意味が込められていたわけです。

この視点を持つと、ポインタは急に遠い話ではなくなります。
すでに自分が使っていた機能の中に、ポインタの考え方が入っていたことに気づけるからです。

ポインタの学習で大切にしたいこと

ポインタは、最初から完璧に理解しようとしなくても大丈夫です。
まずは次の流れをしっかり押さえることが大切です。

覚えたいこと内容
変数には値とアドレスがある値だけでなく場所もある
ポインタはアドレスを保存する場所を覚えるための変数
& はアドレスを取り出す住所を調べる
* は指している先を表すその住所にある中身を見る
関数にアドレスを渡すと元の値を変更できるポインタの重要な使い道

この土台ができると、この先に学ぶ配列とポインタの関係、関数へのポインタ渡し、文字列操作、動的メモリ確保なども理解しやすくなります。

ポインタはC言語の中でも特に重要なテーマですが、恐れる必要はありません。
むしろ、ここを丁寧に理解できると、C言語のしくみがとてもよく見えてきます。
値だけでなく、その値がどこにあるのかまで意識できるようになると、C言語の世界が一段深く理解できるようになります。