トラック7・🧩関数とスコープ

関数分割の設計センス

読む目安 約12分・ゴール:1関数1仕事の原則で、長い処理を読みやすい関数に分けられる

アプリでこのトピックを学ぶ(無料・登録不要)

このトピックの要点

文法を覚えた次は、「どこで切るか」という設計の話です。判断の軸はひとつ、1つの関数は1つの仕事。これだけ守れば、たいていうまくいきます。

分割前のコードを見てください。

#include <stdio.h>

int main(void)
{
    int t[5] = {60, 85, 40, 90, 75};
    int sum = 0;
    for (int i = 0; i < 5; i++) sum += t[i];
    int max = t[0];
    for (int i = 1; i < 5; i++) if (t[i] > max) max = t[i];
    printf("合計 %d / 最高 %d / 平均 %.1f\n", sum, max, (double)sum / 5);
    return 0;
}

やっていることは「合計」「最高点」「表示」の3つです。名前を付けて分けます。

#include <stdio.h>

int sum_of(int a[], int n)
{
    int sum = 0;
    for (int i = 0; i < n; i++) sum += a[i];
    return sum;
}

int max_of(int a[], int n)
{
    int max = a[0];
    for (int i = 1; i < n; i++) if (a[i] > max) max = a[i];
    return max;
}

int main(void)
{
    int t[5] = {60, 85, 40, 90, 75};
    int sum = sum_of(t, 5);
    printf("合計 %d / 最高 %d / 平均 %.1f\n", sum, max_of(t, 5), (double)sum / 5);
    return 0;
}

行数はむしろ増えました。それでも読みやすくなったと感じるはずです。main が「何をしているか」の一覧になり、細部が名前の裏に隠れたからです。

名前が説明になる

分割の効果でいちばん大きいのは、実は行数ではなく名前です。sum_of(t, 5) という1行は、中身を読まなくても「合計を出している」と分かります。コメントで補わなくても名前が説明になっている状態がゴールです。逆に、良い名前が思いつかない関数は、たいてい仕事が2つ以上混ざっています。

入力と出力を先に決める

分けるときは「この関数は何を受け取り、何を返すのか」を最初に決めます。それが決まっていれば、中身は後回しでもプロトタイプ宣言の形で書き出せます。

int sum_of(int a[], int n);   // 配列と個数を受け取り、合計を返す
int max_of(int a[], int n);   // 配列と個数を受け取り、最大値を返す

先に宣言だけを並べて設計を確かめ、あとから中身を埋めていく。実務でもよく使う進め方です。

分けるサイン

やりすぎにも注意

1行しかない関数を大量に作ると、こんどは呼び出しを追いかける旅が始まります。「読むために5か所を行き来する」なら、それは分けすぎのサインです。また、引数が5個を超えたら設計を疑ってください。関係のあるデータをひとまとめにする道具(構造体)が第3部で登場し、引数の行列はそこで一気に片づきます。

素直な関数を目指す

同じ引数を渡したら必ず同じ結果が返り、外の世界を書き換えない関数は、読むのもテストするのも簡単です。上の sum_of max_of がまさにそれです。逆に、グローバル変数を書き換える関数や static で状態を持つ関数は、便利でも扱いが難しくなります。

判断に迷ったら「この関数だけを単体で呼んで、結果を確かめられるか」と自問してください。答えがイエスなら、その分割はうまくいっています。表示と計算が混ざっていると単体で確かめにくいので、計算する関数と表示する関数を分けるのが定番の第一歩です。

ビット迷ったら「この関数、ひとことで説明できる?」って聞いてみて

最初から完璧に分ける必要はありません。まず動くものを一気に書き、動いたあとで「ここは名前を付けられるな」と思った場所を切り出す。この順番のほうが、手が止まらずに済みます。分割は後からでもやり直せる作業です。

まとめ: 1関数1仕事、動詞で命名、短く、素直に。分割はセンスではなく、いくつかの目印で判断できる技術です。

アプリでこのトピックを学ぶ(無料・登録不要)

確認問題

確認問題 1

次のプログラムの出力はどれですか。

#include <stdio.h>

int twice(int x)
{
    return x * 2;
}

int plus_one(int x)
{
    return x + 1;
}

int main(void)
{
    printf("%d\n", twice(plus_one(3)));
    return 0;
}
  1. 7
  2. 8
  3. 9
  4. 6
答えを見る

正解:B(8)

内側から評価されます。plus_one(3) が 4 を返し、その 4 が twice に渡って 8 になります。twice(3) + 1 = 7 と読み違えないよう、カッコの内側から順に「戻り値に置き換える」読み方を身につけましょう。

確認問題 2

「1からnまでの合計を返す関数を書いて」と頼んだら、アイがこう書きました。sum_upto(5) は 15 になるはずです。アイの答案にバグはありますか。

1#include <stdio.h>
2
3int sum_upto(int n)
4{
5    int total = 0;
6    for (int i = 1; i <= n; i++) {
7        total += i;
8        return total;
9    }
10}
11
12int main(void)
13{
14    printf("%d\n", sum_upto(5));
15    return 0;
16}
答えを見る

判定:バグあり(8行目)

8行目の return が for の中にあります。return は「値を返して即座に関数を終わらせる」ので、1周目の i = 1 の時点で 1 が返ってしまいます。return はループの外(9行目の } の後)に置くのが正解です。ついでに、ループの外に return が無い経路も未定義動作になるため、この位置なら両方の問題が同時に直ります。

「関数とスコープ」の目次

  1. 関数とは——処理に名前を付ける
  2. 引数と戻り値
  3. プロトタイプ宣言
  4. 変数のスコープ——見える範囲
  5. 変数の寿命とstatic
  6. コールスタック——関数呼び出しの舞台裏
  7. 値渡しの本質
  8. グローバル変数との付き合い方
  9. 関数分割の設計センス
  10. 再帰の入口——自分を呼ぶ関数

もっと先へ:ポインタ・メモリ・ファイル入出力・セキュアコーディングを含む全26トラックと、段位検定・模試のフルセットは完全版に収録しています。

完全版の販売ページは準備中です。

ほかのトラック

📚 姉妹教材:手を動かして覚える Linux 教材 — Linuxとインフラの仕組みを地図で学ぶ

Web版