【C言語入門】ポインタの基礎を完全マスター!使い方から注意点まで

2025年4月14日月曜日

C言語

C言語のプログラミング学習の中でも特に「???」となりやすいポインタ。アドレスとか、* とか & とか、記号が多くて頭から湯気が出そう…なんて経験、ありませんか?

ポインタはC言語のパワフルな機能の源泉ですが、初学者にとっては高い壁に感じられますよね。

この記事では、ポインタの基礎の基礎に絞って、複雑に見える概念を図解も使いながら、これでもかというくらい噛み砕いて説明していきます。

この記事で学べること

  • ポインタが何者で、なぜ必要なのかがスッキリわかる
  • ポインタの基本的な書き方や使い方を覚えられる
  • 初心者がやりがちなミスや注意点が理解できる
  • ポインタとメモリの関係をイメージできるようになる

C言語の最重要概念「ポインタ」とは?

さて、いきなり核心ですが、ポインタとは一体何者なのでしょうか?
一言でいうと、ポインタは「メモリアドレスを格納するための特別な変数」です。

ちょっと待って、メモリアドレスって何?って思いますよね。
コンピューターはデータを「メモリ」という場所に記憶します。メモリはよく、たくさんの小さな「箱」が並んでいる場所に例えられます。それぞれの箱には、データを区別するための「住所」が割り当てられています。これがメモリアドレスです。

変数を宣言すると、コンピューターはメモリ上のどこかの箱をその変数用に確保し、その箱の住所(メモリアドレス)が決まります。

まず、変数numを考えます。この変数は、メモリ上の特定のアドレスに格納されており、例えばそのアドレスが0x7ffeeabcであるとします。numの中には値10が入っています。

次に、ポインタ変数ptrを考えます。このポインタは、変数numのアドレスを格納します。仮にptrのアドレスが0x7ffeeab0で、ptrの中にはnumのアドレス0x7ffeeabcが入っているとします。

ポインタptrは、変数numのアドレスを指し示しています。この関係を矢印で示すと、以下のようになります。

この図から、ポインタptrが変数numのアドレスを保持し、ptrを通じてnumの値にアクセスできることがわかります。ポインタを使用することで、メモリの効率的な管理や、関数間でのデータの受け渡しが可能になります。

なぜ学ぶ?C言語でポインタの基礎知識が必要な理由

「ふーん、住所を覚える変数ね。でも、なんでわざわざそんなものが必要なの?」
良い質問です! ポインタを理解すると、C言語でできることの幅がぐんと広がり、プログラムの効率も上げられるようになります。主な理由をいくつか挙げてみましょう。

  • メモリへの直接アクセス
    ポインタを使うと、メモリ上の特定のアドレスに直接アクセスできます。これにより、処理速度の向上が期待できる場面があります。
  • 動的なメモリ確保
    プログラム実行中に、必要な分だけメモリを確保したり解放したりできます。(これは少し応用的な話ですが、ポインタが必須!)
  • 関数での効率的なデータのやり取り
    関数に大きなデータ(例えば、サイズの大きな構造体など)を渡すとき、データそのものをコピーする代わりに、データがある場所の住所(ポインタ)だけを渡せば、メモリの節約になり、処理も速くなります。
  • 配列や文字列操作の柔軟性
    ポインタを使うと、配列や文字列をより柔軟に、効率的に操作できます。
  • データ構造の実装
    リスト構造や木構造といった、より複雑なデータ構造を作る際に、ポインタは欠かせません。

最初は難しく感じるかもしれませんが、ポインタを使いこなせると、C言語プログラミングがもっと面白く、パワフルになりますよ! まさに、C言語を深く理解するためのパスポートみたいなものです。

C言語のポインタの基礎:宣言と初期化の正しい書き方

では、実際にポインタ変数を使うための第一歩、宣言と初期化の方法を見ていきましょう。

ポインタ変数の宣言は、通常の変数宣言に「*」(アスタリスク)を付けるだけです。書式は以下のようになります。

データ型 *ポインタ変数名;

例えば、整数(int)へのポインタなら、こう書きます。

int *ptr;

`int` は「指し示す先のデータ型」を表し、`*` が「これはポインタ変数ですよ」という目印、`ptr` が変数名です。これで、「整数データのある場所のアドレスを格納できる変数 `ptr`」が用意されました。

