閉じる

【C言語】変数を初期化しないとバグ地獄に?未初期化変数の挙動を完全解説

C言語では、変数を宣言するだけで満足してしまい、初期化を忘れるミスがとても起こりやすいです。

しかし、未初期化変数は「コンパイルは通るのに動かすとおかしい」タイプの厄介なバグの温床です。

本記事では、C言語仕様に基づいて未初期化変数の挙動を整理し、典型的なバグ例や防止策まで、図解とサンプルコードを交えて丁寧に解説します。

C言語の未初期化変数とは?挙動と危険性

未初期化変数とは何かをC言語の仕様から整理

C言語では、変数は「宣言」だけでは値が決まりません

宣言した変数が「初期化されるかどうか」は、次の要素で変わります。

  • どのストレージ期間(自動・静的)を持つか
  • どこに宣言されたか(関数内、ファイルスコープなど)

まず、用語を整理します。

変数の宣言と初期化

C言語での変数宣言は、次の2パターンがあります。

  • 宣言のみ
    • 例: int x;
  • 宣言と同時に初期化
    • 例: int x = 0;

宣言のみでは「値は未定義」であり、初期化子(= 以降)を書いた場合にのみ、値が保証されるという点が重要です。

C言語仕様における「未初期化」

C標準規格では、変数の型やストレージ期間ごとに「デフォルト初期値」が定められています。

  • 自動変数(ローカル変数)はデフォルト初期値を持たない
  • 静的記憶域期間(グローバル変数・static変数)は0で初期化される

したがって、関数内で宣言しただけのint x;は典型的な未初期化変数です。

「不定値」とは?ランダムに見える値が入る理由

未初期化の自動変数を読むと、その値は「不定値(indeterminate value)」になります。

不定値とは何か

不定値とは、C標準が「どんな値になるか一切保証しない」状態を指します。

よく「ランダムな値が入る」と説明されますが、正確には以下の性質を持ちます。

  • 以前そのメモリ領域にあったデータの「残りかす」が読まれることがある
  • 実行のたびに変わる場合もあれば、環境によってはたまたま同じ値が続くこともある
  • プログラマはその値に一切依存してはならない

つまり、「たまたま期待通りの値っぽく見えても、それは仕様ではなく偶然」です。

なぜランダムに「見える」のか

メモリには、過去に使われていた変数・配列・関数のスタックフレームなど、さまざまなデータが置かれては消えていきます。

未初期化の変数は、その領域に残っている「ゴミデータ」をそのまま読むため、実行タイミングや呼び出し順、最適化の有無などにより値が変わります。

この挙動により、「デバッグ時には再現せず、本番でだけ謎の動作をする」といった、厄介な不具合が生まれやすくなります。

なぜ未初期化変数はバグ地獄を招くのか

未初期化変数が危険なのは、バグが「再現性の低い、原因不明な振る舞い」になりやすいからです。

  • テスト環境では「たまたま良い値」になり、バグが顕在化しない
  • 本番環境では「悪い値」になり、障害として表面化する
  • ログを見ても原因が特定しにくく、直感的に再現できない

このように、品質保証やデバッグコストを極端に押し上げるため、未初期化変数は「絶対に避けるべき危険なコード」として、コーディング規約でも強く禁止されることが多いです。

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で初期化されます

C言語
#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で初期化されます。

C言語
#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で初期化」と定めています。

ポインタ変数を未初期化のまま使う危険性

未初期化のポインタ変数は、プログラムを即クラッシュさせる潜在的爆弾です。

サンプル: 未初期化ポインタの使用

C言語
#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が割り当てていない領域へのアクセス
  • 別の変数やシステム領域への書き込み

といった危険な操作になり、セグメンテーションフォルトや深刻なデータ破壊を引き起こします。

構造体・配列の未初期化と部分初期化の挙動

配列の未初期化

ローカルな配列も、初期化をしないと中身は不定値です。

C言語
#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で初期化されます。

C言語
#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で初期化されます。

C言語
#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文やループ条件で未初期化変数を使ってしまう例

サンプル: 未初期化フラグの使用

C言語
#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

どちらが出力されるかは一切保証されず、コードを読んだだけでは意図が分かりません。

ループ条件での例

C言語
#include <stdio.h>

int main(void) {
    int n;  // 未初期化

    // n の値次第でループ回数が変わる、あるいは無限ループの危険もある
    for (int i = 0; i < n; i++) {
        printf("i = %d\n", i);
    }

    return 0;
}

このようなコードは、ある環境では1回もループしないかもしれませんし、別の環境では非常に大きな回数ループするかもしれません。

計算結果が毎回変わる再現性の低いバグ

サンプル: 合計値がおかしくなる例

C言語
#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

不定値に対して計算を重ねるため、結果もまた不定です。

未初期化ポインタによるセグメンテーションフォルト

サンプル: 関数からのポインタ返却でのミス

C言語
#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;
}

この種のバグは、スタックやヒープの破壊につながり、プログラムが突然落ちたり、謎の挙動を示したりします。

マルチスレッドで顕在化しやすい未初期化バグ

シングルスレッドでは滅多に再現しない未初期化バグも、マルチスレッド環境では顕在化しやすくなります。

例: スレッドローカルな未初期化変数

C言語
#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言語のベストプラクティス

変数宣言と同時に初期化するコーディングスタイル

最も基本かつ強力な対策は、「変数は宣言と同時に必ず初期化する」というルールを徹底することです。

良くない例

C言語
int main(void) {
    int sum;          // ここでは初期化していない
    /* ... いろいろな処理 ... */
    sum = 0;          // ここで初めて初期化
    /* ... さらに処理 ... */
}

このスタイルだと、sum を初期化する前にうっかり使ってしまう危険があります。

良い例

C言語
int main(void) {
    int sum = 0;      // 宣言と同時に初期化

    /* sum を使う前に必ず 0 が入っていることが保証される */
    /* ... */
}

ポインタも同様に、必ずNULLで初期化しておくと安全です。

C言語
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で確保したメモリを0xCC0xCDで埋める
  • 解放済みメモリを0xDDで埋める
  • C++ですが、MSVCのデバッグランタイムなどが有名

Cで自前に似たことを行う場合、デバッグ用マクロを用意しておくことがあります。

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}などで全メンバを初期化する
  • 条件式に未初期化の可能性がある変数を絶対に使用しない

レビュー時のチェック観点

コードレビューでは、次の点を意識して確認します。

  • ローカル変数が初期値なしで宣言されていないか
  • 変数が使われるまでに、必ず値が代入されているか
  • 条件式やループ条件に、初期化済みであることが明らかでない変数が使われていないか
  • ポインタがmalloccalloc&演算子などで正しく初期化されているか

このような観点をチェックリスト化し、レビュー文化として定着させることで、未初期化バグの混入を大幅に減らすことができます。

まとめ

未初期化変数は、C言語の世界で最も危険で、かつ発見しにくいバグ原因の1つです。

自動変数は初期値を持たず、不定値を使った瞬間に未定義動作となり、テストと本番で挙動が変わる厄介な問題を生みます。

グローバル・static変数が0で初期化されることや、配列・構造体の部分初期化などの仕様を正しく理解しつつ、「宣言と同時初期化」「ポインタのNULL初期化」「コンパイラ警告・静的解析の活用」「チームとしてのコーディング規約」を徹底することで、未初期化変数によるバグ地獄を未然に防ぐことができます。

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

URLをコピーしました!