構造体 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要 🔗

このページでは、型が混在した複数の値を、1つの型にまとめて扱える構造体という機能を取り上げます。配列と違って、型がばらばらでも構わないという特徴がありますが、その自由度を得るために、まずはプログラマーがみずから型の定義を作らなければならないという考え方の違いを理解しなければなりません。構造体には実に多くの機能がありますが、このページでは、複数の値をまとめるという目的に十分な範囲での説明にとどめます。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



構造体 🔗

蔵書リストに登録するデータの種類を想像すると、書名や著者名、購入日、出版日、価格、評価、メモなど多数の項目を思いつきます。これらの項目ごとに std::vector を用意することも考えられますが、1冊の本を構成する情報なのですから、1つの std::vector にまとめて取り扱うのが適切といえます。すると、std::vector<???>??? のところが問題になります。

すべての項目を文字列で表現すれば、std::vector<std::string> とすることで、0番目に書名、1番目に著者名、2番目に購入日・・・というふうにデータを格納しておくことは可能です。これが本の冊数分だけ必要なので、さらに std::vector に入れて std::vector<std::vector<std::string>> ということになります。これでも実現はできますが、少し複雑すぎるような気もしますし、価格や評価などは整数型のほうが妥当なようにも思えます。

そこで、色々な型が混在した複数の項目を、ひとまとめにして管理できる方法が欲しいということになります。std::string型だったり int型だったりする項目が複数あるが、全部まとめて「本の情報をあらわす型」、つまり Book型だ、といえるような型が欲しいのです。これがあれば、std::vector<book> books; のようにして、1つの変数ですべての本を管理できます。

このような、型が混在している複数の値をまとめた型を実現する方法が、このページのテーマである、構造体 (structure) です。

構造体という用語には、2つの意味が含まれています。1つは「型」、もう1つは「その型の変数」です。この2つをあまり区別せずに「構造体」と呼ぶことが多いですが、誤解がないように、前者を構造体型 (structure type)、後者を構造体型の変数(構造体変数)のように呼び分けることにします。

構造体はC言語にもありますが、C++ では、オブジェクト指向プログラミング (object-oriented programming) と呼ばれるプログラミング手法に適応するために、多くの機能が追加されており、名称も、クラス (class) と呼ぶのが普通です。C++ では構造体とクラスは同一のものであって、本来、区別する必要はありません(ごくわずかなルールの違いはありますが)。そのため、C++ の解説記事などでは、構造体という用語を使わず、クラスという名称で統一していることがあります。あえて構造体という用語を使うときには、C言語での考え方の範囲の話であることを強調していることが多いです。

このページでは、オブジェクト指向プログラミングの考え方による機能は省き、C言語の構造体の考え方の範囲で解説を行っています。

現時点では、オブジェクト指向プログラミングの詳細を知る必要はありませんが、現代のプログラミングにおいては半ば常識化した考え方になっています。逆にいえば、それほど意識せずとも、知らず知らずのうちにオブジェクト指向プログラミングの考え方に触れることになるはずです。

構造体型を定義する 🔗

構造体型の内容(どんな型の項目がいくつあるのか)はプログラマーが決めなければならないことです。そのため、まずは構造体型の定義を記述することから始めなければなりません。int型とか double型、std::string型のような、すでに用意されている型を組み合わせて、新たな型を我々が作るのです。

構造体型を定義する構文は次のとおりです。

struct 構造体型の名前 {
    データメンバの宣言;
      :
};

struct は、構造体であることを表すキーワードです。このキーワードに続けて、構造体型の名前を記述します。

【C言語プログラマー】構造体型の名前を記述する場面で毎回 structキーワードと構造体タグ名を書く必要はなくなっています。そのため、typedef で型の別名も付けておくという方法も C++ では必要ありません。

{} の内側には、データメンバ (data member) と呼ばれる変数の宣言を書き並べます。データメンバのことをメンバ変数 (member variable) と呼ぶこともあります。

