C言語では、変数を宣言するだけで満足してしまい、初期化を忘れるミスがとても起こりやすいです。
しかし、未初期化変数は「コンパイルは通るのに動かすとおかしい」タイプの厄介なバグの温床です。
本記事では、C言語仕様に基づいて未初期化変数の挙動を整理し、典型的なバグ例や防止策まで、図解とサンプルコードを交えて丁寧に解説します。
C言語の未初期化変数とは?挙動と危険性
未初期化変数とは何かをC言語の仕様から整理

C言語では、変数は「宣言」だけでは値が決まりません。
宣言した変数が「初期化されるかどうか」は、次の要素で変わります。
- どのストレージ期間(自動・静的)を持つか
- どこに宣言されたか(関数内、ファイルスコープなど)
まず、用語を整理します。
変数の宣言と初期化
C言語での変数宣言は、次の2パターンがあります。
- 宣言のみ
- 例:
int x;
- 例:
- 宣言と同時に初期化
- 例:
int x = 0;
- 例:
宣言のみでは「値は未定義」であり、初期化子(= 以降)を書いた場合にのみ、値が保証されるという点が重要です。
C言語仕様における「未初期化」
C標準規格では、変数の型やストレージ期間ごとに「デフォルト初期値」が定められています。
- 自動変数(ローカル変数)はデフォルト初期値を持たない
- 静的記憶域期間(グローバル変数・static変数)は0で初期化される
したがって、関数内で宣言しただけのint x;は典型的な未初期化変数です。
「不定値」とは?ランダムに見える値が入る理由

未初期化の自動変数を読むと、その値は「不定値(indeterminate value)」になります。
不定値とは何か
不定値とは、C標準が「どんな値になるか一切保証しない」状態を指します。
よく「ランダムな値が入る」と説明されますが、正確には以下の性質を持ちます。
- 以前そのメモリ領域にあったデータの「残りかす」が読まれることがある
- 実行のたびに変わる場合もあれば、環境によってはたまたま同じ値が続くこともある
- プログラマはその値に一切依存してはならない
つまり、「たまたま期待通りの値っぽく見えても、それは仕様ではなく偶然」です。
なぜランダムに「見える」のか
メモリには、過去に使われていた変数・配列・関数のスタックフレームなど、さまざまなデータが置かれては消えていきます。
未初期化の変数は、その領域に残っている「ゴミデータ」をそのまま読むため、実行タイミングや呼び出し順、最適化の有無などにより値が変わります。
この挙動により、「デバッグ時には再現せず、本番でだけ謎の動作をする」といった、厄介な不具合が生まれやすくなります。
なぜ未初期化変数はバグ地獄を招くのか

未初期化変数が危険なのは、バグが「再現性の低い、原因不明な振る舞い」になりやすいからです。
- テスト環境では「たまたま良い値」になり、バグが顕在化しない
- 本番環境では「悪い値」になり、障害として表面化する
- ログを見ても原因が特定しにくく、直感的に再現できない
このように、品質保証やデバッグコストを極端に押し上げるため、未初期化変数は「絶対に避けるべき危険なコード」として、コーディング規約でも強く禁止されることが多いです。
C言語における未初期化変数の具体的な挙動
ローカル変数(自動変数)の未初期化時の挙動

サンプル: 未初期化ローカル変数
#include <stdio.h>
int main(void) {
int x; // ローカル変数(自動変数)。ここでは初期化していない
// 未初期化変数をそのまま使うのは未定義動作
printf("x = %d\n", x);
return 0;
}
このプログラムはコンパイルでき、多くの環境で実行も「できてしまい」ます。
しかし、表示される値は完全に不定です。
x = 32767
別の実行では次のようになるかもしれません。
x = 0
なぜローカル変数は初期化されないのか
ローカル変数はスタック領域に確保され、関数呼び出しごとに使い回されます。
C標準では、自動変数の内容を0クリアする義務は課していません。
コンパイラやOSが速度を重視しているため、「そのまま残っている値を使う」のです。
グローバル変数・静的変数の初期値

グローバル変数の初期値
ファイルスコープ(関数の外)で宣言された変数は、初期化を書かなくても0で初期化されます。
#include <stdio.h>
int g; // グローバル変数。暗黙に0で初期化される
static int s; // ファイルスコープのstatic変数も0で初期化される
int main(void) {
printf("g = %d\n", g);
printf("s = %d\n", s);
return 0;
}
g = 0
s = 0
関数内static変数の初期値
関数の中でもstaticを付けると、静的記憶域期間を持ちます。
この場合も、初期化を書かなくても0で初期化されます。
#include <stdio.h>
void func(void) {
static int counter; // ここも暗黙に0から開始される
counter++;
printf("counter = %d\n", counter);
}
int main(void) {
func();
func();
func();
return 0;
}
counter = 1
counter = 2
counter = 3
静的変数が0で初期化される理由
静的記憶域期間の変数は、プログラム起動時に一度だけ確保されるため、OSやランタイムがまとめて0クリアしやすい構造になっているためです。
C標準もこれを前提に「明示的な初期化子がない静的変数は0で初期化」と定めています。
ポインタ変数を未初期化のまま使う危険性

