web-dev-qa-db-ja.com

なぜcppファイルを含めず、代わりにヘッダーを使用する必要があるのですか?

だから私は最初のC++プログラミングの割り当てを終え、成績を受け取りました。しかし、グレーディングによると、including cpp files instead of compiling and linking themのマークを失いました。私はそれが何を意味するのかあまり明確ではありません。

コードを振り返って、クラス用のヘッダーファイルを作成しないことを選択しましたが、cppファイル内のすべてを実行しました(ヘッダーファイルがなくても正常に動作するようです...)。私は、グレーダーが「#include "mycppfile.cpp";」を書いたことを意味していると推測しています。私のファイルのいくつかで。

Cppファイルを#include 'する理由は次のとおりでした。 、他のヘッダーファイルがファイルに#include 'dされているのを見たので、cppファイルにも同じことをしました。

それで私は正確に何を間違えたのか、そしてそれはなぜ悪いのか?

129
ialm

私の知る限り、C++標準はヘッダーファイルとソースファイルの違いを認識していません。言語に関する限り、法的コードを含むテキストファイルは他のテキストファイルと同じです。ただし、違法ではありませんが、ソースファイルをプログラムに含めると、最初にソースファイルを分離することによる利点がほとんどなくなります。

本質的に、#includeは、指定したファイル全体をプリプロセッサに伝え、compilerが手に入る前にアクティブなファイルにコピーするように指示しますその上。したがって、プロジェクトにすべてのソースファイルを一緒に含めると、これまでに行ったことと、まったく分離せずに1つの巨大なソースファイルを作成することとの間に基本的な違いはありません。

「ああ、それは大したことではない。それが実行される場合、それは問題ありません」私はあなたが泣くのを聞きます。そしてある意味では、あなたは正しいでしょう。しかし、今は、ごく小さな小さなプログラムと、それをコンパイルするためのニースで比較的邪魔にならないCPUを扱っています。あなたはいつもそれほど幸運ではありません。

本格的なコンピュータープログラミングの領域を掘り下げると、数十個ではなく数百万個に達する可能性のある行数のプロジェクトが表示されます。それはたくさんの行です。また、これらのいずれかを最新のデスクトップコンピューターでコンパイルしようとすると、数秒ではなく数時間かかる場合があります。

「ああ、いや!恐ろしいですね!しかし、この悲惨な運命を防ぐことはできますか?!」残念ながら、それについてできることはあまりありません。コンパイルに時間がかかる場合、コンパイルに時間がかかります。しかし、それは本当に重要なのは初めてです-一度コンパイルしたら、再度コンパイルする必要はありません。

何かを変更しない限り。

200万行のコードを1つの巨大な巨大なものにマージし、x = y + 1などの単純なバグ修正を行う必要がある場合、200万行すべてを再度コンパイルする必要があります。これをテストします。そして、代わりにx = y - 1を実行するつもりであることがわかった場合、再び200万行のコンパイルが待っています。それは何時間も無駄になり、他の何かをするのに費やすことができます。

「しかし、非生産的であることは嫌いです!コンパイルコードベースの個別の部分を個別に、そしてどういうわけかリンク後で一緒に!! =理論的には優れたアイデア。しかし、プログラムが別のファイルで何が起こっているかを知る必要がある場合はどうでしょうか?代わりに多数の小さな.exeファイルを実行する場合を除き、コードベースを完全に分離することは不可能です。

"しかし、それは確かに可能でなければなりません!それ以外の場合、プログラミングは純粋な拷問のように聞こえます!interface from implementation?これらの個別のコードセグメントから十分な情報を取得して、プログラムの残りの部分でそれらを識別し、代わりに何らかの種類のheaderファイルに入れますか?その方法で、#includepreprocessor directiveを使用してコンパイルするのに必要な情報だけを持ち込んでください! "

うーん。あなたはそこに何かがあるかもしれません。それがあなたのためにどのように機能するか教えてください。

156
goldPseudo

これはおそらくあなたが望んでいたよりも詳細な答えですが、まともな説明が正当化されると思います。

CおよびC++では、1つのソースファイルは1つのtranslation unitとして定義されます。慣例により、ヘッダーファイルには関数宣言、型定義、およびクラス定義が保持されます。実際の関数実装は、翻訳単位、つまり.cppファイルにあります。

