[C言語のはじめ方] Part8: 関数定義と呼び出し(引数・戻り値)

プログラミングの世界へようこそ!C言語学習の旅、順調に進んでいますか? Step 1 では、C言語の基本的な文法や環境構築について学びましたね。いよいよ Step 2 では、プログラムをより整理し、効率的に書くための重要な概念、「関数」について学んでいきましょう!

今回は、関数の「定義」と「呼び出し」、そして関数と情報をやり取りするための「引数(ひきすう)」と「戻り値(もどりち)」に焦点を当てて、詳しく解説していきます。関数を使いこなせるようになると、複雑なプログラムも部品ごとに分けて考えられるようになり、コードが見やすく、そして再利用しやすくなりますよ!

この記事で学ぶこと:

  • 関数の基本的な作り方(定義)
  • 作った関数を実際に使う方法(呼び出し)
  • 関数に情報を渡す「引数」とは?
  • 関数から結果を受け取る「戻り値」とは?
  • 関数を使う前に知っておくべき「プロトタイプ宣言」

1. 関数ってなんだろう? なぜ必要なの?

関数とは、特定の処理をひとまとめにしたものです。イメージとしては、料理のレシピにおける「野菜を切る」「炒める」といった個々の手順や、数学における `f(x) = 2x + 1` のような式に近いものです。

これまで皆さんが書いてきた `main` 関数も、実はC言語における特別な関数の一つです。プログラムを実行すると、まずこの `main` 関数が呼び出されて処理が始まります。

では、なぜ `main` 関数以外にも関数を作る必要があるのでしょうか? 主な理由は以下の通りです。

  • コードの整理整頓: 長いプログラムを機能ごとに小さな関数に分割することで、全体の構造が分かりやすくなります。どこで何をしているのかが一目瞭然になります。
  • コードの再利用: 同じ処理をプログラムの複数の場所で行いたい場合、その処理を関数として定義しておけば、何度も同じコードを書く必要がなくなり、関数を呼び出すだけで済みます。これにより、コードが短くなり、修正も楽になります。
  • デバッグの効率化: プログラムにエラー(バグ)が発生した場合、関数ごとに処理がまとまっていると、問題のある箇所を特定しやすくなります。
  • チーム開発の円滑化: 複数人で開発を行う際に、機能ごとに関数を分担して作成することができます。

このように、関数はプログラムをより良く、効率的にするための強力なツールなのです。

2. 関数の作り方(定義)

それでは、実際にC言語で関数をどうやって作るのか(定義するのか)を見ていきましょう。関数の定義は、基本的に以下の形式に従います。

戻り値の型 関数名(引数リスト) {
    // 関数の処理内容
    // ...
    return 戻り値; // 戻り値がある場合
}

それぞれの要素を詳しく見ていきましょう。

  • 戻り値の型 (Return Type):

    関数が処理を終えた後、呼び出し元に返す値のデータ型を指定します。例えば、計算結果の整数を返したい場合は `int`、文字を返したい場合は `char` と書きます。何も値を返さない場合は `void` と指定します。(`void` については後ほど詳しく説明します。)

  • 関数名 (Function Name):

    関数を識別するための名前です。変数名と同じように、英数字とアンダースコア `_` を使って自由に(ただし、予約語は除く)付けることができます。関数が何をするのか分かりやすい名前を付けるのがポイントです。(例: `calculateSum`, `printMessage`)

  • 引数リスト (Parameter List):

    `()` の中に、関数が処理を行うために外部(呼び出し元)から受け取る情報を記述します。各引数は `データ型 引数名` の形式で書き、複数ある場合はカンマ `,` で区切ります。引数が必要ない場合は `()` の中を空にするか、`void` と書きます。(例: `(int a, int b)`, `(void)`)

    ここで定義される引数を仮引数 (parameter) と呼びます。

  • 関数本体 (Function Body):

    `{}` で囲まれた部分で、実際に行う処理を記述します。変数宣言や計算、制御構文など、これまでに学んだC言語の文法を自由に使えます。

  • `return` 文:

    関数が値を返す場合、`return` 文を使ってその値を指定します。`return` 文が実行されると、関数の処理はそこで終了し、指定された値が呼び出し元に返されます。戻り値の型が `void` の場合は、`return` 文を省略するか、値を指定せずに `return;` と書くことで関数を途中で終了させることもできます。

具体的な例: 2つの整数の合計を計算する関数

