C言語入門|#define とマクロ定数のしくみ

― コンパイル前に行われる「文字列の翻訳」

プリプロセッサには、インクルード処理のほかにも重要な仕事があります。
その代表例が #define によるマクロ処理 です。

#include が「ファイルをその場に展開する命令」だったのに対し、
#define は ソースコード中の文字列を、別の文字列に置き換える命令 です。

この置換はコンパイル前に行われるため、
コンパイラは「置換後のコード」だけを見て処理します。

この仕組みを理解すると、

  • なぜ昔のC言語では #define が多用されていたのか
  • なぜ今は使いどころに注意が必要なのか

が、自然に見えてきます。

#define プリプロセッサ命令の基本形

#define の書式はとてもシンプルです。

#define 置換前の文字列 置換後の文字列

プリプロセッサは、
ソースコード中に現れる「置換前の文字列」をすべて探し、
そのまま「置換後の文字列」に入れ替えます。

計算も型チェックも行いません。
ただの文字列置換です。

マクロ定数の例

まずは、最もよく使われる「マクロ定数」の例を見てみましょう。

サンプルプログラム

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

#include <stdio.h>

#define TAX_RATE 0.1

int main(void)
{
    int price = 1200;
    int total = price + price * TAX_RATE;

    printf("total = %d\n", total);
    return 0;
}

このコードでは、TAX_RATE という名前を使っていますが、
コンパイル前にプリプロセッサが次のように置き換えます。

int total = price + price * 0.1;

TAX_RATE という名前の変数が存在しているわけではありません。
単に文字列が置き換えられているだけです。

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

見た目は定数のようですが、
マクロ定数は C言語の型システムとは一切関係ありません。

たとえば、次のような記述も可能です。

#define TAX_RATE "0.1"

この行自体ではエラーになりません。
エラーが出るとしたら、展開された先のコードです。

つまり、

  • 定義した場所では問題なし
  • 使った場所で突然エラー

という状況が起こり得ます。

これが、マクロ定数が扱いにくい理由の1つです。

副作用① 型や構文のチェックが効かない

#define は、コンパイラより前に処理されます。
そのため、

  • 型チェック
  • 構文チェック

は一切行われません。

マクロ定数は、
安全性よりも手軽さを優先した仕組み だと考えるとよいでしょう。

大規模なプログラムでは、
思わぬ場所でエラーが発生し、原因が追いにくくなることもあります。

副作用② 危険な置換ができてしまう

マクロは「単なる文字列置換」なので、
極端な話、予約語や関数名すら置き換えられます。

#define main game_start

と書けば、ソースコード中の main はすべて game_start に置き換えられます。

これは意図的に使えば強力ですが、
意図せず行うと非常に危険です。

そのため、

  • 定数として使いたいだけ
  • 型安全に扱いたい

という場合は、通常の定数宣言を使うほうが安全です。

const double TAX_RATE = 0.1;

NULL も実はマクロだった

これまで「ポインタが何も指していない状態」として使ってきた NULL も、
歴史的には #define で定義されたマクロ定数です。

処理系によって異なりますが、
次のような定義を見つけられることがあります。

#define NULL ((void*)0)

つまり、NULL も
「特別な値」ではなく
プリプロセッサによる置換結果にすぎません。

定義済みマクロ定数

C言語には、開発者が定義しなくても
最初から用意されているマクロ定数があります。

これらは主にデバッグやログ出力で使われます。

マクロ名意味
FILEソースファイル名
LINE行番号
DATEプリプロセッサ実行日
TIMEプリプロセッサ実行時刻
func関数名(厳密にはマクロではない)

これらもすべて、
コンパイル前に文字列として展開されるものです。

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

マクロ定数は、

  • 古くから使われてきた。
  • 処理が軽く。
  • コンパイル後のコードに影響を残さない。

という利点があります。

一方で、

  • 型安全ではない。
  • デバッグが難しい。

という欠点もあります。

そのため現在では、

  • 定数 → const や enum
  • 処理の抽象化 → 関数

を使い、
#define は「本当に必要な場面」に絞って使うのが一般的です。