web-dev-qa-db-ja.com

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

私はスクリプトのバックグラウンドから来ており、Cのプリプロセッサはいつもいように思えました。それでもなお、小さなCプログラムを書くことを学んでいるので、私はそれを受け入れてきました。私は自分の関数用に書いた標準ライブラリとヘッダーファイルを含めるためにプリプロセッサを実際に使用しています。

私の質問は、なぜCプログラマーがすべてのインクルードをスキップし、Cソースファイルを単純に連結してコンパイルしないのですか?すべてのインクルードを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私のMakefileでは、書くコードの量を減らすことができます。

これは、作成する関数ごとにヘッダーファイルを記述する必要がないことを意味します(既にメインソースファイルにあるため)。また、作成する各ファイルに標準ライブラリを含める必要もありません。これは私にとって素晴らしいアイデアのようです!

しかし、私はCが非常に成熟したプログラミング言語であることを認識しており、私よりもはるかに賢い誰かがすでにこの考えを持ち、それを使用しないことに決めたと想像しています。何故なの?

74
user3420382

あなたはcouldを行いますが、Cプログラムを個別のtranslation unitsに分離したいのは、主に次の理由によります:

  1. ビルドを高速化します。変更したファイルのみを再構築する必要があり、それらは他のコンパイル済みファイルとlinkedで最終プログラムを形成できます。

  2. C標準ライブラリは、事前にコンパイルされたコンポーネントで構成されています。すべてを再コンパイルする必要が本当にありますか?

  3. コードベースが異なるファイルに分割されている場合、他のプログラマとの共同作業が容易になります。

26
Bathsheba

.cファイルを連結するアプローチは完全に壊れています。

  • コマンドcat *.c > to_compile.cは、すべての関数を単一のファイルに入れます。順序が重要です:最初に使用する前に各関数を宣言する必要があります。

    つまり、特定の順序を強制する.cファイル間に依存関係があります。連結コマンドがこの順序に従わない場合、結果をコンパイルできません。

    また、相互に再帰的に使用する2つの関数がある場合、2つのうち少なくとも1つに対して前方宣言を記述する方法はまったくありません。同様に、それらの前方宣言を、人々がそれらを見つけることを期待するヘッダーファイルに入れることもできます。

  • すべてを1つのファイルに連結すると、プロジェクトの1行が変更されるたびに完全な再構築が強制されます。

    古典的な.c/.h分割コンパイルアプローチでは、関数の実装を変更するには1つのファイルのみを再コンパイルする必要がありますが、ヘッダーを変更するには実際にこのヘッダーを含むファイルを再コンパイルする必要があります。これにより、100倍以上の小さな変更(.cファイルの数に応じて)後の再構築を簡単にスピードアップできます。

  • 並列コンパイルのすべての機能を失いますすべてを単一のファイルに連結する場合。

    ハイパースレッディングを有効にした大きな12コアプロセッサをお持ちですか?残念ながら、連結されたソースファイルは単一のスレッドによってコンパイルされます。 20倍以上のスピードアップを失いました...これは極端な例ですが、make -j16すでに、そして私はあなたに言った、それは大きな違いを生むことができる。

  • コンパイル時間は通常not線形です。

    通常、コンパイラには、少なくとも2次ランタイム動作を行うアルゴリズムが含まれています。その結果、通常、集約されたコンパイルでは、独立した部分のコンパイルよりも実際に遅いしきい値があります。

    明らかに、このしきい値の正確な場所はコンパイラーとそれに渡す最適化フラグによって異なりますが、1つの巨大なソースファイルでコンパイラーが30分以上かかることがわかりました。 change-compile-testループにこのような障害を持ちたくありません。

間違いはありません:これらすべての問題がありますが、実際には.cファイルの連結を使用する人がいます。一部のC++プログラマーは、すべてをテンプレートに移動することでほぼ同じポイントに到達します(実装は.hppファイルと関連付けられた.cppファイルはありません)、プリプロセッサに連結を行わせます。これらの問題をどのように無視できるかはわかりませんが、実際はそうです。

また、これらの問題の多くは、プロジェクトのサイズが大きくなると明らかになることに注意してください。プロジェクトのコードが5000行未満の場合でも、コンパイル方法は比較的重要ではありません。しかし、50000行を超えるコードがある場合、インクリメンタルビルドとパラレルビルドをサポートするビルドシステムが必要です。 それ以外の場合、あなたはあなたの労働時間を無駄にしている。

16
cmaster
  • モジュール性により、コードを共有せずにライブラリを共有できます。
  • 大規模なプロジェクトの場合、1つのファイルを変更すると、プロジェクト全体がコンパイルされてしまいます。
  • 大きなプロジェクトをコンパイルしようとすると、メモリ不足が発生しやすくなります。
  • モジュールに循環依存関係がある場合がありますが、モジュール性はそれらを維持するのに役立ちます。

アプローチにはいくつかの利点がありますが、Cなどの言語の場合、各モジュールをコンパイルする方が理にかなっています。

16
Mohit Jain

ものを分割することは、優れたプログラム設計だからです。優れたプログラム設計とは、モジュール性、自律型コードモジュール、およびコードの再利用性です。結局のところ、プログラム設計を行うとき、常識はあなたを非常に遠くまで連れて行ってくれます。

無関係なコードを異なる翻訳単位に配置すると、変数と関数のスコープを可能な限りローカライズできます。

物事を一緒にマージすると、密結合が作成されます。これは、お互いの存在を知る必要さえないコードファイル間の厄介な依存関係を意味します。これが、プロジェクトのすべてのインクルードを含む「global.h」が悪いことである理由です。これは、プロジェクト全体のすべての無関係なファイル間に密接な結合を作成するためです。

