閉じる

C言語の関数風マクロ(引数付き)の作り方と注意点

関数風マクロ(引数付き)は、関数のように見える書き方で使えるプリプロセッサマクロです。

コンパイル前にテキストとして展開されるため、高速で柔軟に書けますが、書き方にコツがあり、うっかりするとバグの温床にもなります。

本記事では、安全に使うための作り方と注意点を、C言語初心者の方向けに丁寧に解説します。

関数風マクロ(引数付き)とは

関数との違いを初心者向けに

関数風マクロは、#defineで定義して引数を受け取る形にしたマクロです。

見た目は関数の呼び出しと同じですが、実体はコンパイル前に行われるテキスト置換です。

つまり、SQUARE(x)のような呼び出しは、コンパイル時点では単に((x)*(x))のような文字列に置換されます。

ざっくり比較

観点関数風マクロ関数
実体テキスト置換機械語のサブルーチン
型チェックなしあり(コンパイラが検査)
引数の評価複数回評価されることがある1回のみ
デバッグしにくいしやすい
実行時オーバーヘッド呼び出しなし呼び出し分のオーバーヘッド(最適化やinlineで軽減)
定義の配置ヘッダに置きやすいヘッダでもstatic inline推奨、通常はソースに定義

型安全性やデバッグしやすさは関数が有利で、展開の軽さや型に縛られない操作はマクロが有利というイメージを持つと理解しやすいです。

コンパイル前に展開されるしくみ

Cのビルドは大まかに、プリプロセス→コンパイル→リンクの順に進みます。

関数風マクロはプリプロセス段階で展開され、コンパイラは展開済みのソースを入力として受け取ります。

展開イメージ

次のように定義して使ったとします。

C言語
#define SQUARE(x) ((x) * (x))

int main(void) {
    int x = 3;
    int y = SQUARE(x);  // ここが展開される
    return y;
}

プリプロセッサ後のイメージは次のようになります(説明用の参考イメージです)。

C言語
/* 展開後(イメージ) */
int main(void) {
    int x = 3;
    int y = ((x) * (x));
    return y;
}

この「文字列置換」であるという事実が、演算子の優先順位や副作用の落とし穴につながります

使いどころ

関数風マクロは、短い式を簡潔に表す用途に向いています。

たとえば簡単な算術、ビット操作、固定長の計算などです。

型に依存しないため、同じ式をintでもdoubleでも使いたいといった場面にも便利です。

適したケースの例

短く純粋(副作用がない)な式の再利用、ビットマスク計算、固定サイズ計算などは相性が良いです。

避けたいケース

複雑なロジック、ループやリソース操作、副作用を伴う引数(++や–、関数呼び出し)は不具合の原因になりやすいので、関数やstatic inline関数を選ぶと安全です。

関数風マクロの作り方

基本構文

関数風マクロは#defineで次のように定義します。

引数と展開全体を括弧で包むのが鉄則です。

C言語
/* 基本形: NAME(arg1, arg2, ...) を 置換テキスト に展開 */
#define NAME(args...)  置換テキスト

/* 例: 足し算マクロ(必ず括弧で保護する) */
#define ADD(a, b)  ((a) + (b))

/* 複数行で書く場合は行末に \ を付ける(可読性のためだけで、機能は同じ) */
#define MUL_ADD(a, b, c) \
    (((a) * (b)) + (c))

ここで括弧の徹底がとても大切です。

後述の「正しく書くコツ」で詳しく解説します。

例: SQUARE(x)で二乗

もっとも定番の例は二乗です。

C言語
#include <stdio.h>

/* 引数も全体も括弧で包むのが安全 */
#define SQUARE(x)  ((x) * (x))

int main(void) {
    printf("SQUARE(5) = %d\n", SQUARE(5));       // 25
    printf("SQUARE(1+2) = %d\n", SQUARE(1+2));   // 9 (展開後は((1+2)*(1+2)))
    return 0;
}
実行結果
SQUARE(5) = 25
SQUARE(1+2) = 9

呼び出しは関数と同じ書き方

