AB - 3.04.構造体 Editorial /

Time Limit: 0 msec / Memory Limit: 0 KB

前のページ | 次のページ

キーポイント

  • 構造体によって、「複数の型をまとめた新しい型」を作ることが出来る
  • 構造体の定義
struct 構造体名 {
  型1 メンバ変数名1
  型2 メンバ変数名2
  型3 メンバ変数名3
  ...(必要な分だけ書く)
};  // ← セミコロンが必要
  • 構造体の変数の宣言
構造体名 変数名;
  • 構造体型の値のことをオブジェクトという
  • 宣言と同時に初期化
構造体名 オブジェクト名 = {メンバ変数1の値, メンバ変数2の値, メンバ変数3の値, ...(必要な分だけ書く)};
  • メンバ変数メンバ関数はそれぞれオブジェクトに紐付いた変数・関数として使うことができる
  • メンバ変数へのアクセス
オブジェクト.メンバ変数
  • メンバ関数の定義
struct 構造体名 {
  返り値の型 メンバ関数名(引数の型1 引数名1, 引数の型2 引数名2, ...) {
    // 関数の内容
    //   (ここではメンバ変数に直接アクセスすることができる)
  }
};
  • メンバ関数の呼び出し
オブジェクト.メンバ関数(引数1, 引数2, ...)

構造体

3.02.pair/tupleとauto」で複数のデータをまとめる方法としてSTLのpair/tupleを紹介しました。 別の方法として構造体を利用することもできます。 実はSTLのpair/tupleも構造体を使って実装されています。

構造体関連の機能はたくさんあるので、このページでは主要なものを簡単に説明します。

構造体を用いることで、「複数の型をまとめた新しい型」を定義することができます。

次のプログラムを見てください。

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;     // 1つ目のデータはint型であり、xという名前でアクセスできる
  string y;  // 2つ目のデータはstring型であり、yという名前でアクセスできる
};

int main() {
  MyPair p = {12345, "hello"};  // MyPair型の値を宣言
  cout << "p.x = " << p.x << endl;
  cout << "p.y = " << p.y << endl;
}
実行結果
p.x = 12345
p.y = hello

MyPairという構造体を定義しています。 この構造体はint型とstring型をまとめたもので、それぞれx, yという名前がついています。 これら1つ1つのデータをメンバ変数といいます。

構造体の定義

次の形式で構造体を定義します。

struct 構造体名 {
  型1 メンバ変数名1
  型2 メンバ変数名2
  型3 メンバ変数名3
  ...(必要な分だけ書く)
};  // ← セミコロンが必要

構造体の定義は関数の外側、内側のどちらにも書くことができます。

構造体を定義することによって新しい型が使えるようになります。 例えば、上の例ではMyPairという構造体を定義したので、それ以降MyPairという型が使えるようになります。

宣言・初期化

構造体の変数を宣言するには、通常の変数の宣言と同じように次のように書きます。

構造体名 変数名;

また、次のように構造体名を用いて構造体の値を生成することもできます。

auto 変数名 = 構造体名();

構造体名を使う方法は一時変数を作りたい場合に便利です。

なお、構造体型の値のことをオブジェクトといいます。

例えば、はじめのサンプルプログラムのpMyPair型のオブジェクトです。

宣言と同時に、メンバ変数の初期化を行う場合は次のようにします。

構造体名 オブジェクト名 = {メンバ変数1の値, メンバ変数2の値, メンバ変数3の値, ...(必要な分だけ書く)};

メンバ変数

構造体が持っている変数をメンバ変数といいます。

メンバ変数へのアクセスは次のようにします。

オブジェクト.メンバ変数

メンバ変数は、通常の変数と同じように使うことができます。

なお通常の変数と同様に、オブジェクトを宣言しただけではメンバ変数の値は未定である点に注意してください。

メンバ関数

構造体には、オブジェクトに関連した処理を行う関数を定義することができ、この関数をメンバ関数といいます。

メンバ関数は次のように定義します。

struct 構造体名 {
  返り値の型 メンバ関数名(引数の型1 引数名1, 引数の型2 引数名2, ...) {
    // 関数の内容
    //   (ここではメンバ変数に直接アクセスすることができる)
  }
};

メンバ関数を呼び出すには次のようにします。

オブジェクト.メンバ関数(引数1, 引数2, ...)

メンバ関数の特徴は「メンバ関数が紐付いているオブジェクトのメンバ変数」に関数内からアクセスする場合に、オブジェクトの指定なしに直接アクセスできることです。 この点を除けば、通常の関数と同じように使うことができます。

メンバアクセスの機能を使う場合には、メンバ関数と通常の関数では違いがありますが、 APG4bで扱う範囲内では基本的に通常の関数と同様に使うことができます。

次のサンプルプログラムで確認してください。