ただ、宣言しただけだと、`ptr` の中身は不定(どこを指しているか分からないゴミデータ)で危険です。そこで、初期化が必須となります。

特定の変数のアドレスで初期化することもできますが、まだ指し示す先が決まっていない場合や、安全のために、「どこも指していませんよ」という意味の特別な値 `NULL` (ナル)で初期化するのが一般的です。

int *ptr = NULL; /* NULLで初期化 */

`NULL` は、多くの環境で `0` 番地として定義されていて、「有効なアドレスではない」ことを示すために使われます。

初期化を怠ると、予期せぬ場所を指してしまい、プログラムがおかしな動作をする原因になるので、ポインタ変数は宣言と同時に `NULL` で初期化する癖をつけましょう! 安全のためのおまじない、と思ってくださいね。

C言語のポインタの基礎:アドレス演算子(&)と間接参照演算子(*)の使い方

ポインタを操作するには、2つの特別な演算子「&」と「*」が不可欠です。それぞれの役割をしっかり区別して覚えましょう。

  • & (アドレス演算子)
    変数の前に付けると、その変数が格納されているメモリアドレスを取得します。「変数の住所を調べる」演算子です。
  • * (間接参照演算子)
    ポインタ変数の前に付けると、そのポインタが指し示しているアドレスに格納されている値を取得したり、書き換えたりします。「住所にある家の中身を見る・変える」演算子です。

言葉だけだと分かりにくいので、具体例を見てみましょう。

int num = 10;     /* 普通の整数型変数 */
int *ptr = NULL;  /* int型へのポインタ変数 */

/* &演算子:変数numのアドレスを取得して、ポインタptrに入れる */
ptr = #        /* ptrはnumの住所を持つ */

/* *演算子:ptrが指す先(つまりnum)の値を取得する */
int value = *ptr;  /* valueには10が入る */

/* *演算子:ptrが指す先(つまりnum)の値を書き換える */
*ptr = 20;         /* numの値が20に変わる! */

どうでしょうか?
`&num` で `num` の住所を調べ、`ptr` に代入。
`*ptr` で `ptr` が持っている住所(`num` の住所)にアクセスし、その中身(`num` の値)を取り出したり、書き換えたりしています。

ポイントは、宣言時の `*` と、使う時の `*` (間接参照演算子)は意味が違うということです。

宣言時の `*` は「これはポインタ変数ですよ」の目印。使う時の `*` は「そのポインタが指す先の中身にアクセスする」という操作を表します。ここ、混同しやすいので注意してくださいね!

【実践】C言語のポインタを使ってみよう

理屈が分かってきたところで、実際にポインタを使った簡単なプログラムを動かしてみましょう! 百聞は一見にしかず、です。

変数のアドレスを表示する

まずは、変数のメモリアドレスが実際にどんなものか、表示してみましょう。アドレス演算子 `&` を使います。

#include <stdio.h>

int main(void) {
  int num = 777;
  double score = 95.5;
  char initial = 'G';

  printf("変数numの値: %d, アドレス: %p\n", num, &num);
  printf("変数scoreの値: %.1f, アドレス: %p\n", score, &score);
  printf("変数initialの値: %c, アドレス: %p\n", initial, &initial);

  return 0;
}

実行結果(例):

変数numの値: 777, アドレス: 0x7ffee1f4a8ac
変数scoreの値: 95.5, アドレス: 0x7ffee1f4a8a0
変数initialの値: G, アドレス: 0x7ffee1f4a89f

コード解説
`int` 型の `num`、`double` 型の `score`、`char` 型の `initial` という3つの変数を宣言しました。

`printf` 関数の中で、`&num`、`&score`、`&initial` のようにアドレス演算子 `&` を付けて、それぞれの変数のメモリアドレスを取得しています。

アドレスを表示するための書式指定子は `%p` を使うのが一般的です。実行結果を見ると、`0x` から始まる16進数でアドレスが表示されていますね。(表示されるアドレスは実行環境によって毎回異なります)
このように、変数ごとにちゃんとメモリ上に住所が割り当てられているのが分かります。

ポインタ経由で変数の値を変更する

次に、ポインタを使って、元の変数の値を間接的に変更してみましょう。アドレス演算子 `&` と間接参照演算子 `*` の両方を使います。

#include <stdio.h>

