パソナについて
記事検索

【ソースコード有】C言語の関数とは − 作り方や呼び出し方の基本を解説

今回は、関数を使ううえで基本となる定義方法と呼び出し方のほか、プロトタイプ宣言やポインタによる参照渡しなどのテクニックについても解説します。

【ソースコード有】C言語の関数とは − 作り方や呼び出し方の基本を解説

今回は、関数を使ううえで基本となる定義方法と呼び出し方のほか、プロトタイプ宣言やポインタによる参照渡しなどのテクニックについても解説します。

スキルアップ

2023/02/27 UP

プログラムを作っていると、複数の箇所で同じような処理を行ないたいケースが出てきます。C言語の「関数」は、そのような場合に便利な仕組みです。一連の処理を関数として定義しておけば、プログラム中で何度でも呼び出して使えるようになります。うまく使えばソースコードが簡潔で読みやすいものになり、プログラミングも効率良く進むようになるでしょう。

今回は、関数を使ううえで基本となる定義方法と呼び出し方のほか、プロトタイプ宣言やポインタによる参照渡しなどのテクニックについても解説します。また、関連知識として関数形式マクロについても説明するので、ぜひ役立ててください。

関数の作り方と呼び出し方の基本

C言語には、さまざまな関数があらかじめ用意されています。画面にテキストを表示するprintf()やファイル操作のためのfopen()、文字列を比較するstrcmp()などは利用頻度も高いでしょう。これらの関数には、プログラム中で何度でも呼び出せるという性質があります。

関数は、自分で作ることも可能です。int型などの単純な変数を用いた計算はもちろん、構造体やポインタを使った高度な処理も関数として定義できます。まずはC言語での関数の作り方と、その呼び出し方法を理解しましょう。

関数の基本型と作り方

C言語では、次のような書式で関数を定義します。

戻り値の型 関数名(引数リスト) {
  /* ここから関数の処理を書く */
  return 戻り値; /* 処理の最後には戻り値を返す */
}

書式中の各要素の意味は、次のとおりです。

・戻り値の型:関数の実行結果を表す値の型

・関数名:関数を識別するための名前

・引数リスト:関数が受け取る値の一覧(複数ある場合はカンマで区切る)

・戻り値:関数の実行結果を表す値

これらについて理解するために、具体例を見てみましょう。以下は、「何時間何分」という値を引数リストとして受け取り、それが「何秒」なのかを計算して戻り値として返却する関数です。

int howManySeconds(int hours, int minutes) {
    int seconds = (hours * 60 + minutes) * 60;
    return seconds;
}

関数から返却すべき値が存在しないこともあるでしょう。その場合は、戻り値の型をvoidとすることで、何も返さない関数を定義できます。

void printHowManySeconds(int hours, int minutes) {
    int seconds = (hours * 60 + minutes) * 60;
    printf("%d時間%d分は%d秒です。\n", hours, minutes, seconds);
    /* 何も返さないためreturnは省略可 */
}

引数を1つも必要としない関数を定義することも可能です。その場合は、引数リストにvoidと書きます。

void printHowManySecondsInOneHour(void) {
    int seconds = 60 * 60;
    printf("1時間0分は%d秒です。\n", seconds);
}

作った関数を呼び出す方法

定義済みの関数は、次の書式で何度でも呼び出せます。

関数名(引数リスト);

引数リストには、関数を定義したときと同じ型の値を与えなければなりません。関数が戻り値を返す場合は、戻り値と同じ型の変数を用意すれば次のように代入できます。

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

それでは、上記3つの関数を呼び出すプログラムを書いてみましょう。以下は、関数の定義と呼び出しを1つにまとめたソースコードです。コンパイルして、実際の動作を確認してみてください。

サンプルコード:

#include <stdio.h>

int howManySeconds(int hours, int minutes) {
    int seconds = (hours * 60 + minutes) * 60;
    return seconds;
}

void printHowManySeconds(int hours, int minutes) {
    int seconds = (hours * 60 + minutes) * 60;
    printf("%d時間%d分は%d秒です。\n", hours, minutes, seconds);
}

void printHowManySecondsInOneHour(void) {
    int seconds = 60 * 60;
    printf("1時間0分は%d秒です。\n", seconds);
}

int main(void) {
    printf("1つ目の関数呼び出し:\n");
    for (int h=0; h<4; h++) {
        int seconds = howManySeconds(h, 30);
        printf("...%d時間30分は%d秒です。\n", h, seconds);
    }

    printf("2つ目の関数呼び出し:");
    printHowManySeconds(1, 15);

    printf("3つ目の関数呼び出し:");
    printHowManySecondsInOneHour();

    return 0;
}

