こんにちは。田原です。

C++は高度で複雑な処理をサポートするため、①データを塊としてまとめ、②それぞれのデータに「振る舞い」を定義できる仕組みを持っています。これにより①データを段階的に細分化し、かつ、②それぞれの階層の管理をデータ自身の設計書に任せることで、メンテナンスしやすいプログラムを開発することができるのです。今回は、その①の基礎について解説します。

 

1.構造体

C++には複数のデータを一纏めにして取り扱えるようにする仕組みが複数あります。
以下の4つです。

1) class
2) struct
3) union
4) 配列

  • classとstructはほぼ同じです。
    そして、classやstructの最大の利点は以下の2つです。
    1) 様々な型の変数に名前を付けて包含できます。
    2) それらを管理する関数(メンバー関数)を定義できます。
    更に、データの隠蔽や振る舞いの抽象化(異なるものを同じI/Fで取り扱う)等の高度な機能もサポートしてます。

  • unionは使い方が非常に難しい特殊なstructです。
    異なる型の変数を同じメモリに割り当てる型です。これは安易に使うとC++の型システムのメリットを無効にしかねない機能です。それを使いこなすためには、C++の型システムの理解、メモリ管理の理解等C++の高度な機能を把握しておく必要があり入門レベルではありません。また多くのケースでポリモーフィズムによりunionの機能をカバーできます。(ポリモーフィズムはC++の重要機能の1つですので後日解説します。)ですので、当入門講座ではunionを扱わないことにします。

  • 配列は同じ型のデータをまとめたものです。
    配列に含まれる各データをインデックス番号で指定します。

今回はこのstructの「1. 名前を付けて様々な型の変数を包含」する部分とコンストラクタについて解説します。
 

1-1.構造体の定義方法

「名前を付けて様々な型の変数を包含」は簡単です。
下記のようにstruct 名前と書いて{}ブロックの間にメンバ変数のリストを記述します。
そして、{}のブロックの最後に ;(セミコロン)を書きます。(このセミコロンはたいへん忘れやすいです。忘れると意味不明なエラーになる場合がありますのでご注意下さい。)

01
02
03
04
05
06
struct 名前
{
    型 メンバ変数名;
    型 メンバ変数名;
       
};

 

1-2.メンバ変数のアクセス方法(ドット演算子)

各メンバ変数へアクセスする時は、下記のように .(ドット演算子)を使ってアクセスします。

01
構造体の変数名.メンバ変数名

注意点は、構造体の変数を定義してから、その変数名.メンバ変数名でアクセスすることです。
構造体名.メンバ変数名ではアクセスできません。(下記の例ではFoo.mData0はできません。)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
#include <iostream>
 
struct Foo
{
    int     mData0;
    int     mData1;
};
 
int main()
{
    Foo foo;
    foo.mData0=123;
    foo.mData1=456;
    std::cout << foo.mData0 << std::endl;
    std::cout << foo.mData1 << std::endl;
    return 0;
}

値など修正して色々試してみる(Wandbox)

2017年4月7日「1-2.メンバ変数のアクセス方法(ドット演算子)」の解説を追加しました。

 

1-3.メンバ変数のアクセス方法(アロー演算子)

構造体へのポインタを使って各メンバ変数へアクセスする時は、下記のように ->(アロー演算子)を使ってアクセスします。

01
構造体へのポインタ->メンバ変数名

ポイントはポイント先をきちんと確保し、かつ、ポイント先のアドレスを設定することです。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
 
struct Foo
{
    int     mData0;
    int     mData1;
};
 
int main()
{
    Foo foo;
 
    Foo* foo_ptr=&foo;
    foo_ptr->mData0=789;
    foo_ptr->mData1=101112;
    std::cout << foo_ptr->mData0 << std::endl;
    std::cout << foo_ptr->mData1 << std::endl;
 
    return 0;
}

値など修正して色々試してみる(Wandbox)

2017年5月14日「1-3.メンバ変数のアクセス方法(アロー演算子)」の解説を追加しました。

 

1-4.コンストラクタについて