車を制御するためのファームウェアを書いているとします。プログラムの1つのモジュールは、カーFMラジオを制御します。次に、別のプロジェクトでラジオコードを再利用して、スマートフォンでFMラジオを制御します。そして、ブレーキ、ホイール、ギアなどを見つけることができないため、ラジオコードはコンパイルされません。FMラジオにとって最も意味のないことはもちろん、スマートフォンも知っています。

さらに悪いことに、密結合があると、バグが存在するモジュールのローカルに留まるのではなく、プログラム全体でバグがエスカレートします。これにより、バグの結果はさらに深刻になります。 FMラジオコードにバグを書くと、突然車のブレーキが機能しなくなります。バグを含むアップデートでブレーキコードに触れていない場合でも。

1つのモジュールのバグが関連性のないものを完全に破壊する場合、それはほぼ間違いなくプログラムの設計が悪いためです。そして、貧弱なプログラム設計を達成する特定の方法は、プロジェクト内のすべてを1つの大きなBLOBにマージすることです。

15
Lundin

ヘッダーファイルはインターフェイスを定義する必要があります。これは従うべき慣習です。それらは、対応する.cファイルまたは.cファイルのグループにあるすべてを宣言することを意図したものではありません。代わりに、ユーザーが使用できる.cファイルですべての機能を宣言します。適切に設計された.hファイルは、たとえコメントが1つでなくても、.cファイル内のコードによって公開されたインターフェースの基本文書で構成されます。 Cモジュールの設計にアプローチする1つの方法は、最初にヘッダーファイルを記述し、次にそれを1つ以上の.cファイルに実装することです。

結果:.cファイルの実装の内部にある関数とデータ構造は、通常ヘッダーファイルに属しません。前方宣言が必要な場合がありますが、それらはローカルであり、宣言および定義されたすべての変数と関数はstaticである必要があります。これらがインターフェイスの一部でない場合、リンカーはそれらを参照しません。

11
Kuba Ober

主な理由はコンパイル時間です。変更時に小さなファイルを1つコンパイルすると、少し時間がかかる場合があります。ただし、単一行を変更するたびにプロジェクト全体をコンパイルする場合は、たとえば毎回10,000ファイルをコンパイルすることになり、これにはかなり時間がかかります。

上記の例のように、10,000個のソースファイルがあり、1つのソースファイルのコンパイルに10ミリ秒かかる場合、(10ミリ秒+リンク時間)でこの変更されたファイルのみをコンパイルすると、プロジェクト全体が(単一ファイルを変更した後)増分的にビルドされます。 (10ミリ秒* 10000 +短いリンク時間)すべてを単一の連結BLOBとしてコンパイルする場合。

8
Freddie Chopin

プログラムをモジュール方式で記述し、単一の翻訳単位としてビルドすることはできますが、すべてを逃すことになりますCがモジュール方式を実施するために提供するメカニズム。複数の翻訳ユニットを使用すると、モジュールのインターフェースを細かく制御できます。 externおよびstaticキーワード。

コードを単一の翻訳単位にマージすることで、コンパイラーがそれらについて警告しないため、モジュール性の問題を逃すことになります。大きなプロジェクトでは、最終的には意図しない依存関係が広がってしまいます。最終的に、他のモジュールでグローバルな副作用を作成せずにモジュールを変更する際に問題が発生します。

7

すべてのインクルードを1つの場所に配置すると、すべてのソースファイルではなく、必要なものを一度定義するだけで済みます。

それが.hファイルの目的です。したがって、必要なものを一度定義して、どこにでも含めることができます。一部のプロジェクトには、個々のeverything.hファイルをすべて含む.hヘッダーさえあります。したがって、proは個別の.cファイルでも実現できます。

つまり、作成する関数ごとにヘッダーファイルを作成する必要はありません[...]

とにかく、関数ごとに1つのヘッダーファイルを記述することは想定されていません。関連する一連の関数に対して1つのヘッダーファイルが必要です。したがって、conも無効です。

4
DepressedDaniel

これは、作成する関数ごとにヘッダーファイルを記述する必要がないことを意味します(既にメインソースファイルにあるため)。また、作成する各ファイルに標準ライブラリを含める必要もありません。これは私にとって素晴らしいアイデアのようです!

あなたが気づいた長所は、実際にはこれが時々小規模で行われる理由です。

大規模なプログラムの場合、実用的ではありません。前述の他の適切な回答のように、これはビルド時間を大幅に増やすことができます。

ただし、翻訳単位を小さなビットに分割するために使用できます。これにより、Javaのパッケージアクセシビリティを連想させる方法で機能へのアクセスが共有されます。

上記の方法には、ある程度の規律とプリプロセッサの助けが必要です。

たとえば、翻訳単位を2つのファイルに分割できます。

// a.c

static void utility() {
}

static void a_func() {
  utility();
}

// b.c

static void b_func() {
  utility();
}

次に、翻訳単位のファイルを追加します。

// ab.c

static void utility();

#include "a.c"
#include "b.c"

そして、あなたのビルドシステムはどちらもビルドしませんa.c または b.c、しかし代わりにab.o out of ab.c

ab.c達成?

単一の翻訳単位を生成する両方のファイルが含まれており、ユーティリティのプロトタイプを提供します。そのため、両方のコードがa.cおよびb.cは、含まれる順序に関係なく、関数がexternである必要なく、表示できます。

2
StoryTeller