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

定数はプログラム中の「変わらない値」を名前で表す仕組みです。

C言語では型のあるconstと、前処理段階で置換される#defineという2つの主役があります。

本稿では両者の基本、落とし穴、使い分けの指針を入門者にもわかりやすく、サンプルコードとともに段階的に解説します。

C言語の定数とは

定数の目的とメリット

定数を使う最大の目的は、意味の明確化と変更容易性の確保です。

生の数値や文字列(リテラル)をコードに散りばめると意図が不明瞭になり、仕様変更時に漏れやミスの原因になります。

意味のある名前を与えた定数にまとめておけば、読みやすく保守もしやすい設計になります。

また、型のある定数は誤用をコンパイラが検出しやすく、バグの早期発見にもつながります。

リテラルと定義済み定数の違い

リテラルは42"Hello"のようにコード中へ直接書く値です。

定義済み定数はそれに名前を与えたものです。

次の短い例は、同じ計算でも名前があると意図が読み取りやすいことを示します。

C言語
#include <stdio.h>

#define PI_MACRO (3.141592653589793)      // マクロ定数(型はコンテキストで決まる)
const double PI_CONST = 3.141592653589793; // 型付き定数オブジェクト

int main(void) {
    double r = 2.0;
    // 同じ計算でも、名前があると意図が明確になります
    printf("周長(マクロ) = %.6f\n", 2 * PI_MACRO * r);
    printf("周長(const) = %.6f\n", 2 * PI_CONST * r);
    // リテラル直接は「なぜこの数値か」が不明瞭になりがちです
    printf("周長(リテラル) = %.6f\n", 2 * 3.141592653589793 * r);
    return 0;
}
実行結果
周長(マクロ) = 12.566371
周長(const) = 12.566371
周長(リテラル) = 12.566371

constと#defineの位置づけ

C言語のconstは「型に対する修飾子」で、値を保持するオブジェクトを定義します。

アドレスを取れ、デバッガで中身を確認でき、型チェックの恩恵があります。

一方#defineは「前処理器の置換規則」であり、コンパイル前に生テキストがそのまま差し替わります。

型がなく、スコープもCの意味解析とは独立です。

さらに整数の列挙定数を作るenumも「コンパイル時に確定した整数値」を作る選択肢として重要です。

constの書き方と基本

constの宣言と初期化

constは「その名前を通じて値を変更しない」ことを表します。

オブジェクト自体を読み取り専用にしたいときに使います。

C言語
#include <stdio.h>

int main(void) {
    const int max_connections = 10;      // 自動記憶域期間。必要なら初期化するのが安全です
    // max_connections = 20;             // コンパイルエラー: constは再代入できません

    static const char banner[] = "MyApp"; // 静的記憶域期間。初期化子が必要
    printf("%s (%d)\n", banner, max_connections);
    return 0;
}
実行結果
MyApp (10)

注意点として、Cではconstオブジェクトは必ずしも初期化必須ではありませんが、意図を明確にするために基本的に初期化しておくことを推奨します。

スコープとリンク(ファイル内/外部)

ファイルスコープのconstは、Cではデフォルトで外部リンケージです(同名が他翻訳単位から参照可能)。

意図に応じてstaticexternを併用します。

  • 翻訳単位内だけで使う小さな定数ならstatic constをヘッダーに置いても安全です(各翻訳単位に別々の実体が生成されます)。
  • 複数ファイルから共有したい定数は、ヘッダーにextern宣言を置き、実体定義を1つの.cにまとめます。ヘッダーに実体を置くと多重定義になります。

複数ファイル構成の良い例を示します。

C言語
/* constants.h */
#ifndef CONSTANTS_H
#define CONSTANTS_H

// 共有する実体はextern宣言だけをヘッダーに
extern const int kBufferSize;

// 小さなTU内定数ならstatic constもヘッダーに置けます(翻訳単位ごとに別実体)
static const double kPi = 3.141592653589793;

#endif
C言語
/* constants.c */
#include "constants.h"

// 実体はちょうど1カ所で定義する
const int kBufferSize = 4096;
C言語
/* main.c */
#include <stdio.h>
#include "constants.h"

int main(void) {
    printf("kBufferSize=%d, kPi=%.2f\n", kBufferSize, kPi);
    return 0;
}
実行結果
kBufferSize=4096, kPi=3.14

