ポインタは、変数や配列、関数などが置かれているメモリ上の場所(アドレス)を扱うC言語の中心概念です。
この記事では、初心者向けに&演算子と*演算子を軸に、ポインタのしくみと安全な使い方を、最小限の例でていねいに解説します。
配列や関数への応用は別記事で扱います。
ポインタとは?C言語でメモリアドレスを扱う基本
メモリアドレスとは
プログラムが実行されると、変数はRAM上のどこかに配置されます。
その「どこか」を一意に示す番号がメモリアドレスです。
アドレスは通常16進数で表示され、変数の「場所」を表します。
C言語では、このアドレスそのものを値として扱えます。
次の短いプログラムは、変数の値とアドレスを並べて表示します。
アドレスは環境ごとに異なるため、出力は毎回変わる点に注意してください。
// 値とアドレスを確認する最小例
#include <stdio.h>
int main(void) {
int a = 42;
char c = 'X';
printf("a の値 = %d\n", a);
printf("a の場所 = %p\n", (void*)&a); // %p でアドレス表示。void* へキャストが慣例
printf("c の値 = %c\n", c);
printf("c の場所 = %p\n", (void*)&c);
return 0;
}
a の値 = 42
a の場所 = 0x7ffee6d448cc
c の値 = X
c の場所 = 0x7ffee6d448c7
値とアドレスの違いを理解する
値は「中身」、アドレスは「場所」です。
a
は整数42そのもの、&a
は42が入っているメモリの位置を指します。
これを分けて考えることが、ポインタ理解の第一歩です。
アドレスを知ると、別の場所からその値にアクセスできるようになります。
ポインタの役割
ポインタ(pointer)は「アドレスを格納する変数」です。
たとえばint *p
は「intが置かれた場所を指す」ポインタです。
ポインタを使うと、アドレスを渡すだけで遠回りに値を読んだり書き換えたりできます。
これを間接参照と呼びます。
次の例は、ポインタ経由で変数を書き換えています。
// ポインタ経由で値を書き換える
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x; // p は x のアドレス(場所)を保持
printf("書き換え前: x = %d\n", x);
*p = 99; // *p は「p が指す場所の中身」を意味する。ここで x を変更
printf("書き換え後: x = %d\n", x);
return 0;
}
書き換え前: x = 10
書き換え後: x = 99
「p は場所、*p は中身」という対応を常に意識すると混乱しにくくなります。
&と*で学ぶポインタの仕組み
&演算子: 変数のアドレスを取得
&
(アンパサンド)は「その変数のアドレスを取る」演算子です。
使えるのは「変数名などの左辺値」に限られ、&(a + 1)や&42のように式や定数には使えません。
配列に関する特殊な挙動は別記事で解説しますが、基本は&変数名
で十分です。
// & 演算子の基本
#include <stdio.h>
int main(void) {
int v = 7;
int *pv = &v; // v のアドレスを取得して pv に格納
printf("v のアドレス = %p\n", (void*)&v);
printf("pv の値 = %p\n", (void*)pv); // pv そのものは「アドレスという値」
return 0;
}
v のアドレス = 0x7ffee6d448cc
pv の値 = 0x7ffee6d448cc
*演算子: アドレス先の値を読む・書く
*
(アスタリスク)は2つの役割があります。
- 宣言のとき: 「ポインタ型」を表す記号。例:
int *p;
- 式の中で: 「間接演算子(デリファレンス)」。
*p
で「p が指す場所の中身」を読む/書く
*p は左辺にも右辺にも置けます。
右辺に置けば読み取り、左辺に置けば書き込みです。
// * 演算子で読み書き
#include <stdio.h>
int main(void) {
int n = 5;
int *pn = &n;
int read = *pn; // 読み取り(右辺)
*pn = 123; // 書き込み(左辺)
printf("read = %d, n = %d\n", read, n);
return 0;
}
read = 5, n = 123
無効なアドレス(未初期化ポインタや解放済み領域)に対して*
を使うと未定義動作になります。
これがポインタで最も危険な点です。
ミニ例
ここまでの要点を20行弱で確認します。
// & と * の最小セット
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x; // & でアドレスを得てポインタに入れる
printf("p(場所) = %p\n", (void*)p);
printf("&x(場所) = %p\n", (void*)&x);
printf("*p(中身) = %d\n", *p);
*p = *p + 1; // 中身を書き換える
printf("x の新しい値 = %d\n", x);
return 0;
}
p(場所) = 0x7ffee6d448cc
&x(場所) = 0x7ffee6d448cc
*p(中身) = 10
x の新しい値 = 11
ポインタの宣言と初期化の基本
型と*の関係
ポインタは「何を指すか」という型情報を持ちます。
int *p;
は「int を指すポインタ」、double *q;
は「double を指すポインタ」です。
型はデリファレンス時の読み書き幅(バイト数)や整合性のチェックに使われます。
// ポインタ型とサイズの違いを観察
#include <stdio.h>
int main(void) {
int x = 0;
double y = 0.0;
int *pi = &x;
double *pd = &y;
printf("sizeof(pi) = %zu (ポインタ自体のサイズ)\n", sizeof(pi));
printf("sizeof(*pi) = %zu (指す先の型のサイズ: int)\n", sizeof(*pi));
printf("sizeof(pd) = %zu\n", sizeof(pd));
printf("sizeof(*pd) = %zu (指す先の型のサイズ: double)\n", sizeof(*pd));
return 0;
}
sizeof(pi) = 8 (ポインタ自体のサイズ)
sizeof(*pi) = 4 (指す先の型のサイズ: int)
sizeof(pd) = 8
sizeof(*pd) = 8 (指す先の型のサイズ: double)
多くの環境では「ポインタ自体のサイズ」は型に関係なく一定ですが、「指す先の型のサイズ」は異なります。
これが型付きポインタの重要性です。
また、宣言の書き方にも注意が必要です。
int* p, q;と書くとp
はint *
ですがq
はint
になります。
紛らわしいため、1行に1宣言を推奨します。
// 推奨
int *p;
int *q;
// 非推奨(意図せず q はポインタにならない)
int* p_bad, q_bad; // p_bad は int*、q_bad は int
宣言と同時に初期化する
ポインタは宣言と同時に有効なアドレスで初期化するのが安全です。
後から代入するときも「まずは指す先を決める」ことが先決です。
#include <stdio.h>
int main(void) {
int v = 100;
int *pv = &v; // 宣言と同時に初期化
printf("*pv = %d\n", *pv);
return 0;
}
*pv = 100
未初期化ポインタは危険
未初期化ポインタを使うのは最悪のバグの温床です。
どこを指しているか分からないため、読み書きは未定義動作になります。
対策としてNULLで初期化し、使う前に必ずチェックします。
// NULL 初期化と安全なチェック
#include <stdio.h>
#include <stddef.h> // NULL の定義
int main(void) {
int *p = NULL; // 何も指していないことを明示
if (p == NULL) {
printf("p は NULL。まだ有効なアドレスを指していません。\n");
}
int x = 3;
p = &x; // ここで初めて有効なアドレスを設定
printf("p が指す値 = %d\n", *p);
return 0;
}
p は NULL。まだ有効なアドレスを指していません。
p が指す値 = 3
NULLは「何も指していない」特別なアドレスです。
NULLをデリファレンス(*p)してはいけません。
異なる型のアドレスは指さない
ポインタの型と指す先の実体の型は一致させます。
一致しないと読み書き幅が変わり、データ破壊や未定義動作の原因になります。
// 悪い例: 型不一致 (コンパイル時に警告/エラーになる可能性)
double d = 1.23;
// int *pi = &d; // NG: double のアドレスを int* に代入
// 回避例: void* は汎用ポインタだが、そのままでは中身に触れない
void *pv = &d; // OK: どんな型のアドレスでも入る
// printf("%d\n", *pv); // NG: void* は型不明なのでデリファレンス不可
// 正しい基本方針: 指す先の型に合わせる
double *pd = &d; // OK
(int*)&d のような強制キャストで型不一致を黙らせるのは原則禁止です。
読み書き幅やアライメントが崩れてクラッシュやデータ破壊を招きます。
初心者が避けたいミスとコツ
&の付け忘れ・*の付けすぎに注意
ポインタに「変数の中身」を代入してしまう、または「中身にアドレスを代入」してしまうのは定番ミスです。
int x = 10;
int *p;
// p = x; // NG: p はアドレスを入れる場所。x は値(10)でありアドレスではない
p = &x; // OK: &x はアドレス
// *p = &x; // NG: *p は中身(int)。そこにアドレスを代入している
*p = 20; // OK: 中身(int)に 20 を代入
「p は場所、*p は中身、&x は場所」という3者関係をいつも言葉にして確認すると防げます。
代入の向きに注意
「何をどこへ入れるか」を言語化してから書くとミスが減ります。
p = &x;
は「x の場所を p に入れる」*p = x;
は「p が指す場所の中身を x と同じ値にする」
int x = 5;
int y = 99;
int *p = &x;
*p = y; // p が指す先(= x)に y の値を入れる。結果として x が 99 になる
アドレス表示で確認する
混乱したらprintfでアドレスと値をいっしょに出すと理解が進みます。
%p
で表示し、(void*)
へキャストするのが定石です。
#include <stdio.h>
int main(void) {
int v = 7;
int *p = &v;
printf("&v = %p, p = %p, *p = %d\n", (void*)&v, (void*)p, *p);
return 0;
}
&v = 0x7ffee6d448cc, p = 0x7ffee6d448cc, *p = 7
コメントで意図を明確にする
ポインタは「何を指しているのか」が読み手に伝われば安全になります。
コメントで「誰の住所(アドレス)なのか」を明示しましょう。
// user_count の住所を持つカウンタポインタ
int user_count = 0;
int *p_count = &user_count; // p_count は user_count のアドレス
// p_count 経由で user_count をインクリメント
(*p_count)++;
このように意図を書き添えるだけで、未来の自分やチームメイトの理解が大きく変わります。
以下は、ここまでの記号と意味をまとめた早見表です。
記号/型 | 読み | 意味(役割) | 例 |
---|---|---|---|
& | アドレス演算子 | 変数のアドレス(場所)を得る | &x |
* (宣言) | ポインタ宣言の記号 | 「〜を指すポインタ型」を作る | int *p; |
* (式) | 間接演算子(デリファレンス) | アドレス先の中身(値)へアクセス | *p = 10; |
T* | ポインタ型 | T型のオブジェクトを指す | double *pd; |
まとめ
本記事では、C言語のポインタを& 演算子(アドレスを取る)と * 演算子(中身へ触る)という核心から解きほぐしました。
ポインタは「場所(アドレス)を入れる変数」、*p は「その場所の中身」という対応関係を忘れなければ、基本操作は確実に身につきます。
特に未初期化ポインタの使用や型不一致のデリファレンスは厳禁です。
宣言と同時初期化、NULLチェック、アドレスの表示確認、意図を明記するコメントという習慣を徹底すれば、安全に扱えるようになります。
配列や関数引数での応用(アドレス渡し)は別記事で丁寧に扱いますので、まずは本稿のコードを手で写し、「p は場所、*p は中身」を体で覚えていきましょう。