定数はプログラム中の「変わらない値」を名前で表す仕組みです。
C言語では型のあるconst
と、前処理段階で置換される#define
という2つの主役があります。
本稿では両者の基本、落とし穴、使い分けの指針を入門者にもわかりやすく、サンプルコードとともに段階的に解説します。
C言語の定数とは
定数の目的とメリット
定数を使う最大の目的は、意味の明確化と変更容易性の確保です。
生の数値や文字列(リテラル)をコードに散りばめると意図が不明瞭になり、仕様変更時に漏れやミスの原因になります。
意味のある名前を与えた定数にまとめておけば、読みやすく保守もしやすい設計になります。
また、型のある定数は誤用をコンパイラが検出しやすく、バグの早期発見にもつながります。
リテラルと定義済み定数の違い
リテラルは42
や"Hello"
のようにコード中へ直接書く値です。
定義済み定数はそれに名前を与えたものです。
次の短い例は、同じ計算でも名前があると意図が読み取りやすいことを示します。
#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
は「その名前を通じて値を変更しない」ことを表します。
オブジェクト自体を読み取り専用にしたいときに使います。
#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ではデフォルトで外部リンケージです(同名が他翻訳単位から参照可能)。
意図に応じてstatic
やextern
を併用します。
- 翻訳単位内だけで使う小さな定数なら
static const
をヘッダーに置いても安全です(各翻訳単位に別々の実体が生成されます)。 - 複数ファイルから共有したい定数は、ヘッダーに
extern
宣言を置き、実体定義を1つの.c
にまとめます。ヘッダーに実体を置くと多重定義になります。
複数ファイル構成の良い例を示します。
/* constants.h */
#ifndef CONSTANTS_H
#define CONSTANTS_H
// 共有する実体はextern宣言だけをヘッダーに
extern const int kBufferSize;
// 小さなTU内定数ならstatic constもヘッダーに置けます(翻訳単位ごとに別実体)
static const double kPi = 3.141592653589793;
#endif
/* constants.c */
#include "constants.h"
// 実体はちょうど1カ所で定義する
const int kBufferSize = 4096;
/* 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なポインタ」(どちらも変えられない)。
実際の振る舞いは次の通りです。
#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の意味解析より前に処理されます。
#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
関数風マクロでは引数もテキスト置換されます。
複雑な式では特に括弧が重要になります。
括弧の徹底と演算の優先順位
マクロは優先順位の罠に陥りやすいので、定数にも引数にも徹底して括弧を付けます。
#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
型がないことの注意点
マクロは型を持たず、型チェックもスコープ制御もできません。
文脈に応じて型が決まるため、意図しない型変換やオーバーフロー、符号の違いが生じやすい点に注意します。
数値リテラルには必要に応じて接尾辞を付け、期待する型を明示しましょう。
#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 | #define | enum (列挙定数) |
---|---|---|---|
型チェック | ある | ない | 整数(型はint系) |
実体(メモリ) | ある | ない | ない |
スコープ/リンク | Cの規則に従う(static /extern 可) | 翻訳単位全体で有効(無制限なテキスト置換) | 宣言スコープ |
コンパイル時定数(ICE)として使用 | 原則不可(CではICEにならない) | 可 | 可 |
デバッガで参照 | できる | できない | 値として見える場合あり |
推奨用途 | 型を伴う設定値、配列長の保持、引数の不変性表明 | 条件コンパイル、ビットマスク、数式マクロ | switchのcase、配列サイズ、定数群 |
注) Cではconst int
は「整数定数式」とはみなされません。
C++とは挙動が異なる点に注意してください。
コンパイル時定数が必要な場面(#ifやswitchのcase)
前処理の#if
やswitch
のcase
ラベルには「整数定数式(ICE)」が必要です。
const
変数は使えません。
正しいパターンを示します。
#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
オブジェクトは以下のどちらかのパターンにします。
- 共有したい定数: ヘッダーに
extern
宣言、1つの.c
で実体定義。
/* config.h */
#ifndef CONFIG_H
#define CONFIG_H
extern const int kPort; // 宣言のみ
#endif
/* config.c */
#include "config.h"
const int kPort = 8080; // 実体はここだけ
/* main.c */
#include <stdio.h>
#include "config.h"
int main(void) {
printf("port=%d\n", kPort);
return 0;
}
port=8080
- 翻訳単位ごとに複製してよい小さな定数: ヘッダーに
static const
で定義。
/* constants.h */
#ifndef CONSTANTS_H
#define CONSTANTS_H
static const char COMPANY_NAME[] = "Acme"; // TUごとに別実体。多重定義を避ける
#endif
- ヘッダーに
const int X = 1;
のように実体を直接書くと、インクルード先ごとに定義が重複してリンクエラーになります。必ずextern
かstatic
を付ける、またはenum
/#define
で表現します。 - 文字列定数を共有したい場合、
#define COMPANY "Acme"
か、型安全性を重視するならextern const char COMPANY[];
宣言+1カ所定義のパターンを選びます。
まとめ
C言語における定数は、型付きのconst
とテキスト置換の#define
の二本柱で使い分けます。
型安全性やデバッガの見やすさを重視するならconst
、コンパイル時定数(整数定数式)が要求される#if
やswitch case
、ビットマスクや式テンプレートには#define
やenum
が適しています。
マクロでは括弧の徹底、数値リテラルの接尾辞による型明示を心がけ、const
はスコープとリンケージを意識してstatic
/extern
を正しく使い、ヘッダー配置で多重定義を避けましょう。
これらの基礎を押さえれば、読みやすく安全で変更に強いCプログラムを組み立てられるようになります。