【C言語】プリプロセッサについて解説!#includeや#defineの意味が理解できる!

プリプロセッサ解説ページアイキャッチ

皆さんが最初にC言語を学んだときにおそらく Hello, World を表示したと思います。

その時に、おまじないのように #include <stdio.h> を書かされませんでしたか?

書いた書いた!

いまだによく意味が分かってない…

この #include はプリプロセッサ指令と呼ばれるものになります。

このページでは、このプリプロセッサ指令・プリプロセッサがどのようなものであるかを解説していきたいと思います。

なぜ #include <stdio.h> というおまじないが必要であったかも理解できると思います!

プリプロセッサとは

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

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

しかし、実は多くのC言語のソースコードにおいては、そのままコンパイルを行わず、一旦前処理を行ってからコンパイルが行われています。

その前処理はプリプロセスと呼ばれ、プリプロセスを行ってくれるプログラムをプリプロセッサと呼びます。

プリプロセスとは、プログラマーが記述したC言語ソースコードをコンパイラが理解できるプリミティブなC言語コードに変換する(コンパイル前の)前処理です。

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

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

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

前処理って何??

分かりやすい例で言うと、コメントの削除だね!

コメントはコンパイルには不要だから、コンパイル前に削除しちゃうんだ

プリプロセッサが行う処理に「コメントの削除」があります。このコメントはプログラマーにとってはソースコードを読みやすくしてくれる非常に重要なものです。

ただしコンパイル時には不要です。コンパイラはコメントなんかなくてもソースコードが理解できるのです。

こういったソースコードを読みやすくするためには必要だけど、コンパイルには不要な情報を消したりするのがプリプロセッサが行う処理である前処理(プリプロセス)です。

後述で説明しますが、消すだけではなくさまざまなことをプリプロセッサは行ってくれます。

プリプロセッサの実行

最近ではコンパイラがこのプリプロセッサの役割も担っていることが多いです。

その場合、自分でプリプロセッサを実行しなくてもコンパイラが自動的にその処理も行ってくれます

例えば gcc はコンパイルを実行するだけでプリプロセッサとコンパイル両方を実行してくれます。

ですので、おそらく皆さんが意識するのは「ソースコード」と、コンパイル後に生成される「実行可能ファイル」のみだと思います。

ですが、実はプリプロセッサ直後のデータを確認することも可能です。

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

プリプロセス後のコード表示
gcc -E main.c

以降では、いろんなプリプロセッサ処理後の結果をお見せしますが、全てこのコマンドで表示したものになります。

スポンサーリンク

プリプロセッサ指令

プリプロセッサはプリプロセッサ指令に基づいて処理を行います。

このプリプロセッサ指令はディレクティブとも言います。

皆さんがC言語プログラミングで良く使用する #include や、#define もプリプロセッサ指令です。

ここからはプリプロセッサ指令にどんなものがあるか、その指令でプリプロセッサがどのような処理を行うのかについて解説していきたいと思います。

#include

#include は、<> 内に指定したヘッダーファイルの中身をソースコード内に組み込むプリプロセッサ指令です。

#include の記述の仕方

下記のように <> 内にヘッダーファイル名を指定します。"" でヘッダーファイル名をしたり、ファイルへのパスを指定することもあります。

#include文の記述
#include <ヘッダファイル名>

プリプロセッサの処理

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

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 をプリプロセッサが処理した後のコードは下記のようになります。

#includeをプリプロセスした後のコード
# 1 "main.c"
# 1 "main.c"
# 1 "" 1
# 1 "" 3
# 362 "" 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;
}

注目は下記の部分です。これは header.h の内容になります。

#includeでヘッダーが組み込まれる様子
int add(int, int);

つまり、#include の部分が置き換わって、#include で指定したヘッダーの中身が main.c に組み込まれています。。

このように、プリプロセッサは #include 文の部分を、指定したヘッダーの中身に置き換える処理を実行します。

今回は簡単な自作の header.h で試しましたが、よく使用する stdio.hstdlib.h においても、#include 文の部分がこれらのファイルの中身に置き換えられることになります。

