#複数のファイルプロジェクト

導入

ソフトウェア プロジェクトには、多くの場合、複数のソース コード ファイルが含まれており、コンパイル時にこれらのファイルをまとめてコンパイルして実行可能ファイルを生成する必要があります。

プロジェクトに 2 つのソース コード ファイル foo.cbar.c があるとします。ここで、foo.c はメイン ファイル、bar.c はライブラリ ファイルです。いわゆる「メイン ファイル」は、ライブラリ ファイルによって定義されたさまざまな関数を参照する main() 関数を含むプロジェクト エントリ ファイルです。

// ファイル foo.c
#include <stdio.h>

int main(void) {
  printf("%d\n", add(2, 3)); // 5!
}

上記のコードでは、メイン ファイル foo.c が、ライブラリ ファイル bar.c で定義されている関数 add() を呼び出します。

// ファイル bar.c

int add(int x, int y) {
  x + y を返します。
}

次に、これら 2 つのファイルを一緒にコンパイルします。

$ gcc -o foo foo.c bar.c

# より簡単な書き方
$ gcc -o foo *.c

上記のコマンドでは、gcc の -o パラメータは、生成されるバイナリ実行可能ファイルのファイル名を指定します。この場合は foo です。

このコマンドを実行すると、コンパイラは警告を発行します。これは、コンパイラが foo.c のコンパイル中に、この関数のプロトタイプが foo.c に存在しないことを検出したためです。 c または定義。したがって、foo.cを変更し、add()`のプロトタイプをファイルの先頭に追加するのが最善です。

// ファイル foo.c
#include <stdio.h>

int add(int, int);

int main(void) {
  printf("%d\n", add(2, 3)); // 5!
}

再コンパイル時に警告が表示されなくなりました。

複数のファイルがこの関数 add() を使用する場合、各ファイルは関数プロトタイプを追加する必要があるとすぐに考えるかもしれません。 add()関数を一度変更(パラメータの数を変更するなど)する必要があると、ファイルを一つ一つ変更する必要があり非常に面倒です。したがって、通常のアプローチは、特別なヘッダー ファイル bar.h を作成し、定義されているすべての関数のプロトタイプを bar.c に配置することです。

// ファイル bar.h

int add(int, int);

次に、include コマンドを使用して、この関数を使用するソース コード ファイル内のヘッダー ファイル bar.h をロードします。

// ファイル foo.c

#include <stdio.h>
#include "bar.h"

int main(void) {
  printf("%d\n", add(2, 3)); // 5!
}

上記のコードでは、#include "bar.h" はヘッダー ファイル bar.h を追加することを意味します。このファイルは山かっこで囲まれていません。これは、ファイルがユーザーによって提供されたことを示します。これは、ファイルが現在のソース コード ファイルと同じディレクトリにあることを意味します。

次に、コンパイラが関数プロトタイプが関数定義と一致しているかどうかを検証できるように、このヘッダー ファイルを bar.c にロードするのが最善です。

// ファイル bar.c
#include "bar.h"

int add(int a, int b) {
  a + b を返します。
}

ここで再コンパイルすると、バイナリ実行可能ファイルを正常に取得できます。

$ gcc -o foo foo.c bar.c

ロードを繰り返す

ヘッダー ファイルには他のヘッダー ファイルもロードできるため、繰り返しロードが発生する可能性があります。たとえば、a.hb.h の両方が c.h をロードし、次に foo.ca.hb.h を同時にロードします。これは、foo.cc.h を 2 回コンパイルすることを意味します。 。

この種の繰り返しロードを避けるのが最善ですが、同じ関数プロトタイプが複数回定義されてもエラーは報告されませんが、同じ Struct データ構造を複数回再定義するなど、一部のステートメントが再利用されるとエラーが報告されます。繰り返しのロードを解決する一般的な方法は、ヘッダー ファイルに特別なマクロを設定することです。ロード中にそのマクロが存在することが判明すると、現在のファイルはロードされなくなります。

// ファイル bar.h
#ifndef BAR_H
  #BAR_H を定義
  int add(int, int);
#endif

上記の例では、ヘッダファイルbar.h#ifndef#endifを使って条件判定を設定しています。このヘッダファイルがロードされるたびに、マクロ BAR_H が設定されているかどうかの判定が行われます。設定されている場合は、ヘッダー ファイルがロードされ、再度ロードされないことを意味します。それ以外の場合は、最初にマクロを設定してから、関数プロトタイプをロードします。

外部指定子

現在のファイルは、他のファイルで定義された変数を使用することもできます。この場合、extern 指定子を使用して、この変数が他のファイルで定義されていることを現在のファイルで宣言する必要があります。

extern int myVar;

上記の例では、extern 指定子は、変数 myvar が別のスクリプト ファイルで宣言されており、ここでその変数にメモリ領域を割り当てる必要がないことをコンパイラに伝えます。

メモリ空間を割り当てる必要がないため、 extern で配列を宣言するときに配列の長さを指定する必要はありません。

extern int a[];

この種の共有変数の宣言は、ソース コード ファイルに直接記述することも、ヘッダー ファイルに配置して #include ディレクティブを通じてロードすることもできます。

静的指定子

通常の状況では、現在のファイル内のグローバル変数は他のファイルで使用できます。場合によっては、このようなことは望ましくありませんが、変数を現在のファイル内でのみ使用し、他のファイルからは参照したくない場合があります。

このとき、変数を宣言するときに static キーワードを使用して、その変数を現在のファイルのプライベート変数にすることができます。

静的 int foo = 3;

上記の例では、変数 foo は現在のファイルでのみ使用でき、他のファイルからは参照できません。

コンパイル戦略

複数のソース コード ファイルを含むプロジェクトの場合、コンパイル中にすべてのファイルを一緒にコンパイルする必要があります。たとえ 1 行変更するだけでも、最初からコンパイルする必要があり、非常に時間がかかります。

時間を節約するために、コンパイルを 2 つのステップに分割するのが一般的です。最初のステップは、GCC の -c パラメータを使用して、各ソース コード ファイルをオブジェクト ファイルにコンパイルすることです。 2 番目のステップでは、すべてのオブジェクト ファイルがリンクされ、結合されてバイナリ実行可能ファイルが生成されます。

$ gcc -c foo.c # foo.o を生成する
$ gcc -c bar.c # bar.o を生成する

# より簡単な書き方
$ gcc -c *.c

上記のコマンドはソース コード ファイル foo.c および bar.c であり、オブジェクト ファイル foo.o および bar.o をそれぞれ生成します。

オブジェクト ファイルは実行可能ファイルではなく、コンパイル プロセス中に段階的に生成されるファイル名はソース コード ファイルと同じですが、サフィックス名は .o になります。

すべてのオブジェクト ファイルを取得したら、再度 gcc コマンドを使用してそれらをリンクおよびマージし、実行可能ファイルを生成します。

$ gcc -o foo foo.o bar.o

# より簡単な書き方
$ gcc -o foo *.o

今後、ソース ファイルが変更された場合、このファイルはオブジェクト ファイルに再コンパイルされます。他のファイルは再コンパイルする必要はありません。元のオブジェクト ファイルを引き続き使用し、最終的にすべてのオブジェクト ファイルを再リンクできます。リンクはコンパイルよりもはるかに短い時間がかかるため、時間を大幅に節約できます。

コマンドを作成する

大規模なプロジェクトのコンパイルを手動で完了すると、非常に面倒でエラーが発生しやすくなります。一般に、make などの特別な自動コンパイル ツールが使用されます。

make は、使用時に現在のディレクトリ内で構成ファイル makefile (Makefile と書くこともできます) を自動的に検索するコマンド ライン ツールです。このファイルはすべてのコンパイル ルールを定義し、各コンパイル ルールはコンパイル製品に対応します。このコンパイルされた製品を取得するには、2 つのことを知る必要があります。

  • 依存関係 (コンパイルされた製品を生成するためにどのファイルが必要か) ・ビルドコマンド(コンパイル済みの製品を生成するコマンド)

たとえば、オブジェクト ファイル foo.o はコンパイル製品であり、その依存関係は foo.c であり、生成コマンドは gcc -c foo.c です。対応するコンパイル ルールは次のとおりです。

foo.o: foo.c
  gcc -c foo.c

上の例では、コンパイル ルールは 2 行で構成されています。最初の行はコンパイルされた製品で、コロンの後にその依存関係が続きます。2 行目はビルド コマンドです。

2 行目のインデントにはタブ キーを使用する必要があることに注意してください。スペース バーを使用すると、エラーが報告されます。

完全な構成ファイル makefile は複数のコンパイル ルールで構成され、次のようになります。

foo: foo.o bar.o
  gcc -o foo foo.o bar.o

foo.o: bar.h foo.c
  gcc -c foo.c

bar.o: bar.h bar.c
  gcc -c bar.c

上記はメイクファイルの例です。これには、3 つのコンパイル製品 (foo.obar.o、および foo) に対応する 3 つのコンパイル ルールが含まれており、各コンパイル ルールは空白行で区切られます。

makefile では、コンパイル時に make コマンドの後にコンパイル対象 (コンパイル製品の名前) を指定すると、対応するコンパイル ルールが自動的に呼び出されます。

$ make foo.o

# または
$ makebar.o

# または
$ フーを作る

上記の例では、make コマンドはさまざまなコマンドに基づいてさまざまなコンパイル製品を生成します。

コンパイル ターゲットが省略された場合、make コマンドは最初のコンパイル ルールを実行し、対応する製品をビルドします。

$make

上の例では、make の後にコンパイル ターゲットがないため、makefile の最初のコンパイル ルール (この場合は make foo) が実行されます。ユーザーは「make」の実行後に最終的な実行可能ファイルを取得することを期待しているため、最終的な実行可能ファイルのコンパイル規則を常に makefile の最初の行に置くことをお勧めします。 Makefile 自体には、コンパイル ルールの順序要件はありません。

make コマンドの利点は、コマンドが実行されるたびにコンパイルを行うのではなく、再コンパイルが必要かどうかをチェックすることです。具体的な方法は、各ソース コード ファイルのタイムスタンプをチェックして、最後のコンパイル以降にどのファイルが変更されたかを判断することです。次に、影響を受けるコンパイル製品を再コンパイルします (つまり、コンパイル製品は、変更されたソース コード ファイルに直接または間接的に依存します)。影響を受けないコンパイル製品は再コンパイルされません。

たとえば、最後のコンパイル以降、foo.c は変更されましたが、bar.cbar.h は変更されていません。したがって、make foo コマンドを再実行すると、Make は bar.cbar.h が変更されていないことを検出するため、bar.o を再コンパイルする必要はなく、foo.o' のみを再コンパイルする必要があります。 を再コンパイルする必要があります。新しい foo.o を取得したら、それを bar.o とともに再コンパイルして、新しい実行可能ファイル foo を作成します。

Make の設計の最大の利点は、コンパイル プロセスを自動的に処理し、変更されたファイルのみを再コンパイルするため、時間を大幅に節約できることです。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0