この背後にある考え方は、関数とクラス/構造体のメンバー関数が一度コンパイルおよびアセンブルされ、他の関数が重複することなくその場所からそのコードを呼び出すことができるということです。関数は暗黙的に「外部」として宣言されます。

/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);

/* function body, or function definition. */
int add(int a, int b) 
{
   return a + b;
}

関数を翻訳単位に対してローカルにする場合は、「静的」として定義します。これは何を意味するのでしょうか?つまり、ソースファイルをextern関数に含めると、コンパイラが同じ実装に複数回遭遇するため、再定義エラーが発生します。そのため、すべての翻訳単位で関数宣言を参照し、関数本体を参照しないようにします。

では、最後にどのようにすべてをマッシュアップするのでしょうか?それがリンカの仕事です。リンカーは、アセンブラーステージによって生成されたすべてのオブジェクトファイルを読み取り、シンボルを解決します。先ほど言ったように、シンボルは単なる名前です。たとえば、変数または関数の名前。関数を呼び出したり型を宣言したりする翻訳単位がそれらの関数や型の実装を知らない場合、それらのシンボルは未解決と言われます。リンカは、未定義のシンボルを保持する変換ユニットと実装を含むものを接続することにより、未解決のシンボルを解決します。ふう。これは、コードに実装されているか、追加のライブラリによって提供されているかにかかわらず、外部から見えるすべてのシンボルに当てはまります。ライブラリは、実際には再利用可能なコードを備えた単なるアーカイブです。

2つの注目すべき例外があります。まず、小さな関数がある場合は、インラインにすることができます。これは、生成されたマシンコードはextern関数呼び出しを生成せず、文字通りインプレースで連結されることを意味します。通常は小さいため、サイズのオーバーヘッドは問題になりません。それらが機能する方法で静的であると想像できます。そのため、ヘッダーにインライン関数を実装しても安全です。クラスまたは構造体定義内の関数実装も、コンパイラによって自動的にインライン化されることがよくあります。

他の例外はテンプレートです。コンパイラーは、それらをインスタンス化するときにテンプレート型定義全体を確認する必要があるため、スタンドアロン関数または通常のクラスのように定義から実装を分離することはできません。さて、おそらくこれは今では可能かもしれませんが、「export」キーワードに対する広範なコンパイラーのサポートを得るには長い時間がかかりました。そのため、「エクスポート」のサポートなしで、翻訳ユニットは、インライン関数が機能する方法と同様に、インスタンス化されたテンプレート型と関数のローカルコピーを取得します。 「エクスポート」のサポートにより、これは当てはまりません。

2つの例外について、一部の人々は、インライン関数、テンプレート関数、およびテンプレート型の実装を.cppファイルに配置し、.cppファイルを#includeする方が「賢い」と感じています。これがヘッダーであるかソースファイルであるかは実際には関係ありません。プリプロセッサは気にせず、単なる慣習です。

C++コード(いくつかのファイル)から最終的な実行可能ファイルまでのプロセス全体の簡単な要約:

  • プリプロセッサが実行され、「#」で始まるすべてのディレクティブが解析されます。 #includeディレクティブは、たとえば、インクルードされたファイルを下位と連結します。また、マクロの置換とトークンの貼り付けも行います。
  • 実際のコンパイラは、プリプロセッサステージの後に中間テキストファイルで実行され、アセンブラコードを出力します。
  • アセンブラはアセンブリファイルで実行され、マシンコードを出力します。これは通常オブジェクトファイルと呼ばれ、問題のオペレーティングシステムのバイナリ実行可能形式に従います。たとえば、WindowsはPE(ポータブル実行可能形式)を使用しますが、LinuxはGNU拡張子を持つUnix System V ELF形式を使用します。この段階では、シンボルは未定義としてマークされます。
  • 最後に、リンカーが実行されます。前のステージはすべて、各翻訳単位で順番に実行されました。ただし、リンカステージは、アセンブラによって生成されたすべての生成されたオブジェクトファイルに対して機能します。リンカはシンボルを解決し、ターゲットプラットフォームとバイナリ形式に依存するセクションとセグメントの作成のような多くの魔法を実行します。プログラマはこれを一般的に知る必要はありませんが、場合によっては確実に役立ちます。

