インクルードガードが再帰的インクルードや複数のシンボル定義を防いでくれないのはなぜですか?
質問
に関する2つのよくある質問です。 ガード :
-
最初の質問です。
なぜインクルードガードは私のヘッダーファイルを 相互、再帰的インクルージョン ? 私は以下のようなものを書くたびに、明らかにそこにあるのに存在しないシンボルについてのエラーや、さらに奇妙な構文エラーを受け続けています。
"a.h"
#ifndef A_H #define A_H #include "b.h" ... #endif // A_H
"b.h"
#ifndef B_H #define B_H #include "a.h" ... #endif // B_H
"main.cpp"です。
#include "a.h" int main() { ... }
なぜ、"main.cpp" をコンパイルするとエラーが発生するのですか?解決するにはどうしたらよいですか?
-
2つ目の質問です。
なぜインクルードガードは 多重定義 ? 例えば、私のプロジェクトが同じヘッダを含む2つのファイルを含んでいるとき、リンカは時々、あるシンボルが複数回定義されていることについて不平を言うことがあります。例えば
"header.h"
#ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H
"source1.cpp"です。
#include "header.h" ...
"source2.cpp"です。
#include "header.h" ...
なぜこのようなことが起こるのでしょうか?問題を解決するためにはどうしたらよいのでしょうか?
どのように解決するのですか?
<ブロッククオート最初の質問です。
なぜインクルードガードは私のヘッダーファイルを 相互、再帰的インクルージョン ?
彼らは .
彼らが役に立っていないのは 相互に含まれるヘッダ内のデータ構造の定義間の依存関係 . これが何を意味するのかを見るために、基本的なシナリオから始めて、なぜインクルードガードが相互包含の手助けになるのかを見てみましょう。
例えば、相互にインクルードする
a.h
と
b.h
ヘッダーファイルはつまらない内容を持っています。つまり、質問のテキストからのコードセクションの省略は、空の文字列に置き換えられます。このような状況で、あなたの
main.cpp
は喜んでコンパイルされます。そして、これはあなたのインクルードガードのおかげです!
もし確信が持てないなら、それらを削除してみてください。
//================================================
// a.h
#include "b.h"
//================================================
// b.h
#include "a.h"
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
コンパイラがインクルージョンの深さの制限に達すると、失敗を報告することに気がつくと思います。この制限は実装に依存します。C++11 標準の第 16.2/6 項による。
他のファイルの#include指示により読み込まれたソースファイルに#includeプリプロセッシング指示が表示される場合があります。 実装で定義された入れ子制限まで。 .
で、どうしたんですか? ?
-
パースするとき
main.cpp
を解析するとき、プリプロセッサーはディレクティブ#include "a.h"
. このディレクティブは、プリプロセッサにヘッダファイルa.h
を処理し、その結果を取って、文字列#include "a.h"
をその結果で置き換える。 -
処理中に
a.h
を処理している間、プリプロセッサはディレクティブである#include "b.h"
と同じメカニズムが適用されます: プリプロセッサはヘッダファイルb.h
を処理し、その処理結果を受け取り、ヘッダファイル#include
ディレクティブをその結果で置き換えます。 -
を処理するとき
b.h
を処理する場合、ディレクティブ#include "a.h"
は、プリプロセッサにa.h
を処理し、そのディレクティブを結果で置き換えます。 -
プリプロセッサーは
a.h
を再び解析し始め、その結果#include "b.h"
ディレクティブに再びぶつかり、無限に続く可能性のある再帰的な処理を設定することになります。重要なネストレベルに達すると、コンパイラはエラーを報告します。
インクルードガードが存在する場合 しかし、ステップ4で無限再帰が設定されることはありません。その理由を見てみましょう。
-
(
同上
) パースするとき
main.cpp
を解析するとき、プリプロセッサーはディレクティブ#include "a.h"
. これはプリプロセッサにヘッダファイルa.h
という文字列を、その処理結果を受け取って、置き換えます。#include "a.h"
をその結果で置き換える。 -
処理中に
a.h
を処理している間、プリプロセッサはディレクティブである#ifndef A_H
. マクロA_H
はまだ定義されていないので、次のテキストを処理し続けます。後続のディレクティブ(#defines A_H
) はマクロを定義します。A_H
. そして、プリプロセッサはディレクティブを満たします。#include "b.h"
: プリプロセッサは次にヘッダファイルb.h
を処理し、その処理結果を受け取って#include
ディレクティブをその結果で置き換えます。 -
を処理するとき
b.h
を処理するとき、プリプロセッサーはディレクティブの#ifndef B_H
. マクロB_H
はまだ定義されていないので、次のテキストを処理し続けます。後続のディレクティブ(#defines B_H
) はマクロを定義します。B_H
. 次に、ディレクティブ#include "a.h"
を処理するようにプリプロセッサに伝えます。a.h
を処理し#include
ディレクティブをb.h
を前処理した結果a.h
; -
コンパイラはプリプロセスを開始します
a.h
を再び開始し#ifndef A_H
ディレクティブに再び出会います。しかし、以前の前処理で、マクロA_H
が定義されています。したがって、コンパイラは今回、以下のテキストをスキップして、マッチする#endif
ディレクティブが見つかるまで次のテキストをスキップし、 この処理の出力は空文字列となります (何も#endif
ディレクティブの後には何もないものとします)。したがって、プリプロセッサは#include "a.h"
ディレクティブをb.h
を空文字列で置き換えるまで、実行をトレースします。#include
ディレクティブをmain.cpp
.
このように インクルードガードは相互インクルードから保護する . しかし クラスの定義間の依存関係 には対応できません。
//================================================
// a.h
#ifndef A_H
#define A_H
#include "b.h"
struct A
{
};
#endif // A_H
//================================================
// b.h
#ifndef B_H
#define B_H
#include "a.h"
struct B
{
A* pA;
};
#endif // B_H
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
上記のヘッダを考えると
main.cpp
はコンパイルされません。
なぜこのようなことが起こるのでしょうか?
何が起こっているのかを見るには、もう一度1~4のステップを行えば十分です。
最初の 3 つのステップと 4 番目のステップの大部分は、この変更の影響を受けないことが簡単にわかります (納得するために読み進めてください)。しかし、ステップ 4 の終わりでは、何か違うことが起こります。
#include "a.h"
ディレクティブを
b.h
を空文字列で指定すると、プリプロセッサは
b.h
の内容の解析を開始し、特に
B
. 残念ながら
B
はクラス
A
という、まさに今まで出会ったことのない
というのは
インクルードガードのためです!
事前に宣言されていない型のメンバ変数を宣言することは、もちろんエラーであり、コンパイラはそれを丁寧に指摘してくれます。
問題を解決するために必要なことは何でしょうか?
必要なのは 前方宣言 .
実際には
定義
クラスの
A
を定義するためには、クラス
B
というのは
ポインタ
への
A
のオブジェクトではなく、メンバ変数として宣言されています。
A
. ポインターは固定サイズなので、コンパイラーは
A
を適切に定義するために、コンパイラは B
. 従って,以下のようにすれば十分です.
前方宣言
クラス
A
で
b.h
を追加し、コンパイラにその存在を認識させます。
//================================================
// b.h
#ifndef B_H
#define B_H
// Forward declaration of A: no need to #include "a.h"
struct A;
struct B
{
A* pA;
};
#endif // B_H
あなたの
main.cpp
はこれで確実にコンパイルされるでしょう。2、3の発言。
-
を置き換えることで、相互包含を壊すだけでなく
#include
ディレクティブを前方宣言に置き換えるだけでなくb.h
の依存関係を効果的に表現するのに十分でした。B
へのA
: 可能な限り/実用的な場合は前方宣言を使用することも 良いプログラミングの実践です。 は、不要なインクルージョンを避けることができるため、全体のコンパイル時間を短縮することができます。しかし、相互包含を排除した上でmain.cpp
は次のように修正されなければならない。#include
両方ともa.h
とb.h
(は、(後者が必要であれば)、なぜならb.h
はもう間接的に#include
を通してa.h
; -
クラスの前方宣言が
A
へのポインタの参照は、コンパイラがそのクラスへのポインタを宣言するのに十分ですが(あるいは不完全な型が許容される他のコンテキストでそれを使用するのに十分です)。A
へのポインタの参照 (例えばメンバ関数を呼び出すため) やサイズの計算は 違法 の操作は不完全な型に対するものです。もしそれが必要なら、完全な定義のA
の完全な定義をコンパイラが利用できるようにする必要があります。 つまり、それを定義するヘッダーファイルが含まれていなければなりません。このため、クラスの定義とそのメンバ関数の実装は通常、ヘッダファイルとそのクラスの実装ファイルに分割されます(クラス テンプレート はこのルールの例外です):実装ファイル、これは決して#include
になることのない実装ファイルは、安全に#include
を使用することで、必要なすべてのヘッダを安全に定義することができます。一方、ヘッダーファイルは#include
他のヘッダーファイル がなければ の定義が本当に必要でない限り、そうする必要があります。 基底クラス を可視化するため)、可能かつ実用的であればいつでも前方宣言を使用します。
2つ目の質問です。
なぜインクルードガードは 多重定義 ?
彼らは .
このような場合、複数の定義からあなたを保護することはできません。 を別々の翻訳ユニットで . これについても この Q&A をStackOverflowでご覧ください。
これを見るには、インクルードガードを削除して、次のように修正したバージョンをコンパイルしてみてください。
source1.cpp
(または
source2.cpp
でもよい)。
//================================================
// source1.cpp
//
// Good luck getting this to compile...
#include "header.h"
#include "header.h"
int main()
{
...
}
コンパイラはここで確実に
f()
が再定義されていることに文句を言うでしょう。それは明らかで、その定義が二度含まれているのです! しかし、上記の
source1.cpp
は問題なくコンパイルされます。
header.h
が適切なインクルードガードを含んでいる場合
. それは期待されています。
それでも、インクルードガードが存在し、コンパイラがエラーメッセージで悩まなくなるときでも
リンカ
のコンパイルで得られたオブジェクトコードをマージする際に、複数の定義が見つかったという事実を主張するでしょう。
source1.cpp
と
source2.cpp
であるため、実行ファイルの生成は拒否されます。
なぜこのようなことが起こるのでしょうか?
基本的に、各
.cpp
ファイル (この文脈での専門用語は
翻訳ユニット
) は別々にコンパイルされ
は独立して
. をパースするとき
.cpp
ファイルを解析するとき、プリプロセッサはすべての
#include
ディレクティブを処理し、遭遇したすべてのマクロを展開します。この純粋なテキスト処理の出力は、オブジェクトコードに変換するためにコンパイラの入力として与えられます。コンパイラは1つの翻訳ユニットのオブジェクトコードを生成し終わると、次の翻訳ユニットに進み、前の翻訳ユニットを処理している間に遭遇したすべてのマクロ定義は忘れ去られます。
実際、プロジェクトを
n
翻訳ユニット (
.cpp
ファイル)を実行するようなもので、同じプログラム(コンパイラ)を
n
回、毎回異なる入力で実行するようなものです。
は前のプログラムの実行の状態を共有しません。
. したがって、各翻訳は独立して実行され、ある翻訳ユニットのコンパイル時に遭遇したプリプロセッサシンボルは、他の翻訳ユニットのコンパイル時には記憶されません(少し考えれば、これが実際に望ましい動作であることに容易に気がつくでしょう)。
したがって、インクルードガードが再帰的な相互包含と 冗長 に含まれているかどうかを検出することはできません。 異なる に含まれているかどうかを検出することはできません。
をコンパイルして生成されたオブジェクトコードをマージする場合、すべての
.cpp
ファイルをマージする際、リンカは
は
は同じシンボルが複数回定義されていることを認識し、これは
一回の定義ルール
. C++11 標準の第 3.2/3 項による。
すべてのプログラムには、すべての 非インライン 関数や変数が含まれていなければなりません。定義はプログラム中に明示的に現れるか、標準ライブラリやユーザ定義ライブラリにあるか、 (適切な場合には) 暗黙的に定義されます (12.1, 12.4, 12.8 を参照)。 インライン関数は、それが使用されるすべての翻訳単位で定義されなければならない。 .
したがって、リンカはエラーを出し、あなたのプログラムの実行ファイルの生成を拒否します。
<ブロッククオート問題を解決するために必要なことは何ですか?
もし
であるヘッダファイル内に関数定義を保持したい場合、そのヘッダファイルは
#include
によって
複数の
翻訳ユニット(ヘッダーが
#include
である場合、問題は発生しません。
一つ
の翻訳ユニット)を使用する場合は
inline
キーワードを使う必要があります。
それ以外の場合は
宣言
にある関数の
header.h
その定義 (本体) を
1
別の
.cpp
ファイルのみを作成します (これは古典的な方法です)。
は
inline
キーワードはコンパイラに対して、通常の関数呼び出しのためのスタックフレームを設定するのではなく、呼び出し先で直接関数本体をインライン化するようにという拘束力のない要求を表します。コンパイラはあなたの要求を満たす必要はありませんが
inline
キーワードはリンカに複数のシンボル定義を許容するように指示することに成功します。C++11 標準の第 3.2/5 項によると。
の定義は 1 つ以上あることがあります。 クラス型(第9項)、列挙型(第7.2項)があります。 外部リンクによるインライン関数 (7.1.2), クラステンプレート (第14節), 非静的関数テンプレート (14.5.6), クラステンプレートの静的データメンバー (14.5.1.3), クラステンプレートのメンバー関数 (14.5.1.1), 又はいくつかのテンプレートパラメータが指定されていないテンプレート特殊化 (14.7, 14.5.5) プログラムにおいて、それぞれの定義が異なる翻訳単位に現れ、その定義が以下の要求を満たしている場合に限る [...]...。
上記のパラグラフは、基本的にヘッダファイルによくある定義をすべてリストアップしています。 なぜなら、それらは安全に複数の翻訳ユニットに含めることができるからです。外部リンクのある他のすべての定義は、代わりにソースファイルに属します。
を使うことで
static
キーワードの代わりに
inline
キーワードはリンカーエラーを抑制することにもつながります。
内部リンク
このため、各翻訳ユニットにはプライベートな
コピー
を保持することになります。しかし、これは最終的に実行ファイルのサイズを大きくすることになり
inline
の使用が一般的に好まれます。
と同じ結果を得るための代替方法として
static
キーワードと同じ結果を得るには、関数
f()
の中に
無名名前空間
. C++11 標準の第 3.5/4 項による。
無名名前空間、または無名名前空間内で直接または間接的に宣言された名前空間は、内部連結を持ちます。他のすべての名前空間は外部連結を持つ。上記の内部連結を与えられていない名前空間スコープを持つ名前は、その名前である場合、包含する名前空間と同じ連結を持つ。
- 変数、または
- 関数 または
- 名前付きクラス (第9項)、またはリンクの目的でクラスが typedef 名を持つ typedef 宣言で定義された無名クラス (第7.1.3項)、あるいは
- 名前付き列挙(7.2)、または列挙がリンク目的のために typedef 名を持つ typedef 宣言で定義された無名列挙(7.1.3)、または
- リンケージを持つ列挙体に属する列挙体、または
- テンプレート。
上述と同じ理由で
inline
のキーワードが優先されるべきです。
関連
-
[解決済み] テスト
-
[解決済み] [Solved] Error C1083: Cannot open include file: 'stdafx.h'
-
[解決済み] string does not name a type Errorが発生するのはなぜですか?
-
[解決済み】変数 '' を抽象型 '' と宣言できない。
-
[解決済み】浮動小数点例外エラーが発生する: 8
-
[解決済み】Visual C++で "Debug Assertion failed "の原因となる行を見つける。
-
[解決済み】C++の余分な資格エラー
-
[解決済み] gdbを使用してもデバッグシンボルが見つからない
-
[解決済み】C++ - ステートメントがオーバーロードされた関数のアドレスを解決できない。
-
[解決済み] スタックアロケーションにより初期化されていない値が作成された
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】識別子 "string "は未定義?
-
[解決済み】関数名の前に期待されるイニシャライザー
-
[解決済み】「corrupted size vs. prev_size」glibc エラーを理解する。
-
[解決済み] 非常に基本的なC++プログラムの問題 - バイナリ式への無効なオペランド
-
[解決済み】C++プログラムでのコンソールの一時停止
-
[解決済み】「Expected '(' for function-style cast or type construction」エラーの意味とは?
-
[解決済み】Visual C++で "Debug Assertion failed "の原因となる行を見つける。
-
[解決済み] 式はクラス型を持つ必要があります。
-
[解決済み】std::cin.getline( ) vs. std::cin
-
[解決済み] to_string は std のメンバーではない、と g++ が言っている (mingw)