M.Hiroi's Home Page

お気楽C++プログラミング超入門

初級編 : 構造体


Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

はじめに

近代的なプログラミング言語は、ユーザーが既存のデータ型を組み合わせて、新しいデータ型を定義する機能を備えています。C言語の場合、typedef を使ってデータ型に別名を付けることができます。既存のデータ型の組み合わせは「構造体 (structure)」を使って行います。

C++はオブジェクト指向機能をサポートしているので、構造体と同様なことを「クラス」を使って行うこともできます。今回は構造体の使い方を簡単に説明します。

●構造体の定義

構造体の定義を下図に示します。

struct 名前 {
  データ型1 変数名1;
    ...
  データ型n 変数名n;
};

  図 : 構造体の定義

struct の後ろに構造体の名前を書き、{ ... } の中に変数を定義します。C++の場合、構造体の名前はデータ型として使用することができます。また、C言語の同様に struct 名前 と宣言することもできます。もちろん、typedef で別名を付けてもかまいません。構造体の中の変数を「メンバ変数」といいます。他のプログラミング言語では、メンバ変数を「フィールド」とか「スロット」と呼ぶ場合もあります。

簡単な例を示しましょう。点の座標を格納する構造体 Point を作ります。

リスト : 点の定義

struct Point {
  double x, y;
};

Point の先頭を英大文字にしましたが point にしてもかまいません。メンバ変数 x, y に x 座標と y 座標を格納します。

●構造体の使い方

次は構造体の使い方を説明します。構造体を格納する変数は次のように宣言します。

構造体名 変数名;
構造体名 変数名 = {値1, 値2, ..., 値n};

メンバ変数の初期値を指定する場合、構造体で定義した順番で値を指定します。初期値が省略された場合、外部変数であれば値は 0 に、局所変数であれば値は不定になります。メンバ変数のアクセスは簡単です。変数名 + ドット ( . ) + メンバ名 で行います。ドットのことをメンバ選択演算子、または直接メンバ演算子といいます。

簡単な例を示しましょう。次のリストを見てください。

リスト : 構造体の使用例 (sample100.cpp)

#include <iostream>
#include <cmath>
using namespace std;

// Point の定義
struct Point {
  double x, y;
};

// 距離を求める
double distance_(Point p, Point q)
{
  double dx = p.x - q.x;
  double dy = p.y - q.y;
  return sqrt(dx * dx + dy * dy);
}

int main()
{
  Point p0;
  Point p1 = {0, 0};
  Point p2 = {10, 10};
  Point p3;
  p3 = p1;
  cout << p0.x << ", " << p0.y << endl;
  cout << p1.x << ", " << p1.y << endl;
  cout << p2.x << ", " << p2.y << endl;
  cout << p3.x << ", " << p3.y << endl;
  cout << distance_(p1, p2) << endl;
}
$ clang++ sample100.cpp
$ ./a.out
6.94905e-310, 6.94905e-310
0, 0
10, 10
0, 0
14.1421

点を表す構造体 Point を定義します。関数内で Point p0; と宣言すると、p0 は局所変数になるので、メンバ変数 x, y の値は不定です。点を (x, y) で表すと、変数 p1 と p2 は (0, 0), (10, 10) に初期化されます。

構造体は代入演算子で値をコピーすることができます。p3 = p1; とすると、メンバ変数 p3.x に p1.x の値が代入され、p3.y には p1.y の値が代入されます。また、Point p3 = p1; のように初期値に構造体を指定することもできます。C++は「初期化」と「代入」を厳密に区別しています。この場合、代入ではなく初期化が行われ、p1 のメンバ変数の値が p3 のメンバ変数の初期値として使用されます。

関数 distance_ は p と q の距離を求めます。distance という名前の関数が標準ライブラリに定義されているので、後ろにアンダースコア ( _ ) をつけました。関数の引数に構造体を渡すとき、配列と違って構造体はコピーされることに注意してください。コピーしたくない場合やメンバ変数の値を書き換えたい場合は参照を使用してください。

関数の返り値に構造体を指定することもできます。次の例を見てください。

リスト : 構造体を返す関数 (sample101.cpp)

#include <iostream>
using namespace std;