未初期化のポインタ変数は、プログラムを即クラッシュさせる潜在的爆弾です。
サンプル: 未初期化ポインタの使用
#include <stdio.h>
int main(void) {
int *p; // 未初期化ポインタ。どこを指しているか不明
// 未初期化のまま書き込みを行う危険なコード
*p = 10; // ここで不正メモリアクセスになる可能性が高い
printf("*p = %d\n", *p);
return 0;
}
多くの環境では、実行すると次のようなエラーになります。
Segmentation fault (core dumped)
あるいは Windows では:
アクセス違反(Access Violation)
なぜ未初期化ポインタが危ないのか
ポインタには「有効なメモリアドレス」を入れなければなりませんが、未初期化のポインタには過去のゴミアドレスが入っています。
そのため:
- OSが割り当てていない領域へのアクセス
- 別の変数やシステム領域への書き込み
といった危険な操作になり、セグメンテーションフォルトや深刻なデータ破壊を引き起こします。
構造体・配列の未初期化と部分初期化の挙動

配列の未初期化
ローカルな配列も、初期化をしないと中身は不定値です。
#include <stdio.h>
int main(void) {
int a[5]; // 未初期化配列。各要素は不定値
for (int i = 0; i < 5; i++) {
printf("a[%d] = %d\n", i, a[i]);
}
return 0;
}
a[0] = 0
a[1] = 32767
a[2] = -1
a[3] = 0
a[4] = 4194304
値は毎回異なる可能性があります。
部分初期化の挙動(配列)
配列リテラルで{}を使って部分的に初期化した場合、指定しなかった要素は0で初期化されます。
#include <stdio.h>
int main(void) {
int a[5] = {1, 2}; // 残りの要素は 0 で初期化される
for (int i = 0; i < 5; i++) {
printf("a[%d] = %d\n", i, a[i]);
}
return 0;
}
a[0] = 1
a[1] = 2
a[2] = 0
a[3] = 0
a[4] = 0
これは<C言語仕様>で定められている挙動です。
部分初期化の挙動(構造体)
構造体でも同様に、指定しなかったメンバは0で初期化されます。
#include <stdio.h>
struct Point {
int x;
int y;
int z;
};
int main(void) {
struct Point p = { .x = 10 }; // y, z は 0 で初期化される
printf("p.x = %d, p.y = %d, p.z = %d\n", p.x, p.y, p.z);
return 0;
}
p.x = 10, p.y = 0, p.z = 0
この性質を利用すると、「まず全メンバを0にしてから、必要なところだけ値を入れる」という安全な初期化が可能です。
未初期化変数が原因の典型的なバグ例
if文やループ条件で未初期化変数を使ってしまう例

サンプル: 未初期化フラグの使用
#include <stdio.h>
int main(void) {
int flag; // 未初期化
if (flag) { // ここで不定値を条件式に使っている
printf("flag is TRUE\n");
} else {
printf("flag is FALSE\n");
}
return 0;
}
このプログラムは、実行のたびに出力が変わる可能性があります。
flag is TRUE
または
flag is FALSE
どちらが出力されるかは一切保証されず、コードを読んだだけでは意図が分かりません。
ループ条件での例
#include <stdio.h>
int main(void) {
int n; // 未初期化
// n の値次第でループ回数が変わる、あるいは無限ループの危険もある
for (int i = 0; i < n; i++) {
printf("i = %d\n", i);
}
return 0;
}
このようなコードは、ある環境では1回もループしないかもしれませんし、別の環境では非常に大きな回数ループするかもしれません。
計算結果が毎回変わる再現性の低いバグ

サンプル: 合計値がおかしくなる例
#include <stdio.h>
int main(void) {
int sum; // 未初期化
for (int i = 0; i < 10; i++) {
sum += i; // sum に初期値を入れていないまま加算している
}
printf("sum = %d\n", sum);
return 0;
}
期待される正しい結果は45ですが、実際には次のようにバラバラになるかもしれません。
sum = 45
sum = 12345
sum = -32723
不定値に対して計算を重ねるため、結果もまた不定です。
未初期化ポインタによるセグメンテーションフォルト

サンプル: 関数からのポインタ返却でのミス
#include <stdio.h>
int *get_value(void) {
int *p; // 未初期化ポインタ
// 本当は何かしらの有効なアドレスを設定すべきだが、書き忘れている
// *p = 100; // ここで書き込むと、その時点で危険
return p; // 不定なアドレスを呼び出し元に返してしまう
}
int main(void) {
int *q = get_value();
printf("*q = %d\n", *q); // ここで不正メモリアクセスになる可能性が高い
return 0;
}
この種のバグは、スタックやヒープの破壊につながり、プログラムが突然落ちたり、謎の挙動を示したりします。
マルチスレッドで顕在化しやすい未初期化バグ

