第7章:関数で整理する

この章で学ぶこと

  • 関数とは何か
  • 関数の定義と呼び出し
  • 引数と戻り値
  • 関数のプロトタイプ
  • スコープ(変数の有効範囲)
  • 複数ファイルでの分割
  • ---

    7.1 関数とは

    処理をまとめる

    関数(Function)は、一連の処理をまとめて名前を付けたものです。

    日常生活で例えると、「コーヒーを淹れる」という作業は、実際には多くのステップからなっています:

  • カップを用意する
  • コーヒー豆を挽く
  • お湯を沸かす
  • フィルターをセットする
  • 粉を入れる
  • お湯を注ぐ
  • 完成
  • しかし、誰かに頼むときは「コーヒーを淹れて」の一言で済みます。詳細は相手が知っているからです。

    関数も同じです。複雑な処理を「関数名」で呼び出せるようにまとめます。

    関数を使うメリット

  • 再利用性: 同じ処理を何度も書かなくて済む
  • 可読性: コードが読みやすくなる
  • 保守性: バグ修正が1箇所で済む
  • テスト性: 部品ごとにテストできる
  • すでに使っている関数

    実は、もう関数を使っています。

    printf("Hello, World!\n");  // printf関数
    scanf("%d", &num);          // scanf関数
    

    これらはライブラリ関数(誰かが作ってくれた関数)です。自分で関数を作ることもできます。

    ---

    7.2 関数の基本

    関数の定義

    戻り値の型 関数名(引数の型 引数名, ...) {
        // 処理
        return 戻り値;
    }
    

    最も簡単な関数

    引数も戻り値もない関数:

    void say_hello(void)
    {
        printf("Hello!\n");
    }
    

  • void: 戻り値がないことを示す
  • say_hello: 関数名
  • (void): 引数がないことを示す

関数の呼び出し

#include <stdio.h>

void say_hello(void)
{
    printf("Hello!\n");
}

int main(void)
{
    say_hello();  // 関数の呼び出し
    say_hello();  // 何度でも呼び出せる
    say_hello();
    return 0;
}

出力:

Hello!
Hello!
Hello!

main関数

main は特別な関数です。プログラムはここから実行が始まります。

int main(void)
{
    // プログラムのエントリーポイント
    return 0;  // 0: 正常終了
}

---

7.3 引数

関数にデータを渡す

引数(Argument/Parameter)を使うと、関数にデータを渡せます。

void greet(char *name)
{
    printf("Hello, %s!\n", name);
}

int main(void)
{
    greet("Alice");
    greet("Bob");
    return 0;
}

出力:

Hello, Alice!
Hello, Bob!

複数の引数

void print_sum(int a, int b)
{
    printf("%d + %d = %d\n", a, b, a + b);
}

int main(void)
{
    print_sum(3, 5);
    print_sum(10, 20);
    return 0;
}

出力:

3 + 5 = 8
10 + 20 = 30

引数の値渡し

C言語では、引数は値渡し(Call by Value)です。関数には値のコピーが渡されます。

void change_value(int x)
{
    x = 100;  // これはコピーを変更している
    printf("Inside function: x = %d\n", x);
}

int main(void)
{
    int num = 5;
    printf("Before: num = %d\n", num);
    change_value(num);
    printf("After: num = %d\n", num);
    return 0;
}

出力:

Before: num = 5
Inside function: x = 100
After: num = 5

関数内で x を変更しても、元の num は変わりません。

---

7.4 戻り値

関数から値を返す

戻り値(Return Value)を使うと、関数から値を返せます。

int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    int result = add(3, 5);
    printf("Result: %d\n", result);
    return 0;
}

出力:

Result: 8

returnの役割

return は2つの役割があります:

  • 値を呼び出し元に返す
  • 関数の実行を終了する

int find_positive(int a, int b, int c)
{
    if (a > 0) return a;
    if (b > 0) return b;
    if (c > 0) return c;
    return 0;  // すべて0以下の場合
}

戻り値の型

関数の戻り値の型と、return で返す値の型は一致する必要があります。

double divide(int a, int b)
{
    return (double)a / b;  // double型を返す
}

戻り値がない関数(void)

void 型の関数は値を返しません。

void print_message(char *msg)
{
    printf("%s\n", msg);
    // return; は省略可能
}

return; で早期に関数を抜けることはできます:

void process(int x)
{
    if (x < 0) {
        printf("Error: negative number\n");
        return;  // ここで関数を抜ける
    }
    printf("Processing: %d\n", x);
}

---

7.5 関数のプロトタイプ

関数の宣言と定義

C言語では、関数を使う前に、その関数が存在することをコンパイラに教える必要があります。

#include <stdio.h>

// 関数のプロトタイプ(宣言)
int add(int a, int b);

int main(void)
{
    printf("%d\n", add(3, 5));  // 関数を呼び出す
    return 0;
}

// 関数の定義(実装)
int add(int a, int b)
{
    return a + b;
}

プロトタイプの書き方

戻り値の型 関数名(引数の型 引数名, ...);

引数名は省略できます:

int add(int, int);  // これも有効

なぜプロトタイプが必要か

