C言語のポインタをメモリ可視化で理解。アドレスと間接参照を図解。
&x で x のアドレス、*p で p の指す先int *p = &x;&(アドレス取得)と *(参照)の使い分け。宣言の * は「ポインタ型」の印、式中の *p は「指し示す先」。この区別だけで半分は越えます。
sizeof(int) の結果が異なる環境もあります。実アドレスもOS・実行ごとに変わり、図のように整然と並ぶとは限りません。
&n と書くと、変数 n のアドレス(例:0x1000)が得られます。printf("%p\n", &n); で実際に住所を表示することも可能。
型 *変数名; の形。型はポインタが指す先の型です。int *p は「int型の変数を指すポインタ p」。scanf("%d", &a); と書いていましたね。この「&」こそがアドレスを渡す演算子だったのです。scanf("%d", a); のように & を忘れると、aの中身(不定値)をアドレスとして扱ってしまい、予期せぬ場所に書き込んでプログラムが落ちることがあります。
void swap(int a, int b){ ... } のように書くと、呼び出し元の変数は変わりません。int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; // arr[0] を指す printf("%d\n", *p); // 10 (arr[0]) printf("%d\n", *(p + 1)); // 20 (arr[1]) printf("%d\n", *(p + 3)); // 40 (arr[3]) p += 2; // p は arr[2] を指すようになる printf("%d\n", *p); // 30
| 配列記法 | ポインタ記法 | 意味 |
|---|---|---|
arr[0] | *arr | 先頭要素 |
arr[i] | *(arr + i) | i番目の要素 |
&arr[i] | arr + i | i番目のアドレス |
arr[i] と *(arr+i) は完全に同じ要素を指す。スライダーで i を動かして、両方の記法が同時に同じセルをハイライトするのを見てみましょう。arr[2]*(arr + 2)arr[i] は実は *(arr + i) の糖衣構文。なので驚くべきことに i[arr] (!) と書いても同じ意味(*(i + arr) = *(arr + i))になります。趣味が悪いので書くべきではありませんが、C 言語が「配列アクセス = ポインタ演算」として実装されている証拠。int arr[5] = {10, 20, 30, 40, 50}; int *p1 = &arr[1]; int *p2 = &arr[4]; printf("%ld\n", p2 - p1); // 3(要素数の差)
const は「この変数は後から変更しない」ことをコンパイラと他の開発者に伝える修飾子です。バグの予防と、コードの意図を明確にするのに役立ちます。const int MAX = 100; printf("%d\n", MAX); // OK: 読み取りは自由 MAX = 200; // ❌ コンパイルエラー: const変数は変更不可
#define との違いは、型チェックが効くこと。| 宣言 | 変更できるか | 意味 |
|---|---|---|
const int *pまたは int const *p |
✅ p は変更可❌ *p は変更不可 |
指す先の値を変更できないポインタ |
int * const p |
❌ p は変更不可✅ *p は変更可 |
別の場所を指すように変更できないポインタ |
const int * const p |
❌ p は変更不可❌ *p は変更不可 |
完全にロックされたポインタ |
int a = 10, b = 20; // ① const int *p : 指す先の値を書き換え不可 const int *p1 = &a; p1 = &b; // ✅ OK: pを別の変数に向ける *p1 = 30; // ❌ エラー: 指す先の値は変更不可 // ② int * const p : ポインタ自体を書き換え不可 int * const p2 = &a; *p2 = 30; // ✅ OK: aの値は変えられる p2 = &b; // ❌ エラー: p2は他に向けられない // ③ const int * const p : 両方ロック const int * const p3 = &a; *p3 = 30; // ❌ エラー p3 = &b; // ❌ エラー
const int *p = 「p は ポインタ to const int(= 整数定数へのポインタ)」int * const p = 「p は const ポインタ to int(= 整数への定ポインタ)」// 文字列の長さを返すだけ → 書き換えないので const int my_strlen(const char *s) { int n = 0; while (*s) { s++; n++; } return n; } // 配列の合計を計算 → 書き換えないので const int sum(const int arr[], int n) { int total = 0; for (int i = 0; i < n; i++) total += arr[i]; return total; }
strlen, strcmp などの標準ライブラリ関数もこの形char *s1 = "Hello"; // ⚠️ 非推奨(警告になる環境あり) s1[0] = 'J'; // ❌ 未定義動作! const char *s2 = "Hello"; // ✅ 正しい書き方 // s2[0] = 'J'; ← コンパイル時にエラーで弾ける char s3[] = "Hello"; // ✅ 配列なら書き換えOK s3[0] = 'J'; // → "Jello"
"Hello" のようにソースに直接書く文字列)は読み取り専用領域に置かれます。ポインタで受ける場合は const char * にしましょう。NULL かどうかチェック (if (p != NULL))p + 1 で次の要素にアクセス& と * を連続して使う *(&x) の挙動*p = 99; で n の値が書き換わることを確認してください。これが「ポインタ経由で間接的に値を変更する」ということです。A. ポインタが必要な理由は:1. 関数で変数の値を変更したい時(参照渡し)、2. 動的にメモリを確保する時、3. 複雑なデータ構造(リスト、ツリーなど)を作る時、4. 文字列を扱う時などです。ポインタがないと実装不可な機能が多くあります。
A. &(アドレス演算子)は「変数がどこに置かれているか」を示すアドレスを取得します。*(間接参照演算子)は「このアドレスに何が入っているか」を見に行きます。ポインタの宣言 `int *p;` の * は「pはint型へのポインタ」という意味です。
A. 3つの状態を区別しましょう。
① NULLポインタ:NULL(値は0)を代入した「意図的にどこも指していない」ポインタ。参照すると必ずクラッシュするので、検知可能な安全な目印として使います。
② 未初期化ポインタ:宣言しただけで値を代入していないポインタ。中身は不定値(どこかのメモリを指す可能性がある)で、参照すると何が起きるかわかりません。
③ ダングリングポインタ:かつて有効な対象を指していたが、その対象が無効になった(free された、スコープを抜けた等)後も残っているポインタ。値自体は有効そうに見えるがアクセスは未定義動作。
安全策: 宣言時に int *p = NULL; と初期化、free(p) の直後に p = NULL; を入れる、スコープ外のローカル変数のアドレスは返さない、を習慣にしましょう。
A. ポインタも変数なので、ポインタ変数自体がメモリに置かれています。したがって「ポインタへのポインタ」も作れます: `int **pp = &p;` のように宣言します。多次元ポインタは複雑ですが、同じ原理を繰り返し適用するだけです。ただし実務ではまれに使うのみです。
この講座の理解度をチェックしましょう!
int *p はint型へのポインタで、int型変数のメモリアドレスを格納します。
*p はポインタの参照先の値(間接参照)で、p が x を指しているので *p = 10 です。
NULLポインタの参照はセグメンテーションフォルト等の実行時エラーを引き起こします。使用前のNULLチェックが重要です。