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

第2章29 プリコンパイラ処理とインクルード文@イチからゲーム作りで覚えるC言語

イチからゲーム作りで覚えるC言語
第2章28 自作関数で引数とプロトタイプ宣言を使う : PREV
NEXT : 第2章30 プリプロセッサ命令のdefine文を使う :

この記事でやること

前回まで簡単な関数の作成方法についてお話しました。

今回は、新しくプロジェクト「0240_precompiler_include」を作り、プリプロセッサプリコンパイラ処理)についてお話します。 プリコンパイラ処理について知ることで、より素早く動作させるプログラムを書いたり、プログラム全体の共通した数値や文字を簡単に書き換えたりすることができるようになります。また、違う環境(Windows だったり MacOS だったり)でコンパイルするときに、環境の違いを吸収するようなソースコードを書くこともできるようになります。

プリプロセッサってどんな処理?

プリプロセッサ(preprocessor)のプリは「~の前の」という意味があります。ですので、直訳するとプリプロセッサは、「前処理するもの」というようなイメージになります。

VisualStudio などで C 言語のプロジェクトをビルドするときは大雑把に以下のようなステップがあると前にお話しました。

C言語のビルドの流れ C言語のビルドの流れ

簡単におさらいとなりますが、ビルドをするときは、まずテキストエディタ等でソースコードを書いてからコンパイルすることにより、 ソースコードごとのコンパイル済みオブジェクト(CPUが解釈することのできる機械語で書いてあるファイル)になります。そして、リンク処理により、コンパイル済みオブジェクトを結合することでPC上で実行可能なアプリにとなる流れでした。

このときのソースコードから「コンパイル済みオブジェクト」を作る過程を少しだけ細かくみてみます

まず最初にプリプロセッサ翻訳プログラム(preprocessing translation unit)によってソースコードに書いたプリプロセッサ命令(preprocessing directive、ディレクティブと呼ばれることも)が読み取られ、 ソースコードがより、コンパイラにとって分かりやすい形に加工(翻訳)されます。

その後、プリコンパイル済みのコードを、機械語に変換するコンパイル処理が動くという流れとなります。(ページの最後に N1570 仕様上に記載のある、さらに細かい 8 ステップをページ最後にメモしておきます。)

プリプロセッサ翻訳プログラムだけを動かす

この処理もVisual Studio などの IDE上でビルドするときに自動的に内部で行われるので、あまり個別にこのプリプロセッサ翻訳プログラムだけを動かすことを意識する必要はありません。 ご参考として、GNU GCC やUNIX系のマシンであれば、cコンパイラのサブセットとして cppコマンドというコマンドが用意されていることが多く、プリプロセッサ翻訳だけを c のソースコードに対して個別に行うなども可能です。

プリプロセッサ命令ってどんな命令

プリプロセッサ命令は以下のように書いてある命令のことをいいます。

プリプロセッサ命令の書き方
#プリプロセッサ命令

行の最初に「#」シャープ記号が書いてあり、そのあとにプリプロセッサ命令が続きます。 改行があるまでが一つの命令となります。 通常のC言語の関数などであれば、引数の間など、見やすいように途中で改行することもできますが、プリプロセッサ命令は改行までなので注意が必要です。

プリプロセッサ命令で改行する方法

一行がものすごく長くなるようなプリプロセッサ命令を書きたいとき、横にながーいと読みづらくなりますね。
そのようなときは、半角スペースを書ける場所に「\」+「改行」でと入れれば行を跨いだプリプロセッサ命令を書くことができます。

この行の頭に「#」シャープがつく形式の命令は、いままでも使ってきたかと思います。 そうです!毎回ソースコードの頭に「#include 」などのようなインクルード文を書いてきました。

こいつもプリプロセッサ命令の一つです。

include文

いままでも利用していたインクルード文は、他のソースコードを書いてある場所に挿入するという機能があります。 利用するプログラムを動かして結果を見てみましょう。 プロジェクト「0240_precompiler_include」を新しく作成し、ソースコード「cppinclude.c」とソースコード「testfunc.c」、「testfunc.h」の3ファイルを作成してみてください。

作るファイルの一覧

