一、C++のコンパイルモード

通常、C++プログラムには、.cppファイルと.hファイルの2種類のファイルのみが含まれます。.cppファイルはC++ソースファイルと呼ばれ、その中にはC++のソースコードが置かれます。一方、.hファイルはC++ヘッダファイルと呼ばれ、その中にもC++のソースコードが置かれます。

C++言語は「分離コンパイル」(separatecompilation)をサポートしています。つまり、プログラムのすべての内容を、異なる部分に分けて、それぞれ別の.cppファイルに置くことができます。.cppファイル内のものは比較の独立しており、コンパイル時には他のファイルとやり取りする必要はなく、コンパイルしてオブジェクトファイルにした後、他のオブジェクトファイルと一度リンクするだけで済みます。例えば、ファイルa.cppでグローバル関数 "void a(){}" を定義し、ファイルb.cppでこの関数を呼び出す必要があるとします。その場合でも、ファイルa.cppとファイルb.cppは互いの存在を知る必要はなく、それぞれ別々にコンパイルし、オブジェクトファイルにコンパイルした後にリンクすれば、プログラム全体を実行できます。

これはどのように実現されるのでしょうか?プログラムを書く観点から言うと、とても簡単です。ファイルb.cpp内で、"void a()" 関数を呼び出す前に、この関数を宣言します。"voida();" と書けばよいのです。これは、コンパイラがb.cppをコンパイルするときにシンボルテーブル(symbol table)を生成し、"void a()" のような定義が見えないシンボルは、このテーブルに格納されるからです。リンクする際、コンパイラは他のオブジェクトファイル内でこのシンボルの定義を探します。見つかれば、プログラムは問題なく生成されます。

ここで2つの概念が言及されていることに注意してください。1つは「定義」、もう1つは「宣言」です。簡単に言うと、「定義」とはシンボルを完全に記述することです。それが変数なのか関数なのか、どの型を返すのか、どのようなパラメータが必要なのかなどです。一方、「宣言」は単にそのシンボルの存在を宣言するだけです。つまり、コンパイラに「このシンボルは他のファイルで定義されている。ここではとりあえず使うので、リンクするときに他の場所へ探しに行って、それが何かを見つけてくれればよい」と伝えるのです。定義するときは、C++の構文に従ってシンボル(変数または関数)を完全に定義する必要がありますが、宣言するときは、そのシンボルのプロトタイプを書き出すだけで済みます。注意すべき点は、あるシンボルはプログラム全体で何度も宣言できますが、定義は1回だけでなければならないということです。もし、あるシンボルに2つの異なる定義が現れたら、コンパイラはどちらの言うことを聞けばよいのでしょうか?このメカニズムはC++プログラマーに多くの利点をもたらすと同時に、プログラムの書き方も導き出しました。考えてみてください。非常によく使われる関数 "void f() {}" があり、プログラム内の多くの.cppファイルから呼び出される場合、1つのファイルでこの関数を定義し、他のファイルではこの関数を宣言するだけで済みます。関数が1つなら対処しやすく、宣言も一言で済みます。しかし、関数が多くなったら、例えば数学関数が何百個もあるとしたら、どうしましょう?すべてのプログラマーがすべての関数の形式を完全に正確に覚えて書けることを保証できますか?

二、ヘッダファイルとは

明らかに、答えは不可能です。しかし、プログラマーがそんなに多くの関数プロトタイプを覚える手間を省く、とても簡単な方法があります。その何百もの関数の宣言文をすべて先に書いて、1つのファイルに置いておき、プログラマーが必要になったときに、それらをすべて彼のソースコードにコピーすればよいのです。

この方法は確かに実行可能ですが、やはり面倒で、しかもぎこちない印象があります。そこで、ヘッダファイルがその役割を発揮できるのです。いわゆるヘッダファイルは、その内容が.cppファイルの内容と同じで、どちらもC++のソースコードです。しかし、ヘッダファイルはコンパイルする必要がありません。すべての関数宣言を1つのヘッダファイルに入れておき、ある.cppソースファイルがそれらを必要とするとき、マクロコマンド "#include" によってその.cppファイルにインクルードし、それによってそれらの内容を.cppファイルに統合します。.cppファイルがコンパイルされるとき、インクルードされた.hファイルの役割が発揮されます。

例を挙げましょう。すべての数学関数が f1 と f2 の2つだけだと仮定します。それらの定義を math.cpp に置きます:

