C言語超入門⑤ 関数で処理を分けよう
関数の定義、引数、戻り値、プロトタイプ宣言、変数の有効範囲を初心者向けに説明します。
同じ計算や表示をコピーして使っていると、修正するときに「あっちも直したかな?」と不安になります。そんな処理に名前を付け、1か所へまとめるのが関数です。
今回は、自分で作った関数へ値を渡し、結果を受け取るところまで進みます。用語は似ていますが、「表示する」と「返す」は別。その違いを先にはっきりさせましょう。
1. 関数は、定義しただけでは動かない
#include <stdio.h>
void greet(void) {
printf("こんにちは。\n");
}
int main(void) {
printf("開始\n");
greet();
greet();
printf("終了\n");
return 0;
}
開始
こんにちは。
こんにちは。
終了
greetは「こんにちは」を表示する処理の名前です。greet();のように名前とかっこを書くことで、処理を呼び出すことができます。
上に書いたgreetの中身が先に実行されるわけではありません。プログラムはmainから始まり、呼び出しがあるたびにgreetへ進み、終わると呼び出した次の行へ戻ります。
voidが2か所ある理由
void greet(void)の左のvoidは「結果の値を返さない」、かっこの中のvoidは「引数を受け取らない」です。
同じ単語ですが、場所によって何がないのかが違います。C11のコードでは、引数なしを明確にするためgreet(void)と書きます。
2. 引数で、関数に材料を渡す
名前だけ変えてあいさつしたり、違う点数を表示したりするには、関数へ値を渡します。その値が引数(ひきすう)です。
#include <stdio.h>
void showScore(int score) {
printf("点数は%d点です。\n", score);
}
int main(void) {
showScore(80);
showScore(95);
return 0;
}
1回目の呼び出しではscoreが80になり、2回目では95になります。処理をコピーせず、材料だけ変えて同じ仕事を頼めるわけです。
厳密には、定義側のint scoreを仮引数、呼び出し側の80などを実引数と呼びます。まずは「入口として用意する変数」と「実際に渡す値」を区別できれば十分です。
3. returnで計算結果を受け取る
#include <stdio.h>
int add(int a, int b) {
int result = a + b;
return result;
}
int main(void) {
int answer = add(7, 5);
printf("答えは%dです。\n", answer);
return 0;
}
結果は答えは12です。です。短いコードですが、値の動きを分けると次のようになります。
| 順番 | 起きること |
|---|---|
| 1 | mainでadd(7, 5)を呼ぶ |
| 2 | addのaに7、bに5が入る |
| 3 | a + bを計算してresultを12にする |
| 4 | return resultで12を呼び出し元へ返す |
| 5 | mainのanswerが12になり、表示する |
関数名の前のintは、戻り値の型です。戻り値は、関数が呼び出し元へ渡す結果のこと。「返す」は画面表示ではありません。
printfとreturnは代わりにならない
addの中をprintf("%d\n", a + b);だけにすると、画面には数字を出せても、answerで使う計算結果を正しく返せません。
計算する関数と表示する処理を分ければ、結果を表示せず別の計算へ回すこともできます。
int answer = add(7, 5);
int doubled = answer * 2;
printf("%d\n", doubled);
結果は24です。練習では小さな整数を使います。intの加算には表せる範囲があるため、任意の巨大な値を安全に足す関数ではありません。
4. returnすると、その関数はそこで終わる
int max(int a, int b) {
if (a >= b) {
return a;
}
return b;
}
return a;が実行されたら、下のreturn b;へは進みません。呼び出した側に戻ります。
この性質を使うと、満たさない条件を先に断る書き方もできます。
void showPositive(int number) {
if (number <= 0) {
return;
}
printf("%d\n", number);
}
voidの関数では、値を付けずにreturn;と書けます。一方、intなどの値を返す関数は、結果が必要なすべての経路で正しい値を返すようにします。
5. プロトタイプ宣言で関数の形を知らせる
これまでは関数をmainより前に置きました。後ろへ置きたい場合は、先に形だけを宣言します。
#include <stdio.h>
int add(int a, int b);
int main(void) {
printf("%d\n", add(3, 4));
return 0;
}
int add(int a, int b) {
return a + b;
}
int add(int a, int b);がプロトタイプ宣言です。名前、引数の型と数、戻り値の型をコンパイラへ伝えます。宣言は;で終わり、中身の{ }はありません。
add(3)のように引数が足りない呼び出しは、宣言と合いません。エラーになったら、定義・宣言・呼び出しの3か所を見比べてください。
関数の呼び出しと引数の規則は、C11公開委員会草案の6.5.2.2と6.9.1で確認できます。
6. スコープと、値のコピーを理解する
スコープは、その変数名を使える範囲です。関数の中で作った変数は、基本的に外から直接使えません。
#include <stdio.h>
void addOne(int number) {
number++;
printf("関数の中: %d\n", number);
}
int main(void) {
int count = 3;
addOne(count);
printf("mainの中: %d\n", count);
return 0;
}
関数の中: 4
mainの中: 3
「あれ、4にならないの?」となりやすい例です。
addOneはcountそのものではなく、値3のコピーを受け取ります。関数内のnumberを変更しても、mainのcountは変わりません。これが値渡しです。
変更した値を使いたいなら、戻り値を受け取ってcount = addOne(count);とする設計に変えるか、後のポインタの回で学ぶ方法を使います。
関数内の変数は、呼ぶたびに作られる
普通のローカル変数は、呼び出しをまたいで前回の値を覚えるものではありません。int total = 0;が関数の中にあれば、呼ぶたびにその初期化が行われます。
関数の外へ全部の変数を出せば解決、とは考えない方がよいでしょう。どこから値が変わるか追いにくくなるため、まず引数と戻り値で受け渡す形を練習します。
7. 練習:大きい方を返すmaxを作ろう
2つの整数を受け取り、大きい方を返してください。同じ値ならどちらを返しても構いません。結果はmainで表示します。
模範解答
#include <stdio.h>
int max(int a, int b) {
if (a >= b) {
return a;
}
return b;
}
int main(void) {
printf("%d\n", max(12, 7));
printf("%d\n", max(-4, -2));
printf("%d\n", max(5, 5));
return 0;
}
12
-2
5
解説
- 12と7では、
a >= bが真なので12を返します。 - -4と-2では条件が偽なので、最後の
return bで-2を返します。 - 5と5は等しいので最初の条件に入り、5を返します。
0から最大値を探し始めるような処理ではないため、負の数だけでも正しく比べられます。この関数は加算しないので、大小比較そのものによる整数オーバーフローも起きません。
追加で「小さい方」を返したい場合の答えは、条件をa <= bへ変更したmin関数です。比較の向きだけが変わり、値を受け取って返す流れは同じです。
次の一歩
関数を作るときは、まず「何を受け取り、何を返すか」を1文にしましょう。「2つの整数を受け取り、大きい方を返す」と言えれば、入口と出口の設計はできています。
次は、たくさんの値を関数やループで扱うための配列を学びます。