Nodachisoft Nodachi Sword Icon
  
@あまじ✎ 2021年1月16日に更新

第2章31 プリプロセッサ命令でインクルードガードを行う

イチからゲーム作りで覚えるC言語
第2章30 プリプロセッサ命令のdefine文を使う : PREV
NEXT : 第2章32 ポインタを理解する前に最低限のハードウェアとOSの知識 :

この記事でやること

前回はプリプロセッサ命令の define 文についてお話しました。

今回は、新しくプロジェクト「0260_precompiler_includeguard」を作り、プリプロセッサ命令での if ~ else と重複したファイルのincludeを防ぐ、一般にインクルードガードと呼ばれる基本的な使い方をお話したいと思います。。

インクルードガードを使ったソースコードをみてみる

今回は下の4つのファイルをみながら、インクルードガードの例を確認します。4つのファイルはビルドで結合されて、一つの実行可能なファイル(.exe)になります。

  1. common.h … 共通のマクロ(しゃべる関数形式のマクロを定義)
  2. entry.c … main関数を定義しているプログラム開始場所
  3. plancspeak.c … ぷらんくちゃんが喋る関数の定義
  4. plancspeak.h … ぷらんくちゃんが喋る関数の宣言

依存関係はこんな感じです。

graph_name commonh common.h entryc entry.c entryc->commonh include plancspeakh plancspeak.h entryc->plancspeakh include plancspeakc plancspeak.c plancspeakc->plancspeakh include plancspeakh->commonh include

まず、1. common.h (共通のマクロしゃべる関数形式のマクロを定義)のコードです。

 
common.h
#ifndef COMMON_HEADER
#define COMMON_HEADER
#ifdef __cplusplus
extern "C" {
#endif
// キャラクタが話すときの speak 関数宣言
#define speak(CHARACTER,MESSAGE) \
    printf("%s : %s\n", CHARACTER, MESSAGE)
#ifdef __cplusplus
}
#endif
#endif // COMMON_HEADERの終了

なにやら、#ifndefや、#ifdef、それにexternといった新しいものが登場しています。

先に、どんな役割のコードかをお話しすると、 この common.h ではキャラクタが話すときの関数形式のマクロ(speak)を定義しています。

つぎに 2. entry.c です。main関数を定義しているプログラム開始場所です。

 
entry.c
#include <stdio.h>
#include "common.h"       // speak のマクロ関数
#include "plancSpeak.h"   // doPlancSpeak関数の宣言
 
int main() {
  speak("システム", "ゲーム起動しました。");
  doPlancSpeak();
  speak("システム", "ゲーム終了しました。");
}

そして、3. plancSpeak.h の中身です。

 
plancspeak.h
#include "common.h" // speak関数が必要
void doPlancSpeak();

最後に 4. plancSpeak.c の中身です。

 
plancspeak.c
#include <stdio.h>
#include "plancSpeak.h" // doPlancSpeak関数の宣言
void doPlancSpeak() {
  speak("ぷらんく", "こんにちは。");
  speak("ぷらんく", "いい天気ですね!");
}

これらを実行したときの結果は下のようになります。

0260_precompiler_if
システム : ゲーム起動しました。
ぷらんく : こんにちは。
ぷらんく : いい天気ですね!
システム : ゲーム終了しました

ゲームが起動し、プレイヤーキャラクタが話す、その後ゲームが狩猟するイメージで、文字列が表示されたと思います。

インクルードガードとは

include文で他のファイルを読み込んでいくとき、同じ共通のヘッダーを読み込もうとする時があります。このとき、ヘッダーファイルの中が重複して取り込まれてしまうことになります。

今回の entry.c の例も同じように重複して common.h を取り込んでいます。

entry.cでcommon.hをインクルードしている部分
#include <stdio.h>
#include "common.h"       // speak のマクロ関数#include "plancSpeak.h"   // doPlancSpeak関数の宣言

例えば、今回のプロジェクトの例では、

  • 2行目で、entry.c → common.h を取り込み
  • 3行目で、entry.c → plancspeak.h を取り込み
  • さらに plancspeak.h の中から 1行目で plancspeak.h → common.h を間接的に取り込んでいます。

ソースコードの依存関係

include 文を書いている箇所を、コードで取り込みしていくと、common.h 部分が 2 箇所存在するコードになります。

