コンパイラは上から下へ読む
Cコンパイラはソースファイルを、最初の行から最後の行へ1パスで処理します。関数呼び出しに到達したとき、すでに3つのことを知っている必要があります。その関数が何を返すのか、引数をいくつ取るのか、そしてその型は何か。それがなければ、正しいコードを生成することも、呼び出しをチェックすることもできません。
ですから、これは問題です。
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* add はまだ見えていない */
return 0;
}
int add(int a, int b) {
return a + b;
}
解決策がプロトタイプです。関数のシグネチャを呼び出しより前に書き、本体が来る位置にセミコロンを置きます。
これで main を先頭に置けます。ほとんどのCファイルはこう構成されています。先頭のプロトタイプがそのファイルの提供するものを示し、main が全体の筋書きとして読め、細部はその下に続きます。
宣言と定義
混ざりやすく、区別しておく価値のある2つの言葉です。
- 宣言は関数が存在することを述べ、その型を与えます。セミコロンで終わり、本体を持ちません。好きなだけ何度でも書けます。
- 定義は本体を与えます。プログラム全体でちょうど一度だけ現れなければなりません - 同じ関数の定義が2つあればリンカエラー("multiple definition of")です。
int add(int a, int b); /* 宣言(プロトタイプ) */
int add(int a, int b) { return a + b; } /* 定義 - これも宣言である */
プロトタイプではパラメータ名は省略可能で、コンパイラにとって重要なのは型だけです。
int add(int, int); /* 合法で、等価 */
int add(int a, int b); /* より良い:名前が順序を説明する */
名前を書きましょう。void drawRect(int, int, int, int); は読み手に何も伝えませんが、void drawRect(int x, int y, int width, int height); はすべてを伝えます。
プロトタイプがもたらすもの
定義の順序を入れ替えられるようになるだけではありません。コンパイラに呼び出しをチェックさせるのがプロトタイプなのです。
プロトタイプがスコープにあれば、3 と 2 は呼び出し前に double へ変換されます。なければ int として積まれ、scale はそれを double として読むことになります - ゴミであり、古いCの方言では診断も出ません。
プロトタイプはまた、引数の個数の誤りや互換性のない型をコンパイル時に捕まえます。
scale(3.0); /* エラー:引数が少なすぎる */
scale(3.0, "two"); /* エラー:double が期待される場所に char * を渡している */
これらこそ、欲しいエラーです - 大きな声で、コンパイル時に、その行を指して。
implicit declaration のエラー
コンパイラが一度も聞いたことのない関数を呼ぶと、C言語で最もよく見るメッセージの1つが出ます。
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
C89では、コンパイラは推測していました。関数は int を返すものと仮定し、渡された引数は何でも受け入れる、というものです。この推測はたいてい外れており、本物の関数が double やポインタを返す場合、結果は意味をなしません。C99は暗黙の宣言を言語から取り除いたので、C99以降ではこれはエラーです - 最近のGCCやclangは、近年のバージョンでは既定で拒否します。
原因は2つ、対処も2つです。
自作の関数がまだ宣言されていない。 呼び出しの上にプロトタイプを追加するか、定義を前へ移動します。
ライブラリ関数のヘッダを忘れている。 ライブラリのプロトタイプはそのヘッダにあるので、#include が必要です。
printf, scanf, fopen -> #include <stdio.h>
malloc, free, exit, atoi -> #include <stdlib.h>
strlen, strcpy, strcmp -> #include <string.h>
sqrt, pow, sin, fabs -> #include <math.h>
isdigit, toupper -> #include <ctype.h>
bool, true, false -> #include <stdbool.h>
関連するメッセージ conflicting types for 'add' は、プロトタイプと定義が食い違っている - パラメータの型か戻り値の型が違う - という意味です。間違っているほうを直しましょう。両者は完全に一致しなければなりません。
相互再帰にはプロトタイプが要る
並べ替えでは解決できないこともあります。互いを呼び合う2つの関数は、両方を先頭に置くことはできません。
プロトタイプ1つが循環を断ち切ります。これは素朴な再帰関数が頼っているのと同じ仕組みです - 関数自身の名前はその本体の中でスコープにあるので、直接再帰にプロトタイプは不要ですが、相互再帰には必要なのです。
ヘッダファイルの中のプロトタイプ
プログラムが複数の .c ファイルにまたがるようになると、プロトタイプはヘッダへ移り、すべてのファイルが1か所から同じ宣言を見るようになります。
mathutils.h - インターフェース:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);
#endif
mathutils.c - 実装:
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int values[], int count) {
if (count == 0) return 0.0;
int total = 0;
for (int i = 0; i < count; i++) total += values[i];
return (double) total / count;
}
main.c - 利用側:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
そして:
gcc main.c mathutils.c -o program
ここで身につけたい習慣が2つあります。#ifndef / #define / #endif の包みはインクルードガードで、ヘッダが2つの経路からインクルードされたときに宣言が二重に処理されるのを防ぎます。そして mathutils.c が自分自身のヘッダをインクルードしているのは、冗長に見えて実はそうではありません。これによってコンパイラは、ほかのファイルが使う宣言とすべての定義を突き合わせてチェックします。両者がずれたら、リンク時ではなく即座に分かるのです。残りはヘッダファイルで扱います。
山かっこ(<stdio.h>)はシステムのインクルードパスを探し、引用符("mathutils.h")はまず自分のプロジェクトのディレクトリを探します。自作のヘッダには引用符を使いましょう。
f(void) と f()
これは本当に意外な話で、このドキュメントのすべての例が int main(void) と書いている理由でもあります。
void ping(void); /* 引数を取らない - 引数付きの呼び出しは拒否される */
void pong(); /* パラメータについて何も言っていない */
void ping(void); はプロトタイプです。この関数がパラメータを取らないと宣言するので、ping(1, 2, 3) はコンパイルエラーになります。
void pong(); は標準化以前のCから受け継いだ旧式の宣言です。戻り値の型だけを宣言し、パラメータについてはまったく何も言わないので、コンパイラは呼び出しをチェックできません - pong(1, 2, 3) は静かにコンパイルが通り、未定義の何かをします。
int main(void) { /* 正しい:main は引数を取らない */
int main() { /* 合法だが、引数チェックは切られている */
パラメータのない関数には常に (void) と書きましょう。C23は () を (void) と同じ意味に変え、ようやくこの罠をなくしますが、多くのコードと多くのコンパイラはまだそこに至っていませんし、(void) はどのC標準でも正しく動きます。
同じ区別は定義にも当てはまります。void ping(void) { } はプロトタイプ形式の定義ですが、void ping() { } はそうではなく、それより前に現れる呼び出しのチェックを有効にしません。
よくある間違い
- 定義にセミコロンを付ける。
int add(int a, int b); { return a + b; }はaddを宣言し、そのあとに迷子のブロックを残します。エラーメッセージは分かりにくいのに、原因はたった1文字です。 - プロトタイプのセミコロンを忘れる。 コンパイラはその後に続くものへ読み進み、何行も下で意味不明な報告をします。
- プロトタイプと定義が食い違う。
conflicting types for ...。両者を同一にしましょう - もっと良いのは、実装ファイルでヘッダをインクルードして、チェックを自動にすることです。 - 関数の中で関数を宣言する。 合法ですが(
mainの中のint add(int, int);)、その宣言はその関数だけにスコープが限られます。プロトタイプはファイルスコープに置きましょう。 - ヘッダで関数を定義する。 2つの
.cファイルからインクルードすると、リンカが重複定義を報告します。ヘッダは宣言を持ち、定義は.cファイルに置きます。 - チェックを
()に頼る。 チェックはされません。(void)と書きましょう。
よくある質問
C言語の関数プロトタイプとは何ですか?
関数のシグネチャ - 戻り値の型、名前、パラメータの型 - を、本体の代わりにセミコロンで終える宣言のことです:int add(int a, int b);。関数が何をするかは言わずに、どう呼ばれるかをコンパイラに伝えるので、呼び出しをチェックできるようになります。
C言語の宣言と定義の違いは何ですか?
宣言は名前と型を導入し(int add(int, int);)、何度現れても構いません。定義は本体を与え(int add(int a, int b) { return a + b; })、プログラム全体でちょうど一度だけ現れなければなりません。すべての定義は宣言でもあります。
C言語の "implicit declaration of function" を直すには?
呼び出しの前にプロトタイプを追加します。自作の関数ならファイルの先頭かヘッダに 戻り値の型 名前(引数の型); を置き、ライブラリ関数なら正しいヘッダをインクルードします - printf には #include <stdio.h>、malloc には <stdlib.h>、strlen には <string.h>、sqrt には <math.h>。
C言語の f(void) と f() の違いは何ですか?
void f(void); は引数をまったく取らない関数を宣言し、引数を渡す呼び出しはコンパイラに拒否されます。void f(); は旧式の宣言で、パラメータについて何も言わないため、引数チェックが切られます。常に (void) と書きましょう。C23でようやく両者は同じ意味になりますが、古いコードやコンパイラでは依然として違います。