C言語である程度まとまったプログラムを書こうとすると、処理が長くなって読みづらくなりがちです。
そこで重要になるのが関数です。
関数を使うことで、処理を小さな部品に分けて整理し、プログラムを理解しやすく、修正しやすくすることができます。
この記事では、C言語初心者の方に向けて、関数の定義と呼び出しについて、基礎から丁寧に解説していきます。
C言語の関数とは
関数の基本
C言語における関数とは、一定の処理をひとまとめにした再利用可能な部品のことです。
ある入力を受け取り、何らかの処理を行い、必要に応じて結果を返す役割を持ちます。
一般的には、以下のようなイメージでとらえると分かりやすいです。
入力(引数) → 処理(関数の中身) → 出力(戻り値)
たとえば、2つの整数を足し算する処理を、毎回その場で書くのではなく、addという名前の関数にしておけば、どこからでも呼び出して使うことができます。
main関数と自作関数の違い
C言語のプログラムは、必ずmain関数から実行が開始されます。
mainは、特別な名前が付けられた関数であり、OSから最初に呼び出される入り口です。
一方で、自作関数とは、プログラマが自由に名前を付けて定義する関数のことです。
例えばaddやprint_helloなど、役割が分かる名前を付けて作ります。
両者の主な違いは次のように整理できます。
| 種類 | 役割 | 特徴 |
|---|---|---|
| main関数 | プログラムの開始点 | OSから最初に呼ばれる特別な関数 |
| 自作関数 | 特定の処理をまとめた部品 | main関数や他の関数から呼び出して使用 |
文法的には、main関数も自作関数も「関数」という点で同じであり、書き方の基本ルールも共通です。
関数を使うメリット
関数を使う最大のメリットは、コードを分割して分かりやすく整理できることです。
具体的には、次のような利点があります。
まず、処理を意味のある単位に分けることで、プログラム全体の見通しが良くなります。
長いmain関数の中にすべての処理を書くのではなく、入力処理、計算処理、出力処理などを別々の関数にすることで、どこで何をしているのかが把握しやすくなります。
次に、同じ処理を何度も使いたい場合、関数にしておけば再利用が可能になります。
たとえば、平均値を計算する処理、最大値を求める処理などを関数にしておけば、別のプログラムでもコピーして簡単に利用できます。
また、バグが発生したときにも、関数単位で動作を確認できるため、原因の切り分けがしやすくなります。
1つの関数に絞ってテストできるため、デバッグ効率が上がります。
関数の定義の基本
関数定義の書き方
関数を使うには、まず定義を行う必要があります。
関数定義は、次のような書式で書きます。
戻り値の型 関数名(引数リスト) {
関数の中で行う処理;
}
それぞれの要素は次の意味を持ちます。
- 戻り値の型: 関数が返す値の型(int, double, void など)
- 関数名: 好きな名前(ただしルールに従うこと)
- 引数リスト: 関数に渡す値の型と名前の組み合わせ(複数ある場合はカンマで区切る)
- 波かっこで囲まれた部分: 実際の処理(関数本体)
シンプルな例として、2つの整数を足して結果を返す関数の定義を見てみます。
// 2つのint型の値を受け取り、その和をint型で返す関数の定義
int add(int a, int b) {
int result = a + b; // aとbを足した結果をresultに代入
return result; // resultの値を呼び出し元に返す
}
このように、型、名前、引数、returnがそろって、ひとつの関数定義になります。
戻り値の型とvoid関数の使い分け
関数が処理の結果として値を返す場合、その値の型を戻り値の型として指定します。
例えば、整数を返すならint、実数ならdoubleを使います。
一方で、値を返さない関数もあります。
たとえば、画面にメッセージを表示するだけで、計算結果などを返さない場合です。
そのようなときにはvoidという型を使います。
戻り値ありの関数の例と、void関数の例を並べてみます。
#include <stdio.h>
// 戻り値あり: 2つの整数の和を返す
int add(int a, int b) {
return a + b; // 計算した結果を返す
}
// 戻り値なし(void): メッセージを表示するだけ
void print_hello(void) {
printf("Hello, C language!\n"); // 戻り値はなく、表示だけを行う
}
int main(void) {
int sum = add(3, 5); // addはint型の値を返す
printf("3 + 5 = %d\n", sum);
print_hello(); // print_helloは値を返さないので、代入はできない
return 0;
}
3 + 5 = 8
Hello, C language!
計算結果などを他の処理で使いたい場合は戻り値あり、画面表示やファイルへの書き込みなど「行為」だけを行う場合はvoid、というように使い分けると考えると理解しやすいです。
引数の型と個数
関数に値を渡す入口のことを引数と呼びます。
引数は、関数のかっこの中に型 名前という形で並べて指定します。
引数は0個でも1個でも複数でも定義でき、個数によって書き方が少し変わります。
| パターン | 書き方の例 | 説明 |
|---|---|---|
| 引数なし | void func(void) | 何も受け取らない関数 |
| 引数1つ | int square(int x) | 1つの値を受け取る |
| 引数2つ以上 | double avg(int a, int b) | カンマで区切って並べる |
特に、引数なしのときにvoidと書くのはC言語のルールです。
func()のように何も書かない書き方もありますが、初心者のうちは明示的にvoidと書くことをおすすめします。
具体例で見るシンプルな関数定義
ここでは、整数を2乗する関数を例に、定義から利用までの流れを確認します。
#include <stdio.h>
// 整数xを受け取り、その2乗を返す関数
int square(int x) {
int result = x * x; // xを2乗する
return result; // 計算結果を返す
}
int main(void) {
int n = 4;
int n2 = square(n); // square関数を呼び出して結果を受け取る
printf("%d の2乗は %d です。\n", n, n2);
return 0; // プログラム正常終了
}
4 の2乗は 16 です。
この例では、square関数の定義と、main関数からの呼び出しが最低限の形でまとまっています。
引数xは、呼び出し時に渡されたnの値を受け取り、その値を使って計算しています。
ヘッダファイルとプロトタイプ宣言
プログラムが大きくなると、関数定義を別ファイルに分けて管理したくなります。
そのときに重要になるのがヘッダファイルとプロトタイプ宣言です。
プロトタイプ宣言とは、「このような関数が存在します」とコンパイラに教えるための宣言で、次のような形をしています。
戻り値の型 関数名(引数の型リスト);
たとえばint add(int a, int b);のように、最後にセミコロンを付けて記述します。
関数本体は後から定義します。
ヘッダファイル(.hファイル)には、主にこのプロトタイプ宣言をまとめて書きます。
そして#includeによって他のファイルから読み込みます。
簡単な分割例を示します。
math_utils.h(ヘッダファイル)
// math_utils.h
// 足し算を行う関数のプロトタイプ宣言
int add(int a, int b);
math_utils.c(関数本体を定義するファイル)
// math_utils.c
#include "math_utils.h"
// add関数の本体(定義)
int add(int a, int b) {
return a + b;
}
main.c(メインプログラム)
// main.c
#include <stdio.h>
#include "math_utils.h" // add関数のプロトタイプ宣言を読み込む
int main(void) {
int x = 10;
int y = 20;
int sum = add(x, y); // 他ファイルで定義されたaddを呼び出す
printf("%d + %d = %d\n", x, y, sum);
return 0;
}
10 + 20 = 30
このようにプロトタイプ宣言をヘッダファイルにまとめ、必要なファイルから#includeすることで、大きなプログラムでも関数の情報を共有しやすくなります。
関数の呼び出しと引数・戻り値
関数の呼び出し方
関数を「使う」ことを呼び出すといいます。
呼び出しはとてもシンプルで、次のような形を取ります。
関数名(実際に渡す値のリスト);
たとえば、int add(int a, int b);という関数を呼び出す場合はadd(3, 5);のように書きます。
戻り値を使わない場合は、単に呼び出すだけですが、戻り値を利用したい場合は変数に代入したり、式の一部として利用したりします。
int result;
result = add(3, 5); // 戻り値を変数に代入
printf("%d\n", add(2, 4)); // 戻り値をprintfに直接渡す
引数の渡し方
関数の呼び出し時にカッコの中に書く値を実引数、関数定義のときに書くint aやint bのようなものを仮引数と呼びます。
C言語では、実引数の値が仮引数にコピーされる形で引数が渡されます。
これを「値渡し」といいます。
つまり、関数の中で仮引数を変更しても、呼び出し元の変数には影響しません。
#include <stdio.h>
// 引数xに10を足して表示する関数
void add_ten(int x) {
x = x + 10; // 仮引数xを変更している
printf("関数内のx = %d\n", x);
}
int main(void) {
int a = 5;
add_ten(a); // aの値(5)がxにコピーされる
printf("mainのa = %d\n", a); // aは変更されていない
return 0;
}
関数内のx = 15
mainのa = 5
このように、関数の中の変数は、呼び出し元とは別物であることを意識しておくと混乱を防げます。
戻り値の受け取り方
関数の処理結果を受け取るには、戻り値を変数に代入します。
戻り値の型と代入する変数の型は、基本的には一致させる必要があります。
#include <stdio.h>
// 2つの整数の大きい方を返す関数
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
int main(void) {
int x = 10;
int y = 20;
int bigger = max(x, y); // maxの戻り値をbiggerに受け取る
printf("大きい方の値は %d です。\n", bigger);
return 0;
}
大きい方の値は 20 です。
return文は、関数の処理を終了し、指定した値を呼び出し元に返します。
戻り値がある関数では、関数のすべての経路でreturn 値;が実行されるようにすることが重要です。
図解で理解する「引数」と「戻り値」の流れ
文章だけだとイメージしにくいので、簡易的な図で引数と戻り値の流れを表現してみます。
ここでは、int add(int a, int b);という関数を例とします。
- main関数から関数を呼び出す:
sum = add(3, 5);
- 流れのイメージ:
呼び出し元(main) 関数(add)
sum = add(3, 5);
│ │
│ └── 実引数 5 ──┐
└────────実引数 3 ──┼──→ 仮引数 a = 3, b = 5
a + b を計算
↓
return 8; (戻り値)
sum に 8 が代入される
実引数の値が仮引数にコピーされ、処理結果が戻り値として呼び出し元に返る、という一方通行の流れであることを意識すると理解しやすくなります。
main関数から自作関数を呼び出すサンプルコード
ここまでの内容をまとめる意味で、少しだけ複雑なサンプルを見てみます。
ユーザーから2つの整数を入力してもらい、その和と差を計算するプログラムです。
#include <stdio.h>
// 2つの整数の和を返す関数
int add(int a, int b) {
return a + b;
}
// 2つの整数の差(a - b)を返す関数
int sub(int a, int b) {
return a - b;
}
// 結果を表示するだけのvoid関数
void print_results(int a, int b, int sum, int diff) {
printf("入力された値: a = %d, b = %d\n", a, b);
printf("和 (a + b) = %d\n", sum);
printf("差 (a - b) = %d\n", diff);
}
int main(void) {
int x, y;
// ユーザーから2つの整数を入力してもらう
printf("整数を2つ入力してください: ");
scanf("%d %d", &x, &y);
// 関数を使って和と差を計算
int sum = add(x, y); // 戻り値を受け取る
int diff = sub(x, y); // 戻り値を受け取る
// 結果を表示する関数を呼び出す
print_results(x, y, sum, diff);
return 0;
}
整数を2つ入力してください: 10 3
入力された値: a = 10, b = 3
和 (a + b) = 13
差 (a - b) = 7
この例では、計算する役割の関数と、表示する役割の関数を分けることで、処理内容が明確になっています。
関数を使うと、このように「何をしている関数か」が名前で分かるようになるため、コードを読みやすくできます。
関数でよくあるエラーと対処法
関数を使い始めると、いくつか典型的なエラーに遭遇します。
ここでは、初心者の方が特にハマりやすいパターンと、その対処法を解説します。
関数の定義と宣言の不一致
プロトタイプ宣言と関数定義で、戻り値の型や引数の型が一致していないと、コンパイル時に警告やエラーが出ます。
たとえば、次のようなケースです。
#include <stdio.h>
// プロトタイプ宣言(誤り)
double add(int a, int b); // 戻り値の型がdoubleになっている
// 実際の定義
int add(int a, int b) { // 戻り値の型がintになっている
return a + b;
}
int main(void) {
int x = 1, y = 2;
int sum = add(x, y); // 実行はできそうに見えるが、型が不一致
printf("%d\n", sum);
return 0;
}
このような場合、コンパイラは関数の宣言と定義が一致しないと警告します。
プロトタイプ宣言と定義の型を完全に合わせることが重要です。
対策としては、プロトタイプ宣言をコピーペーストして定義を作るなど、ヒューマンエラーを減らす工夫をするのがおすすめです。
関数の順番によるエラー
Cコンパイラは、上から順にソースコードを読み、関数が存在することを知っていなければ呼び出しを解釈できません。
そのため、次のようなコードはエラーになります。
#include <stdio.h>
int main(void) {
int result = add(3, 4); // addがまだ定義されていない位置で呼び出している
printf("%d\n", result);
return 0;
}
// add関数の定義(後ろにある)
int add(int a, int b) {
return a + b;
}
この場合、コンパイラは暗黙の宣言や未定義の関数に関する警告・エラーを出します。
対処法は2つあります。
1つ目は、add関数の定義をmainより前に書く方法です。
もう1つは、プロトタイプ宣言を前に書いておく方法です。
#include <stdio.h>
// プロトタイプ宣言
int add(int a, int b);
int main(void) {
int result = add(3, 4); // ここでaddの存在をコンパイラは知っている
printf("%d\n", result);
return 0;
}
// add関数の本体
int add(int a, int b) {
return a + b;
}
このように、呼び出しより前で宣言だけしておくことで、定義の位置に依存せずに関数を使うことができます。
戻り値の書き忘れとreturnのミス
戻り値ありの関数でreturnを書き忘れると、コンパイル時に警告が出たり、実行時に不定の値が返されたりします。
特に、条件分岐が多い関数では、ある経路だけreturnを書き忘れることがあります。
// 誤った例
int judge(int x) {
if (x > 0) {
return 1;
}
// x <= 0 のときにreturnがない
}
このような場合、コンパイラは制御が関数の終わりに到達しますが、戻り値が指定されていませんといった警告を出すことがあります。
対策として、関数の最後にデフォルトのreturnを書くやり方がよく使われます。
int judge(int x) {
if (x > 0) {
return 1;
} else if (x == 0) {
return 0;
} else {
return -1;
}
// すべての経路でreturnしている状態が望ましい
}
また、void関数でreturn 値;と書いてしまうミスにも注意が必要です。
void関数でreturn;を使う場合は、単にreturn;だけを書きます。
引数の数・型が合わない呼び出しエラー
関数を呼び出すときに、定義と異なる数や型の引数を渡すとエラーや警告になります。
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
int x = 1, y = 2, z = 3;
// 誤った呼び出し例
// int s1 = add(x); // 引数が1つしかない(不足)
// int s2 = add(x, y, z); // 引数が3つある(多すぎる)
// 型が違う例
double d = 1.5;
int s3 = add(x, d); // 警告(必要に応じて型変換が行われることもある)
printf("%d\n", s3);
return 0;
}
特に、プロトタイプ宣言がない状態で関数を呼び出すと、コンパイラが誤った仮定をしてしまい、意図しない動作になることがあります。
必ずプロトタイプ宣言を用意し、引数の数と型を一致させることが重要です。
セミコロンや波かっこ漏れによるコンパイルエラー
初心者がもっともよく遭遇するのが、セミコロン;や波かっこ{}の付け忘れです。
関数定義の直後にセミコロンを書いてしまうミスもあります。
// 誤りの例
int add(int a, int b); // ここにセミコロンを書いてしまうと「宣言」になってしまう
{ // そのため、次の波かっこでエラーになる
return a + b;
}
正しくは、次のようになります。
// 正しい関数定義(セミコロンは不要)
int add(int a, int b) {
return a + b;
}
また、if文やfor文の波かっこを閉じ忘れると、関数の終わり}が見つからないなどのエラーが出ます。
エラー行が実際のミスの場所とずれて表示されることもあるため、対応するかっこを確認する習慣をつけておくと良いです。
最近のエディタやIDEでは、かっこの自動補完や対応表示機能があるので、それらを活用することもエラー防止の有効な手段です。
まとめ
この記事では、C言語の関数の定義と呼び出しについて、基本的な考え方から、戻り値や引数の扱い、ヘッダファイルとプロトタイプ宣言、そして初心者がつまずきやすいエラー例まで解説しました。
関数は、プログラムを小さな部品に分割し、見通しを良くするための最重要テクニックです。
まずは、簡単な計算や表示処理を自作関数として切り出すところから練習し、少しずつ複雑な処理を任せられる関数を書けるようになっていくと良いでしょう。
