プログラムは正しく動いているのに原因不明の数値が混入する、そんなときは未初期化が疑われます。
本記事では、C++における「変数を宣言と同時に初期化する」ための方法を体系的に整理します。
コピー初期化、直接初期化、リスト初期化、ゼロ初期化の違いを押さえ、型ごとの安全な書き方と注意点を具体例で解説します。
初心者向け: C++の変数宣言と初期化の基本
宣言と初期化の違い
変数の宣言は、型と名前をコンパイラに伝える行為です。
対して初期化は、その変数に最初の値を与えることです。
C++では宣言と同時に初期化する書き方が複数あり、目的や安全性に応じて使い分けます。
とくにローカル変数の組み込み型は初期化しないと不定値になり、読み出しは未定義動作です。
#include <iostream>
int main() {
int a; // 宣言のみ(ローカル変数)。初期化されない(不定値)。読み出しはダメ
int b = 42; // 宣言と同時に初期化(コピー初期化)
int c(42); // 宣言と同時に初期化(直接初期化)
int d{42}; // 宣言と同時に初期化(リスト初期化)
int e = {42}; // 宣言と同時に初期化(コピー・リスト初期化)
std::cout << b << " " << c << " " << d << " " << e << std::endl;
// std::cout << a; // これは未定義動作。コメントアウトのままにすること
return 0;
}
42 42 42 42
ここでの核心は、不定値を決して読み出さないこと、そして宣言と同時に初期化する習慣をつけることです。
宣言時に同時初期化する理由
宣言と同時に初期化すると、コードが安全になり、意図が明確になります。
const変数は必ず初期化が必要ですし、オブジェクトは初期化で必要な資源を獲得できます。
さらに二度手間を避け性能面でも有利です(デフォルト構築してから代入するより、望む値で直接構築するほうが無駄がありません)。
#include <string>
// 悪い例: デフォルト構築(空文字列)してから代入
std::string s1;
s1 = "hello";
// 良い例: 望む値で直接構築
std::string s2 = "hello"; // または std::string s2{"hello"};
最初から正しい状態で作るのがC++の基本発想です。
未初期化の危険と挙動
ローカルな組み込み型(例: int
, double
, bool
)は、初期化しないと値が不定です。
その不定値を読み出すと未定義動作になり、環境や最適化によって振る舞いが変わります。
静的記憶域期間(グローバルやstatic
)の変数はゼロ初期化されますが、ローカル変数は必ず自分で初期化しましょう。
{}での値初期化を癖にすると安全です。
C++の初期化方法いろいろ
C++には大きく分けて4つの初期化スタイルがあります。
それぞれの特徴を理解すると、意図に合った最適な書き方が選べます。
コピー初期化(=)
=
を使うおなじみの形です。
右辺の式から必要な暗黙変換が行われ、左辺の型の値で初期化されます。
#include <string>
int i = 10; // 整数のコピー初期化
double d = 3; // int -> double へ暗黙変換
std::string s = "text"; // 文字列リテラルから std::string へ
コピー初期化は使いやすい一方、意図しない変換を通してしまう場合があります。
縮小変換の検出には次の「リスト初期化」が有効です。
直接初期化(())
丸括弧でコンストラクタや変換を明示します。
クラス型ではコンストラクタ呼び出しの形になり、「その型の作り方」を選べます。
#include <string>
int x(42); // int の直接初期化
std::string a("hello"); // const char* からの構築
std::string b(5, 'x'); // 'x' を5個繰り返した文字列を構築
注意として、()
は関数宣言と解釈される「最も厄介な構文問題(most vexing parse)」を引き起こす場合があります。
初心者はシンプルに{}
を基本にすると安全です。
リスト初期化({})
波括弧による統一的な初期化です。
縮小変換を禁止してくれるのが最大の利点です。
int a{10}; // OK
double d{3.14}; // OK
// int b{3.14}; // エラー: 3.14 を int に縮小するのは禁止(安全)
= {}も「コピー・リスト初期化」と呼ばれますが、縮小変換の禁止は同じです。
配列や集成体(後述)の要素を並べるのにも向いています。
0や{}でのゼロ初期化
{}
を値なしで使うと値初期化になり、組み込み型は0、ポインタはヌルポインタ、クラス型はデフォルトコンストラクタで初期化されます。
= 0
は数値型のゼロ初期化としては有効ですが、ポインタにはnullptrを使うのがC++では安全です。
int zi{}; // 0
double zd{}; // 0.0
bool zb{}; // false
char zc{}; // 'int zi{}; // 0
double zd{}; // 0.0
bool zb{}; // false
char zc{}; // '\0'
int* zp{}; // nullptr と同等(ヌルポインタ)
'
int* zp{}; // nullptr と同等(ヌルポインタ)
次の表は4つの初期化を簡潔に比較したものです。
形式 | 記法 | 主な用途 | 縮小変換の扱い | 備考 |
---|---|---|---|---|
コピー初期化 | = | 手軽な初期化 | 許可される(注意) | 暗黙変換が通りやすい |
直接初期化 | ( ) | コンストラクタ選択 | 許可される(注意) | クラス型で多用 |
リスト初期化 | { } | 安全な初期化 | 禁止してエラー | 要素列の初期化に便利 |
値初期化 | {} | ゼロ/デフォルト初期化 | 該当なし | 組み込み型は0、ポインタはヌル |
迷ったら{}
を基本にすると覚えておくと、縮小変換による事故を防げます。
型別の初期化例
ここからは代表的な型ごとの安全な初期化方法を、短い例とともに示します。
未初期化を避け、意図が伝わる書き方を選びます。
整数(int)と浮動小数点(double)
整数や浮動小数点では、値の範囲に注意しながらリスト初期化を使うと安全です。
#include <iostream>
int main() {
int i1{42}; // 安全
double d1{3.14}; // 安全
int i2 = 3.14; // 3 に丸められる(許可されるが注意)
// int i3{3.14}; // エラー: 縮小変換を禁止(安全)
std::cout << i1 << " " << d1 << " " << i2 << '\n';
}
42 3.14 3
リスト初期化で縮小変換をコンパイルエラーにするのが安全運用の基本です。
真偽値(bool)
bool
はtrue
/false
で初期化するのが最も明確です。
整数からの変換は、{}
だと不正な値を弾けます。
bool b1{true}; // OK
bool b2{}; // false
bool b3 = 1; // true (許可されるが明確さに欠ける)
// bool b4{2}; // エラー: 2 は bool に正確に表せないため禁止
意味が明確なリテラルで初期化すると読みやすく、誤変換も防げます。
文字(char)
文字リテラルで初期化するのが可読性に優れます。
数値コードで初期化する場合は範囲に注意します。
char c1{'A'}; // OK
char c2{}; // 'char c1{'A'}; // OK
char c2{}; // '\0'
// char c3{300}; // 多くの環境でエラー: char の範囲外(縮小変換)
'
// char c3{300}; // 多くの環境でエラー: char の範囲外(縮小変換)
char の符号付き/なしは実装依存なので、数値での初期化は注意が必要です。
ポインタはnullptrで初期化
ポインタは必ずnullptrで初期化します。
0
やNULL
よりnullptr
が明確で安全です。
int* p1{nullptr}; // 推奨
int* p2{}; // ヌルポインタ(これも推奨)
int* p3 = nullptr; // これもOK
int* p4 = 0; // 許可されるが非推奨
ヌルポインタの意図が一目でわかるため、nullptr
を使いましょう。
文字列(std::string)
std::string
は安全な文字列型です。
空文字や既定の内容で初期化できます。
#include <iostream>
#include <string>
int main() {
std::string s1{}; // 空文字列
std::string s2{"hello"}; // リスト初期化
std::string s3 = "world"; // コピー初期化
std::string s4(5, 'x'); // "xxxxx"
std::cout << s1.size() << " " << s2 << " " << s3 << " " << s4 << '\n';
}
0 hello world xxxxx
必要な形で直接構築することで、余計な代入を避けられます。
配列の初期化(組み込み配列)
組み込み配列は{}
で要素を並べるか、空の{}
でゼロ初期化します。
部分的に与えた場合、残りはゼロになります。
#include <iostream>
int main() {
int a[3]{1, 2, 3}; // 各要素を指定
int b[5]{}; // すべて 0
int c[5]{1, 2}; // 残りは 0
char str[]{"abc"}; // 'a','b','c','#include <iostream>
int main() {
int a[3]{1, 2, 3}; // 各要素を指定
int b[5]{}; // すべて 0
int c[5]{1, 2}; // 残りは 0
char str[]{"abc"}; // 'a','b','c','\0' の4要素
std::cout << a[0] << a[1] << a[2] << " "
<< b[0] << b[4] << " "
<< c[0] << c[2] << " "
<< str << '\n';
}
' の4要素
std::cout << a[0] << a[1] << a[2] << " "
<< b[0] << b[4] << " "
<< c[0] << c[2] << " "
<< str << '\n';
}
123 00 10 abc
配列は{}
で安全に全体を初期化するのが基本です。
構造体/クラスの簡単な初期化
集成体(メンバが公開で、ユーザ定義のコンストラクタなどがない単純な型)は{}
でメンバ順に初期化できます。
クラスはコンストラクタに従います。
#include <iostream>
struct Point {
int x;
int y;
};
struct Size {
int w{};
int h{};
};
int main() {
Point p1{1, 2}; // 集成体のリスト初期化
Point p2{}; // x=0, y=0
Size s1{}; // 既定メンバ初期化子により w=0, h=0
std::cout << "(" << p1.x << "," << p1.y << ") "
<< "(" << p2.x << "," << p2.y << ") "
<< s1.w << "x" << s1.h << '\n';
}
(1,2) (0,0) 0x0
集成体には{}
が最適で、読みやすく安全です。
安全な初期化のコツ
初期化の落とし穴は、いつも同じパターンで避けられます。
以下の要点を意識すると事故が激減します。
{}を基本にして縮小変換を防ぐ
{}
は危険な縮小変換をコンパイル時に止めます。
異常値が紛れ込む余地をなくせます。
// int a{3.14}; // エラー: 縮小変換
int b = 3.14; // 許可され 3 に丸められる(静かに情報が失われる)
失敗は早いほど良い(コンパイル時が最良)という原則に合致します。
constは必ず宣言と同時に初期化
const
変数は後から代入できません。
宣言と同時に初期化する必要があります。
不変条件を壊さないためにも徹底しましょう。
const int max_count{100};
// max_count = 50; // エラー: 再代入不可
代入より初期化を優先する
同じ結果に見えても、初期化のほうが無駄がありません。
とくにクラス型では、望む状態で直接構築しましょう。
#include <string>
// 非推奨(二段階): デフォルト構築 → 代入
std::string s1;
s1 = "data";
// 推奨(一段階): 直接初期化またはコピー初期化
std::string s2{"data"};
必要なスコープで宣言して初期化
変数の有効範囲を最小化すると、未初期化や誤用のリスクが減少し、可読性も上がります。
#include <iostream>
int main() {
// 使う直前のスコープで初期化
if (int value{42}; value > 0) { // C++17 の if 初期化
std::cout << value << '\n';
} // ここで value は寿命を終える
}
42
スコープを絞ることで、変数の寿命と責務が明確になります。
まとめ
宣言と同時に初期化することは、C++で安全かつ明快なコードを書くための最重要テクニックです。
コピー初期化(=
)、直接初期化(()
)、リスト初期化({}
)、そして空の{}
による値初期化の違いを理解し、基本は{}
を使って縮小変換を防ぐという指針を持ちましょう。
constは必ず同時初期化し、代入より初期化を優先、必要なスコープで宣言する、という3点を徹底すれば未初期化や型変換のバグを大幅に回避できます。
今日から、「宣言したら即、正しい値で初期化」を習慣にしてください。