で、この置き換えが行われた後に、コンパイルが行われて実行ファイルが生成されることになります。

ここで仮に #include 文なんて存在しない世界を考えてみましょう。

この場合、例えば fopen の戻り値の型である FILE 構造体を利用しようと思うと、わざわざ FILE 構造体が定義されている stdio.h の中身(FILE 構造体が stdio.h で定義されている前提で書いてます)をソースコードにコピペしたり、FILE 構造体が定義されている部分のみをコピペしたりする必要が出てきます。

これってめちゃめちゃ面倒ですよね…。

ですが実際は、#include を用いれば、プリプロセッサが勝手にソースコードにヘッダーファイルを組み込んでくれるので、めちゃめちゃ楽にプログラミングできるようになっています。

確かにこう聞くと #include ってめちゃめちゃ便利なんだね!

みんな当たり前に使ってるから恩恵感じないかもしれないけど、実はプログラミングしやすくするために非常に重要な役割を果たしているんだ

で、この「プログラミングしやすくする」ためのプリプロセッサ指令をコンパイルしやすい形に変換してくれるのがプリプロセッサだよ!

こんな感じで、ここから説明するプリプロセッサ指令も #include 同様に、プログラミングしやすくするためのものが多いです。

ここからも、プリプロセッサにより「どのようにプログラミングしやすくなっているか」を意識しながら読んでいただけるとよりプリプロセッサの重要性を理解しやすくなると思います。

#define(定数のマクロ)

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

マクロ…?

C言語でのマクロは「ある規則によって何かを何かに置き換えること」を言うよ

つまり、#define で「何を」「何に」置き換えるかの規則を定義しておくと、プリプロセス時にその置き換えを行ってくれるんだ

#define では定数のマクロと関数のマクロを定義することが多いです。

この節では前者の「定数のマクロ」について解説し、次の節で後者の「関数のマクロ」について解説します。

#define(定数)の記述の仕方

#define で定数のマクロを定義する際には下記のように記述します。

#define マクロ名 定数

定数は数がついているからといって数値である必要はないです。文字列などでも良いです。

プリプロセッサの処理

下記の 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 - 1; i++){
        x[i] = 'a';
    }

    x[STR_NUM_MAX - 1] = '\0';
    printf("%s\n", x);

    return 0;
}

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

#defineをプリプロセスした後のコード
# 1 "main.c"
# 1 "" 1
# 1 "" 3
# 362 "" 3
# 1 "" 1
# 1 "" 2
# 1 "main.c" 2


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

    for(i = 0; i < 100 - 1; i++){
        x[i] = 'a';
    }

    x[100 - 1] = '\0';
    printf("%s\n", x);

    return 0;
}

ここで注目していただきたいのはプリプロセッサ処理後では STR_NUM_MAX が全て 100 に置き換わっている点です。

このように置き換えが行われているのは、下記の #define で、STR_NUM_MAX を定数 100 に置き換えるマクロを定義しているからです。

STR_NUM_MAXを100に置き換えるマクロ
#define STR_NUM_MAX 100

このように、#define の後に、#define の引数として「置き換え前の文字列」と「置き換え後の文字列」を記述することで、ソースコード内の文字列を置き換えることができます。

うーん、わざわざ #define で置き換えしなくても、

そのまま 100 って書けば済む話じゃないの?

いや、この #define で置き換えを使うことでもちろんメリットがあるよ

#define での置き換えを行うことで、下記のようなメリットがあります。

  • ソースコードが読みやすくなる
  • 変更が楽になる

単純にソースコードに数値が記述されているだけだと、その数値の意味(どこからその数値が来ているのか?)が分かりにくいんですよね…。

特にそのソースコードを書いた人以外がソースコードを読むときに、いちいちその数値の意味を考えないといけないので大変です。

なので、数値そのものではなく、その数値の意味を表す文字列として表すことで、ソースコードが読みやすくなります。

また、複数箇所に同じ意味を持つ数値が使用されている場合、その数値が変わると複数箇所に対して修正を行う必要があります。

