C++ Learning

第9回 初期化と nullptr

C で int x; と書いた変数は中身がゴミ値。C++ にも同じ仕様は残っていますが、{} を使う統一初期化という安全で統一的な書き方が C++11 で入りました。また、C 時代の NULL(実体は 0)が引き起こしていた混乱は、nullptr という専用キーワードで解決されます。本回は「変数を作るとき、何が初期値か」を明確に理解します。

このページで押さえること
✅ 最低限ここだけ覚える
  • ローカル変数の int x; はゴミ値(危険)
  • 安全に作るには int x{}; または int x = 0;
  • ポインタの「空」は nullptr(NULL は使わない)
  • {} は「何にでも使える統一初期化」と覚える
⭐ 余裕があれば読む
  • 初期化の 4 種類(default / value / direct / copy)
  • 「もっとも厄介な構文解析」(Most Vexing Parse)
  • narrowing conversion(狭める変換)を {} が禁止
  • nullptr の型(std::nullptr_t

1. まず触ってみる ― ゴミ値 vs 初期化

C や C++ の変数は、明示的に値を入れないと「ゴミ値」(メモリにたまたま残っていた値)を持ちます。「0 から始まる」と思って使うとバグの温床に。下のデモで体感してください。

▶ 変数の初期値を比べる

まずは 3 行だけ

今日覚える最小の書き方はこれ:

safe_init.cppC++ 最小例
int x{}; // 0 で初期化(int は 0、double は 0.0、bool は false ...) int y{42}; // 42 で初期化 int* p = nullptr; // ポインタの「空」は nullptr
覚える 2 つ:
  • {} を付けておけば安全(ゴミ値にならない)
  • 空のポインタは nullptrNULL0 は使わない)

よくある素朴な疑問

Q. int x = 0; じゃダメなの?
→ ダメではありません。ただし int x{}; のほうがどんな型でも同じ書き方で済みます(std::string s{}; も OK)。型を変えたときに初期値を書き直さなくていいので保守性が上がる。

Q. グローバル変数も未初期化になる?
→ なりません。グローバル変数と static 変数は自動で 0 初期化されます(C と同じ)。問題になるのは関数ローカルの変数だけ。

Q. nullptrNULL と何が違うの?
NULL は C 由来で実体が整数 0。そのせいで関数オーバーロードで誤ってマッチすることがありました。nullptr は専用の型 std::nullptr_t を持つポインタ専用の値。詳細は §5 で。

2. C の「未初期化」問題を思い出す

C の落とし穴として有名な、こんなコード:

trap.cC の罠
#include <stdio.h> int main(void) { int sum; // 初期化してない! for (int i = 1; i <= 10; i++) sum += i; printf("%d\n", sum); // ??? }
コンパイルは通る。実行結果は実行ごとに違うことも
correct.cppC++ で安全に
#include <iostream> int main() { int sum{}; // ← 0 で初期化 for (int i = 1; i <= 10; i++) sum += i; std::cout << sum << "\n"; // 55 }
{} を付けるだけで安全

どうしてゴミ値になる?

関数ローカル変数はスタックに取られます。スタック上のメモリは前に使った関数の痕跡が残っていて、そのまま再利用されます。明示的な初期化がないと、そのゴミがそのまま変数の値になります。

📦 関数ローカルのメモリ(スタックフレーム)
int sum;
739281
← 前の関数が使った後の残骸(ゴミ値)
int sum{};
0
{} で 0 に上書き(安全)
int sum{42};
42
← 値を指定して初期化
なぜ C/C++ は自動で 0 にしないのか: 性能のため。大量に変数を作るコード(関数呼び出しを秒間 1000 万回、など)で、毎回 0 埋めするとオーバーヘッドが無視できません。「必要な人だけ初期化する」設計思想です。ただし初学者はほぼ常に初期化すべきなので、{} を癖にしましょう。

3. 4 つの初期化の書き方

