6日でできる 新C言語入門|関数の演習プログラム②

関数は、作って使うほど理解が深まる。
合計計算、絶対値、乱数クイズ、ファイル分割を通して、C言語の関数設計を実践的に身につけよう。

C言語の関数は、プログラムを読みやすく整理するための大切な仕組みです。

これまでに、関数の作り方、引数、戻り値、void関数、プロトタイプ宣言などを学んできたと思います。ここからは、実際にいくつかの演習プログラムを作りながら、関数をどのように使い分けるのかを確認していきます。

関数を使うと、計算処理、判定処理、表示処理などを小さな部品として分けられます。たとえば、3つの値を合計する処理を関数にしたり、負の値を正の値に直す処理を関数にしたり、計算クイズの答え合わせだけを関数にしたりできます。

また、関数の数が増えてきたら、1つのmain.cにすべてを書くのではなく、ヘッダファイルとソースファイルに分けて管理する方法も役立ちます。関数をファイル分割できるようになると、プログラムが大きくなっても見通しを保ちやすくなります。

この記事では、3つの整数値の合計を返す関数、自作の絶対値関数、乱数を使った計算クイズ、関数を別ファイルに分ける方法を、サンプルプログラムと一緒にやさしく解説していきます。

関数演習で確認したいポイント

関数の演習では、ただ動くプログラムを書くのではなく、関数がどの役割を担当しているかを意識することが大切です。

確認すること内容
引数関数へ渡す値は何か
戻り値関数から返ってくる結果は何か
関数名何をする関数なのか分かりやすい名前か
main関数の役割全体の流れを読みやすく書けているか
関数の分割1つの関数に役割を詰め込みすぎていないか

小さな関数を作る練習を重ねると、プログラム全体を部品の組み合わせとして考えられるようになります。

3つの整数値の合計を求める関数

まずは、3つの整数値を受け取り、その合計を返す関数を作ります。

今回作る関数はsum3です。

項目内容
関数名sum3
引数int型を3つ
戻り値int型
役割3つの整数値を合計して返す

3つの整数を合計する処理は、main関数の中に直接書くこともできます。

しかし、関数に分けておくと、同じような合計処理を別の場所でも使いやすくなります。また、main関数の中では、入力、関数呼び出し、結果表示という流れが分かりやすくなります。

図:引数で受け取った3つの値を合計して返す

この図から分かること

この図から分かるのは、関数が値を受け取り、処理を行い、結果を返す流れです。

sum3関数は、3つの整数を引数として受け取ります。そして、関数の中で合計を計算し、その結果を戻り値としてmain関数へ返します。計算処理を関数にまとめることで、main関数の流れがすっきりします。

3つの点数を合計するプログラム

次のプログラムでは、3科目の点数を入力し、その合計点をsum3関数で求めます。

プロジェクト/ファイル名: Lesson48_1/main.c

#include <stdio.h>

// 3つの整数の合計を返す関数
int sum3(int x, int y, int z) {
    return x + y + z;
}

int main(void) {
    int japanese;
    int math;
    int english;
    int total;

    printf("国語の点数を入力してください: ");
    scanf("%d", &japanese);

    printf("数学の点数を入力してください: ");
    scanf("%d", &math);

    printf("英語の点数を入力してください: ");
    scanf("%d", &english);

    // 3科目の合計点を関数で求める
    total = sum3(japanese, math, english);

    printf("3科目の合計点は %d 点です。\n", total);

    return 0;
}

実行結果

国語の点数を入力してください: 72
数学の点数を入力してください: 85
英語の点数を入力してください: 90
3科目の合計点は 247 点です。

このプログラムでは、国語、数学、英語の点数をそれぞれ入力しています。

入力された3つの値は、sum3関数へ渡されます。

total = sum3(japanese, math, english);

sum3関数では、受け取った3つの値を足し合わせて返します。

int sum3(int x, int y, int z) {
    return x + y + z;
}

このように、計算だけを関数に分けると、main関数では全体の流れが読みやすくなります。

Visual Studioでscanfを使う場合、環境設定によって警告が表示されることがあります。学習用として動作確認する場合は、必要に応じてSDLチェックの設定も確認しておきましょう。

sum3関数のポイント

sum3関数は、引数あり、戻り値ありの関数です。

