C言語では、変数を「宣言するだけ」で使えてしまうため、うっかり初期化を忘れてもコンパイルが通ることがあります。
しかし、そのまま実行すると結果が毎回変わったり、原因不明の不具合につながったりします。
未初期化は典型的なバグの温床です。
本記事では、未初期化変数の正体と危険性、そして確実に回避するための初期化の基本とコツを、初心者の方にも分かりやすく解説します。
C言語の未初期化変数とは?
未初期化変数とは、宣言はしたものの何の値も代入していない変数のことです。
C言語では、記憶域期間(スコープではなくメモリの性質)によって「初期化されない場合の状態」が異なります。
初期化の意味
初期化とは、変数を定義したときに最初の値を与えることを指します。
たとえば、int x = 0;
のように書きます。
Cでは初期化されていないローカル変数の値は「不定」であり、読み出すと未定義動作になります。
これは「何が起きてもおかしくない」というC規格上の取り扱いです。
ローカル変数は初期値なし
関数の中で宣言した通常の変数(自動記憶域)は、初期化しない限り不定値を持ちます。
次のコードはあくまで「危険例」で、結果は環境やビルド設定によって変わります。
#include <stdio.h>
int main(void) {
int x; // ローカル変数。初期化していない(不定値)
printf("x = %d\n", x); // xの読み出しは未定義動作(実行ごとに出力が変わる可能性)
return 0;
}
実行例(環境により異なります)
x = 32764
同じプログラムでも、別の実行例では次のようになることがあります。
x = 0
このように、未初期化の値に意味はありません。
たまたま0に見えても「0に初期化された」わけではないことに注意してください。
グローバルとstaticは0になる
一方で、グローバル変数やstatic
指定子を持つ変数(静的記憶域)は、明示的に初期化しなかった場合必ず0に初期化されます。
#include <stdio.h>
int g; // グローバル変数。明示しなくても0で初期化される
void show(void) {
static int s; // 関数内staticも0で初期化される
printf("g = %d, s = %d\n", g, s);
}
int main(void) {
show();
return 0;
}
g = 0, s = 0
この違いを整理しておくと混乱しにくくなります。
記憶域期間(代表例) | 例 | 初期化しない場合の初期値 | 読み出しの可否と注意点 |
---|---|---|---|
自動(関数内の通常の変数) | int x; | 不定 | 読み出しは未定義動作(何が起きても不思議でない) |
静的(グローバル/static) | static int y; | 0 | 読み出し可能(0が入る) |
動的(ヒープ: malloc領域) | int *p = malloc(...); | 不定 | 読み出しは未定義動作。calloc は0埋め |
動的確保領域は構成外ですが、実務では重要です。
メモリの動的確保の場合、malloc
は未初期化、calloc
は0初期化であることを覚えておくと安全です。
変数を初期化しないとどうなる?
未初期化はとにかく危険です。
実行するたびに結果が変わる、開発環境を変えると挙動が変わる、最適化の有無で変わるなど、デバッグを難しくします。
条件分岐が誤判定
ifの条件に未初期化変数を使うのは致命的です。
分岐の真偽が実行ごとに変わることがあります。
#include <stdio.h>
int main(void) {
int flag; // 初期化していない
if (flag) {
printf("ON\n");
} else {
printf("OFF\n");
}
return 0;
}
実行例1
OFF
実行例2(同じバイナリでも起こり得ます)
ON
コンパイラによっては0で初期化してくれることもありますが、絶対ではありません。「たまたま正しく動いた」ように見えても、それは偶然です。
計算結果が不安定
未初期化の値を使った計算は、当然ながら結果が不安定です。
#include <stdio.h>
int main(void) {
int a; // 未初期化
int b = 10;
int sum = a + b; // aの値が不定なのでsumも不定
printf("a = %d, b = %d, sum = %d\n", a, b, sum);
return 0;
}
a = -1059357568, b = 10, sum = -1059357558
別の実行例
a = 0, b = 10, sum = 10
このような不安定さはテストでは見逃されやすく、本番環境で発火する典型的なバグ要因です。
予測不能な動作(未定義動作)
未初期化を使うと、想定外の範囲アクセスなどの二次被害を誘発します。
たとえば、未初期化のインデックスで配列にアクセスすると、境界外アクセスを引き起こすかもしれません。
#include <stdio.h>
int main(void) {
int idx; // 未初期化
int arr[4] = {0, 1, 2, 3};
// idxの値しだいで配列範囲外アクセス(未定義動作)になる可能性
printf("arr[idx] = %d\n", arr[idx]);
return 0;
}
このコードは「クラッシュするか」「一見動くか」は実行時の偶然次第です。
未定義動作は環境・最適化・実行タイミングによって結果が変わり続けます。
デバッグが困難になる
未初期化バグは、以下の理由で特に厄介です。
- デバッガでステップ実行するとたまたま値が変わり、再現しないことがあるため。
- 最適化(
-O2
など)でコードが変形され、症状が消えたり悪化したりするため。 - ログを入れると挙動が変わり、根本原因に辿り着けないため。
「再現率が低い不具合」「環境依存で壊れる」場合、未初期化を真っ先に疑うのが良い習慣です。
変数の初期化の基本
原則はシンプルです。
宣言したら初期化する。
そして使う直前に最適な初期値を与える。
この2点を守るだけで、多くの事故を防げます。
宣言と同時に初期化(int x = 0;)
最も確実で読みやすい初期化方法です。
#include <stdio.h>
int main(void) {
int x = 0; // 宣言と同時に初期化
printf("x = %d\n", x);
return 0;
}
x = 0
使う直前に初期化
スコープを狭く保ち、値の意味が最もはっきりする場所で初期化します。
これにより、誤用の余地が減ります。
#include <stdio.h>
int main(void) {
int n = 3;
int sum = 0;
for (int i = 0; i < n; ++i) {
int value = i * 2; // 使う直前で初期化
sum += value;
}
printf("sum = %d\n", sum); // 0 + 2 + 4 = 6
return 0;
}
sum = 6
数値型の例(int,double)
整数や実数では、0や意味のある初期値を与えましょう。
#include <stdio.h>
int main(void) {
int count = 0; // カウンタは0から
double ratio = 1.0; // 比率は1.0など意味のある初期値
double result = count + ratio; // 0 + 1.0 = 1.0
printf("count = %d, ratio = %.2f, result = %.2f\n", count, ratio, result);
return 0;
}
count = 0, ratio = 1.00, result = 1.00
文字型の例(char)
文字1個ならシングルクォート、文字列ならNUL終端に注意します。
空文字を表したい場合は'\0'
または0
を使います。
#include <stdio.h>
int main(void) {
char ch = 'A'; // 文字の初期化
char nul = '#include <stdio.h>
int main(void) {
char ch = 'A'; // 文字の初期化
char nul = '\0'; // NUL文字(数値0)
char str[] = "Hello"; // 'H','e','l','l','o','\0' の6バイト
printf("ch = %c, nul = %d, str = %s\n", ch, nul, str);
return 0;
}
'; // NUL文字(数値0)
char str[] = "Hello"; // 'H','e','l','l','o','#include <stdio.h>
int main(void) {
char ch = 'A'; // 文字の初期化
char nul = '\0'; // NUL文字(数値0)
char str[] = "Hello"; // 'H','e','l','l','o','\0' の6バイト
printf("ch = %c, nul = %d, str = %s\n", ch, nul, str);
return 0;
}
' の6バイト
printf("ch = %c, nul = %d, str = %s\n", ch, nul, str);
return 0;
}
ch = A, nul = 0, str = Hello
配列の初期化の基本
配列はブレース{}
を使うと安全に初期化できます。
部分初期化では、指定しなかった要素は0になります。
#include <stdio.h>
int main(void) {
int a[5] = {0}; // 全要素が0で初期化
int b[5] = {1, 2, 3}; // 残り2要素は0で初期化(= {1,2,3,0,0})
char s1[6] = "Hello"; // 'H','e','l','l','o','#include <stdio.h>
int main(void) {
int a[5] = {0}; // 全要素が0で初期化
int b[5] = {1, 2, 3}; // 残り2要素は0で初期化(= {1,2,3,0,0})
char s1[6] = "Hello"; // 'H','e','l','l','o','\0'
char s2[6] = {0}; // 6バイトすべて0(空文字列)
printf("a: ");
for (int i = 0; i < 5; ++i) printf("%d ", a[i]);
printf("\n");
printf("b: ");
for (int i = 0; i < 5; ++i) printf("%d ", b[i]);
printf("\n");
printf("s1 = %s, s2 length = %zu\n", s1, strlen(s2));
return 0;
}
'
char s2[6] = {0}; // 6バイトすべて0(空文字列)
printf("a: ");
for (int i = 0; i < 5; ++i) printf("%d ", a[i]);
printf("\n");
printf("b: ");
for (int i = 0; i < 5; ++i) printf("%d ", b[i]);
printf("\n");
printf("s1 = %s, s2 length = %zu\n", s1, strlen(s2));
return 0;
}
a: 0 0 0 0 0
b: 1 2 3 0 0
s1 = Hello, s2 length = 0
構造体や配列をすべて0で初期化したい場合、Cでは= {0}
が簡潔で安全です。
memset(&obj, 0, sizeof obj)
も0初期化には使えますが、0以外の値での初期化には適しません。
未初期化を防ぐコツ
ミスを「起こさない」工夫と「起きても気付ける」仕組みの両立が重要です。
コンパイラ警告を有効化(-Wall)
GCCやClangでは、最低限-Wall -Wextra
を有効にし、可能なら-Wuninitialized
も使います。
WindowsのMSVCなら/W4
以上がおすすめです。
# GCC/Clangの例(C17でビルド)
gcc -std=c17 -Wall -Wextra -O0 sample.c -o sample
# さらに未初期化検出の強化
gcc -std=c17 -Wall -Wextra -Wuninitialized -O0 sample.c -o sample
警告例
sample.c: In function 'main':
sample.c:5:22: warning: 'x' is used uninitialized [-Wuninitialized]
5 | printf("%d\n", x);
| ^
実行時検出の支援として、Clangなら-fsanitize=memory
、GCC/Clangなら-fsanitize=address,undefined
が有用です(学習段階では特に効果的)。
必ず初期値を入れる習慣
- ローカル変数は宣言と同時に意味のある初期値を入れます。決められないなら
0
やNULL
、ブールならfalse
(#include <stdbool.h>
)を使います。 - 条件分岐に使うフラグは必ず初期化し、初期状態を明確にします。
- 構造体や配列は
= {0}
で一括初期化すると安全です。
使わない変数を作らない
未使用変数は初期化漏れの温床です。
必要になる直前まで宣言しない、使わない変数は削除する、ビルドで-Werror=unused-variable
を使って未使用をエラー化する、などの方針が効果的です。
「宣言は最小限、スコープは最小限」を合言葉にしましょう。
まとめ
未初期化変数は、C言語のバグの中でも発見が難しく、被害が大きい代表格です。
ローカル変数は初期値なし(不定)、グローバルやstaticは0になるというルールを正しく理解し、宣言と同時の初期化と使う直前の適切な初期化を徹底しましょう。
配列や構造体は= {0}
で安全に初期化でき、コンパイラ警告(-Wall -Wextra -Wuninitialized
)やサニタイザで未初期化の芽を早期に摘むことができます。
「初期化しないまま使わない」これだけで、原因不明の不具合の多くを未然に防げます。