閉じる

C言語の関数風マクロの作り方:引数付き#defineの書き方と落とし穴

C言語で処理を軽くしたい、短い式を簡潔に書きたい、というときに便利なのが関数風マクロです。

これはプリプロセッサの#defineで引数を受け取る形を定義し、関数のように使えるようにする仕組みです。

本記事では、基本の書き方から実用例、落とし穴、関数との使い分けまでを丁寧に解説します。

関数風マクロ入門

引数付き#defineの基本

プリプロセッサと展開のしくみ

関数風マクロは、コンパイルの前段階であるプリプロセス時に、ソースコードの文字列として置換される仕組みです。

つまり、「呼び出す」のではなく「展開される」だけで、実行時コストはありません。

型の概念はなく、型チェックや引数個数の検査は一切行われない点が、関数との大きな違いです。

定義と使用の最小例

最小の関数風マクロは次のように書きます。

#defineの直後にマクロ名、その直後に引数リストを丸括弧で書きます。

C言語
// sample_min.c
#include <stdio.h>

// 引数xを1増やす関数風マクロ。展開は ((x) + 1)
#define ADD1(x) ((x) + 1)

int main(void) {
    int a = 5;
    printf("ADD1(a) = %d\n", ADD1(a));      // 6
    printf("ADD1(10) = %d\n", ADD1(10));    // 11
    return 0;
}
実行結果
ADD1(a) = 6
ADD1(10) = 11

関数との違い

マクロと関数の根本的な差

マクロはテキスト置換、関数は実行時の処理です。

以下の観点を押さえておくと使い分けが明確になります。

観点関数風マクロ関数
実体テキスト置換(コンパイル前)実行可能コード(コンパイル後)
型チェックなしあり
オーバーヘッド呼び出しなし(ゼロ)呼び出しオーバーヘッドあり(最適化やinlineで低減)
副作用の再評価起こりうる引数は一度だけ評価
デバッグステップイン不可ステップイン可
可読性・保守性式が複雑だと悪化署名と本体が明確

短く単純な式ならマクロが便利ですが、複雑な処理や型安全が必要な箇所は関数が向きます。

引数付き#defineの書き方と例

書式とスペースの注意

正しい定義の書式

定義のときはマクロ名と左括弧の間にスペースを入れてはいけません

これが最重要の基本ルールです。

  • 正: #define NAME(x, y) ((x) + (y))
  • 誤: #define NAME (x, y) ((x) + (y)) ← これは「関数風」にならず、単なるオブジェクト様マクロになります

呼び出し側ではNAME(1, 2)でもNAME (1, 2)でも動作します。

「定義」ではスペース禁止、「呼び出し」はスペース許容と覚えてください。

以下はわざと誤った定義例です。

このコードはコンパイルエラーになります

C言語
// bad_space.c
#define SQUARE (x) ((x) * (x))  // 誤: NAME と '(' の間のスペース

int main(void) {
    int v = SQUARE(3);          // 期待どおり展開されずエラー
    return 0;
}

括弧の付け方の大原則

  • 実体全体を括弧で包むこと
  • 各引数の出現箇所も括弧で包むこと

#define SQUARE(x) ((x) * (x)) のように、全体と引数に括弧を徹底します。

SQUARE(x)の作り方

正しい定義と間違った定義の差

次の2つの定義を見比べてください。

C言語
// square_good_bad.c
#include <stdio.h>

// 悪い例: 引数xや全体を括弧で包んでいない
#define SQUARE_BAD(x) x * x

// 良い例: 引数も全体も括弧で包む
#define SQUARE(x) ((x) * (x))

int main(void) {
    int a = SQUARE_BAD(2 + 3);  // 展開: 2 + 3 * 2 + 3 → 11
    int b = SQUARE(2 + 3);      // 展開: (2 + 3) * (2 + 3) → 25
    printf("SQUARE_BAD(2 + 3) = %d\n", a);
    printf("SQUARE(2 + 3) = %d\n", b);
    return 0;
}
実行結果
SQUARE_BAD(2 + 3) = 11
SQUARE(2 + 3) = 25

演算子の優先順位を誤らないために括弧は必須です。

SQUARE_BADは2 + 3 * 2 + 3のように展開され、意図どおりの計算になりません。

MAX(a,b)の作り方

三項演算子と括弧の徹底

最大値を返す関数風マクロは三項演算子を使うのが定番です。

C言語
// max_macro.c
#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main(void) {
    printf("MAX(10, 20) = %d\n", MAX(10, 20));
    printf("MAX(1 + 2, 3 + 4) = %d\n", MAX(1 + 2, 3 + 4)); // 7
    // 文字と整数でも動く(型チェックはない)
    printf("MAX('A', 65) = %d\n", MAX('A', 65));
    return 0;
}
実行結果
MAX(10, 20) = 20
MAX(1 + 2, 3 + 4) = 7
MAX('A', 65) = 65

MAXのような式マクロでも、引数と全体の括弧を必ず付けることが正確さのカギです。

複数行マクロの書き方

バックスラッシュとdo{…}while(0)イディオム

複数文からなるマクロは、行末に\を置いて継続します。

このときバックスラッシュの後ろに空白やコメントを置かないことに注意します。