サンプルプログラム

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;
  string y;
  // メンバ関数
  void print() {
    // 直接x, yにアクセスできる
    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
  }
};

int main() {
  MyPair p = { 12345, "Hello" };
  p.print();  // オブジェクト`p`の`print`を呼び出す

  MyPair q = { 67890, "APG4b" };
  q.print();  // オブジェクト`q`の`print`を呼び出す
}
実行結果
x = 12345
y = Hello
x = 67890
y = APG4b

細かい話

コンストラクタ

オブジェクトが作られるときに、独自の初期化処理などを行いたい場合にコンストラクタを使うことができます。

コンストラクタは次のように定義します。

struct 構造体名 {
  // コンストラクタ
  構造体名() {
    // コンストラクタの内容
  }
};

サンプルプログラム

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;
  string y;
  // コンストラクタ
  MyPair() {
    cout << "constructor called" << endl;
  }
};

int main() {
  MyPair p;  // ここでコンストラクタが呼ばれる
  p.x = 12345;
  p.y = "hello";
  cout << "p.x = " << p.x << endl;
  cout << "p.y = " << p.y << endl;
}
実行結果
constructor called
p.x = 12345
p.y = hello

コンストラクタはメンバ関数と同様に引数を取ることができます。

struct 構造体名 {
  // コンストラクタ
  構造体名(引数1の型 引数1の名前, 引数2の型 引数2の名前, ...) {
    // コンストラクタの内容
  }
};

コンストラクタが引数を取る場合、次のようにオブジェクトの宣言時にコンストラクタの引数に対応する値を渡す必要があります。

構造体名 オブジェクト名(引数1, 引数2, 引数3, ...);

サンプルプログラム

#include <bits/stdc++.h>
using namespace std;

struct NumString {
  int length;
  string s;
  // コンストラクタ
  NumString(int num) {
    cout << "constructor called" << endl;

    // 引数のnumを文字列化したものをsに代入し、sの文字数をlengthに代入する
    s = to_string(num);  // (STLの関数)
    length = s.size();
  }
};

int main() {
  NumString num(12345);  // コンストラクタに 12345 が渡される
  cout << "num.s = " << num.s << endl;
  cout << "num.length = " << num.length << endl;
}
実行結果
constructor called
num.s = 12345
num.length = 5

コンストラクタは複数定義することができ、与える引数の型や引数の個数によって自動的に呼び分けることができます。

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;
  string y;
  // コンストラクタ1
  MyPair() {
    cout << "初期化無し" << endl;
  }
  // コンストラクタ2
  MyPair(int x_) {
    cout << "xのみ初期化" << endl;
    x = x_;
  }
  // コンストラクタ3
  MyPair(int x_, string y_) {
    cout << "x, y両方初期化" << endl;
    x = x_;
    y = y_;
  }
};

int main() {
  MyPair p;  // コンストラクタ1が呼ばれる
  cout << "p.x = " << p.x << endl;
  cout << "p.y = " << p.y << endl;

  MyPair q(6789);  // コンストラクタ2が呼ばれる
  cout << "q.x = " << q.x << endl;
  cout << "q.y = " << q.y << endl;

  MyPair r(11111, "good bye");  // コンストラクタ3が呼ばれる
  cout << "r.x = " << r.x << endl;
  cout << "r.y = " << r.y << endl;
}
実行結果(例)
初期化無し
p.x = 4198608
p.y = 
xのみ初期化
q.x = 6789
q.y = 
x, y両方初期化
r.x = 11111
r.y = good bye

なお、{}を使ったオブジェクトの初期化を行う場合、コンストラクタの引数に対応するものを{}で囲む必要があります。

構造体名 オブジェクト名 = {引数1, 引数2, 引数3, ...};

メンバ変数ではなく、コンストラクタの引数を{}で囲むことに注意してください。

コピーコンストラクタ

コンストラクタはオブジェクトが作られる際に呼ばれますが、 関数の引数としてオブジェクトを渡す場合などの条件を満たした場合には、コピーコンストラクタという特殊なコンストラクタが呼ばれます。

次のような場合にコピーコンストラクタが呼ばれます。

  • 関数の引数としてオブジェクトを渡した場合
  • オブジェクトを宣言する際にMyStruct new_obj = old_obj;で初期化する場合
  • オブジェクトを宣言する際にMyStruct new_obj(old_obj);の形で初期化する場合
  • など

コピーコンストラクタについては込み入った話が多くなるので、基本的なことのみ紹介します。

コピーコンストラクタは次のように定義します。

struct 構造体名 {
  // コピーコンストラクタ
  構造体名(const 構造体名 &old) {
    // コンストラクタの内容
    // (oldの内容を使って初期化などを行う)
  }
};

constは変数の内容を書き換えられなくなる機能です。この例では、oldのメンバ変数を書き換えることができなくなっています。 詳しくは3.07で扱います。

