C言語の基本|オブジェクト形式マクロの使い方

数字に名前を付ければ、意味も変更箇所もひと目で分かる。オブジェクト形式マクロで、読みやすく直しやすいコードを作ろう。

for文の繰り返し回数や配列の上限、表示する件数などは、プログラムの仕様変更によって変わることがあります。

ところが、同じ数字を複数の場所へ直接書いていると、変更するときにすべての場所を探さなければなりません。一部を直し忘れると、処理によって上限が異なるといった不具合につながります。

そこで役立つのがオブジェクト形式マクロです。

オブジェクト形式マクロを使うと、数値や文字列などの置換内容に名前を付けられます。繰り返し回数を表す3という数字をCHECK_ROUNDSのような名前で表せば、数字の役割がコードから読み取れるようになります。

主な利点は次のとおりです。

  • 数値や文字列の意味が分かりやすくなる
  • 変更箇所をマクロ定義へ集められる
  • 複数箇所の変更漏れを防ぎやすい
  • 同じ基準値をプログラム全体で共有できる

図1:前処理でマクロ名が置換される仕組み

この図から分かること

#defineは、コンパイラがC言語の文法を処理する前に働く前処理指令です。

ソースコード内のCHECK_ROUNDSは、前処理によって3へ置き換えられます。コンパイラが受け取る段階では、マクロ名ではなく置換後の内容になっています。

#defineは前処理指令

#defineは、プリプロセッサに対してマクロを定義する前処理指令です。

基本的な書式は次のとおりです。

#define マクロ名 置換文字列

たとえば、繰り返し回数を3回とする場合は、次のように定義できます。

#define CHECK_ROUNDS 3

この定義より後ろに書かれたCHECK_ROUNDSは、前処理によって3へ置き換えられます。

for (i = 1; i <= CHECK_ROUNDS; i++)

前処理後のイメージは次のとおりです。

for (i = 1; i <= 3; i++)

#defineは通常、#includeの後、関数定義の前に記述します。

#include <stdio.h>

#define CHECK_ROUNDS 3

int main(void)
{
    /* 処理 */
}

オブジェクト形式マクロと関数形式マクロ

C言語のマクロには、代表的にオブジェクト形式と関数形式があります。

種類主な用途定義例
オブジェクト形式マクロ数値や文字列などに名前を付ける#define LIMIT 20
関数形式マクロ引数を受け取る形で置換する#define MAX(a, b) ...

オブジェクト形式マクロには、マクロ名の直後に引数の丸括弧がありません。

#define LIMIT 20

この記事では、回数や上限などへ名前を付けるオブジェクト形式マクロを扱います。

オブジェクト形式マクロの構成

要素意味書き方のポイント
#defineマクロを定義する前処理指令#から行を始める
マクロ名置換対象となる名前大文字とアンダースコアがよく使われる
置換文字列実際に置き換わる内容数値、文字列、式などを指定できる
空白マクロ名と置換文字列を区切る1つ以上の空白を入れる

マクロ名には、次のような名前が使われます。

  • MAX_USERS
  • LOOP_COUNT
  • BUFFER_SIZE
  • LINE_WIDTH
  • RETRY_LIMIT

大文字で書くことはC言語の文法上の必須ルールではありません。ただし、通常の変数と区別しやすくするため、オブジェクト形式マクロは大文字で書くのが一般的です。

#defineの行にセミコロンを付けない

#defineによるマクロ定義の末尾には、セミコロンを付けません。

正しい書き方は次のとおりです。

#define LIMIT 20

次のようにセミコロンを付けると、そのセミコロンも置換文字列に含まれます。

#define LIMIT 20;

たとえば、次の条件式でLIMITを使用するとします。

i <= LIMIT

LIMITが20;として定義されていると、前処理後は次のような形になります。

i <= 20;

式の途中へ意図しないセミコロンが入り、コンパイルエラーや予想外の構文になる原因になります。