言葉だけだと分かりにくいので、簡単な例を見てみましょう。2つの整数を受け取り、その合計を計算して返す関数 `add` を作ってみます。

#include <stdio.h>

// add 関数の定義
int add(int x, int y) {
    int sum; // 合計を格納する変数
    sum = x + y; // 受け取った2つの引数を足し算
    return sum; // 計算結果 (sum) を返す
}

int main(void) {
    int a = 5;
    int b = 10;
    int result;

    // add 関数を呼び出し、戻り値を受け取る (詳細は後述)
    result = add(a, b);

    printf("%d + %d = %d\n", a, b, result); // 結果を表示

    return 0;
}

この例では、

  • 戻り値の型は `int` (整数の合計を返すため)
  • 関数名は `add`
  • 引数リストは `(int x, int y)` (2つの整数を受け取るため)
  • 関数本体では、受け取った `x` と `y` を足して変数 `sum` に代入し、その `sum` を `return` 文で返しています。

`main` 関数の中での `result = add(a, b);` という部分が、この `add` 関数を実際に使っている(呼び出している)箇所になります。これについては次のセクションで詳しく説明します。

戻り値がない関数 (`void`)

関数は必ずしも値を返す必要はありません。例えば、単にメッセージを表示するだけの関数の場合、戻り値は不要です。そのような場合は、戻り値の型として `void` を指定します。

#include <stdio.h>

// メッセージを表示する関数 (戻り値なし)
void printGreeting(void) { // 引数も受け取らない場合は (void) と書くのが一般的
    printf("こんにちは!関数へようこそ!\n");
    // return; // void 型の関数の最後では return は省略可能
}

int main(void) {
    // printGreeting 関数を呼び出す
    printGreeting();

    return 0;
}

この `printGreeting` 関数は、呼び出されるとメッセージを表示するだけで、何も値を返しません。そのため、戻り値の型は `void` となっています。

3. 関数の使い方(呼び出し)

関数を定義しただけでは、その処理は実行されません。関数を使うためには、`main` 関数などの他の関数からその関数を呼び出す (call) 必要があります。

関数の呼び出しは、基本的に以下の形式で行います。

// 戻り値を受け取る場合
  変数 = 関数名(実引数リスト);

  // 戻り値を受け取らない (void型) または受け取る必要がない場合
  関数名(実引数リスト);
  • 関数名: 呼び出したい関数の名前を指定します。

  • 実引数リスト (Argument List):

    `()` の中に、関数に渡したい具体的な値や変数を記述します。関数定義の引数リスト(仮引数)の順番と型に合わせて指定する必要があります。複数ある場合はカンマ `,` で区切ります。引数がない場合は `()` の中を空にします。

    ここで渡す値や変数を実引数 (argument) と呼びます。

  • 変数 = (代入):

    呼び出す関数が戻り値を持つ場合(`void` 型でない場合)、その戻り値を変数に代入して受け取ることができます。戻り値を受け取る必要がない場合は、代入を行わなくても構いません。

具体的な例: `add` 関数の呼び出し

先ほどの `add` 関数の例をもう一度見てみましょう。

int main(void) {
    int a = 5;
    int b = 10;
    int result; // 戻り値を受け取るための変数

    // add 関数を呼び出し、実引数として a と b を渡す
    // add 関数の戻り値 (計算結果) が result に代入される
    result = add(a, b);

    printf("%d + %d = %d\n", a, b, result); // 5 + 10 = 15 と表示される

    // 別の値を渡して再度呼び出すことも可能
    int result2 = add(100, 200);
    printf("100 + 200 = %d\n", result2); // 100 + 200 = 300 と表示される

    return 0;
}

// add 関数の定義 (main 関数の後でも可、ただしプロトタイプ宣言が必要になる場合がある)
int add(int x, int y) {
    int sum = x + y;
    return sum;
}

`result = add(a, b);` の行で、`add` 関数が呼び出されています。

  1. `main` 関数内の変数 `a` (値は 5) と `b` (値は 10) が、`add` 関数の仮引数 `x` と `y` にそれぞれコピーされます。(`x` に 5、`y` に 10 が入る)
  2. `add` 関数内で `sum = x + y;` (つまり `sum = 5 + 10;`) が実行され、`sum` は 15 になります。
  3. `return sum;` によって、値 15 が `add` 関数の呼び出し元(`main` 関数)に返されます。
  4. 返された値 15 が、`main` 関数内の変数 `result` に代入されます。

