🇯🇵 日本語 | 🇺🇸 English

第34回 ポインタの基礎

C言語のポインタをメモリ可視化で理解。アドレスと間接参照を図解。

📖 このページで覚えること
✅ 最低限ここだけ覚える
  • &x で x のアドレス、*p で p の指す先
  • 宣言: int *p = &x;
  • NULLポインタは「どこも指さない目印」
⭐ 余裕があれば読む
  • ポインタ演算と配列
  • 未初期化ポインタ vs ダングリングポインタ
  • 関数にポインタを渡して値を書き換える
💪 初心者は1回で分からなくて普通
ポインタはC言語最大の壁。抽象度が高く、1回で完全に理解できる人はほとんどいません。
再挑戦のステップ
  1. 配列 を復習 — 「連続したメモリ」の感覚を取り戻す
  2. このページのメモリ図(家と住所のアナロジー)を何度も見返す
  3. 具体例の swap 関数(値を入れ替える)から用途を理解
  4. 紙にメモリとアドレスを手で書いて追ってみる
  5. 今日わからなくてもOK。次の講座に進んで、数日後に戻ってくる
💡 コツ: つまずきの大半は &(アドレス取得)と *(参照)の使い分け。宣言の * は「ポインタ型」の印、式中の *p は「指し示す先」。この区別だけで半分は越えます。

ポインタとは

変数はメモリ上の箱でしたね。その箱には住所(アドレス)がついています。ポインタとは、そのアドレスを記憶する変数のことです。

メモリは「番地つきの連続した倉庫」

コンピュータのメモリ(RAM)は、1バイト(8ビット)ごとに通し番号(アドレス)がふられた、ずらっと並んだ棚のようなものです。変数を宣言すると、OSが空いている場所を確保し、そこに番地が決まります。
💾 メモリ (RAM) ― 各マスは1バイト
int n = 10; (4バイト)
int m = 20; (4バイト)
char c = 'A'; (1バイト)
未使用
ポイント: n の先頭アドレス = 0x1000m の先頭アドレス = 0x1004c のアドレス = 0x1008
int は 4バイト使うので、次の変数は 4番地先に置かれます。このように型の大きさ分だけ連続して確保されます。
※ この図は int が 4バイトの典型的な 32/64bit 環境を想定した例示です。型のサイズは処理系依存で、sizeof(int) の結果が異なる環境もあります。実アドレスもOS・実行ごとに変わり、図のように整然と並ぶとは限りません。
家と住所のイメージ: 変数が「家」、アドレスはその「住所(番地)」ポインタは、その住所を書きとめた「メモ」です。住所さえ知っていれば、その家の中身を参照したり書き換えたりできます。
& 演算子でアドレスを取り出せる: &n と書くと、変数 n のアドレス(例:0x1000)が得られます。printf("%p\n", &n); で実際に住所を表示することも可能。

普通の変数 vs ポインタ

普通の変数
int n = 10;
箱の中に値(10)を直接入れる
ポインタ変数
int *p = &n;
箱の中にnのアドレスを入れる
宣言は 型 *変数名; の形。型はポインタが指す先の型です。int *p は「int型の変数を指すポインタ p」。

&演算子 と *演算子

ポインタを扱うには2つの演算子が必須です。
& (アドレス演算子)
&n → nのアドレス
変数の住所を取得する演算子。
scanfで使う & と同じ!
* (間接参照演算子)
*p → pが指す変数の中身
ポインタの指し示す先の値にアクセス。
読み書き両方できる。
int n = 10;
int *p = &n; // pにnのアドレスを代入
printf("%d", *p); // → 10 (pが指す値)
*p = 99; // nを書き換える!
printf("%d", n); // → 99

アドレスと値 ― ビジュアライザ

ボタンで操作して、変数 nポインタ p の関係を確認しましょう。
アドレス 0x1000
int n
アドレス 0x2000
int *p
ボタンを順に押してください...

メモリ内の動きをステップ実行

ポインタがメモリの中でどう動くかを、1行ずつ実行して観察しましょう。ポインタ pn を指したり m を指したり切り替わります。
プログラム
int n = 10;
int m = 20;
int *p;
p = &n;
*p = 99;
p = &m;
*p = 77;
💾 メモリ (RAM)
0x1000
int n
0x1004
int m
0x2000
int *p
「次の行を実行」を押してください...
観察ポイント:

scanfの & の正体