#defineはC言語の変数宣言ではなく、前処理指令です。通常の文のように末尾へセミコロンを付けないようにしましょう。

数字の直書きとマクロを比較する

次のループは、処理を6回繰り返します。

for (i = 1; i <= 6; i++) {
    /* 処理 */
}

このコードだけを見ると、6が何を意味しているのか分かりにくい場合があります。

マクロを使うと、数字の役割を名前で表せます。

#define CHECK_ROUNDS 6
for (i = 1; i <= CHECK_ROUNDS; i++) {
    /* 処理 */
}
書き方読み取れること
i <= 66まで繰り返すことは分かる
i <= CHECK_ROUNDS点検回数の上限であることが分かる
i <= MAX_USERS利用者数の上限であることが分かる
i <= RETRY_LIMIT再試行回数の上限であることが分かる

同じ6という数字でも、点検回数、利用者数、再試行回数では意味が異なります。数字に役割に合った名前を付けると、別の目的で使われている同じ数値と区別できます。

マクロで点検回数をまとめて管理する

次のプログラムでは、SYSTEM_CHECKSを基準となる点検回数として定義します。

最初のfor文ではSYSTEM_CHECKS回、2番目のfor文ではその2倍の回数だけログを確認します。

基準回数と2倍の回数をマクロで管理するプログラム

ファイル名:9_6_1.c

#include <stdio.h>

#define SYSTEM_CHECKS 3

int main(void)
{
    int i;

    /* 基準回数だけシステム点検を行う */
    for (i = 1; i <= SYSTEM_CHECKS; i++) {
        printf("システム点検 %d回目\n", i);
    }

    printf("-----\n");

    /* 基準回数の2倍だけログを確認する */
    for (i = 1; i <= (SYSTEM_CHECKS * 2); i++) {
        printf("ログ確認 %d回目\n", i);
    }

    return 0;
}
実行結果の例
システム点検 1回目
システム点検 2回目
システム点検 3回目
-----
ログ確認 1回目
ログ確認 2回目
ログ確認 3回目
ログ確認 4回目
ログ確認 5回目
ログ確認 6回目
プログラムの詳しい解説

SYSTEM_CHECKSは、基準となる点検回数です。

#define SYSTEM_CHECKS 3

最初のfor文では、SYSTEM_CHECKSをそのまま上限として使用しています。

for (i = 1; i <= SYSTEM_CHECKS; i++)

前処理後は、次のような条件になります。

for (i = 1; i <= 3; i++)

2番目のfor文では、SYSTEM_CHECKSを使った式を上限にしています。

for (i = 1; i <= (SYSTEM_CHECKS * 2); i++)

SYSTEM_CHECKSが3なので、ログ確認は6回行われます。

マクロ定義を次のように変更したとします。

#define SYSTEM_CHECKS 5

変更後は、最初のループが5回、2番目のループが10回になります。2つのfor文を個別に修正する必要はありません。

マクロ化によって変更箇所を集められる

同じ基準値を複数の場所で使用するとき、マクロにまとめておくと変更箇所を1か所にできます。

変更方法必要な作業起こりやすい問題
数字を直接記述すべての使用箇所を探して修正する変更漏れが起こりやすい
マクロで管理#defineの定義を修正する関連箇所をまとめて変更できる

マクロ名を使えば、数字の意味を保ったまま複数の式で利用できます。

SYSTEM_CHECKS
SYSTEM_CHECKS * 2
SYSTEM_CHECKS + 1

ただし、式として使用する場合は、計算のまとまりが分かるように丸括弧を付けると読みやすくなります。

(SYSTEM_CHECKS * 2)

図2:マクロ定義1か所の変更が複数のループへ反映される

この図から分かること

SYSTEM_CHECKSを複数のループで利用していても、変更するのは#defineの定義部分だけです。

基準値を3から5へ変更すると、SYSTEM_CHECKSをそのまま使うループだけでなく、SYSTEM_CHECKS * 2を使うループにも変更が反映されます。

