プログラミング

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

解説

  1. 12と7では、a >= bが真なので12を返します。
  2. -4と-2では条件が偽なので、最後のreturn bで-2を返します。
  3. 5と5は等しいので最初の条件に入り、5を返します。

0から最大値を探し始めるような処理ではないため、負の数だけでも正しく比べられます。この関数は加算しないので、大小比較そのものによる整数オーバーフローも起きません。

追加で「小さい方」を返したい場合の答えは、条件をa <= bへ変更したmin関数です。比較の向きだけが変わり、値を受け取って返す流れは同じです。

次の一歩

関数を作るときは、まず「何を受け取り、何を返すか」を1文にしましょう。「2つの整数を受け取り、大きい方を返す」と言えれば、入口と出口の設計はできています。

次は、たくさんの値を関数やループで扱うための配列を学びます。

前の回 / 次の回:配列と文字列

← 記事一覧へ戻る