このページでは、C言語や C++ におけるインクルードガードについて説明します!
Contents
インクルードガードとは
インクルードガードとは、下記のような、ヘッダーファイルの先頭の #ifndef
と #define
および、最後の #endif
の組み合わせのことを言います。
#ifndef HEADER_H
#define HEADER_H
// 型の定義・マクロの定義など
#endif
もしくは、下記の #pragma once
も同様にインクルードガードになります。
#pragma once
// 型の定義・マクロの定義など
逆に言うと、上記のようなコードはインクルードガードを実現するために記述されていることになります。
では、このインクルードガードにはどんな役割・効果があるのでしょうか?
次はこの点について説明していきます。
インクルードガードの役割・効果
このインクルードガードは、特定のソースファイルから “同じヘッダーファイルのインクルードが二重に行われることによるコンパイルエラー” を防ぐ(ガードする)ことを役割とする仕組みとなります。
スポンサーリンク
インクルードが重複するとコンパイルエラーが発生
ヘッダーファイルのインクルードの重複の何がまずいかというと、これは “同じ型” 等の定義が複数回行われることになります。
同じ型の多重定義はコンパイルエラー
ヘッダーファイルでは、そのヘッダーファイル自体や、そのヘッダーファイルをインクルードするソースファイルで利用する “型の定義” を行うことが多いです。他にも、マクロの定義や関数のプロトタイプ宣言、さらには他のヘッダーファイルのインクルード等も行うのですが、ここでは型の定義に注目して説明していきます。例えば、ヘッダーファイルでは構造体や共用体や列挙体を定義したり、さらには typedef
によって既存の型を他の型名で定義するようなことも行います。
ここで、下記のようなヘッダーファイルについて考えてみましょう。このヘッダーファイルでは struct _DATA
型の定義を行っています。そして、この型の定義を2回行ってしまっています。
// 構造体の定義
struct _DATA {
int x;
int y;
};
struct _DATA {
float x;
float y;
};
このように、同じ型の定義を複数回行うと、そのヘッダーファイルをインクルードするソースコードのコンパイル時に下記のようにコンパイルエラーが発生することになります。同じ型の定義を行っても意味がないので意図したコードになっていない可能性が高いですし、そもそも複数回同じ型の定義が行われると、その中のどれを採用すれば良いか判断できないのでエラーになるのは当然といえば当然ですね。
gcc source.c In file included from source.c:1: header1.h:7:8: error: redefinition of ‘struct _DATA’ 7 | struct _DATA { | ^~~~~ header1.h:2:8: note: originally defined here 2 | struct _DATA { |
まずは、ここまでの説明で、同じ型を複数回定義するとコンパイル時にエラーになること、さらにヘッダーファイルでは型の定義を行うことが多いことを理解していただければと思います。
ヘッダーファイルのインクルードの仕組み
先ほど説明したコンパイルエラーとインクルードガードの関係について説明する前に、ここでヘッダーファイルのインクルードについて整理しておきたいと思います。
C言語におけるインクルードとは、特定のファイルへの他のファイルのコードの埋め込みになります。
例えば、特定のソースファイルからヘッダーファイルのインクルードを行うと、コンパイルの前、すなわちプリプロセスの際に、インクルードしたファイルのコードがソースファイルに埋め込まれることになります。そして、その後にコンパイルが実行されることになります。
プリプロセスについてや、プリプロセスとコンパイルとの関係性等については下記ページで解説していますので、詳細は下記ページをご参照いただければと思います。
【C言語】ソースコードが実行可能ファイルになるまでの処理の流れここで、実際に下記のようなヘッダーファイル header.h
をソースファイル source.c
からインクルードする例について考えてみましょう。
// 構造体の定義
struct _DATA {
int x;
int y;
};
#include "header.h"
int main(void) {
struct _DATA data;
data.x = 100;
data.y = 200;
}
この source.c
に対してプリプロセスを行った結果はどのようなものになるでしょうか?これを実際に確認してみたいと思います。
gcc
の場合、-E
オプションを付加することで、引数で指定したソースファイルに対してプリプロセスのみを実施することができます。
gcc -E source.c
source.c
に対してプリプロセスを行った結果は下記のようになります。プリプロセスを実行する環境によって結果は多少異なるかもしれませんが、ここで注目していただきたいのが赤背景の部分になります。ここは #include "header.h"
が header.h
のコードに置き換えられた部分、すなわちヘッダーファイルのコードがソースファイルの中に埋め込まれた部分になります。
# 0 "source.c"
# 0 ""
# 0 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "" 2
# 1 "source.c"
# 1 "header.h" 1
struct _DATA {
int x;
int y;
};
# 2 "source.c" 2
int main(void) {
struct _DATA data;
data.x = 100;
data.y = 200;
}
コンパイルを行う際には、このプリプロセスが実行された後にコンパイルが行われることになります。そして、上記のようにヘッダーファイルに記述した型の定義等がソースファイルに埋め込まれるため、その定義された型をソースコードファイル内で利用することもできますし、コンパイルもエラーが出ることなく成功することになります。
皆さんも、いつも stdio.h
や stdlib.h
等をインクルードしてプログラムを開発していると思いますが、これらのヘッダーファイルも同様にコンパイル前にソースファイルに埋め込まれることで利用されていることになります。このように、インクルードとは、指定したファイルのコードをソースファイルに埋め込むことになります。
ここまでがヘッダーファイルのインクルードの説明になります。
ヘッダーファイルのインクルードの重複により型の多重定義が発生
次は下記のようなソースコードファイル source_err.c
について考えてみましょう。この source_err.c
は、先ほど示した source.c
に対し、#include "header.h"
を2回実行するように変更したものになります。
#include "header.h"
#include "header.h"
int main(void) {
struct _DATA data;
data.x = 100;
data.y = 200;
}
この source_err.c
に対してプリプロセスを実行した結果は下記のようになります。ここまでの説明を理解していただけた方であれば結果は予想できたのではないかと思いますが、#include "header.h"
を2回行っているため、ソースコードファイルに header.h
のコードが2つ分埋め込まれることになります。そして、結果的にソースファイル内で struct _DATA
の定義が2回行われることになります。
# 0 "source_err.c"
# 0 ""
# 0 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "" 2
# 1 "source_err.c"
# 1 "header.h" 1
struct _DATA {
int x;
int y;
};
# 2 "source_err.c" 2
# 1 "header.h" 1
struct _DATA {
int x;
int y;
};
# 3 "source_err.c" 2
int main(void) {
struct _DATA data;
data.x = 100;
data.y = 200;
}
そして、これは最初に説明した “型の多重定義” となります。なので、上記の source_err.c
をコンパイルすると次のようなエラーが発生することになります。
In file included from source_err.c:2: header.h:2:8: error: redefinition of ‘struct _DATA’ 2 | struct _DATA { | ^~~~~ In file included from source_err.c:1: header.h:2:8: note: originally defined here 2 | struct _DATA { | ^~~~~
インクルードの重複によるコンパイルエラーを防ぐ
ここまで説明してきたように、特定のソースファイルから同一のヘッダーファイルを複数回インクルードするとコンパイル時にエラーが発生することがあります(異なるソースファイルから同じヘッダーファイルがインクルードされるのは問題ありません)。これは、型の定義等が多重に実施されることになるためです。
ただ、これはインクルードガードを利用していない場合の話で、インクルードガードを利用することで、同一のヘッダーファイルを複数回インクルードしてもエラーが発生しなくなるようにすることができます。
これは、インクルードガードにより、同じヘッダーファイルを複数回インクルードしたとしても、そのヘッダーのコードがソースファイルには一度しか埋め込まれないようにすることができることになるからです。
インクルードガードの利用方法についての詳細は次の章で説明しますが、インクルードガードを利用した場合、先ほど紹介した source_err.c
のプリプロセスの結果は下記のようになります。このように、同じヘッダーファイルを2回インクルードしても、ヘッダーファイルのコードはソースファイルに一度しか埋め込まれません。そのため、当然コンパイルもエラーが発生することなく成功することになります。
# 0 "source_err.c"
# 0 ""
# 0 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "" 2
# 1 "source_err.c"
# 1 "header.h" 1
struct _DATA {
int x;
int y;
};
# 2 "source_err.c" 2
int main(void) {
struct _DATA data;
data.x = 100;
data.y = 200;
}
次は、インクルードガードの利用法について説明し、その後にインクルードガードの作用について説明していきたいと思います。
インクルードガードの利用方法
では、続いてはインクルードガードの利用方法について説明していきます。
スポンサーリンク
インクルードガードの書き方
ページの冒頭でも少し説明しましたが、インクルードガードを利用するには2つの方法があります。
#ifndef
と #define
の組み合わせ
1つ目は、ヘッダーファイルの先頭および最後に下記のように3行分のディレクティブ(プリプロセス時に処理される命令)を記述する方法になります。つまり、#ifndef ヘッダーを識別する文字列
と #endif
でファイル全体を囲み、さらに #ifndef ヘッダーを識別する文字列
の次の行に #define ヘッダーを識別する文字列
を記述します。
#ifndef ヘッダーを識別する文字列
#define ヘッダーを識別する文字列
// ヘッダーの本文
#endif
ここで、ヘッダーを識別する文字列
には、そのヘッダーファイルの名前に基づいた文字列を指定することが多いです。特に、ヘッダーファイルのファイル名を大文字、かつ .
を _
に変換した文字列を指定することが多いです。例えばファイル名が header.h
の場合は、下記のように ヘッダーを識別する文字列
に HEADER_H
を指定したりします。
#ifndef HEADER_H
#define HEADER_H
// ヘッダーの本文
#endif // HEADER_H
ここで重要なのは、ヘッダーを識別する文字列
には他のヘッダーファイルと重複しない文字列を指定することになります。そのため、前述の通り、ヘッダーファイルのファイル名に基づいた文字列を指定することが多いです。
#pragma once
2つ目の方法はヘッダーファイルの先頭に #pragma once
を記述する方法になります。#pragma once
を記述すればよいだけなので楽ですね。
#pragma once
// ヘッダーの本文
これらの2つの方法での効果はほぼ同じになります(全く同じかも)。なので、どちらを利用しても良いのですが、#pragma once
に関してはコンパイラが古い場合等は利用できない可能性があります。お手軽さは #pragma once
の方が上なので、使えるのであれば #pragma once
で良いかなぁと思います。また、会社でソフトウェア開発を行うのであれば、どちらを利用するのかを社内ルールで決められていたりすると思うので、それに従うので良いと思います。
インクルードガードの作用
#ifndef
と #define
の組み合わせによってインクルードガードを利用する場合、特定のソースコードから複数回同じヘッダーファイルがインクルードされた時に次のように作用することになります。
まず、#ifndef ヘッダーを識別する文字列
は ヘッダーを識別する文字列
が define
されていない場合にのみ成立するディレクティブとなります。つまり、ヘッダーを識別する文字列
が define
されていない場合のみ、#ifndef ヘッダーを識別する文字列
~ #endif
の内部に記述されたコードがプリプロセス対象となります。すなわち、インクルードを行ったソースファイルに埋め込まれる対象となります。
それに対し、ヘッダーを識別する文字列
が define
されている場合は #ifndef ヘッダーを識別する文字列
~ #endif
の内部のコードがプリプロセス対象外となります。そして、プリプロセス対象外のコードはインクルードを行ったソースファイルに埋め込まれません。
そして、1回目のヘッダーファイルのインクルード時には、(ソースコードや他のヘッダーファイルで define
していなければ)ヘッダーを識別する文字列
が define
されていない状態となります。なので、#ifndef ヘッダーを識別する文字列
~ #endif
の内部に記述されたコードがソースコードに埋め込まれることになります。
そして、次の行の #define ヘッダーを識別する文字列
によって ヘッダーを識別する文字列
が define
されることになります。
そのため、2回目以降のヘッダーファイルのインクルード時には、ヘッダーを識別する文字列
が define
されている状態となり、#ifndef ヘッダーを識別する文字列
~ #endif
の内部に記述されたコードがソースコードに埋め込まれなくなります。
このように、ヘッダーファイルが1度しかプリプロセスで処理されないように #ifndef
と #define
を組み合わせることによって、複数回同じヘッダーファイルがソースコードからインクルードされたとしても、ソースファイルに埋め込まれるのは一度のみに制限することが可能となります。そして、これにより、例えばヘッダーファイルで型の定義を行っていたとしても多重定義を避けることができてコンパイル時にもエラーが発生しなくなります。
また、#pragma once
の作用も同様で、これを記述したヘッダーファイルは同じソースコードファイルに対して一度しか埋め込まれなくなります。
ちなみに、C言語標準のヘッダーファイル(stdio.h
や stdlib.h
等)でもインクルードガードが利用されています。そのため、これらのファイルも意図せず複数回インクルードしたとしても、インクルードガードの作用によってコンパイルエラーは発生しません。
インクルードガード利用時の注意点
#ifndef
と #define
の組み合わせによるインクルードガードを利用する場合は ヘッダーを識別する文字列
の選定には十分気を付けるようにしてください。あくまでも #ifndef
と #define
の組み合わせによるインクルードガードは特定のソースファイルからの一度目のインクルード時に ヘッダーを識別する文字列
が define
されていないことを前提としたものになります。
つまり、他のファイルで ヘッダーを識別する文字列
が define
されていると、一度目のインクルード時にもインクルードガードが働いてしまうことになります。この場合、インクルードしようとしたヘッダーファイルが一度もソースファイルに埋め込まれなくなるためコンパイルエラーが発生することになります。
他のファイルで define
されないようにするためには、そのヘッダー特有の文字列を ヘッダーを識別する文字列
に指定する必要があり、前述のとおり “ヘッダーファイルのファイル名を大文字、かつ .
を _
に変換した文字列” を指定するのが無難だと思います。これで、ヘッダーを識別する文字列
が偶然他のファイルで define
されることは防げるはずです。
ただし、インクルードガードをコピペで記述する場合、ヘッダーを識別する文字列
部分の変更を忘れないように注意しましょう。これを忘れると意図せずインクルードガードが作用することになります。
あとは、1行目に記述するのは #ifndef
であり、#ifdef
ではないという点に注意してください。
さらに、ヘッダーファイルが異なるフォルダーに分かれて設置されている場合はヘッダーファイルのファイル名が重複するような可能性もあります。なので、このような場合はフォルダー名等も考慮して ヘッダーを識別する文字列
を指定してやる必要があるので、この点にも注意してください。
スポンサーリンク
インクルードガードの必要性
ここまでの説明を読んで、インクルードガードの利用に関して下記のように疑問を持った方もおられるのではないでしょうか?
これは、まさにその通りの疑問で、そもそも同じヘッダーファイルを複数回インクルードしなければインクルードガードは不要です。
なんですが、「同じヘッダーファイルを複数回インクルードしない」って結構難しいです。もちろん、前述で示したような source_err.c
のように、特定の1つのソースコードファイルから同じヘッダーファイルを “直接” 複数回インクルードしてるような場合は簡単に気付けるため、これを修正することは簡単です。
厄介なのが、ヘッダーファイルから他のヘッダーファイルをインクルードするケースが存在するところになります。そして、このようなインクルードは普通に行われることであり、これが各ヘッダーファイルの関係性を複雑にします。
さらに、特に大規模なソフトウェアを開発する場合、ヘッダーファイルの数も多くなり、時には 100 個以上のヘッダーファイルからソフトウェアが構成されるような場合もあります。そして、これらの多数のヘッダーファイルが互いにインクルードし合うことによって非常に複雑なインクルード関係になります。
なので、ソースコードファイルからは同じヘッダーファイルを直接的に複数回インクルードしていないように見えても、実はヘッダーファイルを経由して間接的に同一のヘッダーファイルがインクルードされてしまっているような場合があります。
そして、この場合はインクルードガードの仕組みを導入していなければコンパイルエラーが発生することになります。
つまり、ソースコードから同じヘッダーファイルを直接的にインクルードしないようにすることは可能なのですが、他のヘッダーファイルを介して同じヘッダーファイルを間接的にインクルードしてしまう可能性があること、さらにヘッダーファイルの数が多くなるとヘッダーファイル同士のインクルード構造が複雑になることより、同じヘッダーファイルを複数回インクルードしないようにコントロールすることは困難です。そのため、同じヘッダーファイルがインクルードされる可能性を考慮し、その場合でもコンパイルが成功するようにインクルードガードを導入することが必要です。
ということで、小規模で一人でお試しでソフトウェアを開発するような場合は除くとして、全ヘッダーファイルにインクルードガードを導入することをオススメします。
まとめ
このページでは、ヘッダーファイルのインクルードガードについて説明しました!
特定のソースコードファイルから同じヘッダーファイルを複数回インクルードすると型の二重定義が発生してコンパイルエラーになる可能性があります。この同じヘッダーファイルの複数回のインクルード時にコンパイルエラーの発生を防ぐのがインクルードガードになります。
このインクルードガードは #ifndef
と #define
の組み合わせ or #pragma once
によって簡単に実現できます。特に規模の大きいソフトウェアを開発するような場合は、忘れずに全ヘッダーファイルにインクルードガードを導入するようにしましょう!
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/