struct Point {
  double x, y;
};

Point make_point(double x, double y)
{
  Point p = {x, y};
  return p;
}

int main(void)
{
  Point p1 = make_point(10, 20);
  cout << p1.x << ", " << p1.y << endl;
}
$ clang++ sample101.cpp
$ ./a.out
10, 20

関数 make_point は構造体を生成して返しますが、変数 p1 に代入するとき、構造体のコピーが行われることに注意してください。なお、構造体のサイズが大きくなると、コピーするのに時間が少々かかるようになります。少しの時間とはいえ、塵も積もれば山となるので、構造体は参照またはポインタを使って受け渡しを行うのが一般的です。

●構造体とポインタ

構造体は new 演算子を使って動的にメモリ領域を取得することができます。簡単な例を示しましょう。次のリストを見てください。

リスト : 構造体とポインタ (sample102.cpp)

#include <iostream>
#include <cmath>
using namespace std;

// Point の定義
struct Point {
  // メンバ変数
  double x, y;
};

// コンストラクタ
Point* make_point(double x, double y)
{
  Point* p = new Point;
  p->x = x;
  p->y = y;
  return p;
}

// 距離を求める
double distance_(const Point& p1, const Point& p2)
{
  double dx = p1.x - p2.x;
  double dy = p1.y - p2.y;
  return sqrt(dx * dx + dy * dy);
}

int main(void)
{
  Point* p0 = make_point(0, 0);;
  Point* p1 = make_point(10, 10);
  Point* p2 = make_point(100, 100);
  cout << distance_(*p0, *p1) << endl;
  cout << distance_(*p2, *p0) << endl;
  delete p0;
  delete p1;
  delete p2;
}
$ clang++ sample102.cpp
$ ./a.out
14.1421
141.421

構造体へのポインタを宣言するときは構造体名の後ろに * を付けます。メンバ変数のアクセスは、ドット ( . ) ではなく、変数名 + 矢印 (->) + メンバ変数名 とします。矢印 (->) のことを「間接メンバ演算子」または「矢印演算子」といいます。

たとえば、Point* p のメンバ変数 x にアクセスする場合は p->x とします。(*p).x としてもアクセスすることはできますが、ポインタの場合は間接メンバ演算子を使うのが普通です。

構造体の実体は new を使って動的に確保することもできます。この場合、make_point のような生成関数 (コンストラクタ) [*1] を定義しておくと便利です。make_point は new で Point の実体を確保し、引数 x, y の値でメンバ変数の値を初期化し、取得した Point のアドレスを返します。返り値の型は Point* になります。

関数 distance_ の引数は参照型 Point& に変更します。これで Point をコピーしないで distance_ に渡すことができます。main では、make_point で Point を生成して、変数 p0, p1, p2 にセットします。distance_ の引数は参照型なので、p0, p1, p2 ではなく、*p0, *p1, *p2 を渡すことに注意してください。

-- note --------
[*1] C++の構造体はC++のクラスとほとんど同じで、構造体の中で関数 (メンバ関数) を定義することができます。もちろん、「コンストラクタ」や「デストラクタ」といった特別なメンバ関数も定義することができます。違いは「アクセス制御」です。クラスの場合、メンバ変数やメンバ関数はデフォルトで「非公開 (private)」に設定されますが、構造体はデフォルトで「公開 (public)」に設定されます。本稿ではC言語と同等の機能だけを説明し、クラスに関しては次回以降で詳しく説明します。

●構造体と配列

構造体は配列に格納することもできます。宣言は次のようになります。

構造体名 配列名[大きさ];
構造体名 配列名[大きさ] = { {値a, ...}, ..., {値z, ,,,,} };
構造体名 配列名[] = { {値a, ...}, ..., {値z, ,,,,} };

構造体のポインタを格納する配列も定義することができます。この場合、構造体名の後ろに * を付けます。

簡単な例を示しましょう。次のリストを見てください。

リスト : 構造体と配列 (sample103.cpp)

#include <iostream>
#include <cmath>
using namespace std;

// Point の定義
struct Point {
  double x, y;
};

// Point の生成
Point *make_point(double x, double y)
{
  Point *p = new Point;
  p->x = x;
  p->y = y;
  return p;
}

