1. ホーム
  2. c

コンパイル前にC言語のソースファイルを連結しないのはなぜですか?重複

2023-12-17 05:57:21

質問

私はスクリプトのバックグラウンドを持ち、C言語のプリプロセッサはいつも醜いものに見えていました。しかし、小さな C プログラムを書くことを学ぶために、私はそれを受け入れています。私は、標準ライブラリと私自身の関数のために書いたヘッダー ファイルを含めるためにのみ、プリプロセッサを実際に使用しています。

私の疑問は、なぜ C プログラマはすべてのインクルードをスキップして、単に C ソースファイルを連結してコンパイルしないのでしょうか?すべてのインクルードを 1 つの場所に置くと、すべてのソース ファイルで定義するのではなく、必要なものを 1 回だけ定義すればよいでしょう。

ここで、私が説明していることの例を示します。ここでは3つのファイルを持っています。

// includes.c
#include <stdio.h>

// main.c
int main() {
    foo();
    printf("world\n");
    return 0;
}

// foo.c
void foo() {
    printf("Hello ");
}

のようにすることで cat *.c > to_compile.c && gcc -o myprogram to_compile.c のようにすることで、書くコードの量を減らすことができます。

これは、作成する関数ごとにヘッダーファイルを書く必要がないことを意味します(なぜなら、それらはすでにメインソースファイルにあるからです)。これは私にとって素晴らしいアイデアのように思えます。

しかし、C 言語は非常に成熟したプログラミング言語であり、私よりもずっと賢い他の誰かがすでにこのアイデアを持ち、それを使用しないことに決めたと想像しています。なぜそうしないのでしょうか?

どのように解決するのですか?

そのように作られているソフトウェアもあります。

