4.3 関数の宣言
4.3.1 関数宣言子
デフォルトの型以外の関数を呼び出すには、関数をそれよりも前に定義する必要がありました。一つ二つ程度の関数ではそれでもよいかもしれません。しかし、実践のプログラムでは数十、数百という関数を扱います。例えば Microsoft Windows が提供する関数は 1000 を越えます。内部では、さらに多くの関数が定義されていることでしょう。それを全てmain() 関数よりも前に定義して、さらに関数から関数を呼び出すときの関係を把握するのは不可能でしょう。
そこで関数宣言子というものがあります。関数宣言子は関数のプロトタイプ宣言とも呼ばれ、戻り値、関数名、パラメータリストだけを指定します。関数宣言子は、C言語が発案された当時には存在しておらず、1989年に標準化されたときに追加された仕様です。
関数宣言子は、関数の定義よりも前で宣言することで、コンパイラに関数の名前やパラメータ型、及び戻り値の型を伝えます。このとき、本体は定義しません。
戻り値型 関数名(パラメータリスト);
こうした関数の宣言をプログラムの先頭にまとめることで、関数の定義位置を気にする必要がなくなります。関数を定義する時は、もちろん関数の宣言に基づいた型で定義されなければなりません。宣言と異なる型で定義した場合、やはり型の不一致によってエラーとなります。
#include <stdio.h> void CharLoop(char chMark , int iNum); int main() { CharLoop('*' , 30); printf("\n---\n"); CharLoop('*' , 40); printf("\n"); return 0; } void CharLoop(char chMark , int iNum) { int iCount; for(iCount = 0 ; iCount < iNum ; iCount++) { printf("%c" , chMark); } }
コード1は文字 chMark を iNum 回だけ繰り返し処理で表示する関数 CharLoop() 関数を定義しています。CharLoop() 関数は main() 関数よりも後ろで定義されていますが、main() 関数よりも前に CharLoop() 関数を宣言しています。そのため、main() 関数での CharLoop() を呼び出していますが、コンパイラはこの時点で CharLoop() 関数のパラメータや戻り値型を認識しているため問題はありません。
この、関数宣言子は比較的新しい仕様です。実は、この仕様が制定される以前は戻り値のデータ型をコンパイラに知らせるために関数宣言子が使われていました。古い関数宣言子は、パラメータリストに型情報を含まず、識別子だけを記述していました。しかし、この状態ではコンパイラが関数に渡す値の型チェックを十分に行うことができないため、現代では推奨されません。
void Function(value1 , value2)
例えば、上のコードは古い関数宣言子です。見てわかるように、この関数宣言のパラメータには型情報が含まれていません。
古い関数宣言子は、過去の言語との互換性を考慮して、通常は現在のコンパイラにも実装されています。しかし、新しくプログラムコードを記述する場合は、常に最新の仕様を心がけて書くべきであり、現代において古い関数宣言子を利用するのはナンセンスです。全ての関数は新しい関数宣言子を利用して、関数を宣言するべきです。
しかし、古い関数宣言子の存在を知らない場合、意外なところで問題が発生します。例えば次のようなプログラムを書いたとしましょう。
void Function();
この場合は、識別子を省略した形の型情報を含まない古い関数宣言子になってしまいます。型情報を明記しているのであれば、それは新しい関数宣言子であると認識されますが、型を省略するという記述は古い関数宣言子の仕様です。そのため、新しい仕様に準拠した関数宣言子のつもりで書いても、実は古い関数宣言子だったということがしばしば発生しかねません。これは、コンパイルできてしまうため、意外と気づかれないミスです。
パラメータのない関数を宣言するには、型情報として void を指定します。
void Function(void);
これは、新しい関数宣言子であると認識されます。
#include <stdio.h> void Function(void); int main() { Function(); return 0; } void Function(void) { printf("Kitty on your lap\n"); }
このプログラムは、新しい関数宣言子を用いてパラメータのない関数宣言しています。
4.3.2 識別子の省略
コード1の関数宣言子 void CharLoop(char chMark , int iNum); を見ると、パラメータは型だけではなく変数名も指定しています。しかし、宣言は本体を含まないため、識別子(変数名)は意味を持ちません。
そこで、関数宣言子では型名のみを指定し、識別子を省略することが可能です。そもそも、新しい関数宣言子の目的は、コンパイラに型名を伝えることによって、関数呼び出し時のエラー検出や引数の対応強化にあります。識別子の記述は古い関数宣言子から引き継いだものであり、定義のない状態では大きな意味はありません。
void Function(int , char , double);
例えば、この場合は順に int 型、char 型、double 型の引数を受け取ることを表しています。引数を受け取るパラメータの識別子は、関数の定義で指定すればよいのです。因みに、識別子を省略したパラメータと、識別子を明記したパラメータを同一の関数宣言子に含むことも可能です。
void Function(int iValue , char , double);
この関数宣言子は、最初のパラメータ iValue のみ識別子も含めて宣言していますが、それ以外のパラメータは識別子を省略しています。これでも問題はありません。
#include <stdio.h> void CharLoop(char chMark , int); int main() { CharLoop('*' , 30); printf("\n---\n"); CharLoop('*' , 40); printf("\n"); return 0; } void CharLoop(char chMark , int iNum) { int iCount; for(iCount = 0 ; iCount < iNum ; iCount++) { printf("%c" , chMark); } }
コード3はコード1を改良し、識別子を明記した場合と、省略した場合の組み合わせで関数を宣言しています。関数宣言子の識別子が使われることはないので、識別子を指定するのはいささか冗長です。そこで、識別子は省略して型だけを指定するのです。