関数 | Programming Place Plus C言語編 第9章

トップページC言語編

このページの概要

以下は目次です。


関数

これまでに、main関数、puts関数、fgets関数といった関数が登場していますが、ここまであまり深入りせずに進めてきました。この章では、もう少し詳細なところを説明をしていきます。

関数は、「ある1つの仕事をこなすために必要なソースコードを1か所にまとめて名前を付けたもの」という捉え方ができます。fgets関数は、「1行分の入力を文字列として受け取る」という仕事をまとめて、それを fgets という名前で表現しています。適切に名前を付けることは可読性の向上につながるので、これは関数を使う利点の1つといえます。

fgets という名前は、省略されすぎていてそれほど分かりやすくはないですが。ちなみに、file get string の略です。

また、何度か必要になる処理が1つの関数としてまとめられていれば、繰り返し同じコードを記述しなくて済みます。これはコードを再利用できるということで、開発効率を向上させる意味があります。また、その処理の一部を修正しなければならなくなったとき、関数の中身だけを直せばよく、プログラム内のあちこちを直してまわる必要がなくなります。これも関数の重要な利点です。

これまで、必須である main関数の中身を記述し、そこから puts関数などのほかの関数を呼び出して使っていました。自分で新たな関数を作成することもでき、C言語のプログラムはそうやって新たな関数を次々に作りながら組み立てていきます。

標準ライブラリ関数

printf関数や fgets関数のように、処理系が用意しなければならないと標準規格で定められている関数がいくつもあります。このような関数を、標準ライブラリ関数 (standard library function) といいます。

標準ライブラリ関数を使うには、#include <stdio.h> のような記述が必要になります。この記述は、インクルード (include) などと呼ばれ、指定したファイルの内容を取り込むという意味があります。stdio.h の内容は、標準ライブラリ関数を使うために必要な、C言語で書かれたソースコードです。

stdio.h などのファイルは、開発環境をインストールしたフォルダのどこかに実際に存在します。Visual Studio の場合なら、#include <stdio.h> という記述のところを右クリックして、「ドキュメント <stdio.h> を開く」を選ぶと簡単に中身を確認できます。絶対にファイルの内容を書き換えないように注意してください。

どの標準ライブラリ関数が、どのファイルを必要とするのかは標準規格によって定められており、使いたい関数に応じて、正しいものを指定しなければなりません。よく使う関数は覚えてしまえばいいのですが、標準ライブラリ関数は非常にたくさんありますから、まれにしか使わない関数は、リファレンスで調べるなどしなくてはなりません。この件にかぎらず、調べる習慣や方法は身に付けてください。

標準ライブラリ関数の詳細については、「標準ライブラリのリファレンス」もご利用ください。

標準ライブラリ関数の価値は、どの処理系でも同じ使い方ができて、同じ結果を得られるということです。標準ライブラリ関数を使うようにしてれば、作ったプログラムをさまざまな環境で使いまわせる可能性が高まります。また、非常にレベルの高いプログラマーが実装しているであろうことから性能も良く、多くの人々が実際に使っているという事実が不具合の少なさを保証しているともいえます。事情がない限り、標準ライブラリ関数でできる処理を自力で書くべきではありません。そして、そのためには標準ライブラリ関数のことを積極的に知ろうとする、なにか使えるものがないか調べようとする態度も必要です。

処理系によっては、標準規格には存在しない関数をいくつか追加で実装していることがあります。このような関数には、標準ライブラリ関数ではできないような、かなり便利な機能が含まれていることが多いのですが、当然、他の処理系では使えません。C言語編としては、標準でない関数については原則として取り上げていません。

関数の宣言

新たな変数を作るときに宣言を記述するように、新たな関数を作るときにも、まずは関数の宣言を記述します。

あとで説明する「関数の定義」があれば、宣言を書かずに済ませられることがあります。

なお、関数の宣言のことを、関数プロトタイプ(関数原型) (function prototype) と呼ぶこともあります。

