第7章:関数で整理する
この章で学ぶこと
- 関数とは何か
- 関数の定義と呼び出し
- 引数と戻り値
- 関数のプロトタイプ
- スコープ(変数の有効範囲)
- 複数ファイルでの分割
- カップを用意する
- コーヒー豆を挽く
- お湯を沸かす
- フィルターをセットする
- 粉を入れる
- お湯を注ぐ
- 完成
- 再利用性: 同じ処理を何度も書かなくて済む
- 可読性: コードが読みやすくなる
- 保守性: バグ修正が1箇所で済む
- テスト性: 部品ごとにテストできる
---
7.1 関数とは
処理をまとめる
関数(Function)は、一連の処理をまとめて名前を付けたものです。
日常生活で例えると、「コーヒーを淹れる」という作業は、実際には多くのステップからなっています:
しかし、誰かに頼むときは「コーヒーを淹れて」の一言で済みます。詳細は相手が知っているからです。
関数も同じです。複雑な処理を「関数名」で呼び出せるようにまとめます。
関数を使うメリット
すでに使っている関数
実は、もう関数を使っています。
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変数
- 再帰
- ファイル分割
関数設計の原則
- 1つの関数は1つの仕事: 関数は単一の目的を持つべき
- 適切な名前: 関数名から動作が分かるように
- 短く保つ: 長すぎる関数は分割を検討
- 副作用を最小限に: グローバル変数を変更しない
次の章の予告
次章では、配列と文字列を学びます。複数のデータをまとめて扱う方法を覚えましょう。
---
Column: 関数の歴史
サブルーチンの誕生
関数の概念は、1940年代のサブルーチン(副プログラム)に遡ります。
初期のコンピュータでは、同じ処理を何度も書くのは大変でした。そこで、処理をまとめて「サブルーチン」として呼び出す仕組みが生まれました。
ライブラリの発展
共通のサブルーチンをまとめて「ライブラリ」として提供するようになりました。printf や scanf もライブラリ関数です。
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);
}
---
次の章では、配列と文字列を学びます。複数のデータを効率よく扱いましょう!