部分内容
int関数がint型の値を返す
sum3関数名
int x, int y, int z3つの整数を受け取る
return x + y + z;合計値を返す

関数を作るときは、何を受け取り、何を返すのかを先に考えると作りやすくなります。

今回の場合、3つの点数を受け取り、合計点を返したいので、引数は3つ、戻り値は1つになります。

自作の絶対値関数を作る

次に、整数の絶対値を返す関数を作ります。

絶対値とは、数直線上で0からどれだけ離れているかを表す値です。たとえば、15の絶対値は15、-15の絶対値も15です。

今回作る関数はmyAbsです。

項目内容
関数名myAbs
引数int型を1つ
戻り値int型
役割整数の絶対値を返す

考え方はシンプルです。

値が0未満なら、符号を反転して返します。
値が0以上なら、そのまま返します。

自作の絶対値関数を使うプログラム

次のプログラムでは、現在地と目的地の座標を入力し、その差の絶対値をmyAbs関数で求めます。

プロジェクト/ファイル名: Lesson48_2/main.c

#include <stdio.h>

// 整数の絶対値を返す関数
int myAbs(int val) {
    if (val < 0) {
        return -val;
    }

    return val;
}

int main(void) {
    int current;
    int goal;
    int diff;
    int distance;

    printf("現在地の座標を入力してください: ");
    scanf("%d", ¤t);

    printf("目的地の座標を入力してください: ");
    scanf("%d", &goal);

    // 目的地との差を求める
    diff = goal - current;

    // 差の絶対値を求める
    distance = myAbs(diff);

    printf("現在地から目的地までの距離は %d です。\n", distance);

    return 0;
}

実行結果

現在地の座標を入力してください: 18
目的地の座標を入力してください: 5
現在地から目的地までの距離は 13 です。

このプログラムでは、現在地と目的地の差を求めています。

diff = goal - current;

この差は、入力値によって正の値にも負の値にもなります。

距離として扱いたい場合は、負の値のままでは不自然です。そこで、myAbs関数を使って絶対値にしています。

distance = myAbs(diff);

myAbs関数では、valが0未満なら-valを返し、そうでなければvalをそのまま返します。

int myAbs(int val) {
    if (val < 0) {
        return -val;
    }

    return val;
}

elseを書かなくても、ifの中でreturnした場合はそこで関数が終了します。そのため、ifの下にreturn val;を書くことで、0以上の場合の処理を表せます。

return文で関数を途中終了できる

myAbs関数では、条件によってreturnする値を変えています。

if (val < 0) {
    return -val;
}

return val;

valが負の値なら、return -val;が実行され、関数はその場で終了します。

valが0以上なら、if文の中は実行されず、最後のreturn val;が実行されます。

このように、return文は戻り値を返すだけでなく、関数をその時点で終了させる役割も持っています。

乱数を使った計算クイズゲーム

次に、乱数を使って計算問題を作るプログラムを作ります。

C言語では、rand関数を使うと乱数を作れます。srand関数とtime関数を組み合わせると、実行するたびに違う値が出やすくなります。

今回のプログラムでは、1から9までの乱数を2つ作り、掛け算クイズを出題します。答え合わせはjudgeProduct関数で行います。

項目内容
関数名judgeProduct
引数int型を3つ
戻り値int型
役割a掛けるbがanswerと一致すれば1、違えば0を返す

判定処理を関数に分けることで、main関数ではクイズの流れに集中できます。

図:計算クイズでは判定処理を関数に分ける

この図から分かること

この図から分かるのは、計算クイズの処理が複数の役割に分かれていることです。

乱数で問題を作り、ユーザーに答えを入力してもらい、判定関数で正解かどうかを確認します。判定処理を関数に分けることで、main関数の中の流れが分かりやすくなります。

乱数を使った掛け算クイズのプログラム

次のプログラムでは、1から9までの乱数を2つ作り、掛け算クイズを出題します。正解するまで入力を繰り返します。

プロジェクト/ファイル名: Lesson48_3/main.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 掛け算の答えが正しいかを判定する関数
int judgeProduct(int a, int b, int answer) {
    if (a * b == answer) {
        return 1;
    }

    return 0;
}