/* math.cpp */ double f1() { //do something here.... return; } double f2(double a) { //do something here... return a * a; } /* end of math.cpp */

そして、「これらの」関数の宣言をヘッダファイル math.h に置きます:

/* math.h */ double f1(); double f2(double); /* end of math.h */

別のファイル main.cpp で、この2つの関数を呼び出したい場合は、ヘッダファイルをインクルードするだけで済みます:

/* main.cpp */ #include "math.h" main() { int number1 = f1(); int number2 = f2(number1); } /* end of main.cpp */

これで、完全なプログラムになります。注意すべき点は、.hファイルをコンパイラのコマンドの後に書く必要はありませんが、コンパイラが見つけられる場所(例えば main.cpp と同じディレクトリ)になければならないということです。main.cpp と math.cpp はそれぞれ個別にコンパイルでき、main.o と math.o を生成し、その後この2つのオブジェクトファイルをリンクすれば、プログラムは実行できます。

三、#include

#include は、C言語由来のマクロコマンドであり、コンパイラがコンパイルする前、つまりプリコンパイルのときに作用します。#include の役割は、その後に書かれたファイルの内容を、完全に、一字も変えずに現在のファイルにインクルードすることです。特筆すべきは、それ自体には他の作用や副次のな機能は何もなく、それが現れるすべての場所を、その後ろに書かれたファイルの内容に置き換えるだけだということです。単純なテキスト置換であり、他には何もありません。したがって、main.cpp ファイルの最初の文(#include "math.h")は、コンパイルの前に math.h ファイルの内容に置き換えられます。つまり、コンパイル処理が始まろうとするとき、main.cpp の内容はすでに変わっているのです:

/* ~main.cpp */ double f1(); double f2(double); main() { int number1 = f1(); int number2 = f2(number1); } /* end of ~main.cpp */

過不足なく、ちょうどよいのです。同様に、main.cpp 以外にも、他にも多くの.cppファイルが f1 と f2 関数を使う場合、それらもすべて、この2つの関数を使用する前に #include "math.h" と1行書くだけで済みます。


四、ヘッダファイルには何を書くべきか

上記の議論から、ヘッダファイルの役割は他の.cppファイルにインクルードされることだと理解できます。ヘッダファイル自体はコンパイルに参加しませんが、実際にはその内容は複数の.cppファイル内でコンパイルされます。「定義は1回だけ」という規則により、ヘッダファイルには変数と関数の宣言だけを置き、定義を置くべきではないと容易に結論づけられます。なぜなら、ヘッダファイルの内容は実際には複数の異なる.cppファイルに取り込まれ、それらはすべてコンパイルされるからです。宣言を置くのは問題ありませんが、定義を置くと、複数のファイルに同じシンボル(変数または関数)の定義が現れることになり、たとえそれらの定義がすべて同じであっても、コンパイラにとっては合法ではありません。

したがって、覚えておくべき点は、.hヘッダファイルには変数または関数の宣言のみが存在でき、定義を置いてはいけないということです。つまり、ヘッダファイルには、extern int a; や void f(); のような文だけを書くのです。これらが宣言です。もし inta; や void f() {} のような文を書くと、そのヘッダファイルが2つ以上の.cppファイルにインクルードされた場合、コンパイラは即座にエラーを出します。(externについては、前に議論しましたので、ここでは定義と宣言の違いについては再び説明しません。)

ただし、この規則には3つの例外があります:

  • 一、ヘッダファイルには const オブジェクトの定義を書くことができます。グローバルな const オブジェクトはデフォルトでは extern の宣言を持たないため、現在のファイル内でのみ有効です。このようなオブジェクトをヘッダファイルに書いても、他の複数の.cppファイルにインクルードされた場合でも、そのオブジェクトはインクルードしたファイル内でのみ有効であり、他のファイルからは見えないため、多重定義を引き起こしません。同時に、これらの.cppファイル内のそのオブジェクトはすべて1つのヘッダファイルからインクルードされているため、これらの.cppファイル内のこの const オブジェクトの値が同じであることが保証されます。まさに一挙両得です。同様に、static オブジェクトの定義もヘッダファイルに入れることができます。
  • 二、ヘッダファイルにはインライン関数(inline)の定義を書くことができます。inline関数は、コンパイラがそれに出会った場所で、その定義に基づいてインライン展開する必要があるため、通常の関数のように先に宣言してからリンクする(インライン関数はリンクされません)ことはできません。したがって、コンパイラはコンパイル時にインライン関数の完全な定義を見る必要があります。もしインライン関数が通常の関数と同じように1回しか定義できないとしたら、これは難しい問題になります。1つのファイル内であればまだ良いのですが、インライン関数の定義を最初に書いておけば、後で使用するときに常に定義を見ることができます。しかし、他のファイルでもこの関数を使用する場合はどうすればよいでしょうか?これに対する良い解決策はほとんどないため、C++では、インライン関数はプログラム内で複数回定義してもよいと規定されています。インライン関数が1つの.cppファイル内に一度だけ現れ、すべての.cppファイルでこのインライン関数の定義が同じであれば、コンパイルを通過できます。したがって、インライン関数の定義をヘッダファイルに入れることは、明らかに非常に賢明な方法です。
  • 三、ヘッダファイルにはクラスの定義を書くことができる。プログラム内でクラスのオブジェクトを生成するとき、コンパイラはそのクラスの定義が完全に見えている場合にのみ、そのクラスのオブジェクトがどのようにレイアウトされるかを知ることができる。そのため、クラスの定義に関する要件は、インライン関数と基本的に同じである。したがって、クラスの定義をヘッダファイルに置き、そのクラスを使用する .cpp ファイルにそのヘッダファイルをインクルードするのは、良い作法である。ここで特筆すべきは、クラスの定義にはデータメンバと関数メンバが含まれるということだ。データメンバは具体的なオブジェクトが生成されて初めて定義(領域の確保)されるが、関数メンバは最初から定義されている必要がある。これが通常「クラスの実装」と呼ばれるものである。一般的な作法としては、クラスの定義をヘッダファイルに置き、関数メンバの実装コードを .cpp ファイルに置く。これは可能であり、良い方法でもある。ただし、もう一つの方法もある。それは、関数メンバの実装コードを直接クラス定義の中に書くことだ。C++ のクラスでは、関数メンバがクラス定義体内で定義されると、コンパイラはその関数をインラインとみなす。したがって、関数メンバの定義をクラス定義体に書き、まとめてヘッダファイルに入れることは合法である。注意すべきは、関数メンバの定義をクラス定義のヘッダファイルに書いても、クラス定義の中に書いていなければ、これは非合法だ。その関数メンバはその時点ではインラインではないからである。ヘッダファイルが2つ以上の .cpp ファイルにインクルードされると、その関数メンバは再定義されてしまう。

五、ヘッダファイルの保護対策

考えてみよう。ヘッダファイルに宣言文だけが含まれているなら、それが同じ .cpp ファイルに何度インクルードされても問題ない。なぜなら宣言文の出現は制限を受けないからだ。しかし、上で議論したヘッダファイルにおける3つの例外は、ヘッダファイルの非常に一般的な用途でもある。そして、ヘッダファイルに上記3つの例外のいずれかが出現すると、それが1つの .cpp に複数回インクルードされると大きな問題になる。なぜなら、これら3つの例外の構文要素は「複数のソースファイルに定義できる」とはいえ、「1つのソースファイルには一度しか出現できない」からだ。想像してみよう。a.h にクラス A の定義が含まれ、b.h にクラス B の定義が含まれるとする。クラス B の定義はクラス A に依存しているため、b.h は a.h を #include している。ここに、クラス A とクラス B の両方を使うソースファイルがあるとする。プログラマはこのソースファイルに a.h と b.h の両方をインクルードした。すると問題が発生する。クラス A の定義がこのソースファイル内に2回出現することになる! その結果、プログラム全体がコンパイルを通らなくなる。これはプログラマのミスだと思うかもしれない——b.h が a.h を含んでいることを知るべきだ、と。しかし実際には、彼が知るべきではないのだ。

#define と条件付きコンパイルを組み合わせることで、この問題をうまく解決できる。ヘッダファイル内で #define によって名前を定義し、条件付きコンパイル #ifndef...#endif によって、コンパイラがこの名前が定義されているかどうかに基づいて、そのヘッダファイル内の後続の内容をコンパイルし続けるかどうかを決定できるようにする。この方法は簡単だが、ヘッダファイルを書くときには必ず書くことを覚えておこう。


C++のヘッダファイルとソースファイルの違い

一、ソースファイルはどのように #include によってヘッダファイルと関連付けるか

  • 1、システム標準のヘッダファイルは山括弧(< >)で囲む。これによりコンパイラはシステムファイルディレクトリ内を検索する。
  • 2、ユーザー定義のファイルは二重引用符(" ")で囲む。コンパイラはまずユーザーディレクトリ内を検索し、次に C++ インストールディレクトリ(VC ではライブラリファイルの検索パスを指定・変更でき、Unix や Linux では環境変数によって設定できる)内を検索し、最後にシステムファイル内を検索する。

#include "xxx.h"(私はこれまで "" と <> に違いはないと思っていましたが、tinyxml.h はシステム外のヘッダファイルなので "" を使う必要があります)

二、ヘッダファイルはどのようにソースファイルと関連付けるか

この問題は実際には次のことを言っている。ヘッダファイル "a.h" が一連の関数を宣言し、"b.cpp" がそれらの関数を実装しているとする。もし "c.cpp" で "a.h" に宣言され、"b.cpp" で実装されているこれらの関数を使用したい場合、通常は "c.cpp" で #include "a.h" を使う。では、c.cpp はどのようにして b.cpp 内の実装を見つけるのか?

実際、.cpp と .h というファイル名には直接の関係は何もない。多くのコンパイラは他の拡張子も受け付けることができる。例えば、私は今、自分の会社のソースコードを見ているが、そこでは .cpp ファイルが .cc ファイルに置き換えられている。

Turbo C では、コマンドライン方式でコンパイルを行い、コマンドライン引数はファイル名である。デフォルトは .cpp と .h だが、.xxx などにカスタマイズすることもできる。

譚浩強先生の『Cプログラミング』という本の中で述べられているが、コンパイラの前処理時には #include コマンドに対して「ファイルインクルード処理」が行われる。つまり、file2.c の全内容が #include "file2.c" の位置にコピーされる。これは、なぜ多くのコンパイラがこのファイルの拡張子が何であるかを気にしないのかをまさに説明している。なぜなら #include 前処理は「コードをコピーして挿入する」作業を完了するからだ。

コンパイル時には、b.cpp ファイル内の関数実装を探すことはない。この作業が行われるのはリンク時だけである。b.cpp または c.cpp で #include "a.h" を使うのは、実際には関連する宣言を導入して、コンパイルが通るようにするためである。プログラムは実装がどこにあるのか、どのように実装されているのかを気にしない。ソースファイルはコンパイル後にオブジェクトファイル(.o または .obj ファイル)を生成し、そのオブジェクトファイル内では、これらの関数や変数は個々のシンボルとみなされる。リンク時には、makefile 内でどの .o または .obj ファイル(ここでは b.cpp が生成した .o または .obj ファイル)をリンクする必要があるかを指定する必要がある。そのとき、リンカはその .o または .obj ファイル内で b.cpp に実装された関数を探し、それらを makefile で指定された実行可能ファイルにビルドする。

Unix では、ソースファイルにヘッダファイルをインクルードせず、makefile の中で指定するだけでも可能である(ただし、これによりプログラムの可読性が大幅に低下するので、良い習慣とは言えない ^_^)。VC では、通常は自分で makefile を書く必要はなく、必要なファイルをすべてプロジェクトに含めるだけで、VC が自動的に makefile を用意してくれる。

通常、C++ コンパイラはすべての .o または .obj ファイル内で必要なシンボルを探す。特定のファイルだけを探したり、一つ見つけたら探すのをやめたりはしない。したがって、複数の異なるファイルで同じ関数を実装したり、同じグローバル変数を定義したりすると、リンク時に "redefined" というメッセージが表示される。

まとめ

.h ファイルに含めることができるもの:

  • クラスのメンバデータの宣言。ただし代入はできない
  • クラスの静的データメンバの定義と代入。ただし推奨はしない。宣言だけにしておくのがよい。
  • クラスのメンバ関数の宣言
  • クラスメンバではない関数の宣言
  • 定数の定義:例:constint a=5;
  • 静的関数の定義
  • クラスのインライン関数の定義

含めることができないもの:

  • 1. すべての非静的変数(クラスのデータメンバではないもの)の宣言
  • 2. 既定の名前空間の宣言をヘッダファイルに置かないこと。using namespace std; などは .cpp に置くべきである。.h ファイルでは std::string を使用する。

原文地址:https://blog.csdn.net/qq_35038153/article/details/71293265