まずは、cppinclude.c のファイルはこちら。

 
cppinclude.c
#include <stdio.h>
#include "testfunc.h"
int main( void ) {
  int kaifuku = yakusou();
  printf("薬草の回復量は %d です。\n" , kaifuku) ;
}

そしてそして、testfunc.c のファイルはこちら

 
testfunc.c
#include "testfunc.h"
int yakusou ( ) {
  return 10;
}

今回新しく登場した「.h」拡張子(ファイル名のドット以降の部分)のファイルですが、 こちらはヘッダーファイルと呼ばれるファイルです。普通、C言語の関数などの宣言やマクロを書いたりする場所で、拡張子の.h は header の頭文字の h です。

 
testfunc.h
// 関数 yakusou の宣言
int yakusou();

このコードを実行してみるとこんな結果になったかと思います

0240_precompiler_includeの実行結果
薬草の回復量は 10 です。

結果、自作の関数 yakusou を呼び出して回復量を表示することができました。

プリプロセッサの処理

コンパイル処理をする前にプリコンパイラによりプリプロセッサ命令が処理されるとお話しました。 いままでのプロジェクトでも include 文をソースコードの最初に書いてきたかと思います。

この #include 文があると、指定したC言語で書かれた別のソースコードファイルの中身を取り込むことができます。

include_source

もし #include 文で指定したC言語のファイルの先にも #include 文などがある場合、それも処理されます。

include_sequence

#include 文の書き方は大きく2つあります。いままでも stdio.h というファイルを毎回 include 文で指定してきましたが、下のように記号「<」 と 「>」で取り込みたいファイル名を囲んで書く方法です。

#include <ファイル名>

この #include は、あらかじめ用意された標準的な機能をまとめたC言語の特定のディレクトリの中にあるものを指定できます。

include するファイルの場所

これらの include で取り込むファイルのディレクトリですが、 具体的に、Visual Studio Comminuty 2017 であれば、stdio.h や、time.h は「C:\Program Files (x86)\Windows Kits\10\Include(バージョン)\ucrt」に、 stdbool.h や limits.h などは「(VisualStudioインストール先フォルダ)\VC\Tools\MSVC(バージョン)\include」に格納されていました。
(バージョン)や(VisualStudioインストール先フォルダ)は環境ごとに異なると思われます。
なお、これらの include で取り込む対象の標準ディレクトリはVisual Studio の設定から変えられます。
visualstudio_include.png

また、もう一つの方法で、ダブルクォーテーション「”」で取り込みたいファイル名を囲んで書く方法です。この方法で書くと、 同じプロジェクトのパス内にあるファイルが取り込まれます。

#include "ファイル名"

自前のソースコードを2つのファイルに分けるときなどは、自分のプロジェクトの中身にファイルを追加していくと思いますので、基本的にはこの「”」で囲む形でファイルを指定して取り込むことになります。

ソースコードの中身を追ってみる

今回のソースコードを追ってみましょう。 Visual Studio であれば、プロジェクトの中にあるソースコードファイル(拡張子が「.c」のもの)単位で処理されますので、2つの「.c」ファイルを追ってみることになります。 まずは main 関数が定義されている cppinclude.c から確認してみます。 ファイルの頭では stdio.h、testfunc.h という2つのヘッダーファイルを取り込んでいます。

cppinclude.c
#include <stdio.h>
#include "testfunc.h"
int main( void ) {
  int kaifuku = yakusou();
  printf("薬草の回復量は %d です。\n" , kaifuku) ;
}

この1行目、2行目の include 文がプリコンパイラで処理されたとき、下のようなソースコードの結果となります。

cppinclude.cのプリプロセッサ処理後
// highlight-next-line
~ stdio.h の中身がココに入る ~
// highlight-next-line
// 関数 yakusou の宣言
// highlight-next-line
int yakusou();
int main( void ) {
  int kaifuku = yakusou();
  printf("薬草の回復量は %d です。\n" , kaifuku) ;
}

stdio.h の中身はとても長いため、ここでは省略しています。 testfunc.h の中身がここで取り込まれており、3行目で関数 yakusou のプロトタイプ宣言が行われています。

もう一つの testfunc.c もプリコンパイラ前後を確認してみます。 こちらはプリコンパイル前の状態です。

testfunc.c(プリコンパイル前)
#include "testfunc.h"
int yakusou ( ) {
  return 10;
}