// 距離を求める
double distance_(Point& p1, Point& p2)
{
  double dx = p1.x - p2.x;
  double dy = p1.y - p2.y;
  return sqrt(dx * dx + dy * dy);
}

int main()
{
  Point p0[] = {{0, 0}, {10, 10}, {100, 100}};
  Point* p1[] = {
    make_point(0,0),
    make_point(10, 10),
    make_point(100, 100)
  };
  cout << distance_(p0[0], p0[1]) << endl;
  cout << distance_(p0[1], p0[2]) << endl;
  cout << distance_(*p1[0], *p1[1]) << endl;
  cout << distance_(*p1[1], *p1[2]) << endl;
  cout << p0[1].x << ", " << p0[1].y << endl;
  cout << p1[2]->x << ", " << p1[2]->y << endl;
}
$ clang++ sample103.cpp
$ ./a.out
14.1421
127.279
14.1421
127.279
10, 10
100, 100

配列 p0 は 3 個の Point を格納しています。配列 p1 は 3 個の Point へのポインタを格納します。p1 は make_point を使って初期化しています。distance_ を呼び出すとき引数は参照渡しなので、p0 の場合は p0[1] のように配列の要素 (構造体) をそのまま渡します。p1 の場合、要素の値は構造体の先頭アドレスなので、*p1[1] のように * を付けてください。メンバ変数にアクセスする場合も簡単で、p0 の場合は p0[n].x で、p1 の場合は p1[n]->x で行うことができます。

●共用体

C/C++の配列は同じデータ型の要素しか格納することができません。たとえば、int a[10] と宣言すれば、この配列に格納できるのは整数 (int) だけで、浮動小数点数 (double) は格納できません。たいていの場合、これで問題はないのですが、時と場合によっては、一つの配列に異なるデータ型を混在させたいこともあります。このような場合、C/C++では「共用体 (union)」を使います。

共用体は複数のデータを同一のメモリ領域に割り当てる方法です。定義方法は構造体とよく似ています。共用体の定義を下図に示します。

union 名前 {
  データ型1 変数名1;
    ...
  データ型n 変数名n;
};

  図 : 共用体の定義

union の後ろに共用体の名前を書き、{ ... } の中に変数を定義します。C++の場合、構造体と同様に名前はデータ型として使用することができます。また、C言語の同様に union 名前 と宣言することもできます。もちろん、typedef で別名を付けてもかまいません。

共用体は構造体と違って、メンバ変数は同一の領域(アドレス)に割り当てられます。簡単な例を示しましょう。

リスト : 共用体の使用例 (sample104.cpp)

#include <iostream>
using namespace std;

union data {
  int fixnum;
  double fltnum;
};

int main()
{
  data n = {1};
  cout << n.fixnum << endl;
  n.fltnum = 1.2345;
  cout << n.fltnum << endl;
  cout << n.fixnum << endl;
  return 0;
}

int と double の共用体を考えてみましょう。構造体であれば、fixnum に 4 バイト、そのあと fltnum に 8 バイトの領域が割り当てられて、全体で 12 バイトの大きさになります。これに対し、共用体では fixnum と fltnum は同一アドレスから領域が確保されます。確保される領域は変数のなかで最大サイズの大きさ、この場合は fltnum の 8 バイトが割り当てられます。

つまり、2 つの変数領域が重なっているわけです。したがって、構造体のように変数に別々の値を代入することはできません。なお、共用体のアクセスは、構造体の場合と同じです。共用体を初期化する場合、初期値は共用体の先頭で定義したデータ型でなければいけません。

それでは実行してみましょう。

$ clang++ sample104.cpp
$ ./a.out
1
1.2345
309237645

fltnum に代入した時点で fixnum の領域は上書きされるので、fixnum は破壊されていることがわかります。

このまま共用体を配列に格納すると、どのデータ型を格納しているか判別できなくなります。このため、データの種類を識別する方法が必要になります。よく使用される方法がデータに識別子 (タグ : tag) を付加することです。次の図を見てください。

 ┌──┬──────┐
 │タグ│  データ部  │
 └──┴──────┘

 図 : タグ付きデータ

