第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言語の関数について学んだ:
次章では、ポインタとメモリについて学ぶ。
---