一方で、#define での置き換えを実施している場合、#define の引数の1箇所のみを修正すれば良いため、変更が楽になります。

スポンサーリンク

#define(関数のマクロ)

#define は 定数だけでなく関数のマクロを定義することも可能です。

#define(定数)の記述の仕方

#define で定数のマクロを定義する際には下記のように記述します。

#define文(関数)の記述例
#define マクロ名(引数) 処理

ポイントはマクロ名の後ろの () 内に引数を記述するところです。処理ではこの引数を受け取り、その引数を使用した処理を行うことができます。

引数は複数指定することができます。

プリプロセッサの処理

関数のマクロの動きを下記の main.c で確認してみましょう!

main.c
#define ADD(x, y) ((x) + (y))

int main(void){
    ADD(2, 3);

    return 0;
}

プリプロセッサ処理後のソースコードは下記のようになりました。

#defineをプリプロセスした後のコード
# 1 "main.c"
# 1 "" 1
# 1 "" 3
# 362 "" 3
# 1 "" 1
# 1 "" 2
# 1 "main.c" 2


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

    return 0;
}

こちらも定数のマクロで紹介した #define 同様に、#define した内容に従って置き換えが行われていることが確認できると思います。

ただ、定数のマクロとは異なり、引数が指定された状態で置き換えが行われています。ここが関数のマクロのポイントです。

#define では、マクロ名の後ろの () 内に引数を指定することで、その引数処理#define の第2引数)に渡した状態で置き換えを行うことができます。

ちょうど、マクロ名(引数)の部分が関数の宣言、処理の部分が関数内の処理のような感じになります。

うーん、これこそメリットが良くわかんない…

普通に関数定義して、その関数呼び出しを行えばいいだけじゃん

どちらかというと、このプリプロセッサ指令はソースコードの読みやすさ、書きやすさを向上させるものではなくて、処理速度を向上させる目的で使用されるよ!

この関数のマクロを用いることで下記のメリットが得られます(他にもメリットあるかもしれませんが…)。

  • 関数呼び出し時の負荷がなくなる

若干ですが、関数は呼び出すと実は負荷がかかります。分かりやすく言うと関数を呼び出すのにちょっとだけ時間がかかります。

例えば、深いループの中で関数を呼び出すようなプログラムを作ってしまうと、関数呼び出しが大量に行われることになります。

そうなると、関数呼び出しにかかる時間が積もり積もってプログラムの処理速度を大幅に低下させてしまうようなこともあります。

一方で、関数のマクロの場合、関数のマクロ呼び出し部分が処理そのものに置き換わるため、関数の呼び出しが行われません。

なので関数呼び出しの負荷をなくすことができます。

なので、前述のようにループの深い位置で関数呼び出しを行うような場合、関数呼び出しの時間が気になるのであればこの関数のマクロを利用することで改善することができることがあります。

__FILE____LINE__

先ほど説明した #define は、プログラマー自身がマクロを定義するものでした。

実は、C言語では事前に定義されているマクロ(識別子)が存在しており、これらのマクロは #define することなく使用することができます。

その中でも良く使用する下記の2つを紹介したいと思います。

  • __FILE__:ファイル名(文字列)に置き換える
  • __LINE__:行数(整数)に置き換える

__FILE____LINE__ の記述

ファイル名や行数を表示したい位置で、printf 関数などを用いて表示します。

定義済マクロの記述
printf("file:%s\n", __FILE__);
printf("line: %d\n", __LINE__);

上記により、printf を実行しているソースコードのファイル名、printf を実行している行の行数を表示することができます。

プリプロセッサの処理

下記の main.c を用いてプリプロセッサの処理によりどのようにコードが変わるかを確認してみましょう。

main.c
#include <stdio.h>
  
int test(void){
   char func[256];
   int line;

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

   return 0;
}

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

__FILE__などをプリプロセスした後のコード
int test(void){
   char func[256];
   int line;

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

   return 0;
}

__FILE__ がファイル名である "main.c" に、__LINE__printf が実行されている行の行数である 7 にそれぞれ置き換わっています。