C言語のコンパイラは、上から下へコードを読みます。関数を呼び出す時点で、その関数の情報(戻り値の型、引数の型)を知っている必要があります。

プロトタイプを書かない場合、関数の定義を呼び出しより前に書く必要があります:

// プロトタイプなしの場合
int add(int a, int b)  // 定義を先に書く
{
    return a + b;
}

int main(void)
{
    printf("%d\n", add(3, 5));
    return 0;
}

複雑なプログラムでは、関数が互いに呼び出し合うことがあるため、プロトタイプが必要になります。

---

7.6 スコープ

変数の有効範囲

スコープ(Scope)は、変数が有効な範囲のことです。

ローカル変数

関数内で宣言された変数は、その関数内でのみ有効です。

void func1(void)
{
    int x = 10;  // func1内でのみ有効
    printf("func1: x = %d\n", x);
}

void func2(void)
{
    int x = 20;  // func2内の別のx
    printf("func2: x = %d\n", x);
}

int main(void)
{
    func1();
    func2();
    // printf("%d\n", x);  // エラー!xは見えない
    return 0;
}

出力:

func1: x = 10
func2: x = 20

ブロックスコープ

波括弧 {} で囲まれたブロック内で宣言された変数は、そのブロック内でのみ有効です。

int main(void)
{
    int x = 1;
    printf("outer x: %d\n", x);

    {
        int x = 2;  // 新しいx(外側のxとは別)
        printf("inner x: %d\n", x);
    }

    printf("outer x again: %d\n", x);
    return 0;
}

出力:

outer x: 1
inner x: 2
outer x again: 1

グローバル変数

関数の外で宣言された変数はグローバル変数で、どの関数からでもアクセスできます。

#include <stdio.h>

int global_count = 0;  // グローバル変数

void increment(void)
{
    global_count++;
}

int main(void)
{
    printf("count: %d\n", global_count);  // 0
    increment();
    printf("count: %d\n", global_count);  // 1
    increment();
    printf("count: %d\n", global_count);  // 2
    return 0;
}

グローバル変数の注意点

グローバル変数は便利ですが、以下の問題があります:

  • どこからでも変更できるため、バグの原因になりやすい
  • 関数の動作が外部状態に依存し、予測しにくくなる
  • 再利用しにくくなる

できるだけローカル変数を使い、グローバル変数は最小限にしましょう。

---

7.7 static変数

関数内で状態を保持

static キーワードをつけると、関数が終了しても値が保持されます。

#include <stdio.h>

void counter(void)
{
    static int count = 0;  // 最初の呼び出し時のみ初期化
    count++;
    printf("Count: %d\n", count);
}

int main(void)
{
    counter();  // Count: 1
    counter();  // Count: 2
    counter();  // Count: 3
    return 0;
}

通常のローカル変数は関数が終了すると消えますが、static 変数は残ります。

static変数の初期化

static 変数は、プログラム開始時に一度だけ初期化されます。関数が呼ばれるたびに初期化されるわけではありません。

void func(void)
{
    static int x = 0;  // 最初の1回だけ実行
    x++;
    printf("%d\n", x);
}

---

7.8 実践:便利な関数を作る

最大値を返す関数

int max(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}

より簡潔に:

int max(int a, int b)
{
    return (a > b) ? a : b;
}

3つの値の最大値

int max3(int a, int b, int c)
{
    return max(max(a, b), c);
}

絶対値

int abs_value(int x)
{
    return (x < 0) ? -x : x;
}

べき乗

int power(int base, int exp)
{
    int result = 1;
    for (int i = 0; i < exp; i++) {
        result *= base;
    }
    return result;
}

階乗