繰り返しになりますが、これはあなたが求めていたよりも明確に多くのことでしたが、重要な詳細が大きな画像を見るのに役立つことを願っています。

43
Mads Elvheim

典型的な解決策は、.h宣言のみのファイルと.cpp実装用ファイル。実装を再利用する必要がある場合は、対応する.hファイルを.cpp必要なクラス/関数/その他が使用され、既にコンパイルされた.cppファイル(.objファイル-通常は1つのプロジェクト内で使用されます-または.libファイル-通常は複数のプロジェクトから再利用するために使用されます)。この方法では、実装のみが変更された場合にすべてを再コンパイルする必要はありません。

9
sharptooth

Cppファイルをブラックボックスと考え、.hファイルをそれらのブラックボックスの使用方法に関するガイドと考えてください。

Cppファイルは事前にコンパイルできます。コンパイルするたびにプログラムにコードを実際に「インクルード」する必要があるため、#includeそれらでは機能しません。ヘッダーを含めるだけの場合、ヘッダーファイルを使用して、プリコンパイル済みcppファイルの使用方法を決定できます。

これは最初のプロジェクトにとって大きな違いにはなりませんが、大規模なcppプログラムを書き始めると、コンパイル時間が爆発するため、人々はあなたを嫌います。

これも読んでください: ヘッダーファイルインクルードパターン

6
Dan McGrath

通常、ヘッダーファイルには関数/クラスの宣言が含まれていますが、.cppファイルには実際の実装が含まれています。コンパイル時に、各.cppファイルはオブジェクトファイル(通常は拡張子.o)にコンパイルされ、リンカーはさまざまなオブジェクトファイルを最終的な実行可能ファイルに結合します。通常、リンクプロセスはコンパイルよりもはるかに高速です。

この分離の利点:プロジェクトの.cppファイルの1つを再コンパイルする場合、他のすべてを再コンパイルする必要はありません。その特定の.cppファイルの新しいオブジェクトファイルを作成するだけです。コンパイラは他の.cppファイルを見る必要はありません。ただし、他の.cppファイルに実装されている現在の.cppファイルの関数を呼び出したい場合は、コンパイラーに引数を指定する必要があります。これがヘッダーファイルを含める目的です。

