結論:ポインタは「値そのもの」ではなく「値が置いてある場所(アドレス)を覚えておく変数」です。& が「住所を調べる」、* が「その住所へ行って中身を見る/書き換える」という2つの動作だと分けて覚えれば、迷わなくなります。
なぜポインタは「わからない」「難しい」と感じるのか
「C言語 ポインタ わからない」「ポインタ 難しい」で検索する人のほとんどは、int *p = &x; のような1行で * と & が入り混じって出てくることに混乱しています。しかも * は「宣言のときはポインタ型を作る記号」「使うときは中身を取り出す記号」と、同じ記号が場面によって意味を変えるので、余計にややこしく感じます。
理由ははっきりしています。ポインタは文章で説明されても像を結びにくく、実際に printf でアドレスと中身の両方を表示してみて初めて「ああ、そういうことか」と腑に落ちるタイプの概念だからです。このサイトの C言語コース は、ポインタの章までブラウザ上でそのままコードを実行できます(無料・登録不要・環境構築なし)。以下の説明を読んだら、すぐ実際のレッスンで手を動かしてみてください。
住所で理解する ― &(アドレスを調べる)と *(中身を見る)
一番のつまずきポイントは「変数」と「ポインタ」の関係です。次の例えで整理できます。
- 変数 = マンションの1部屋。中に住所(変数名でアクセスできる値)が入っている。
- アドレス(
&変数名) = その部屋の「住所」。何号室かという番地の情報。 - ポインタ = 「住所を書いたメモ」を持っている変数。メモ自体は部屋の中身ではなく、あくまで住所を指すだけ。
*ポインタ(中身を見る) = メモに書かれた住所へ実際に行って、その部屋の中身を見る・書き換える動作。
コードにするとこうなります。
int x = 10;
int *p = &x; // pにはxの「住所」が入る(xの値そのものではない)
printf("%d\n", x); // 10(部屋の中身を直接見る)
printf("%d\n", *p); // 10(メモの住所へ行って中身を見る=結局同じ部屋)
*p = 20; // メモの住所へ行って中身を書き換える
printf("%d\n", x); // 20(xの中身が変わっている)
p に入っているのは x のコピーではなく住所そのものなので、*p 経由で書き換えると元の x にも反映されます。ここが「値渡し」と根本的に違う点です。
この住所と中身の関係をそのまま動かして確認できるのが、メモリのしくみ ― 変数はどこにある? と ポインタ入門 ― 住所(&)と中身(*) です。どちらもブラウザでそのまま printf("%p", &x) を実行して、実際のアドレスが表示されるところまで体験できます。
ポインタと配列がつながっている理由
ポインタの次のつまずきどころが「配列名は実はポインタっぽく振る舞う」という話です。C言語では配列名を関数に渡すと、配列そのものではなく先頭要素のアドレスが渡されます。だから arr[i] と *(arr + i) は同じ意味になり、配列を関数に渡すときは「サイズも一緒に渡す」必要があります(配列自体は住所しか伝わらないため、大きさの情報が失われるからです)。
この仕組みは ポインタと配列 ― 密接な関係 で、実際に arr と &arr[0] が同じアドレスを指すことを確認しながら学べます。関数に配列を渡すときのサイズの話は 配列を関数に渡す ― 大きさも一緒に渡す が前提知識として先にあると理解が速いです。
「値を書き換えてもらう」にポインタが要る理由
もう一つの代表的なつまずきが「関数に変数を渡しても、関数の中で書き換えた値が呼び出し元に反映されない」という現象です。これはC言語の関数呼び出しが基本的に**値渡し(コピーを渡す)**だからで、関数の中の変数は呼び出し元とは別物の部屋になっています。
呼び出し元の変数を書き換えてもらいたいときは、変数そのものではなくその変数の住所(アドレス)を渡すことで解決します。関数側は受け取った住所へポインタ経由でアクセスし、* を使って直接書き換えます。この一連の流れを実際に書いて確認できるのが ポインタと関数 ― 値を書き換えてもらう です。scanf("%d", &x) の & が必要な理由も、ここまで理解すると自然に納得できます。
アドレスと参照の違い ― C++と比較するとすっきりする
「アドレス と 参照 違い」で調べている人向けに整理すると、C言語のポインタと、C++にある**参照(reference)**は近い役割を持ちつつ性質が違います。
- ポインタ:住所そのものを値として持つ変数。
NULLを入れられる、途中で別の住所を指し直せる、*/&を明示的に書く必要がある。 - 参照:変数の「別名」。宣言時に必ずどれかの変数を指す必要があり、後から別のものを指し直すことはできない。使うときも
*を書かず、普通の変数のように扱える。
つまり参照は「ポインタの中でも、常に有効で書き換え不可な使い方だけに絞って安全にしたもの」とイメージすると理解しやすいです。C++の ポインタ入門 ― 住所と中身 でC言語と同じ &/* の基本を確認したあと、参照 ― 変数に別名をつける と 参照渡し ― 関数に本体を渡す を実行すると、同じ「値を書き換えてもらう」目的をポインタと参照それぞれでどう書くか、両方をブラウザで見比べられます。
さらに先へ ― ポインタのポインタと関数ポインタ
基本が掴めたら、次に出てくる応用が「ポインタのポインタ(**)」と「関数ポインタ」です。
- ポインタのポインタ(
**) ― 動的な文字列配列:住所を書いたメモの、さらにその住所を書いたメモ、という二重構造を扱います。文字列の配列(char **)を動的に作るときに必須の考え方です。 - 関数ポインタ ― 関数を変数のように扱う:関数そのものの「アドレス」を変数に入れておき、後から呼び出す仕組みです。
qsortに比較関数を渡す qsortとbsearchコースのレッスン で実際の使い道が見えてきます。
つまずきやすいところ ― Segmentation fault と double free
ポインタを使い始めると必ず一度は遭遇するのが、実行時に落ちるエラーです。
- Segmentation fault:触ってはいけないメモリ領域にポインタ経由でアクセスしてしまうと起きます。原因の典型と直し方は Segmentation fault ― 触ってはいけないメモリを触った にまとまっています。
- incompatible pointer types / makes integer from pointer without a cast:型の違うポインタを代入したり、ポインタと整数を混同したときに出る警告・エラーです。incompatible pointer types と makes integer from pointer without a cast を参照してください。
- double free:
mallocで確保したメモリを2回freeしてしまう事故です。double free ― 同じメモリを2回解放した と メモリリーク ― free し忘れ を、malloc/free ― ヒープから借りて、自分で返す の直後に読んでおくと、動的メモリ確保でつまずきにくくなります。ダングリングポインタなどよくあるバグ全般は よくあるバグ ― リーク・ダングリング・ダブルフリー に整理されています。
ここで慌てて丸暗記する必要はありません。エラーメッセージが出たら、まず該当ページで「何が起きているか」を確認する、という使い方で十分です。
次に読む
- C言語コースを最初から見る →(変数・関数からポインタ・構造体・ファイル入出力までブラウザで実行しながら学べます)
- メモリのしくみ ― 変数はどこにある? → ポインタ入門 → ポインタと配列 → ポインタと関数(この順で動かすと理解が積み上がります)
- スタックとヒープ ― メモリの使い分け(読み物)(ポインタの先にある「メモリはどこから来るのか」を整理した回)
- C++コースの参照・ポインタの回を見る →(C言語のポインタとの違いを見比べられます)
- C言語のエラー逆引き一覧 →(実行時にエラーで詰まったとき)