int main(void) {
  int value = 100;
  int *p_value = NULL; /* ポインタ変数をNULLで初期化 */

  /* valueのアドレスをp_valueに格納 */
  p_value = &value;

  printf("変更前のvalueの値: %d\n", value);
  printf("p_valueが指すアドレス: %p\n", p_value);
  printf("p_valueが指す先の値(*p_value): %d\n", *p_value);

  /* ポインタ経由で値を変更 */
  printf("\n*p_valueを使って値を変更します...\n\n");
  *p_value = 555; /* p_valueが指す先(つまりvalue)に555を代入 */

  printf("変更後のvalueの値: %d\n", value); /* valueの値が変わっている! */
  printf("p_valueが指す先の値(*p_value): %d\n", *p_value);

  return 0;
}

実行結果(例):

変更前のvalueの値: 100
p_valueが指すアドレス: 0x7ffeea1c38bc
p_valueが指す先の値(*p_value): 100

*p_valueを使って値を変更します...

変更後のvalueの値: 555
p_valueが指す先の値(*p_value): 555

コード解説
まず、整数変数 `value` を100で初期化し、ポインタ変数 `p_value` を `NULL` で初期化します。

`p_value = &value;` で、`value` のアドレスを `p_value` に代入。これで `p_value` は `value` を指すようになりました。
最初の `printf` 群で、変更前の `value` の値、`p_value` が持つアドレス、`*p_value` (つまり `p_value` が指す先の値) を表示しています。当然 `value` の値と同じ 100 が表示されますね。

次に、`*p_value = 555;` という行がミソです。間接参照演算子 `*` を使って、`p_value` が指している場所(つまり変数 `value` の場所)に直接 555 を書き込んでいます。
最後の `printf` 群で確認すると、`value` 自身の値が 555 に変わっていることがわかります! ポインタ `p_value` を使って、離れた場所から `value` の値を変更できた、というわけです。

C言語のポインタとメモリの関係

コードだけだとまだイメージしにくいかもしれないので、ここでもう一度、図を使ってメモリとポインタの関係を整理しましょう。

さっきの「ポインタ経由で変数の値を変更する」例で考えてみます。

ステップ1: int value = 100;
  メモリ上に value 用の箱が確保され、値 100 が入る。

ステップ2: int *p_value = NULL;
  メモリ上に p_value 用の箱が確保され、値 NULL (0) が入る。

ステップ3: p_value = &value;
  value のアドレス (0x1000) が p_value の箱に格納される。
  p_value の箱の中身が 0x1000 になる。

ステップ4: *p_value = 555;
  p_value が指しているアドレス (0x1000) の箱の中身を 555 に書き換える。
  value の箱の中身が 100 から 555 に変わる。

このように、ポインタはメモリ上の「住所」を保持し、その住所を頼りにデータにアクセスするための仕組みです。ポインタ変数自体もメモリ上に場所を持っていることを意識すると、より理解が深まりますよ。

初心者が陥るC言語のポインタの罠

ポインタは強力な反面、使い方を間違えるとプログラムがクラッシュしたり、予期せぬ動作を引き起こしたりする、ちょっと危険な側面もあります。ここでは、初心者が特に注意すべき点をいくつか紹介します。

無効なポインタ参照 (初期化、NULLチェック)

最もよくある間違いの一つが、初期化されていないポインタや、`NULL` が入っているかもしれないポインタを、チェックせずに使おうとすることです。

例えば、こんなコードは危険です。

#include <stdio.h>

int main(void) {
  int *ptr; /* 初期化されていない! */
  /* ptrがどこを指しているか不明な状態で... */
  *ptr = 10; /* ここでクラッシュする可能性大! */
              /* (セグメンテーション違反など) */
  printf("%d\n", *ptr);
  return 0;
}

上の例では、`ptr` を宣言しただけで、どこも指していない(ゴミデータが入っている)状態です。そんな `ptr` に対して `*ptr = 10;` とすると、プログラムがアクセスしてはいけないメモリ領域を書き換えようとしてしまい、OSによって強制終了させられることがほとんどです(セグメンテーション違反と呼ばれるエラーが有名)。

また、関数からポインタを受け取る場合など、そのポインタが `NULL` である可能性も考慮しなければなりません。