1行目の include が取り込まれて、下のようになります。

testfunc.c(プリコンパイル後)
// highlight-next-line
// 関数 yakusou の宣言
// highlight-next-line
int yakusou();
int yakusou ( ) {
  return 10;
}

これでinclude文のプリコンパイル処理がそれぞれのC言語で書いたソースコードファイルに対して行われました。

コンパイル時の処理

プリコンパイル処理が終わった後、コンパイル処理が行われます。 プリコンパイル処理を経て、cppinclude.c と testfunc.c からそれぞれ include 文の箇所が置換されたファイルが出来上がりました。

この2ファイルに対してそれぞれコンパイル処理が行われる流れとなります。

cppinclude.c のプリコンパイル処理後のファイル(下)がコンパイルされることを考えてみます。

cppinclude.cのプリプロセッサ処理後
~ stdio.h の中身がココに入る ~
// 関数 yakusou の宣言
int yakusou();
int main( void ) {
  int kaifuku = yakusou();
  printf("薬草の回復量は %d です。\n" , kaifuku) ;
}

3行目 「int yakusou();」で、関数のプロトタイプ宣言をしています。 関数は宣言と、定義の二つで分けることができると、前のページでお話しました。

ここでは宣言のみしており、どのように動く関数なのかの定義はしていません。

どこで定義しているかというと、もう一つのファイルである、「testfunc.c」をプリプロセッサ処理したファイルの中です。

こちらで yakusou 関数が定義されておりコンパイルするときに、同じプロジェクト内の他のC言語ファイルがチェックされ、 他のファイル内に存在するようであれば、宣言に合致する定義のファイルに対して、コンパイル後にリンカーというソフトによりリンク(結合)されて ソースコードをまたいだ yakusou 関数が呼び出されるようになる、という仕組みです。

include_source_relation

ヘッダーファイルに書くとまずい内容

今回、自作のヘッダーファイル「testfunc.h」を作り、そこでは int yakusou() の宣言のみを書きました。 このヘッダーファイルに関数の定義(中身)を書くことは出来ないのでしょうか。

ここでヘッダーファイル自体に下のように宣言だけでなく、定義も書くことを考えてみます。

良くないtestfunc.hの例
int yakusou ( ) {
  return 10;
}

ファイル  testfunc.c は不要になり、cppinclude.c から、testfunc.h をインクルードすればよいだけになりそうですね。 実際、コンパイルを実行してみるとコンパイルエラー等は出ないかと思います

良くないヘッダーの書き方例 include_bad_example

ではどんな時にこの書き方が問題となるでしょうか。

例えばプログラムが大きくなっていって、追加されたソースコード tuika.c からも、testfunc.h に書いた機能を使いたくなった時を考えてみましょう。

二つのCのコードから良くないtestfunc.hをインクルード include_bad_example

この時、プリコンパル処理をソースコードの「.c」単位で行っていき、全てを結合したとき、下のようにプリコンパイル後のファイルが出来上がります。 (stdio.hは省略)

結合後のイメージ(エラーとなる)
// studio.h の中身(省略)
int yakusou() {  return 10;}int main( void ) {
  int kaifuku = yakusou();
  printf("%d\n",kaifuku);
}
int yakusou() {  return 10;}int tuika() {
  return yakusou();
}

出来上がったファイルの 2~4行目、9~11行目が include 文で取り込んだ testfunc.h の部分です。

こんな感じでリンカが結合したとき、まったく同じyakusou() 関数の定義が 2 箇所に存在することとなり重複してしまっています。 yakusou 関数を使おうとしたとき、 2 つ定義されている yakusou() のどちらの関数を呼び出したらよいのかリンカはわからなくなり、ビルドは失敗します。

このように include 文により定義を重複で取り込まないように、定義(動作の本体)はヘッダーファイル(.h ファイル)ではなく、ソースコードファイル(.c)側に書いてあげるのが良さそうです。 また、宣言のみであれば、重複してもエラーとはなりませんので、宣言はヘッダー側に書いてあげて複数回 include で取り込まれても大丈夫です。

プリコンパイル処理で複数回

プリコンパイル時に include文で複数回、同じヘッダーを取り込むとき、まったく同じようなプリコンパイラ処理をしなければならず CPUに余計な負荷をかけてしまうので、一般にインクルードガードという方法が使われます。後ほどこちらについてもお話します。

