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

「見た目は同じでも、中身は別モノ。関数とマクロの“違い”がバグを減らす!」

C言語を書いていると、関数みたいに呼べるのに #define で作る“関数形式マクロ”に出会います。
ぱっと見は foo(x) で同じ形なのに、動き方はけっこう別物なんですよね。

  • 関数は「呼び出す」
  • 関数形式マクロは「展開される(貼り付けられる)」

この違いを知らないまま使うと、思わぬ副作用(意図しない増減、関数が2回呼ばれる等)が起きがちです。
ここでは、シンプルな別例を使って「違い」と「安全に使うコツ」を、表と図でしっかり整理します。

まずは題材:絶対値を求める(関数版 / マクロ版)

ここでは分かりやすい 絶対値 abs を求めるプログラムを例に解説していきます。

サンプルA:関数で絶対値を求める

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

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

// 絶対値(関数)

#include <stdio.h>

int abs_int(int x)
{
    return (x < 0) ? -x : x;
}

int main(void)
{
    int n;

    printf("数値を入力してください:");
    scanf("%d", &n);

    printf("絶対値は%dです。\n", abs_int(n));
    return 0;
}

実行例

数値を入力してください:-7
絶対値は7です。

サンプルB:関数形式マクロで絶対値を求める

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

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

// 絶対値(関数形式マクロ)

#include <stdio.h>

#define ABS(x) ((x) < 0 ? -(x) : (x))

int main(void)
{
    int n;

    printf("数値を入力してください:");
    scanf("%d", &n);

    printf("絶対値は%dです。\n", ABS(n));
    return 0;
}

実行例

数値を入力してください:-7
絶対値は7です。

見た目の結果は同じ。
でも「中で起きていること」は結構違います。

関数は呼び出し、マクロは展開

図:処理のイメージの違い

この図の説明

  • 関数は、プログラムの流れが abs_int に移動して戻ってきます(呼出しと復帰)。
  • マクロは、コンパイル前に ABS(n) が式へ展開され、main の中に貼り付けられます。

ポイント1:型の扱いが違う(関数は型を決める、マクロは式に広く適用)

型に関する違い

観点関数関数形式マクロ
引数の型定義時に決まる(例:int)基本的に型を固定しない
返却値の型定義時に決まる展開された式の型に依存
型が増えたときabs_int, abs_long…と増えがちABS(x) は比較と単項-ができれば使える

表の説明
関数は「この型で受け取る」と決めて作ります。だから型ごとに関数名が増えやすいです。
マクロは「式の形を広げる」ので、比較 < や - ができる型なら、同じ書き方で適用しやすくなります。

ポイント2:関数は“内部で作業が起きる”が、マクロは起きない

関数呼び出しでは、私たちが意識しないところで次が行われます。

関数呼び出しで起きること

起きることざっくり説明
引数の受け渡し実引数の値が仮引数へコピーされる
呼び出しと復帰main から関数へ移動し、戻ってくる
返却値の受け渡し戻り値を main 側で受け取る

一方マクロは、式として貼り付くので、これらの「呼び出し作業」はありません。

速度とサイズの傾向(ざっくり)

観点関数関数形式マクロ
実行速度呼び出し分のコストがある場合も呼び出しがない分、わずかに有利なことも
実行ファイルのサイズ関数本体は1つ展開先が多いと大きくなりやすい

表の説明
マクロは使った場所ごとに式が埋め込まれます。複雑な式をあちこちで使うと、その分だけサイズが増えやすい、というわけです。

ポイント3:副作用(side effect)が起きやすいのはマクロ

ここが超重要です。
マクロは展開されるので、引数が式の中で複数回出てくると、その式が複数回評価されることがあります。

ABS(a++) の危険な展開

呼び出し側がこう書いたら…

ABS(a++)

展開後はこうなります。

((a++) < 0 ? -(a++) : (a++))

この図の説明
a++ が条件判定でも結果でも出てきます。つまり状況によっては a++ が複数回実行され、a が想定以上に増えます。
これが「副作用が見えにくい」典型例です。

安全な引数 / 危険な引数

引数の例安全?理由
n, x, value(ただの変数)だいたい安全何回参照しても値が変わらない
10, -3.5(定数)安全何回でも同じ
a++, i += 2危険評価回数が増えると値が変わる
func()危険関数が2回呼ばれる可能性がある

関数なら副作用が起きにくい理由

関数 abs_int をこう呼んでも…

abs_int(a++)

a++ は「引数を作る時点」で一度だけ評価され、abs_int の中では仮引数 x として扱われます。
だから a++ が2回走る、みたいなことは基本起きません(関数は引数がコピーされるため)。

超重要:関数形式マクロとオブジェクト形式マクロの違い(空白の罠)

関数形式マクロは、マクロ名の直後に ( が続く形で定義します。

関数形式マクロの書式

#define 名前(引数) 展開する式

ところが、次のように 名前と(の間に空白が入ると…

#define ABS (x) ((x) < 0 ? -(x) : (x))

これは関数形式マクロではなく、オブジェクト形式マクロっぽく扱われて、
「ABS を (x) ((x) < 0 ? -(x) : (x)) に置換する」
みたいな、意図しない状態になりがちです。

空白あり/なしの意味

定義扱われ方意図
#define ABS(x) ...関数形式マクロABS(値) を式へ展開したい
#define ABS (x) ...別物になりやすいABS 自体を文字列的に置換しがち

結論:関数形式マクロは、マクロ名と(の間に空白を入れないのが鉄則です。

命令(ここで使ったもの)の書式と役割まとめ

#include の書式

#include <stdio.h>

何をする?
printf と scanf を使うための宣言を読み込みます。

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

#define ABS(x) ((x) < 0 ? -(x) : (x))

何をする?
ABS(何か) を見つけたら、後ろの式へ展開します(コンパイル前に展開)。

printf の書式

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

何をする?
画面にメッセージを表示します。

scanf の書式

scanf("書式文字列", 変数のアドレス, ...);

何をする?
入力を読み取り、変数に格納します。
%d は int 用、%lf は double 用です。

まとめ:使い分けのコツ

こうしたい向いているのは
副作用の危険を減らしたい関数
型ごとに同じ処理を手軽に書きたい関数形式マクロ(ただし注意して使う)
大きい処理を何度も使う関数(サイズ増加を避けやすい)
小さく単純な式を高速に展開したい関数形式マクロ