プログラム中の数値や文字列を直接書くと読みづらく、変更にも弱いです。
そこで定数を名前で表すと、意味が伝わりやすくバグも減ります。
本記事では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
は型修飾子で、対象のオブジェクトを読み取り専用にします。
宣言と同時に初期化するのが基本です。
// 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に置くのが原則です。
/* config.h (ヘッダ) */
#ifndef CONFIG_H
#define CONFIG_H
extern const int BUF_SIZE; // ここでは宣言だけ
extern const double PI;
#endif
/* config.c (実体の定義は1箇所だけ) */
#include "config.h"
const int BUF_SIZE = 1024; // 実体
const double PI = 3.141592653589793;
/* 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」
#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
はテキスト置換で型がなく、意図せぬ型変換やオーバーフローを招くことがあります。
#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を使います。
#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
はプリプロセッサによるテキスト置換です。
簡潔に書け、演算式も使えます。
#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
で無効化できます。
他のファイルには自動では伝わらないため、共有したい場合はヘッダに置きます。
括弧の徹底と演算順序の罠
マクロはテキスト置換なので、必ず括弧で囲って演算順序の事故を防ぐのが鉄則です。
#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
などを使います。
#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
はシンボルとして存在するため、ウォッチしやすいです。
ログ出力では、マクロを使って条件付きでログを無効化するなどの使い方が便利です。
#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では
#define
かenum
。C99以降のVLAを使うならconst int n
も書けますが、VLAは可搬性に課題があり、固定サイズなら#define/enum推奨です。 - caseラベル: 整数定数式のみ。
const
は不可、#define
かenum
を使います。
公開APIは#define、内部はconstが無難
- ライブラリのバージョンやビルド切り替えフラグなどは
#define
で公開すると、C/C++/他言語のビルドシステムからも扱いやすいです。 - モジュール内部のしきい値や係数は
const
にして型で意図を固定し、デバッガでも観察しやすくします。
よくあるミスとチェックリスト
- ヘッダに
const int X = ...;
と実体を書いて重複定義 →extern
宣言にして実体は1ファイルに限定、またはstatic const
にする。 - マクロで括弧を省略 → すべて(引数)と(全体)に括弧。
- ビット定数を符号付きで定義 →
U
/UL
サフィックスを付ける。 - 配列サイズに
const
を使って移植性が落ちる →#define
かenum
を使う。 - ポインタの
const
の位置を誤解 → 「*
の左は指す先、右はポインタ自体」と覚える。
まとめ
本記事ではconstと#defineの基本・違い・使い分けを解説しました。
型安全性やデバッグ容易性が欲しい場面ではconst
、配列サイズやビット定数、条件コンパイルなどコンパイル時に決め打ちしたい整数には#define
やenum
を選ぶのが基本方針です。
ヘッダで共有する際はextern
やstatic
を正しく使い、マクロでは括弧の徹底とサフィックスに注意しましょう。
これらを習慣化すれば、変更に強く、読みやすく、安全なCプログラムを書けるようになります。