閉じる

【C言語】定数の書き方:constと#defineの基本と注意点

プログラム中の数値や文字列を直接書くと読みづらく、変更にも弱いです。

そこで定数を名前で表すと、意味が伝わりやすくバグも減ります。

本記事ではC言語における定数の定義方法であるconstと#defineの基本と注意点を、初心者にもわかりやすく、順を追って詳しく解説します。

C言語の定数とは?constと#defineの基礎

定数の意味と変数との違い

定数とは、実行時に値が変化しないデータのことです。

通常の変数は代入により値が変わりますが、定数は一度決めた値を守ることで、プログラムの意図を明確にし、誤変更を防ぎます。

C言語では主に2つの手段で定数を表します。

  • const修飾子を使った型付きの定数(読み取り専用の変数)
  • #defineマクロを使ったテキスト置換の定数(プリプロセッサ)

リテラルとシンボリック定数の違い

その場で直接値を書くリテラル(例: 10, 'A', 3.14)は簡単ですが、意味が読み取りにくく、同じ値を複数箇所で変えるのが面倒です。

これに対し、シンボリック定数(例: const int Max = 10;, #define MAX 10)は名前に意味を込められ、変更が一箇所で済みます。

初心者がまず覚えるポイント

まずは「意味のある名前を付ける」「型が必要ならconst、単純な数式や条件コンパイルには#define」という方針を覚えましょう。

特にCでは配列サイズやswitchのcaseラベルには#defineやenumが安全で、constは使えない場面がある点に注意します。

constの書き方と使い方

constの基本構文と初期化

constは型修飾子で、対象のオブジェクトを読み取り専用にします。

宣言と同時に初期化するのが基本です。

C言語
// constの基本例
#include <stdio.h>

int main(void) {
    const int MaxUsers = 100;      // 型付きの定数(読み取り専用)
    const double Pi = 3.1415926535;

    printf("MaxUsers = %d\n", MaxUsers);
    printf("Pi = %.10f\n", Pi);

    // MaxUsers = 200; // ← コンパイルエラー(読み取り専用なので代入不可)
    return 0;
}
実行結果
MaxUsers = 100
Pi = 3.1415926535

再代入不可と読み取り専用の意味

constは「再代入できない」という約束をコンパイラに伝える修飾子です。

ポイントは「その名前(左辺値)からは書き換えられない」という性質であり、オブジェクト自体が絶対不変という意味ではありません。

たとえばポインタ経由で別名が書き換え可能なら、constの参照側からは書き換えられないだけで、実体の値が変わってしまうことはあります。

これを避けるには、constを適切に伝播させる「const正当性(const-correctness)」を意識します。

スコープとリンク(ブロック/ファイル)

  • ブロックスコープ(関数内)のconstは、そのブロック内でのみ有効です。
  • ファイルスコープ(関数外)でconstを付けた場合、C言語では外部リンケージ(他の翻訳単位から参照可)がデフォルトです。複数ファイルで同名定数を重複定義するとリンカエラーになります。
  • 外部公開したくないファイル内限定の定数にしたい場合はstatic constを使います。これは内部リンケージを与え、同名でも翻訳単位ごとに別物になります。

ヘッダで共有するextern const

ヘッダに直接const int X = 123;と書くと、インクルード先ごとに定義され重複定義になります。

ヘッダではextern宣言だけを書き、実体は1つの.cに置くのが原則です。

C言語
/* config.h (ヘッダ) */
#ifndef CONFIG_H
#define CONFIG_H

extern const int BUF_SIZE;   // ここでは宣言だけ
extern const double PI;

#endif
C言語
/* config.c (実体の定義は1箇所だけ) */
#include "config.h"

const int BUF_SIZE = 1024;     // 実体
const double PI    = 3.141592653589793;
C言語
/* use.c (利用側) */
#include <stdio.h>
#include "config.h"

int main(void) {
    printf("BUF_SIZE=%d, PI=%.6f\n", BUF_SIZE, PI);
    return 0;
}
実行結果
BUF_SIZE=1024, PI=3.141593

ヘッダに書く代替として、static constを使えば「各翻訳単位に同じ値の内部定数を複製」できます(外部公開の必要がない場合に便利)。

ポインタとconstの位置(概要)

constの位置で意味が変わるため、読み方を覚えましょう。

  • const int *p または int const *p: 「指す先がconst」(指し先のintを書き換え不可、p自体は他を指せる)
  • int * const p: 「ポインタ自体がconst」(pの付け替え不可、指し先のintは書き換え可)
  • const int * const p: 「指す先もポインタ自体もconst」
C言語
#include <stdio.h>

int main(void) {
    int x = 10, y = 20;

    const int *p = &x;   // 指す先が読み取り専用
    // *p = 11;          // エラー: 指す先は書き換え不可
    p = &y;              // OK: 別の場所を指せる

    int * const q = &x;  // ポインタ自体が固定
    *q = 11;             // OK: 指す先は変更可能
    // q = &y;           // エラー: 付け替え不可

    printf("x=%d, y=%d\n", x, y);
    return 0;
}
実行結果
x=11, y=20

型安全とコンパイル時チェック

const「型を持つ定数」なので、関数引数や演算で型チェックが効きます。

一方、#defineはテキスト置換で型がなく、意図せぬ型変換やオーバーフローを招くことがあります。

C言語
#include <stdio.h>
#include <stdint.h>

void takes_uint32(uint32_t v) {
    printf("v=%u\n", (unsigned)v);
}

int main(void) {
    const int CONST_NEG = -1;    // 型: int
    #define MACRO_NEG -1         // 型なし(展開されて -1)

    // takes_uint32(CONST_NEG);  // 警告が出る可能性: 符号なしに変換
    takes_uint32((uint32_t)CONST_NEG); // 明示キャストで意図を示す
    takes_uint32(MACRO_NEG);     // テキスト置換後に暗黙変換(意図不明になりやすい)

    return 0;
}
実行結果
v=4294967295
v=4294967295

配列サイズに使うときの注意(C言語)

C90では配列サイズやcaseラベルにconstは使えません。

C99以降のVLA(可変長配列)ではconst int nでも配列サイズに書ける場合がありますが、「整数定数式」ではないためswitch-caseのラベルには使えません。

確実性が必要な場面では#define か enumを使います。

C言語
#include <stdio.h>

enum { BUF_SIZE = 1024 }; // 整数定数式として安全(配列サイズ・caseラベルで使用可)

int main(void) {
    int buf[BUF_SIZE];        // OK: 常にコンパイル時に決まる
    switch (10) {
        case BUF_SIZE - 24:   // OK: 整数定数式
            printf("hit\n");
            break;
    }
    printf("buf size=%zu\n", sizeof(buf));
    return 0;
}
実行結果
hit
buf size=4096

#defineの書き方と使い方

#defineマクロ定数の基本構文

#defineはプリプロセッサによるテキスト置換です。

簡潔に書け、演算式も使えます。

C言語
#include <stdio.h>

#define APP_NAME "MyApp"
#define MAJOR    1
#define MINOR    2
#define PATCH    3
#define VERSION  (MAJOR * 10000 + MINOR * 100 + PATCH) // 括弧で保護

int main(void) {
    printf("%s version=%d\n", APP_NAME, VERSION);
    return 0;
}
実行結果
MyApp version=10203

テキスト置換とスコープ(翻訳単位)

#defineは宣言以降、その翻訳単位内で有効です。

関数内・外の別はなく、#undefで無効化できます。

他のファイルには自動では伝わらないため、共有したい場合はヘッダに置きます。

括弧の徹底と演算順序の罠

マクロはテキスト置換なので、必ず括弧で囲って演算順序の事故を防ぐのが鉄則です。

C言語
#include <stdio.h>

// 悪い例(括弧なし)
#define SQR_BAD(x) x * x
// 良い例(引数・全体を括弧で保護)
#define SQR(x)     ((x) * (x))

int main(void) {
    printf("SQR_BAD(1+2) = %d\n", SQR_BAD(1+2)); // 1+2*1+2 → 5 になってしまう
    printf("SQR(1+2)     = %d\n", SQR(1+2));     // ((1+2)*(1+2)) → 9
    return 0;
}
実行結果
SQR_BAD(1+2) = 5
SQR(1+2)     = 9

型がないことの注意点(サフィックスU/F)

マクロには型がありません。

整数や浮動小数点のリテラルのサフィックスで意図を明確にします。

  • U, UL, ULL: 符号なし、長さを指定
  • F: floatリテラル
  • ビットシフトでは符号あり1のシフトは未定義や実装依存の落とし穴1uなどを使います。
C言語
#include <stdio.h>
#include <inttypes.h>

#define ONE_S   1          // 型未指定 → int
#define ONE_U   1u         // unsigned
#define MASK31  (1u << 31) // 31ビット目を立てる(32ビット環境を想定)

int main(void) {
    printf("ONE_S type is int, value=%d\n", ONE_S);
    printf("ONE_U treated as unsigned, value=%u\n", ONE_U);
    printf("MASK31 as unsigned=%u\n", MASK31);
    return 0;
}
実行結果
ONE_S type is int, value=1
ONE_U treated as unsigned, value=1
MASK31 as unsigned=2147483648

デバッグ時の見え方とログ

#defineはコンパイル前に消えてしまうため、デバッガで値としては追えません

一方constはシンボルとして存在するため、ウォッチしやすいです。

ログ出力では、マクロを使って条件付きでログを無効化するなどの使い方が便利です。

C言語
#include <stdio.h>

// デバッグ時だけログを出す例
#define DEBUG 1

#if DEBUG
    #define LOG(fmt, ...) fprintf(stderr, "[DBG] " fmt "\n", __VA_ARGS__)
#else
    #define LOG(fmt, ...) ((void)0)
#endif

int main(void) {
    const int Threshold = 10; // デバッガで観察しやすい
    LOG("Threshold=%d", Threshold);
    return 0;
}
実行結果
[DBG] Threshold=10

ヘッダでの共有と命名規則

共有したいマクロ定数はヘッダに置きます。

衝突や可読性を避けるため、マクロは大文字スネークケース(例: MAX_BUFFER_SIZE)、const意味のある識別子(例: MaxBufferSize)がよく使われます。

プロジェクト接頭辞(例: MYAPP_MAX_BUFFER_SIZE)で重複も防げます。

constと#defineの違い・使い分け・注意点

以下に主な違いを整理します。

項目const#define
ある(型安全)ない(テキスト置換)
評価タイミングコンパイル時または実行時(最適化で埋め込み可能)プリプロセス時に置換
デバッガでの参照可能(シンボルとして見える)不可(基本的に消える)
スコープ/リンクCではファイルスコープで外部リンケージがデフォルト。staticで内部化、externで共有宣言翻訳単位内で有効、#undefまで
配列サイズ/caseに使用Cでは不可(整数定数式でない)。VLAは可だがcase不可可能(整数定数式になる)
メモリ配置必要に応じて領域を持つ可能性(最適化で消えることも)生成物としては値が埋め込まれるのみ
表現力型に基づく安全性、ポインタ修飾など単純な数式、条件コンパイル、ビット定数に便利

使い分けの基本方針

  • 型が重要ならconst。関数インターフェース付近や浮動小数の精度管理などで有利です。
  • コンパイル時に絶対に決まる整数(配列サイズ、ビットマスク、caseラベル)には#defineかenum
  • ヘッダ公開用の数値定義(ビルド条件やAPIバージョン)は#defineが無難。モジュール内部の意味付き値はconstが読みやすいです。

パフォーマンスとメモリの違い

一般に、どちらでも最適化により即値埋め込みが行われ、速度差はありません。

constに実体メモリが割り当てられるかは状況と最適化次第です。

重要なのは性能よりも「型安全性」「可読性」で選ぶことです。

配列サイズやcaseラベルに使えるか

  • 配列サイズ: C90では#defineenum。C99以降のVLAを使うならconst int nも書けますが、VLAは可搬性に課題があり、固定サイズなら#define/enum推奨です。
  • caseラベル: 整数定数式のみconstは不可、#defineenumを使います。

公開APIは#define、内部はconstが無難

  • ライブラリのバージョンやビルド切り替えフラグなどは#defineで公開すると、C/C++/他言語のビルドシステムからも扱いやすいです。
  • モジュール内部のしきい値や係数はconstにして型で意図を固定し、デバッガでも観察しやすくします。

よくあるミスとチェックリスト

  • ヘッダにconst int X = ...;と実体を書いて重複定義 → extern宣言にして実体は1ファイルに限定、またはstatic constにする。
  • マクロで括弧を省略 → すべて(引数)と(全体)に括弧
  • ビット定数を符号付きで定義 → U/ULサフィックスを付ける。
  • 配列サイズにconstを使って移植性が落ちる → #defineenumを使う。
  • ポインタのconstの位置を誤解 → 「*の左は指す先、右はポインタ自体」と覚える。

まとめ

本記事ではconstと#defineの基本・違い・使い分けを解説しました。

型安全性やデバッグ容易性が欲しい場面ではconst、配列サイズやビット定数、条件コンパイルなどコンパイル時に決め打ちしたい整数には#defineenumを選ぶのが基本方針です。

ヘッダで共有する際はexternstaticを正しく使い、マクロでは括弧の徹底サフィックスに注意しましょう。

これらを習慣化すれば、変更に強く、読みやすく、安全なCプログラムを書けるようになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!