関数マクロ(#define)
| 対応: | C89(1989) |
|---|
引数を取るマクロです。関数のように使えますが、コンパイル前にプリプロセッサが単純なテキスト置換を行います。インライン展開されるため関数呼び出しのオーバーヘッドがなく、型に依存しない汎用的なコードが書けます。
構文
// 引数なしの定数マクロです。
#define 定数名 値
// 引数を取る関数マクロです(引数と全体を括弧で囲みます)。
#define マクロ名(引数1, 引数2) ((引数1) 演算 (引数2))
// 複数行マクロはバックスラッシュで行を継続します。
#define マクロ名(x) \
do { \
処理; \
} while (0)
// 文字列化演算子(# )です。引数を文字列リテラルに変換します。
#define STRINGIFY(x) #x
// トークン連結演算子(##)です。2つのトークンを結合します。
#define CONCAT(a, b) a##b
関数マクロの記法と注意点
| 記法・演算子 | 概要 |
|---|---|
| 引数を括弧で囲む | 『#define SQUARE(x) ((x) * (x))』のように引数を括弧で囲みます。展開時の演算子優先順位による誤動作を防ぎます。 |
| 全体を括弧で囲む | マクロ全体を括弧で囲むことで、式の中に埋め込んだときの意図しない結合を防ぎます。 |
| do { } while (0) | 複数文マクロを安全に書くイディオムです。if文の本体に使われても正しく動作します。このイディオムを使わないと、if文の中でマクロを呼んだ際に意図しない動作になる場合があります。 |
| # (文字列化) | マクロ引数をそのままダブルクォートで囲んだ文字列リテラルに変換します。 |
| ## (トークン連結) | 2つのトークンを結合して1つの識別子にします。 |
サンプルコード
sample_define_macro.c
#include <stdio.h>
/* よくある関数マクロの例です。 */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define ABS(x) ((x) >= 0 ? (x) : -(x))
/* # で変数名を文字列化するデバッグマクロです。 */
#define PRINT_INT(x) printf(#x " = %d\n", (x))
/* do-while(0) を使った複数文マクロです。 */
#define SWAP(type, a, b) \
do { \
type _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while (0)
/* ## でトークンを連結します。 */
#define MAKE_VAR(name, num) name##num
int main(void) {
int x = 5, y = 3;
printf("MAX(%d, %d) = %d\n", x, y, MAX(x, y));
printf("MIN(%d, %d) = %d\n", x, y, MIN(x, y));
printf("SQUARE(%d) = %d\n", x, SQUARE(x));
printf("ABS(-7) = %d\n", ABS(-7));
int score = 98;
PRINT_INT(score); /* score = 98 と出力されます。 */
printf("交換前: x=%d, y=%d\n", x, y);
SWAP(int, x, y);
printf("交換後: x=%d, y=%d\n", x, y);
int MAKE_VAR(val, 1) = 100; /* val1 = 100 に展開されます。 */
printf("val1 = %d\n", MAKE_VAR(val, 1));
/* 副作用のある式をマクロに渡してはいけません。 */
/* int a = 3; printf("%d\n", SQUARE(a++)); */ /* a が2回インクリメントされます。 */
return 0;
}
コンパイルして実行すると次のようになります。
gcc define_macro.c -o define_macro ./define_macro MAX(5, 3) = 5 MIN(5, 3) = 3 SQUARE(5) = 25 ABS(-7) = 7 score = 98 交換前: x=5, y=3 交換後: x=3, y=5 val1 = 100
デバッグログマクロを作る
__FILE__・__LINE__・__func__ などの定義済みマクロと組み合わせると、ファイル名・行番号・関数名を自動で付けたデバッグログマクロが作れます。
define_debug_macro.c
#include <stdio.h>
#define LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define ASSERT(cond) \
do { \
if (!(cond)) { \
fprintf(stderr, "ASSERT FAILED: %s (%s:%d)\n", #cond, __FILE__, __LINE__); \
} \
} while (0)
int main(void) {
int hp = 100;
LOG("hp = %d", hp);
hp -= 150;
LOG("ダメージ後 hp = %d", hp);
ASSERT(hp >= 0); /* hp が負なのでアサート発火 */
ASSERT(hp <= 100); /* こちらは通過 */
LOG("処理完了");
return 0;
}
コンパイルして実行すると次のようになります。
gcc define_debug_macro.c -o define_debug_macro ./define_debug_macro [define_debug_macro.c:14] hp = 100 [define_debug_macro.c:17] ダメージ後 hp = -50 ASSERT FAILED: hp >= 0 (define_debug_macro.c:19) [define_debug_macro.c:22] 処理完了
よくあるミス
よくあるミス: 引数に副作用のある式を渡す
マクロはテキスト置換なので、引数に副作用のある式(インクリメントなど)を渡すと展開後に複数回評価されます。
define_sideeffect_ng.c
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
/* NG: a++ が2回評価されて a が2回インクリメントされる */
int a = 3;
int result = SQUARE(a++); /* ((a++) * (a++)) に展開される */
printf("SQUARE(a++) 後の a = %d, result = %d\n", a, result);
/* a が 5 になり result も不定 */
/* NG: 副作用のある式を MAX に渡す */
int x = 5, y = 3;
int m = MAX(x++, y++); /* 大きい方が2回インクリメントされる */
printf("MAX(x++, y++) 後の x = %d, y = %d, m = %d\n", x, y, m);
/* OK: 変数を事前に計算してから渡す */
int b = 3;
int sq = b * b;
printf("正しい SQUARE(3) = %d\n", sq);
return 0;
}
修正後は次の通りです。
gcc define_sideeffect_ng.c -o define_sideeffect_ng ./define_sideeffect_ng SQUARE(a++) 後の a = 5, result = 12 MAX(x++, y++) 後の x = 7, y = 4, m = 5 正しい SQUARE(3) = 9
概要
関数マクロはプリプロセッサによるテキスト置換のため、引数に副作用のある式(インクリメントなど)を渡すと意図しない動作になります。『SQUARE(a++)』は『((a++) * (a++))』に展開され、インクリメントが2回起きます。C99以降では同じ目的に『inline』関数や型汎用の『_Generic』を使うことを検討してください。
引数を括弧で囲まないと演算子優先順位の問題が起きます。例えば『#define DOUBLE(x) x * 2』の場合、『DOUBLE(1 + 2)』は『1 + 2 * 2 = 5』と展開されます。引数は必ず括弧で囲んでください。
定数マクロについては『#include / #define(定数)』を、条件付きコンパイルについては『#ifdef / #ifndef』を参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。