このようにプリプロセッサは、__FILE____ LINE__ をファイル名や行数に置き換える処理を行います。

ソースコードのファイル数や行数が多くなると、どこで printf が行われて表示されたかを追跡するのが大変になります。

かといって、わざわざ printf で表示する際に一緒にファイル名や行数を自身で記述するのも面倒です。

そんな時にこの __FILE____ LINE__ を用いれば、プリプロセッサが自動的にファイル名や行数に置き換えてくれるので便利です。

例えばエラーが発生した時に、エラーの原因と __FILE____ LINE__ を表示するようにすれば、どこでエラーが発生したかがすぐに分かるようになります。

確か __func__ もあるんだよね

関数名に置き換えてくれるから便利

良く知ってるね!

ただ __func__ はプリプロセッサによって置き換えられているわけではなくて、おそらくコンパイラによって置き換えらるものだからここでは省略したよ

使い方は __FILE____LINE__ と同じだから、何が置き換えるかは気にせず同じように使用していいと思うよ!

#ifdef#endif#else

これらのプリプロセッサを用いることで、ソースコードを部分単位でコンパイルするかどうかを切り替えることができます。

C言語で普通にプログラミングするときにも if 文や else 文を使いますよね。

この if 文や else 文は「プログラム実行時」に条件に応じて「実行する処理を切り替えるも」のですが、ここで説明する #ifdef#else などは「プリプロセス時」に条件に応じて「コンパイルするコードを切り替える」ものです。

このように条件に応じてコンパイルするコードを切り替えることを「条件付きコンパイル」と呼びます。

他にも下記のようにコンパイルするコードを切り替えるプリプロセッサ指令はありますが、今回は #ifdef#endif#else の3つを用いて動作を解説していきたいと思います。

  • #if
  • #ifndef
  • #elif

#ifdef#endif#else の記述

#ifdef#endif を用いて条件付きコンパイルを行う際の記述は下記のようになります。

条件付きコンパイル1
#ifdef マクロ名
    処理1
#endif

この例では、マクロ名が “定義されている場合” のみ、処理1 の部分がコンパイルされることになります。

さらに #else を用いて条件付きコンパイルを行う際の記述は下記のようになります。

条件付きコンパイル2
#ifdef マクロ名
    処理2
#else
    処理3
#endif

この例では、マクロ名が “定義されている場合” は 処理2 の部分がコンパイルされて 処理3 の部分はコンパイルされないことになります(処理3 の部分はプリプロセッサの処理で削除される)。

逆に マクロ名が “定義されていない場合” は 処理3 の部分がコンパイルされて 処理2 の部分はコンパイルされないことになります(処理2 の部分はプリプロセッサの処理で削除される)。

スポンサーリンク

プリプロセッサの処理

下記の main.c を用いて、#ifdef#endif#else がどのようにプリプロセッサによって処理されるのかを確認してみましょう。

main.c
#define DEBUG_MODE
  
#ifdef DEBUG_MODE
#include 
#endif

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

    x = 5;
    y = 10;

#ifdef DEBUG_MODE
    z = x * y;
    printf("z = %d\n", z);
#else
    z = x * y
#endif

    return 0;
}

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

#ifdefなどをプリプロセスした後のコード1
# 1 "main.c"
# 1 "" 1
# 1 "" 3
# 362 "" 3
# 1 "" 1
# 1 "" 2
# 1 "ifdef.c" 2
# stdio.h 部分は省略
# 5 "mainc" 2


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

    x = 5;
    y = 10;


    z = x * y;
    printf("z = %d\n", z);




    return 0;
}

前述の main.c の最初の行の #define DEBUG_MODE により DEBUG_MODE が定義されているため、#ifdef DEBUG_MODE が成立することになります。

この例のように、#define ではマクロ名のみを定義することが可能です

これを利用して #ifdef によりさまざまな条件付きコンパイルを行うことができます

そのためプリプロセッサ処理後は #ifdef#endif 及び、 #ifdef#else までのコードが残り、逆に #else#endif のコードが削除されています。

