関数プロトタイプの混乱①
CとC++では関数プロトタイプの考え方が違いますが、そのことを理解されている方というのは意外と少ないように思います。Cでの関数プロトタイプに関する仕様の曖昧さが為、混乱を招いていると思いましたので少しまとめてみます。ところで、
関数定義 ≠ 関数宣言
変数定義 ≒ 変数宣言
として言葉を使い分けています。関数定義は実際のプログラムコードが書かれている部分を指します。変数の場合は定義という言葉が宣言部分に対しても使われるので混乱してしまいますね。
では前方宣言に関して見て行きましょう。基本的にCもC++も後方参照を認めていません(classの定義内ではOK)。それはコンパイラが上から下へと処理していく為、関数の呼び出し処理があったその時点で定義、もしくは宣言されていないものに関しては不定として扱うからです。そのために関数の呼び出し処理の前(上方)で定義、もしくは宣言がされている必要あります。順列・組み合わせ的にみると前方宣言のケースは下記のパターンになるかと思います。
・前方宣言あり ┳ 関数プロトタイプ宣言 ┳ 記憶クラスなし
┃ ┣ static あり
┃ ┗ extern あり
┗ 関数宣言 ┳ 記憶クラスなし
┣ static あり
┗ extern あり
・前方宣言なし
すべて大域領域(グローバルの領域)で記述します。コンパイル時は記憶クラス(extern, static)があってもなくても前方宣言としての役割は何も変わらないので、記憶クラスは抜きにして考えます。(リンカまで考えた場合は外部結合、内部結合、無結合という概念で扱います。)プロトタイプ宣言の役割は
①関数の名前
②戻り値の型
③引数の数
④それぞれの引数の型
をコンパイラに知らせる為にあります。コンパイラは関数呼び出し時にこれらの型と呼び出し関数の型が一致するかを比較してエラーかどうかを判断します。それでは例を見ていきましょう。
Pattern 1
int main(){ func(3); } int func(int i_a, int i_b ){ printf("i_a=%d\n",i_a); return(0); }
プロトタイプ宣言がありませんが、Pattern 1のコンパイルは通るでしょうか?実際やってみるとCだと通ります(引数の数を一致させていないので一部通らないコンパイラがあるかもしれません)がC++だとエラーになります。コンパイラの立場で見ていきましょう。上からプログラムを見ていくといきなりfunc(3) が現れます。リテラルの最後に()を見つけるとコンパイラはそれを関数だと判断しますが、この時点でfuncという関数は未定義な上宣言もされていません。しかしCでは初めて見る関数に対し暗黙の型情報という規則を当てはめ「きっと戻り値はintに違いない」と仮定してエラーをはかずにコンパイルを進めてしまいます。引数に関しては数が一つ、型は即値なのでconst intと記憶します。C++では暗黙の型情報という規則はないのでこの時点でエラーです。その後下に進んで、関数定義を見つけます。名前が一致しました。戻り値がintなので、呼び出し時の型とも一致しました。引数の数は一致していないのですがCコンパイラの仕様では未定義ということで、関数名と戻り値の型さえコンシステントであればコンパイルが通ります。なおC++では引数の数によって関数名を変える処理があるため引数の数も厳密に一致していないといけません。最後に引数の型ですが、この場合は暗黙の型変換が行われconst int から intとなりエラーにもワーニングにもなりません。この型変換はC++でも行われるものです。次のプログラムを見てみましょう。
Pattern 2
int main(){ func(3); } double func(int i_a){ printf("i_a=%d\n",i_a); return(0); }
Pattern2のコンパイルは通るでしょうか?これはCでもC++でもエラーです。ただしそれぞれエラーの理由が異なります。Cでは暗黙の型情報でintと仮定したfuncの戻り値の型と実際の定義の戻り値の型が異なる為エラーとなり、C++では暗黙の型情報で関数の戻り値をintと仮定する処理がない為エラーとなります。では次はどうでしょうか?
Pattern 3
double func(int i_a){ printf("i_a=%d\n",i_a); return(0); } int main(){ func(3); }
またもやプロトタイプ宣言がありませんが、Pattern3はCでもC++でもコンパイル可能です。この場合は関数定義が呼び出し処理よりも先に来ているため後方参照が発生しません。funcの呼び出し処理の時点で戻り値の型情報を仮定する必要がなくなっています。そのため型一致の比較処理が行われずコンパイルが可能となります。ここまでを一回まとめると
①Cでは暗黙の型情報という規則がある為、初めてみる関数をint型と仮定する。その為int型に限って前方宣言が必須ではない。
②CでもC++でも戻り値の型情報は厳密に一致する必要がある。
③Cでは引数の数に関してコンパイラ動作未定義。C++では引数の数によって関数名を変える処理(マングル処理)がある為、引数の数は厳密に一致する必要がある。
④Cでの引数の型は暗黙の型変換が概ね働く。C++でも一部暗黙の型変換が働く。(ただし暗黙の型変換は避けるべきなので型は一致させるべきです。)
⑤CでもC++でも前方定義を行う場合は前方宣言を省略できる。(C++に関してはプロトタイプ宣言必須と書かれている書籍がありますが、実際は省略できてしまいます。ただしプロトタイプ宣言は省略するべきではありません。)
Cでは戻り値の型に関してだけ厳密で、引数の数と型に関してはチェックがずぶずぶです。C++は戻り値、引数の数ともに厳密にしないとエラーとなりますが、引数の型に関してだけ少し寛容です。
今回は前方参照なしのパターンについて見てみました。次回はもう少しバリエーションを広げていく事で理解を深めたいと思います。