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


146

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

私のコードを振り返ってみると、クラスのヘッダーファイルを作成しないことを選択しましたが、cppファイルですべてを行いました(ヘッダーファイルがなくても正常に動作するように見えました...)。採点者が私が '#include "mycppfile.cpp";'と書いたことを意味していたと思います。一部のファイルで。

#includecppファイルを作成する理由は次のとおりです。-ヘッダーファイルに入るはずのものがすべてcppファイルにあるため、ヘッダーファイルのように振る舞った-monkey-see-monkeyのやり方で、他のファイルを見たヘッダーファイルが#includeファイルに含まれていたので、cppファイルにも同じことを行いました。

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


36
これは本当に良い質問です。私は多くのc ++初心者がこれによって助けられることを期待しています。
ミアクラーク

回答:


174

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

基本的に、どのような#include行うことは教えてくれているプリプロセッサを使用すると、指定したファイル全体を取るために、そして前にアクティブなファイルにコピーし、コンパイラはそれにその手を取得します。したがって、プロジェクト内のすべてのソースファイルを一緒に含める場合、基本的には何を行っても違いはなく、分離することなく1つの巨大なソースファイルを作成するだけです。

「ああ、それは大したことではない。走ったとしても大丈夫だ」と叫び声が聞こえる。そして、ある意味で、あなたは正しいでしょう。しかし、今あなたは小さな小さな小さなプログラムと、それをコンパイルするための素敵で比較的邪魔されないCPUを扱っています。あなたはいつもとても幸運であるとは限りません。

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

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

何かを変えない限り。

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

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

「しかし、確かにそれは可能であるはずです。そうでなければプログラミングは純粋な拷問のように聞こえます。インターフェイスを実装から分離する何らかの方法を見つけたらどうなるでしょう?これらの個別のコードセグメントから十分な情報を取得して、プログラムの残りの部分で識別し、代わりに、何らかのヘッダーファイルにそれらを追加しますか?そうすれば、#include プリプロセッサディレクティブを使用して、コンパイルに必要な情報のみを取り込むことができます!」

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


13
良い答えです 読みやすくてわかりやすかったです。私の教科書がこのように書かれたらいいのに。
ialm 2009年

@veol Head Firstシリーズの書籍を検索-C ++バージョンがあるかどうかはわかりません。headfirstlabs.com
Amarghosh

2
これは、私が聞いた、または考えた中で、これまでで最高の表現です。熟練した初心者であるジャスティンケースは、まだ出荷されていない100万回のキーストロークでプロジェクトを達成し、実際のユーザーベースでアプリケーションの光を見る称賛に値する「最初のプロジェクト」は、クロージャによって対処される問題を認識しました。OPの元の問題定義の高度な状態に非常に似ているが、「これをほぼ100回コード化し、例外によるプログラミングを使用せずにnull(オブジェクトなし)とnull(甥)の関係を理解できない」
ニコラスジョーダン

もちろん、ほとんどのコンパイラーは 'export'キーワードをサポート/実装していないため、これはすべてテンプレートではバラバラです。
KitsuneYMG 2009年

1
もう1つのポイントは、ヘッダーのみのクラスを使用する多くの最新のライブラリ(BOOSTを考える場合)があるということです。経験豊富なプログラマーがインターフェイスを実装から分離しないのはなぜですか?答えの一部は盲目的に言ったことかもしれません、別の部分は可能であれば1つのファイルが2つよりも優れているということ、そして別の部分はリンクが非常に高くなることができるよりもコストがかかるということです。ソースとコンパイラを直接含めることで、プログラムが10倍速く動作することを確認しました。リンクは主に最適化をブロックするためです。
クリス

45

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

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

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

