C言語基礎|ポインタとは

「0のまま問題」で分かる!C言語ポインタは“値”じゃなく“場所”を渡す技術。

C言語を学んでいると、ある日こんな現象に出会います。

  • 関数の中ではちゃんと計算できているのに
  • 関数を抜けたら、呼び出し元の変数が変わっていない

「えっ、代入したのに…?」ってなりますよね。

これはC言語の基本ルールである 値渡し(call by value) が原因です。
C言語の関数は、引数として渡された値をコピーして受け取ります。だから関数の中で引数を書き換えても、コピーが変わるだけで、呼び出し元の変数そのものは変わりません。

そこで登場するのが ポインタ
ポインタを使うと、関数に「値」ではなく「その値が置かれている場所(アドレス)」を渡せます。つまり、関数の中から呼び出し元の変数を直接書き換えられるようになります。

まず押さえる:値渡しで起きる「変わらない問題」

例:関数で2つの結果を受け取りたいのに、0のまま

下のプログラムは、2つの整数の大きい方小さい方を求めるつもりですが、結果が変わりません(わざと誤りの例です)。

プロジェクト名:chap10-1-1 ソースファイル名:chap10-1-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

void min_max(int x, int y, int min, int max)  // 誤り:min/maxはコピー
{
    if (x < y) {
        min = x;
        max = y;
    } else {
        min = y;
        max = x;
    }
}

int main(void)
{
    int a, b;
    int mn = 0, mx = 0;

    puts("2つの整数を入力してください。");
    printf("1つ目:");
    scanf("%d", &a);
    printf("2つ目:");
    scanf("%d", &b);

    min_max(a, b, mn, mx);

    printf("小さい方は%d、大きい方は%dです。\n", mn, mx);
    return 0;
}

実行イメージ(例)

  • 入力:8 と 3
  • 表示:小さい方は0、大きい方は0です。
    (0のまま…!)

なぜ0のまま?(値渡しのしくみを図で理解)

値渡しは「コピーを渡す」

呼び出し元の mn / mx と、関数内の min / max は 別物 です。

イメージ図(箱=変数)

どこが“一方通行”なの?

「main → 関数」へは値がコピーされて入っていきますが、「関数 → main」へは自動では戻りません
戻せるのは return による返り値1個だけ
なので、2つ以上の結果(minとmaxなど)を返したいときに困ります。

ポインタとは(結論から)

ポインタは、メモリ上の“場所(アドレス)”を入れておくための変数です。

  • 普通の変数:数値そのもの(例:10)を持つ
  • ポインタ変数:その数値が置かれている場所(例:0x7ff…)を持つ

「値」と「場所」を表で比較

項目普通の変数ポインタ変数
中身値そのものアドレス(場所)
a = 10p = &a
役割値を使う値が置かれている場所を指す
できること自分の値を変える指している先の値を変えられる

アドレス演算子 & と 間接参照演算子 *

ポインタの理解は、この2つが軸になります。

&(アドレス演算子)

変数の場所(アドレス)を取り出す演算子です。

  • &a は「変数aが置かれている場所」

*(間接参照演算子)

ポインタが指している先の値にアクセスする演算子です。

  • p が a のアドレスを持っているなら、*p は a そのもの

図でイメージ(超重要)

ポインタで解決:呼び出し元の変数を書き換える

さっきの min_max を、ポインタで正しく直すとこうなります。

プロジェクト名:chap10-1-2 ソースファイル名:chap10-1-2.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

void min_max(int x, int y, int *min, int *max)  // 正しい:住所を受け取る
{
    if (x < y) {
        *min = x;
        *max = y;
    } else {
        *min = y;
        *max = x;
    }
}

int main(void)
{
    int a, b;
    int mn = 0, mx = 0;

    puts("2つの整数を入力してください。");
    printf("1つ目:");
    scanf("%d", &a);
    printf("2つ目:");
    scanf("%d", &b);

    min_max(a, b, &mn, &mx);  // mn/mxの住所を渡す

    printf("小さい方は%d、大きい方は%dです。\n", mn, mx);
    return 0;
}

何が変わった?

  • 関数の引数が int *min, int *max に変わった(住所を受け取る)
  • 代入が *min = ... の形に変わった(指している先を書き換える)
  • 呼び出しが &mn, &mx になった(住所を渡す)

ここで登場した要素を表で整理(記号・用語・役割)

要素意味よくあるつまずき
ポインタ型int *pintの住所を入れる変数*は「掛け算」じゃない(宣言では型の一部)
アドレス演算子&aaの住所を取り出すscanfで & を忘れる
間接参照*ppが指す先の値*p に代入すると「先の値」が変わる
値渡しmin_max(a,b,mn,mx)値のコピーを渡す関数内で変えても呼び出し元は変わらない
住所渡し(実現手段)min_max(a,b,&mn,&mx)場所を渡して直接変更&と*の対応がごちゃごちゃになる

使用した図・表の説明(読み方のコツ)

  • 箱の図は「変数=入れ物」を表しています。
    値を持つ箱(a, mnなど)と、住所を持つ箱(p, minなど)を分けて考えると混乱が減ります。
  • 矢印は「ポインタが指している先」を表しています。
    矢印の先の箱の中身に触るのが *(間接参照)です。
  • 比較表は「普通の変数」と「ポインタ変数」の役割の違いを整理しています。
    ポインタは“値”そのものではなく、“値の置き場所”を扱う道具だと分かればOKです。

この記事で出てきた命令(関数)と記号の書式・役割

※ここでは「命令」=主に標準ライブラリ関数や演算子として説明します。

puts

  • 書式:puts(文字列);
  • 何をする?:文字列を表示して、最後に改行も出します
  • 例:puts("2つの整数を入力してください。");

printf

  • 書式:printf(書式文字列, 引数1, 引数2, ...);
  • 何をする?:書式に合わせて値を表示します
  • 例:printf("小さい方は%d\n", mn);

scanf

  • 書式:scanf(書式文字列, 変数のアドレス, ...);
  • 何をする?:キーボード入力を読み取り、指定した変数に格納します
  • 超重要ポイント:格納先の「住所」が必要なので &a のように書きます
  • 例:scanf("%d", &a);

関数呼び出し(自作関数)

  • 書式:関数名(引数1, 引数2, ...);
  • 何をする?:処理をまとめた関数を実行します
  • 例:min_max(a, b, &mn, &mx);

return

  • 書式:return 値; または return;
  • 何をする?:関数を終了して呼び出し元に戻ります
  • mainが0を返すと「正常終了」の意味になります(慣習)

& と *

  • 書式:&変数、*ポインタ
  • 何をする?
    &:変数のアドレス(場所)を取り出す
    *:その場所にある値にアクセスする(読む/書く)

最後に:ポインタのコツ(やさしい覚え方)

  • & は「住所ちょうだい」
    * は「住所の先を開けて中身を見る(または書き換える)」
  • 関数で呼び出し元を変えたい、複数の結果を返したい → ポインタが活躍