オブジェクト形式マクロを使う利点

変更を1か所にまとめられる

上限や回数を変更するとき、マクロ定義だけを修正できます。

同じ値を使用する場所が増えても、それぞれの数字を探して書き換える必要がありません。

数字の意味がコードに残る

次の2つの条件は、どちらも30まで繰り返す処理です。

i <= 30
i <= MAX_RECORDS

i <= 30では、30が何を表しているのか周囲の処理から判断する必要があります。

i <= MAX_RECORDSなら、記録件数の上限であることが変数名から読み取れます。

同じ数字を別の目的と区別できる

プログラム内に10という数字が複数回登場しても、すべてが同じ意味とは限りません。

#define RETRY_LIMIT 10
#define DISPLAY_ROWS 10

現在は同じ10でも、再試行回数と表示行数は別の仕様です。意味ごとに異なるマクロを定義しておけば、片方だけを変更できます。

保守しやすくなる

マクロ名から値の目的が分かると、後からコードを修正するときに影響範囲を判断しやすくなります。

ただし、何でもマクロにすればよいわけではありません。一度しか使わず、意味も明確な数値まで大量にマクロ化すると、定義を探す手間が増えることがあります。

仕様として名前を付ける価値がある数値や、複数箇所で共有する値を中心にマクロ化すると効果的です。

constとの違い

オブジェクト形式マクロとconstは、どちらも変更させたくない値へ名前を付ける用途で使えます。ただし、仕組みは異なります。

比較項目オブジェクト形式マクロconst
#define LIMIT 24const int limit = 24;
処理される時期コンパイル前の前処理C言語のコンパイル時
型を持たない置換intなどの型を持つ
仕組みマクロ名を置換文字列へ置き換える変更できない変数として扱う
セミコロン定義の末尾に付けない宣言の末尾に付ける

マクロは、前処理によって置換されます。

#define LIMIT 24

constは、型を持った変更できない変数として扱われます。

const int limit = 24;

最初は、マクロが単純な文字列置換であり、constはC言語の型を持つという違いを押さえておくとよいでしょう。

上限と改行間隔をマクロで指定する

次のプログラムでは、1から24までの整数を表示し、6個表示するごとに改行します。

表示する上限をNUMBER_LIMIT、1行に表示する個数をLINE_WIDTHとして定義します。

数値の上限と1行の表示数をマクロで管理するプログラム

ファイル名:9_6_2.c

#include <stdio.h>

#define NUMBER_LIMIT 24
#define LINE_WIDTH 6

int main(void)
{
    int i;

    for (i = 1; i <= NUMBER_LIMIT; i++) {
        printf("%d ", i);

        /* LINE_WIDTH個表示するごとに改行する */
        if (i % LINE_WIDTH == 0) {
            printf("\n");
        }
    }

    /* 最後の行が途中で終わった場合に改行する */
    if (NUMBER_LIMIT % LINE_WIDTH != 0) {
        printf("\n");
    }

    return 0;
}
実行結果の例
1 2 3 4 5 6
7 8 9 10 11 12
13 14 15 16 17 18
19 20 21 22 23 24
プログラムの詳しい解説

NUMBER_LIMITは、どこまで数値を表示するかを表します。

#define NUMBER_LIMIT 24

LINE_WIDTHは、1行に何個の数値を表示するかを表します。

#define LINE_WIDTH 6

for文ではNUMBER_LIMITを上限として使用します。

for (i = 1; i <= NUMBER_LIMIT; i++)

数値を1つ表示するたびに、iをLINE_WIDTHで割った余りを調べます。

if (i % LINE_WIDTH == 0)

iが6、12、18、24になると余りが0になるため、改行が出力されます。

NUMBER_LIMITがLINE_WIDTHの倍数ではない場合、最後の行ではループ内の改行が実行されません。そこで、ループ終了後に次の条件を確認しています。