int main(void) {
    int num1;
    int num2;
    int answer;
    int result;

    // 現在時刻を使って乱数のシード値を設定する
    srand((unsigned)time(NULL));

    // 1から9までの乱数を作る
    num1 = rand() % 9 + 1;
    num2 = rand() % 9 + 1;

    printf("計算問題: %d × %d\n", num1, num2);

    do {
        printf("答えを入力してください: ");
        scanf("%d", &answer);

        // 入力された答えが正しいか判定する
        result = judgeProduct(num1, num2, answer);

        if (result) {
            printf("正解です。\n");
        } else {
            printf("違います。もう一度入力してください。\n");
        }
    } while (!result);

    return 0;
}

実行結果

計算問題: 7 × 6
答えを入力してください: 40
違います。もう一度入力してください。
答えを入力してください: 42
正解です。

このプログラムでは、最初にsrand関数で乱数のシード値を設定しています。

srand((unsigned)time(NULL));

その後、rand関数で1から9までの乱数を作っています。

num1 = rand() % 9 + 1;
num2 = rand() % 9 + 1;

判定処理はjudgeProduct関数で行います。

result = judgeProduct(num1, num2, answer);

judgeProduct関数は、num1とnum2の積がanswerと等しければ1を返し、違っていれば0を返します。

int judgeProduct(int a, int b, int answer) {
    if (a * b == answer) {
        return 1;
    }

    return 0;
}

do while文を使っているため、少なくとも1回は答えを入力します。不正解の場合はresultが0になるため、もう一度入力を求めます。

関数の戻り値を条件分岐に使う

judgeProduct関数は、正解なら1、不正解なら0を返します。

この戻り値は、if文やdo while文の条件として使えます。

if (result) {
    printf("正解です。\n");
} else {
    printf("違います。もう一度入力してください。\n");
}

C言語では、0は偽、0以外は真として扱われます。

そのため、resultが1ならif文の中が実行されます。resultが0ならelse側が実行されます。

また、do while文では次のように使っています。

} while (!result);

!resultは、resultが0のときに真になります。つまり、不正解の間は繰り返し、正解したらループを終了するという意味になります。

関数のファイル分割を実践する

関数が増えてくると、main.cにすべての関数を書くと長くなってしまいます。

そこで、関数の宣言をヘッダファイルに書き、関数の本体を別のソースファイルに分ける方法を使います。

今回の例では、三角形を表示する関数をtriangle.hとtriangle.cに分けます。

ファイル名役割
triangle.h関数のプロトタイプ宣言を書く
triangle.c関数の本体を書く
main.c入力を受け取り、関数を呼び出す

main.cは、三角形を表示する細かい処理を直接持ちません。triangle.hを読み込み、triangle.cにある関数を利用します。

図:関数の宣言と本体を別ファイルに分ける

この図から分かること

この図から分かるのは、関数をファイル分割すると、main.cの役割がすっきりするということです。

triangle.hには関数の使い方を宣言し、triangle.cには実際の処理を書きます。main.cはtriangle.hを読み込むことで、関数の本体を直接書かなくても、三角形を表示する関数を呼び出せます。

triangle.hを作成する

triangle.hには、三角形表示に使う関数のプロトタイプ宣言を書きます。

プロジェクト/ファイル名: Lesson48_4/triangle.h

#ifndef TRIANGLE_H
#define TRIANGLE_H

// 指定された個数の記号を表示する関数
void printMarks(int n);

// 指定された段数の三角形を表示する関数
void printTriangle(int n);

#endif

このヘッダファイルには、関数の本体は書きません。

printMarks関数とprintTriangle関数を、他のファイルから使えるように宣言しています。

また、インクルードガードも書いています。

#ifndef TRIANGLE_H
#define TRIANGLE_H

インクルードガードを入れておくと、同じヘッダファイルが複数回読み込まれても、内容が重複して展開されるのを防げます。

triangle.cを作成する

triangle.cには、三角形表示に使う関数の本体を書きます。

プロジェクト/ファイル名: Lesson48_4/triangle.c

#include <stdio.h>
#include "triangle.h"

// n個の■を横並びで表示する関数
void printMarks(int n) {
    int i;

    for (i = 0; i < n; i++) {
        printf("■");
    }

    printf("\n");
}

// n段の右下がり三角形を表示する関数
void printTriangle(int n) {
    int i;

    for (i = 1; i <= n; i++) {
        printMarks(i);
    }
}