正確にいえば、このあと説明する仮引数の型や個数を明確に示す宣言が、関数プロトタイプです。

宣言とは、「こういう名前のものがある」ということをコンパイラに伝える記述です。関数の宣言を記述することで、その名前の関数が存在するという事実を明示したことになります。標準ライブラリの関数を使うときに、#include <****> のようなインクルードを書くのは、標準ライブラリ関数の宣言を取り込むためです。


関数の宣言は次の構文で記述します。

戻り値の型 識別子(仮引数);

「戻り値の型」には、関数から返す値の型を記述します。“関数から返す値” のことを戻り値(返し値、返り値、返却値) (return value) といいます。C言語では戻り値は 1個、あるいは 0個でなければならず、1個ならその値の型を、0個なら void と記述します。

int 識別子(仮引数);    // int型の値を返す関数の宣言
void 識別子(仮引数);   // 戻り値がない関数の宣言

「戻り値の型」を配列型にすることはできません。これは重大な制約ですが、回避する方法はあります。ただ、現時点の知識では難しいので、ここでは取り上げないことにします(第33章で取り上げます)。


「識別子」には、関数の名前を記述します。使える文字のルールは変数のときと同様です(「第4章」を参照)。

int get_score(仮引数);

関数に限りませんが、名前の付け方についていくつか流派のようなものがあって、代表的なものに、get_score のように小文字を使い、_ で区切る規則で付ける方法(スネークケース (snake case))と、GetScore のように単語の頭を大文字にする規則で付ける方法(キャメルケース (camel case))があります。標準ライブラリではスネークケースが用いられているので、当サイトではスネークケースを採用します(変数名でも同様の方針です)

一緒に使うほかのライブラリが異なる方法を採用していると、結局どこかで混在することになりますが、自分たちで書く部分については統一しておこうとするのが一般的だと思います。

仮引数 (parameter)」には、関数を呼び出す側から渡されてくる値を受け取る変数の型と名前を、, で区切りながら書き並べます。仮引数が必要なければ (void) と書きます。「仮引数」の指定を空にして、int get_score(); と書くことができますが、これは勧められない記述です。

int get_score(void);            // 仮引数がない関数の宣言
int get_score(int player_no);   // 仮引数が1つの関数の宣言
int get_winner_no(int player1_no, int player2_no);  // 仮引数が2つの関数の宣言
int get_score();  // よくない。仮引数不明

戻り値と同じく、仮引数を配列型にすることはできませんが、回避する方法があります(第33章で取り上げます)。


関数の宣言に、関数の名前、仮引数の型と個数・順番が記述されていることによって、その関数を呼び出そうとしている箇所の記述が適切かどうかをコンパイラが判断できます。たとえば、仮引数がないのに、score = get_score(100); のように呼び出すのは間違っているためコンパイルエラーにできます。また、仮引数が int型のところへ、12.3 のような型が異なる値を渡そうとしていることも検出できます。

「仮引数」が空の場合、「仮引数が何であるかは問わない」といっていることになり、このようなコンパイラのチェック機構がはたらかないことになります。

仮引数が空である関数宣言は、関数プロトタイプとは呼べません。1

【C++プログラマー】C言語では、仮引数を空にした場合と void と書いたときの意味は異なります。void は仮引数がないことを表しますが、空にした場合は、実引数として渡している内容の型や個数を特にチェックしないことを意味します。空にすることに利点はないので、必ず void と記述するようにすべきです。

仮引数が複数必要な場合は、記述する順序にも気を留めるようにしてください。関数を呼び出す側は、仮引数の順番に合わせて、値を書き並べなければならないので、あまりに不自然な順序であったり、関数ごとに一貫性が欠けていたりするのは嫌われます。


関数の宣言を記述した位置よりもうしろからでないと、その関数を呼び出すことはできません。インクルードをソースファイルの先頭付近に書くのはこのためです。

// 宣言(別のところに定義が必要)
void func(int value);

int main(void)
{
    func(100);  // OK
}
int main()
{
    func(100);  // コンパイルエラー。宣言がみえない
}

