分割コンパイルとリンク
ある程度の大きさがあるプログラムは、複数のモジュールに分けて作るのが通常です。その場合は分割コンパイル(各モジュールのソースファイルを別々にコンパイル)したあとで、最終的な実行ファイルにリンク(コンパイル結果を1つに結合)します。
例えば、「メイン」と「サブ」の2つのモジュールがあったとしましょう。「サブ」のソースファイル「sub.c」は、次のようになっています。
void Example(int n) { /* 関数の定義 */
……
}
Example()
という名前の関数が「定義」されていることが分かります。これを「メイン」から呼び出すことを考えてみましょう。こちらのソースファイルは「main.c」です。
#include <stdlib.h>
int main(void) {
Example(12345); /* エラー:いきなり「サブ」モジュールにある関数は呼び出せない! */
return EXIT_SUCCESS;
}
このように「サブ」にある関数をいきなり使おうとしても、コンパイルエラーになってしまいます。これは、関数がまだ「宣言」されていないためです。
宣言は、モジュールごとのヘッダーファイルに書くのが一般的です。「サブ」のヘッダーファイル「sub.h」は、次のようになっています。
#ifndef sub_h
#define sub_h
void Example(int n); /* 関数の宣言 */
#endif /* sub_h */
Example()
がどのような形の関数なのかが書かれていますね。これが宣言、もう少し詳しくいえば「プロトタイプ宣言」です。プロトタイプ宣言を見れば、引数の個数や型、戻り値の有無などが分かります。
では、「メイン」を修正してみましょう。「main.c」で次のようにヘッダーファイルをインクルードすれば、コンパイルエラーは発生しなくなります。
#include <stdlib.h>
#include "sub.h" /* 「サブ」モジュールから宣言を取り込む */
int main(void) {
Example(12345); /* 宣言があれば、「サブ」モジュールの関数でも呼び出せる */
return EXIT_SUCCESS;
}
これで分割コンパイルができるようになりました。ソースファイル「main.c」と「sub.c」をそれぞれコンパイルして、その結果をリンクすれば実行可能なファイルになるはずです。
プロトタイプ宣言の食い違い
ここで、今の状況を詳しく確認してみましょう。まずは「メイン」について。
- 「main.c」で関数
Example()
を使うために、「sub.h」をインクルードしています - 「sub.h」には、関数
Example()
のプロトタイプ宣言があります
次に「サブ」について。
- 「sub.c」に関数
Example()
の実体が定義されています - 「sub.c」では「sub.h」を使用していません
Example()
がどういう形の関数なのか「sub.h」を見て判断しているでしょう?
Example()
の定義(=実体)があるわけだから、宣言を見てもしょうがないですもんね。
関数Example()
は、「sub.h」で次のように宣言されています。
void Example(int n); /* 関数の宣言(引数がint型) */
もし、「sub.c」での定義が次のようになっていたら何が起こるでしょうか?
void Example(char n) { /* 関数の定義(引数がchar型) */
……
}
引数の型がint
とchar
で食い違っていますね。ところが、これでも分割コンパイルは可能です。「メイン」をコンパイルするときは「sub.h」をインクルードするだけなので「sub.c」を使わず、「サブ」をコンパイルするときは「sub.c」があれば十分なので「sub.h」を使いません。「sub.h」の宣言と「sub.c」の定義が、照合される機会がないのです。
さらに、このような状況でもC言語ではリンクまでできてしまいます。宣言と定義が食い違っているにもかかわらず、実行可能なファイルを作れるのです。
プロトタイプ宣言を定義と突き合わせる
リンクのミスは、コンパイルの時点で宣言と定義の食い違いを見過ごしてしまっているために起こります。コンパイルするときに両者を突き合わせれば、もっと確実に問題に気付くことができるでしょう。
そのためには、各モジュールから自分自身のヘッダーファイルをインクルードするのが簡単です。具体的には、「sub.c」から「sub.h」をインクルードするということです。すると、コンパイル時に宣言と定義が照合されて、正確に一致していない限りコンパイルエラーが発生するようになります。
#include "sub.h" /* 宣言を取り込んで突き合わせる */
void Example(int n) { /* 関数の定義 */
……
}