printMarks関数は、指定された個数だけ■を表示します。

void printMarks(int n)

printTriangle関数は、1段目からn段目まで順番にprintMarks関数を呼び出します。

void printTriangle(int n)

このように、1つの関数から別の関数を呼び出すことで、処理をさらに細かく分けられます。

printTriangle関数は三角形全体の流れを担当し、printMarks関数は1行分の表示を担当しています。

main.cを作成する

main.cでは、triangle.hを読み込み、ユーザーが入力した段数をもとにprintTriangle関数を呼び出します。

プロジェクト/ファイル名: Lesson48_4/main.c

#include <stdio.h>
#include "triangle.h"

int main(void) {
    int height;

    printf("三角形の段数を入力してください: ");
    scanf("%d", &height);

    // 1以上の場合だけ三角形を表示する
    if (height > 0) {
        printTriangle(height);
    } else {
        printf("1以上の数を入力してください。\n");
    }

    return 0;
}

実行方法

1.「SDLチェック」を「いいえ」にします。

2.「ビルド」 メニューから「ソリューションのビルド」を選択します。

3.ビルドが成功すると、「main.c」「triangle.c」の両方がコンパイルされ、正しくリンクされます。

4.「デバッグ」 メニューから「デバッグなしで開始」を選択します。

5.結果ウィンドウに、次のような出力が表示されます。

実行結果

三角形の段数を入力してください: 5
■
■■
■■■
■■■■
■■■■■

main.cでは、三角形をどのように表示するかという細かい処理は書いていません。

三角形の表示処理はtriangle.cにあります。

main.cは、triangle.hを読み込むことで、printTriangle関数を呼び出せるようになります。

#include "triangle.h"

このようにファイルを分けると、main.cにはプログラム全体の流れを残し、表示に関する処理は別ファイルに整理できます。

Lesson48_4をビルドして実行する流れ

Lesson48_4では、main.c、triangle.c、triangle.hの3つのファイルを使います。

Visual Studioで実行する場合は、これらのファイルを同じプロジェクトに追加してからビルドします。

手順内容
1triangle.h、triangle.c、main.cをプロジェクトに追加する
2ソリューションのビルドを実行する
3main.cとtriangle.cがコンパイルされる
4リンクによって1つの実行ファイルになる
5デバッグなしで開始して実行する

ビルド時には、main.cとtriangle.cがコンパイルされます。triangle.hは直接コンパイルされるファイルではありませんが、main.cやtriangle.cから読み込まれて使われます。

関数の本体はtriangle.cにあるため、triangle.cがプロジェクトに追加されていないとリンクエラーになることがあります。ファイル分割では、ヘッダファイルだけでなく、関数本体が書かれたソースファイルもプロジェクトに追加することが大切です。

関数を分けるとプログラムの見通しがよくなる

今回の演習では、さまざまな関数を作りました。

演習関数学べる内容
3つの整数の合計sum3複数の引数と戻り値
自作の絶対値myAbs条件分岐とreturn文
計算クイズjudgeProduct戻り値を判定に使う方法
三角形表示printMarks、printTriangle関数同士の呼び出しとファイル分割

関数を使うと、処理を小さな単位に分けられます。

計算する関数、判定する関数、表示する関数のように役割を分けておくと、main関数が読みやすくなります。

また、関数の数が増えてきたら、ヘッダファイルとソースファイルに分けることで、さらに管理しやすくなります。

関数演習で身につけたい実践力

関数を使いこなすには、引数、戻り値、条件分岐、ループ、乱数、ファイル分割を組み合わせて考えることが大切です。

3つの整数を合計するsum3関数では、複数の値を引数で受け取り、計算結果を戻り値として返しました。

myAbs関数では、if文とreturn文を使って、条件によって返す値を変えました。

judgeProduct関数では、戻り値の1と0を使って、正解か不正解かを判定しました。

triangle.h、triangle.c、main.cの例では、関数の宣言と本体を別ファイルに分け、main.cから関数を呼び出す形を確認しました。

このような演習を積み重ねると、関数をただ書けるだけでなく、どの処理を関数に分けるべきか、どの値を引数にするべきか、戻り値をどう使うべきかを考えられるようになります。

関数を小さな部品として設計できるようになると、C言語のプログラムはぐっと読みやすく、修正しやすくなります。