もうちょっと細かいC11仕様のプリプロセッサ手順

この項目は細かい内容ですので、飛ばしていただいて構いません。 個人的なメモの意味も含めて、C言語のほぼ最新の仕様書であるC11のN1570から、プリプロセッサのより詳細な手順を抜粋して記載したものです。

プリプロセッサによるソースコードの翻訳は以下のような8つの手順にわかれています。

  1. ソースコードファイルに書いてある日本語などのマルチバイト文字を基本ソース文字コードセット(大文字小文字英数字といくつかの記号)に変換して、マルチバイトに含まれる改行コードを、本当の改行と区別をつけてわかりやすくします。また古いトライグラフ(ISO 646)の文字コードも変換します。
  2. 改行前の記号「\」は削除して、見やすくするためなどで改行された長い1命令を、改行なしの1命令に直します。ちなみに、ソースコードの最後は改行で終わらせないとここでうまく翻訳できなくなるよ。
  3. ソースコードはプリプロセッサ処理するために、トークンと呼ばれる命令に分解されます。このとき、プログラム実行に不要なコメントや半角スペースは除去されたりします。改行はそのままです。
  4. プリプロセッサ処理が行われます。プリプロセッサ命令(#includeとか)が処理されます。その他マクロだとか Pragma といった処理もされます。ここで#include 文が読み込まれたとき、読み込む先のソースコードについても手順1~4を繰り返します。その先でも #include してたらそれも同じように処理します。(再帰的に処理)処理が無事に終わったらプリプロセッサ命令は消します。
  5. ソースコードを書くのに使った文字コードセットで、文字リテラルに使っている文字列は対応するプログラム実行時の文字コードに変換しておきます。対応する文字コードがなかったら null 以外の文字コードに変換します。(ここはコンパイラ実装次第)
  6. ソースコード中に登場する連続した文字リテラルのトークンを結合しておきます。例)「”a” “b” L”c” 」→「L”abc”」
  7. ソースコードに空白が不要となったので削除します。数字の表現などのトークンを一つ一つ解釈されてコンパイルしやすい形に変換ます。
  8. 関数などを宣言しているが、実態(定義)は外部ライブラリにあるような参照を探して解決してあげます。

あとがき

今回はいままでずっとソースコードに書いてきた、けれど"おまじない" として深く触れてこなかった、include文についてお話しました。 プリプロセッサ命令は他の関数などとまったく扱いが異なるので、どのタイミングでお話したらよいかは悩みどころでしたが、このあたりで主要なプリプロセッサ命令をまとめ&お話ししたいと思います。

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

ページの更新履歴

更新日 更新内容
更新なし
イチからゲーム作りで覚えるC言語
第2章28 自作関数で引数とプロトタイプ宣言を使う : PREV
NEXT : 第2章30 プリプロセッサ命令のdefine文を使う :
 
 
送信しました!

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

なんかエラーでした

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

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

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

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

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

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

#ツール#ゲームプログラミング✎ 2021-01-24
ゲームなどで使えるループ画像、パターンテクスチャのツール、手動での作り方をまとめ
広告領域
追従 広告領域
目次
第2章29 プリコンパイラ処理とインクルード文@イチからゲーム作りで覚えるC言語
第2章29 プリコンパイラ処理とインクルード文@イチからゲーム作りで覚えるC言語
この記事でやること
この記事でやること
プリプロセッサってどんな処理?
プリプロセッサってどんな処理?
プリプロセッサ命令ってどんな命令
プリプロセッサ命令ってどんな命令
include文
include文
プリプロセッサの処理
プリプロセッサの処理
ソースコードの中身を追ってみる
ソースコードの中身を追ってみる
コンパイル時の処理
コンパイル時の処理
ヘッダーファイルに書くとまずい内容
ヘッダーファイルに書くとまずい内容
もうちょっと細かいC11仕様のプリプロセッサ手順
もうちょっと細かいC11仕様のプリプロセッサ手順
あとがき
あとがき
非常に参考になったサイトさまや、参考文献など
非常に参考になったサイトさまや、参考文献など
ページの更新履歴
ページの更新履歴
Nodachisoft © 2020