C言語の「#include」「#define」の意味は?プリプロセッサについて解説!

皆さんが最初にC言語を学んだときにおそらく「Hello, World」を表示したと思います。その時に、おまじないのように「#include<stdio.h>」を書きませんでしたか?この include はプリプロセッサ指令と呼ばれるものになります。このページでは、このプリプロセッサ指令・プリプロセッサがどのようなものであるかを解説していきたいと思います。なぜこのおまじないが必要であったかが分かると思います。

プリプロセッサとは

C言語におけるプリプロセッサとは、コンパイルする前に、ソースコードに前処理を行うプログラムのことを言います。

C言語では、コンパイラがC言語ソースコードをコンパイルし、その後アセンブルやリンク処理が行われて実行可能なファイルが生成されます。

しかし、実は多くのC言語のソースコードにおいては、そのままコンパイルを行わず、一旦前処理を行ってからコンパイルが行われています。その前処理はプリプロセスと呼ばれ、プリプロセスを行ってくれるプログラムをプリプロセッサと呼びます。

C言語ソースコードが実行可能ファイルに変換されるまでの流れは下のページでまとめていますので興味がある方は読んでみてください。

C言語ソースコードが実行可能ファイルになるまでの流れ

このページではこの中のプリプロセッサの処理に焦点を当てて解説を行います。

プリプロセッサの実行

最近ではコンパイラがこのプリプロセッサの役割も担っていることが多いです。その場合、自分でプリプロセッサを実行しなくてもコンパイラが自動的にその処理も行ってくれます。例えば gcc はコンパイルを実行するだけでプリプロセッサとコンパイル両方を実行してくれます。

プリプロセッサ直後のデータを確認することも可能です。確認方法はコンパイラによって異なりますが、 gcc であれば下記のようにオプション -E を不可してやれば、プリプロセッサ処理直後のデータが表示されます。

gcc -E main.c

スポンサーリンク

プリプロセッサ指令

プリプロセッサはプリプロセッサ指令に基づいて処理を行います。このプリプロセッサ指令はディレクティブとも言います。皆さんがC言語プログラミングで良く使用する include や、 define もプリプロセッサ指令です。ここからはプリプロセッサ指令にどんなものがあるか、その指令でプリプロセッサがどのような処理を行うのかについて解説していきたいと思います。

#include

include は指定したファイルの中身ををinclude 指定元のファイルに組み込むプリプロセッサ指令です。

記述の例

#include <ヘッダファイル名>

プリプロセッサの処理

例えば下記のようなソースコードとヘッダーファイルがあるとします。

main.c
#include "header.h"
  
int main(void){
    add(3, 5);
    return 0;
}

int add(int x, int y){
    return x + y;
}
header.h
int add(int, int);

main.cをプリプロセッサが処理した後のデータは下記のようになります。

# 1 "main.c"
# 1 "" 1
# 1 "" 3
# 361 "" 3
# 1 "" 1
# 1 "" 2
# 1 "main.c" 2
# 1 "./header.h" 1
int add(int, int);
# 2 "main.c" 2

int main(void){
    add(3, 5);
    return 0;
}

int add(int x, int y){
    return x + y;
}

注目ポイントは、#include の部分が置き換わって、#include で指定したヘッダーの中身がmain.cに組み込まれている点です。このようにプリプロセッサは #include 部分を指定したヘッダーの中身に置き換える処理を実行します。今回は簡単な自作の header.h で試しましたが、よく使用する stdio.h や stdlib.h においても、#include で指定された部分はこれらのファイルの中身に置き換えられることになります。

最初におまじないとして記述していた #include <stdiio.h> は stdio.h をソースコードに組み込み、それにより stdio.h の中にあるprintf関数の宣言をmain関数から見えるようにするためのプリプロセッサ指令だったのです。

#define

#define は マクロを定義するプリプロセス指令になります。

記述の例1

#define N 100

記述の例2

#define add(x, y) ((x)+(y))

