第5章: 関数

5.1 関数の理論

抽象化としての関数

関数(Function)は、プログラミングにおける最も基本的な抽象化メカニズムである。

数学における関数: $$f: X \rightarrow Y$$

ここでXは定義域(入力の集合)、Yは値域(出力の集合)。

プログラミングにおける関数は、この数学的概念を拡張し:

  • 副作用を持つことができる
  • 状態を変更することができる
  • 手続きとして振る舞うことができる

手続き vs 関数

/* 純粋な関数(数学的関数) */
int add(int a, int b)
{
    return a + b;  /* 同じ入力には常に同じ出力 */
}

/* 手続き(副作用を持つ) */
void print_number(int n)
{
    printf("%d\n", n);  /* 出力という副作用 */
}

/* 状態に依存する関数 */
int counter(void)
{
    static int count = 0;
    return ++count;  /* 呼び出すたびに異なる値 */
}

参照透過性

参照透過性(Referential Transparency)は、式を評価結果で置き換えても意味が変わらない性質。

/* 参照透過 */
int square(int x) { return x * x; }
int a = square(3) + square(3);  /* 9 + 9 = 18 */
/* square(3) を 9 で置き換えても同じ */

/* 非参照透過 */
int rand_num(void) { return rand(); }
int b = rand_num() + rand_num();  /* 異なる2つのランダム値 */
/* rand_num() を値で置き換えると意味が変わる */

5.2 関数の定義と宣言

関数定義

return_type function_name(parameter_list)
{
    /* 関数本体 */
    return expression;
}

/* 例: 2つの整数の最大値を返す */
int max(int a, int b)
{
    if (a > b)
        return a;
    return b;
}

関数宣言(プロトタイプ)

return_type function_name(parameter_list);

プロトタイプの目的:

  • 型チェック: 引数と戻り値の型を検証
  • 暗黙の宣言防止: C99以降では未宣言関数はエラー
  • 相互再帰: 2つの関数が互いを呼び出す場合に必要

/* プロトタイプ宣言 */
int factorial(int n);

/* 使用(定義の前でも可能) */
int main(void)
{
    printf("%d\n", factorial(5));
    return 0;
}