/* 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関数を使用してソースファイルをインクルードすると、コンパイラが同じ実装を複数回検出するため、再定義エラーが発生します。したがって、すべての翻訳単位に関数宣言は表示するが、関数本体は表示しないようにする必要があります

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

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

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

2つの例外については、インライン関数、テンプレート関数、テンプレート型の実装を.cppファイルに入れてから、.includeで.cppファイルを作成する方が「いい」と感じる人もいます。これがヘッダーであるかソースファイルであるかは重要ではありません。プリプロセッサは気にせず、単なる慣例です。

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

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

繰り返しになりますが、これはあなたが要求したよりも明らかに上でしたが、細部にまでこだわった詳細が全体像を理解するのに役立つことを願っています。


2
十分な説明ありがとうございます。私は認めますが、それはまだ私には意味がありません。私はあなたの答えをもう一度(そしてもう一度)読む必要があると思います。
ialm 2009年

1
優れた説明のための+1。ひどすぎると、C ++の初心者全員が恐怖を感じるでしょう。:)
goldPseudo 2009年

1
ええ、悪い気分を感じないでください。Stack Overflowでは、最も長い回答が最良の回答になることはほとんどありません。

int add(int, int);関数宣言です。それのプロトタイプ部分はちょうどint, intです。ただし、C ++のすべての関数にはプロトタイプがあるので、この用語は実際にはCでのみ意味があります。この影響に対するあなたの回答を編集しました。
メルポメン

exportテンプレート用は2011年に言語から削除されました。コンパイラによって実際にサポートされることはありませんでした。
メルポメン

10

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


6

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

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

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

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


より具体的な例をありがとうございます。私はあなたのリンクを読んでみましたが、今私は混乱しています...ヘッダーを明示的に含めることと前方宣言を含めることの違いは何ですか?
ialm 2009年

これは素晴らしい記事です。Veol、ここには、コンパイラーがクラスのサイズに関する情報を必要とするヘッダーが含まれています。ポインターのみを使用する場合は、前方宣言が使用されます。
pankajt 2009年

前方宣言:int someFunction(int requiredValue); タイプ情報と(通常は)中括弧がないことに注意してください。これは、与えられたように、ある時点でintを取得してintを返す関数が必要になることをコンパイラーに伝えます。コンパイラーは、この情報を使用してその呼び出しを予約できます。それはフォワード宣言と呼ばれます。より洗練されたコンパイラーは、これを必要とせずに関数を見つけることができるはずです。ヘッダーを含めると、一連の前方宣言を宣言するのに便利です。
ニコラスジョーダン

6

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

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

短所:特定の.cppファイルをコンパイルするとき、コンパイラは他の.cppファイルの内容を「見る」ことができません。そのため、そこに実装されている関数がどのように実装されているかがわからないため、積極的に最適化することはできません。しかし、私はあなたがまだそれを気にする必要はないと思います(:


5

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


3

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

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

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


それで、コンパイル時間が長くなることに加えて、インクルードされたcppファイルの関数を使用するさまざまなファイルにcppファイルを#includeすると、問題が発生し始めますか?
ialm 2009年

はい、これは名前空間の衝突と呼ばれます。ここで興味深いのは、libsへのリンクが名前空間の問題を引き起こすかどうかです。一般に、コンパイラは翻訳単位のスコープのコンパイル時間を改善し(すべて1つのファイルに)、名前空間の問題を引き起こします-これにより、再び分離されます...各翻訳単位にインクルードファイルを含めることができます(これを強制することになっているプラ​​グマ(#pragma once)さえありますが、それは坐剤の仮定です。32ビットリンクが強制されていないため、どこからでもlib(.Oファイル)に盲目的に依存しないように注意してください。
ニコラスジョーダン

2

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

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


2

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

ここに例があります:

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

次に、メインプログラム(main.cppなど)にヘッダーを含め、mystring.objにリンクします。プログラムでmystringを使用するには、ヘッダーにができるが記載されているため、mystringの実装方法の詳細は気にしません。

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

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

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


2

それがあなたのために働くなら、それはそれで問題はありません-それが物事を行う唯一の方法があると考える人々の羽を揺さぶるということを除いて。

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

、時間の経過とともに、プロジェクトが進化して、ビルドプロセスに時間がかかりすぎることが判明した場合、その後、あなたはすることができますリファクタリング速くインクリメンタルビルドのために複数のソースファイルを使用するようにコードを。

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

インクルードファイルに「.h」や「.hpp」ではなく「.cpp」という名前を付けたことだけが、実際に行った「通常とは異なる」ことです。


1

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

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


ヘッダーファイルから実装を除外するとすみませんが、Javaインターフェースのように聞こえますか?
gansub

1

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

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

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


本の提案をありがとう。大規模なC ++プログラムを作成する段階に到達するかどうかはわかりませんが...
ialm 2009年

大規模なプログラムをコーディングすることは楽しいことであり、多くの挑戦にとっては楽しいことです。私はそれが好きになり始めています:)
pankajt 2009年

1

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

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

しかし、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つのカテゴリのいずれにも多くのコードがあるため、モジュール性が必要です。この種のコードベースは、実世界のシステムでは驚くほど典型的です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.