実行結果:

1つ目の関数呼び出し:
...0時間30分は1800秒です。
...1時間30分は5400秒です。
...2時間30分は9000秒です。
...3時間30分は12600秒です。
2つ目の関数呼び出し:1時間15分は4500秒です。
3つ目の関数呼び出し:1時間0分は3600秒です。

プロトタイプ宣言とは

C言語の関数は、原則として呼び出し前に定義されている必要があります。先程のソースコードでも、関数を呼び出している箇所よりも上の行で、それぞれの関数が定義されていました。しかし、プログラムが複雑になるほど、常に定義を先に書くというのは簡単ではなくなっていきます。このような問題を解決するのが、プロトタイプ宣言です。

プロトタイプ宣言には、関数を呼び出す際に必要となる関数名や引数リスト、戻り値の型だけを記述し関数の処理内容は含めません。ソースコードの序盤に宣言しておくことで、関数が呼び出されたときの処理をあとから定義できるようになります。

プロトタイプ宣言の仕方

プロトタイプ宣言の書式は次のとおりです。

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

関数を定義するときの書き方から、中かっこ(「{」と「}」)で囲まれた部分を取り除いた形になっていることがわかるでしょうか。これにより、関数の処理内容が定義される前でも、呼び出しだけは行なえるようになります。

それでは、プロトタイプ宣言を用いて先程のプログラムを書き直してみましょう。

サンプルコード:

#include <stdio.h>

/* プロトタイプ宣言 */
void printHowManySecondsInOneHour(void);
void printHowManySeconds(int hours, int minutes);
int howManySeconds(int hours, int minutes);

int main(void) {
    /* プロトタイプ宣言が済んでいる関数は、定義前でも呼び出せる */
    printf("1つ目の関数呼び出し:\n");

    for (int h=0; h<4; h++) {
        int seconds = howManySeconds(h, 30);
        printf("...%d時間30分は%d秒です。\n", h, seconds);
    }

    printf("2つ目の関数呼び出し:");
    printHowManySeconds(1, 15);

    printf("3つ目の関数呼び出し:");
    printHowManySecondsInOneHour();

    return 0;
}

/* 関数の定義(プロトタイプ宣言が済んでいるので、あとから定義できる) */

void printHowManySecondsInOneHour(void) {
    int seconds = 60 * 60;
    printf("1時間0分は%d秒です。\n", seconds);
}

void printHowManySeconds(int hours, int minutes) {
    int seconds = (hours * 60 + minutes) * 60;
    printf("%d時間%d分は%d秒です。\n", hours, minutes, seconds);
}

int howManySeconds(int hours, int minutes) {
    int seconds = (hours * 60 + minutes) * 60;

    return seconds;
}

ソースコードの序盤にプロトタイプ宣言を配置することで、関数を定義する順番に制約がなくなっている様子がわかるでしょうか。結果として、プログラムの主要部分を上のほうにもっていくことができました。

ポインタと関数の関係

C言語によるプログラミングでは、「ポインタ」が頻繁に使われます。ポインタとは、メモリ内にあるデータの位置を示すアドレスを格納するための変数です。次のように、「*(アスタリスク)」を使って宣言します。

型 *ポインタの変数名;

ポインタの役割は、「メモリ内で特定のアドレスにあるデータを操作できるようにする」という単純なものでしかありません。しかし、関数と組み合わせて用いることによって、その利用価値は大きく高まります。

ポインタの使い方

ポインタは、サイズの大きなデータを関数の引数にしたいときに力を発揮します。まずは、ポインタを使わない場合から見てみましょう。

サンプルコード:

#include <stdio.h>

typedef struct MyData {
    int total;
    int one;
} MyData;

void addOneByValue(MyData data) { /* 値渡し */
    data.total += data.one;
    printf("totalは%dです。\n", data.total);
}

int main(void) {
    MyData data = {
        .total = 0,
        .one = 1
    };

    for (int i=0; i<4; i++) {
        addOneByValue(data);
    }

    return 0;
}

実行結果:

totalは1です。
totalは1です。
totalは1です。
totalは1です。

この例は、引数として構造体を受け取る関数を繰り返し呼び出すというものです。関数の内側では、構造体の値を更新してから表示しています。ところが、何度関数が呼び出されても、表示される値に変化はみられません。これは、関数呼び出しのたびに、呼び出し元にある構造体のコピーが渡されているためです。このような引数の受け渡し方を「値渡し」といいます。

