関数プロトタイプ

| 関数の型情報と関数プロトタイプ | 関数プロトタイプの書き方 | 演習問題 | 暗黙の型情報 | ライブラリ関数の関数プロトタイプ |

関数の型情報と関数プロトタイプ

一般にC言語で関数を実行する際には、 その関数を定義するときに指定した型の引数を渡し、 それらの値を用いて何らかの処理を実行した結果が、 同様に関数の定義で指定した型の値(戻り値)として戻ってきます。 たとえば下のように定義された関数addではdouble型の二つの引数が渡されて、 それらを足した結果としてdouble型の値が戻ってきます。

double add(double a, double b)
{
  return a+b;
}

この関数を使ったプログラムは例えば以下のようになります。

#include <stdio.h>

double add(double a, double b)
{
  return a+b;
}

int main()
{
  printf("answer=%f\n", add(3.0, 4.0) );
  return 0;
}

C言語ではプログラムをコンパイルするとき、 引数として渡す値や関数から戻されるはずの値が 関数の定義のときに指定した型に合致しているかどうかをチェックします。 これがうまく適合していないとコンパイル時にエラーとなります。 こうすることで誤った型で引数を渡してしまうような失敗を事前(実行より前)に チェックできます。

ところでこの仕組みには「順序(記述位置の上下関係)」が重要です。 つまりプログラム上で関数を呼び出す位置より「前」に関数が定義されていないと コンパイラはその関数の型が何なのかわかりません。

上のプログラムではaddを定義した「後」にmainからaddを呼び出していますので、 コンパイラはaddの定義に与えられた引数や戻り値の型の情報と、 mainの中でaddに渡している引数(の値)の型やaddからの戻り値の型を 照らし合わせてチェックすることが出来ます。

しかしいつも順序よく書けるとは限りません。 プログラムが複雑になると、このような情報を知ることが出来ない位置で関数を 使う必要が出てきます。

(そのような場合を想定しながら) 例えば、上のプログラムでmainとaddの位置を入れ替えてみましょう。

#include <stdio.h>

int main()
{
  printf("answer=%f\n", add(3.0, 4.0) );
  return 0;
}

double add(double a, double b)
{
  return a+b;
}

このプログラム(test2.c)をコンパイルすると以下のようなエラーがでます。

test2.c:10: error: conflicting types for 'add'
test2.c:6: error: previous implicit declaration of 'add' was here

このようにコンパイルが失敗するのは 関数addの定義より前にaddが使われているために、 addの型のチェックがうまくできないためです。

「この関数は未知のものです」といったエラーが出て欲しいところですが、実際にはそうなりません。 この理由については後の「暗黙の型情報」で説明があります。

そこでC言語では、関数の定義より前に、関数について情報を与えるための方法が用意されています。 それが関数プロトタイプ(関数の「原型」というような意味)です。 関数プロトタイプを使うと上のプログラムは次のように書き換えることができます。

#include <stdio.h>

double add(double fst, double snd);

int main()
{
  printf("answer=%f\n", add(3.0,4.0));
  return 0;
}

double add(double a, double b)
{
  return a+b;
}

このようにaddを使用するmainの前にaddの関数プロトタイプ

double add(double fst, double snd);

を挿入しておくことにより、 コンパイラはaddがdouble型の値を返す、 また二つの double 型の引数をとる関数であることを事前に知ることができるので、 main の中でその戻り値の型や引数の型と個数が一致していることをチェックすることができます。

「関数はそれが呼び出される「前」に存在しなければならない」 ということから判るように C 言語では記述の順が重要です。 C コンパイラはプログラムを「上から下に読みながら」処理しているからです。 いったん全部プログラムを読んで、関数に関する情報を全部抽出しておいてから 全体をチェックしなおせば良さそうなものですが、実際にはそうなっていません。 この「上から下に一度読む (one path) だけで処理できる」というコンパイラの挙動は C 言語が設計された当時としては重要なポイントでした。 当時のコンピュータではメモリやディスクが非常に少なかったためです。 それらが少ないマシンでもコンパイルできるようにしたい、という意図が あったかもしれません。 なぜメモリやディスクの量が少ないと、こうした工夫(や制限)が必要になるのか、 興味のある人は調べてみると良いでしょう。

ページ先頭へ戻る

関数プロトタイプの書き方

このような例からわかるように、 関数プロトタイプの一般的な形は以下のようになります (引数が二つある場合)。

戻り値型 関数名(引数型1 仮引数名1, 引数型2 仮引数名2);

ただし、関数プロトタイプの仮引数名は、 その関数の定義で用いた仮引数名と異なっていても構いません。 実際、前節の例ではプロトタイプでの引数名は fst と snd でしたが、 関数定義では a と b となっています。

