C言語のマクロについてまとめる

# マクロとは
C言語などにおけるマクロとは，**プログラム内の文字列をあらかじめ定義されている規則にしたがって置換する機能**のことをいい，これを**マクロ置換**と呼びます．マクロには**固有のデータ型がなく，コンパイラによる型の整合性の確認が行われません**．  
マクロは，`#define`というプリプロセッサ指令により定義します．

:::note
**プリプロセッサ**とは，コンパイルに先立って行われるプリプロセス(前処理)を行うプログラムのことで，それに対する指令が**プリプロセッサ指令**で，**前処理指令**や**ディレクティブ**とも呼ばれます．
:::

マクロには，プログラム中の文字列を単に指定した文字列に変換する**オブジェクト形式マクロ**と，引数を用いて関数のようにふるまう**関数形式マクロ**の２種類があります．

## オブジェクト形式マクロ
オブジェクト形式マクロはプログラム内の文字列を指定した文字列に変換するもので，以下のように定義します．  
`#define 文字列１ 文字列２`  
このように定義することでプログラム内の文字列１を文字列２に置換することができ，文字列２は省略することもできます．  
よく使われる例として，`#define NUM 10`などとして配列の要素数を指定したり，定数を定義したりします．定数に置換するマクロ名は，一般に**全て大文字で記述**します．  
このマクロを利用するメリットとして，

- マクロの定義部分だけを変更すればすべてに反映されるため，保守性が向上して修正がしやすいこと．
- 単なる値としてではなく，文字列に置換することで可読性が向上すること．

などがあります．

## 関数形式マクロ
関数形式マクロは関数のようにふるまうマクロで，次のように定義します．　　
`#define マクロ名(引数)　処理`   
関数形式マクロにおいて，引数は型を指定する必要がありません．また，戻り値を`return`句で返す必要もありません．  
実際に以下の例をみてみましょう．  
```c:関数形式マクロ
#include <stdio.h>   

#define PI 3.14 // オブジェクト形式マクロで円周率を定義
#define area(r) (r*r*PI) // 関数形式マクロで面積計算
#define prt(f) printf("%f\n", f) // 関数形式マクロで標準出力
     
int main(void) {
  prt(area(3.0));

  return 0;
}
```
```:実行結果
28.260000
```
マクロ置換が行われることで，関数呼び出し時のオーバーヘッドがない反面，呼び出しが多いとプログラムのサイズが増加するといった特徴があります．  

# マクロの応用
ここからはマクロの応用例をいくつか紹介したいと思います．

## 複数行に分けてマクロを書く
複数行にわたってマクロを定義する場合は，各行の終わりにプリプロセッサの制御命令であるバックスラッシュ「\」を付けて改行します．このとき，**バックスラッシュの後には空白やコメントも含め，何も書いてはいけない**ことに注意してください．  
また，可読性やエラー対策として，`do-while`文を併用することも多いです．以下に例を示します．
```c:複数行のマクロ
#include <stdio.h>

#define MULTI_LINE_MACRO(x, y) \
  do { \
    printf("x：%d\n", x); \
    printf("y：%d\n", y); \
  } while (0)

int main() {
  int a = 10;
  int b = 20;

  MULTI_LINE_MACRO(a, b);

  return 0;
}
```
```:実行結果
x：10
y：20
```

## マクロによる分岐
マクロが定義されているかどうかを判断基準にして，プリプロセスにおける分岐を行えます．  
`if defined`を使用した記述方法は以下の通りです．
```
#if defined(マクロ名)
  マクロが定義されているときの処理
#endif
```
`defined`は指定したマクロが定義されていたら１に，定義されていなければ０になります．条件式のマクロが定義されているときだけ処理が実行されます．

`ifdef`や`ifndef`を用いた条件分岐も行えます．  
```
#ifdef マクロ名
  マクロが定義されているときの処理
#endif

#ifndef マクロ名
  マクロが定義されていないときの処理
#endif
```
上のいずれのディレクティブにおいても`else`句を追加することができます．

:::note
分岐の制御のためだけに使うマクロは，置換後の文字列に注目しないので，置換後の文字列を省略して定義することもあります．
:::

## インクルードガード
ヘッダファイル内で多数のヘッダファイルをインクルードする際，マクロの定義が重複したり，インクルード先のファイルでインクルード元のファイルをインクルードするといった無限ループが起きてコンパイルエラーとなることがあります．  
このような事態を避けるために，先ほどのマクロの分岐を利用した**インクルードガード**といった仕組みがあります．
```:sample.h
#ifndef SAMPLE_H
#define SAMPLE_H
// ヘッダファイルの中身
#endif
```
`#define`で定義される識別子には，一般的にヘッダファイルのファイル名を大文字にしたものが使用されます．

:::note
プラグマ`#pragma once`を使用してインクルードガードを実現することもあります．
:::