呼び出しは関数と同じ見た目になります。

NAME(引数...)の形で書けます。

C言語
#include <stdio.h>

#define SQUARE(x)  ((x) * (x))

int main(void) {
    int v = 3;
    printf("関数のように呼べます: SQUARE(%d) = %d\n", v, SQUARE(v));
    return 0;
}
実行結果
関数のように呼べます: SQUARE(3) = 9

式として値に使える

マクロは式としてそのまま使えるため、他の式に組み込めます。

配列長の計算のような定数式でも便利です。

C言語
#include <stdio.h>

/* 二乗マクロ */
#define SQUARE(x)  ((x) * (x))

int main(void) {
    int side = 3;
    int area = SQUARE(side) + 1;  // 式の一部に組み込み
    int arr[SQUARE(3)];           // 9要素の配列(展開後は int arr[9];)

    printf("area = %d\n", area);
    printf("arr length = %zu\n", sizeof(arr) / sizeof(arr[0]));
    return 0;
}
実行結果
area = 10
arr length = 9

正しく書くコツ

引数と全体を括弧で包む

引数も展開全体も括弧で包むのが基本です。

これを怠ると、呼び出し側の演算子の優先順位に巻き込まれ、意図しない計算になります。

C言語
#include <stdio.h>

/* 悪い例: 括弧がない */
#define SQUARE_BAD(x)  x * x
/* 良い例: 括弧で守る */
#define SQUARE_GOOD(x) ((x) * (x))

int main(void) {
    printf("SQUARE_BAD(1+2) = %d\n", SQUARE_BAD(1+2)); // 1+2*1+2 → 5
    printf("SQUARE_GOOD(1+2) = %d\n", SQUARE_GOOD(1+2)); // ((1+2)*(1+2)) → 9
    return 0;
}
実行結果
SQUARE_BAD(1+2) = 5
SQUARE_GOOD(1+2) = 9

常に((x) * (x))のように書く癖を付けると事故を防げます。

演算子の優先順位トラブルを防ぐ

演算子の優先順位(例えば*+より強い)によって、マクロ展開後の式が意図と異なることがあります。

C言語
#include <stdio.h>

/* 悪い例: x*2 は x が (3+4) だと 3+4*2 になってしまう */
#define DOUBLE_BAD(x)  x * 2
/* 良い例: 引数と全体を括弧で保護 */
#define DOUBLE_GOOD(x) ((x) * 2)

int main(void) {
    printf("DOUBLE_BAD(3+4) = %d\n", DOUBLE_BAD(3+4));   // 11
    printf("DOUBLE_GOOD(3+4) = %d\n", DOUBLE_GOOD(3+4)); // 14
    return 0;
}
実行結果
DOUBLE_BAD(3+4) = 11
DOUBLE_GOOD(3+4) = 14

「引数」「各引数の使用箇所」「展開全体」の3層すべてを括弧で包むのがポイントです。

複数文のマクロは避ける

複数文を含むマクロは、if文と組み合わせたときに構文が崩れたり途中でreturnしてしまったりしてバグを生みやすいです。

基本的には複数文のマクロは避け、関数(もしくはstatic inline)で書くのが安全です。

どうしても必要な場合は、次のようにdo { ... } while (0)イディオムで1文に見せかけるのが常套手段です。

C言語
#include <stdio.h>

/* できれば関数で書くのが安全 */
static inline void swap_int(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

/* マクロで書く必要がある場合の安全形(1文として扱える) */
#define SWAP_INT(a, b) do { \
    int t__ = (a);          \
    (a) = (b);              \
    (b) = t__;              \
} while (0)

int main(void) {
    int x = 1, y = 2;
    SWAP_INT(x, y);
    printf("x=%d, y=%d\n", x, y);
    swap_int(&x, &y);
    printf("x=%d, y=%d\n", x, y);
    return 0;
}
実行結果
x=2, y=1
x=1, y=2

結論としては「複数文は関数で」を基本方針にすると安全です。

関数風マクロの注意点と使い分け