C++ には歴史的経緯で初期化の書き方が 4 種類あります。結果はほぼ同じですが、微妙に違いがあるので整理しておきます。

① 代入風(= 値)― C でもおなじみ

コピー初期化C 由来
int x = 42; std::string s = "hello";

C と同じ書き方。読みやすい。クラス型のコピーコンストラクタが呼ばれます(気にしなくて OK、同じ結果になります)。

② 丸カッコ(直接初期化)

直接初期化C++
int x(42); std::vector<int> v(5, 0); // 引数を複数渡せる(①では書けない)

コンストラクタを直接呼ぶ形。引数が複数ある場合(vector の (5, 0) など)はこの書き方が必要です。

③ 中カッコ(統一初期化・リスト初期化)― C++11 ★

統一初期化C++11 推奨
int x{42}; int y{}; // 空 → デフォルト値 (0) std::vector<int> v{1, 2, 3}; // 中身 {1,2,3} std::string s{"hello"};

本サイトの推奨する書き方。どんな型でも同じ {} で書けるので、コードの見た目が統一されます。しかも危険な型変換を禁止してくれる(§4 で詳しく)。

④ 空の {} ― 「デフォルト値で初期化」

値初期化 (value init)0 / 空
int x{}; // 0 double y{}; // 0.0 bool z{}; // false std::string s{}; // "" (空文字列) std::vector<int> v{}; // 空の vector int* p{}; // nullptr

特に覚えておいてほしい形。「型ごとのデフォルト値」で初期化されます。ゴミ値になる心配がなく、どんな型でも書き方が変わらないのが利点。

どれを使えばいい? 迷ったら③と④の {} にしましょう。C 出身者の癖で ① の = 値 を書くのも自然です(読みやすい)。② の丸カッコは、関数っぽく見えて初学者が混乱するので、本当に必要な複数引数コンストラクタでだけ使う、くらいが安全。

4. 統一初期化 {} の強み

なぜ {} が推奨なのか、2 つの具体的な強みを挙げます。

強み 1: 狭める変換(narrowing)を禁止する

危険な型変換(doubleint で小数部を切り捨てるなど)をコンパイル時に弾いてくれます。

丸カッコ通る(危険)
int x(3.14); // コンパイルは通る // x は 3(小数部が静かに切り捨て)
中カッココンパイルエラー
int x{3.14}; // エラー: narrowing conversion // 意図しない情報欠落を止めてくれる
明示変換は OK: 本当に切り捨てたいなら int x{static_cast<int>(3.14)}; と明示的にキャストを書きます。意図をコードに残すのが C++ の流儀。

強み 2: 「もっとも厄介な構文解析」を回避

C++ には Most Vexing Parse(MVP)と呼ばれる歴史的な地雷があります。

MVP ― 意図と違う解釈落とし穴
// 引数なしコンストラクタで Widget を作りたい! Widget w(); // ← 実は「引数なしで Widget を返す関数 w の宣言」 // コンパイラが関数宣言と誤解する
{} なら意図通り推奨
Widget w{}; // ← 確実に「引数なしコンストラクタで w を作る」

このあたりの話は、クラスを扱う STEP 4(第 18 回)でもう一度出てきます。今は「() より {} の方が安全」とだけ覚えておけば OK。

ここまでで変数の初期化は OK
残り 2 節は nullptr とよくある罠。ポインタをあまり使わない段階ならサラッと読むだけで構いません。

5. nullptr ― NULL との違い

C 時代、「ポインタが何も指していない」ことは NULL(実体は整数の 0)で表していました。C++ ではこれをnullptr という専用キーワードに置き換えます。

見た目の違い

C 時代の書き方非推奨
int* p = NULL; // NULL は #define NULL 0 で定義された整数 if (p == NULL) { ... }
モダン C++C++11~ 推奨
int* p = nullptr; // ポインタ専用の値 if (p == nullptr) { ... } if (!p) { ... } // これも OK(ブールに変換される)

なぜ NULL はダメなのか