これまで scanf では scanf("%d", &a); と書いていましたね。この「&」こそがアドレスを渡す演算子だったのです。
なぜ & が必要?
scanf関数は「どこに値を書き込めばいいか」を知る必要があります。そのため変数の住所(アドレス)を渡します。
→ scanfは内部でポインタを使って、その住所の場所に入力値を書き込んでいるのです。
int a;
scanf("%d", &a); // aのアドレスを渡す

// scanf内部では…こんなイメージ
// void scanf(char *fmt, int *p){ *p = 入力値; }
⚠️ よくあるミス: scanf("%d", a); のように & を忘れると、aの中身(不定値)をアドレスとして扱ってしまい、予期せぬ場所に書き込んでプログラムが落ちることがあります。

swap関数 ― ポインタの実用例

2つの変数の値を入れ替える swap関数は、ポインタの代表的な応用例です。
値渡しでは入れ替えできない!
void swap(int a, int b){ ... } のように書くと、呼び出し元の変数は変わりません。
(関数内でコピーを入れ替えているだけだから)
// ポインタを受け取って中身を入れ替える
void swap(int *x, int *y){
  int t = *x;
  *x = *y;
  *y = t;
}

int main(void){
  int a = 5, b = 10;
  swap(&a, &b); // アドレスを渡す
  printf("%d %d", a, b); // → 10 5
}
ポイント:関数にアドレスを渡す=参照渡し。配列を関数に渡すと自動的に参照渡しになっていたのも、これと同じ仕組みです。

ポインタ演算(ポインタの加減算)

ポインタに整数を足したり引いたりすると、型のサイズ分だけアドレスが移動します。配列とポインタの関係を理解する鍵です。
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
p + 1 は「次の要素」であり、「次の1バイト」ではありません。
int型(4バイト)のポインタなら、p + 1 はアドレスが 4バイト 進みます。
10
p+0
20
p+1
30
p+2
40
p+3
50
p+4

配列とポインタの等価性

C言語では、以下はすべて同じ意味です。
配列記法ポインタ記法意味
arr[0]*arr先頭要素
arr[i]*(arr + i)i番目の要素
&arr[i]arr + ii番目のアドレス

🎚 スライダーで等価性を体感

arr[i]*(arr+i)完全に同じ要素を指す。スライダーで i を動かして、両方の記法が同時に同じセルをハイライトするのを見てみましょう。
📝 配列記法
arr[2]
添字でアクセス(人間が読みやすい)
= 30
🎯 ポインタ記法
*(arr + 2)
先頭アドレスから i 個分進む
= 30
// アドレス計算 (int = 4バイト、arr の先頭アドレス = 0x1000 と仮定)
arr = 0x1000
arr + 2 = 0x1000 + 2 × 4 = 0x1008
*(arr + 2) = arr[2] = 30
💡 コンパイラが内部でやっていること: 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 は「この変数は後から変更しない」ことをコンパイラと他の開発者に伝える修飾子です。バグの予防と、コードの意図を明確にするのに役立ちます。

基本: const変数

const int MAX = 100;
printf("%d\n", MAX);  // OK: 読み取りは自由

MAX = 200;             // ❌ コンパイルエラー: const変数は変更不可
用途: 円周率や配列サイズなど、「プログラム実行中に変わらない値」を表現するときに使います。#define との違いは、型チェックが効くこと。

ポインタと const の組み合わせ(3パターン)

ポインタと const の組み合わせは混乱しやすいポイント。「const が何の左にあるか」で意味が変わります
宣言変更できるか意味
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 の使い方

関数に配列や文字列を渡すとき、関数内で書き換えないことを明示するために使います。
// 文字列の長さを返すだけ → 書き換えないので 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;
}
ベストプラクティス: 関数内で書き換えない引数には必ず const を付ける
・読む人に「この関数は引数を変えない」と伝わる
・うっかり書き換えようとするとコンパイルエラーで気づける
strlen, strcmp などの標準ライブラリ関数もこの形

文字列リテラルと const

char *s1 = "Hello";        // ⚠️ 非推奨(警告になる環境あり)
s1[0] = 'J';                // ❌ 未定義動作!

const char *s2 = "Hello";  // ✅ 正しい書き方
// s2[0] = 'J';  ← コンパイル時にエラーで弾ける

char s3[] = "Hello";         // ✅ 配列なら書き換えOK
s3[0] = 'J';                // → "Jello"
重要: 文字列リテラル("Hello" のようにソースに直接書く文字列)は読み取り専用領域に置かれます。ポインタで受ける場合は const char * にしましょう。
まとめ: const は「バグ防止のバリア」です。使わなくてもコードは動きますが、付けると間違いをコンパイル時に検出でき、コードの意図も明確になります。