副作用と多重評価に注意

マクロは同じ引数を展開中に複数回使うことがあります。

すると副作用(例: ++, –, 関数呼び出し)が複数回発生し、予期しない結果になります。

典型例はMAXマクロです。

C言語
#include <stdio.h>

/* 典型的なMAXマクロ(良い書き方ではない) */
#define MAX(a, b)  ((a) > (b) ? (a) : (b))

/* 安全な代替: 引数を1回だけ評価する関数 */
static inline int max_int(int a, int b) {
    return a > b ? a : b;
}

int main(void) {
    int i = 1, j = 2;
    int k = MAX(i++, j++);     // i++ と j++ が「合計3回」発生しうる
    printf("After MAX macro: i=%d, j=%d, k=%d\n", i, j, k);

    int p = 1, q = 2;
    int r = max_int(p++, q++); // それぞれ1回しか評価されない
    printf("After function : p=%d, q=%d, r=%d\n", p, q, r);
    return 0;
}
実行結果
After MAX macro: i=2, j=4, k=3
After function : p=2, q=3, r=2

このように、マクロは引数が何回評価されるかを保証しません

副作用のある式を引数に渡さない、もしくは関数(やstatic inline)に置き換える判断が重要です。

デバッグしにくい点

マクロはシンボルとして存在しないため、ブレークポイントを置けず、ステップインもできません

原因切り分けには次のような工夫を使います。

展開結果を確認する

コンパイラにより異なりますが、GCCやClangなら-Eオプションでプリプロセス後のソースを確認できます。

gcc -E source.c > preprocessed.c

位置情報をログに出す

__FILE____LINE__といった組み込みマクロを利用して、簡単なログを出すと追跡しやすくなります。

C言語
#include <stdio.h>

/* 1行で完結するロギングマクロ(C99の可変長引数マクロ) */
#define LOG(fmt, ...) \
    fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)

int main(void) {
    int value = 42;
    LOG("start");
    LOG("value=%d", value);
    LOG("end");
    return 0;
}

出力例(ファイル名と行番号は環境に依存):

実行結果
[main.c:8] start
[main.c:9] value=42
[main.c:10] end

関数との使い分け

「短い純粋な式」ならマクロ、「型安全や可読性を重視」するなら関数が基本指針です。

性能面の心配からマクロを選びがちですが、現代のコンパイラは小さな関数をインライン化してくれるため、まずは関数で書いて問題がなければそのまま使うのが安全です。

使い分け早見表

条件推奨
短い算術・ビット式で副作用なし関数風マクロまたはstatic inline
型チェックが欲しい、デバッグしやすくしたい関数(できればstatic inline)
すべての数値型に対応したい(型非依存)マクロ、またはC11の_Genericと関数の併用
複数文やリソース操作を含む関数一択

まずは関数、必要が明確なときだけマクロという順序で検討すると安全です。

名前の付け方

マクロ名は大文字とアンダースコアで表記すると、コードを読む人が「マクロだ」と認識しやすくなります。

またプロジェクト接頭辞(例: MY_)を付けると衝突しにくくなります。

  • 良い例: MY_SQUAREUTIL_MAX
  • 避けたい例: 標準ライブラリと紛らわしい名前、先頭がアンダースコア+大文字(例: _X)や二重アンダースコア(例: __X)は処理系予約なので使わない

「マクロだと一目で分かる命名」と「予約識別子を避ける」の2点を守りましょう。

まとめ

関数風マクロは、短い純粋な式を手早く再利用するための強力な道具です。

一方で、括弧不足による優先順位の誤り副作用の多重評価といった落とし穴もあります。

安全に使うためには、次の原則を心がけてください。

まず、引数と展開全体を必ず括弧で包むこと。

次に、副作用のある引数を渡さないこと。

さらに、複数文は関数(できればstatic inline)で書くことです。

はじめは関数で実装し、必要性が明確な場面でのみ関数風マクロに切り替えるという順序をとると、初心者の方でも安全に活用しやすくなります。

C言語 プリプロセッサとマクロ
この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!