典型的な例としては SQLite . としてコンパイルされることもあります. アマルガム (としてコンパイルされることもあります(多くのソースファイルからビルド時に行われます)。

しかし、そのアプローチには長所と短所があります。

明らかに、コンパイル時間はかなり増加します。ですから、滅多にそのようなものをコンパイルしない場合にのみ、実用的です。

おそらく、コンパイラはもう少し最適化するかもしれません。しかし、リンク時の最適化で(たとえば 最近の GCC を使用している場合、コンパイルとリンクは gcc -flto -O2 でコンパイルしリンクします)、同じ効果を得ることができます(もちろん、ビルド時間の増加を犠牲にして)。

各関数のヘッダーファイルを書く必要がない

それは間違ったアプローチです(関数ごとに1つのヘッダーファイルを持つという)。一人用のプロジェクト (コード行数が 10 万行未満、別名 KLOC = kilo line of コード ) では、少なくとも小さなプロジェクトでは、ヘッダファイルとして シングル 共通ヘッダーファイル (これは プリコンパイル を使用する場合 GCC を使用している場合)、すべてのパブリック関数と型の宣言が含まれ、おそらくは 定義 static inline 関数(十分に小さく、十分に頻繁に呼び出されるものであれば インライン化 ). 例えば sash シェル はそのように構成されています (そして lout フォーマッタ で、52KLOC)。

また、いくつかのヘッダーファイルがあり、おそらくいくつかの単一の "grouping"ヘッダーがあることでしょう。 #include -というヘッダがあるかもしれません(そしてそれはプリコンパイルできます)。例えば jansson (これは実際には単一の 公開 ヘッダーファイルがあります)と GTK (これは があります。 の内部ヘッダを持ちますが、これを使うほとんどのアプリケーションは があります。 を一つ持つだけです。 #include <gtk/gtk.h> で、その中にすべての内部ヘッダが含まれます)。反対側では POSIX は非常に多くのヘッダーファイルを持っており、どのファイルをどの順番でインクルードすべきかを文書化しています。

多くのヘッダーファイルがあることを好む人もいます (そして、単一の関数宣言をそれ自身のヘッダーに置くことを好む人さえいます)。私はそうしませんが (個人的なプロジェクトや、2、3 人でコードをコミットするような小さなプロジェクトでは)。 ということである . ところで、プロジェクトが大きくなると、ヘッダーファイルのセット(および翻訳ユニット)が大幅に変更されることがよくあります。また REDIS を見てみてください (これには 139 の .h ヘッダファイル、214個の .c ファイル、つまり合計126 KLOCの翻訳ユニット)。

1 つまたは複数の 翻訳ユニット を持つかどうかは、好みの問題でもあります (そして、利便性や習慣、慣習の問題でもあります)。私の好みは、あまり小さすぎないソースファイル(つまり翻訳ユニット)を持つことで、通常それぞれ数千行になり、(60 KLOC 未満の小さなプロジェクトでは)共通の単一のヘッダーファイルがあることがよくあります。その際、いくつかの ビルド オートメーション のようなツールを使うことを忘れないでください。 GNU メイク (多くの場合 並列 を通して構築します。 make -j というように いくつか のコンパイルプロセスを同時に実行することになります)。このようなソースファイル構成をとることの利点は、コンパイルがそれなりに速くできることです。ところで、場合によっては メタプログラミング のアプローチは価値があります。(内部ヘッダや翻訳ユニット)C の "source" ファイルのいくつかは、以下のようにすることができます。 を生成します。 を他の何かによって生成することができます (たとえば AWK のような特殊な C プログラム。 バイソン のような特殊なプログラム、または独自のもの)。

C 言語は 1970 年代に、現在愛用しているノートパソコンよりもはるかに小さく遅いコンピュータのために設計されたことを思い出してください (通常、当時のメモリはせいぜい 1 メガバイト、あるいは数百キロバイトで、コンピュータは現在の携帯電話よりも少なくとも 1000 倍は遅かったのです)。

私は次のことを強くお勧めします。 ソースコードを研究し、いくつかの 既存の フリーソフトウェア プロジェクト (例: GitHub または ソースフォージ またはあなたの好きなLinuxディストリビューション)。それらが異なるアプローチであることを学ぶことができます。以下のことを忘れないでください。 であり、C 規約 習慣 実践で重要なのは ということで があります。 異なる でプロジェクトを整理する方法があります。 .c.h ファイル . について読む Cプリプロセッサー .

<ブロッククオート

また、作成する各ファイルに標準ライブラリをインクルードする必要がないことも意味します。

ライブラリではなくヘッダファイルをインクルードする(ただし リンク ライブラリ) を使用することができます。しかし、あなたはそれらをそれぞれの .c ファイルに含めることもできますし(多くのプロジェクトがそうしています)、一つのヘッダに含めてそのヘッダをプリコンパイルすることもできますし、何十ものヘッダを用意して、それぞれのコンパイルユニットでシステムヘッダの後に含めることもできます。 YMMVです。 プリプロセスの時間は今日のコンピュータでは速いことに注意してください(少なくとも、コンパイラに最適化を依頼する場合、最適化はパース&amp; プリプロセスよりも時間がかかるので)。

ある #include -d ファイルに入るものは 従来の (であり、C の仕様では定義されていません)。プログラムによっては、コードの一部がこのようなファイル (これはヘッダと呼ばれるべきではなく、単にインクルードファイルと呼ばれるべきです) に含まれており、そのファイルには .h のようなサフィックスではなく、他の何か .inc ). 例えば XPM ファイルを見てください。もう一方の極端な例では、原則としてあなた自身のヘッダファイルを持たないかもしれません (それでも実装からのヘッダファイルが必要です。 <stdio.h><dlfcn.h> にコピー&ペーストし、重複するコードを .c ファイルにコピー&ペーストします。 int foo(void); という行を各 .c ファイルを作成することがありますが、これは非常に悪い習慣であり、嫌われます。しかし、いくつかのプログラムは を生成しています。 C ファイルを生成して、いくつかの共通のコンテンツを共有しています。

ところで、CやC++14には(OCamlが持っているような)モジュールがありません。言い換えれば、C言語ではモジュールはほとんど 規約 .

(何千もの 非常に小さい .h そして .c のファイルがそれぞれ数十行しかない場合、ビルド時間が劇的に遅くなることがあります。数百行ずつのファイルを数百個持つ方が、ビルド時間という点では合理的です)。

C言語で一人プロジェクトを始める場合、まずヘッダーファイルを1つ(そしてそれをプリコンパイルする)、そしていくつかの .c の翻訳ユニットを用意することをお勧めします。実際には .c ファイルよりもはるかに頻繁に .h のものよりもはるかに多いのです。10KLOCを超えたら、それをいくつかのヘッダーファイルにリファクタリングするかもしれません。このようなリファクタリングは、設計は難しいですが、実行は簡単です(コードの塊をたくさんコピー&ペーストするだけです)。他の人は違う提案やヒントをくれるでしょう(それでいいんです!)。しかし、コンパイル時にすべての警告とデバッグ情報を有効にすることを忘れないでください(だから、コンパイル時に gcc -Wall -g でコンパイルし、おそらく CFLAGS= -Wall -g を設定します。 Makefile ). を使用します。 gdb デバッガ(および バルグラインド ...). 最適化を依頼する ( -O2 )を求める。また、以下のようなバージョン管理システムを使用する。 Git .

逆に、より大きなプロジェクトを設計している場合、その上で 数人 が働くような大きなプロジェクトを設計している場合は、複数のファイル、たとえ複数のヘッダーファイルがあったほうがよいでしょう (直感的には、各ファイルには主に担当する一人の人がいて、他の人はそのファイルに少し貢献する)。

コメントで、あなたは付け加えます。

自分のコードをたくさんの異なるファイルに書き、それらを連結するために Makefile を使うことについて話しています。

なぜそれが有用なのかわかりません (非常に奇妙なケースを除いて)。各翻訳ユニット (例えば、それぞれの .c ファイル) をその オブジェクトファイル (a .o エルフ ファイル(Linux)と リンク を追加することができます。これは make を使うのは簡単です (実際には、1つの .c を変更する場合、そのファイルだけがコンパイルされ、インクリメンタルビルドは本当に速くなります)。 並列 を使って make -j (を使えば、マルチコアプロセッサでビルドがとても速くなります)。