一般には上図のように、データ部の先頭にタグを付加します。この方法はタグの分だけ余分なメモリを必要とすること、タグの判別に時間がかかること、などの欠点があります。現在は高速 CPU と大容量メモリを搭載しているマシンがほとんどなので、これらの欠点はあまり問題にならないでしょう。

タグを付け加えると、プログラムは次のようになります。

リスト : タグ付きデータ (sample105.cpp)

#include <iostream>
using namespace std;

// 列挙
enum {FIX = 1, FLT};

union data {
  int fixnum;
  double fltnum;
};

// データの定義
struct Data {
  int tag;
  data value;
};

// 整数の生成
Data* make_fixnum(int x)
{
  Data* d = new Data;
  d->tag = FIX;
  d->value.fixnum = x;
  return d;
}

// 浮動小数点数の生成
Data* make_fltnum(double x)
{
  Data* d = new Data;
  d->tag = FLT;
  d->value.fltnum = x;
  return d;
}

// データの表示
void print_data(Data* d)
{
  if (d->tag == FIX)
    cout << d->value.fixnum << endl;
  else
    cout << d->value.fltnum << endl;
}

int main()
{
  Data* a[] = {
    make_fixnum(1),
    make_fltnum(1.234),
    make_fixnum(2),
    make_fltnum(5.678),
  };
  for (int i = 0; i < 4; i++)
    print_data(a[i]);
}
$ clang++ sample105.cpp
$ ./a.out
1
1.234
2
5.678

構造体 Data を定義します。この中のメンバ変数 tag がタグを、value が値を表します。value のデータ型は union data なので、値は整数か浮動小数点数になります。関数 make_fixnum は整数を格納する Data を、関数 make_fltnum は浮動小数点数を格納する Data を生成して返します。タグは列挙型を使って宣言しています。

列挙型の宣言には enum を使います。

enum [タグ名] {name0, name1, name2, name3, name4, name5, ... }

C/C++の場合、enum タグ名 で列挙型を宣言します。{ ... } の中の要素を「列挙定数」とか「列挙子」と呼びます。列挙子には整数値が順番に割り当てられます。デフォルトでは先頭の name0 に 0 が割り当てられ、name1 に 1 が、name2 に 2 が割り当てられます。

列挙子の値を指定することもできます。たとえば、name3 = n とすると、name3 の値は n になり、name4 には n + 1, name5 には n + 2 が割り当てられます。なお、定数を定義するだけでよければ、タグ名を省略してもかまいません。列挙子を整数として扱うことができます。

関数 print_data は Data の値を表示します。タグ tag をチェックして、FIX ならば value.fixnum を、そうでなければ value.fltnum を表示します。main では Data へのポインタを格納する配列 a を定義して、要素を make_fixnum と make_fltnum で生成してセットします。あとは、for ループで配列の要素を取り出して、print_data で値を表示します。このように、共用体を使って一つの配列に異なるデータ型を混在させることができます。[*2]

-- note --------
[*2] C++はオブジェクト指向言語なので、実際にはタグや共用体を使うよりも、「継承」と「仮想関数」を使って同様のことを行うことができます。

●無名の構造体/共用体

C++の場合、名前を省略しても構造体や共用体を定義することが可能になりました。次の例を見てください。

リスト : 無名の共用体

// (1) 今までの方法
struct Data1 {
  int tag;
  union data {
    int fixnum;
    double fltnum;
  } value;
 ;

// (2) タグ名を省略する
struct Data2 {
  int tag;
  union {
    int fixnum;
    double fltnum;
  } value;
};

// (3) 変数名も省略できる
struct Data3 {
  int tag;
  union {
    int fixnum;
    double fltnum;
  };
};

(1) は今までの方法で Data を定義しなおしたものです。struct の中で union を定義しています。(2) は無名の共用体を使っています。名前を省略していることに注意してください。無名の共用体はもう一つ便利な機能があって、(3) のようにメンバ変数名も省略することができます。たとえば、Data3 d と宣言した場合、d.fixnum, d.fltnum で共用体のメンバ変数にアクセスすることができます。


初版 2015 年 8 月 30 日
改訂 2023 年 4 月 9 日