関数オーバーロードの場面で意図しない選ばれ方をするからです。

NULL の落とし穴誤マッチ
void foo(int n); void foo(int* p); foo(NULL); // NULL は整数 0 なので foo(int) が呼ばれる! // ポインタ版を呼びたかったのに…
nullptr なら意図通りC++11~
void foo(int n); void foo(int* p); foo(nullptr); // nullptr は std::nullptr_t 型 // ポインタ版 foo(int*) が確実に呼ばれる
C++ での使い分け:
  • ポインタの空nullptr
  • 整数の 00(そのまま)
  • NULL0 をポインタ用途に使う → 非推奨

nullptr の型

nullptr の型は std::nullptr_t という特殊な型です。あらゆるポインタ型に暗黙変換できるけれど、整数には変換できません。この仕組みで foo(int) が誤ってマッチすることが防がれています。実装の詳細は気にしなくて OK ですが、「nullptr は特別な型を持つ安全な NULL」と覚えてください。

6. よくある罠

罠 1: std::vector<int> v(3)v{3} は違う

§2 の vector でも触れましたが、ここで再確認:

丸カッコサイズ指定
std::vector<int> v(3); // 要素数 3、中身は {0, 0, 0}
中カッコ中身を並べる
std::vector<int> v{3}; // 要素数 1、中身は {3}

「vector だけの特別な挙動」ではなく、初期化子リストを取るコンストラクタを持つ型の普遍的な動きです(中カッコの中身が「配列風リスト」と解釈される)。

罠 2: クラスのメンバも必ず初期化

NGゴミ値
struct Point { int x; int y; }; Point p; std::cout << p.x; // ゴミ値
OKメンバ初期化子
struct Point { int x{}; // デフォルト 0 int y{}; }; Point p; std::cout << p.x; // 0

C++11 から、メンバ変数に宣言時に初期値を書ける(default member initializer)ようになりました。クラスのメンバも {} の癖を付けておくと安全です。詳しくは STEP 4 のクラス編で。

罠 3: const は必ず初期化必須

const の例必ず初期化
const int MAX; // ← コンパイルエラー const int MAX = 100; // OK const int MAX{100}; // OK

const は後から書き換えられないので、宣言時に必ず初期値が必要です。忘れるとコンパイラが教えてくれる、というのが C++ の良い点。

広告スペース

確認クイズ

初期化と nullptr の理解度を 4 問で確認しましょう。

Q1. 次のコードで出力される値として正しいものは?
int main(){ int x; std::cout << x; }

必ず 0
必ず -1
コンパイルエラー
不定値(未定義動作)
関数ローカルの組み込み型は自動初期化されません。スタックに残っていたゴミ値をそのまま読み出します。コンパイラによっては警告を出しますが、言語仕様上はエラーにはなりません。int x{}; と書けば安全に 0 になります。

Q2. 次のうち、コンパイルエラーになるのはどれ?

int x = 3.14;
int x(3.14);
int x{3.14};
すべてエラーにならない
{} 初期化は狭める変換(narrowing)を禁止します。double→int で小数部を切り捨てるのは情報欠落なのでエラー。①と②は通ってしまう(静かに 3 になる)点が {} より危険な理由です。

Q3. int* 型のポインタが何も指していないことを表すには?

NULL
nullptr
0
どれでも同じ
C++11 以降は nullptr を使います。NULL0 も動きますが、整数扱いされるためオーバーロード時に誤マッチの可能性があり非推奨。nullptrstd::nullptr_t という専用の型を持つので、ポインタ用途で確実に選ばれます。

Q4. std::vector<int> v{5}; を実行したあとの v の中身は?

要素数 5、すべて 0
要素数 0
要素数 1、中身は 5
コンパイルエラー
{}「中身を並べる」解釈が優先されます。{5} は「要素 5 を 1 つだけ持つ vector」。「要素数 5(サイズ指定)」にしたいなら丸カッコで v(5) を書きます。この差は vector あるあるの落とし穴。
この記事をシェア