引数名をoldとしましたが、自由に名前をつけて良いです。

なお、コピーコンストラクタを定義しなかった場合には、 「全てのメンバ変数をそのままコピーして新しいオブジェクトを作る」 という動作をするコピーコンストラクタが自動的に作られるので、 ただコピーしたいだけならコピーコンストラクタを自分で書く必要はありません。

コピーコンストラクタの呼び出しはコンパイラの最適化によって省略される場合があるので注意が必要です。

サンプルプログラム

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;
  string y;
  // コンストラクタ
  MyPair() {
    cout << "normal constructor called" << endl;
  }
  // コピーコンストラクタ
  MyPair(const MyPair &old) {
    cout << "copy constructor called" << endl;
    x = old.x + 1;
    y = old.y + " new";
  }
};

int main() {
  MyPair p;  // ここでコンストラクタが呼ばれる
  p.x = 12345;
  p.y = "hello";
  cout << "p.x = " << p.x << endl;
  cout << "p.y = " << p.y << endl;

  MyPair q(p);  // コピーコンストラクタが呼ばれる
  cout << "q.x = " << q.x << endl;
  cout << "q.y = " << q.y << endl;

  MyPair r = q;  // コピーコンストラクタが呼ばれる
  cout << "r.x = " << r.x << endl;
  cout << "r.y = " << r.y << endl;
}
実行結果
normal constructor called
p.x = 12345
p.y = hello
copy constructor called
q.x = 12346
q.y = hello new
copy constructor called
r.x = 12347
r.y = hello new new

このサンプルプログラムのように、通常のコンストラクタとコピーコンストラクタを同時に定義することもできます。

演算子オーバーロード

新たに定義した構造体型のオブジェクトに対してC++の演算子を使えるようにすることができ、この機能を演算子オーバーロードといいます。

演算子オーバーロードを行える演算子は限られてはいますが、殆どの演算子をオーバーロードすることができます。

演算子オーバーロードの使用はプログラムがシンプルになる一方で、意味がわかりにくくなることもあるので注意しましょう。

極端な例では、+という演算子に引き算をさせるようなこともできてしまいますが、このような非自明な挙動はなるべく避けるべきです。

+演算子のオーバーロード

サンプルプログラム

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;
  string y;
  // 別のMyPair型のオブジェクトをとって、x, yにそれぞれ+したものを返す
  // +演算子をオーバーロード
  MyPair operator+(const MyPair &other) {
    MyPair ret;
    ret.x = x + other.x;  // ここではint型の+演算子が呼ばれる
    ret.y = y + other.y;  // ここではstring型の+演算子が呼ばれる
    return ret;
  }
};

int main() {
  MyPair a = {123, "hello"};
  MyPair b = {456, "world"};

  // MyPair型の+演算子が呼ばれる
  MyPair c = a + b;

  cout << "c.x = " << c.x << endl;
  cout << "c.y = " << c.y << endl;
}
実行結果
c.x = 579
c.y = helloworld

演算子オーバーロードをメンバ関数として定義しています。

+演算子をオーバーロードする場合は次のように定義します。

struct 構造体名 {
  返り値の型 operator+(引数の型 引数) {
    // 処理内容
  }
};

返り値や引数の型は自由に決めることができます。ただし、引数の個数は1つです。

+演算子のオーバーロードは構造体の外側で定義することもできます。 下の「外側で定義する場合」で説明します。

代入演算子のオーバーロード

=演算子(代入演算子)も他の演算子と同様にオーバーロードによって動作をカスタマイズすることができます。

オーバーロードを行わなかった場合、代入演算子は自動的に定義されます。 自動的に定義される代入演算子では、「全てのメンバ変数をそのまま代入していく」というような処理が行われます。

サンプルプログラム

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;
  string y;
  // 代入演算子をオーバーロード
  void operator=(const MyPair &other) {
    cout << "= operator called" << endl;
    x = other.x;
    y = other.y;
  }
};

int main() {
  MyPair a = {123, "hello"};

  MyPair b;
  b = a;  // 代入演算子が呼ばれる

  cout << "b.x = " << b.x << endl;
  cout << "b.y = " << b.y << endl;
}
実行結果
= operator called
b.x = 123
b.y = hello

代入演算子のオーバーロードは次のように定義します。

  void operator=(const 構造体名 &other) {
    // 処理内容
  }

その他の演算子のオーバーロード

他の演算子も同様にオーバーロードすることができます。 メンバ関数として演算子オーバーロードを定義する場合、次のようにします。

struct 構造体の型 {
  返り値の型 operator演算子(引数の型 引数) {
    // 処理内容
  }
};

「演算子」の部分には、+, -, *, /, %, <, >, ==, !=, &&, ||などを書きます。

外側で定義する場合

