C言語基礎|ポインタと関数

関数に「値」じゃなく「住所」を渡すだけで、C言語は一気に自由になる。

C言語のポインタが本領発揮する場面のひとつが、関数の引数です。

関数に値を渡すだけだと、関数の中でいくら頑張っても、呼び出し元の変数は変わりません。
これはC言語が基本的に 値渡し(コピー) だからです。

でも「呼び出し元の変数を、関数から変更したい」「結果を2つ以上返したい」みたいな要求って、実務でも学習でもめちゃくちゃよく出ますよね。

そこで、関数に渡すのを「値」から「住所(アドレス)」へ切り替えます。
つまり ポインタを引数にするということです。

ポインタを渡す = 「この住所の箱を直接いじってください」とお願いする

この発想が分かると、関数設計がグッと楽になりますよ。

なぜ普通の引数だとダメ?(値渡しの復習)

まず、呼び出し側の変数を変更したいのに、次のように書くのはNGです。

NG例:仮引数はコピーなので、呼び出し元は変わらない

void set_fixed(int p)
{
    p = 42;
}

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

解決策:ポインタを引数にして、参照外しで書き換える

ポインタを受け取る関数は、こう考えると分かりやすいです。

  • 呼び出し側:&x を渡す(xの住所を渡す)
  • 関数側:*p に代入する(住所の先の中身を書き換える)

ポインタ引数の基本形

やりたいこと呼び出し側関数側
x を関数から変更したい&x を渡す*p に代入する

サンプルプログラム

指定した変数を0にリセットする関数 reset_to_zeroを用いたプログラム例です。

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

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

#include <stdio.h>

// pが指す変数を0にする
void reset_to_zero(int *p)
{
    *p = 0;
}

int main(void)
{
    int hp = 80;
    int mp = 25;
    int sel;

    puts("現在のステータスです。");
    printf("hp = %d\n", hp);
    printf("mp = %d\n", mp);

    printf("どちらをリセットしますか? 0:hp  1:mp  = ");
    scanf("%d", &sel);

    if (sel == 0) {
        reset_to_zero(&hp);
    } else {
        reset_to_zero(&mp);
    }

    puts("リセット後のステータスです。");
    printf("hp = %d\n", hp);
    printf("mp = %d\n", mp);

    return 0;
}

何が嬉しい?

関数 reset_to_zero の中で直接 hp や mp に触っていないのに、呼び出し元の値が変わります
これは、渡しているのが「値」ではなく「住所」だからです。

関数呼び出しで起きていること(図で理解)

sel が 0 のときを例にします。

図:&hp が p にコピーされ、*p が hp の別名になる

ここがポイント(超重要)

  • p 自体は「住所」という値を持つだけ
  • *p が「住所の先の本人(オブジェクト)」を表す。
  • だから *p に代入すると、呼び出し元が変わる。

和や差など「複数結果」を返すときもポインタが便利

C言語の関数が返せる return は基本的に1個だけです。
そこで「結果を入れる箱を呼び出し側が用意し、その住所を渡す」というスタイルが定番になります。

複数結果の受け渡しパターン

目的方法イメージ
返り値が1個で足りない結果格納用の変数を渡す合計用、差分用など
関数から呼び出し元の値を更新したい住所を渡して書き換えるreset_to_zero のような感じ

演習問題

演習10-1:値を範囲内に丸める関数を作ろう

score が 0 未満なら 0 に、200 より大きければ 200 に更新し、0~200 の範囲なら変更しない関数 clamp_score を作成せよ。

関数プロトタイプ:

void clamp_score(int *score);

解答例

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

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

#include <stdio.h>

void clamp_score(int *score)
{
    if (*score < 0) {
        *score = 0;
    } else if (*score > 200) {
        *score = 200;
    }
}

int main(void)
{
    int s;

    puts("点数を入力してください(例:-5 や 250 も試してね)。");
    printf("score = ");
    scanf("%d", &s);

    clamp_score(&s);

    printf("補正後の score = %d\n", s);
    return 0;
}

解説(なぜポインタが必要?)

  • clamp_score の中で *score を変更すると、呼び出し元の変数 s が書き換わります。
  • 値渡し(int score)にしてしまうと、関数内のコピーが変わるだけで s は変わりません。
  • 「呼び出し元の変数を更新したい」=「住所を渡して書き換える」が合言葉です。

登場する命令(関数)と書式・何をする?

この記事で出てきた要素を、命令(関数)中心に整理します。

reset_to_zero(自作関数)

  • 書式:reset_to_zero(変数のアドレス);
  • 何をする?:渡された住所の先にある int を 0 に書き換える
  • 重要:引数は int *(intの住所)

clamp_score(自作関数)

  • 書式:clamp_score(変数のアドレス);
  • 何をする?:渡された住所の先の値を 0~200 に収める

puts

  • 書式:puts(文字列);
  • 何をする?:文字列を表示し、最後に改行も出す

printf

  • 書式:printf(書式文字列, 引数1, 引数2, ...);
  • 何をする?:書式に合わせて値を表示する

scanf

  • 書式:scanf(書式文字列, 変数のアドレス, ...);
  • 何をする?:入力を読み取り、指定した変数へ格納する。
  • ポイント:格納先の住所が必要なので &s のように書く。

& と * の役割をもう一度表で整理

記号名前何をする?
&アドレス演算子変数の住所を取り出す&hp
*間接演算子住所の先の本人を扱う*p = 0

使った表や図の説明(読み方)

  • 値渡しの図は「コピーが渡される」ので呼び出し元が変わらないことを表しています。
  • ポインタ渡しの図は「住所がコピーされる」だけなので、コピー先の p から元の hp に到達できることを表しています。
  • & と * の表は「住所を渡す側」と「住所の先をいじる側」を対応づけるための整理です。