ここまではC言語の構造体と同じですが、ここでC++らしく初期化関数(コンストラクタ)について簡単な部分を説明します。コンストラクタは構造体の領域を確保した後、自動的に呼び出される関数です。
コンストラクタを構造体内で定義する場合は下記のように記述します。(構造体の外で定義することも可能ですが、それについてはクラスの解説時に説明します。)

01
02
03
04
構造体名(仮引数リスト) : 初期化子リスト
{
    実行文;
}

「名前」は構造体の名前です。
「仮引数リスト」は関数と同じです。
「初期化子リスト」は、各メンバ変数とその初期値を ,(カンマ)で区切ったものです。
コンストラクタの0個以上の実行文を{}ブロック内に記述できます。

初期化子リスト:
実行文に記述した方が慣れた形式で書けますし、自由度も高いです。ですので、初期化子リストを使い難いと感じる人もいると思います。しかし、一般に初期化子で初期化した方が高速です。また、初期化子でないと初期化できないメンバもあります。ですので、できるだけ初期化子リストに慣れておくことをお勧めします。

デフォルト・コンストラクタ:
コンストラクタの内、仮引数リストを全て省略した時に呼び出されるコンスラクタのことを「デフォルト・コンストラクタ」と呼びます。これは実引数を指定せずに呼び出すことができるため、コンパイラが必要に応じて自動的に呼び出すことが可能なため、特別扱いされるものの1つです。

 

1-5.サンプル・プログラム
struct.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
 
struct Foo
{
    int     mData;  // member variable
 
    // constructor
    Foo(int iData) : mData(iData)
    {
        std::cout << "Foo(" << iData << ")\n";
    }
};
 
struct Bar
{
    int     mData;  // member variable
    Foo     mFoo;   // member variable
 
    // constructor
    Bar() : mData(0), mFoo(456)
    {
        std::cout << "Bar()\n";
    }
};
 
int main()
{
    Bar bar;
 
    std::cout << "\n";
    std::cout << "bar.mData       = " << bar.mData << "\n";
    std::cout << "bar.mFoo.mData  = " << bar.mFoo.mData  << "\n";
 
    bar.mData=123;
 
    std::cout << "\n";
    std::cout << "bar.mData       = " << bar.mData << "\n";
    std::cout << "bar.mFoo.mData  = " << bar.mFoo.mData  << "\n";
 
    return 0;
}

main()関数の先頭で、構造体Bar型の変数barを定義しています。
この時、bar用のメモリが通常のローカル変数と同様にスタック上に確保され、続けてBarのコンストラクタが呼び出されます。構造体Barのコンストラクタは以下のように処理されます。

  1. mDataを0で初期化
  2. mFooのコンストラクタを呼び出して456を渡す
  3. std::cout << "Bar()\n"実行

つまり、初期化子による初期後にコンストラクタ(初期化関数)の実行文が処理されます。

CMakeLists.txt
01
02
03
04
05
06
07
08
09
project(struct)
 
if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()
 
add_executable(struct struct.cpp)
実行結果:
01
02
03
04
05
06
07
08
Foo(456)
Bar()
 
bar.mData       = 0
bar.mFoo.mData  = 456
 
bar.mData       = 123
bar.mFoo.mData  = 456

値など修正して色々試してみる(Wandbox)

初期化子で使うのは()? or {}?
FooとBarのそれぞれのコンストラクタでそれぞれのメンバ変数mDataを初期化しています。その時、mData(iData)のように()を使っています。C++11にて()の代わりに{}も使えるようになってます。ほとんどの場合はどちらを使っても同じ意味になるのですが、稀に意味が異なる時があります。その場合は使い分けしますが、頻繁に遭遇する問題ではないので、今は忘れていてよいと思います。
Qiitaの記事「C++11 Universal Initialization は、いつでも使うべきなのか」はこの問題が比較的分かりやすく解説されています。(とは言え、そもそも難しい話なので簡単ではないですが。)

 

2.enum型

enum型とは、整数値を割り当てたシンボルを列挙(enumeration)して定義する型です。
C++11にてscoped enumが追加されましたので、現在は大きく2種類あります。

  • unscoped enum
  • scoped enum

 