構造体の外側で演算子オーバーロードを行うと、 自分が定義していない構造体(例えばSTLのpairなど)に対しても演算子をオーバーロードすることができます。

構造体の外側で演算子オーバーロードを定義する場合は、次のようにします。

返り値の型 operator演算子(引数の型1 引数1, 引数の型2 引数2) {
  // 処理内容
}

以下はpair<int, int>に対する演算子オーバーロードのサンプルプログラムです。

pairに対して+演算子をオーバーロードする例

#include <bits/stdc++.h>
using namespace std;

pair<int, int> operator+(pair<int, int> a, pair<int, int> b) {
  pair<int, int> ret;
  ret.first = a.first + b.first;
  ret.second = a.second + b.second;
  return ret;
}

int main() {
  pair<int, int> a = {1, 2};
  pair<int, int> b = {3, 4};
  auto c = a + b;
  cout << c.first << ", " << c.second << endl;  // 4, 6
}
実行結果
4, 6

pair<演算子をオーバーロードして.secondを優先して比較するように変更する例

#include <bits/stdc++.h>
using namespace std;

// .second → .first の順に比較
bool operator<(pair<int, int> l, pair<int, int> r) {
  if (l.second != r.second) {
    return l.second < r.second;
  } else {
    return l.first < r.first;
  }
}
// <演算子 を用いて定義
bool operator> (pair<int, int> l, pair<int, int> r) { return r < l; }
bool operator<=(pair<int, int> l, pair<int, int> r) { return !(r < l); }
bool operator>=(pair<int, int> l, pair<int, int> r) { return !(l < r); }

int main() {
  pair<int, int> a = {1, 5};
  pair<int, int> b = {3, 2};
  cout << (a < b) << endl;  // 0
  cout << (a > b) << endl;  // 1
}
実行結果
0
1

なお、複合代入演算子(+=, -=, *=, /=, %=など)は構造体の外側で定義することはできないことに注意してください。

STLのコンテナの要素として使う場合の注意

STLのコンテナや関数は、特定の演算子を要求することがあります。

次のような場合には比較演算子<をオーバーロードする必要があります。

  • mapのKeyとして構造体を使う場合
  • priority_queueの要素の型として構造体を使う場合
  • 構造体の配列に対してsort関数を使う場合

比較演算子<は次のように定義します。

bool operator<(const 構造体名 &left, const 構造体名 &right) {
  // left < right の場合に true を返すように実装する
}

メンバ初期化子リスト

メンバ変数を初期化する方法として、メンバ初期化子リストを使う方法があります。

基本的にはコンストラクタ内で代入するのと変わりませんが、 例えばメンバ変数が参照型である場合のように、コンストラクタ内で初期化することができない場合には、 メンバ初期化子リストで初期化する必要があります。

サンプルプログラム

#include <bits/stdc++.h>
using namespace std;

struct MyPair {
  int x;
  string y;
  // 初期化子リストを用いた初期化
  MyPair() : x(123), y("hello") {
  }
};

int main() {
  MyPair p;  // コンストラクタにより初期化される
  cout << "p.x = " << p.x << endl;
  cout << "p.y = " << p.y << endl;
}
実行結果
p.x = 123
p.y = hello

メンバ初期化子リストは次のようにコンストラクタに:を続けて書きます。

struct 構造体名 {
  型1 メンバ変数1;
  型2 メンバ変数2;
  ...(必要な分だけ書く)

  構造体名() : メンバ変数名1(初期化内容), メンバ変数名2(初期化内容), ...(必要な分だけ書く)
  {
  }
};

初期化の部分には、メンバ変数名{初期化内容1, 初期化内容2, ...}のように書くこともできます。

クラス

C++には構造体とほとんど同じ機能であるクラスがあります。

クラスと構造体の違いはメンバアクセスのデフォルトの挙動です。 メンバアクセスについてはAPG4bでは扱いませんが、メンバ変数やメンバ関数を使用できる範囲を制限する機能です。 「メンバ関数の中からしか扱えないメンバ変数」や「メンバ関数の中でしか呼べないメンバ関数」などを実現できます。

デフォルトのメンバアクセスが、クラスはプライベートメンバアクセス(private:を指定したときの挙動)で、構造体はパブリックメンバアクセス(public:を指定したときの挙動)です。

クラス(構造体)には継承という機能もあり、 オブジェクト指向プログラミングを行う際に便利なさまざまな機能が用意されています。 興味のある人は調べてみてください。

扱わなかった構造体関連の機能のリスト

このページで紹介しきれなかった機能のリストを次に示します。 より詳しく勉強したい人は調べてみてください。

  • デストラクタ
  • メンバアクセス
  • 継承
  • 静的メンバ変数/関数
  • friend宣言
  • union
  • ビットフィールド
  • など

演習問題

リンク先の問題を解いてください。