// 宣言(別のところに定義がある)
void func(int value);

関数の定義

関数の宣言には、肝心の「関数の中身」がありません。つまり、その関数が何をするのかが記述されていません。関数の中身を記述したものを、関数の定義 (definition) と呼びます。

関数の定義は、次の構文で記述します。

戻り値の型 識別子(仮引数)
{
     処理
}

このかたちはいつも書いている main関数のかたちと同じです。つまりこれまでのページで書いてきたプログラムは、main関数の定義を書いていたわけです。

最初の行は、末尾の ; がないことを除いて、関数の宣言とまったく同じですし、同じでなければなりません。

{} の内側に、関数が行う処理(関数の本体)を書きます。これまで main関数の内側に書いてきたコードが同じように書けます。たとえば、変数を宣言したり、ほかの関数を呼び出したりできます。関数の中で宣言した変数は、別の関数からは使えません。

関数が呼び出されると、呼び出し側で指定した値(f(100, n); のように呼び出したとすれば 100 と n)が、対応する仮引数の初期値として渡されてきます。関数の本体では、その仮引数を使用できます。

// 宣言
void print_values(int a, int b);

// 定義
// 仮引数a、b は、呼び出し側から渡されてきた値で初期化される変数
void print_values(int a, int b)
{
    printf("%d, %d\n", a, b);
}

「戻り値の型」が void の場合は、処理が末尾の } のところまで実行されると、呼び出し元へ戻ります。あるいは、return; という記述によっても戻ることができます。これは return文 (return statement) という文です。

「戻り値の型」が void でない場合は、return文が必須です(main関数だけは例外的に省略できる)。return文は、return 0; とか return a; といったふうに、戻り値を指定することができ、これが呼び出し元に返されます。

// 整数の入力を受け取る
// 戻り値: 標準入力から整数の入力を受け取り、その値を返す。
int get_input_integer(void)
{
    char s[40];
    fgets(s, sizeof(s), stdin);
    int value;
    sscanf(s, "%d", &value);
    return value;  // value の値を戻り値として返す
}

戻り値には名前を付けられませんから、呼び出す側が、どんな意味の戻り値が返ってくるのか分かるようにすることが望まれます。1つには、関数名を分かりやすくする方法がありますが、限界もあるので、コメントを書き添えておくのが妥当な手段です。戻り値の意味のほか、引数の意味やルール、その関数がどんなことをするものなのかといったことを書きます。呼び出し側から確実にみえるところにあるのは関数の宣言のほうなので、これらのコメントは、関数の宣言のところに書きます(定義しかない場合は定義のところでもいい)。

関数を活用すると、プログラムの実行順は上から下へ進むというだけでなく、ほかの関数の内側に移動したり戻ってきたりもすることを理解しなければなりません。

Visual Studio のステップ実行の機能を使うなどして、実行の流れを確認してみるのもいいかもしれません。「Visual Studio編>ステップ実行」のページを参照してください。

関数の定義は、関数の宣言の代わりを努めることができます。つまり、その関数を呼び出そうとしている位置より手前に定義があれば、宣言を記述しなくても問題ありません。

// 定義(宣言はない)
void func(int value)
{
    // ...
}

int main(void)
{
    func(100);  // OK
}

しかし、プログラムの複雑が大きくなってくると、宣言を記述しないとうまくコンパイルできない場面が出てくることがあります。基本的には、宣言は記述するものです。

関数呼び出し

関数は、その名前と () を使った、関数呼出し (function call) と呼ばれる式によって呼び出せます。

関数名(実引数)

() の内側に関数に渡す値を書き並べます。この値のことを、実引数 (argument) と呼びます。仮引数と実引数を区別しなくていい場面では、いずれも単に引数 (parameter、argument) と呼ぶことがあります。

実引数は、関数の宣言や定義のところに書いた仮引数の順番に合わせて記述しなければなりません。型が一致しない場合はコンパイルエラー(警告の場合もある)になります。結果的に値になればいいので、f(a + b) のような計算式を記述しても構いません。