配列の場合は要素に名前がなく、代わりに添字を使って区別しました。構造体型の場合は、データメンバに名前を与えます。また、各データメンバにはそれぞれの型の指定が必要ですから、結果的に、変数の宣言と同じかたちの記述を書くことになります。たとえば、書名、著者名、価格、評価の4項目を持った、本の構造体型を定義するとすれば、次のようになります。

struct Book {
    std::string    title;
    std::string    author;
    int            price;
    int            evaluate;
};

強制されているわけではないですが、構造体型の名前は、先頭を大文字にすることが多いです。

構造体にはもっと多くの機能があるため、構造体型の定義はより複雑になることがあります。Book構造体は、構造体型としてはシンプルなかたちをしているといえます。このような、データメンバの型と名前以外のものがないシンプルな構造体型は、集成体(アグリゲート) (aggregate) という分類に含まれます。構造体型が集成体とみなせるかどうかは、C++ のルールの一部に影響を与えます(たとえば、後で取り上げるように、リスト初期化のルールが変わります)。

【上級】コンストラクタを定義する、protected や private な非静的なデータメンバを定義する、基底クラスがある、仮想関数がある、のいずれかを満たすと、集成体ではなくなります[1]

【上級】集成体に分類されるものとしてほかに、通常の配列(std::vector などではなく)があります[1]

データメンバの初期化 🔗

データメンバを宣言するときに、初期化子を与えることができます。

struct Book {
    std::string    title {};
    std::string    author {};
    int            price {};
    int            evaluate {5};
};

構造体変数を宣言すると、そのデータメンバはここで指定した初期化子によって初期化されます(構造体変数の側に与えた初期化子の内容で上書きできます)。

データメンバの初期化子として使える記法は、={} によるものに限られており、() は使えません。たとえば、std::vector<int> v(100); のような宣言はできません。また、定数式でなければなりません。なお、データメンバの型を auto にして、初期化子から型推論させることはできません。

C++14 より前の規格では、データメンバに初期化子を与えると、その構造体型は集成体でなくなるという仕様がありました[2]。集成体でなくなると、構造体変数を宣言するときに、Book book {"aaa", "bbb", 1000, 1}; のように、データメンバ1つ1つに初期化子を与える記法が禁じられてしまいます。C++14 からは、データメンバに初期化子を与えても集成体とみなされるように変更されましたが、Visual Studio 2015 では以前の仕様に沿っており、集成体でなくなってしまいます。

変数が初期化されていない瞬間は無くしたほうが安全であり、良い習慣なので、データメンバの宣言時の初期化は積極的に行ってよいです。しかし、新C++編としては、動作確認の最低環境を Visual Studio 2015 に設定しているので、データメンバの宣言時の初期化は避けることにします。

構造体変数を宣言する 🔗

構造体型の定義ができたら、構造体変数を宣言できます。変数宣言の方法に違いはありません。たとえば、Book構造体の構造体変数を次のように宣言できます。

Book book;

【C言語プログラマー】structキーワードは不要になりました(付けることもできます)。

この場合、データメンバの初期値は、データメンバの宣言時に初期化子を与えているならそれに従います。与えていないなら、初期化子なしのときと同じ結果になります(たとえば、std::string型のデータメンバは空文字列になるが、int型のデータメンバは不定な値にってしまう)。そのため、データメンバの宣言時に初期化子を与えておくと安全ですが、前述したとおり、Visual Studio 2015 での事情があるため、新C++編ではこれは行わないことにしています。

データメンバの宣言時に初期化子を与えているかどうかに関わらず、いつものように、構造体変数の宣言時にリスト初期化しておくのが安全です。

Book book1 {};
Book book2 {"C++ Programming Guide Book", "Thomas Murray", 3500, 5};

リスト初期化のルールについては、あとであらためて確認します


すでに存在する構造体変数のコピーを作ることができます。

Book book2 = book1;  // book1 と同じ内容の book2 を宣言

【C++20】指示付き初期化という機能が追加され、S s { .a {100}, .b {200} }; のように、. とデータメンバ名を指定しながら初期化子を記述できるようになりました。この機能を使う場合は、s {~} の部分の括弧は {} でなければならず、() の場合には許可されません。[3]