/* 定義 */
int factorial(int n)
{
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

K&Rスタイル vs ANSIスタイル

/* K&Rスタイル(古い、非推奨) */
int add(a, b)
int a;
int b;
{
    return a + b;
}

/* ANSIスタイル(現代的) */
int add(int a, int b)
{
    return a + b;
}

5.3 引数渡し

値渡し(Pass by Value)

C言語は常に値渡しを使用する。引数は全てコピーされる。

void increment(int x)
{
    x++;  /* ローカルコピーを変更 */
}

int main(void)
{
    int a = 10;
    increment(a);
    printf("%d\n", a);  /* 10(変更されない) */
    return 0;
}

ポインタによる参照渡しの模倣

void increment(int *x)
{
    (*x)++;  /* ポインタが指す値を変更 */
}

int main(void)
{
    int a = 10;
    increment(&a);
    printf("%d\n", a);  /* 11(変更された) */
    return 0;
}

配列の引数渡し

配列を引数として渡すと、最初の要素へのポインタに変換される(配列の減衰)。

void func(int arr[10])    /* int *arr と同じ */
{
    sizeof(arr);  /* ポインタのサイズ(8バイト等) */
}

void func2(int arr[])     /* int *arr と同じ */
{
}

void func3(int *arr)      /* 明示的なポインタ */
{
}

/* 配列サイズを別途渡す */
void process_array(int *arr, size_t size)
{
    for (size_t i = 0; i < size; i++)
        arr[i] *= 2;
}

構造体の引数渡し

struct Point {
    double x, y;
};

/* 値渡し(コピーが発生) */
void print_point(struct Point p)
{
    printf("(%f, %f)\n", p.x, p.y);
}

/* ポインタ渡し(効率的) */
void print_point_ptr(const struct Point *p)
{
    printf("(%f, %f)\n", p->x, p->y);
}

5.4 戻り値

基本的な戻り値

/* 値を返す */
int add(int a, int b)
{
    return a + b;
}

/* 何も返さない */
void greet(void)
{
    printf("Hello!\n");
    /* returnは省略可能 */
}

構造体の戻り値

struct Point create_point(double x, double y)
{
    struct Point p = {x, y};
    return p;  /* 構造体全体をコピーして返す */
}

/* C99: 複合リテラル */
struct Point create_point_c99(double x, double y)
{
    return (struct Point){x, y};
}

ポインタの戻り値

/* 危険!ローカル変数へのポインタを返す */
int *bad_function(void)
{
    int local = 42;
    return &local;  /* ダングリングポインタ! */
}

/* 安全: 動的メモリを返す */
int *create_array(size_t n)
{
    int *arr = malloc(n * sizeof(int));
    return arr;  /* 呼び出し側がfreeする責任 */
}

/* 安全: 静的変数へのポインタ */
int *get_counter(void)
{
    static int counter = 0;
    counter++;
    return &counter;
}

エラーの戻り値

/* パターン1: 特別な値を返す */
int find_index(int *arr, size_t n, int target)
{
    for (size_t i = 0; i < n; i++)
        if (arr[i] == target)
            return i;
    return -1;  /* 見つからない */
}

/* パターン2: ポインタでNULLを返す */
char *my_strdup(const char *s)
{
    char *copy = malloc(strlen(s) + 1);
    if (!copy)
        return NULL;  /* エラー */
    strcpy(copy, s);
    return copy;
}

/* パターン3: 成功/失敗を返し、結果はポインタで */
int get_value(const char *key, int *result)
{
    /* 成功 */
    *result = 42;
    return 0;

    /* 失敗 */
    return -1;
}

5.5 関数ポインタ

関数ポインタの宣言

/* 関数ポインタの宣言 */
int (*func_ptr)(int, int);

/* 読み方: func_ptrは、
   2つのintを取り、intを返す関数へのポインタ */

関数ポインタの使用

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

int main(void)
{
    int (*op)(int, int);

    op = add;  /* &add も可 */
    printf("%d\n", op(5, 3));  /* 8 */

    op = sub;
    printf("%d\n", op(5, 3));  /* 2 */

    return 0;
}

コールバック関数

/* qsortの比較関数 */
int compare_ints(const void *a, const void *b)
{
    return (*(int *)a - *(int *)b);
}

int main(void)
{
    int arr[] = {3, 1, 4, 1, 5, 9};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, n, sizeof(int), compare_ints);

    return 0;
}

typedefによる簡略化

/* typedefで関数ポインタ型を定義 */
typedef int (*operation_t)(int, int);

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int calculate(operation_t op, int a, int b)
{
    return op(a, b);
}

int main(void)
{
    operation_t operations[] = {add, sub, mul};

    for (int i = 0; i < 3; i++)
        printf("%d\n", calculate(operations[i], 10, 5));

    return 0;
}

5.6 可変長引数

基本的な使用

#include <stdarg.h>

int sum(int count, ...)
{
    va_list args;
    int total = 0;

    va_start(args, count);
    for (int i = 0; i < count; i++)
        total += va_arg(args, int);
    va_end(args);

    return total;
}

int main(void)
{
    printf("%d\n", sum(3, 10, 20, 30));  /* 60 */
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));  /* 15 */
    return 0;
}

printfの実装例

void my_printf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    while (*fmt) {
        if (*fmt == '%') {
            fmt++;
            switch (*fmt) {
                case 'd':
                    printf("%d", va_arg(args, int));
                    break;
                case 's':
                    printf("%s", va_arg(args, char *));
                    break;
                case 'c':
                    printf("%c", va_arg(args, int));  /* charはintに昇格 */
                    break;
                case '%':
                    putchar('%');
                    break;
            }
        } else {
            putchar(*fmt);
        }
        fmt++;
    }

    va_end(args);
}

5.7 インライン関数(C99)

