プリプロセッサ

ソースプログラムをコンパイルする前に、ソースプログラムに対して行われる前処理をプリプロセスという。このプリプロセスを行なうプログラムのことをプリプロセッサと呼ぶ。通常はコンパイラがプリプロセッサの機能を兼ね備えている。

プリプロセッサとは

C言語はプログラミングを容易にするためのマクロを利用できる。

C言語のプリプロセッサは、マクロが使われたC言語ソースコードをプリミティブなC言語ソースコードに変換するものである。

プリプロセッサの出力は内部的に使われるだけで、ファイルに保存されるわけではない。ただし、たいていのコンパイラにはプリプロセッサの結果を出力するためのオプションが用意されている。

gcc の場合、-E オプションを指定すると、プリプロセッサの結果を出力できる。

$ cat example.c
#include <stdio.h>
#define BUFSIZE 1024
int main(int argc, char *argv[]) {
  printf("%d", BUFSIZE);
}
$ gcc -E example.c
#(stdio.hの内容が展開されるがここでは省略)
int main(int argc, char *argv[]) {
  printf("%d", 1024);
}
$

C言語標準のプリプロセッサの他に、Oracle Pro*C/C++もプリプロセッサの一種である。

プリプロセッサ指令(ディレクティブ)

プリプロセッサに対する指令を「ディレクティブ」と呼ぶ。C言語プリプロセッサのディレクティブは、先頭にシャープ(#)が付く。

C言語には次のようなディレクティブがある。

ディレクティブ
ディレクティブ 説明
#define マクロを定義する。
#ifdef シンボルが定義されているときに実行する。
#if 式が真のときに実行する。
#include ヘッダファイルをインクルードする。
#error コンパイラにエラーを発生させる。
#warning コンパイラに警告を発生させる。
#pragma マシンやOS固有の機能をサポートする。

#define

#define ディレクティブはマクロの定義を行う。C言語では数値や文字列、数式にに名前を付けて定数を定義することができます。

#define identifier
#define identifier replacement
#define identifier (parameter) replacement
identifier
マクロの識別子を指定する。識別子は大文字と小文字を区別する。マクロの識別子は慣習的に大文字で付けることが多い。
#include <stdio.h>
#define DEBUG

int main(int argc, char **argv) {
  int i = 1;
#ifdef DEBUG
  printf("i = %d\n", i);
#endif
}

上記プログラムの実行結果を以下に示す。

$ gcc -o example example.c
$ ./example
i = 1
replacement
置き換える文字列を指定する。プリプロセッサによってマクロは対応する文字列に置き換えられる。

#defineディレクティブの使用例を次に示す。この例では文字列と数値をマクロで定義している。

#include <stdio.h>
#define TAX_NAME "消費税"
#define TAX_RATE  0.1

void main() {
  double price = 2500;
  printf("%s %f\n", TAX_NAME, price * TAX_RATE);
}

上記プログラムの実行結果を以下に示す。

$ gcc -o example example.c
$ ./example
消費税 250.000000
parameter
引数のリストを指定する。

マクロには引数を指定することができる。この例では数式をマクロで定義している。数式で使っている変数は、マクロを呼び出す際に引数として指定する。

#include <stdio.h>
#define MAX(A, B) A > B ? A : B

void main() {
  printf("%d\n", MAX(1, 2));
  printf("%d\n", MAX(4, 3));
}

上記プログラムの実行結果を以下に示す。

$ gcc -o example example.c
$ ./example
2
4

定義済みマクロ

Cコンパイラで既に定義されているマクロがある。定義済みマクロの一覧を以下に示す。

__DATE__
コンパイル日付
__FILE__
ソースファイル名 (Windows はフルパス、Solaris はファイル名のみ)
#include <stdio.h>
int main(char argc, char **argv) {
  int i = 1;
  printf("%s:%d i = %d\n", __FILE__, __LINE__, i);
}

上記プログラムの実行結果を以下に示す。

$ gcc -o example example.c
$ ./example
example.c:4 i = 1
__LINE__
ソースファイルの行番号
__STDC__
ANSI規格対応ならば1、ANSI規格非対応ならば0
#if __STDC__
/* ANSI規格対応コンパイラ用 */
#else
/* ANSI規格非対応コンパイラ用 */
#endif
__STDC_VERSION__

標準Cのバージョンを long int 値で表す。

__STDC_VERSION__
バージョン
C89 未定義
C90 未定義
C95 199409L
C99 199901L
C11 201112L
C17 201710L

標準Cのバージョンを出力するプログラムの例を次に示す。

#include <stdio.h>
int main(int argc, char *argv[]) {
  printf("%ld\n", __STDC_VERSION__);
}

上記プログラムの実行例を次に示す。

$ gcc -o example example.c
$ ./example
201710

__STDC_VERSION__ は long int の値なので、大小比較もできる。

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199409L
/* C95 compatible source code */
#elif defined(__STDC__)
/* C89 compatible source code */
#endif
__TIME__
コンパイル時間

#ifdef

#ifdef プリプロセッサ命令は、シンボルが定義されているときに #ifdef から #endif までのプリプロセッサ命令を実行します。

#ifdef シンボル名
/* シンボルが定義されているときに実行する */
#endif

シンボルは#define プリプロセッサ命令で定義するか、Cコンパイラーのオプションで定義します。

シンボルが定義されていないときにプリプロセッサ命令を実行させるには、次のようにします。

#ifndef シンボル名
/* シンボルが定義されていないときに実行する */
#endif

シンボルが定義されているときと定義されていないときのプリプロセッサ命令を分けるには、次のようにします。

#ifdef シンボル名
/* シンボルが定義されているときに実行する */
#else
/* シンボルが定義されていないときに実行する */
#endif

#if

#if プリプロセッサ命令は、式が真のときに #if から #endif までのプリプロセッサ命令を実行します。

#if 式
  /* 式が真のときに実行する */
#endif

式が偽のときにプリプロセッサ命令を実行させるには、次のようにします。

#if !式
  /* 式が偽のときに実行する */
#endif

式が真のときと偽のときでプリプロセッサ命令を分けて実行させるには、次のようにします。

#if 式
  /* 式が真のときに実行する */
#else
  /* 式が偽のときに実行する */
#endif

条件分岐の条件式が複数ある場合は、#elifプリプロセッサ命令を使用する。

#if 式1
  /* 式1が真のときに実行する */
#elif 式2
  /* 式1が偽、かつ式2が真のとき実行する */
#elif 式3
  /* 式1が偽、かつ式2が偽、かつ式3が真のとき実行する */
#else
  /* 式1、式2及び式3がすべて偽のとき実行する */
#endif

次のようにすることで、#ifdef プリプロセッサ命令と同じことができます。

#if defined(シンボル名)
  /* シンボルが定義されているときに実行する */
#endif

また、次のようにすることで、 #ifndef プリプロセッサ命令と同じことができます。

#if !defined(シンボル名)
  /* シンボルが定義されていないときに実行する */
#endif

#include

C言語では様々なマクロを定義したヘッダファイルが用意されています。ヘッダファイルはインクルードファイルとも呼ばれます。 このヘッダファイルを読み込むには、 #includeプリプロセッサ・ディレクティブを使用します。

#include <ファイル名>

どのようなヘッダファイルがあるかは処理系によって異なりますので、ライブラリーのマニュアルを参照してください。典型的なヘッダファイルを次に示します。

C言語のヘッダファイル
ファイル名 説明
limits.h 実装に依存する値に関するヘッダファイル
stdio.h 標準入出力に関するヘッダファイル
signal.h シグナルに関するヘッダファイル
stdlib.h 標準ライブラリに関するヘッダファイル
string.h 文字列操作に関するヘッダファイル
sys/types.h システムに依存する変数タイプに関するヘッダファイル
iconv.h 文字コード変換ライブラリ(iconv API)のヘッダファイル
time.h 時刻操作に関するヘッダファイル
unistd.h UNIX標準に関するヘッダファイル
wchar.h ワイドキャラクタに関するヘッダファイル

C++のプリプロセッサでもC言語のヘッダファイルが利用できます。C++専用のヘッダファイルも用意されています。

C++のヘッダファイル
ファイル名 説明
cstddef C言語のsys/types.hなどに相当
cstdio C言語のstdio.hに相当
cstdlib C言語のstdlib.hに相当
cstring C言語のstring.hに相当
ctime C言語のtime.hに相当
cwchar C言語のwchar.hに相当

あらかじめ用意されているヘッダファイルだけでなく、自分で作ったヘッダファイルを読み込む(インクルードする)こともできます。

#include "ファイル名"

ヘッダファイルのファイル名拡張子は慣習的に .h が使われていますので、自分でヘッダファイルを作成する場合もこれに習います。

自分で作成したヘッダファイルは任意のディレクトリに配置できます。ソースファイルと異なるディレクトリにヘッダファイルを配置した場合には、Cコンパイラのオプションでヘッダファイルの配置ディレクトリを指示しなければなりません。

インクルードガード

システムから提供されているヘッダファイルは、同じヘッダーファイルを二重にインクルードしないよう工夫がされています。

たとえば、Solarisのstdio.hの場合は、次のようになっています。

#ifndef _STDIO_H

#define _STDIO_H

#ifdef __cplusplus
extern "C" {
#endif

......

#ifdef __cplusplus
}
#endif

#endif /* _STDIO_H */

_STDIO_Hというシンボルが定義されていない場合のみ、プリプロセッサの処理が行われるようになっています。次に_STDIO_Hというシンボルを定義しています。これにより、2回目以降のstdio.hのインクルードでは、2重にインクルードされることがなくなります。

Microsoft Windows (Visual C++) の場合は次のようになっています。

#ifndef _INC_STDIO

#define _INC_STDIO

#ifdef __cplusplus
extern "C" {
#endif

......

#ifdef __cplusplus
}
#endif

#endif /* _INC_STDIO */

自分でヘッダファイルを作る場合も、これに習うとよいでしょう。

#error

#errorは、コンパイラにコンパイルエラーを発生させるプリプロセッサ命令である。

#error エラーメッセージ

#warning

#warningは、コンパイラに警告を発生させるプリプロセッサ命令である。

#warning 警告メッセージ

#pragma

#pragma プリプロセッサ命令は、ホストマシンやオペレーティングシステムに固有の機能をサポートします。たとえば、データが置かれるメモリ領域の正確な管理や、バージョン情報をプログラムコードに埋め込んだりします。 #pragma プリプロセッサ命令は、マシンまたはオペレーティングシステム固有であり、通常コンパイラごとに使用できる機能が異なります。

#pragmra keyword

keywordに指定できるキーワードはCコンパイラによって異なる。

#pragma once

一度読み込まれたヘッダファイルを記憶しておき、同じヘッダファイルが再度読み込まれたときは、その読み込みを無視する。Visual C++ やgccなど、多くのコンパイラで使用できる。インクルードガードと同じ役割を果たす。

#pragma once

#pragma comment

#pragma commentは、オブジェクトファイルや実行ファイルにコメントを書き込むプリプロセッサ命令であり、Visual C++ で使用できる。

ライブラリ(lib)に対するコメントは、リンカのオプションやリンクするライブラリを指定できる。

#pragma commentプリプロセッサ命令を使用して、リンクするライブラリを指定する例を次に示す。

#pragma comment(lib, "jvm.lib")

#pragma commentプリプロセッサ命令を使用して、リンカのオプションを指定する例を次に示す。

#pragma comment(lib, "/nologo")

#pragma ident

Solarisのccでは、#pragma identでSCCSバージョン情報をLMFに埋め込むことができる。

#pragma ident "@(#)main.c 92/02/07"

マクロ置き換え演算子

プリプロセッサでは、マクロを置き換える演算子を使用できる。プリプロセッサで使えるマクロ置き換え演算子には次のものがある。

演算子 説明
# マクロ実引数を文字列化する。
## 前後の字句列を結合する。

#

#演算子は、マクロ実引数を文字列化します。

#define strgen1(x) "x"
#define strgen2(x) x
#define strgen3(x) #x

char *p, *string = "abc";

p = strgen1(string); /* p = "x"      */
p = strgen2(string); /* p = "abc"    */
p = strgen3(string); /* p = "string" */

下記の例の場合、"main-" "func" ".c" が文字列連結されて、"main-func.c" となります。

#define PREFIX "main-"
#define SUFFIX ".c"
#define fname(name) PREFIX #name SUFFIX

p = fname(func); /* p = "main-func.c" */

##

##演算子は、前後の字句列を結合します。

#define symadd(x, y)  sym##x + sym##y

int sym1, sym2;
i = symadd(1, 2); /* sym1 + sym2 */

参考文献

Microsoft (2022) 定義済みマクロ