C言語入門|マクロ関数のしくみと注意点

― 関数のように見える、ただの文字列置換

#define は、数値や文字列を置き換えるだけの命令だと思っていませんか?
実は #define には、引数を受け取る形も用意されています。

この仕組みを使うと、見た目はまるで関数のような記述ができるのです。
ただし、ここには C言語らしい落とし穴 が待っています。

引数を持つ #define ― マクロ関数

まずは、次の例を見てみましょう。

サンプルプログラム

プロジェクト名:13-6-1 ソースファイル名: sample13-6.1.c

#include <stdio.h>

#define ADD(X, Y) X + Y

int main(void)
{
    printf("%d\n", ADD(4, 9));
    return 0;
}

7行目を見ると、ADD という関数を呼び出しているように見えますね。
しかし、これは 関数ではありません

ADD は、あくまで #define による置換ルールです。

プリプロセッサは、ADD(4, 9) を次のように置き換えます。

printf("%d\n", 4 + 9);

このように、引数を伴った文字列置換を行う #define を
マクロ関数 と呼びます。

マクロ関数は「関数」ではない

マクロ関数は、第8章で学んだ通常の関数とはまったく別物です。

項目通常の関数マクロ関数
処理タイミング実行時コンパイル前
型チェック行われる行われない
実体機械語として存在文字列置換のみ

マクロ関数は、
少し賢くなった文字列置換機能
にすぎません。

そのため、マクロ定数と同様の注意点に加えて、
さらに厄介な副作用が発生する可能性があります。

予期しない動作が起きる例

次のコードを見て、結果を想像してみてください。

printf("%d\n", ADD(4, 9) * 2);

もし ADD が通常の関数なら、

  • ADD(4, 9) → 13
  • 13 * 2 → 26

となり、26が表示されるはずです。

しかし、マクロ関数では次のように置換されます。

printf("%d\n", 4 + 9 * 2);

C言語では、掛け算が足し算より優先されるため、

  • 9 * 2 → 18
  • 4 + 18 → 22

結果は 22 になります。

これは、マクロ関数が
式全体を考慮せず、単純に文字列を並べただけ
だから起こる現象です。

カッコで守る ― マクロ関数の基本ルール

このような事故を防ぐために、
マクロ関数には「お作法」があります。

#define ADD(X, Y) ((X) + (Y))

こうしておけば、

ADD(4, 9) * 2

は次のように展開されます。

((4) + (9)) * 2

これなら、先に足し算が行われ、
期待どおり 26 が得られます。

マクロ関数の安全ルール

  • 引数は必ずカッコで囲む
  • 置換後の式全体もカッコで囲む

この2点は、必須事項として覚えておきましょう。

それでもマクロが使われてきた理由

ここまで読むと、こう思うかもしれません。

「危ないなら、普通の関数を使えばいいのでは?」

その判断は、現代の開発では正解です。

ただし、C言語が誕生した当初は、

  • CPU性能が低い。
  • メモリが極端に少ない。

という環境でした。

マクロ関数は、

  • 関数呼び出しのオーバーヘッドがない。
  • 展開後は単なる式になる。

という理由から、
速度とメモリ効率を最優先する場面で重宝されてきたのです。

現代C言語におけるマクロ関数の立ち位置

現在では、

  • 高性能なCPU
  • 十分なメモリ
  • 高度な最適化を行うコンパイラ

が当たり前になっています。

インライン展開や最適化によって、
通常の関数でもマクロと同等、あるいはそれ以上の効率が得られます。

そのため、

  • 新規開発で積極的に使う理由は少ない。
  • ただし、既存コードを読むためには必須知識

という位置づけになっています。

マクロ関数との付き合い方

マクロ関数は、

  • 強力
  • 危険
  • 読みにくい

という性質を併せ持っています。

マクロ利用ポリシー

積極的に使う理由はあまりない。
しかし、理解しておく理由は非常に大きい

過去の資産やライブラリを正しく理解するためにも、
マクロ関数の仕組みと注意点は、しっかり頭に入れておきましょう。