プリコンパイル後のコード例
#include <stdio.h>(の取り込みされたコード。略)
~ common.h の取り込まれたコード ~~ common.h の取り込まれたコード ~void doPlancSpeak();
entry.c のコード続き

このように重複してファイルのインクルードが発生したときに、関数の宣言ではなく、定義を二重取り込みしたときコンパイルエラーとなります。 宣言だけであれば、二重で取り込んでもエラーにはなりません。

具体的にはエラーとなる例は下のようなコードです。インクルードした結果、 下のように2か所以上、同じ関数の定義があるとエラーになっちゃいます。

定義を二重取り込み(エラーとなる!)
void testspeak(){
  printf("Hellow!");
}
void testspeak(){
  printf("Hellow!");
}

関数の宣言が2か所以上あってもエラーにならないです。下みなたいなコードですね。

関数の宣言を二重取り込み(エラーにはならない)
void testspeak();
void testspeak();

さてさて、このようにinclude で取り込んだ先が重複するときは、 プリコンパイル処理やコンパイル処理が(エラーになったりはしなくとも)無駄に何度も実行されてしまいます。 それを防ぐためには、プリプロセッサ命令で一度読み込んだヘッダーファイルの定義は二重で読み込むのを防いであげるとよさそうですね。

このような二重取り込みを防ぐような処理を一般的にインクルードガードと呼びます。

インクルードガードの例

今回のソースコードでインクルードガードの例が common.h の中で書いてあります。 ソースコードを見ながらお話していきます。

 
common.h
#ifndef COMMON_HEADER
#define COMMON_HEADER
#ifdef __cplusplus
extern "C" {
#endif
// キャラクタが話すときの speak 関数宣言
#define speak(CHARACTER,MESSAGE) \
    printf("%s : %s\n", CHARACTER, MESSAGE)
#ifdef __cplusplus
}
#endif
#endif // COMMON_HEADERの終了

1行目に「#ifndef」というプリプロセッサ命令が登場しています。

ifndef 文は endif 文とペアで書きます

「#ifndef 〇〇」で、〇〇というシンボルがまだdefine宣言されていなければ、#endif までをプリプロセッサ処理を実行する という意味の命令になります。 なお、ifndef という名前は 「If Not Defined」を省略したことから来ていると思われます。

ifndef 命令や、すぐあとに出てくる ifdef 命令は入れ子にすることができる条件分岐のプリプロセッサ命令です。

もしシンボル「COMMON_HEADER」が define されていないなら、1行~13行目まで、プリプロセッサは処理をしてよい、ということになります。

2行目ではシンボル「COMMON_HEADER」を define された状態にしています。

#define COMMON_HEADER

置換する後の文字を何も書いていません。この状態でシンボル「COMMON_HEADER」が定義された、というととになります。

C++言語でのコンパイル処理を考慮

 
common.h
#ifdef __cplusplus
extern "C" {
#endif

3行目~5行目で ifdef ~ endif 文が登場します。 ifdef は シンボル〇〇がすでに定義されていたら、endifまでをプリプロセッサ処理する、という命令となります。

ここで、__cplusplus とは、C++言語でプリコンパイルしようとした時に、あらかじめ定義されているシンボルのことです。 C言語としてプリコンパイルしようとしたときは定義されていません。C++言語とC言語は互換性があり、C++言語でもC言語で作られたプログラムの資材を活かせるようになっています。

ただし、C++言語ではC言語でサポートしていない文法などが登場しますので、そういったC++言語だけで使いたいような文があるときは、シンボル__cplusplus が定義されているかどうかをチェックして、部分的に処理を差し込むようにすることで対応できます。

ですのでC言語としてプリコンパイルしようとしたときは、この「#ifdef __cplusplus」 ~ 「#endif」 の間は処理されません。

関数型マクロの定義

7~8行目で、関数型マクロが定義されています。 なお、speak 関数型マクロはちょっと横に長くなってしまったので、途中で「\ + 改行」を入れて6行目と7行目に分かれていますが、一つのdefine文です。 二つの引数、CHARACTER と MESSAGE を持っており、printf 関数に変換(展開)されます。

 
common.h
// キャラクタが話すときの speak 関数宣言
#define speak(CHARACTER,MESSAGE) \
          printf("%s : %s\n", CHARACTER, MESSAGE)

例えば、以降のソースコードでは以下のような変換(展開)がされます。

  • 展開前… speak ( “キャラクタ名” , “村人Aです。よろしく。” );
  • 展開後… printf ( “%s : %s \n”, “キャラクタ名”, “村人Aです。よろしく。”);

C++言語のための記載・その2

9~11行目は、先ほど同様、C++言語からのみプリコンパイル処理が通るように ifdef で条件分岐しています。C++言語からであれば、10行目はソースコードで生成される対象となります。

 
common.h
#ifdef __cplusplus
}
#endif