if (NUMBER_LIMIT % LINE_WIDTH != 0)

たとえばNUMBER_LIMITを22へ変更した場合、最後の行は19 20 21 22で終わります。この条件によって、最後に改行を追加できます。

定義名には役割が分かる名前を付ける

マクロ名は短ければよいというものではありません。何を表している値なのか分かる名前を付けることが大切です。

分かりにくい名前分かりやすい名前
NNUMBER_LIMIT
WLINE_WIDTH
CCHECK_COUNT
MMAX_USERS
RRETRY_LIMIT

小さなプログラムではNでも意味を推測できますが、処理が増えると役割を見失いやすくなります。

名前が少し長くなっても、上限、回数、間隔などの意味が伝わる名前を選ぶと保守しやすくなります。

図3:マクロ・const・変数の役割の違い

この図から分かること

マクロは前処理で置き換えられる名前であり、C言語の型を持ちません。constはintなどの型を持ち、プログラム中で変更しない値として扱います。

利用者から入力される値や、計算によって実行中に決まる値は、マクロではなく変数で管理します。固定値と実行時の値を区別することが大切です。

入力値から谷型の数列を表示する

次のプログラムでは、入力された正の整数を数列の長さとして使います。

表示例は次のとおりです。

入力値表示する数列
95 4 3 2 1 2 3 4 5
105 4 3 2 1 1 2 3 4 5
11

このプログラムのnやedgeは、実行時の入力や計算によって値が決まります。そのため、オブジェクト形式マクロではなく変数を使用します。

入力された長さに合わせて谷型の数列を作るプログラム

ファイル名:9_6_3.c

#include <stdio.h>

int main(void)
{
    int n;
    int edge;
    int i;

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

    if (n <= 0) {
        printf("エラー:1以上の整数を入力してください。\n");
        return 0;
    }

    /* 数列の端に表示する最大値を求める */
    edge = (n + 1) / 2;

    /* edgeから1まで減らしながら表示する */
    for (i = edge; i >= 1; i--) {
        printf("%d ", i);
    }

    /* 偶数個の場合は中央の1をもう一度表示する */
    if (n % 2 == 0) {
        printf("1 ");
    }

    /* 2からedgeまで増やしながら表示する */
    for (i = 2; i <= edge; i++) {
        printf("%d ", i);
    }

    printf("\n");

    return 0;
}
実行結果の例

入力値が9の場合は、中央の1を1回表示します。

正の整数値を入力してください > 9
5 4 3 2 1 2 3 4 5

入力値が10の場合は、中央の1を2回表示します。

正の整数値を入力してください > 10
5 4 3 2 1 1 2 3 4 5

入力値が0以下の場合は、エラーメッセージを表示して終了します。

正の整数値を入力してください > 0
エラー:1以上の整数を入力してください。
プログラムの詳しい解説

nには利用者が入力した数列の長さが入ります。

scanf("%d", &n);

nはプログラムを実行するたびに変わる可能性があるため、マクロには向いていません。

0以下が入力された場合は数列を作れないため、エラーメッセージを表示してmain関数を終了します。

if (n <= 0) {
    printf("エラー:1以上の整数を入力してください。\n");
    return 0;
}

edgeには、数列の両端に表示する最大値を格納します。

edge = (n + 1) / 2;

nが9の場合はedgeが5、nが10の場合もedgeが5になります。

最初のfor文では、edgeから1まで1ずつ減らします。

for (i = edge; i >= 1; i--)

nが偶数の場合は、数列の中央に同じ1を2つ並べる必要があります。

if (n % 2 == 0) {
    printf("1 ");
}

最後のfor文では、2からedgeまで1ずつ増やします。

for (i = 2; i <= edge; i++)

このプログラムでは、マクロを使わないことにも意味があります。NUMBER_LIMITのような仕様上の固定値はマクロで管理できますが、nやedgeのように入力や計算によって変化する値は変数で管理するのが自然です。