当然、同じ関数を何度も呼び出すことができ、そのたびに実引数の値を変えることが可能です(これまでにも、printf関数や puts関数に渡す実引数はいつも違っていました)。そのため、うまく関数を作ることで、同じ仕事をするコードを1つにまとめられます。将来同じコードが必要になったときには、その関数を呼び出すだけで簡単に済みますし、同じコードが色々なところに散らばっていないので、後からコードを修正することも容易になります。


さきほど定義した get_input_integer関数を次のように呼び出して使用できます。

#include <stdio.h>

// 整数の入力を受け取る
// 戻り値: 標準入力から整数の入力を受け取り、その値を返す。
int get_input_integer(void)
{
    char s[40];
    fgets(s, sizeof(s), stdin);
    int value;
    sscanf(s, "%d", &value);
    return value;  // value の値を戻り値として返す
}

int main(void)
{
    puts("Please enter the integer.");
    int value1 = get_input_integer();

    puts("Please enter the integer.");
    int value2 = get_input_integer();

    printf("%d + %d = %d\n", value1, value2, value1 + value2);
}

実行結果:

Please enter the integer.
15  <-- 入力された内容
Please enter the integer.
7  <-- 入力された内容
15 + 7 = 22

関数の呼び出し元は、戻り値を必ずしも変数で受け取らないといけないわけではなく、返された値をほかの関数の実引数に使うといったことも可能です。

printf("%d\n", get_input_integer());  // 戻り値をほかの関数の実引数に使う

戻り値を無視する

関数が戻り値を返すように実装されていても、呼び出し側はその戻り値が不要だというケースがあります。たとえば、printf関数にも戻り値はあって、正常に出力できた文字数を返すという仕様になっています(リファレンス参照)。ほとんどの場合、これを受け取っても使い道がないため無視しています。

しかし、戻り値を無視することが明らかにおかしいといえる関数もあります。たとえば、さきほど定義した get_input_integer関数は、整数の入力を受け取るという目的をもって使う関数なので、呼び出しておきながら、戻り値はいらないというケースはあり得なさそうです。しかし、戻り値を受け取らないという選択が許されている以上、次のようなコードが書けてしまい、バグの原因になるので注意が必要です。

int value;
get_input_integer();    // 戻り値を受け取るのを忘れている
printf("%d\n", value);  // それでも変数value 自体は存在しているからアクセスできる (だが中身は不定値)

コンパイラが警告を出している場合があるので、しっかり確認するようにしましょう。このコードの場合、Visual Studio 2015 は、“初期化されていないローカル変数 ‘value’ が使用されます” という警告を出しました。戻り値を受け取っていないという直接的な指摘ではありませんが、変数value に値が入らないまま使ってしまっていることを明確に教えてくれています。

main関数の戻り値

main関数の戻り値の型は原則として int型です。ほかの型が使える可能性はありますが、それは処理系定義です2

main関数にかぎっては、戻り値の型が void でなくても return文を省略でき、関数の末尾に return 0; があるかのように扱われます3

【上級】main関数の戻り値が int型の場合に、main関数を終了することは、戻り値を実引数として exit関数を呼び出すことと同じ意味になります。4


練習問題

問題① 次のプログラムの実行結果はどうなりますか?

#include <stdio.h>

void f1(int n);
void f2(int n1, int n2);

int main(void)
{
    f1(1);
    f2(10, 20);
}

void f1(int n)
{
    f2(n, n + 1);
}

void f2(int n1, int n2)
{
    printf("%d, %d\n", n1, n2);
}

問題② int型で渡される引数の値を、2乗して返す関数を作ってください。


解答ページはこちら

参考リンク


更新履歴

’2018/4/22 解説中で C95 を(C89 に対して)特別扱いしないように修正。そもそもC言語編は C95ベースなので、余計な説明は省く。

≪さらに古い更新履歴を展開する≫



前の章へ (第8章 文字と文字列)

次の章へ (第10章 定数)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る