また、単一の文として振る舞わせるためにdo { ... } while(0)で包むのが定番です。

C言語
// swap_macro.c
#include <stdio.h>

// 悪い例: ブロックで包まないと if/else と組み合わせたときに崩れる
#define SWAP_BAD(T, x, y) \
    T tmp__ = (x);        \
    (x) = (y);            \
    (y) = tmp__

// 良い例: do { ... } while(0) で1文として扱える
#define SWAP(T, x, y)            \
    do {                         \
        T tmp__ = (x);           \
        (x) = (y);               \
        (y) = tmp__;             \
    } while(0)

int main(void) {
    int a = 3, b = 7;
    printf("before: a=%d, b=%d\n", a, b);

    // if と一緒でも安全に使える
    if (a < b)
        SWAP(int, a, b);
    else
        puts("no swap");

    printf("after : a=%d, b=%d\n", a, b);
    return 0;
}
実行結果
before: a=3, b=7
after : a=7, b=3

複数行マクロは最後の行以外、各行末に必ずバックスラッシュを置く行末以外に置かないという2点を守りましょう。

関数風マクロの落とし穴と注意点

括弧で全体と引数を包む

なぜ全体と引数の両方に括弧が必要か

  • 引数の括弧は呼び出し側が演算子を含む式を渡したときの安全策です。
  • 全体の括弧はマクロの展開結果が更なる式の一部として使われるときの優先順位保持に効きます。

「引数も全体も括弧」は、関数風マクロの鉄則です。

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

ありがちな失敗と回避

次のように、係数を掛ける位置で誤動作します。

C言語
// precedence_trap.c
#include <stdio.h>

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

int main(void) {
    printf("1 + SQUARE_BAD(2 + 3) = %d\n", 1 + SQUARE_BAD(2 + 3));
    printf("1 + SQUARE(2 + 3)     = %d\n", 1 + SQUARE(2 + 3));
    return 0;
}
実行結果
1 + SQUARE_BAD(2 + 3) = 12
1 + SQUARE(2 + 3)     = 26

必ず括弧で包むことだけで、これらのトラブルはほぼ回避できます。

副作用に注意

引数が複数回評価される問題

マクロは単なるテキスト置換のため、引数式が展開中に複数回評価されることがあります。

インクリメントや関数呼び出しなど副作用のある式を渡すと、思わぬ挙動になります。

C言語
// side_effect.c
#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main(void) {
    int i = 3, j = 4;
    int r = MAX(i++, j++); // (i++) と (j++) が最初に1回ずつ評価
                           // より大きい方(ここでは j)が選ばれて、さらにもう1回評価される
    printf("r=%d, i=%d, j=%d\n", r, i, j);
    return 0;
}
実行結果
r=5, i=4, j=6

MAX(i++, j++)のような使い方は避け、副作用のある式は事前に一時変数へ格納してから渡すのが安全です。

デバッグしにくい点は簡潔な式で回避

ステップ実行と可視性の問題

マクロは実体が存在しないためデバッガでステップインできません

また、-Eオプションで前処理後のコードを出力できるものの、可読性は落ちます

  • 複雑なロジックは関数化する
  • マクロは短い式に限定する
  • 必要ならstatic inline関数へ置換する

といった方針で、「マクロは短く」を徹底しましょう。

マクロと関数の使い分け

短い計算はマクロが向く

ごく短い式、たとえばSQUARE(x)ABS(x)のような単純な変換はマクロが向きます。

関数呼び出しのオーバーヘッドがないため、ホットパスで微小な差を詰めたいときに役立ちます。

ただし、副作用や優先順位を壊さないよう必ず括弧を徹底します。

型安全や読みやすさは関数

ロジックが複雑、または型安全が必要な場合は関数にしましょう。

関数ならコンパイラが型不一致を検出し、デバッガでステップ実行が容易です。

inline関数という選択肢

関数の読みやすさを保ちながらオーバーヘッドを抑えたいときはinlineも有力です。

C99以降ではstatic inlineが使えます。

C言語
// inline_alt.c
#include <stdio.h>

// マクロ代替の inline 関数
static inline int square_i(int x) {
    return x * x; // 引数は1回しか評価されず型安全
}

static inline int max_i(int a, int b) {
    return (a > b) ? a : b;
}

int main(void) {
    printf("square_i(5) = %d\n", square_i(5));
    printf("max_i(3, 7) = %d\n", max_i(3, 7));
    return 0;
}
実行結果
square_i(5) = 25
max_i(3, 7) = 7

副作用やデバッグ容易性、型安全が気になる場面では inline 関数の採用を検討すると良いでしょう。

まとめ

関数風マクロは、プリプロセス段階のテキスト置換で手軽に高速化や簡潔さを得られる強力な道具です。

一方で、定義時のスペース禁止引数と全体の括弧徹底副作用を渡さない複数行はdo{…}while(0)といったルールを守らないと、優先順位の破綻や意図せぬ多重評価、デバッグ困難といった落とし穴に陥ります。

短い計算ならマクロ、型安全や可読性が大切なら関数、両立を図るならinlineと、目的に応じて適切に使い分けることがC言語の初学者にとっての最短の上達法です。

この記事を書いた人
エーテリア編集部
エーテリア編集部

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

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

URLをコピーしました!