C言語基礎|マクロで広がるC言語プログラミング

「同じ処理、型ごとに何回書く?─マクロで“1回”にまとめよう。」

C言語って、シンプルで速くて、書いたとおりに動いてくれるのが最高なんですが……その反面、型が違うだけで同じような関数を何個も作りたくなること、ありますよね。

たとえば「小さいほうを返す」「上限・下限に収める」「2つを比較する」みたいな処理は、intでもdoubleでもやりたいのに、関数として書くと min_int、min_double…と増えていきがちです。

そこで活躍するのが 関数形式マクロ(function-like macro)
これは「関数っぽい形で呼び出せるのに、実体は置換・展開でできる」仕組みです。うまく使うと、同じ書き方で複数の型に対応できて、プログラムの見通しがグッと良くなります。

この記事では、関数形式マクロの基本と注意点を、表と図でしっかり解説します。

まずは「型ごとに関数を作り分ける」例

ここでは「2つの値のうち小さいほうを返す」処理を題材にします。

サンプル1:int用とdouble用で関数を分ける

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

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

// 整数と実数の小さいほうを返す(関数で作り分け)

#include <stdio.h>

//--- int型:小さいほうを返す ---//
int min_int(int a, int b)
{
    return (a < b) ? a : b;
}

//--- double型:小さいほうを返す ---//
double min_double(double a, double b)
{
    return (a < b) ? a : b;
}

int main(void)
{
    int ai, bi;
    double ad, bd;

    printf("整数を2つ入力してください:");
    scanf("%d%d", &ai, &bi);
    printf("小さいほうは%dです。\n", min_int(ai, bi));

    printf("実数を2つ入力してください:");
    scanf("%lf%lf", &ad, &bd);
    printf("小さいほうは%fです。\n", min_double(ad, bd));

    return 0;
}

実行例

整数を2つ入力してください:8 3
小さいほうは3です。
実数を2つ入力してください:2.5 9.1
小さいほうは2.500000です。

何が困りやすい?

同じロジックなのに、型が違うだけで関数名が増えます。

やりたいことintdouble型が増えると…
小さいほうを返すmin_intmin_doublelong, float, unsigned…でさらに増える

この「型の数だけ似た関数が増える問題」を、関数形式マクロでスッキリさせます。

関数形式マクロで「同じ書き方」にまとめる

サンプル2:関数形式マクロ min(a, b)

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

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

// 整数と実数の小さいほうを返す(関数形式マクロ)

#include <stdio.h>

#define min(a, b) ((a) < (b) ? (a) : (b))
// マクロ名と(の間に空白を入れない(min (a,b) は別扱いになりやすい)

int main(void)
{
    int ai, bi;
    double ad, bd;

    printf("整数を2つ入力してください:");
    scanf("%d%d", &ai, &bi);
    printf("小さいほうは%dです。\n", min(ai, bi));

    printf("実数を2つ入力してください:");
    scanf("%lf%lf", &ad, &bd);
    printf("小さいほうは%fです。\n", min(ad, bd));

    return 0;
}

実行例

整数を2つ入力してください:8 3
小さいほうは3です。
実数を2つ入力してください:2.5 9.1
小さいほうは2.500000です。

同じ min(ai, bi) / min(ad, bd) という書き方で、intでもdoubleでも動きます。ここが気持ちいいポイントです。

図でつかむ:マクロは「置換」ではなく「展開」

第5章のオブジェクト形式マクロが「文字列の置換」っぽかったのに対し、関数形式マクロは「形を保ったまま展開」されます。

図:翻訳の流れ(マクロが効く場所)

この図の意味
min(a, b) はコンパイル前に中身へ展開されるので、コンパイラは「すでに展開された式」として扱います。

展開例:実際にどう広がる?

次の1行があったとします。

printf("小さいほうは%dです。\n", min(ai, bi));

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

printf("小さいほうは%dです。\n", ((ai) < (bi) ? (ai) : (bi)));

関数とマクロの見え方の違い

項目関数関数形式マクロ
実体実行時に呼び出すコンパイル前に展開される
型への対応型ごとに関数名が増えやすい同じ書き方で通りやすい
デバッグ呼び出しとして追いやすい展開後の式を意識する必要あり
注意点引数は1回評価引数が複数回評価されることがある

ここで登場する「命令」「書式」をやさしく整理

この章のプログラムには、Cの基本部品がいくつか出てきます。ひとつずつ役割を確認します。

#include の書式と役割

書式

#include <ヘッダファイル名>

何をする?
標準ライブラリなどの宣言を読み込みます。今回の <stdio.h> は、printf や scanf を使うために必要です。

意味
#include <stdio.h>入出力(printf/scanf)の宣言を使えるようにする

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

書式(関数形式マクロ)

#define マクロ名(仮引数, ...) 展開後の式

何をする?
マクロ名(…) の形を見つけたら、展開後の式に広げるという指示です。

意味
#define min(a, b) ((a) < (b) ? (a) : (b))min(x,y) を条件演算子の式に展開する

大事ポイント

  • マクロ名と(の間に空白を入れない(見た目は似てても扱いが変わることがあります)
  • 引数も全体も ( ) でしっかり囲う(後で説明します)

printf / scanf の書式と役割

printf の書式

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

画面に出力します。

scanf の書式

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

キーボード入力を読み取り、変数に入れます。scanf は「入れ物の場所」が必要なので、&ai のようにアドレスを渡します。

書式指定意味対象例
%dint の入力/出力int ai
%lfdouble の入力double ad
%fdouble の出力double ad

なぜマクロ定義は ( ) だらけなの?

マクロは展開されるので、展開後の式が他の演算子と混ざっても安全にする必要があります。

よくある事故1:全体を囲わない

もし、こう書いてしまうと…

#define min(a, b) (a < b ? a : b)

一見動きそうですが、別の式に混ざると読みにくく、優先順位が絡む場面で危険になります。
なので 全体も ( ) で包むクセをつけるのが安心です(今回の例は条件演算子なので特に丁寧に包みます)。

よくある事故2:引数を囲わない

もし、こう書いてしまうと…

#define min(a, b) (a < b ? a : b)

min(x + 1, y) のような呼び出しで、展開後が読みにくくなり、意図せず優先順位の影響を受けることがあります。
だから (a) や (b) のように引数を必ず囲うのが定石です。

超重要:マクロは「引数が複数回評価される」ことがある

ここが関数と一番違うところです。

たとえば、次の呼び出しは危険です。

min(i++, j++)

min(a, b) は a と b を条件判定と結果でそれぞれ使うので、展開すると a や b が 複数回登場しやすいです。
結果として i++ が2回実行されてしまう、みたいな事故が起こりえます。

安全な渡し方・危険な渡し方

渡すもの安全?理由
変数(ai, bi)安全何回使われても値が変わらない
定数(10, 3.14)安全何回使われても同じ
i++ や 関数呼び出し危険展開で複数回実行される可能性

コツ:マクロには「副作用のない式」を渡す、が鉄則です。

まとめ:関数形式マクロは“便利さ”と“注意点”がセット

うれしいこと気をつけること
同じ書き方で複数の型に対応しやすい引数が複数回評価される可能性
小さな処理をスッキリ書ける( ) を丁寧に付けて事故防止
コードの重複を減らせるデバッグ時は展開後の形を意識