int factorial(int n)
{
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

---

7.9 再帰

自分自身を呼び出す関数

再帰(Recursion)とは、関数が自分自身を呼び出すことです。

階乗の再帰版

数学的定義:

  • 0! = 1
  • n! = n × (n-1)!

int factorial(int n)
{
    if (n == 0)
        return 1;           // 基底条件
    return n * factorial(n - 1);  // 再帰呼び出し
}

実行の流れ(n=4の場合):

factorial(4)
= 4 * factorial(3)
= 4 * 3 * factorial(2)
= 4 * 3 * 2 * factorial(1)
= 4 * 3 * 2 * 1 * factorial(0)
= 4 * 3 * 2 * 1 * 1
= 24

再帰の要素

  • 基底条件(Base Case): 再帰を止める条件
  • 再帰呼び出し: より小さい問題で自分自身を呼ぶ

基底条件がないと、無限に再帰してスタックオーバーフローになります。

フィボナッチ数列

int fibonacci(int n)
{
    if (n <= 1)
        return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

注意: この実装は効率が悪いです(同じ計算を何度もする)。実際には反復(ループ)で書く方が効率的です。

再帰 vs 反復

多くの場合、再帰は反復(ループ)で書き換えられます。

// 再帰版
int sum_recursive(int n)
{
    if (n == 0)
        return 0;
    return n + sum_recursive(n - 1);
}

// 反復版
int sum_iterative(int n)
{
    int total = 0;
    for (int i = 1; i <= n; i++) {
        total += i;
    }
    return total;
}

再帰は:

  • コードが簡潔になることがある
  • スタックメモリを消費する
  • オーバーヘッドがある

初心者のうちは、まずループで考えることをお勧めします。

---

7.10 複数ファイルへの分割

大きなプログラムの管理

プログラムが大きくなると、1つのファイルでは管理しにくくなります。関連する関数をグループ化して、別ファイルに分けます。

ヘッダーファイル (.h)

関数のプロトタイプや定数を宣言します。

math_utils.h:

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int max(int a, int b);
int min(int a, int b);
int abs_value(int x);

#endif

#ifndef ... #define ... #endifインクルードガードで、同じヘッダーが複数回読み込まれるのを防ぎます。

ソースファイル (.c)

関数の実装を書きます。

math_utils.c:

#include "math_utils.h"

int max(int a, int b)
{
    return (a > b) ? a : b;
}

int min(int a, int b)
{
    return (a < b) ? a : b;
}

int abs_value(int x)
{
    return (x < 0) ? -x : x;
}

メインファイル

main.c:

#include <stdio.h>
#include "math_utils.h"

int main(void)
{
    printf("max(3, 7) = %d\n", max(3, 7));
    printf("min(3, 7) = %d\n", min(3, 7));
    printf("abs(-5) = %d\n", abs_value(-5));
    return 0;
}

コンパイル

$ gcc main.c math_utils.c -o program
$ ./program
max(3, 7) = 7
min(3, 7) = 3
abs(-5) = 5

複数のファイルを一緒にコンパイルします。

---

7.11 この章のまとめ

学んだこと

  • 関数の基本
- 処理をまとめて再利用 - 定義と呼び出し

  • 引数
- 関数にデータを渡す - 値渡し(コピーが渡される)

  • 戻り値
- return で値を返す - void は戻り値なし

  • プロトタイプ
- 関数の宣言 - 使用前に必要

  • スコープ
- ローカル変数: 関数内のみ - グローバル変数: どこからでも(なるべく避ける)

  • static変数
- 関数内で状態を保持

  • 再帰
- 自分自身を呼び出す - 基底条件が必要

  • ファイル分割
- ヘッダーファイル (.h): プロトタイプ - ソースファイル (.c): 実装

関数設計の原則

  • 1つの関数は1つの仕事: 関数は単一の目的を持つべき
  • 適切な名前: 関数名から動作が分かるように
  • 短く保つ: 長すぎる関数は分割を検討
  • 副作用を最小限に: グローバル変数を変更しない

次の章の予告

次章では、配列と文字列を学びます。複数のデータをまとめて扱う方法を覚えましょう。

---

Column: 関数の歴史

サブルーチンの誕生

関数の概念は、1940年代のサブルーチン(副プログラム)に遡ります。

初期のコンピュータでは、同じ処理を何度も書くのは大変でした。そこで、処理をまとめて「サブルーチン」として呼び出す仕組みが生まれました。

ライブラリの発展

共通のサブルーチンをまとめて「ライブラリ」として提供するようになりました。printfscanf もライブラリ関数です。

C言語の標準ライブラリには、数学関数、文字列操作、ファイル入出力など、多くの便利な関数が含まれています。

モジュール化の思想

1970年代には「構造化プログラミング」が提唱され、プログラムを小さな関数に分割する考え方が広まりました。

この思想は現代のソフトウェア開発にも受け継がれ、「関数は短く、1つの仕事だけをする」という原則として残っています。

関数型プログラミング

近年は「関数型プログラミング」というパラダイムも注目されています。関数を値として扱い、副作用を避ける考え方です。

C言語は手続き型言語ですが、関数ポインタ(上級者向け)を使えば、関数型プログラミングの一部の概念を取り入れることもできます。

---

確認問題

問題1

以下の関数の問題点は何ですか?

int add(int a, int b)
{
    int result = a + b;
}

解答

return 文がない。return result; を追加する必要がある。

問題2

以下のコードの出力は何ですか?

void change(int x)
{
    x = x * 2;
}

int main(void)
{
    int num = 5;
    change(num);
    printf("%d\n", num);
    return 0;
}

解答

5

C言語では引数は値渡しなので、関数内での変更は元の変数に影響しない。

問題3

2つの整数の平均を返す関数 average を書いてください。

解答

double average(int a, int b)
{
    return (double)(a + b) / 2;
}

整数同士の割り算では小数が切り捨てられるため、double にキャストする。

問題4

関数のプロトタイプとは何ですか?なぜ必要ですか?

解答

プロトタイプは関数の宣言(戻り値の型、関数名、引数の型を宣言)。

コンパイラは上から下へコードを読むため、関数を呼び出す前にその情報を知っている必要がある。プロトタイプを先頭に書くことで、関数の定義をどこに書いても呼び出せるようになる。

問題5

再帰を使って、1からnまでの整数の合計を計算する関数 sum(n) を書いてください。

解答

int sum(int n)
{
    if (n == 1)
        return 1;
    return n + sum(n - 1);
}

---

次の章では、配列と文字列を学びます。複数のデータを効率よく扱いましょう!