
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 に到達できることを表しています。
- & と * の表は「住所を渡す側」と「住所の先をいじる側」を対応づけるための整理です。