短所:指定された.cppファイルをコンパイルするとき、コンパイラは他の.cppファイルの中身を「見る」ことができません。そのため、そこにある機能がどのように実装されているのかがわからず、その結果、積極的に最適化することができません。しかし、あなたはまだそれについて心配する必要はないと思います(:

6
int3

ヘッダーのみが含まれ、cppファイルはコンパイルのみされるという基本的な考え方。これは、多くのcppファイルがあれば便利になり、そのうちの1つだけを変更すると、アプリケーション全体を再コンパイルするのが遅くなります。または、ファイル内の機能が相互に依存して開始するとき。そのため、クラス宣言をヘッダーファイルに分離し、cppファイルに実装を残し、Makefile(または使用しているツールに応じて何か)を記述してcppファイルをコンパイルし、結果のオブジェクトファイルをプログラムにリンクする必要があります。

5

プログラム内の他のいくつかのファイルにcppファイルを#includeすると、コンパイラはcppファイルを複数回コンパイルしようとし、同じメソッドの複数の実装があるためエラーを生成します。

#included cppファイルを編集し、そのファイルを含むファイルを強制的に再コンパイルすると、コンパイルに時間がかかります(大規模プロジェクトでは問題になります)。

宣言をヘッダーファイルに入れて(実際にコードを実際に生成しないので)含めると、リンカーは宣言を対応するcppコードにフックします(その後、一度だけコンパイルされます)。

3
NeilDurant

再利用性、アーキテクチャ、データのカプセル化

以下に例を示します。

クラスmystringにすべての単純な形式の文字列ルーチンを含むcppファイルを作成するとします。mystring.cppを.objファイルにコンパイルするmystring.hにこのクラスdeclを配置します

メインプログラム(例:main.cpp)にヘッダーを含め、mystring.objとリンクします。プログラムでmystringを使用するには、詳細を気にしない どうやって ヘッダーが言うので、mystringは実装されています  できる

現在、バディがmystringクラスを使用したい場合、mystring.hとmystring.objを与えると、動作する限り、その動作を知る必要はありません。

後でそのような.objファイルがさらにある場合は、それらを.libファイルに結合して、代わりにリンクできます。

また、mystring.cppファイルを変更してより効果的に実装することもできます。これは、main.cppまたはバディプログラムには影響しません。

2
Anders

それがあなたのために働くなら、それで何も悪いことはありません-それは物事を行うための唯一の方法があると思う人々の羽を乱すことを除いて。

ここで与えられる回答の多くは、大規模なソフトウェアプロジェクトの最適化に対応しています。これらは知っておくと良いことですが、小さなプロジェクトを大きなプロジェクトであるかのように最適化しても意味がありません。これが「時期尚早な最適化」です。開発環境によっては、プログラムごとに複数のソースファイルをサポートするためにビルド構成をセットアップする際に、非常に複雑になる場合があります。

時間の経過とともにプロジェクトが進化し、ビルドプロセスに時間がかかりすぎることがわかった場合は、thenrefactor your増分ビルドを高速化するために複数のソースファイルを使用するコード。

いくつかの回答では、インターフェイスを実装から分離することについて説明しています。ただし、これはインクルードファイルに固有の機能ではなく、実装を直接組み込む#include "header"ファイルが非常に一般的です(C++標準ライブラリでさえ、かなりの程度までこれを行います)。

あなたがやったことについて本当に「型にはまらない」唯一のものは、「。h」または「.hpp」の代わりに「.cpp」というインクルードファイルの名前を付けることでした。

2
nobar

確かにあなたがしたように行うことができますが、標準的な方法は、共有宣言をヘッダーファイル(.h)に、関数と変数の定義(実装)をソースファイル(.cpp)に入れることです。

慣例として、これはすべてがどこにあるかを明確にし、インターフェイスとモジュールの実装を明確に区別するのに役立ちます。また、.cppファイルが別のユニットに含まれているかどうかを確認する必要がないため、複数の異なるユニットで定義されている場合に破損する可能性があるものを追加する必要がありません。

2
Avi

本を書くようなもので、完成した章を一度だけ印刷したい

本を書いているとしましょう。チャプタを別のファイルに配置する場合、変更した場合にのみチャプタを印刷する必要があります。ある章で作業しても、他の章は変更されません。

しかし、コンパイラの観点から見ると、cppファイルを含めることは、1つのファイルで本のすべての章を編集するようなものです。次に、変更した場合は、改訂された章を印刷するために、本全体のすべてのページを印刷する必要があります。オブジェクトコード生成には「選択したページを印刷」オプションはありません。

ソフトウェアに戻る:Linuxがあり、Ruby srcが横たわっています。大まかなコード行...

     Linux       Ruby
   100,000    100,000   core functionality (just kernel/*, Ruby top level dir)
10,000,000    200,000   everything 

これら4つのカテゴリのいずれにも多くのコードがあるため、モジュール性が必要です。この種のコードベースは、実世界のシステムの驚くほど典型的なものです。

1
DigitalRoss

John Lakosによる大規模C++ソフトウェア設計 を実行することをお勧めします。大学では、通常、このような問題に遭遇しない小さなプロジェクトを作成します。この本は、インターフェースと実装を分離することの重要性を強調しています。

通常、ヘッダーファイルには、それほど頻繁に変更されることのないインターフェースがあります。同様に、Virtual Constructorイディオムのようなパターンを調べると、概念をさらに理解するのに役立ちます。

私はまだあなたのように学んでいます:)

1
pankajt

プログラムをコンパイルおよびリンクすると、コンパイラは最初に個々のcppファイルをコンパイルしてから、それらをリンク(接続)します。最初にcppファイルに含まれていない限り、ヘッダーはコンパイルされません。

通常、ヘッダーは宣言であり、cppは実装ファイルです。ヘッダーでは、クラスまたは関数のインターフェースを定義しますが、実際の詳細の実装方法は省略します。この方法では、1つに変更を加えた場合、すべてのcppファイルを再コンパイルする必要はありません。

1
Jonas