C++の構造体は、複数のデータをひとまとめにして扱うための仕組みです。
たとえば、名前・年齢・身長をまとめて管理したい場合は、次のような構造体を定義できます。
#include <string>
struct Person {
std::string name;
int age;
double height;
};
Personには、次の3つのメンバがあります。
name:名前age:年齢height:身長
構造体を使用するときは、Person型の変数を作成し、それぞれのメンバに値を設定します。
C++では構造体を初期化する方法が複数あるため、それぞれの特徴を理解して使い分けることが重要です。
波括弧{}を使って構造体を初期化する方法
メンバの順番に値を指定する
単純な構造体では、波括弧{}を使ってメンバを順番に初期化できます。
Person person{"Taro", 20, 170.5};
この場合、構造体で宣言されている順番に値が対応します。
name // "Taro"
age // 20
height // 170.5
つまり、
Person person{"Taro", 20, 170.5};
では、
person.name == "Taro"
person.age == 20
person.height == 170.5
という状態になります。
このようにメンバを順番に初期化する方法は、単純なデータ保持用の構造体でよく利用されます。
=を使った書き方もできる
次のように、=と波括弧を組み合わせることもできます。
Person person = {"Taro", 20, 170.5};
この場合も、上記の例と同様に各メンバが初期化されます。
現代的なC++では、
Person person{"Taro", 20, 170.5};
のように、=を省略した書き方も広く使われています。
空の{}で構造体を初期化する方法
Person person{};の意味
値を明示せず、
Person person{};
のように空の波括弧を指定することもできます。
この場合、各メンバはその型に応じた規則で初期化されます。
たとえば、
struct Data {
int number;
double value;
};
に対して、
Data data{};
とすると、
data.number == 0
data.value == 0.0
となります。
また、
struct Person {
std::string name;
int age;
double height;
};
の場合は、
Person person{};
とすると、概ね次の状態になります。
person.name.empty() == true
person.age == 0
person.height == 0.0
std::stringは整数型のように「0が代入される」のではなく、空の文字列として初期化されます。
そのため、「すべてのメンバが単純にゼロ初期化される」と考えるよりも、「それぞれの型に応じて適切に初期化される」と理解する方が正確です。
{}を付けない場合との違い
次の構造体を考えてみます。
struct Data {
int x;
int y;
};
ローカル変数として、
Data data;
とだけ書いた場合、xやyのような組み込み型メンバが未初期化になることがあります。
その状態で、
std::cout << data.x;
のように値を使用するのは避けるべきです。
一方、
Data data{};
とすれば、
data.x == 0
data.y == 0
となります。
特に理由がなければ、
Data data{};
のように明示的に初期化しておくと、未初期化による不具合を防ぎやすくなります。
一部のメンバだけを指定して初期化する方法
残りのメンバは規則に従って初期化される
次のような構造体があるとします。
struct Data {
int x;
int y;
int z;
};
最初のメンバだけを指定して、
Data data{10};
とすることもできます。
この場合、
data.x == 10
data.y == 0
data.z == 0
となります。
同様に、
Data data{10, 20};
なら、
data.x == 10
data.y == 20
data.z == 0
です。
指定されなかったメンバについても、集成体初期化の規則に従って初期化されます。
初期化と代入の違い
構造体を作るときに値を設定するのが初期化
次のコードは初期化です。
Person person{"Taro", 20, 170.5};
personというオブジェクトが作られると同時に、それぞれのメンバに値が設定されています。
作成後に値を変更するのが代入
一方、
Person person{};
person.name = "Taro";
person.age = 20;
person.height = 170.5;
のように、すでに作成されたオブジェクトへ値を設定するのは代入です。
また、
person.age = 21;
のように既存の値を変更する操作も代入です。
初期化と代入は似ていますが、C++では異なる処理として扱われます。
最初から値が分かっている場合は、
Person person{"Taro", 20, 170.5};
のように、オブジェクト作成時に初期化する方が簡潔です。
構造体のメンバにデフォルト値を設定する方法
デフォルトメンバ初期化子を使う
C++11以降では、構造体の定義内でメンバの初期値を指定できます。
#include <string>
struct Person {
std::string name = "unknown";
int age = 0;
double height = 0.0;
};
このような初期値は「デフォルトメンバ初期化子」と呼ばれます。
たとえば、
Person person;
と作成すると、
person.name == "unknown"
person.age == 0
person.height == 0.0
という状態になります。
設定値を持つ構造体で便利
たとえば、画面設定を表す構造体なら、
struct Config {
int width = 800;
int height = 600;
bool fullscreen = false;
};
としておくと便利です。
Config config{};
と作成するだけで、
config.width == 800
config.height == 600
config.fullscreen == false
になります。
同じ初期値を何度も記述する必要がないため、設定情報などを扱う構造体で有効です。
コンストラクタを使って構造体を初期化する方法
structでもコンストラクタを定義できる
C++のstructは、classと同じようにコンストラクタを持つことができます。
#include <string>
struct Person {
std::string name;
int age;
Person(std::string n, int a)
: name(n), age(a)
{
}
};
この場合、
Person person("Taro", 20);
や、
Person person{"Taro", 20};
のように初期化できます。
単純に値を保持するだけの構造体ではコンストラクタを用意しなくてもよい場合があります。
一方、初期化時に値を検証したり、一定のルールを適用したりしたい場合は、コンストラクタが役立ちます。
コンストラクタではメンバ初期化リストを使う
コンストラクタ本体で代入する方法との違い
次のようにコンストラクタ本体で値を代入することもできます。
Person(std::string n, int a)
{
name = n;
age = a;
}
しかし、基本的にはメンバ初期化リストを使用します。
Person(std::string n, int a)
: name(n), age(a)
{
}
メンバ初期化リストでは、各メンバを指定した値で直接初期化できます。
一方、コンストラクタ本体で代入する方法では、メンバが初期化されたあとに値を代入する形になります。
そのため、C++ではメンバ初期化リストを基本として考えるとよいでしょう。
constメンバを持つ構造体の初期化
constメンバは作成時に初期化する
次のようにconstメンバを持つ構造体を考えてみます。
struct User {
const int id;
};
この場合、
User user{100};
のように、オブジェクト作成時にidを初期化する必要があります。
コンストラクタを定義する場合も、メンバ初期化リストを使用します。
struct User {
const int id;
User(int value)
: id(value)
{
}
};
次のようにコンストラクタ本体で代入することはできません。
User(int value)
{
id = value; // エラー
}
constメンバは初期化後に値を変更できないためです。
参照型のメンバについても、同様に初期化時の扱いが重要になります。
C++20の指定初期化子
メンバ名を指定して初期化できる
C++20では、集成体に対して「指定初期化子」を利用できます。
たとえば、
struct Person {
std::string name;
int age;
double height;
};
であれば、
Person person{
.name = "Taro",
.age = 20,
.height = 170.5
};
と記述できます。
通常の、
Person person{"Taro", 20, 170.5};
という書き方と比べると、どの値がどのメンバに対応しているのか分かりやすいのが特徴です。
たとえば、
.age = 20
と書いてあれば、20が年齢であることが一目で分かります。
すべてのstructで使えるわけではない
C++20だからといって、すべての構造体で指定初期化子を使用できるわけではありません。
基本的には、対象となる型が「集成体(aggregate)」である必要があります。
単純なデータ保持用の構造体であれば集成体として扱われることが多いですが、厳密な条件は使用するC++標準によって異なります。
指定する順番にも注意する
C++の指定初期化子では、基本的にメンバが宣言されている順番に指定する必要があります。
たとえば、
struct Data {
int x;
int y;
};
なら、
Data data{
.x = 10,
.y = 20
};
は正しい書き方です。
一方、
Data data{
.y = 20,
.x = 10
};
のように順番を入れ替えることは、標準C++の指定初期化としては認められません。
C言語の指定初期化子と仕様が異なる部分があるため注意が必要です。
構造体の配列を初期化する方法
配列の各要素をまとめて初期化する
構造体は配列として扱うこともできます。
struct Point {
int x;
int y;
};
たとえば、
Point points[3] = {
{10, 20},
{30, 40},
{50, 60}
};
と初期化できます。
それぞれ、
points[0].x == 10
points[0].y == 20
points[1].x == 30
points[1].y == 40
points[2].x == 50
points[2].y == 60
となります。
std::arrayを利用する方法
固定長の配列にはstd::arrayを利用することもできます。
#include <array>
std::array<Point, 3> points{
Point{10, 20},
Point{30, 40},
Point{50, 60}
};
std::arrayには、通常の配列よりも標準ライブラリと組み合わせやすいという特徴があります。
構造体の中に構造体がある場合の初期化
入れ子になった構造体も初期化できる
構造体のメンバとして、別の構造体を持たせることもできます。
#include <string>
struct Address {
std::string city;
int zip;
};
struct Person {
std::string name;
Address address;
};
この場合、
Person person{
"Taro",
{"Tokyo", 100}
};
のように初期化できます。
より明示的に、
Person person{
"Taro",
Address{"Tokyo", 100}
};
と書くこともできます。
このとき、
person.name == "Taro"
person.address.city == "Tokyo"
person.address.zip == 100
となります。
std::vectorに構造体を格納する場合
初期化リストを使って複数要素を作成できる
実際のC++では、構造体をstd::vectorに格納するケースもよくあります。
#include <string>
#include <vector>
struct Person {
std::string name;
int age;
};
次のように初期化できます。
std::vector<Person> people{
{"Taro", 20},
{"Hanako", 25},
{"Jiro", 30}
};
要素を追加する場合は、
people.push_back({"Saburo", 35});
のように記述できます。
emplace_backは構造体の設計によって使い方が変わる
emplace_backは、要素をコンテナ内部で直接構築するための関数です。
たとえば、適切なコンストラクタを持つ型であれば、
people.emplace_back("Saburo", 35);
のようにコンストラクタ引数を直接渡せる場合があります。
ただし、利用できる書き方はC++の標準バージョンや、その構造体が集成体なのか、コンストラクタを持つのかによって変わります。
単純な構造体を追加するだけであれば、
people.push_back({"Saburo", 35});
でも十分分かりやすいでしょう。
集成体初期化とは
単純な構造体でよく使われる初期化方法
C++の構造体初期化を理解するうえで重要なのが、「集成体初期化(aggregate initialization)」です。
たとえば、
struct Point {
int x;
int y;
};
に対して、
Point point{10, 20};
と書くような初期化です。
Pointのように一定の条件を満たす型は「集成体」として扱われ、メンバを順番に初期化できます。
ただし、どの型が集成体として扱われるかという厳密な条件は、C++11・C++14・C++17・C++20などの標準バージョンによって細かな違いがあります。
初心者の段階では、
「単純なデータ保持用の構造体では、集成体初期化を利用できることが多い」
と理解しておくとよいでしょう。
()と{}による初期化の違い
どちらも似ているが常に同じ意味ではない
コンストラクタを持つ型では、
Person person("Taro", 20);
と、
Person person{"Taro", 20};
のどちらも利用できる場合があります。
ただし、()と{}は常に同じ意味になるわけではありません。
特に、std::initializer_listを受け取るコンストラクタが存在する場合は、{}を使用したときのコンストラクタ選択が変わることがあります。
代表的なのがstd::vectorです。
std::vector<int> a(3, 10);
の場合は、
10, 10, 10
という3要素のvectorになります。
一方、
std::vector<int> b{3, 10};
の場合は、
3, 10
という2要素のvectorになります。
そのため、()と{}は見た目が似ていても、意味が変わる場合があることを覚えておきましょう。
{}による初期化は縮小変換を制限できる
意図しない型変換を見つけやすい
波括弧を使ったリスト初期化には、縮小変換を制限する特徴があります。
たとえば、
int x{3.14};
はエラーになります。
3.14はdouble型であり、intに変換すると小数部分が失われるためです。
一方、
int x = 3.14;
では、doubleからintへの変換が行われる場合があります。
そのため、
int x{3.14};
のようなリスト初期化を利用すると、意図しないデータ損失につながる型変換をコンパイル時に発見しやすくなります。
構造体を初期化するときの注意点
組み込み型を未初期化のまま使わない
次のようなコードには注意が必要です。
struct Data {
int value;
};
Data data;
このようなローカル変数では、valueが未初期化になる場合があります。
そのため、
Data data{};
とするか、
struct Data {
int value = 0;
};
のように初期値を設定しておくと安全です。
メンバの宣言順を意識する
集成体初期化では、基本的にメンバの宣言順に値が対応します。
struct Rectangle {
int width;
int height;
};
Rectangle rect{100, 200};
なら、
rect.width == 100
rect.height == 200
です。
構造体のメンバ順序を後から変更すると、既存の初期化コードの意味まで変わる可能性があります。
そのため、メンバ数が多い構造体では、C++20の指定初期化子を利用することも選択肢になります。
共通の初期値は構造体側に定義する
同じ初期値を何度も使用する場合は、
struct Config {
int width = 800;
int height = 600;
bool fullscreen = false;
};
のように、構造体側にデフォルト値を持たせる方法が便利です。
これにより、
Config config{};
だけで適切な初期状態を作れます。
複雑なルールがあるならコンストラクタを検討する
単純にデータを保持するだけなら、集成体初期化は非常に便利です。
しかし、
- 値の範囲をチェックしたい
- 不正な状態を作れないようにしたい
- 初期化時に追加処理を行いたい
といった要件がある場合は、コンストラクタを使った設計も検討します。
C++の構造体初期化で覚えておきたい基本形
値を指定して初期化する
Person person{"Taro", 20, 170.5};
単純な構造体で、各メンバの値が最初から決まっている場合に便利です。
空の{}で初期化する
Person person{};
各メンバを型に応じた規則で初期化できます。
未初期化の組み込み型を避けたい場合にも有効です。
デフォルト値を構造体側に持たせる
struct Person {
std::string name = "unknown";
int age = 0;
double height = 0.0;
};
共通の初期状態がある場合に便利です。
C++20では指定初期化子も利用できる
対象が集成体であれば、
Person person{
.name = "Taro",
.age = 20,
.height = 170.5
};
のようにメンバ名を指定して初期化できます。
値とメンバの対応関係が明確になるため、メンバ数の多い構造体では可読性を高められます。
C++の構造体初期化を正しく使い分けよう
C++の構造体には、さまざまな初期化方法があります。
単純なデータ保持用の構造体なら、
Person person{"Taro", 20, 170.5};
のような波括弧による初期化が分かりやすいでしょう。
値がまだ決まっていない場合は、
Person person{};
としておけば、組み込み型の未初期化を避けやすくなります。
また、共通の初期値があるならデフォルトメンバ初期化子を使用し、初期化時に制約や処理が必要ならコンストラクタを検討します。
C++20を利用できる環境では、集成体に対して指定初期化子を使うことで、どの値がどのメンバに対応しているのかを明確にすることもできます。
構造体の初期化では、「とりあえず値を入れる」のではなく、オブジェクトを最初から正しい状態で生成することを意識するのが重要です。
特に、
Person person{};
による安全な初期化、
struct Person {
int age = 0;
};
のようなデフォルトメンバ初期化子、
Person person{
.name = "Taro",
.age = 20
};
のようなC++20の指定初期化を理解しておくと、構造体をより安全かつ読みやすく扱えるようになります。
以上、C++の構造体の初期化についてでした。
最後までお読みいただき、ありがとうございました。
