ポインタ:アドレスモデルと間接参照
R — 原文
「アドレスによって目的の変数ユニットを見つけられるので、アドレスはその変数ユニットを指していると言える……したがって、アドレスを分かりやすく「ポインタ」と呼ぶ。つまり、それをアドレスとするメモリユニットを見つけられるという意味である。」 ——第8章 §8.1 ポインタとは
「まず、ポインタの意味を正確に理解する必要がある……『ポインタはアドレスである』と明確にすれば、理解が容易になり、多くの問題も解決する。」 ——第8章 §8.9 ポインタに関するまとめ(1)
「ポインタとポインタ変数を区別すること。ポインタはアドレスであり、ポインタ変数はアドレスを格納するための変数である。」 ——第8章 §8.9 ポインタに関するまとめ(3)
「p の値が NULL であることと、p に値が代入されていないことは、異なる概念である。前者は値がある(値は0)が、どの変数も指していない。後者は p に値が代入されていないが、p に値がないことを意味するわけではなく、その値は予測できない値である……したがって、ポインタ変数を参照する前に、それに値を代入すべきである。」 ——第8章 §8.9 ポインタに関するまとめ(8)
I — 自己紹介
ポインタが難しい理由は、2つの層を同時に扱うからです:アドレス(位置番号)と、その位置にどのような型のデータが格納されているか。この2層を分ければ、章全体が理解できます。
第一層:アドレス。 プログラム内の各変数はコンパイル時に数バイトのメモリを割り当てられ、各バイトには番号(アドレス)が付けられます。これはホテルの部屋番号に相当します。部屋番号で部屋が見つかるので、「アドレスはそのユニットを指す」と言い、アドレスを比喩的に「ポインタ」と呼びます。これが本書の最も核心的な一文です:ポインタとはアドレスである。
変数にアクセスするには2つの方法があります:直接アクセスは変数名で読み書きします(コンパイラ内部に「変数名→アドレス」の対応表があるので、名前でアドレスを見つけられます)。間接アクセスは、まずアドレスを別の変数に格納し、それを経由して目的の変数を見つけます。その「アドレスを専門に格納する変数」がポインタ変数です。
第二層:型。 アドレスだけでは不十分です。「2000番地からデータを取得せよ」と言われても、1バイト(文字)、2バイト(短整数)、4バイト(整数)のどれを取得すべきか、また2の補数表現か浮動小数点形式かがわかりません。そのためCのアドレスは型付きアドレスです:単なる位置情報だけでなく、「指し示すデータの型」も含んでいます。これが、ポインタ変数を定義する際に基本型を必ず指定しなければならない理由です(int *p の int):これによって *p が何バイト読み取るか、どの形式で解釈するかが決まり、また p+1 が1バイト進むのか4バイト進むのかも決まります。
これより、間接参照 *p の2つの要点が導かれます:*p は「p が指している変数」を表し、読み取りも書き込みもできます(*p=3 は3をpが指すユニットに格納するのであって、3をpに格納するわけではありません)。一方、p 自体の値はアドレスであり、p を変更するということは「誰を指しているか」を変えることです。
最も混同しやすい3つの概念は、厳密に区別する必要があります:
- ポインタ=アドレス(値、例:2000)
- ポインタ変数=アドレスを格納する変数(その値がアドレス)
- 基本型=指し示すデータの型
したがって「変数iのポインタは2000である」と言えますが、「iのポインタ変数は2000である」とは言えません。ポインタ変数はp自体であり、2000ではありません。
安全な使用法はただ一つの鉄則:使う前に、有効なオブジェクトを指すようにすること。 ポインタ変数を定義した後に値を代入しないと、その値はメモリ内の残存ゴミであり、どこを指しているかわかりません。この状態で *p = 3 を実行すると、無関係なメモリをランダムに破壊し、症状がずっと後になって現れることがあります。3つの状態を区別する必要があります:①未代入(ゴミ値、危険)、②値が NULL(どのオブジェクトも指さないと明確に示しており、チェック可能な状態)、③有効なオブジェクトを指している(唯一、間接参照可能な状態)。NULL は stdio.h で0と定義されており、システムは0番地を他の用途に使用しないことが保証されています。
ポインタの真の用途は3つあります(原書 §8.9 の最後にまとめ):プログラムの効率向上;関数が呼び出し元の変数を変更し、複数の結果を返せるようにする;動的メモリ割り当てを実現する(連結リストなど)。同時に原書は、ポインタは「柔軟すぎる」ため、エラーはしばしば隠れてプログラム全体を破壊する可能性があると警告しています。使用時は細心の注意が必要です。
A1 — 書中の事例
- 例8.2 2つのポインタの指す先を交換する(原書は実行結果を示す):a=5、b=9 を入力、
if(a<b){p=p1;p1=p2;p2=p;}の後、a、b の値は交換されていません(まだ5と9のまま)が、p1、p2の指す先は交換されているので、*p1、*p2を出力すると9と5が得られます。原書の原文:「a と b の値は交換されておらず、元の値を保持しているが、p1 と p2 の値は変更されている。」この事例は、「ポインタの指す先を変更すること」と「目的の変数の値を変更すること」を正確に区別しています。 - 例8.3 swap でポインタを渡して目的の値を交換する(原書は結果を示す):仮引数は
int *p1, int *p2、関数内でtemp=*p1; *p1=*p2; *p2=temp;とすると、*p1、*p2が変更されるため、a、b は実際に交換されます。同じ「交換」という操作で、例8.2 は指す先を交換し、例8.3 は値を交換します。この対比はpと*pの違いを理解するための最良の比較材料です。 - §8.1 アドレスと内容の違い(原書が強調):int 変数 i が 2000〜2003、j が 2004〜2007、k が 2008〜2011 に割り当てられていると仮定すると、
k=i+jは「2000番地からiを取り出し、2004番地からjを取り出し、加算して2008番地に送る」という意味です。scanf を使用する場合はscanf("%d",&i)で値を2000番地からのユニットに送ります。この説明は「変数名→アドレス→型に従ってデータを取り出す」という全プロセスを明らかにしています。
A2 — 発動タイミング
ユーザーは以下のような状況でこのスキルを必要とします:
- ポインタの学習中に詰まって、
*p、&a、pがそれぞれ何なのか説明できない - 関数で呼び出し元の変数を変更したいが、誰かが「ポインタを渡せ」と言うが、なぜそうするのか理解できない
- プログラムがランダムにクラッシュする、または無関係な変数の値が突然変わった
- 「ポインタ」と「ポインタ変数」という言葉の区別がつかない
int *pの*と*pの*が同じものかどうかを知りたい- 配列、文字列、連結リストのあちこちにポインタが登場し、根本で何が起こっているのか見えない
- ポインタを使ったが初期化しておらず、再現困難な障害が発生した
言語的な信号:「ポインタとは何か」「ポインタはアドレスであるとはどういうことか」「*p と &a の違い」「ポインタ変数とポインタの違い」「なぜ基本型を指定する必要があるのか」「p+1 はなぜ4増えるのか、1ではないのか」「ダングリングポインタ/ヌルポインタ」「segmentation fault」「関数にポインタを渡すと実引数が変更できる理由」。
隣接するスキルとの区別:本スキルは「アドレスモデル+ポインタのセマンティクス+安全ルール」を扱います。問題が「ポインタを使って配列を走査する書き方」→ 先に array-and-string を使用し、その後本スキルに戻ってください。「連結リストの作り方」→ 同じパッケージ内の「構造体と連結リスト」スキルカードを使用してください。「関数の引数の書き方」→ function-and-recursion を使用してください(引数渡しのメカニズム部分は本スキルと補完関係にあります)。
E — 実行手順
入力契約:必要なものは①ユーザーの現在の混乱点(概念不明/コードの動作が期待と異なる/クラッシュ)、②関連するコード断片、③クラッシュが関係する場合は再現条件とエラーメッセージ。「ポインタがうまく使えない」といったあいまいな説明だけでは、まずユーザーに具体的なコードや問題を提示してもらわなければ、概念の説明しかできません。
出力契約:概念的な状況では「3層分解(アドレス/ポインタ変数/基本型)+1つの実行可能な比較例」を提供。デバッグ状況では「問題の分類(ダングリングポインタ/型不一致/整数をアドレスとして使用/ポインタとポインタ変数の混同)→ 原因 → 修正方法」を提供します。
手順:
- 2層を分離する:まずユーザーが「アドレスの層が不明」なのか「型の層が不明」なのかを確認します。完了基準:ユーザーが具体的にどの文を理解していないかを言えること。
- アドレスモデルを構築する:「部屋番号」「引き出しの鍵を別の引き出しに入れる」などの比喩を使って、直接アクセス vs 間接アクセスを説明します。完了基準:ユーザーが
&aとは何か、p=&aは何をしているかを自分で説明できる。 - 3つの概念を区別する:「ポインタ(アドレス、値)」「ポインタ変数(アドレスを格納する変数)」「基本型(何を指すか)」を明確にします。完了基準:ユーザーが「p の値は何か」「*p は何か」「p の型は何か」の3つの質問に正しく答えられる。
*の二義性を説明する(判定停止点):定義文int *p;の*は単に「これはポインタ変数です」という印;式の中の*pは「間接参照」です。ユーザーがこの2つを混同していたら → 手順3に戻って再説明し、先に進まない。- 基本型の一致を確認する:
int *pはint型変数のアドレスだけを受け入れられます。floatのアドレスをint*に代入すると、バイト列を誤った形式で解釈します。完了基準:代入の両側で型が一致していること。 - 安全性チェック(必須):間接参照されるすべてのポインタについて、①有効なオブジェクトを指すように代入されている、または②明示的に
NULLが設定され、かつNULLチェックが行われていることを確認します。完了基準:「定義後に初期化されずに*pが使用される」パスが見つからないこと。 - 実行可能な比較例を提示する:「指す先の交換 vs 値の交換」の対比(例8.2 / 例8.3)を使って、
pと*pの違いを示します。完了基準:ユーザーが2つのコードのそれぞれの出力を予測できる。
B — 境界
- 適用外:ユーザーが単に「ポインタで配列を走査したい」だけで原理に関心がない場合、書き方を直接示すだけで十分であり、アドレスモデル全体を押し付ける必要はありません。
- 動的メモリの範囲を明確に区別する(本カードのA2で malloc/free に言及していますが、範囲の深浅には境界があります):
- カバーする範囲:
mallocによる割り当て、freeによる解放、free を忘れるとメモリリークが発生する(プログラムの実行が長くなるほど占有量が増える)、プログラム終了時にOSがプロセス全体のメモリを回収する(小さなプログラムのリークは次回の実行に影響しないが、長時間稼働するサービスはメモリを使い果たす)。 - カバーしない範囲:メモリ所有権モデル(誰が解放する責任を負うか)、ダングリングポインタ(free 後に同じポインタを使い続ける)、重複free、ポインタのエイリアシングによる最適化問題、
restrict、厳格なエイリアスルール、マルチスレッドでの共有ポインタの競合状態。 - ユーザーが「free した後もこのポインタを使えるのか」と尋ねた場合→ 本カードでは「使えない、動作は未定義、NULL を設定すべき」と答えるだけですが、より深い所有権の設計については本書の範囲外であることを説明する必要があります。
- カバーする範囲:
- 著者の盲点(フェーズ0 批判):本書のポインタ解説は「アドレス=部屋番号」という比喩を中心としています。この説明は
&/*の理解に非常に有効ですが、メモリ所有権、ライフタイム、ダングリングポインタ、ポインタのエイリアシングによる最適化問題については扱っていません。ユーザーが実際の開発で free 後の使用、マルチスレッドでのポインタ共有などの問題に遭遇した場合、本カードは基礎的な判断を提供するだけであり、範囲外であることを説明する必要があります。 - 混同しやすい点:①
*は定義文と式で意味が異なる;②&は「アドレス取得」と「ビットごとのAND」の2つの意味を持ち、文脈で区別する;③NULL(値がある、0)≠ 未代入(値がない、ゴミ値);④配列名は先頭要素のアドレス(ポインタ)であるが、ポインタ変数ではない——代入できない(a = pは不正)。 - 失敗パターン:ポインタを「高度なテクニック」として持ち上げ、他人には読めない
*(*(p+i)+j)のような式を書くこと。本書の立場は明確です:学習初期段階では「C言語の、エラーを起こしやすい細部を早期に使うべきではない」、優先すべきは明確で読みやすいコードを書くことです。 - 安全上の赤線:ポインタが有効であることを確認しないまま、間接参照や書き込み操作をユーザーに推奨してはなりません。システムの機密アドレス(0番地、ハードウェアアドレスなど)に関わる場合は、原理のみを説明し、直接実行可能な書き込み操作の例は提供してはなりません。