C言語の学習を始めると、サンプルコードで当たり前のようにint x = 0;のような「初期化」が出てきます。
実はこの初期化をサボると、プログラムが毎回ちがう動きをする不思議で厄介なバグを生み出します。
本記事では、変数の初期化とは何か、なぜ必要なのか、そして初期化しなかった場合にどんな失敗が起きるのかを、C言語初心者向けに丁寧に解説します。
C言語の変数の初期化とは
変数の初期化とはなにか
C言語における変数の初期化とは、変数を「作った瞬間」に最初の値を与えることを指します。
例えば次のようなコードです。
int count = 0; // 変数countを宣言し、同時に0で初期化
double pi = 3.14; // 変数piを宣言し、同時に3.14で初期化
C言語の世界では、メモリにどんな値が入っているかは基本的に「保証されていない」ため、最初に自分で意味のある値を入れておかないと、何が起きるかわからないという性質があります。
そのため、変数を使う前に初期化しておくことは、C言語プログラムを書くうえでの基本中の基本となります。
宣言と初期化の違い
まず、変数には宣言という段階があります。
宣言とは、「この名前の変数を、この型で使います」とコンパイラに知らせることです。
int x; // これは「宣言」のみ(初期化していない)
int y = 5; // これは「宣言」と「初期化」を同時に行っている
この2つの違いを整理すると次のようになります。
| 書き方 | 意味 | メモリの中身 |
|---|---|---|
int x; | 型intの変数xを用意する宣言 | 中身は不定(ゴミ値) |
int y = 5; | 変数yを用意し、5を設定(初期化) | 必ず5が入っている |
宣言だけでは「メモリの場所を確保しただけ」であり、その中がどんな値かはわかりません。
初期化を行うことで、その変数の「最初の状態」を自分で決めることができます。
代入との違い
もう1つ混同しやすいのが代入です。
代入は、すでに存在している変数の値を後から書き換える操作です。
int a = 10; // 宣言と同時に初期化
a = 20; // 代入(初期化とは別の操作)
この例では、変数aは次のように変化します。
int a = 10;の時点で、aには10が入る(初期化)a = 20;によって、aの値は20に上書きされる(代入)
初期化は「最初の1回の代入」と考えるとイメージしやすいですが、C言語の文法上は「宣言と同時に行う特別な代入」だと思ってください。
変数を初期化しないとどうなるか
ローカル変数は「ゴミ値」が入る
ローカル変数とは、関数の中で宣言される変数のことです。
C言語では、ローカル変数は自動では0になりません。
初期化をしないと、たまたまそのメモリに残っていた「ゴミ値」がそのまま入ることになります。
次のコードを見てみます。
#include <stdio.h>
int main(void) {
int x; // 初期化していないローカル変数
// xの値を表示してみる
printf("x = %d\n", x);
return 0;
}
実行結果の例です。
x = 32767
別の環境や別のタイミングで実行すると、たとえば次のような結果になることもあります。
x = -124592384
値が毎回違ううえに、どんな値になるかは完全に未定です。
これが「ゴミ値」です。
初心者のうちは、ローカル変数は必ず明示的に初期化すると覚えておくと安全です。
グローバル変数は自動的に0で初期化される
一方、関数の外で宣言されたグローバル変数は、C言語の仕様により自動的に0で初期化されるというルールがあります。
#include <stdio.h>
int g; // グローバル変数(自動的に0で初期化される)
int main(void) {
printf("g = %d\n", g); // 必ず0が表示される
return 0;
}
g = 0
同じように、static付きの変数(静的変数)も、自動的に0で初期化されます。
#include <stdio.h>
void func(void) {
static int s; // 自動的に0で初期化される
printf("s = %d\n", s);
s++;
}
int main(void) {
func(); // 1回目
func(); // 2回目
func(); // 3回目
return 0;
}
s = 0
s = 1
s = 2
このように、グローバル変数と静的変数は「暗黙の初期化(0)」が行われるため、ローカル変数と挙動が異なります。
ただし、「勝手に0になるから初期化しなくてよい」と考えるのは危険です。
後から読む人にとっても、int g = 0;と明示されている方が意図が伝わります。
配列・ポインタを初期化しない危険性
配列やポインタを初期化しないと、単なる「おかしな値」では済まず、プログラムが落ちる・メモリ破壊が起きるなど、より危険な状態になります。
配列を初期化しない場合
次のプログラムでは、配列の要素を初期化していません。
#include <stdio.h>
int main(void) {
int a[3]; // 要素は初期化されていない(ゴミ値)
printf("a[0] = %d\n", a[0]);
printf("a[1] = %d\n", a[1]);
printf("a[2] = %d\n", a[2]);
return 0;
}
a[0] = -608212992
a[1] = 32767
a[2] = 0
要素ごとに値がバラバラで、しかも毎回違う可能性があります。
配列は多くの場合ループ処理や合計計算などに使われるため、初期化忘れにより計算結果がおかしくなる典型パターンの1つです。
ポインタを初期化しない場合
さらに危険なのがポインタです。
初期化しないポインタには、どこを指しているかわからないアドレスが入ります。
#include <stdio.h>
int main(void) {
int *p; // 初期化していないポインタ(危険)
// pが指している先に値を書き込もうとする
*p = 10; // どこに書くかわからない
printf("*p = %d\n", *p);
return 0;
}
多くの場合、このようなコードは実行時にセグメンテーションフォルト(異常終了)を引き起こします。
運よく落ちない場合でも、他の領域のメモリを書き換えてしまい、説明不能なバグを生むことがあります。
ポインタは必ず有効なアドレスかNULLで初期化することが重要です。
if文や計算で未初期化変数を使うとどうなるか
未初期化の変数を条件式や計算に使うと、プログラムの挙動は一気に不安定になります。
if文で使った場合
#include <stdio.h>
int main(void) {
int flag; // 初期化していない
if (flag) {
printf("flagは真(0以外)と判定されました\n");
} else {
printf("flagは偽(0)と判定されました\n");
}
return 0;
}
実行するたびに、ifの中に入る場合と入らない場合が変わることがあります。
なぜならflagの中身が毎回違うからです。
プログラムを書いている自分は「falseのつもり」でも、たまたま0以外のゴミ値が入っていてtrue扱いになるようなことが普通に起こります。
計算で使った場合
#include <stdio.h>
int main(void) {
int sum; // 初期化していない
int value = 10;
sum = sum + value; // 未初期化のsumを使って計算
printf("sum = %d\n", sum);
return 0;
}
本来はsumを0から始めたいのに、sumにゴミ値が入っているため、結果が何になるのかまったく予測できません。
このタイプのバグは、特に合計値・カウント・平均値の計算で起こりやすく、初心者がもっとも苦しみやすい部分です。
実行のたびに結果が変わる不安定なバグ
未初期化変数の一番の厄介な点は、実行のたびに結果が変わることです。
これは、毎回メモリの状態が違うためです。
次の例を見てください。
#include <stdio.h>
int main(void) {
int i;
int sum;
for (i = 0; i < 5; i++) {
sum = sum + i; // sumを初期化していない
}
printf("sum = %d\n", sum);
return 0;
}
本来期待したい値は0 + 1 + 2 + 3 + 4 = 10ですが、sumが未初期化のため、あるときは123456789のような大きな値になったり、別の実行では負の値になったりします。
こうしたバグは
- デバッグ出力を入れると「たまたま」動いてしまう
- コンパイルオプションを変えると再発する
- 実行環境を変えると再現しない
といった特徴があり、「再現性が低くて原因がわかりにくい」ため、経験者でも苦労することがあります。
未初期化変数を避けることは、安定したプログラムを書くための最重要ポイントです。
C言語の変数を正しく初期化する書き方
宣言と同時に初期化する基本の書き方
もっとも素直で安全な方法は、宣言と同時に初期化することです。
#include <stdio.h>
int main(void) {
int count = 0; // 整数を0で初期化
double rate = 1.0; // 浮動小数点を1.0で初期化
char ch = 'A'; // 文字を'A'で初期化
printf("count = %d, rate = %f, ch = %c\n", count, rate, ch);
return 0;
}
count = 0, rate = 1.000000, ch = A
「変数を宣言するなら、必ず同時に初期化もする」という習慣をつけると、未初期化バグの多くを自然に防ぐことができます。
配列の初期化の書き方
配列の初期化にはいくつかパターンがあります。
用途に合わせて使い分けると便利です。
要素を並べて初期化する
#include <stdio.h>
int main(void) {
int a[5] = {1, 2, 3, 4, 5}; // 要素を順番に指定
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] = 3
a[3] = 4
a[4] = 5
すべて0で初期化する
すべて0にしたい場合、{0}と1つだけ書く方法がよく使われます。
#include <stdio.h>
int main(void) {
int a[5] = {0}; // すべての要素が0で初期化される
for (int i = 0; i < 5; i++) {
printf("a[%d] = %d\n", i, a[i]);
}
return 0;
}
a[0] = 0
a[1] = 0
a[2] = 0
a[3] = 0
a[4] = 0
配列を宣言するときは「{0}」でゼロクリアするという書き方を覚えておくと、未初期化バグを防ぎやすくなります。
ポインタの初期化
ポインタは、使う前に必ず有効なアドレスかNULLで初期化することが重要です。
NULLで初期化する
NULLは「どこも指していない」という特別な値です。
標準ヘッダstdio.hなどをインクルードすれば使えます。
#include <stdio.h>
int main(void) {
int *p = NULL; // どこも指していないことを明示
if (p == NULL) {
printf("pは有効なアドレスをまだ指していません\n");
}
return 0;
}
pは有効なアドレスをまだ指していません
変数のアドレスで初期化する
#include <stdio.h>
int main(void) {
int value = 10;
int *p = &value; // valueのアドレスで初期化
printf("value = %d\n", value);
printf("*p = %d\n", *p); // ポインタ経由でも同じ値
return 0;
}
value = 10
*p = 10
「ポインタ変数 = &対象変数」という形で、&演算子を使ってアドレスを取得し、初期化するのが基本です。
for文のカウンタ変数の初期化
for文は、カウンタ変数の宣言・初期化と相性がよい構文です。
#include <stdio.h>
int main(void) {
int sum = 0;
// iを0で初期化し、その後インクリメントしていく
for (int i = 0; i < 5; i++) {
sum = sum + i;
}
printf("sum = %d\n", sum);
return 0;
}
sum = 10
このようにfor文の中でカウンタ変数を宣言し、その場で初期化すると、未初期化のまま使ってしまう危険が減ります。
また、変数の有効範囲もfor文の中だけに限定されるため、意図しない再利用も防げます。
構造体・列挙型の初期化
構造体の初期化
構造体は、複数のメンバをまとめた型です。
初期化するときは、中かっこ{}の中にメンバの初期値を並べて書きます。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
// メンバの順番に初期値を並べる
struct Point p1 = {10, 20};
// 指定子(設計がC99以降の場合)を使うと、可読性が上がる
struct Point p2 = {.x = 30, .y = 40};
printf("p1: x = %d, y = %d\n", p1.x, p1.y);
printf("p2: x = %d, y = %d\n", p2.x, p2.y);
return 0;
}
p1: x = 10, y = 20
p2: x = 30, y = 40
構造体も{0}で全メンバを0に初期化できます。
struct Point p0 = {0}; // xもyも0になる
列挙型の初期化
列挙型(enum)は、整数に名前をつけるための型です。
初期化自体は基本的に整数と同じように行います。
#include <stdio.h>
enum Color {
RED, // 0
GREEN, // 1
BLUE // 2
};
int main(void) {
enum Color c = GREEN; // 列挙子で初期化
if (c == GREEN) {
printf("色はGREENです\n");
}
return 0;
}
色はGREENです
列挙型の変数は、必ず何かの列挙子で初期化しておくことで、意図しない値(範囲外の整数)が入るのを防ぎやすくなります。
未初期化変数のバグを防ぐコツ
コンパイラ警告を最大限に活用する
多くのコンパイラは、「未初期化のまま使われている可能性がある変数」を警告してくれます。
この警告を無視せず、必ず対処することが重要です。
たとえばGCCなら、次のようにオプションをつけます。
| オプション | 意味 |
|---|---|
-Wall | 代表的な警告をすべて有効にする |
-Wextra | 追加の警告も有効にする |
-Wuninitialized | 未初期化変数の利用に関する警告を出す |
「警告0件でコンパイルできること」を目標にする習慣をつけておくと、未初期化バグだけでなく、さまざまなミスを早期に発見できます。
変数は使う直前に宣言して初期化する
古いC言語では、関数の先頭でまとめて変数を宣言するスタイルが一般的でした。
しかし、初心者には「どの変数がどこで使われているか分かりづらく、初期化忘れも起こりやすい」という欠点があります。
現在のC言語(特にC99以降)では、変数は「必要になった場所」で宣言し、その場で初期化する書き方が推奨されます。
#include <stdio.h>
int main(void) {
int n = 5; // ここで初期化
for (int i = 0; i < n; i++) { // iもここで宣言・初期化
int value = i * 2; // valueも使う直前で宣言・初期化
printf("%d\n", value);
}
return 0;
}
こうすることで、
- 未初期化のまま使ってしまう可能性が減る
- 変数の有効範囲が限定され、意図しない再利用を防げる
- コードを読む人にとって「この変数はどこで使うものか」が分かりやすい
といったメリットがあります。
あり得ない初期値を入れて気づきやすくする
デバッグの観点から、「もしバグがあったときにすぐ気づける初期値」を設定しておくテクニックも有効です。
例えば、配列の添字や選択肢番号など、本来は0以上の値しか入らない変数の場合、あえて-1で初期化しておくと、間違ってそのまま使われたときに異常値として発見しやすくなります。
#include <stdio.h>
int main(void) {
int index = -1; // 本来0以上のはずなので、-1で初期化
// 何らかの処理でindexを書き換えるはずだが、バグで書き換わらなかったとする
if (index < 0) {
printf("エラー: indexが設定されていません(index = %d)\n", index);
}
return 0;
}
エラー: indexが設定されていません(index = -1)
「0で初期化」も安全ですが、あえて「あり得ない値」で初期化することで、バグが早く発覚するという利点があります。
静的解析ツールで未初期化をチェックする
コンパイラの警告だけでは見つけきれない未初期化変数の問題もあります。
そこで役立つのが静的解析ツールです。
静的解析ツールは、プログラムを実行せずにソースコードを解析し、次のような問題を検出してくれます。
- 未初期化の可能性がある変数の使用
- ポインタの不正な参照
- メモリリークの疑い など
代表的なツールとして、例えば次のようなものがあります。
| ツール名 | 特徴 |
|---|---|
| cppcheck | 無料で使えるC/C++静的解析ツール |
| clang-tidy | Clangコンパイラに付属する解析ツール |
| commercial製品 | Coverityなど、より高度な解析が可能なもの |
学習段階から、「コンパイル」+「静的解析」をセットにしておくと、未初期化バグに早く気づけるようになります。
初心者がやりがちな未初期化のパターンと対策
C言語初心者がよくハマる未初期化のパターンと、その対策をいくつか挙げます。
パターン1: 合計値・カウンタの初期化忘れ
#include <stdio.h>
int main(void) {
int i;
int sum; // 初期化していない
for (i = 0; i < 10; i++) {
sum = sum + i;
}
printf("sum = %d\n", sum);
return 0;
}
対策として、合計値やカウンタは必ず0で初期化します。
int sum = 0;
パターン2: 条件式に使うフラグ変数の初期化忘れ
#include <stdio.h>
int main(void) {
int found; // 初期化していない
// 何かを探す処理を書くつもりだが、実装を忘れたとする
if (found) { // foundの値は不定
printf("見つかりました\n");
} else {
printf("見つかりませんでした\n");
}
return 0;
}
対策として、フラグ変数は最初に0(偽)で初期化し、条件を満たしたときに1(真)を代入するようにします。
int found = 0; // 最初は「見つかっていない」とする
パターン3: 未初期化ポインタの使用
#include <stdio.h>
int main(void) {
int *p; // 初期化していない
*p = 10; // どこに書き込むかわからない
return 0;
}
対策として、ポインタは必ずNULLまたは有効なアドレスで初期化します。
int *p = NULL;
パターン4: 関数からの値の受け取り忘れ
#include <stdio.h>
int get_value(void) {
return 42;
}
int main(void) {
int value; // 初期化していない
get_value(); // 戻り値を受け取っていない
printf("value = %d\n", value); // valueは未初期化
return 0;
}
対策として、関数の戻り値は必ず変数に代入してから使うようにします。
また、その変数も宣言時に初期化しておくと安心です。
int value = 0;
value = get_value();
まとめ
変数の初期化は、C言語プログラミングの「安全運転」の基本です。
ローカル変数を初期化しないと、ゴミ値が入り、プログラムの挙動は予測不能になります。
グローバル変数や静的変数は自動で0になりますが、それに頼るよりも、自分の意図を明示するために積極的に初期化を書くことが大切です。
配列やポインタの未初期化は、単なるおかしな値にとどまらず、クラッシュやメモリ破壊といった深刻な問題を起こします。
特にポインタは、必ずNULLまたは有効なアドレスで初期化することを習慣にしてください。
実際のコーディングでは、
- 変数は宣言と同時に初期化する
- 配列は
{0}や初期値リストで確実に初期化する - カウンタやフラグは必ず0からスタートさせる
- ポインタは
NULLまたは&変数で初期化する - コンパイラ警告と静的解析ツールを活用し、警告を放置しない
といったルールを自分なりに決めて守ることで、未初期化変数による不安定なバグをほとんど防ぐことができます。
C言語はとても強力ですが、そのぶん「自分で責任を持ってメモリを管理する」ことが求められます。
変数の初期化は、その第一歩です。
今日から書くすべてのCプログラムで、「この変数はどの値からスタートさせるべきか」を意識しながらコードを書く練習をしてみてください。