【C言語プログラマー】C99 では、要素指示子を使って、データメンバを指定しながら初期化子を記述できます(C言語編第26章)。この機能は C++ には長らく受け継がれていませんでしたが、C++20 で(完全に同じではないですが)導入されました[3]

リスト初期化 🔗

リスト初期化のルールは以前、「std::vector」のページで取り上げていますが、構造体関係の追加ルールがあるので、あらためて取り上げます。以下の説明は判断の順番どおりに並べています。最初に適合したところの方法で初期化します。

リスト初期化の正確なルールはかなり複雑なので、ここではすべてを正確に示してはいません。正確なルールは言語仕様を確認してください[4]

リスト初期化の対象が集成体であれば、集成体初期化 (aggregate initialization) を行います。集成体初期化は、{} の内側に書き並べた初期化子を、データメンバに、宣言順どおりに適用していくというものです。もし、データメンバの個数に対して、初期化子のほうが少なかった場合には、残りのデータメンバは、データメンバ宣言時に与えた初期化子で初期化されます。それもない場合は値初期化されます[5](値初期化とは、もっとも自然と思われる値で初期化するということでした。「std::vector」のページを参照)。

データメンバの個数の方が少ない場合は、コンパイルエラーです。

// 集成体
struct Book {
    std::string    title;
    std::string    author;
    int            price;
    int            evaluate {3};
};

Book book1 {"ABC", "K.S", 3500, 5};  // title="ABC" author="K.S" price=3500 evaluate=5
Book book2 {"ABC", "K.S"};           // title="ABC" author="K.S" price=0 evaluate=3
Book book3 {};                       // title="" author="" price=0 evaluate=3

【C++20】集成体初期化になる場合には、{} の代わりに () を使えるようになりました。ただし、縮小変換を許すかどうかなどの違いがあります。[6]

このページで説明する範囲の機能しか使っていない構造体は、必ず集成体とみなせるので、ここより下の判断に進むことはありません(前述したとおり、Visual Studio 2015 では(C++11 以前の仕様で実装されているコンパイラでは)、データメンバの宣言に初期化子を与えていると、集成体でなくなるので、この下の判断に進みます)。


{} の内側が空で、対象が構造体型である場合、初期化子を与えているデータメンバはその初期化子によって初期化され、それ以外のデータメンバは値初期化されます。

【上級】正確にいえば、デフォルトコンストラクタが呼び出せる場合です。構造体とクラスは同一のものなので、C言語の構造体のつもりで構造体型を定義していても、暗黙のデフォルトコンストラクタは作られています。

{} の内側に初期化子がある場合で、リスト初期化の対象の型が、初期化子リストを受け付けて初期化できる場合は、その方法を呼び出して初期化します。たとえば、std::vector や std::string で、{} の内側の初期化子を使って要素を1つ1つ初期化することが該当します。

【上級】仮引数の型が std::initializer_list<T> のコンストラクタ(初期化リストコンストラクタ)を呼び出しています(「コンストラクタ」のページを参照)。

{} の内側に初期化子があるが、初期化子リストを受け付けて初期化できない場合は、{}() とみなして初期化を試みます。() はいわゆるコンストラクタと呼ばれる機能を呼び出す構文でした(「std::vector」のページを参照)。

【上級】1つ前のルールがあるので、仮引数が std::initializer_list<T> のコンストラクタが、そうでないコンストラクタよりも優先されることになります。

{} の内側の初期化子が1つだけの場合で、リスト初期化の対象の型が int型や double型のような、1つの値しか格納できないシンプルな型(データメンバが1つだけの構造体型は不可)の場合は、その初期化子によって初期化します。

【上級】対象が参照型の場合には複雑なルールがありますが、ここでは割愛します。

{} の内側が空の場合は、値初期化を行います。

ここまでどこにも該当しなかった場合は、コンパイルエラーとなります。