プリプロセッサの処理

下記の main.c を

main.c
#define STR_NUM_MAX 100
  
int main(void){
    int i;
    char x[STR_NUM_MAX];

    for(i = 0; i < STR_NUM_MAX; i++){
        x[i] = i % 100;
    }

    return 0;
}

をプリプロセッサで処理すると下記のようになります。

# 1 "define.c"
# 1 "" 1
# 1 "" 3
# 361 "" 3
# 1 "" 1
# 1 "" 2
# 1 "define.c" 2


int main(void){
    int i;
    char x[100];

    for(i = 0; i < 100; i++){
        x[i] = i % 100;
    }

    return 0;
}

#define で定義したマクロ STR_NUM_MAX が展開され 100 に置換されていることがわかると思います。このようにプリプロセッサは #define のプリプロセッサ指令で定義されたマクロを展開(マクロを定義したものに置換)する処理を実行します。

マクロは記述の例2のように関数を定義することもできます。下記の main.c を

main.c
#define add(x, y) ((x) + (y))
  
int main(void){
    add(2, 3);

    return 0;
}
をプリプロセッサで処理すると下記のようになります。

# 1 "define2.c"
# 1 "" 1
# 1 "" 3
# 361 "" 3
# 1 "" 1
# 1 "" 2
# 1 "define2.c" 2


int main(void){
    ((2) + (3));

    return 0;
}

こちらも先ほどの #define 同様に add部分が展開されてadd(2, 3)が((2) + (3))に置き換わっていることが分かると思います。このように #define では定数だけでなく関数マクロを定義することも可能です。

#define を使用せずに、ソースコード中に定数を記述してしまうことも可能なのですが、数字だけだとそれがどういう意味か分かりにくくなってしまいます。#define であれば数字の意味をマクロ名にすることができるので、その数字がどういう意味のものかが分かりやすいです。

また同じ定数を何回もソースコード中にそのまま記述すると、その定数が変わった場合に、その定数を使用している箇所を全て修正する必要があります。しかしその定数を #define で定義しておけば、この #define によるマクロの定義値の1箇所のみを変更してやるだけで全体のソースコードを修正すること可能で、利便性が高いです。

#ifdef #endif #elseif

#ifdef で指定されたマクロが定義されているかどうかで、ソースコードをコンパイル対象にするかどうかを切り替えることができます。

記述の例1

#ifdef TEST
    プログラム
#endif

記述の例2

#ifdef TEST
    プログラム
#elseif
    プログラム
#endif

プリプロセッサの処理

下記の main.c を

main.c
#define MUL
  
int main(void){
   int x, y, z;

   x = 5;
   y = 10;
#ifdef MUL
   z = x * y;
#endif

   return 0;
}

プリプロセッサで処理すると下記のようになります。

# 1 "ifdef.c"
# 1 "" 1
# 1 "" 3
# 361 "" 3
# 1 "" 1
# 1 "" 2
# 1 "ifdef.c" 2


int main(void){
   int x, y, z;

   x = 5;
   y = 10;

   z = x * y;


   return 0;
}

z = x * y; 部分のソースコードがあることが確認できます。しかし main.c で下記のように #ifdef MUL をコメントアウトすると、

//#define MUL
  
int main(void){
   int x, y, z;

   x = 5;
   y = 10;
#ifdef MUL
   z = x * y;
#endif

   return 0;
}

プリプロセッサ処理後の結果は下記のように変わります。

# 1 "ifdef.c"
# 1 "" 1
# 1 "" 3
# 361 "" 3
# 1 "" 1
# 1 "" 2
# 1 "ifdef.c" 2


int main(void){
   int x, y, z;

   x = 5;
   y = 10;




   return 0;
}

今度は z = x * y; 部分が消えてしまっています。このように #ifdef および #endif (さらには#elseif)を用いることで、#ifdef で指定したマクロが定義されているかどうかでコンパイル対象にするかどうかを切り替えることができます。プリプロセッサはこれらの #ifdef, #endif, #ifdef を解釈してコンパイル対象にするかどうかを切り替える処理を実行します。