C++とは異なり、Cにおけるファイルスコープconstは自動的に内部リンケージにはなりません。

内部専用にしたいときはstatic constを明示します。

ポインタとconstの位置

constは「どちらが不変か」を位置で表します。

右から読むと理解しやすいです。

  • const int *pは「constなintへのポインタ」(指す先の値を変えられない、ポインタ自体は差し替え可能)。
  • int * const pは「intへのconstなポインタ」(ポインタは固定、指す先の値は変えられる)。
  • const int * const pは「constなintへのconstなポインタ」(どちらも変えられない)。

実際の振る舞いは次の通りです。

C言語
#include <stdio.h>

int main(void) {
    int v = 10;

    const int *p = &v;    // 読み取り専用ビュー
    // *p = 20;           // コンパイルエラー: 指す先の変更は禁止
    v = 20;                // 変数v自体はconstではないので変更可能
    printf("*p=%d\n", *p); // 20が表示される

    int * const q = &v;    // 固定ポインタ
    *q = 30;               // 指す先の値は変更可能
    // q = &v2;            // コンパイルエラー: ポインタの再代入は禁止
    printf("v=%d\n", v);   // 30

    const int * const r = &v; // 何も変えられない
    (void)r;                   // 未使用警告抑止
    return 0;
}
実行結果
*p=20
v=30

文字列リテラルはCでは型がchar[]ですが変更は未定義動作です。

常にconst char *で扱い、書き換えない契約を表現してください。

#defineの書き方と基本

マクロ定数の宣言と置換の仕組み

マクロ定数は前処理器が「そのトークン列をそのまま置換」します。

型はなく、Cの意味解析より前に処理されます。

C言語
#include <stdio.h>

#define APP_NAME    "MyApp"     // 文字列マクロ
#define VERSION_MAJOR 1         // 整数マクロ
#define VERSION_MINOR 2

int main(void) {
    printf("%s v%d.%d\n", APP_NAME, VERSION_MAJOR, VERSION_MINOR);
    return 0;
}
実行結果
MyApp v1.2

関数風マクロでは引数もテキスト置換されます。

複雑な式では特に括弧が重要になります。

括弧の徹底と演算の優先順位

マクロは優先順位の罠に陥りやすいので、定数にも引数にも徹底して括弧を付けます。

C言語
#include <stdio.h>

#define SQUARE_BAD(x) x * x              // 悪い例: 括弧がない
#define SQUARE_OK(x)  ((x) * (x))        // 良い例: すべて括弧で保護
#define PI (3.141592653589793)           // 定数マクロも括弧で包む

int main(void) {
    printf("SQUARE_BAD(1+2) = %d\n", SQUARE_BAD(1+2)); // 1+2*1+2 → 5
    printf("SQUARE_OK(1+2)  = %d\n", SQUARE_OK(1+2));  // ((1+2)*(1+2)) → 9
    printf("2*pi = %.3f\n", 2 * PI);
    return 0;
}
実行結果
SQUARE_BAD(1+2) = 5
SQUARE_OK(1+2)  = 9
2*pi = 6.283

型がないことの注意点

マクロは型を持たず、型チェックもスコープ制御もできません。

文脈に応じて型が決まるため、意図しない型変換やオーバーフロー、符号の違いが生じやすい点に注意します。

数値リテラルには必要に応じて接尾辞を付け、期待する型を明示しましょう。

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

#define MASK_8   (0xFFu)         // 明示的にunsigned
#define U32_MAX  (4294967295u)   // 32bit想定の最大値
#define PI_F     (3.1415927f)    // float
#define PI_D     (3.141592653589793) // double

int main(void) {
    printf("sizeof(PI_F)=%zu, sizeof(PI_D)=%zu\n", sizeof(PI_F), sizeof(PI_D));
    uint32_t x = 0x1234u;
    printf("x & MASK_8 = 0x%02" PRIX32 "\n", x & MASK_8);
    return 0;
}
実行結果
sizeof(PI_F)=4, sizeof(PI_D)=8
x & MASK_8 = 0x34

マクロはデバッガで値を直接見ることが難しく、展開ミスも追跡しづらいため、可能な場面ではconstを検討すると安全です。

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

