マクロとは
C言語などにおけるマクロとは,プログラム内の文字列をあらかじめ定義されている規則にしたがって置換する機能のことをいい,これをマクロ置換と呼びます.マクロには固有のデータ型がなく,コンパイラによる型の整合性の確認が行われません.
マクロは,#define
というプリプロセッサ指令により定義します.
プリプロセッサとは,コンパイルに先立って行われるプリプロセス(前処理)を行うプログラムのことで,それに対する指令がプリプロセッサ指令で,前処理指令やディレクティブとも呼ばれます.
マクロには,プログラム中の文字列を単に指定した文字列に変換するオブジェクト形式マクロと,引数を用いて関数のようにふるまう関数形式マクロの2種類があります.
オブジェクト形式マクロ
オブジェクト形式マクロはプログラム内の文字列を指定した文字列に変換するもので,以下のように定義します.
#define 文字列1 文字列2
このように定義することでプログラム内の文字列1を文字列2に置換することができ,文字列2は省略することもできます.
よく使われる例として,#define NUM 10
などとして配列の要素数を指定したり,定数を定義したりします.定数に置換するマクロ名は,一般に全て大文字で記述します.
このマクロを利用するメリットとして,
- マクロの定義部分だけを変更すればすべてに反映されるため,保守性が向上して修正がしやすいこと.
- 単なる値としてではなく,文字列に置換することで可読性が向上すること.
などがあります.
関数形式マクロ
関数形式マクロは関数のようにふるまうマクロで,次のように定義します.
#define マクロ名(引数) 処理
関数形式マクロにおいて,引数は型を指定する必要がありません.また,戻り値をreturn
句で返す必要もありません.
実際に以下の例をみてみましょう.
#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
文を併用することも多いです.以下に例を示します.
#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
は指定したマクロが定義されていたら1に,定義されていなければ0になります.条件式のマクロが定義されているときだけ処理が実行されます.
ifdef
やifndef
を用いた条件分岐も行えます.
#ifdef マクロ名
マクロが定義されているときの処理
#endif
#ifndef マクロ名
マクロが定義されていないときの処理
#endif
上のいずれのディレクティブにおいてもelse
句を追加することができます.
分岐の制御のためだけに使うマクロは,置換後の文字列に注目しないので,置換後の文字列を省略して定義することもあります.
インクルードガード
ヘッダファイル内で多数のヘッダファイルをインクルードする際,マクロの定義が重複したり,インクルード先のファイルでインクルード元のファイルをインクルードするといった無限ループが起きてコンパイルエラーとなることがあります.
このような事態を避けるために,先ほどのマクロの分岐を利用したインクルードガードといった仕組みがあります.
#ifndef SAMPLE_H
#define SAMPLE_H
// ヘッダファイルの中身
#endif
#define
で定義される識別子には,一般的にヘッダファイルのファイル名を大文字にしたものが使用されます.
プラグマ#pragma once
を使用してインクルードガードを実現することもあります.
マクロの有効範囲
マクロの有効範囲はコンパイル単位となり,C言語の文法ルールの枠外にあります.ここで,コンパイル単位とは,#include
でインクルードしたファイルが展開された後のファイルのことです.そのため,ヘッダファイルにマクロ定義を書いた場合,そのヘッダファイルをインクルードしたすべてのファイルに影響を及ぼします.
また,マクロは#define
で定義された以降の行で有効で,C言語の規格上,同じ名前のオブジェクト形式マクロの定義が2度現れた場合,置換結果がまったく同じであれば問題ないことになっています.置換結果が異なる場合はエラーとある可能性があります.
複数のファイルで使用するマクロはインクルード用ファイルに記述しておき,そのマクロを使用するファイルにおいて,インクルード用ファイルをインクルードして使用するというのが一般的な使い方です.
注意
#define int float
のようなマクロも作成可能であり,有効範囲も広いため,マクロの使用は慎重に行う必要があります.
C++などでは,マクロをできるだけ避けるべきであるといった考え方もありますが,C言語にはconstexpr
変数などがないため,マクロを使う必要性が出てくる場面があります.
マクロの無効化
マクロを無効にするには,#undef
ディレクティブを使います. 存在しないマクロ名を指定した場合は何も起こりません.記述方法は以下の通りです.
#undef マクロ名
マクロの効力は#undef
の記述がある部分で無効化されるため,マクロの有効範囲を抑えることができます.
#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
と出力されてしまいます.これを解消するには,以下のように2段階でマクロを使用することでうまくマクロを展開できます.
#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
この文字列化演算子はデバッグやログ生成などにも役に立ちます.
トークン連結演算子
トークン連結演算子「##」を用いると,2つのトークンを結合して新しいトークンを生成できます.
#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
の値が出力されます.
トークン連結演算子においても,パラメータにマクロを使用する場合は,先ほどのようにマクロを2段階にして展開する必要があります.
まとめ
今回はC言語のマクロについて整理しました.
定数や関数などを再利用でき,紹介してきたようにとても便利な機能ですが,C言語のルールに準拠せずにプリプロセッサで処理されることや,影響範囲が広いことから,使い方を間違えると意図しない問題が起こってしまいます.
マクロを含め,プログラムがプリプロセッサなどで,どのように実行されているのかを一度探究してみても面白いかもしれませんね✨