もし、関数に渡される値がコピーではなく呼び出し元の構造体そのものなら、表示される値は変化していくでしょう。そのような引数の受け渡し方を「参照渡し」といいます。しかし、C言語には参照渡しがありません。そこで、ポインタを引数とする「ポインタ渡し」というテクニックで、擬似的な参照渡しを行ないます。

では、ポインタ渡しを試してみましょう。

サンプルコード:

#include <stdio.h>

typedef struct MyData {
    int total;
    int one;
} MyData;

void addOneByReference(MyData *dataPointer) { /* ポインタ渡し */
    dataPointer->total += dataPointer->one;
    printf("totalは%dです。\n", dataPointer->total);
}

int main(void) {
    MyData data = {
        .total = 0,
        .one = 1
    };

    for (int i=0; i<4; i++) {
        addOneByReference(&data); /* アドレスを引数にする */
    }

    return 0;
}

実行結果:

totalは1です。
totalは2です。
totalは3です。
totalは4です。

今度は関数が呼び出されるたびに、表示される値が1ずつ増えるようになりました。つまり、関数側からポインタを経由して、呼び出し元の構造体を操作しているのです。

関数の呼び出し元では、引数の手前に「&(アンパサンド)」が記述されているのがわかるでしょうか。これは、変数のアドレスを取り出すための演算子です。この書き方により、ポインタ渡しの関数にデータを渡すことができます。

ポインタ渡しの特徴

ポインタ渡しを使うと、関数の内側からでも呼び出し元の変数を操作できることを説明してきました。もう1点、ポインタ渡しには重要な特徴があります。

上の例で、ポインタ渡しによって構造体のコピーが行なわれなくなったことを思い出してみましょう。これにより、値渡しの場合に比べて関数を高速に呼び出せるようになっています。関数呼び出し1回あたりの効果はわずかですが、同じ関数を繰り返し呼び出すようなケースでは、大幅なスピードアップが期待できるでしょう。

なお、malloc()のように、C言語には引数ではなく戻り値をポインタとする関数もあります。これは、関数側で用意したデータを、コピーせずに呼び出し元に返すテクニックです。

関数形式マクロの考え方

C言語には、コンパイル時にソースコード中の特定のワードを置き換える「マクロ」という機能があります。なかでも「関数形式マクロ」は、関数のような使い方ができる便利な手法です。

関数形式マクロは、次の書式で定義します。

#define マクロ名(引数リスト) 処理内容

実際の関数形式マクロ

関数形式マクロの使い方を理解するために、実例を見てみましょう。以下のプログラムでは、2つの文字列が等しいかどうかをチェックするために、STR_EQ()という名前のマクロを定義しています。

サンプルコード6:

#include <stdio.h>
#include <string.h>

#define STR_EQ(lhs, rhs) (strcmp((lhs), (rhs)) == 0)

int main(void) {
    if (STR_EQ("abc", "abc")) {
        printf("同じ文字列です。\n");
    }

    if (!STR_EQ("abc", "xyz")) {
        printf("異なる文字列です。\n");
    }

    return 0;
}

実行結果6:

同じ文字列です。
異なる文字列です。

関数呼び出しと同じような書き方で、関数定義マクロを使用している様子がわかるでしょう。また、一度定義したマクロは繰り返し呼び出せる点も、関数と同様です。

ただし、マクロを使うのは特定のワードを置き換えることと変わらないため、上のソースコードは次のように書くのと同じ意味になります。

サンプルコード7:

#include <stdio.h>
#include <string.h>

int main(void) {
    if ((strcmp(("abc"), ("abc")) == 0)) {
        printf("同じ文字列です。\n");
    }

    if (!(strcmp(("abc"), ("xyz")) == 0)) {
        printf("異なる文字列です。\n");
    }

    return 0;
}

このように、見た目が関数のようでも、関数定義マクロは実際には関数ではありません。関数呼び出しをともなわない分だけ、関数で定義するより動作が高速になります。

関数を使用することで処理は劇的に速くなる

C言語における関数の定義と呼び出しについて、駆け足で説明してきました。プログラム中で繰り返し行なう処理は、関数にまとめておけば何度でも利用可能です。プロトタイプ宣言を併用すれば、ソースコードを簡潔で読みやすいものにできるでしょう。

ポインタ渡しや関数形式マクロも、覚えておいて損のないテクニックです。より高速なプログラムを書く必要が出てきたときに、これらについて知っていれば役立つでしょう。