使い分けの指針(型安全性と可読性)

基本方針は「型があるべきものはconst、コンパイル時の条件分岐や列挙値など“真の定数式”が必要な場面は#defineまたはenum」です。

可読性と型安全性を重視して選びます。

次の表は特徴の要点をまとめたものです。

観点const#defineenum (列挙定数)
型チェックあるない整数(型はint系)
実体(メモリ)あるないない
スコープ/リンクCの規則に従う(static/extern可)翻訳単位全体で有効(無制限なテキスト置換)宣言スコープ
コンパイル時定数(ICE)として使用原則不可(CではICEにならない)
デバッガで参照できるできない値として見える場合あり
推奨用途型を伴う設定値、配列長の保持、引数の不変性表明条件コンパイル、ビットマスク、数式マクロswitchのcase、配列サイズ、定数群

注) Cではconst intは「整数定数式」とはみなされません。

C++とは挙動が異なる点に注意してください。

コンパイル時定数が必要な場面(#ifやswitchのcase)

前処理の#ifswitchcaseラベルには「整数定数式(ICE)」が必要です。

const変数は使えません。

正しいパターンを示します。

C言語
#include <stdio.h>

// #ifの例: マクロが必要
#define FEATURE_X 1

// switchのcaseの例: enumやマクロが有効
enum {
    STATUS_OK  = 0,
    STATUS_ERR = 1
};

int main(void) {
#if FEATURE_X
    printf("FEATURE_X 有効\n");
#else
    printf("FEATURE_X 無効\n");
#endif

    int status = STATUS_OK;
    switch (status) {
        case STATUS_OK:
            printf("OKです\n");
            break;
        case STATUS_ERR:
            printf("エラーです\n");
            break;
        // const int NotAllowed = 2;           // 参考: これはcaseに使えません
        // case NotAllowed:                     // Cでは整数定数式でないため不可
        //     break;
    }
    return 0;
}
実行結果
FEATURE_X 有効
OKです

配列サイズ(可変長配列を使わない場合)やビットフィールド幅など、整数定数式を要求する箇所でもenum#defineを使います。

ヘッダー配置と多重定義の回避

ヘッダーファイルへの配置はリンカエラーや予期しない重複を避けるために重要です。

マクロ定数はヘッダーに置いて構いません。インクルードガード(#ifndef ... #define ... #endif、または#pragma once)を忘れないでください。

constオブジェクトは以下のどちらかのパターンにします。

  1. 共有したい定数: ヘッダーにextern宣言、1つの.cで実体定義。
C言語
/* config.h */
#ifndef CONFIG_H
#define CONFIG_H
extern const int kPort;      // 宣言のみ
#endif
C言語
/* config.c */
#include "config.h"
const int kPort = 8080;      // 実体はここだけ
C言語
/* main.c */
#include <stdio.h>
#include "config.h"
int main(void) {
    printf("port=%d\n", kPort);
    return 0;
}
実行結果
port=8080
  1. 翻訳単位ごとに複製してよい小さな定数: ヘッダーにstatic constで定義。
C言語
/* constants.h */
#ifndef CONSTANTS_H
#define CONSTANTS_H
static const char COMPANY_NAME[] = "Acme"; // TUごとに別実体。多重定義を避ける
#endif
  • ヘッダーにconst int X = 1;のように実体を直接書くと、インクルード先ごとに定義が重複してリンクエラーになります。必ずexternstaticを付ける、またはenum/#defineで表現します。
  • 文字列定数を共有したい場合、#define COMPANY "Acme"か、型安全性を重視するならextern const char COMPANY[];宣言+1カ所定義のパターンを選びます。

まとめ

C言語における定数は、型付きのconstとテキスト置換の#defineの二本柱で使い分けます。

型安全性やデバッガの見やすさを重視するならconst、コンパイル時定数(整数定数式)が要求される#ifswitch case、ビットマスクや式テンプレートには#defineenumが適しています。

マクロでは括弧の徹底、数値リテラルの接尾辞による型明示を心がけ、constはスコープとリンケージを意識してstatic/externを正しく使い、ヘッダー配置で多重定義を避けましょう。

これらの基礎を押さえれば、読みやすく安全で変更に強いCプログラムを組み立てられるようになります。

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

URLをコピーしました!