構造体型の定義と構造体変数の宣言をまとめる 🔗

構造体型の定義と、その型の変数の宣言をまとめて書くことができます。

struct Book {
    std::string    title;
    std::string    author;
    int            price;
    int            evaluate;
} book {};

初期化子の有無は自由です。

データメンバにアクセスする 🔗

構造体変数を宣言できるようになったので、データメンバにアクセスしてみます。データメンバは次の構文でアクセスできます。

構造体変数名.データメンバ名

次のプログラムは、本の情報を標準入力から入力させて、最終的な情報を出力しています。

#include <iostream>
#include <string>

int main()
{
    struct Book {
        std::string    title;
        std::string    author;
        int            price;
        int            evaluate;
    };

    Book book {};
    std::cout << "Please enter the title.\n";
    std::getline(std::cin, book.title);
    std::cout << "Please enter the author.\n";
    std::getline(std::cin, book.author);
    std::cout << "Please enter the price.\n";
    std::cin >> book.price;
    std::cout << "Please enter the evaluate.\n";
    std::cin >> book.evaluate;

    std::cout << "title: " << book.title << "\n"
              << "author: " << book.author << "\n"
              << "price: " << book.price << "\n"
              << "evaluate: " << book.evaluate << "\n";
}

実行結果:

Please enter the title.
C++ Programming Guide Book  <-- 入力した内容
Please enter the author.
Thomas Murray  <-- 入力した内容
Please enter the price.
3500  <-- 入力した内容
Please enter the evaluate.
5  <-- 入力した内容
title: C++ Programming Guide Book
author: Thomas Murray
price: 3500
evaluate: 5

比較 🔗

構造体変数どうしの比較は、直接的には行えません。たとえば、book1 == book2 のように比較しようとしてもコンパイルエラーになります。

構造体変数どうしで比較したい場合は、データメンバ1つ1つを別個に比較する必要があります。

【C言語プログラマー】パディングを巻き込むため、memcmp関数で比較していけないというガイドラインはそのまま当てはまります(C言語編第34章)。

#include <iostream>
#include <string>

int main()
{
    struct Book {
        std::string    title;
        std::string    author;
        int            price;
        int            evaluate;
    };

    Book book1 {"C++ Programming Guide Book", "Thomas Murray", 3500, 5};
    Book book2 = book1;

    std::cout << std::boolalpha
              << (book1.title == book2.title &&
                  book1.author == book2.author &&
                  book1.price == book2.price &&
                  book1.evaluate == book2.evaluate)
              << "\n";
}

実行結果:

true

すべてのデータメンバを比較するために、論理AND演算子を使って条件式をつなげ合わせます。あとから構造体型にデータメンバが追加される場合、条件式を増やしわすれないように注意が必要です。

代入 🔗

型が同じであれば、構造体変数を別の構造体変数へ代入できます。代入元のデータメンバそれぞれの値が、代入先の対応するデータメンバにコピーされます。

#include <iostream>
#include <string>

int main()
{
    struct Book {
        std::string    title;
        std::string    author;
        int            price;
        int            evaluate;
    };

    Book book1 {"C++ Programming Guide Book", "Thomas Murray", 3500, 5};
    Book book2 {};
    
    book2 = book1;

    std::cout << std::boolalpha
              << (book1.title == book2.title &&
                  book1.author == book2.author &&
                  book1.price == book2.price &&
                  book1.evaluate == book2.evaluate)
              << "\n";
}

実行結果:

true

「型が同じ」であるためには、文字通り、構造体型が同じでなければなりません。データメンバが同じならいいということではありません。たとえば、次の2つの構造体型は、データメンバはまったく同じですが異なる型なので、互いの型の変数を代入することはできません。

// 従業員
struct Employee {
    std::string  name;  // 名前
    int          age;   // 年齢
};

// 顧客
struct Customer {
    std::string  name;  // 名前
    int          age;   // 年齢
};

集成体に対しては、代入元を初期化子リストにできます。

#include <iostream>
#include <string>