ifndef の終了

1行目のifndef 文のペアとなる endif文が最後に書いてあります。

 
common.h
#ifdef __cplusplus
}
#endif

ご注意となりますが、プリコンパイラ命令は最後に改行が必要です。 この#endif などは特に改行を入れることを忘れやすいので注意しましょう。

インクルードガードの処理概要

common.h のソースコードを1行1行見ていきました。ここで、二重に取り込まれることを防ぐためのインクルードガード処理を最初と最後に記載しています。

インクルードガードの書き方
#ifndef COMMON_HEADER
#define COMMON_HEADER
 /* 一度だけ取り込みたいソースコード部分 */
#endif // COMMON_HEADERの終了

上記のような書き方をしていれば、シンボル「COMMON_HEADER」が一度定義されたら、2度目は条件に合致せず素通りすることになりますね。 このシンボル「COMMON_HEADER」のような名前は、他のヘッダーファイルと重複しないように注意する必要があります。

その他のインクルードガードの方法

Visual Studio を使っていれば、Microsoft が推奨している(とおもわれる)「#pragma once」という命令があります。 これをヘッダーの中で使うと、そのファイルパスは2度目は取り込まないという処理ができます。これは処理系によって動作しないことがありますので使うときは少し注意してあげる必要があります。

あとがき

プリプロセッサ命令はいままでのC言語の文法とは処理されるタイミングや文法が異なるので、ややこしいですね。 でも巨大なプログラムであればあるほど、こういったプリプロセッサ命令を活用して、パフォーマンスやコンパイル時の省力化が工夫されている印象があります。

非常に参考になったサイトさまや、参考文献など

ページの更新履歴

更新日 更新内容
更新なし
イチからゲーム作りで覚えるC言語
第2章30 プリプロセッサ命令のdefine文を使う : PREV
NEXT : 第2章32 ポインタを理解する前に最低限のハードウェアとOSの知識 :
 
 
送信しました!

コメント、ありがとうございます。

なんかエラーでした

ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。

Twitter:@NodachiSoft_jp
お名前:
 
連絡先:
 
メッセージ:
 
戻る
内容の確認!

以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください

お名前:
 
連絡先:
 
メッセージ:
 
Roboto からの操作ではないという確認のため確認キーを入れてください。
確認キー=95
戻る
 / 
送信確認へ
コメント欄
コメント送信確認へ

関連ありそうな記事(5件)です!

ゲーム等で使えるつなぎ目のないループするテクスチャ画像の作り方

#ツール#ゲームプログラミング✎ 2021-01-24
ゲームなどで使えるループ画像、パターンテクスチャのツール、手動での作り方をまとめ
広告領域
追従 広告領域
目次
第2章31 プリプロセッサ命令でインクルードガードを行う
第2章31 プリプロセッサ命令でインクルードガードを行う
この記事でやること
この記事でやること
インクルードガードを使ったソースコードをみてみる
インクルードガードを使ったソースコードをみてみる
インクルードガードとは
インクルードガードとは
インクルードガードの例
インクルードガードの例
C++言語でのコンパイル処理を考慮
C++言語でのコンパイル処理を考慮
関数型マクロの定義
関数型マクロの定義
C++言語のための記載・その2
C++言語のための記載・その2
ifndef の終了
ifndef の終了
インクルードガードの処理概要
インクルードガードの処理概要
その他のインクルードガードの方法
その他のインクルードガードの方法
あとがき
あとがき
非常に参考になったサイトさまや、参考文献など
非常に参考になったサイトさまや、参考文献など
ページの更新履歴
ページの更新履歴
Nodachisoft © 2020