【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){
    int a = 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){
    int a = ((2) + (3));

    return 0;
}

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

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

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

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

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

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

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

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

  • 型に依存しない関数が実現できる
  • 関数呼び出し時の負荷がなくなる

関数のマクロにより型に依存しない関数が実現できる

関数を作成する場合、必ず戻り値と引数の型を指定する必要があります。

したがって、同様の処理を行う関数であっても、戻り値や引数の型が異なるのであれば別の関数として用意する必要があります。

例えば下記のように2つの int 型の引数の足し算を行って、その足し算結果を int 型として返却する関数 add_int が既にあったとしても、

int用のadd関数
int add_int(int x, int y) {
    return x + y;
}

この関数では double 型の足し算を行いたいような場合には使用することができません。これは引数と戻り値の型が int 型で double 型でないため、正常に足し算を行うことができないためです(小数点以下が切り捨てられて計算される)。

そのため、double 型の足し算を関数実行により行いたい場合、引数と戻り値の型を double 型とした別の関数を用意する必要があります。

double用のadd関数
double add_double(double x, double y) {
    return x + y;
}

add_intadd_double は引数と戻り値の型は異なるものの、関数内部の処理は全く同じなので、異なる型用にわざわざ関数を作るのももったいない気がしますね…。

こんな時に便利なのが関数のマクロです。

関数のマクロの場合、上記の ADD(x, y) のように結局はプリプロセッサによってそのマクロが単なる 処理#define の第2引数)に置き換えられるので、置き換え後には引数は存在しなくなります(処理 の一部に組み込まれる)。

ですので、関数のマクロの場合、引数や戻り値の型は当然指定する必要はありませんし、その 処理を実行可能な変数や値であれば、どんな型の引数に対しても処理を実行することが可能です。

したがって、上記の add_intadd_double を用意しなくても、前述のマクロの関数 ADD(x, y) を用意しておけば、これだけで int 型や double 型の足し算を行うことができます。

上記のように、引数や戻り値の型は違うものの、関数内部の処理が全く同一のものであるような関数であれば、この関数のマクロを利用することで定義を1つにまとめられて便利です。呼び出し側も使用する変数の型に応じて関数の呼び分けをする必要もなくなります。

ただ、後述の プリプロセッサで気をつける点 でも説明していますが、マクロの置き換えは非常に単純に行われますので、通常の関数を使用するときよりも関数のマクロを使用した時の方がバグが発生しやすくなります。

また、通常の関数を利用する場合、コンパイル時に実引数と仮引数の型のチェックが行われ、不適切な引数が関数に指定された場合は警告が表示されます。

ですので、その警告から誤った関数の使い方をしていることに気づくことができるのですが、関数のマクロの場合はこういったチェックが行われないため、誤った関数のマクロの使用をしていることに気づきにくいです。つまり、バグが発生する可能性があっても、その可能性に気づきにくくなります。

こういった理由より、関数のマクロは便利な場合もありますが、むしろバグが発生することを考えると不便な場合の方が多いです。したがって、基本は関数のマクロではなく、通常の関数を使用する方が良いです。

使うとしたら、「非常に単純な処理」「引数の型によらず同じ処理を行う」「引数としていろんな型の変数(値)が指定される」を全て満たす時くらいかなぁと思います。

例えば下記ページで絶対値の算出を関数のマクロで行う例を示していますが、これも上記の3つを満たしており、これくらいなら関数のマクロを使用しても良いかなと思います。

C言語で絶対値を求める方法の解説ページアイキャッチ 【C言語】絶対値を求める方法(abs関数の利用・関数使わない・マクロなど)

関数のマクロにより関数呼び出しの負荷がなくなる

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

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

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

例えば下記のようなソースコードのプログラムを実行してみると、関数呼び出しの負荷がどれくらいであるかを測ることができます。

関数呼び出しの負荷の計測
#include <stdio.h>
#include <time.h>

#define NUM 1000000000

#define FUNC(x) ((x) + (x))

long long func(long x) {
    return x + x;
}

int main(void) {

    long long i;
    long long x;
    clock_t start, end;

    start = clock();
    for (i = 0; i < NUM; i++) {
        x = func(i);
    }
    end = clock();

    printf("func cpu time:%f\n", (double)(end - start) / CLOCKS_PER_SEC);

    start = clock();
    for (i = 0; i < NUM; i++) {
        x = FUNC(i);
    }
    end = clock();

    printf("macro cpu time:%f\n", (double)(end - start) / CLOCKS_PER_SEC);
    
    return 0;
}

私の環境だと下記のような結果になりました(処理がスキップされることを防ぐために最適化 OFF でコンパイル)。

func cpu time:3.506775
macro cpu time:2.791070

関数からマクロに置き換えることで多少処理時間が減っていることが確認できると思います。他の要因もあるかもしれませんが、基本的には関数呼び出しの負荷が無くなったことにより処理時間が減ったと考えることができると思います。

が、上記は10億回のループを行った時の差であり、1回あたりの関数呼び出しの負荷は非常に小さいです。

ですので、上記のように大量にループを行う中で関数呼び出しを行なっているような場合は多少効果はありますが、それ以外の場合は、関数のマクロに置き換えることで得られる処理速度的なメリットは低いと考えた方が良いです(むしろ前述のようにバグが出やすくなるというデメリットの方が大きい)。

__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 でのマクロ定義では、マクロ名に置き換える定数や処理は括弧でくくっておく方が無難だと思います。

まとめ

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

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

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

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

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

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示