このように、関数呼び出しによって処理が `main` 関数から `add` 関数に移り、`add` 関数の処理が終わると `main` 関数に戻ってきます。

注意: 値渡し (Pass by Value)

C言語の基本的な関数呼び出しでは、実引数のが仮引数にコピーされます。これを「値渡し」と呼びます。これは、関数内で仮引数の値を変更しても、呼び出し元の実引数の変数(上の例でいう `a` や `b`)の値は変更されないことを意味します。

#include <stdio.h>

void tryToChange(int val) {
    printf("関数内 (変更前): val = %d\n", val);
    val = 100; // 仮引数 val の値を変更
    printf("関数内 (変更後): val = %d\n", val);
}

int main(void) {
    int num = 10;
    printf("main 関数 (呼び出し前): num = %d\n", num);
    tryToChange(num); // num の値 (10) が val にコピーされる
    printf("main 関数 (呼び出し後): num = %d\n", num); // num の値は変わらない!
    return 0;
}
// --- 実行結果 ---
// main 関数 (呼び出し前): num = 10
// 関数内 (変更前): val = 10
// 関数内 (変更後): val = 100
// main 関数 (呼び出し後): num = 10

関数内で `val` を変更しても、`main` 関数の `num` には影響がないことがわかりますね。呼び出し元の変数を関数内で変更したい場合は、「ポインタ」という別の仕組み(Step 3 で学習します)を使う必要があります。

4. 引数(パラメータ)- 関数への情報伝達

引数は、関数が処理を行うために必要な情報を、呼び出し元から受け取るための窓口です。

  • 仮引数 (Parameter): 関数を定義するときに `()` の中に書く、いわば「受け皿」となる変数です。`int add(int x, int y)` の `x` と `y` が仮引数です。
  • 実引数 (Argument): 関数を呼び出すときに `()` の中に書く、実際に渡す値や変数のことです。`result = add(a, b);` の `a` と `b` が実引数です。

関数を呼び出す際には、実引数の数と型が、関数定義の仮引数の数と型に(暗黙的な型変換が可能な範囲で)一致している必要があります。

#include <stdio.h>

// 3つの double 型の数値の平均を計算する関数
double calculateAverage(double n1, double n2, double n3) {
    double sum = n1 + n2 + n3;
    return sum / 3.0; // double 型で返すために 3.0 で割る
}

int main(void) {
    double score1 = 75.5;
    double score2 = 88.0;
    double score3 = 92.1;
    double avg;

    // 3つの double 型の変数を実引数として渡す
    avg = calculateAverage(score1, score2, score3);

    printf("平均点: %.2f\n", avg); // .2f で小数点以下2桁まで表示

    return 0;
}

この例では、`calculateAverage` 関数は 3つの `double` 型の仮引数 `n1`, `n2`, `n3` を持ちます。`main` 関数から呼び出す際には、`double` 型の変数 `score1`, `score2`, `score3` を実引数として渡しています。

引数を使うことで、関数はより汎用的に(いろいろな値に対応できるように)なります。もし `add` 関数に引数がなければ、決まった値(例えば 1 と 2)しか足し算できない関数になってしまいますよね。

5. 戻り値(リターン値)- 関数からの結果報告

戻り値は、関数が処理を実行した結果を、呼び出し元に返すための仕組みです。`return` 文を使って返したい値を指定します。

関数定義時に指定した「戻り値の型」と、`return` 文で実際に返す値の型は、一致している(または互換性がある)必要があります。

#include <stdio.h>

// 2つの整数のうち大きい方を返す関数
int getMax(int num1, int num2) {
    if (num1 > num2) {
        return num1; // num1 が大きければ num1 を返す
    } else {
        return num2; // そうでなければ num2 を返す (num1 == num2 の場合も含む)
    }
    // この関数は必ずどちらかの return 文を通るので、
    // 関数の最後に到達することはない。
}

int main(void) {
    int x = 15;
    int y = 28;
    int max_value;

    max_value = getMax(x, y); // getMax の戻り値 (大きい方の値) を受け取る

    printf("%d と %d のうち大きい方は %d です。\n", x, y, max_value);

    return 0;
}

この `getMax` 関数は、`int` 型の値を返すように定義されています。関数内では `if` 文を使って `num1` と `num2` を比較し、大きい方の値を `return` 文で返しています。`main` 関数では、その戻り値を `max_value` 変数で受け取っています。