また、仮引数名を省略することもできます。 例えば、次のような関数プロトタイプを使っても構いません。

double add(double, double);

こちらの方がシンプルですが、上手に仮引数名を付ければ読むときに引数の意味がわかりやすくなるという利点もありますので、 これらの書き方は必要に応じて使い分けましょう。

いくらか特殊な場合の書き方

次に、関数定義や関数プロトタイプでいくつか特別に考えなければない場合があります。 まず関数に引数がない場合です。このような場合は引数型として voidと書きます(この場合は仮引数名は意味がないので書きません)。 例えば引数のない関数fooのプロトタイプは以下のようになります。

double foo(void);

さらに、C言語にはprintfのように引数の数が可変の関数もあります。 そのような関数のプロトタイプは...を用いて次のように書きます。

int printf(char *format, ...);

なお、古いプログラムでは次のように関数プロトタイプの引数の部分が書かれていないこともあります。

double add();

これは以前のC言語で用いられていた宣言の仕方で 関数宣言と呼ばれます。 この場合は上のvoidと違って引数の有無は不明と見なされるので引数に関する型のチェックは行われません。 当然その分エラーが起こる可能性は高くなります。 皆さんが書く場合は特に必要がない限りは関数プロトタイプを用いるようにしましょう。

参考:void は戻り値をもたない関数を表現する場合にも(ここで紹介したのとは異なる表記法で)使います。 それについてはプログラミング B で紹介しますが、ここで説明した引数のない場合の void 表記と混同しないように注意してください。

ページ先頭へ戻る

演習問題

A. 次のように用いられている関数fとgの関数プロトタイプを書きなさい。

char a[10];
int i;
double d;
.....

i=f(a);
d=g();
.....

B. 以下のプログラムはpos, neg, mainの三つの関数の順序をどのように変えてもコンパイルに失敗します。その理由を述べなさい。 また、正しくコンパイルして実行できるようにプログラムを変更しなさい。 ただし関数の内容に変更を加えてはいけません。

/* arctan xのx=1における展開形 */
/* pi/4 = 1 - 1/3 + 1/5 - 1/7 + ... + 1/(4k+1) - 1/(4k+3) + ... */

#include <stdio.h>
#define COUNT 300

double pos(double n)
{
  return neg(n - 1) + 1 / (4 * n + 1);
}

double neg(double n)
{
  if (n < 0)
    return 0;
  else
    return pos(n) - 1 / (4 * n + 3);
}

int main(void)
{
  printf("pi = %f\n", 4 * pos(COUNT));
  return 0;
}

ヒント:演習問題 B. のプログラムの内容が(数学的にも、プログラム的にも)すぐ理解できないかもしれません。 しかしそれでも互いの関数の呼び出し関係に注目すれば、これがうまくコンパイルできないことは理解できるでしょう。

ページ先頭へ戻る

参考:暗黙の型情報

実はdoubleの代わりにintを用いた次のようなプログラムは関数プロトタイプがなくてもコンパイルし実行することができます。

#include <stdio.h>

int main()
{
  printf("answer=%d\n", add(3,4));
  return 0;
}

int add(int a, int b)
{
  return a+b;
}

これはC言語では関数の型の情報が得られないときには戻り値の型は暗黙にintと仮定するという約束があるために、たまたま型がintで一致したためです。 しかしこれはたまたま一致しただけなので、皆さんは関数プロトタイプを書く習慣をつけましょう。

なおこれを踏まえれば、以前のdoubleを用いた時のエラーメッセージは 「この暗黙の仮定のintとaddの定義に書かれているdoubleが一致していない」 ということを意味していたことが理解できるでしょう。 英語ですがもう一度読んでみてください。

ページ先頭へ戻る

参考:ライブラリ関数の関数プロトタイプ

C言語でプログラムを開発する場合には入出力関数や数学関数など様々なライブラリ関数を用いますが、 それらについても当然ながら型についての情報をどこかから得る必要があります。 この講義のはじめにインクルードファイルについて学びましたが、 実際にはライブラリ関数の関数プロトタイプはインクルードファイルにまとめて書かれたものがあらかじめ用意されており、 それをライブラリ関数を使用する前に(ソースファイルの先頭などで)インクルードするのが普通です。

例えばman sinでマニュアルをみると、以下のようにライブラリ関数sinの用法とともに、 使用すべきインクルード指令 (この場合は#include <math.h>) が書かれています。

SYNOPSIS
            #include <math.h>
            double sin(double X);

ライブラリ関数の使用は、プログラムをいくつかの部分に分けて開発する分割コンパイルという手法の一種に当たりますが、このことについては今は説明しません。 (二年次配当科目でやります。興味のある人は 分割コンパイルとリンケージ を参照のこと)

ページ先頭へ戻る