void process_pointer(int *p) {
  /* pがNULLでないかチェックせずに使うのは危険 */
  /* *p = 100;  もし p が NULL ならここでクラッシュ! */

  /* 正しくは、使う前にNULLチェックを入れる */
  if (p != NULL) {
    *p = 100; /* NULLでなければ安全にアクセスできる */
    printf("ポインタ経由で値を設定しました。\n");
  } else {
    printf("エラー: 無効なポインタです。\n");
  }
}

int main(void) {
  int *p1 = NULL;
  int num = 10;
  int *p2 = &num;

  process_pointer(p1); /* NULLを渡すケース */
  process_pointer(p2); /* 有効なアドレスを渡すケース */

  return 0;
}

【教訓】ポインタは宣言時に必ず `NULL` などで初期化し、使う前(特に `*` でアクセスする前)には `NULL` でないかをチェックする習慣をつけましょう!

メモリ解放後のアクセス

これは動的メモリ確保(`malloc` など)を学んだ後の話になりますが、非常に陥りやすいミスなので触れておきます。

`malloc` などで確保したメモリは、使い終わったら `free` 関数で解放(OSに返却)する必要があります。問題は、`free` で解放した後のメモリ領域を指しているポインタ(ダングリングポインタと呼びます)を使ってしまうことです。

#include <stdio.h>
#include <stdlib.h> /* malloc, free を使うため */

int main(void) {
  int *ptr = (int *)malloc(sizeof(int)); /* メモリ確保 */

  if (ptr == NULL) {
    printf("メモリ確保失敗\n");
    return 1;
  }

  *ptr = 123;
  printf("確保したメモリの値: %d\n", *ptr);

  free(ptr); /* メモリを解放 */

  /* 解放したのに、再度アクセスしようとするのはNG! */
  /* *ptr = 456; */ /* ここで何が起こるか分からない(危険!) */
  /* printf("%d\n", *ptr); */ /* 同様に危険! */

  /* 安全策:解放後はNULLを代入しておく */
  /* ptr = NULL; */

  return 0;
}

解放済みのメモリは、OSが他の用途に使い始めるかもしれません。そこにアクセスすると、プログラムがクラッシュしたり、データを破壊したりする原因になります。

【教訓】`free` した後のポインタは絶対に使わないこと! 安全のため、`free` したらすぐに `NULL` を代入しておくのが良い習慣です。

C言語のポインタと配列の関係性

C言語では、ポインタと配列は非常に密接な関係にあります。実は、配列名は、その配列の先頭要素のアドレスを示すポインタ定数のように扱われるのです!

どういうことか、見てみましょう。

#include <stdio.h>

int main(void) {
  int arr[5] = {10, 20, 30, 40, 50};
  int *ptr = NULL;

  /* 配列名をポインタに代入できる */
  ptr = arr; /* arrは &arr[0] と同じ意味を持つ */

  printf("arr[0]の値: %d\n", arr[0]);
  printf("ptrが指す先の値 (*ptr): %d\n", *ptr); /* arr[0]と同じになる */

  printf("arrのアドレス: %p\n", arr);
  printf("arr[0]のアドレス: %p\n", &arr[0]);
  printf("ptrの値(アドレス): %p\n", ptr); /* 上2つと同じアドレス */

  /* ポインタ演算で次の要素へアクセス */
  printf("arr[1]の値: %d\n", arr[1]);
  printf("*(ptr + 1)の値: %d\n", *(ptr + 1)); /* arr[1]と同じになる! */

  /* 配列アクセス arr[i] は *(arr + i) とほぼ同じ */
  printf("arr[2] = %d, *(arr + 2) = %d\n", arr[2], *(arr + 2));

  return 0;
}

コード解説
`ptr = arr;` と書くと、ポインタ `ptr` は配列 `arr` の先頭要素 (`arr[0]`) のアドレスを持つことになります。`arr` 自体が `&arr[0]` と同じ意味で使われるからです。
そのため、`*ptr` は `arr[0]` と同じ値を持ちます。

さらに面白いのは、ポインタを使った演算です。
`ptr + 1` と書くと、`ptr` が指すアドレスの「次の要素」のアドレスを意味します。(`int` 型なら通常4バイト先)
そして `*(ptr + 1)` と書けば、その次の要素の値、つまり `arr[1]` の値を取得できます。

一般的に、配列アクセス `arr[i]` は、内部的には `*(arr + i)` というポインタ演算として解釈されることが多いのです。この関係を知っておくと、配列とポインタを使ったコードの理解が深まりますね。