今度は main.c で下記のように #define DEBUG_MODE をコメントアウトしてみましょう。

このコメントアウトにより DEBUG_MODE が未定義の状態になります。

コメントアウト後のmain.c
//#define DEBUG_MODE
  
#ifdef DEBUG_MODE
#include 
#endif

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

    x = 5;
    y = 10;

#ifdef DEBUG_MODE
    z = x * y;
    printf("z = %d\n", z);
#else
    z = x * y
#endif

    return 0;
}

プリプロセッサ処理後の結果が下記のように変化します。

[codebox title="#ifdefなどをプリプロセスした後のコード2"]
# 1 "main.c"
# 1 "" 1
# 1 "" 3
# 362 "" 3
# 1 "" 1
# 1 "" 2
# 1 "main.c" 2






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

    x = 5;
    y = 10;





    z = x * y


    return 0;
}

DEBUG_MODE が未定義ですので、#ifdef DEBUG_MODE は成立しません。

その為、先程の例とは逆に、プリプロセッサ処理後は #ifdef#endif 及び、 #ifdef#else までのコードが削除され、#else#endif のコードが残ることになります。

こんな感じで、#ifdef#endif#else を用いることで、マクロ名の定義・未定義を変更するだけでコンパイルするコードを切り替えることが可能です。

今回示した例のように、デバッグ時や動作確認時のみ printf で動作確認用の表示を行ったり、コンパイルする環境によってコンパイルを行うソースコード自体を切り替えを行ったりする場合に便利です。

例えばコンパイルを行う OS によってコンパイルするコード自体を切り替える目的で使用されることも多いです。

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

特に #define での利用時には注意が必要な点があります。

#define でのマクロ名と定数や処理への置き換えは非常に単純に行われます。

例えば下記の main.c を見てみましょう。

main.c
#include <stdio.h>
 
#define N 10 + 5
#define M 2

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

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

    return 0;
}

表示される x の値はどうなるでしょうか?

N には 10 + 5 の結果の 15 が格納されて、M には 2 が格納されるわけだから x = N * M30 じゃない?

実は x の値は 20 になるよ!

なんで…

上記の main.c をコンパイルして実行すると、x の値としては 20 が表示されます。

この理由は main.c をプリプロセッサで処理した結果を見てみると分かりやすいと思います。

main.cをプリプロセスした結果
int main(void){
    int x;
    x = 10 + 5 * 2;

    return 0;
}

この結果を見てみると分かるように、プリプロセッサはソースコード上の文字列を単にマクロ名から定数や処理へ置き換えするだけです。

本当にそのまま置き換えられます。

なので、例えば下記を変数への値の格納と同様に考えてしまうと、N15 が格納された状態で計算が行われると勘違いしてしまいます。

Nの#define
#define N 10 + 5

実際にプリプロセッサで行われる置き換えは単純にソースコード上の文字列が N10 + 5 です。

このように、プリプロセッサでは単純な置き換えしか行われないことを理解した上で #define でのマクロ定義を行わないと、上記のように計算結果がおかしくなる可能性があるので注意が必要です。

例えば #define 時には、マクロ名に置き換えられる定数や式を括弧で括るようにすれば、上記のように計算おかしくなることを防ぐことができます。

修正後のmain.c
#include <stdio.h> 
 
#define N (10 + 5)
#define M (2)

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

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

    return 0;
}

基本的に #define でのマクロ定義では、マクロ名に置き換える定数や処理は括弧でくくっておく方が無難だと思います。

まとめ

このページではまずプリプロセッサについて解説を行い、基本的なプリプロセッサ指令(ディレクティブ)について紹介を行いました。

「プリプロセッサのことは知らなかったけど実は結構使ってた!」という人も多いと思います。

プリプロセッサを活用することにより、ソースコードを読みやすく・書きやすくすることができますので、どんどん使っていきましょう!

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

ぜひ皆さんも自身が書いたソースコードがプリプロセッサによりどのように処理されるかを確認してみてください。

コメントを残す

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