戻り値の型が `void` の関数は、値を返しません。呼び出し元で戻り値を受け取ろうとするとコンパイルエラーになります。

void printMessage(void) {
    printf("メッセージ!\n");
}

int main(void) {
    printMessage(); // OK
    // int result = printMessage(); // これはコンパイルエラー! void 型は値を持たない
    return 0;
}

6. プロトタイプ宣言 – 「こういう関数がありますよ」宣言

C言語では、基本的に、関数を呼び出す前にその関数が定義されているか、または宣言されている必要があります。コンパイラは上から下にコードを読んでいくため、呼び出し時点で使用する関数の情報を知らないと、正しくコンパイルできないことがあるからです。

ここまでの例では、`main` 関数の前に他の関数を定義していたので問題ありませんでした。しかし、`main` 関数の後に関数を定義したい場合や、複数のソースファイルに関数を分割する場合など、呼び出し時点より後に関数定義があるケースがあります。

そのような場合に必要になるのが関数プロトタイプ宣言 (Function Prototype Declaration) です。

プロトタイプ宣言は、コンパイラに対して「これからこういう名前で、こういう引数を受け取り、こういう型の値を返す関数を使いますよ」と事前に教えてあげる役割を果たします。関数の実際の処理内容は含みません。

プロトタイプ宣言の書き方は、関数定義の最初の行(ヘッダー)の最後にセミコロン `;` を付けた形です。

戻り値の型 関数名(引数リスト);

引数リストの中の仮引数名は省略しても構いませんが、型は必ず指定する必要があります。(ただし、分かりやすさのために仮引数名も書いておくことが推奨されます。)

先ほどの `add` 関数の例を、プロトタイプ宣言を使って書き換えてみましょう。

#include <stdio.h>

// プロトタイプ宣言 (main 関数より前)
// これでコンパイラは main 関数内で add 関数が使われても情報を知っている
int add(int x, int y); // 仮引数名 (x, y) は書いても書かなくても良いが、書いた方が分かりやすい
// int add(int, int); // これでも OK

int main(void) {
    int a = 5;
    int b = 10;
    int result;

    // main 関数より後に関数定義があっても、プロトタイプ宣言があるので OK
    result = add(a, b);

    printf("%d + %d = %d\n", a, b, result);

    return 0;
}

// add 関数の定義 (main 関数より後)
int add(int x, int y) {
    int sum = x + y;
    return sum;
}

このように、`main` 関数の前(通常は `#include` の後あたり)にプロトタイプ宣言を書いておくことで、コンパイラは `main` 関数内で `add` 関数が呼び出されたときに、その関数の引数や戻り値の型を正しくチェックできます。

ヘッダーファイル (`.h`) との関係

皆さんが使っている `stdio.h` などのヘッダーファイルには、実は `printf` や `scanf` といった標準ライブラリ関数のプロトタイプ宣言がたくさん書かれています。だから、私たちはこれらの関数を自分で定義しなくても、`#include ` と書くだけで呼び出すことができるのです。

将来的に自分で作った関数を別のファイルで使いたい場合も、同様にヘッダーファイルに関数プロトタイプ宣言を記述することになります。(これは Step 8 で詳しく学びます。)

7. まとめ

今回は、C言語における関数の基本的な概念である「定義」「呼び出し」「引数」「戻り値」、そして「プロトタイプ宣言」について学びました。

  • 関数は、特定の処理をひとまとめにしたもの。コードの整理、再利用、保守性を高める。
  • 関数の定義: `戻り値の型 関数名(引数リスト) { 処理 }`
  • 関数の呼び出し: `変数 = 関数名(実引数リスト);` または `関数名(実引数リスト);`
  • 引数: 関数に情報を渡す仕組み (仮引数と実引数)。基本は値渡し。
  • 戻り値: 関数から結果を受け取る仕組み (`return` 文)。`void` は値を返さない。
  • プロトタイプ宣言: 関数呼び出しより前に関数の情報をコンパイラに教える宣言。

関数は、C言語プログラミングにおいて非常に重要で基本的な要素です。最初は少し難しく感じるかもしれませんが、実際に自分で簡単な関数を作って、呼び出してみる練習を繰り返すことで、必ず身についていきます。

今回の内容をしっかり理解できれば、より複雑なプログラムにも挑戦できるようになります。次は、変数を使える範囲を決める「スコープ」や、関数が自分自身を呼び出す「再帰関数」について学んでいきましょう!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です