## マクロの有効範囲
マクロの有効範囲はコンパイル単位となり，C言語の文法ルールの枠外にあります．ここで，コンパイル単位とは，`#include`でインクルードしたファイルが展開された後のファイルのことです．そのため，ヘッダファイルにマクロ定義を書いた場合，そのヘッダファイルをインクルードしたすべてのファイルに影響を及ぼします．  
また，マクロは`#define`で定義された以降の行で有効で，Ｃ言語の規格上，同じ名前のオブジェクト形式マクロの定義が２度現れた場合，置換結果がまったく同じであれば問題ないことになっています．置換結果が異なる場合はエラーとある可能性があります．  
複数のファイルで使用するマクロはインクルード用ファイルに記述しておき，そのマクロを使用するファイルにおいて，インクルード用ファイルをインクルードして使用するというのが一般的な使い方です．  

:::note warn
注意
`#define int float`のようなマクロも作成可能であり，有効範囲も広いため，マクロの使用は慎重に行う必要があります．  
C++などでは，マクロをできるだけ避けるべきであるといった考え方もありますが，Ｃ言語には`constexpr`変数などがないため，マクロを使う必要性が出てくる場面があります．
:::

## マクロの無効化
マクロを無効にするには，`#undef`ディレクティブを使います． 存在しないマクロ名を指定した場合は何も起こりません．記述方法は以下の通りです．  
`#undef マクロ名`  
マクロの効力は`#undef`の記述がある部分で無効化されるため，マクロの有効範囲を抑えることができます．
```c:マクロの無効化
#include <stdio.h>

int main(void) {
  // printf("%s¥n", HELLO_WORLD); // コンパイルエラー

  #define HELLO_WORLD "Hello world!"

  printf("%s\n", HELLO_WORLD);

  #undef HELLO_WORLD // マクロの無効化

  // printf("%s¥n", HELLO_WORLD); // コンパイルエラー
  return 0;
}
```
```:実行結果
Hello world!
```

## 関数形式マクロの計算で気を付けること
関数形式マクロを使用するときは記述方法に気を付けないといけません．  
`#define TIMES(x) x * 2`  
上のようなマクロがあった場合，`TIMES(3)`とすると，`3 * 2`と展開されて正しく計算できますが，`TIMES(2 + 5)`として`14`と計算したくても，`2 + 5 * 2`と展開され，意図しない答えとなってしまいます．これは，  
`#define TIMES(x) ((x) * 2)`  
のように，パラメータに括弧を付けることで解決できます．  
このように，マクロはあくまで「文字列の置換」を行うだけなので，マクロで計算を行う際は，できるだけ括弧を付けて，意図した通りに計算を行うようにしておくことが大切です．

## 文字列化演算子
関数形式マクロのみ使用できる文字列化演算子「#」があります．これはパラメータを文字列に変換するもので，  
`#define TOSTRING(x) #x`  
といったマクロがあるとき，
`printf("%s\n", TOSTRING(3));`  
と実行すると，文字列として`3`が出力されます．  
しかし，パラメータに以下のようなマクロを使用するとうまくマクロが展開されません．  
`#define NAME Taro`  
このマクロを`TOSTRING`マクロに入れても`NAME`と出力されてしまいます．これを解消するには，以下のように２段階でマクロを使用することでうまくマクロを展開できます．  
```c:文字列化演算子
#include <stdio.h>

#define TOSTRING(x) #x
#define EXPAND(x) TOSTRING(x) // ここでマクロを展開してからTOSTRINGに代入
#define NAME Taro

int main() {
  printf("%s\n", EXPAND(NAME));

  return 0;
}
```
```:実行結果
Taro
```
この文字列化演算子はデバッグやログ生成などにも役に立ちます．   

## トークン連結演算子
トークン連結演算子「##」を用いると，２つのトークンを結合して新しいトークンを生成できます．
```c:トークン連結演算子
#include <stdio.h>

#define CONCAT(x, y) x ## y // トークンを連結する

int main() {
    int num10 = 10;
    printf("%d\n", CONCAT(num, 10)); // num10の値が表示される
    return 0;
}
```
```:実行結果
10
```
上のコードでは，トークン連結演算子によって`num`と`10`が結合して`num10`といったトークンとなり，int型の変数`num10`の値が出力されます．  
トークン連結演算子においても，パラメータにマクロを使用する場合は，先ほどのようにマクロを２段階にして展開する必要があります．

# まとめ
今回はC言語のマクロについて整理しました．  
定数や関数などを再利用でき，紹介してきたようにとても便利な機能ですが，C言語のルールに準拠せずにプリプロセッサで処理されることや，影響範囲が広いことから，使い方を間違えると意図しない問題が起こってしまいます．  
マクロを含め，プログラムがプリプロセッサなどで，どのように実行されているのかを一度探究してみても面白いかもしれませんね✨