2-1.unscoped enum

C++11以前からあるenum型とほぼ同じです。unscoped enumと呼ばれます。
enum型はシンボルを並べて定義してシンボルに値を割り当てます。

01
02
03
04
05
06
07
08
enum 名前 : 基底型
{
    シンボル0,
    シンボル1,
    シンボル2=値,
    シンボル3,
       
};

先頭のシンボルの値は0から始まります。そして、並び順で1ずつ増えていきます。
=を用いて、そのシンボルの値を明示的に指定することもできます。
更に、定義するenum型のシンボル値として使える範囲を明示的に定義するため「基底型」を指定できます。これには整数型を指定できます。
基底型は省略可能です。unscoped enumの場合、全てのシンボル値を表現できるような型が選択されますが、具体的な型は処理系依存です。

例えば、下記のように定義します。

01
02
03
04
05
06
07
08
09
enum Foo
{
    none,        // 0
    symbolA,     // 1
    symbolB,     // 2
    number1=10,  // 10
    number2,     // 11
    number3      // 12
};

なお、unscoped enumの各シンボルは整数型へ暗黙的に変換されます。逆に整数型は暗黙的にはunscoped enumへ変換されません。明示的にキャストする必要が有ります。

値など修正して色々試してみる(Wandbox)
 

2-2.scoped enum

C++11でscoped enumが追加されました。

1.unscoped enumでは各シンボルは、そのenum型の名前空間で定義されます。
そのため、noneのようによく用いるシンボルは多重定義エラーになりやすいです。
例えば、下記のような定義を行うと、noneが2重定義エラーとなります。

01
02
03
04
05
06
07
08
09
10
11
12
13
enum Foo
{
    none,        // 0
    symbolA,     // 1
    symbolB      // 2
};
 
enum Bar
{
    none=10,     // 10
    bar1,        // 11
    bar2         // 12
};

2.unscoped enumは暗黙的に整数型へ変換されるため、バグが隠れやすいと言われてます。
異なるunscoped enum同士の代入はできないので、enumシンボル値を使って足し算などの計算を行わない限りバグは起きにくいと思いますが、計算で済ませる誘惑に駆られやすい点が問題かも知れません。

これらの問題に対処するenum型がscoped enum型です。
使い方はunscoped enum型とほぼ同じです。
ただし、基底型を省略した時の振る舞いがunsoped enum型と異なります。scoped enumは基底型を省略するとint型になります。

01
02
03
04
05
06
07
08
enum 種別 名前 : 基底型
{
    シンボル0,
    シンボル1,
    シンボル2=値,
    シンボル3,
       
};

「種別」には、class、もしくは、structと書きます。どちらを書いても意味は全く同じです。
scoped enumを使えば下記がコンパイルに通るようになります。

01
02
03
04
05
06
07
08
09
10
11
12
13
enum class Foo
{
    none,        // 0
    symbolA,     // 1
    symbolB      // 2
};
 
enum class Bar
{
    none=10,     // 10
    bar1,        // 11
    bar2         // 12
};

それぞれのシンボルを使う時は、Foo::noneやBar::noneとして使います。
また、これらのシンボル値は暗黙的に整数型変換されませんので、unscoped enumと違って計算やstd::coutへの出力がそのままではできません。

Wandboxで見てみる

計算はバグの元なので避けた方が良いのですが、std::coutへお手軽に出力できないのはデバッグが不便です。簡単なツールを公開してますのでよろしければお使い下さい。

3.まとめ

今回は、構造体とそのコンストラクタ、および、enum型について解説しました。本文で解説したようにstructはclassとほぼ同じです。つまり、オブジェクト指向プログラミングを行うための各種の仕組みも備えています。その解説は後日行います。

さて、次回はデータ構造を構築するための基礎の残りの配列、ポインタをメインに解説します。
データ構造を表現する際に、最も理解し辛いものは、構造体の配列へのポインタや構造体へのポインタの配列と思います。これらについて図を書きながら解説します。お楽しみに。