C言語基礎|関数と関数形式マクロの違い

「引数ゼロでも“関数っぽい”。小さな定番処理を、読みやすく安全にまとめよう!」

関数形式マクロというと sqr(x) みたいに引数がある形を想像しがちですが、実は 引数が0個の関数形式マクロ も作れます。

ポイントはこれです。

  • 見た目は関数呼び出し:alert() のように書ける
  • 実体はマクロ展開:コンパイル前に式が埋め込まれる
  • 短い定番処理を“名前付き”で使える:読みやすさアップ

今回は alert() の例を、よりシンプルで分かりやすい別例に置き換えながら、
「どう定義する?」「どう展開される?」「何に注意する?」を、表と図でしっかり説明します。
後半では、演習8-1〜8-3に似た問題も3つ、解答と解説つきで用意しますね。

引数のない関数形式マクロの基本

まず結論:引数がなくても () を付けて呼べる

引数なし関数形式マクロは、こんな形です。

引数なし関数形式マクロの形

種類定義例呼び出し例ざっくり意味
引数なし関数形式マクロ#define NAME() 式NAME()NAME() を式に展開

表の説明
NAME() という「() 付きの形」を見つけたら、後ろの式に展開する、というルールです。
NAME(カッコなし)とは別物として扱われやすいので、定義と呼び出しの両方で () を付けるのがコツです。

サンプルプログラム

ログの区切り線を出すマクロです。

  • マクロ名:bar()
  • 役割:区切り線を表示して改行する(ちょい便利)

サンプル:bar() を呼び出すだけの小さな例

プロジェクト名:chap8-3-1 ソースファイル名:chap8-3-1.c

#include <stdio.h>

#define bar() (puts("---- ここから処理を開始します ----"))

int main(void)
{
    bar();
    puts("計算が終わりました。");

    return 0;
}

実行例

---- ここから処理を開始します ----
計算が終わりました。

図で理解:関数ではなく、展開されて“埋め込まれる”

引数ありの関数形式マクロと同じで、引数なしでも動きは「呼び出し」ではなく「展開」です。

図:プリプロセッサによる展開の流れ

図の説明
bar() は実行時にどこかへ飛ぶわけではなく、コンパイル前に puts("...") に置き換えられます。
つまり、ソースコードが“書き換えられてから”コンパイルされるイメージです。

展開例:bar() は最終的にどうなる?

呼び出し側のこの行は…

bar();

プリプロセッサによって、こう展開されます。

(puts("---- ここから処理を開始します ----"));

関数呼び出しとマクロ展開の違い(引数なし版でも同じ)

観点関数引数なし関数形式マクロ
実行時の動き呼び出して戻る呼び出しではなく展開
コードの増え方本体は1か所使う場所ごとに展開が増える
デバッグ呼び出しとして追える展開後の式を意識する必要あり

表の説明
引数がなくても、マクロは「その場に貼り付く」ので、使いすぎるとコード量(結果のバイナリ)が増える可能性があります。

命令(ここで登場するもの)の書式と役割

ここでは、プログラム内で使った命令・要素を「何をするの?」目線で整理します。

#define の書式(引数なし関数形式マクロ)

書式

#define 名前() 置き換える内容

何をする?
名前() を見つけたら、右側の内容に展開します(コンパイル前)。

puts の書式

書式

puts("文字列");

何をする?
文字列を表示して、最後に改行も出してくれます。ログ表示に便利です。

printf の書式

書式

printf("書式文字列", 値, ...);

何をする?
数値を埋め込むなど、書式付きの出力を行います。
puts は簡単、printf は柔軟、という住み分けですね。

引数なしでも大事:マクロは ( ) で包むと安全

今回の定義はこうでした。

#define bar() (puts("---- ここから処理を開始します ----"))

この ( ) は、将来こういう使い方をしても事故りにくくするためです。

  • if の後に置く
  • 別の式の一部にする(おすすめはしないけど、事故耐性は上がる)

なぜ ( ) で包む?

目的効果
優先順位の事故を防ぐ展開先が他の演算子と混ざっても崩れにくい
見た目を“式”として安定させる展開後の形が読みやすくなる

注意:引数なしでも “副作用” はあり得る

「引数がないなら安全でしょ?」と思いがちですが、展開先が何をするか次第です。

たとえば、もしこんなマクロを作ると…

#define tick() (counter++)

tick() を呼ぶたびに値が変わります。これは意図通りならOKですが、
条件式の中などで使うと、分かりにくい動作になりやすいです。

引数なしマクロの使いどころ

目的向いている例理由
表示・ログbar(), debug_on()効果が分かりやすい
定数っぽい値pi() を 3.14159 に展開など読みやすさが上がる
状態を変える処理tick() で counter++使いどころを限定しないと混乱しやすい

マクロ定義内の式は ( ) で囲む(超重要)

ここは引数ありの話ですが、引数なしでも“考え方”は同じです。
関数形式マクロは 展開された結果が、別の演算子と組み合わさるので、( ) がないと壊れます。

悪い例:全体を囲まない sum_of

#define sum_of(x, y) x + y

これをこう使うと…

z = sum_of(a, b) * sum_of(c, d);

展開はこうなり、意図が崩れます。

z = a + b * c + d;

良い例:引数と全体を ( ) で囲む

#define sum_of(x, y) ((x) + (y))

展開後はこうなり、安全です。

z = ((a) + (b)) * ((c) + (d));

図:なぜ壊れるのか(優先順位のイメージ)

図の説明
C言語は掛け算 * が足し算 + より優先されます。
だから ( ) で守らないと、勝手に計算順が変わってしまいます。

演習問題

ここからは、同じノリで手を動かせる練習問題です!

演習8-1

二つの値の小さいほうを返す関数形式マクロを定義せよ

仕様:min(x, y) は小さいほうの値を返す。

解答例

#define min(x, y) (((x) < (y)) ? (x) : (y))

解説

  • 引数 (x), (y) を ( ) で囲う。
  • 全体も ( ) で囲う。
  • 比較結果で返す値が変わるので条件演算子を使う。

演習8-2

三つの値 a, b, c の中央値(真ん中の値)を返すマクロを定義せよ

仕様:mid(a, b, c) は3つのうち“真ん中”を返す(ソートは不要、式でOK)

解答例

#define mid(a, b, c) (max(min((a),(b)), min(max((a),(b)), (c))) ) )

…は長いので、まずは補助マクロを作って安全に書くのがおすすめです。

#define max(x, y) (((x) > (y)) ? (x) : (y))
#define min(x, y) (((x) < (y)) ? (x) : (y))
#define mid(a, b, c) (max(min((a),(b)), min(max((a),(b)), (c))))

解説

  • こういう複雑な式ほど、( ) で守らないと崩れます。
  • ただし注意:引数に a++ みたいな副作用を入れると危険です。

演習8-3

二つの変数を交換するマクロを定義せよ

仕様:swap(type, a, b) で a と b を入れ替える。

解答例

#define swap(type, a, b) do { type tmp = (a); (a) = (b); (b) = tmp; } while (0)

解説

  • do { ... } while (0) の形にしておくと、if の中で使っても安全になりやすい。
  • (a) や (b) を ( ) で囲って、式の崩れを防ぐ。
  • 交換は「一時変数 tmp」を使うのが王道