この記事は主にGCCドキュメントに言及しているため、他のコンパイラでは詳細(マクロパラメータのスペースが処理されるかどうかなど)が若干異なる場合があります。対応するドキュメントを参照してください。
マクロの基本
マクロは、Cの前処理段階での単なるテキスト置換ツールであり、コンパイル後はバイナリコードからは見えません。基本的な使用法は次のとおりです。
- 識別子エイリアス
#define BUFFER_SIZE 1024
前処理段階ではfoo = (char *) malloc (BUFFER_SIZE)
、;は;に置き換えられfoo = (char *) malloc (1024)
ます。
マクロの本文の折り返しには、行末に円記号が必要です\
#define NUMBERS 1, \
2, \
3
前処理段階int x[] = { NUMBERS }
;に拡張されint x[] = { 1, 2, 3 }
ます;
- マクロ関数
マクロ名の後に括弧が付いているマクロは、マクロ関数と見なされます。使用法は通常の関数と同じですが、マクロ関数が前処理段階で展開される点が異なります。利点は、通常の関数でレジスタとパラメータ転送を保存するためのオーバーヘッドがないことです。拡張されたコードは、CPUキャッシュと命令予測の利用に役立ち、速度が高速です。欠点は、実行可能コードが大きいことです。
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
y = min(1, 2)
;に拡張されy = ((1) < (2) ? (1) : (2))
ます;
マクロの特殊な使用法
- 文字列化
マクロ本体で、マクロパラメータの前に#
マクロパラメータを追加すると、マクロ本体が展開されるときにマクロパラメータが文字列の形式に展開されます。といった:
#define WARN_IF(EXP) \
do { \
if (EXP) { \
fprintf (stderr, "Warning: " #EXP "\n"); \
} \
} while (0)
WARN_IF (x == 0)
;に拡張されます:
do {
\
if (x == 0) {
\
fprintf (stderr, "Warning: " "x == 0" "\n"); \
} \
} while (0);
この使用法はで使用できます。assert
アサーションが失敗した場合、失敗したステートメントをフィードバックメッセージに出力できます。
- 連結は
マクロ本体にあります。マクロ本体が識別子##
にある場合、マクロ本体が展開されると、マクロパラメータは識別子に直接置き換えられます。といった:
#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command
{
char *name;
void (*function) (void);
};
マクロ展開中
struct command commands[] =
{
COMMAND (quit),
COMMAND (help),
...
};
に拡張されます:
struct command commands[] =
{
{
"quit", quit_command },
{
"help", help_command },
...
};
これにより、多くの時間が節約され、効率が向上します。
いくつかのピット
-
文法上の問題
プレーンテキストの置換であるため、Cプリプロセッサはマクロ本体の文法チェックを行いません。括弧とセミコロンがないプリプロセッサは問題ではありません。ここでは特に注意してください。これはあらゆる種類の奇妙な問題につながる可能性があり、根本原因を一度に見つけることは困難です。 -
演算子の優先順位の問題
マクロ本体がプレーンテキストに置き換えられるだけでなく、マクロパラメータもプレーンテキストに置き換えられます。乗算を実現するための次の単純なマクロがあります。
#define MULTIPLY(x, y) x * y
MULTIPLY(1, 2)
問題ありません、正常に拡張します1 * 2
。問題は、この種の表現です。MULTIPLY(1 + 2, 3)
展開した後1 + 2 * 3
、明らかに優先順位が間違っています。マクロ本体で、引用符で囲まれたパラメーターに括弧を追加すると、この問題を回避できます。
#define MULTIPLY(x, y) (x) * (y)
MULTIPLY(1+2, 3)
に展開され(1 + 2) * (3)
、優先度は正常です。実際、この問題と以下に述べる問題のいくつかは、プレーンテキストの置換によって引き起こされる意味破壊の問題に属しているので、特に注意してください。
- セミコロン嚥下問題
次のマクロ定義があります。
#define SKIP_SPACES(p, limit) \
{ \
char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; \
break; \
} \
} \
} \
次のコードがあるとします。
if (*p != 0)
SKIP_SPACES (p, lim);
else ...
コンパイル、GCCが報告しましたerror: ‘else’ without a previous ‘if’
。関数のように見えるこのマクロは、中括弧で囲まれたコードブロックに展開されていることがわかります。セミコロンを追加すると、ifロジックブロックが終了するため、コンパイラは、これに対応するifがないことを検出します。この問題は通常、次do ... while(0)
の形式で解決されます。
#define SKIP_SPACES(p, limit) \
do { \
char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; \
break; \
} \
} \
} while (0)
展開後は
if (*p != 0)
do ... while(0);
else ...
これにより、セミコロンを飲み込む問題が解消されます。この手法は、このセットマクロ(arch / mips / include / asm / mach-pnx833x / gpio.hにあります)などのLinuxカーネルソースコードで非常に一般的です。
#define SET_REG_BIT(reg, bit) do { (reg |= (1 << (bit))); } while (0)
- マクロパラメータの繰り返し呼び出し
次のマクロ定義があります。
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
次の呼び出しが行われるnext = min (x + y, foo (z));
と、マクロ本体が展開されnext = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));
、foo(z)が2回呼び出され、計算が繰り返されていることがわかります。さらに深刻なことに、fooが再入可能でない場合(グローバル変数または静的変数がfooで変更される場合)、プログラムは論理エラーを生成します。したがって、マクロパラメータで関数呼び出しを渡さないようにしてください。
- それ自体への再帰的参照
次のマクロ定義があります。
#define foo (4 + foo)
以前の理解によれば、それ(4 + foo)
はに拡張され(4 + (4 + foo))
、その後、メモリが使い果たされるまで拡張し続けます。ただし、プリプロセッサで採用されている戦略は、一度だけ拡張することです。つまり、にfoo
展開されるだけで、展開(4 + foo)
後foo
の意味は文脈に応じて決まります。
以下の相互参照では、マクロ本体は1回だけ展開されます。
#define x (4 + y)
#define y (2 * x)
x
(4 + y) -> (4 + (2 * x))
にy
展開し、に展開し(2 * x) -> (2 * (4 + y))
ます。これは強く推奨される書き込み方法であり、プログラムの可読性は非常に低いことに注意してください。
- マクロパラメータの前処理
マクロパラメータに別のマクロが含まれている場合、マクロ本体に#
またはが含まれていない限り、マクロパラメータはマクロ本体に置き換えられる前に完全に展開されます##
。次のマクロ定義があります。
#define AFTERX(x) X_ ## x
#define XAFTERX(x) AFTERX(x)
#define TABLESIZE 1024
#define BUFSIZE TABLESIZE
AFTERX(BUFSIZE)
に展開されX_BUFSIZE
ます。マクロ本体にはが含まれているため、マクロ##
パラメータはマクロ本体に直接置き換えられます。XAFTERX(BUFSIZE)
に展開されX_1024
ます。ので、XAFTERX(x)
マクロ本体でありAFTERX(x)
、何が存在しない#
、または##
、それはBUFSIZE
完全に置換される前に、1024に拡大し、その後になるためにマクロ本体に代入しますX_1024
。
参考資料:http://gcc.gnu.org/onlinedocs/cpp/Macros.html