C言語のポインタと関数の関係性

ポインタは、関数の引数として使うことで、真価を発揮する場面がたくさんあります。

通常、関数に引数を渡すと、その値がコピーされて関数の中で使われます(値渡し)。そのため、関数内で引数の値を変更しても、呼び出し元の変数の値は変わりません。

しかし、引数としてポインタ(アドレス)を渡すと、関数はそのアドレスを使って、呼び出し元の変数に直接アクセスし、値を変更できるようになります。これは参照渡し(あるいはアドレス渡し)と呼ばれるテクニックです。

有名な例として、2つの変数の値を入れ替える `swap` 関数を見てみましょう。

#include <stdio.h>

/* ポインタを使って値を入れ替える関数 */
void swap(int *pa, int *pb) { /* 引数はint型へのポインタ */
  printf("swap関数内 - 変更前: *pa=%d, *pb=%d\n", *pa, *pb);
  int temp = *pa; /* paが指す先の値をtempに退避 */
  *pa = *pb;      /* paが指す先に、pbが指す先の値を入れる */
  *pb = temp;     /* pbが指す先に、退避しておいた値を入れる */
  printf("swap関数内 - 変更後: *pa=%d, *pb=%d\n", *pa, *pb);
}

int main(void) {
  int a = 5;
  int b = 10;

  printf("main関数 - swap呼び出し前: a=%d, b=%d\n", a, b);

  /* swap関数に、aとbのアドレスを渡す! */
  swap(&a, &b);

  printf("main関数 - swap呼び出し後: a=%d, b=%d\n", a, b); /* aとbの値が入れ替わっている! */

  return 0;
}

実行結果(例):

main関数 - swap呼び出し前: a=5, b=10
swap関数内 - 変更前: *pa=5, *pb=10
swap関数内 - 変更後: *pa=10, *pb=5
main関数 - swap呼び出し後: a=10, b=5

コード解説
`swap` 関数は、引数として `int *pa`, `int *pb` という2つのポインタを受け取ります。
`main` 関数から `swap` を呼び出す際、`swap(&a, &b)` のように、変数 `a` と `b` のアドレスを `&` で取得して渡しています
`swap` 関数の中では、`*pa`, `*pb` を使って、渡されたアドレスが指す先の値(つまり `main` 関数の `a` と `b` の値)を直接操作しています。
一時変数 `temp` を使いながら値を入れ替えることで、`swap` 関数が終わった後、`main` 関数の `a` と `b` の値が実際に入れ替わっていますね!

このように、関数にポインタを渡すことで、

  • 関数内で呼び出し元のデータを変更できる
  • 大きなデータをコピーする無駄を省ける(アドレスだけ渡せばよい)

といったメリットがあります。これもポインタの強力な使い道の一つです。

【まとめ】C言語ポインタの基礎を確実に身につけよう

さて、ここまでC言語のポインタの基礎について、その概念から使い方、注意点、配列や関数との関係まで、駆け足で見てきました。

もう一度、今回のポイントをおさらいしましょう。

  • ポインタはメモリアドレスを格納する特別な変数
  • `&` (アドレス演算子)で変数のアドレスを取得
  • `*` (間接参照演算子)でポインタが指す先の値にアクセス
  • 宣言時の初期化(`NULL`推奨)と、使用前のNULLチェックがミスを防ぐコツ
  • 配列名は先頭要素のアドレスとして扱える
  • 関数にポインタを渡すと呼び出し元の変数を変更できる

ポインタは、確かにC言語学習における一つの山場です。最初は戸惑うことも多いでしょう。でも、今回学んだ基礎をしっかり理解し、実際にコードを書いて動かしてみることで、必ず使いこなせるようになります。

焦らず、一つずつ、着実に理解を深めていってください。ポインタを乗り越えれば、C言語の持つパワーと面白さがもっと見えてくるはずです!


【関連記事】

>> C言語とは?特徴・できること・学ぶメリット

このブログを検索

  • ()

自己紹介

自分の写真
リモートワークでエンジニア兼Webディレクターとして活動しています。プログラミングやAIなど、日々の業務や学びの中で得た知識や気づきをわかりやすく発信し、これからITスキルを身につけたい人にも役立つ情報をお届けします。 note → https://note.com/yurufuri X → https://x.com/mnao111

QooQ