こんにちは、ナナです。
関数の「プロトタイプ宣言」というものを、なぜ書くのかを知らない方って初心者に限らず結構たくさんいます。
プロトタイプ宣言ってなんでしたっけ?前に聞いたことある気がするけど、忘れちゃいました…。
先輩に書けって言われたからとりあえず書いてますけど、面倒なんですよね。
そうなんです。
この「忘れる」や「面倒」ということがなぜ生まれるのかというと、必要な理由を知らないからなんです。必要性を知ることで、「ちゃんと忘れずに書かないと!」という気持ちが生まれるのです。
本記事では次の疑問点を解消する内容となっています。
では、プロトタイプ宣言を学んでいきましょう。
関数のプロトタイプ宣言と関数定義の違い
では、まず最初に関数の「プロトタイプ宣言」とは何かを、おさらいしておきましょう。
関数の「プロトタイプ宣言」の書き方
プログラムを使って、具体的にプロトタイプ宣言の書き方を紹介しましょう。
#include <stdio.h>
// プロトタイプ宣言
long sub(long num1, long num2);
int main(void)
{
long sum;
// sub関数の呼び出し
sum = sub(100, 200);
printf("%d", sum);
return 0;
}
long sub(long num1, long num2)
{
return num1 + num2;
}
これが、プロトタイプ宣言ですね。
「関数定義」と「プロトタイプ宣言」の違いを明確にしましょう。
関数定義
long sub(long num1, long num2)
{
return num1 + num2;
}
関数のプロトタイプ宣言
long sub(long num1, long num2);
関数定義は{}を使って、関数として動作する処理の中身を持っているものです。それに対し、プロトタイプ宣言とは{}の処理が存在しないのがわかりますね。
関数定義の「頭」の部分を取り出してセミコロンで閉じたものを、関数の「プロトタイプ宣言」と呼びます。
関数定義とプロトタイプ宣言とは明確に異なるものと認識してください。初心者の方はこの違いを理解していない方が多いです。
プロトタイプ宣言を書かないことが引き起こす問題
「プロトタイプ宣言」というものは軽視されがちなものです。
「書かなくても動くから書いてないけど」といった方もいることでしょう。では、プロトタイプ宣言を書かないことが、いったいどのような問題を引き起こすのかを示していきましょう。
プロトタイプ宣言がないといったいどうなるのか?
それでは、先ほどのプログラムにおいて、プロトタイプ宣言をコメントアウトしてみましょう。
#include <stdio.h>
// プロトタイプ宣言(無効)
//long sub(long num1, long num2);
int main(void)
{
long sum;
// sub関数の呼び出し
sum = sub(100, 200);
printf("%d", sum);
return 0;
}
long sub(long num1, long num2)
{
return num1 + num2;
}
ビルド結果は次のものになります。
main.c(11): warning C4013: 関数 'sub' は定義されていません。int 型の値を返す外部関数と見なします。
11行目のsub関数の呼び出し箇所で、コンパイラの校正機能による警告が行われました。なんと「sub関数が定義されていません」との指摘です。
いやいや、「sub関数の定義は17行目にあるでしょ!」と思うかもしれません。
しかし、コンパイラはソースコードの上から順に校正作業を行っていくため、呼び出し箇所より下に定義されたsub関数の定義に気づけないのです。
そのため、コンパイラは「sub関数というものを呼び出そうとしているけど、私は詳細を知りませんよ」というスタンスになります。
コンパイラについて知りたい方は『コンパイラの役割【エラーの取り除き方の鉄則教えます】』の記事を読むとよいでしょう。
プロトタイプ宣言は「コンパイラ」に密接した機能なんです。コンパイラが何をするのかを知っていないと、プロトタイプ宣言は正しく理解できません。
プロトタイプ宣言を書かないプログラムの危険性
プロトタイプ宣言を書かない場合に、どのような危険性があるかを具体的に示しましょう。
先ほどのプログラムにおいて、sub関数は2つの引数を受け取り合計値を返す関数です。では、sub関数の呼び出し箇所で、引数を1つしか渡さないよう変更したとしましょう。
#include <stdio.h>
// プロトタイプ宣言(無効)
//long sub(long num1, long num2);
int main(void)
{
long sum;
// sub関数の呼び出し(引数1つ)
sum = sub(100);
printf("%d", sum);
return 0;
}
long sub(long num1, long num2)
{
return num1 + num2;
}
これは明らかに不正な関数呼び出しですが、ビルド結果は次のものです。
main.c(11): warning C4013: 関数 'sub' は定義されていません。int 型の値を返す外部関数と見なします。
========== ビルド: 1 正常終了、0 失敗、0 更新不要、0 スキップ ==========
警告は出るものの、正常終了しているのがわかります。
これは不正な関数呼び出しに対して、コンパイラによる校正機能が正常に動作していないことを示しているのです。
このプログラムは実際に動作させることも可能であり、sub関数の第2引数には不定値が設定され、意図した結果は得られません。
コンパイラの構成機能で、不正がチェックできない。これは非常に大きな問題なんです。
プロトタイプ宣言を書かないことにより、皆さんのプログラムミスをコンパイラがチェックできないということなんです。
プロトタイプ宣言の必要性
それでは、プロトタイプ宣言を復活してビルドしてみましょう。
#include <stdio.h>
// プロトタイプ宣言(復活)
long sub(long num1, long num2);
int main(void)
{
long sum;
// sub関数の呼び出し
sum = sub(100);
printf("%d", sum);
return 0;
}
long sub(long num1, long num2)
{
return num1 + num2;
}
main.c(11): error C2198: 'sub': 呼び出しに対する引数が少なすぎます。
========== ビルド: 0 正常終了、1 失敗、0 更新不要、0 スキップ ==========
このように、「sub関数の引数が少ないよ!」とエラーを通知してくれるようになりました。
つまり、
プロトタイプ宣言は、皆さんの関数呼び出しのプログラムミスを検出するための機能
なのです。
C言語におけるプロトタイプ宣言は、安全なプログラムを作るために欠かせない道具なんです。だから必要なんです!
コンパイラによるデータ型のチェック機構
コンパイラの校正機能には「型の整合性をチェックする」という重要な仕事があり、プログラマーが起こす型に関わるミスをコンパイラがチェックしています。
まず、コンパイラのデータ型チェックの基本を押さえましょう。
次のプログラムは、変数間における型の不整合をコンパイラがチェックするケースです。
#include <stdio.h>
int main(void)
{
char num1;
short num2 = 10000;
// short ==> char :警告出力
num1 = num2;
return 0;
}
main.c(9): warning C4244: '=': 'short' から 'char' への変換です。データが失われる可能性があります。
このように、コンパイラは型というものが正当に使われているかをチェックして、間違いや怪しいといった処理に対し警告かエラーを指摘します。
コンパイラは変数だけでなく、関数呼び出しに対しても型のチェックを行います。
プロトタイプ宣言によるコンパイラの型チェック機構
変数と同様に関数にも型があります。関数においても型チェック機構が働くのです。
関数の型について詳しく知りたい方は『関数ポインタ【ポインタを使って関数を呼ぶ仕組み解説】』の記事を読むとよいでしょう。
関数呼び出しにおいて正当な呼び出しとは、関数呼び出しの際の引数の個数や引数のデータ型が関数定義とずれていないかということです。
ただ、コンパイラが間違いかを判断するということは、裏を返せば正解を知っている必要があります。ここで登場するのが、プロトタイプ宣言なんです。
つまり、プロトタイプ宣言とは
コンパイラが、関数呼び出しが正当に行われているかをチェックするために、皆さんが『正解データを事前提供するためのもの』
なのです。
プロトタイプ宣言を書かないということは、関数呼び出しの正当性を皆さん自身が保証しなければならない、ということになります!
それをコンパイラに任せたいのであれば、プロトタイプ宣言をしっかりと書くことです。