オブジェクトの初期化
変数などは初期化した上で使用しますが、その初期化には様々な方法があります。
ここでは初期化構文について説明します。
なお、初期化のルールは非常に複雑で多岐に渡るため、このページの説明は全てをカバーしているわけではありません。
デフォルト初期化
デフォルト初期化は初期化子(初期化に使用する値)が指定されない場合に行われる初期化です。
//非クラスのグローバル変数はゼロ初期化
int i;
class A
{
//メンバイニシャライザやデフォルトメンバ初期化子で
//初期化されないメンバ変数はデフォルト初期化
int n;
public:
A() {};
};
int main()
{
//ローカル変数の基本型は不定値初期化
int a;
int* b;
new int;
//クラス型
//デフォルトコンストラクタが呼ばれる
//デフォルトコンストラクタを実行できない場合はコンパイルエラー
A c;
new A;
//配列の要素はデフォルト初期化される
//要素が基本型なら値は不定
int d[3];
//要素がクラス型ならそれぞれデフォルトコンストラクタが呼ばれる
A e[3];
//非クラスのstatic変数はゼロ初期化
static int f;
//参照型はデフォルト初期化できない
//int& g;
}
基本型(int型など)は不定値で初期化されます。
不定値を使用することは未定義動作なので、使用する前に何かしらの値を代入する必要があります。
ただし静的変数、グローバル変数は0で初期化されます。
(正確には、グローバル変数および静的変数はデフォルト初期化の前にゼロ初期化が行われ、デフォルト初期化では何もしません)
クラス型はデフォルトコンストラクタが実行されます。
デフォルトコンストラクタが実行できない場合はコンパイルエラーとなります。
(引数なしコンストラクタがない、privateで定義されている、deleteされている、など)
呼ぶべきコンストラクタが呼べない場合にエラーになるのはどの初期化方法でも共通です。
配列型は、その要素が基本型の場合は各要素は不定値で初期化され、クラス型の場合は各要素それぞれがデフォルトコンストラクタを実行します。
そのため、デフォルトコンストラクタを持たないクラスを配列にし、初期化子を与えなければコンパイルエラーになります。
値初期化
値初期化は、空の初期化子を与えたときに行われる初期化です。
class A
{
int n;
public:
//空のメンバイニシャライザは値初期化
A() : n() {}
A(int) : n{} {} //「{}」を使えるのはC++11以降
};
int main()
{
//値初期化
//基本型ではゼロ初期化
int();
int{}; //C++11
new int();
new int{}; //C++11
int a{}; //C++11
//これは変数宣言ではなく
//関数のプロトタイプ宣言とみなされるので注意
int b();
//配列は各要素を値初期化
int c[3]{};
//参照型は値初期化できない
//int& d{};
}
基本型はゼロで初期化されます。
なお、波括弧による記法はC++11から可能です。
(後述)
配列型は各要素が値初期化されます。
クラス型はデフォルト初期化されます。
ただし、暗黙的なデフォルトコンストラクタを持つ場合(自分でコンストラクタを何も定義しない場合)は先にゼロ初期化され、その後に各メンバがそれぞれの方法で初期化されます。
これにより初期化方法が提供されていないメンバであってもゼロで初期化されます。
ただしメンバがクラス型で、そのコンストラクタが呼べない場合はエラーになります。
class A
{
public:
int n;
//コンストラクタでメンバ変数nを初期化しないので
//通常であればnは不定値
A() {};
};
class B
{
public:
A a;
//コンストラクタを定義しないので
//暗黙的なコンストラクタを持つクラス
};
int main()
{
//一時オブジェクトA()は値初期化される
//ユーザー定義のデフォルトコンストラクタが呼ばれる
std::cout << A().n << std::endl; //不定値
//一時オブジェクトB()は値初期化される
//暗黙的なデフォルトコンストラクタはゼロ初期化を行う
//メンバaはゼロ初期化された後にデフォルトコンストラクタが呼ばれる
std::cout << B().a.n << std::endl; //0
}
なおこれはC++11からの動作で、それ以前はゼロ初期化を行いません。
一様初期化
C++03までは、変数やクラス型などのコンストラクタの呼び出しは丸括弧()
を使用していましたが、C++11からは波括弧{}
を使用することも可能です。
これにより、配列や構造体などと共通の記法で初期化ができます。
これを一様初期化(uniform initialization)といいます。
丸括弧による初期化構文は関数の宣言と形式が同じになることがあり、この場合は関数の宣言とみなされます。
それが意図通りでない場合は波括弧による初期化構文を使用します。
C++11より前では波括弧が使えませんが、丸括弧を二重にすることで対処できます。
class C
{
public:
int n = 1;
//int型への変換関数
operator int() {
return n;
}
};
int main()
{
//これは戻り値がint型の関数aの宣言
int a(C());
//これは変数bの直接初期化
int b(C{});
//C++03まで
//変数cの直接初期化
int c((C()));
}
波括弧による初期化は、初期化子を渡す型が明確である場合はその型のコンストラクタを呼び出してオブジェクトを構築することができます。
struct S
{
S(int a, std::string b) {}
};
S func(S s)
{
//戻り値の型コンストラクタを呼び出し可能
return { 2, "def" };
}
int main()
{
//引数の型コンストラクタを呼び出し可能
func({ 1, "abc" });
}
直接初期化
直接初期化は、引数付きコンストラクタを明示的に呼び出す初期化です。
class C
{
int n;
public:
//メンバイニシャライザは直接初期化
C(int a) : n(a) {}
};
int main()
{
//直接初期化
int a(1);
int b{ 1 }; //C++11
int(1);
int{ 1 }; //C++11
new int(1);
static_cast<int>(1);
bool c(nullptr);
C c(1);
C d{ 1 }; //C++11
//ラムダ式のコピーキャプチャも直接初期化
[a]() { return a; };
}
基本型では指定の値で初期化します。
bool型に対してnullptr
を指定した場合はfalse
になります。
クラス型の場合は引数に合致するコンストラクタが呼び出されます。
(明示的/暗黙的に関わらず)
コピー初期化
コピー初期化は、初期化に別のオブジェクトを使用する初期化です。
class C
{
int n;
public:
C(const C& c) : n(c.n) {}; //コピーコンストラクタ
C(int n) : n(n) {}; //変換コンストラクタ
};
int func(int n)
{
//引数とreturn文はコピー初期化
//(値渡しの場合)
return n;
}
int main()
{
//コピー初期化
int a = 1;
int b = func(1);
int c[3] = { 1, 2, 3 };
C d = 1;
C e = d;
//throwおよびcatchはコピー初期化(値渡しの場合)
try {
throw 1;
}
catch (int f) {}
}
基本型では指定の値で初期化します。
クラス型の場合は暗黙的に呼び出し可能な(explicit
ではない)変換の処理を呼び出します。
これはコピーコンストラクタ、ムーブコンストラクタ、変換コンストラクタ、ユーザ定義変換が該当します。
基本的に代入演算子(=
)を使用したときにコピー初期化が行われますが、関数の引数および戻り値なども暗黙的にこれらが呼び出されるのでコピー初期化に当たります。
(ただし参照渡しを除く)
「コピー初期化」という名称の通り、初期化子をコピーしたものを値として保存しますが、クラス型に対して同じ型の一時オブジェクト(その場限りで破棄されるオブジェクト)を初期化子に使用すると、コピーやムーブの処理を発生させることなくその一時オブジェクト自体が保存される場合があります。
これは高速化のための処理です。
リスト初期化
リスト初期化は、波括弧を使用した初期化子リストから初期化します。
class C
{
public:
int a{ 1 }; //直接リスト初期化
int b = { 2 }; //コピーリスト初期化
C(int x) : a(x) {}
//メンバの直接リスト初期化
C(int x, int y) : a{ x }, b{ y } {}
//暗黙的に呼び出せないコンストラクタ
explicit C(int, int, int) {}
C& operator=(const C& r)
{
a = r.a;
b = r.b;
return *this;
}
};
C func(C x)
{
//戻り値のコピーリスト初期化
return { x.a, x.b };
}
int main()
{
//直接リスト初期化
C a{ 1 };
C b{ 1, 2 };
C c{ 1, 2, 3 }; //explicitでも実行可能
C{ 1 };
new C{ 1, 2 };
//コピーリスト初期化
C d = { 1 };
C e = { 1, 2 };
//C f = { 1, 2, 3 }; //explicitは呼べない
func({ 1, 2 }); //引数のコピーリスト初期化
//初期化子リストから目的のデータ型への変換時に
//生成される一時オブジェクトからのコピーリスト初期化
//変数dは初期化ではなくただの代入
d = { 3, 4 };
d = C({ 1, 2 });
//d = { 3, 4, 5 }; //explicitは呼べない
//文字列配列のリスト初期化
char g[]{ "abc" };
char h[] = { "abc" };
}
リスト初期化は値初期化、直接初期化、コピー初期化の、波括弧を使用した記法です。
基本型は、初期化子リストが空の場合は値初期化、要素がひとつの場合は直接初期化またはコピー初期化になります。
要素数がふたつ以上の場合はエラーになります。
クラス型は、集成体である場合は集成体初期化が行われます。
集成体でない場合は引数がリストに合致するコンストラクタの呼び出しになります。
ただしコピーリスト初期化は暗黙的(explicit
)なコンストラクタは実行できません。
これはコピー初期化における制限です。
集成体でない自作クラスに対してリスト初期化をするにはstd::initializer_list型を引数に取るコンストラクタを定義します。
なお、リスト初期化は縮小変換は許可されません。
縮小変換
情報が失われるおそれのあるデータ型の変換を縮小変換といいます。
(実際に情報が失われるか否かは問わない)
以下は縮小変換の例です。
- 小数型から整数型への変換
- 整数型から小数型への変換
ただし変換元が定数で、変換先のデータ型で表現できる範囲の値である場合は除く long double
型からdouble
型またはfloat
型への変換、およびdouble
型からfloat
型への変換
ただし変換元が定数でオーバーフローが発生しない場合は除く- データサイズが小さくなる整数型の変換
ただし変換元が定数で、変換先のデータ型で表現できる範囲の値である場合は除く - ポインタから
bool
への変換(C++20以降)
集成体初期化
集成体とは配列、およびC言語の構造体のような単純なデータ構造のオブジェクトです。
以下に簡単な定義を示します。
- ユーザー定義のコンストラクタを持たない
private
、protected
な非静的メンバを持たない
(静的メンバの有無は問わない)- 基底クラスを持たない
- 仮想メンバ関数を持たない
(通常のメンバ関数の有無は問わない)
集成体はC++のバージョンにより差異があるので細かい定義は後述します。
こういったデータ集合はまとめて初期化が可能です。
//集成体クラス
class C
{
public:
int a;
double b;
std::string s;
};
int main()
{
C a = { 1, 2.3, "abc" };
C b{ 4, 5.6, "def" }; //C++11
int c[] = { 1, 2, 3 };
int d[]{ 1, 2, 3 }; //C++11
//以下は全てC++20以降
C e = { .a = 1, .b = 2.3, .s = "abc" };
C f{ .a = 4, .b = 5.6, .s = "def" };
C g(1, 2.3, "abc");
int h[](1, 2, 3);
}
集成体はコンストラクタを持ちませんが、C言語の構造体のように波括弧を使用してメンバを初期化することができます。
初期化子はクラスや構造体の定義で宣言された順番通りに指定する必要があります。
(静的メンバ、無名ビットフィールドが含まれる場合は無視されます)
波括弧による初期化子リストはリスト初期化の一種なので、縮小変換は許可されません。
C++20以降で可能な丸括弧による初期化では縮小変換が可能です。
集成体
集成体は配列のほか、以下の定義を満たすクラス、構造体、共用体です。
private
、protected
な非静的メンバを持たない
ただしC++17以降は直接持っていない(継承である)場合は許可される- C++11より以前では、ユーザー宣言のコンストラクタを持たない
- C++11~17では、ユーザー提供のコンストラクタ、継承されたコンストラクタ、
explicit
コンストラクタを持たない
(default
やdelete
されたコンストラクタは許容される) - C++20以降では、ユーザー宣言のコンストラクタ、継承されたコンストラクタを持たない
- 基底クラスを持たない
ただしC++17以降は、virtual
、private
、protected
な基底クラスでなければ許可される - 仮想メンバ関数を持たない
- C++11のみ、デフォルトメンバ初期化子を持たない
指示付き初期化子
C++20以降では、ドットに続いてメンバ名を指定して初期化子を与えることができます。
これを指示子付き初期化子といいます。
class C
{
public:
int a;
double b;
std::string s;
};
int main()
{
C a{ .a = 1, .b{2}, .s = "abc" };
C b{ .s = "abc" };
}
記述する順序はクラス内で定義されている順である必要がありますが、必要のないメンバの初期化は省略することができます。
(C言語では順序は自由だがC++ではNG)
初期化子が与えられなかったメンバは以下のルールに従います。
初期化子の省略
メンバに対して初期化子の数が少ない場合、あるいは指示付き初期化子で初期化子を指定しなかった場合、残りのメンバは以下のルールによって初期化されます。
- C++11より前では、メンバは値初期化されます。
- C++11以降では、メンバが集成体である場合は集成体初期化、そうでない場合は値初期化されます。
- 上記に加えてC++14以降では、デフォルトメンバ初期化子がある場合はそれにより初期化されます。
- いずれの場合でも、参照型であるメンバに初期化子を与えなかった場合はコンパイルエラーです。
初期化子のネスト
波括弧の中にさらに波括弧を記述することで、該当するメンバに対してリスト初期化をすることができます。
(ネスト=入れ子のこと)
配列の場合は多次元配列の各次元に対するリスト初期化となります。
ネストの波括弧を省略すると、該当するメンバが必要とする分だけ初期化子が使用され、後続のメンバには残りの初期化子が使用されます。
該当するメンバが必要とする初期化子の数が明確でない場合(任意の数を指定可能な場合)はコンパイルエラーになります。
class X
{
public:
int a;
int b;
};
class Y
{
public:
int a;
X b;
int c;
};
int main()
{
Y a{ 1, {1, 2}, 2 };
//a.a = 1
//a.b.a = 1
//a.b.b = 2
//a.c = 2
Y b{ 1, {}, 2 };
//b.a = 1
//b.b.a = 0
//b.b.b = 0
//b.c = 2
Y c{ 1, 2, 3, 4 }; //波括弧の省略
//c.a = 1
//c.b.a = 2
//c.b.b = 3
//c.c = 4
int d[][3]{ { 1, 2 }, { 3, 4, 5 } };
//{1, 2, 0}
//{3, 4, 5}
}
#include <vector>
class C {
public:
int a;
std::vector<int> b;
};
int main()
{
C a{ 1, { 2, 3 } }; //OK
//vectorは任意の数の初期化子が指定できる
//逆に言えば必要な初期化子の数があらかじめ決まっていない
//C b{ 1, 2, 3 };
//上記はvectorを「2」で初期化しようとするが
//一致するコンストラクタがないのでエラー
//「3」は該当するメンバがなく初期化子が多すぎるのでエラー
}
なお、C++11まではコピーリスト初期化の形式(代入演算子を使用する初期化)でないとネストの波括弧の省略ができないという制限があります。