自分で書いてみよう ― ポインタ

ポインタを使って値を書き換えるプログラムです。実行してみましょう。
pointer.c
出力
「実行」を押してください...
💡 こんなことも試してみよう
*p = 99;n の値が書き換わることを確認してください。これが「ポインタ経由で間接的に値を変更する」ということです。

関連する講座

関数編
第30回 配列を引数に
C言語で関数に配列を渡す方法。ポインタとの関係も解説。
発展編
第40回 動的メモリ(malloc/free)
C言語の動的メモリ確保。malloc, free, メモリリークの原因と対策。
発展編
第36回 構造体(struct)
C言語のstruct(構造体)の定義・メンバアクセス・配列との組合せを解説。
← 前の講座
第30回 配列を引数に
次の講座 →
第36回 構造体(struct)

よくある質問(FAQ)

Q. なぜポインタを使うの? 普通の変数では だめ?

A. ポインタが必要な理由は:1. 関数で変数の値を変更したい時(参照渡し)、2. 動的にメモリを確保する時、3. 複雑なデータ構造(リスト、ツリーなど)を作る時、4. 文字列を扱う時などです。ポインタがないと実装不可な機能が多くあります。

Q. * と & の違いが混乱します。どっちどっち?

A. &(アドレス演算子)は「変数がどこに置かれているか」を示すアドレスを取得します。*(間接参照演算子)は「このアドレスに何が入っているか」を見に行きます。ポインタの宣言 `int *p;` の * は「pはint型へのポインタ」という意味です。

Q. NULLポインタって何ですか? ダングリングポインタとの違いは?

A. 3つの状態を区別しましょう。
① NULLポインタNULL(値は0)を代入した「意図的にどこも指していない」ポインタ。参照すると必ずクラッシュするので、検知可能な安全な目印として使います。
② 未初期化ポインタ:宣言しただけで値を代入していないポインタ。中身は不定値(どこかのメモリを指す可能性がある)で、参照すると何が起きるかわかりません。
③ ダングリングポインタかつて有効な対象を指していたが、その対象が無効になった(free された、スコープを抜けた等)後も残っているポインタ。値自体は有効そうに見えるがアクセスは未定義動作。
安全策: 宣言時に int *p = NULL; と初期化、free(p) の直後に p = NULL; を入れる、スコープ外のローカル変数のアドレスは返さない、を習慣にしましょう。

Q. ポインタがポインタを指すってどういうこと?

A. ポインタも変数なので、ポインタ変数自体がメモリに置かれています。したがって「ポインタへのポインタ」も作れます: `int **pp = &p;` のように宣言します。多次元ポインタは複雑ですが、同じ原理を繰り返し適用するだけです。ただし実務ではまれに使うのみです。

確認クイズ

この講座の理解度をチェックしましょう!

Q1. int *p; の p に格納されるのは?

整数値
整数型変数のアドレス
文字列

int *p はint型へのポインタで、int型変数のメモリアドレスを格納します。

Q2. int x=10; int *p=&x; のとき *p の値は?

x のアドレス
10
ポインタ p のアドレス

*p はポインタの参照先の値(間接参照)で、p が x を指しているので *p = 10 です。

Q3. NULL ポインタを参照(*p)するとどうなる?

0が返る
実行時エラー(セグフォ)
コンパイルエラー

NULLポインタの参照はセグメンテーションフォルト等の実行時エラーを引き起こします。使用前のNULLチェックが重要です。

この記事をシェア
X(Twitter)でシェア Facebookでシェア LINEで送る はてブ

この講座の理解を深めるおすすめ書籍

サイトで動きを理解し、書籍で演習量を補うと効果的です

📘
苦しんで覚えるC言語
MMGames 著
初心者向けの定番入門書。丁寧な解説で基礎を固められます。
Amazonで見る
📗
新・明解C言語 入門編
柴田望洋 著
図解が豊富で、演習問題も充実。大学の教科書としても採用多数。
Amazonで見る
📙
プログラミング言語C 第2版
B.W.カーニハン, D.M.リッチー 著
通称K&R。C言語の原典。基礎を終えた後のステップアップに最適。
Amazonで見る

※ 上記リンクはアフィリエイトリンクです。購入によりサイト運営を支援いただけます。