inline指定子

inline int square(int x)
{
    return x * x;
}

インライン関数の特徴:

  • 関数呼び出しのオーバーヘッドを削減
  • コンパイラへのヒント(強制ではない)
  • ヘッダファイルに定義を置ける

static inline

/* 最も一般的な使用法 */
static inline int max(int a, int b)
{
    return (a > b) ? a : b;
}

extern inline

/* header.h */
inline int min(int a, int b)
{
    return (a < b) ? a : b;
}

/* source.c */
extern inline int min(int a, int b);  /* 外部定義を提供 */

5.8 _Noreturn(C11)

#include <stdlib.h>

_Noreturn void die(const char *msg)
{
    fprintf(stderr, "Fatal: %s\n", msg);
    exit(1);
    /* この関数は戻らない */
}

/* C23: [[noreturn]] */
[[noreturn]] void fatal_error(void)
{
    abort();
}

5.9 再帰

直接再帰

/* 階乗 */
int factorial(int n)
{
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

/* フィボナッチ(非効率) */
int fibonacci(int n)
{
    if (n <= 1)
        return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

末尾再帰

/* 末尾再帰版の階乗 */
int factorial_tail(int n, int acc)
{
    if (n <= 1)
        return acc;
    return factorial_tail(n - 1, n * acc);  /* 末尾呼び出し */
}

int factorial(int n)
{
    return factorial_tail(n, 1);
}

末尾再帰最適化(TCO):

  • 一部のコンパイラは末尾再帰をループに変換
  • スタックオーバーフローを防止
  • -O2 以上の最適化で有効になることが多い
  • 相互再帰

    /* 前方宣言 */
    int is_odd(int n);
    
    int is_even(int n)
    {
        if (n == 0)
            return 1;
        return is_odd(n - 1);
    }
    
    int is_odd(int n)
    {
        if (n == 0)
            return 0;
        return is_even(n - 1);
    }
    

    5.10 関数の設計原則

    単一責任の原則

    /* 悪い例: 複数の責任 */
    void process_and_print_and_save(int *data, int n)
    {
        /* 処理 */
        /* 出力 */
        /* 保存 */
    }
    
    /* 良い例: 分離された責任 */
    void process(int *data, int n) { /* ... */ }
    void print(const int *data, int n) { /* ... */ }
    void save(const int *data, int n) { /* ... */ }
    

    適切な引数の数

    /* 多すぎる引数 */
    void create_window(int x, int y, int w, int h,
                       int r, int g, int b, int border);
    
    /* 構造体でグループ化 */
    struct WindowConfig {
        int x, y, width, height;
        int r, g, b;
        int border;
    };
    
    void create_window(const struct WindowConfig *config);
    

    const修飾子の活用

    /* 入力パラメータにはconstを付ける */
    int string_length(const char *s)
    {
        int len = 0;
        while (s[len])
            len++;
        return len;
    }
    

    5.11 まとめ

    本章では、C言語の関数について学んだ:

  • 関数の理論: 抽象化、副作用、参照透過性
  • 定義と宣言: プロトタイプの重要性
  • 引数渡し: 値渡し、ポインタ、配列
  • 戻り値: 値、ポインタ、エラー処理
  • 関数ポインタ: コールバック、関数テーブル
  • 可変長引数: stdarg.hの使用
  • インライン関数: 最適化ヒント
  • 再帰: 直接、末尾、相互再帰
  • 設計原則: 単一責任、const修飾
  • 次章では、ポインタとメモリについて学ぶ。

    ---

    参考文献

  • Abelson, H., & Sussman, G. J. (1996). "Structure and Interpretation of Computer Programs", 2nd Edition, MIT Press
  • ISO/IEC 9899:2011, Programming languages — C
  • Kernighan, B. W., & Ritchie, D. M. (1988). "The C Programming Language", 2nd Edition, Prentice Hall
  • Martin, R. C. (2008). "Clean Code: A Handbook of Agile Software Craftsmanship", Prentice Hall