動作確認しながらソースコード作成していく時に便利です。デバッグ用の表示を #ifdef – #endif で囲み、本番リリース時に #ifdef で指定したマクロ定義をコメントアウトしてやると、デバッグ時にのみデバッグ用の表示が行われるようになるので、デバッグ用とリリース用途で処理を切り替えることができます。

__FILE__ __LINE__

__FILE__ はこれが記述されているファイル名の文字列、__LINE__ はこれが記述されている行数に置き換えられます。これによってエラーが起こっている箇所を特定することができます。

記述の例1

printf("This file is %s\n", __FILE__);

記述の例2

printf("Error! line : %d\n", __LINE__);

プリプロセッサの処理

下記のmain.cを

main.c
#include 
  
int main(void){
   char func[256];
   int line;

   printf("file : %s, line : %d\n", __FILE__, __LINE__);

   return 0;
}

プリプロセッサで処理すると下記のようになります(stdio.hが組み込まれている部分はは省略)。

int main(void){
   char func[256];
   int line;

   printf("file : %s, line : %d\n", "main.c", 7);

   return 0;
}

__FILE__ がファイル名である”main.c”に、__LINE__ が元々の main.c で記載されている行数である 7 に置き換わっています。プリプロセッサは __FILE__ や __LINE__ をファイル名や行数に置き換える処理を実行します。

プログラムの規模が大きくなってソースコードのファイル数や行数が大きくなると、エラーが起こった場所を特定するのが大変になることがあります。エラーが発生する箇所にファイル名と行数が表示されると特定を楽にすることができますが、わざわざ行数を数えたりファイル名を記述したりするのは面倒です。__FILE__、__LINE__ではそれらを具体的に書かなくても、プリプロセッサが具体的なファイル名や行数に置き換えてくれるのでプログラムを書くのが楽になります。

プリプロセッサで気をつける点

プリプロセッサは基本的にプリプロセッサ指令の部分を文字列や数字・空白に置き換えるものです。プリプロセッサ指令はプログラミングしやすくなったり読みやすくなったりするのでどんどん活用して良いですが、注意点があります。それは、プリプロセッサの置き換え方は非常に単純であることです。

例えば下記のプログラムを見てみましょう。

main.c
#include 
  
#define N 10 + 5
#define M 2

int main(void){
    int x;
    x = N * M;

    printf("%d\n", x);

    return 0;
}

xの値はどうなるでしょうか?Nが15でMが2になるので30になると思う方もいると思います。おそらくこのプログラムを作る人もそれを意図していると思いますが。ですが、結果は20になります。上記プログラムをプリプロセッサで処理した後の結果は下記のようになります。

int main(void){
    int x;
    x = 10 + 5 * 2;

    return 0;
}

これを見ると結果が20になることに納得してもらえると思います。足し算よりも掛け算の方が演算優先順位の方が高いので結果は20になります。

プリプロセッサは単純にマクロを定義したものにそのまま置き換えるだけです。ですのでソースコードはマクロが展開された時にどのようになるのかを考慮して作成する必要があります。例えば下記のように #define を変更すると、マクロが展開されてもまずNが計算されてから N * M の計算が行われるようになります。基本的に #define でのマクロの定義値は括弧でくくっておく方が無難だと思います。

main.c
#include 
  
#define N (10 + 5)
#define M (2)

int main(void){
    int x;
    x = N * M;

    printf("%d\n", x);

    return 0;
}

まとめ

いかがだったでしょうか?プリプロセッサのことは知らなかったけど、実は結構使ってた!という人も多いと思います。プリプロセッサを活用すると読みやすいソースコードを作れるのでどんどん使っていきましょう!

プリプロセッサ処理後のデータを見ると、自分の書いたソースコードに対してプリプロセッサがどのような処理をしてくれたかが分かるので面白いです。興味があれば試してみてください。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です