前回まで簡単な関数の作成方法についてお話しました。
今回は、新しくプロジェクト「0240_precompiler_include」を作り、プリプロセッサ(プリコンパイラ処理)についてお話します。 プリコンパイラ処理について知ることで、より素早く動作させるプログラムを書いたり、プログラム全体の共通した数値や文字を簡単に書き換えたりすることができるようになります。また、違う環境(Windows だったり MacOS だったり)でコンパイルするときに、環境の違いを吸収するようなソースコードを書くこともできるようになります。
プリプロセッサ(preprocessor)のプリは「~の前の」という意味があります。ですので、直訳するとプリプロセッサは、「前処理するもの」というようなイメージになります。
VisualStudio などで C 言語のプロジェクトをビルドするときは大雑把に以下のようなステップがあると前にお話しました。
簡単におさらいとなりますが、ビルドをするときは、まずテキストエディタ等でソースコードを書いてからコンパイルすることにより、 ソースコードごとのコンパイル済みオブジェクト(CPUが解釈することのできる機械語で書いてあるファイル)になります。そして、リンク処理により、コンパイル済みオブジェクトを結合することでPC上で実行可能なアプリにとなる流れでした。
このときのソースコードから「コンパイル済みオブジェクト」を作る過程を少しだけ細かくみてみます
まず最初にプリプロセッサ翻訳プログラム(preprocessing translation unit)によってソースコードに書いたプリプロセッサ命令(preprocessing directive、ディレクティブと呼ばれることも)が読み取られ、 ソースコードがより、コンパイラにとって分かりやすい形に加工(翻訳)されます。
その後、プリコンパイル済みのコードを、機械語に変換するコンパイル処理が動くという流れとなります。(ページの最後に N1570 仕様上に記載のある、さらに細かい 8 ステップをページ最後にメモしておきます。)
この処理もVisual Studio などの IDE上でビルドするときに自動的に内部で行われるので、あまり個別にこのプリプロセッサ翻訳プログラムだけを動かすことを意識する必要はありません。 ご参考として、GNU GCC やUNIX系のマシンであれば、cコンパイラのサブセットとして cppコマンドというコマンドが用意されていることが多く、プリプロセッサ翻訳だけを c のソースコードに対して個別に行うなども可能です。
プリプロセッサ命令は以下のように書いてある命令のことをいいます。
#プリプロセッサ命令
行の最初に「#」シャープ記号が書いてあり、そのあとにプリプロセッサ命令が続きます。 改行があるまでが一つの命令となります。 通常のC言語の関数などであれば、引数の間など、見やすいように途中で改行することもできますが、プリプロセッサ命令は改行までなので注意が必要です。
一行がものすごく長くなるようなプリプロセッサ命令を書きたいとき、横にながーいと読みづらくなりますね。
そのようなときは、半角スペースを書ける場所に「\」+「改行」でと入れれば行を跨いだプリプロセッサ命令を書くことができます。
この行の頭に「#」シャープがつく形式の命令は、いままでも使ってきたかと思います。
そうです!毎回ソースコードの頭に「#include
こいつもプリプロセッサ命令の一つです。
いままでも利用していたインクルード文は、他のソースコードを書いてある場所に挿入するという機能があります。 利用するプログラムを動かして結果を見てみましょう。 プロジェクト「0240_precompiler_include」を新しく作成し、ソースコード「cppinclude.c」とソースコード「testfunc.c」、「testfunc.h」の3ファイルを作成してみてください。
まずは、cppinclude.c のファイルはこちら。
#include <stdio.h>
#include "testfunc.h"
int main( void ) {
int kaifuku = yakusou();
printf("薬草の回復量は %d です。\n" , kaifuku) ;
}
そしてそして、testfunc.c のファイルはこちら
#include "testfunc.h"
int yakusou ( ) {
return 10;
}
今回新しく登場した「.h」拡張子(ファイル名のドット以降の部分)のファイルですが、 こちらはヘッダーファイルと呼ばれるファイルです。普通、C言語の関数などの宣言やマクロを書いたりする場所で、拡張子の.h は header の頭文字の h です。
// 関数 yakusou の宣言
int yakusou();
このコードを実行してみるとこんな結果になったかと思います
薬草の回復量は 10 です。
結果、自作の関数 yakusou を呼び出して回復量を表示することができました。
コンパイル処理をする前にプリコンパイラによりプリプロセッサ命令が処理されるとお話しました。 いままでのプロジェクトでも include 文をソースコードの最初に書いてきたかと思います。
この #include 文があると、指定したC言語で書かれた別のソースコードファイルの中身を取り込むことができます。
もし #include 文で指定したC言語のファイルの先にも #include 文などがある場合、それも処理されます。
#include 文の書き方は大きく2つあります。いままでも stdio.h というファイルを毎回 include 文で指定してきましたが、下のように記号「<」 と 「>」で取り込みたいファイル名を囲んで書く方法です。
#include <ファイル名>
この #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 の設定から変えられます。
また、もう一つの方法で、ダブルクォーテーション「”」で取り込みたいファイル名を囲んで書く方法です。この方法で書くと、 同じプロジェクトのパス内にあるファイルが取り込まれます。
#include "ファイル名"
自前のソースコードを2つのファイルに分けるときなどは、自分のプロジェクトの中身にファイルを追加していくと思いますので、基本的にはこの「”」で囲む形でファイルを指定して取り込むことになります。
今回のソースコードを追ってみましょう。 Visual Studio であれば、プロジェクトの中にあるソースコードファイル(拡張子が「.c」のもの)単位で処理されますので、2つの「.c」ファイルを追ってみることになります。 まずは main 関数が定義されている cppinclude.c から確認してみます。 ファイルの頭では stdio.h、testfunc.h という2つのヘッダーファイルを取り込んでいます。
#include <stdio.h>
#include "testfunc.h"
int main( void ) {
int kaifuku = yakusou();
printf("薬草の回復量は %d です。\n" , kaifuku) ;
}
この1行目、2行目の include 文がプリコンパイラで処理されたとき、下のようなソースコードの結果となります。
// 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 もプリコンパイラ前後を確認してみます。 こちらはプリコンパイル前の状態です。
#include "testfunc.h"
int yakusou ( ) {
return 10;
}
1行目の include が取り込まれて、下のようになります。
// highlight-next-line
// 関数 yakusou の宣言
// highlight-next-line
int yakusou();
int yakusou ( ) {
return 10;
}
これでinclude文のプリコンパイル処理がそれぞれのC言語で書いたソースコードファイルに対して行われました。
プリコンパイル処理が終わった後、コンパイル処理が行われます。 プリコンパイル処理を経て、cppinclude.c と testfunc.c からそれぞれ include 文の箇所が置換されたファイルが出来上がりました。
この2ファイルに対してそれぞれコンパイル処理が行われる流れとなります。
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 関数が呼び出されるようになる、という仕組みです。
今回、自作のヘッダーファイル「testfunc.h」を作り、そこでは int yakusou() の宣言のみを書きました。 このヘッダーファイルに関数の定義(中身)を書くことは出来ないのでしょうか。
ここでヘッダーファイル自体に下のように宣言だけでなく、定義も書くことを考えてみます。
int yakusou ( ) {
return 10;
}
ファイル testfunc.c は不要になり、cppinclude.c から、testfunc.h をインクルードすればよいだけになりそうですね。 実際、コンパイルを実行してみるとコンパイルエラー等は出ないかと思います
ではどんな時にこの書き方が問題となるでしょうか。
例えばプログラムが大きくなっていって、追加されたソースコード tuika.c からも、testfunc.h に書いた機能を使いたくなった時を考えてみましょう。
二つのCのコードから良くないtestfunc.hをインクルード
この時、プリコンパル処理をソースコードの「.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に余計な負荷をかけてしまうので、一般にインクルードガードという方法が使われます。後ほどこちらについてもお話します。
この項目は細かい内容ですので、飛ばしていただいて構いません。 個人的なメモの意味も含めて、C言語のほぼ最新の仕様書であるC11のN1570から、プリプロセッサのより詳細な手順を抜粋して記載したものです。
プリプロセッサによるソースコードの翻訳は以下のような8つの手順にわかれています。
今回はいままでずっとソースコードに書いてきた、けれど"おまじない" として深く触れてこなかった、include文についてお話しました。 プリプロセッサ命令は他の関数などとまったく扱いが異なるので、どのタイミングでお話したらよいかは悩みどころでしたが、このあたりで主要なプリプロセッサ命令をまとめ&お話ししたいと思います。
更新日 | 更新内容 |
---|---|
更新なし |
コメント、ありがとうございます。
ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。
Twitter:@NodachiSoft_jpお名前:以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください
お名前: