C言語でプログラムを書くとき、同じ値を何度も使う場面はとても多いです。
円周率や配列のサイズ、ゲームの最大HPなど、意味を持った値には名前を付けておくと読みやすくなります。
このような変化しない値を表す「定数」をどう定義するかは、C言語の基礎でありつつ、意外と奥が深いテーマです。
ここではconstと#defineの2つの方法を中心に、役割の違いとメリットを丁寧に解説します。
C言語の定数とは
定数とは何か
プログラムで扱う値には、大きく分けて「変数」と「定数」があります。
定数とは、プログラムの実行中に値が変わらないものを指します。
例えば、次のような値は定数の代表例です。
- 円周率(3.14159…)
- 1週間の日数(7)
- プログラムで許可する最大人数(100人など)
これらは、プログラムの途中で変わってしまうと意味が成立しません。
そのため、「変えない」と決めて、コード上でも変更できないようにすることが重要になります。
なぜ定数が必要なのか
C言語で定数を使う理由は、主に次のような点にあります。
1つ目は可読性の向上です。
例えば、コードに7という数字だけが書いてあると、何の7なのか分かりにくくなります。
そこをDAYS_PER_WEEKという名前の定数にしておくと、「1週間の日数だな」とすぐに理解できます。
2つ目は変更のしやすさです。
例えば、ゲームの最大HPを100と決めていたが、後から150に変えたくなったとします。
あちこちに100がベタ書きされていると、すべて探して書き換える必要がありますが、MAX_HPという定数にしておけば定義を1か所変えるだけで済みます。
3つ目は間違いを防ぐことです。
定数を変数として扱ってしまい、うっかり代入して値を変えてしまうと、バグにつながります。
定数として定義しておけば、コンパイラが「これは書き換えてはいけない」とチェックしてくれる場合があります。
このように、定数をきちんと定義することは、読みやすく安全なコードを書くために必須です。
C言語で定数を作る2つの方法
C言語では、代表的に次の2つの方法で定数を定義します。
- constキーワードを使う方法
- #defineマクロを使う方法
簡単に違いをまとめると次のようになります。
| 方法 | 役割のイメージ | 型はあるか | どのタイミングで決まるか |
|---|---|---|---|
| const | 「書き換え禁止の変数」を用意する | ある | コンパイル時(型チェックあり) |
| #define | 「文字の置き換えルール」を決める | ない | プリプロセス時(置き換えのみ) |
constは「中身を変えない変数」、#defineは「コードの一部を、別の文字列で置き換える仕組み」とイメージすると理解しやすくなります。
以降では、それぞれの特徴を初心者向けに整理しながら見ていきます。
constによる定数定義
constの基本的な書き方と意味
constは、C言語では「この変数は書き換えてはいけません」という意味を持つ修飾子です。
基本形は次のようになります。
const 型名 変数名 = 初期値;
例えば、整数型の定数を定義したいときは次のように書きます。
const int MAX_COUNT = 100;
この宣言の意味は次のとおりです。
MAX_COUNTという名前のint型の領域を確保する- 初期値として
100を代入する - その後は
MAX_COUNTに代入してはいけない(コンパイラが禁止する)
「変数としてメモリ上に存在するが、書き換え禁止」という点がポイントです。
具体例で見るconstの使い方
初心者がイメージしやすいように、簡単なサンプルプログラムで見てみます。
#include <stdio.h>
int main(void) {
// constを使って定数を定義
const int MAX_HP = 100; // 最大HPは100。途中で変わらない
int current_hp = MAX_HP; // 現在のHPを最大HPで初期化
printf("最大HP: %d\n", MAX_HP);
printf("現在のHP: %d\n", current_hp);
// ダメージを受けてHPが減る
current_hp -= 30;
printf("ダメージ後の現在のHP: %d\n", current_hp);
// constで定義したMAX_HPは変更しようとするとコンパイルエラーになります
// MAX_HP = 50; // ← これはエラーになるのでコメントアウトしています
return 0;
}
最大HP: 100
現在のHP: 100
ダメージ後の現在のHP: 70
この例ではMAX_HPをconstで定義しています。
MAX_HPが定数であることが明確になり、「ゲーム中に最大HPそのものは変わらない」という意図が読み取れます。
もしコメントアウトしているMAX_HP = 50;の行を有効にすると、多くのコンパイラでは次のようなエラーが出ます。
error: assignment of read-only variable 'MAX_HP'
このようにconstは、うっかり代入してしまうミスをコンパイル時に教えてくれる働きがあります。
const変数のスコープ(有効範囲)と寿命
constは「書き換え禁止」であることを表しますが、スコープ(どこから見えるか)や寿命(いつまで存在するか)は、通常の変数と同じルールに従います。
スコープ(有効範囲)
例えば、関数内で定義したconst変数はその関数の中だけで有効です。
#include <stdio.h>
void print_price(void) {
const int TAX_RATE = 10; // この関数の中だけで有効な定数
printf("消費税率: %d%%\n", TAX_RATE);
}
int main(void) {
print_price();
// ここでTAX_RATEを使おうとするとエラーになります
// printf("%d\n", TAX_RATE); // ← コンパイルエラー
return 0;
}
この場合、TAX_RATEはprint_price関数の中でしか使えません。
constであっても、変数のスコープのルールは変わりません。
一方、関数の外で定義した場合は、グローバルな定数として扱われます。
#include <stdio.h>
const int MAX_USER = 1000; // プログラム全体で共有される定数
void print_max_user(void) {
printf("最大ユーザー数: %d\n", MAX_USER);
}
int main(void) {
printf("mainから参照する最大ユーザー数: %d\n", MAX_USER);
print_max_user();
return 0;
}
寿命
寿命についても通常の変数と同じです。
- 関数内で定義したconst変数は、その関数が呼ばれている間だけ存在します(自動変数)。
- ファイルスコープ(関数の外)で定義したconst変数は、プログラムの開始から終了まで存在します(静的記憶域期間)。
「constだから特別な領域に置かれる」と考える必要はなく、あくまで「書き換え禁止」という性質が追加されるだけと理解しておくと混乱しにくくなります。
constポインタとポインタへのconstの違いは後回しでOKな理由
C言語を勉強していると、早い段階で次のような記法に出会うことがあります。
const int *p; // → ポインタが指す先の値がconst
int * const p2; // → ポインタ自体がconst
const int * const p3; // → どちらもconst
どれも似たような見た目ですが、意味は少しずつ異なります。
ここで一気に理解しようとすると、多くの初心者が混乱してつまずきます。
しかし、最初の段階では、ポインタとconstの組み合わせを完全に理解する必要はありません。
まずは次のように考えてください。
- 「通常の変数にconstを付けると、代入が禁止される」という基本だけ理解する
- ポインタの文法に慣れてきたら、改めて「ポインタとconst」の関係を勉強する
C言語は学習の順番がとても大切です。
いきなり全てを理解しようとせず、「変数にconstを付けると書き換え禁止になる」という1点に集中しておくのが、挫折しないコツです。
#defineによるマクロ定数
#defineの基本的な書き方と意味
#defineは、C言語のプリプロセッサ(前処理)という仕組みで使われる命令です。
定数を定義するときには、次のような形で使います。
#define 名前 置き換える値
例えば、円周率を定義するなら次のようになります。
#define PI 3.1415926535
この宣言は、「ソースコード中のPIという文字列を、コンパイル前に3.1415926535という文字列に単純に置き換える」というルールを作っているだけです。
ここがconstとの大きな違いです。
#defineには「型」という概念がなく、単に文字列を置き換えているだけです。
コンパイル前の置き換え(プリプロセッサ)のしくみ
C言語のコンパイルは、大きく分けて次のような流れで行われます。
| 段階 | 役割 |
|---|---|
| プリプロセス | #include, #defineなどを処理する |
| コンパイル | Cのソースコードを機械語に変換する準備 |
| アセンブル・リンク | 実行可能ファイルを作成する |
#defineは、コンパイラが動き出す「前」に行われる処理で、プリプロセッサという部分が担当します。
仕組みを、かなり単純化してイメージだけ説明すると、次のようになります。
- ソースコードを上から順に読み込む
#define PI 3.14のような行を見つけたら、「PIは3.14に置き換える」というルールを記録する- その後に出てくる
PIという文字列を、すべて3.14という文字列に置き換える - 置き換えが終わったソースコードを、コンパイラに渡す
実際の処理はもっと複雑ですが、初心者のうちは「PIという文字が、コンパイル前に3.14という文字にすげ替えられている」程度のイメージで問題ありません。
マクロ定数の具体例
実際の例で、constと同じような定数を#defineで書いてみます。
#include <stdio.h>
// マクロ定数の定義
#define MAX_HP 100
#define TAX_RATE 10
#define PI 3.1415926535
int main(void) {
int hp = MAX_HP; // ここではMAX_HPが100に置き換えられる
int price = 1000;
int tax_included = price * (100 + TAX_RATE) / 100;
printf("最大HP: %d\n", MAX_HP);
printf("HPの初期値: %d\n", hp);
printf("税込価格: %d\n", tax_included);
printf("円周率: %f\n", PI);
return 0;
}
最大HP: 100
HPの初期値: 100
税込価格: 1100
円周率: 3.141593
この例では、ソースコード中のMAX_HPやTAX_RATEが、プリプロセスの段階で数値に置き換えられています。
ここでの重要なポイントは、#defineで定義したマクロ定数には「型」の情報がないということです。
コンパイラが見るときにはすでに単なる数値になっているため、「intなのかdoubleなのか」といった区別が付与されていません。
マクロに型がないことのメリットとデメリット
マクロ定数に型がないことは、状況によってメリットにもデメリットにもなります。
メリット
どんな場面でも、そのまま数字として使えるという柔軟さがあります。
例えば、次のようにintにもdoubleにも使えます。
#include <stdio.h>
#define VALUE 10
int main(void) {
int a = VALUE; // VALUE → 10 (intとして扱う)
double b = VALUE; // VALUE → 10.0 (doubleとして扱う)
printf("a = %d, b = %f\n", a, b);
return 0;
}
a = 10, b = 10.000000
1つのマクロ定数で、複数の型の場面に使い回しやすいという点は、マクロの利点です。
デメリット
一方で、型がないために、ミスをコンパイル時に検出しにくいという問題があります。
例えば、次のようなコードを考えます。
#define PI 3.1415926535
int main(void) {
int x = PI; // 本当はdoubleで扱いたかったが、intに代入してしまった
return 0;
}
この場合、PIは3.1415926535に置き換えられますが、その後の処理は「double型の3.1415926535をintに代入する」という扱いになってしまい、小数部分が切り捨てられます。
constであれば、型がはっきり決まっているので、「intに代入するのはおかしい」といったことに気付きやすいですが、マクロでは単なる数値になってしまうため、問題が見えにくくなります。
また、マクロは単純な文字の置き換えであるため、次のような落とし穴も発生しやすくなります。
この点については後の章で詳しく説明します。
constと#defineの使い分け
constと#defineの主な違い
ここまで見てきた内容を、初心者向けに整理して比較してみます。
| 項目 | const | #define |
|---|---|---|
| 役割 | 書き換え禁止の変数を定義する | 文字列の置き換えルールを定義する |
| 型 | ある | ない |
| チェックのタイミング | コンパイル時に型チェックされる | 置き換えだけで型チェックなし |
| スコープのルール | 変数と同じ(ブロック・ファイル単位) | 主にファイル全体(条件付きも可能) |
| デバッグ時の見え方 | 変数としてデバッガで確認しやすい | 置き換え済みの値として扱われる |
| 間違った代入時の扱い | コンパイルエラーになる | そもそも「変数」ではない |
constは「C言語の文法の一部」であり、#defineは「コンパイル前のテキスト処理」であるという根本的な違いを押さえておくことが重要です。
エラーを防ぐための基本方針
初心者にとって重要なのは、「安全で、バグを生みにくい書き方」を選ぶことです。
その観点から、次のような基本方針をおすすめします。
- 型付きの定数が欲しいときは、まずconstを選ぶ
例えば、配列のサイズやゲームのパラメータなど、多くのケースではconstで十分です。 - プリプロセッサ特有の機能が必要な場合だけ#defineを使う
例えば、次のような場面です。#ifdefなどと組み合わせて、コンパイルの有無を切り替える- 条件付きコンパイルで、デバッグ用のコードをON/OFFする
- 古いCコンパイラで
constサポートが弱い場合(最近はあまりない)
- 「単純な定数」にはconstを使う習慣を付ける
こうすることで、マクロ特有の落とし穴を避けられるようになります。
「まずconstで書けないかを考える。どうしても必要なときだけ#defineを使う」という順番で考えると、安全なコードになりやすくなります。
#defineを使ったときのよくある落とし穴
#defineは便利ですが、初心者がハマりやすい落とし穴がいくつかあります。
その代表的なものを紹介します。
1. 括弧を付けないことによる計算ミス
例えば、次のようなマクロを考えてみましょう。
#define DOUBLE(x) x * 2
一見すると「xを2倍するマクロ」に見えますが、次のように使うと問題が発生します。
#include <stdio.h>
#define DOUBLE(x) x * 2
int main(void) {
int a = 3;
int b = DOUBLE(a + 1); // 期待するのは (a + 1) * 2 = 8
printf("b = %d\n", b);
return 0;
}
このコードの実際の置き換えを考えると、DOUBLE(a + 1)はa + 1 * 2に展開されてしまいます。
演算子の優先順位に従うと、これはa + (1 * 2)、つまり3 + 2 = 5となってしまいます。
このような問題を防ぐためには、マクロを書くときに次のように必ず括弧で囲む必要があります。
#define DOUBLE(x) ((x) * 2)
しかし、このような括弧の付け方を毎回正しく行うのは、初心者にとっては負担が大きいです。
そのため、「できるだけマクロで関数のようなことをしない」というのが最近の一般的な方針です。
2. デバッグがしにくい
マクロはコンパイル前にすでに置き換えられてしまっているため、デバッガ上では「元のマクロの名前」が見えません。
例えば、MAX_HPをマクロで定義した場合、デバッガではMAX_HPという名前の値を参照することはできず、単に100というリテラル値として処理されてしまいます。
一方、constで定義しておけば、デバッガ上でMAX_HPという変数名をそのまま参照できるため、値の確認やバグ調査がしやすくなります。
3. スコープが分かりにくい
マクロは基本的に、そのソースファイル全体で有効です。
関数の中だけで有効にするといった制御は、通常の変数よりも複雑になります。
初心者にとっては、「どこからどこまで、どのマクロが有効なのか」が見えにくくなりやすいため、まずはスコープが分かりやすいconst中心で書くほうが理解しやすくなります。
安全な定数定義の書き方サンプルコード集
最後に、初心者が実際のコードで使いやすい「安全な定数定義」の書き方を、いくつか具体例としてまとめます。
例1: 配列のサイズをconstで定義する
#include <stdio.h>
int main(void) {
// 配列サイズをconstで定数定義
const int NUM_STUDENTS = 3;
int scores[NUM_STUDENTS] = {80, 90, 75};
int i;
int sum = 0;
for (i = 0; i < NUM_STUDENTS; i++) {
sum += scores[i];
}
double average = (double)sum / NUM_STUDENTS;
printf("人数: %d\n", NUM_STUDENTS);
printf("平均点: %.2f\n", average);
return 0;
}
人数: 3
平均点: 81.67
このように配列サイズにはconstを使うと、意味が分かりやすく、安全に変更できるようになります。
例2: グローバルな設定値をconstで定義する
#include <stdio.h>
// プログラム全体で使う設定値をconstで定義
const int WINDOW_WIDTH = 800;
const int WINDOW_HEIGHT = 600;
void print_window_size(void) {
printf("ウィンドウサイズ: %d x %d\n", WINDOW_WIDTH, WINDOW_HEIGHT);
}
int main(void) {
print_window_size();
// 書き換えようとするとエラーになります
// WINDOW_WIDTH = 1024; // ← コンパイルエラー
return 0;
}
ウィンドウサイズ: 800 x 600
このように「プログラム全体で共有するが変わらない値」は、グローバルなconstとして定義しておくとよいです。
例3: 条件付きコンパイルにだけ#defineを使う
条件付きコンパイルでは、#defineが必要になります。
このような「プリプロセッサが必須の場面」でのみ、#defineを使うと安全です。
#include <stdio.h>
// デバッグビルドのときだけDEBUGを定義するイメージ
#define DEBUG
int main(void) {
int x = 10;
#ifdef DEBUG
// DEBUGが定義されているときだけ、このprintfがコンパイルされる
printf("デバッグ用: x = %d\n", x);
#endif
printf("通常の処理\n");
return 0;
}
実行結果(このままビルドした場合)の一例です。
デバッグ用: x = 10
通常の処理
このように、「コンパイルするかどうかを切り替える」といった用途では#defineが適しているため、使い分けることが重要です。
例4: マクロをどうしても使う場合の、できるだけ安全な書き方
どうしてもマクロを使う場合は、次のようなルールを守ると、危険性をある程度抑えられます。
#include <stdio.h>
// マクロ定数には必ず括弧を付ける
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
int x = 3;
int y = 5;
int m = MAX(x + 1, y); // ((x + 1) > (y) ? (x + 1) : (y)) に展開される
printf("最大値: %d\n", m);
return 0;
}
最大値: 5
ただし、マクロ関数は副作用(引数に++を渡すなど)の問題もあり、初心者には扱いが難しいです。
そのため、まずは通常の関数とconstを組み合わせて書けないかを優先的に考えるとよいでしょう。
まとめ
この記事では、C言語の定数を定義する2つの方法(constと#define)について、初心者向けに解説しました。
まず、定数とはプログラム実行中に値が変わらないものであり、可読性や保守性、バグ防止の観点から非常に重要であることを説明しました。
そして、C言語には次の2つの代表的な方法があることを見てきました。
- const … 書き換え禁止の「変数」を定義する(型あり、コンパイル時チェックあり)
- #define … 文字列の置き換えルールを定義する(型なし、プリプロセッサによる置き換え)
そのうえで、安全でエラーを防ぎやすい基本方針として、「まずconstを使う」ことを提案しました。
constであれば、通常の変数と同じスコープや寿命のルールに従い、デバッガでも扱いやすく、型チェックも働きます。
一方、#defineはとても強力ですが、括弧不足による計算ミスや、デバッグのしにくさ、スコープの分かりにくさなど、多くの落とし穴があることも紹介しました。
そのため、条件付きコンパイルなど、プリプロセッサ固有の機能が必要な場面に限定して使うのが、初心者にとっては安全な選択です。
最後に、配列サイズや設定値をconstで定義する実用的なサンプルコードを通して、日常的なC言語プログラミングでは「型付きの定数=const」が基本であることを確認しました。
これらのポイントを押さえておけば、定数の定義で迷ったときに「なぜそう書くのか」を自分で判断できるようになります。
まずは、普段書いているコードの「魔法の数字」を、1つずつ意味のあるconst定数に置き換えていくところから始めてみてください。