int main()
{
    struct Book {
        std::string    title;
        std::string    author;
        int            price;
        int            evaluate;
    };

    Book book {};
    book = {"C++ Programming Guide Book", "Thomas Murray", 3500, 5};

    std::cout << "title: " << book.title << "\n"
              << "author: " << book.author << "\n"
              << "price: " << book.price << "\n"
              << "evaluate: " << book.evaluate << "\n";
}

実行結果:

title: C++ Programming Guide Book
author: Thomas Murray
price: 3500
evaluate: 5

std::vector の要素が集成体とみなせる構造体型のとき、push_back関数などでも初期化子リストの構文を使えます。

std::vector<Book> books {};
books.push_back({"C++ Programming Guide Book", "Thomas Murray", 3500, 5});

大きなデータのコピー 🔗

構造体には複数のデータメンバが含まれるため、構造体変数はデータが大きくなりがちです。大きなデータのコピーには処理時間がかかることを意識しておかなければなりません。

特に注意が必要な場面の1つに範囲for文があります。

#include <iostream>
#include <string>
#include <vector>

int main()
{
    struct Book {
        std::string    title;
        std::string    author;
        int            price;
        int            evaluate;
    };

    std::vector<Book> books {};
    books.push_back({"C++ Programming Guide Book", "Thomas Murray", 3500, 5});
    books.push_back({"C++ Programming Primer", "Sonia Elis", 2200, 3});
    books.push_back({"C++ Tutorial", "Jack Simon", 2800, 4});
    
    for (auto book : books) {
        std::cout << "title: " << book.title << "\n"
                  << "author: " << book.author << "\n"
                  << "price: " << book.price << "\n"
                  << "evaluate: " << book.evaluate << "\n";
    }
}

実行結果:

title: C++ Programming Guide Book
author: Thomas Murray
price: 3500
evaluate: 5
title: C++ Programming Primer
author: Sonia Elis
price: 2200
evaluate: 3
title: C++ Tutorial
author: Jack Simon
price: 2800
evaluate: 4

範囲for文のところで、for (auto book : books) としており、books から取り出された本1冊分のデータが、変数book にコピーされます。このプログラムでは、データメンバの値がわかればいいだけなので、わざわざコピーを作るのは無駄であるといえます。添字やイテレータで要素を直接アクセスすればコピーが不要であることを考えても、やはり無駄が気になります。

この場面では、参照型(「std::vector」のページを参照)を使うと、範囲for文を使いつつも、データメンバのコピーをなくせます。

for (auto& book : books) {
    std::cout << "title: " << book.title << "\n"
              << "author: " << book.author << "\n"
              << "price: " << book.price << "\n"
              << "evaluate: " << book.evaluate << "\n";
}

autoauto& に変更しただけです。こうすると、book は、books の要素の別名としてはたらくことになるので、データメンバのコピーがなくなります。

【上級】さらにいえば、book を経由した書き換えを行わないので、const auto& book : books のように const参照にするのが良いです。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

四角形の幅と高さを保持する構造体を作ってください。

解答・解説

問題2 (確認★)

問題1の構造体型の変数を2つ宣言し、データメンバが同じ値かどうかを比較するプログラムを作成してください。

解答・解説

問題3 (基本★)

問題1の構造体型の変数を2つ宣言し、どちらのほうが面積が大きいかを判定するプログラムを作成してください。

解答・解説

問題4 (応用★★)

std::vector に以下のように本のデータが登録されています。標準入力から評価値を入力させ、その評価値以上の本の一覧を出力するプログラムを作成してください。

struct Book {
    std::string    title;
    std::string    author;
    int            price;
    int            evaluate;
};

std::vector<Book> books {
    {"C++ Programming Guide Book", "Thomas Murray", 3500, 5},
    {"C++ Programming Primer", "Sonia Elis", 2200, 3},
    {"C++ Tutorial", "Jack Simon", 2800, 4},
    {"C++ Game Programming", "Richard Bill", 5800, 5},
    {"C++ Reference Book", "Jack Simon", 4000, 2},
};

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る