シングルスレッドでは滅多に再現しない未初期化バグも、マルチスレッド環境では顕在化しやすくなります。
例: スレッドローカルな未初期化変数
#include <stdio.h>
#include <pthread.h>
void *worker(void *arg) {
int id; // 未初期化: スレッドIDとして使うつもりだった
printf("Thread id = %d\n", id); // 不定なIDが出力される
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, NULL);
pthread_create(&t2, NULL, worker, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
実行のたびに:
Thread id = 0
Thread id = 12345
や
Thread id = -32768
Thread id = 5
など、さまざまなパターンが現れます。
マルチスレッドでは、スケジューリングやスタックレイアウトが実行ごとに変化するため、未初期化変数の影響がより顕著になります。
その結果、「テストでは全く再現しないのに、本番だけで障害が出る」といった悪夢のような状況になりがちです。
未初期化変数を防ぐためのC言語のベストプラクティス
変数宣言と同時に初期化するコーディングスタイル

最も基本かつ強力な対策は、「変数は宣言と同時に必ず初期化する」というルールを徹底することです。
良くない例
int main(void) {
int sum; // ここでは初期化していない
/* ... いろいろな処理 ... */
sum = 0; // ここで初めて初期化
/* ... さらに処理 ... */
}
このスタイルだと、sum を初期化する前にうっかり使ってしまう危険があります。
良い例
int main(void) {
int sum = 0; // 宣言と同時に初期化
/* sum を使う前に必ず 0 が入っていることが保証される */
/* ... */
}
ポインタも同様に、必ずNULLで初期化しておくと安全です。
int *p = NULL; // 未初期化ではなく、「何も指していない」と明示される
NULLで初期化しておけば、if (p != NULL)のように安全確認ができ、意図しないメモリアクセスを防ぎやすくなります。
コンパイラ警告と静的解析ツールで未初期化変数を検出

コンパイラや静的解析ツールを活用すると、多くの未初期化変数を自動的に検出できます。
コンパイラ警告を最大限に活用する
gcc/clangの場合、次のようなオプションを利用します。
-Wall -Wextra-Wuninitialized-Werrorで警告をエラー扱いにするのも有効
warning: ‘x’ is used uninitialized in this function [-Wuninitialized]
のような警告が出たら、必ず原因を突き止めて修正します。
静的解析ツールの利用
より厳しくチェックするには、次のようなツールを使うと効果的です。
- clang-tidy
- cppcheck
- commercialな静的解析ツール(例: Coverity、Fortify など)
これらは、複雑な制御フローにおける「特定のパスで初期化されていない」ケースまで検出してくれます。
デバッグビルドでの「既知のパターン埋め」でバグを炙り出す

一部のライブラリやランタイムでは、デバッグビルド時に未初期化メモリへ特定のパターンを書き込む機能があります。
代表的な手法
- mallocで確保したメモリを
0xCCや0xCDで埋める - 解放済みメモリを
0xDDで埋める - C++ですが、MSVCのデバッグランタイムなどが有名
Cで自前に似たことを行う場合、デバッグ用マクロを用意しておくことがあります。
#include <string.h>
#ifdef DEBUG
#define DEBUG_INIT(ptr, size) memset((ptr), 0xCC, (size))
#else
#define DEBUG_INIT(ptr, size) ((void)0)
#endif
int main(void) {
int a[10];
// デバッグビルド時だけ 0xCC で埋める
DEBUG_INIT(a, sizeof(a));
// ここで a をきちんと初期化せずに使うと、異常な値が検出しやすくなる
return 0;
}
このように、わざと「異常な値」を入れておくことで、未初期化利用を早期に炙り出せます。
コーディング規約(初期化ルール)の導入とレビュー観点

チーム開発では、個々人の注意に依存せず、組織として未初期化変数を防ぐ仕組みが重要です。
コーディング規約として定めるべき項目の例
- 全てのローカル変数は宣言と同時に初期化する
- ポインタは必ず
NULLで初期化する - 構造体は
= {0}などで全メンバを初期化する - 条件式に未初期化の可能性がある変数を絶対に使用しない
レビュー時のチェック観点
コードレビューでは、次の点を意識して確認します。
- ローカル変数が初期値なしで宣言されていないか
- 変数が使われるまでに、必ず値が代入されているか
- 条件式やループ条件に、初期化済みであることが明らかでない変数が使われていないか
- ポインタが
mallocやcalloc、&演算子などで正しく初期化されているか
このような観点をチェックリスト化し、レビュー文化として定着させることで、未初期化バグの混入を大幅に減らすことができます。
まとめ
未初期化変数は、C言語の世界で最も危険で、かつ発見しにくいバグ原因の1つです。
自動変数は初期値を持たず、不定値を使った瞬間に未定義動作となり、テストと本番で挙動が変わる厄介な問題を生みます。
グローバル・static変数が0で初期化されることや、配列・構造体の部分初期化などの仕様を正しく理解しつつ、「宣言と同時初期化」「ポインタのNULL初期化」「コンパイラ警告・静的解析の活用」「チームとしてのコーディング規約」を徹底することで、未初期化変数によるバグ地獄を未然に防ぐことができます。
