C++/構造体・共用体

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++
ナビゲーションに移動 検索に移動

構造体[編集]

「構造体」とは、関連性のある、いくつもの変数を、ひとまとまりに、あつかうための機能のひとつです。

C言語にも「構造体」機能がありますが、C++の構造体は、若干、機能が違います。


さて、車が、3台あったとしましょう。そして、山田くん、伊藤くん、中野くんが、それぞれ車を1台ずつ持っているとします。 ナンバープレート番号がそれぞれ、

1台目の、山田くんちの車(変数名:yamadacar)のナンバープレート番号が「1234」だとしましょう。
2台目の、伊藤くんちの車(変数名:itoucar)のナンバープレート番号が「7722」だとしましょう。
3台目の、中野くんちの車(変数名:nakanocar)のナンバープレート番号が「8181」だとしましょう。

このような場合、itoucar.bangouという表記で、2台目の車のナンバープレート番号「7722」の事を表現できます。

あるいは yamadacar.bangouという表記なら、これは yamadacar のナンバープレート番号を表現しています。

このような記法が、構造体をもちいた場合の記法です。(どうやって、「構造体」の機能を利用できる状態にするかは、あとの節で説明します。)

「yamadacar」と「bangou」の間にある「.」は、ドット記号「.」です。

この派生元のデータ「yamadacar」は、構造体の型名というものです。

そして、派生先のデータ「bangou」は、構造体のメンバというものです。


このように、あるデータをもとにして(例の場合は「車」)、別の変数が派生して存在するとき(例の場合は「ナンバープレート番号」)、構造体やクラスを使うと、コード内にある階層性のあるデータと、そのデータから派生した変数とを、階層化できます。

そして、構造体をもちいてデータと変数を階層化することにより、コード内容を把握しやすくなります。

手順[編集]

※ 前節のように、「車」で説明するので、先に前節をお読みください。

手順として、まず、クラスとメンバとの関係を定義させるため、次のようにコードを書きます。

そして、「yamadacar」も「itoucar」も「nakanocar」も、すべて車なので、「kuruma」という名の構造体に所属させます。

#include <iostream>
using namespace std;

struct kuruma{
        int bangou;
        double nenryou;
        double soukou;
};

int main()
{
    struct kuruma yamadacar; // 構造体変数の作成
    yamadacar.bangou = 1234;
    yamadacar.nenryou = 30;

    struct kuruma itoucar;
    itoucar.bangou = 7722;
    cout << "山田くんちの車の番号は" << yamadacar.bangou << endl;
    return 0;
}


解説

まず、構造体は、

struct 構造体タグ名 {
        1 変数名1; // これがメンバの定義である
        2 変数名2; // これもメンバの定義
        // 以下略
};

のように定義されます。

この定義の時点では、まだ具体的な値は決まっていないです。たとい具体的な値が無くても、構造体を定義する事が可能です。


さて、構造体を利用する際には、「構造体変数」といわれる変数を作成する必要があります。

構造体変数の作成は、上記コードの

struct kuruma yamadacar

のように、

struct 構造体タグ名 変数名

のような書式により、構造体タグと各変数との関係を定義します。

このように、あらかじめ、kurumaという構造体タグとその内容(メンバ)を定義したあとで、kuruma yamadacar;と宣言することにより、yamadacarを構造体kurumaの変数として定義できます。

実はC++では、

kuruma yamadacar

のように、構造体変数の作成の際に struct を省略しても、構いません。


さて、以降の解説の都合のため、main関数を再掲します。

// 再掲(抜粋)

int main()
{
    struct kuruma yamadacar; // 構造体変数の作成
    yamadacar.bangou = 1234;
    yamadacar.nenryou = 30;

    struct kuruma itoucar;
    itoucar.bangou = 7722;
    cout << "山田くんちの車の番号は" << yamadacar.bangou << endl;
    return 0;
}
上記のコードの場合なら、
・山田くんちの車が「yamadacar」で、そのナンバープレート番号(変数名:「bangou」)が文字列"1234"です。
山田くんちの車の燃料量(変数名:「nenryou」)が30リットルです。
山田くんちの車の走行距離(「soukou」)が、6000kmです。

・いっぽう、伊藤くんちの車の番号は、「7722」です。

オブジェクト定義のこのような定義の記法については、構造体名を、まるで新しい型のように解釈すると、あたかも変数の宣言と似ていることに、注目してください。このため書籍によっては、クラスについての説明で、「変数に対する型のようなもの」のような解説をしている書籍もあります。

ともかく、上記のコードのようにして、それぞれのオブジェクトをクラスに所属させる事ができます。

よって、まとめると、まず、

struct kuruma{
   int bangou;
};

int main()
{
   struct kuruma yamadacar;  
}

のように、記述する必要があります。


そして、struct kuruma yamadacar; 以降に、

   yamadacar.bangou = 1234;

と記述すれば、各構造体変数のメンバに、具体的な数値を代入できます。(上記のコードの例の場合は値1234を代入している。)

まとめると、

struct kuruma{
   int bangou;
};

int main()
{
   struct kuruma yamadacar;
   yamadacar.bangou = 1234;
}

のように、記述する必要があります。このように、オブジェクトの定義よりも先に、クラス名といっしょにメンバが定義されます。

つまり、構造体を利用するために行う定義・宣言の順序は、

1番目: 構造体名の定義、および、構造体の構造としてメンバを定義。
2番目: それぞれの構造体変数の定義。(その構造体変数を、対応する構造体名と関連づける方法で、構造体変数の構造を定義する。)
3番目: それぞれの構造体のメンバへの、数値の代入など。

という順番になります。

メンバについて言うと、具体的な個別のデータとしてのメンバについては、メンバはオブジェクトから派生したものになりますが、しかし、メンバの定義の時では、メンバは構造体の構造として記述したものになります。

そして、オブジェクトを定義する際に、そのオブジェクトが、どの構造体名の構造体から派生されたのかを宣言することにより、構造体変数の構造を定義します。

こうすることにより、もしも構造体変数が何個も大量にあるときに、構造体変数の定義の手間を、単に構造体の型に当てはめるだけで構造体変数を定義できるので、構造体変数の定義の手間を簡略化できます。

コード例[編集]

次に示すコードは、実際に動作するコードです。

#include <iostream>
using namespace std;

struct kuruma{
        int bangou;
        double nenryou;
};

int main()
{
    kuruma yamadacar;
    yamadacar.bangou = 1234;
    yamadacar.nenryou = 30;

    kuruma itoucar;
    itoucar.bangou = 7722;
    cout << "山田くんちの車の番号は" << yamadacar.bangou << endl;
    return 0;
}

上記のコードをコンパイルして実行すると、

山田くんちの車の番号は1234

と表示します。


複数個のメンバの場合[編集]

構造体のメンバは、複数個あっても、かまいません。

まず、前節までに作成したコードを再掲します。

struct kuruma{
   int bangou;
};

メンバとは、上記のコードでは、「bangou」がメンバです。

では、複数個のメンバがある場合を考えます。

クラスのメンバとして、さらに、ある車の燃料の量(変数名「nenryou」)があったとしましょう。

そのような場合、コードは単に、

struct kuruma{
   int bangou;
   double nenryou;
};

このように定義するだけです。

メンバ関数[編集]

(C++でない)標準C言語の構造体は、関数を所有することができないです。

しかし、C++の構造体は、関数を所有できます。また、構造体だけでなくクラスも、関数を所有できます。

構造体やクラスの所有する関数のことを「メンバ関数」と言います。

コード例
#include <iostream>
using namespace std;

struct kouzou {
  // ↓ これがメンバ関数
  int kansu(int a, int b) { 
    return a + b;  
  }

};

int main() {
  struct kouzou khen; // 構造体変数の作成. khen とは「構造kouzouの変数hensu」のつもり
  int result = khen.kansu(10, 3); 

  printf("a+bの値は%d\n", result);
}
出力結果
a+bの値は13


書式は単に、構造体の定義ブロックの中で、普通に関数を宣言すればいいだけですが、しいて構文を記述するなら、

struct 構造体タグ名 {
  戻り値の型 関数名(引数) { 
    処理内容  
  }
};

のようになります。


メンバ関数へのアクセス方法も、(関数でなく「変」数の)メンバ変数のアクセス方法と同様に、

まず構造体変数を作成し、

struct 構造体タグ名 構造体変数名;

の書式で記述し、(なお、冒頭のstructは省略できる。だが読み易さを考えて、記述したほうがイイだろう。)

そして ドット記号(.)を使った記法で

構造体変数名.メンバ関数

によってアクセスすればいいだけです。

※ 市販の書籍では、クラスのメンバ関数ばかり紹介していたりしますが、構造体でもメンバ関数は利用が可能です。むしろ、単にメンバ関数を利用したいだけなら、構造体でメンバ関数を作成するほうが便利です。

メンバ関数での構造体変数の省略[編集]

メンバ関数の作成において、C++では構造体変数の作成を省略できますが、メンバのアクセス時の手法が、ドット「.」でなく「::」に変わります。

また、static 宣言が必要です。 メンバ関数でも同様です。

#include <iostream>
using namespace std;

struct kouzou {
  static int kansu(int a, int b) { 
    return a + b;  
  }
};

int main() {
  int result = kouzou::kansu(10, 3); 

  printf("a+bの値は%d\n", result);
}
出力結果
a+bの値は13

発展的な話題[編集]

関数内でのメンバ関数の定義[編集]

基本[編集]

構造体のメンバ関数の機能の応用として、関数の中で、別の関数を定義する事に流用できる。

なぜなら、

C++の構造体およびクラスでは関数を「メンバ関数」として内部に所有することができ、
そして関数の中で構造体を宣言する事ができるので、

結果的にC++では、C言語には無い、関数の中で関数を定義できる機能をもつことになる。


コード例
#include <iostream>
using namespace std;

int main()
{
    // ↓ これが関数内の関数
    struct Localkansu {
        static void kansu(){
            printf("aaa\n");
        }
 
    };

    // ↓ 構造体変数の作成を省略したので、「.」ではなく「::」でアクセスです
    Localkansu::kansu();
    Localkansu::kansu();

}
実行結果
aaa
aaa
(※ コンパイラは gcc-c++ でも clang++ でも同様の結果。2020年6月24日にFedora32上で確認。以降のコードも同様。)
(※ Windows の Visual Studio 2019 でも同様に動く。)

関数内関数には 上記コードのように static キーワードが必要である。static が無いとエラーになり、コンパイルできない。

クラス宣言 class でも同様の事ができるが、class宣言だとpublicとかprivateだのと言ったアクセス権限の設定が面倒なので、単に関数内で別の関数を定義したい場合には、構造体で定義するほうがラクである。

変数の寿命[編集]

ローカル関数[編集]

メンバ関数で変数を宣言するとき、 int など宣言子をつけて変数宣言することで、その変数はそのメンバ関数だけで有効なローカル変数にできます。

コード例
#include <iostream>
using namespace std;

int a = 25;

void nanika(){
    struct Localkansu {
        static void kansu(){
            int a = 4;
            printf("ローカル関数内でaの値は%d\n", a);
        }
    };

    Localkansu::kansu();
    printf("ローカル関数のとなりではaの値は%d\n", a);
}

int main()
{
    a = 1;
    nanika(); 

    printf("main関数内でaの値は%d\n", a);

}
実行結果
ローカル関数内でaの値は4
ローカル関数のとなりではaの値は1
main関数内でaの値は1

このように、int などをつけて宣言すれば、メンバ関数の中でだけ有効になり、ローカル変数として振舞います。


関数内グローバル関数[編集]

いっぽう、int をつけなくても実行できますが、グローバル領域にアクセスして同名のグローバル変数を上書きします。

コード例
#include <iostream>
using namespace std;

int a = 25;

void nanika(){
    struct Localkansu {
        static void kansu(){
            a = 4; // int が無い
            printf("ローカル関数内でaの値は%d\n", a);
        }
    };

    Localkansu::kansu();
    printf("ローカル関数のとなりではaの値は%d\n", a);
}

int main()
{
    a = 1;
    nanika(); 

    printf("main関数内でaの値は%d\n", a);

}
実行結果
ローカル関数内でaの値は4
ローカル関数のとなりではaの値は4
main関数内でaの値は4
(※ コンパイラは gcc-c++ でも clang++ でも同様の結果。2020年6月24日にFedora32上で確認。以降のコードも同様。)
(※ Windows の Visual Studio 2019 でも同様に動く。)

int を付けない場合、実行結果の数値がすべて「4」で上書きされている事に注目してください。


operator() オブジェクト[編集]

基本[編集]

operator宣言という、演算子を拡張する機能をもつ命令を使うと、

下記のようにメンバ関数の名前を作成せずに、アクセス時に構造体変数の名前だけでアクセスできる。

C++では、この手法は「関数オブジェクト」と言われる。

operatorという予約語を使う。

下記コードにある「operator()」は、記載の場所があたかも関数名っぽいが、(関数名ではなく)宣言子である。

operator だけで宣言であり、規格上は、つづく「()」をoperator 命令によって拡張している。


コード例
#include <iostream>
using namespace std;

struct kouzou {
  int operator() (int a, int b) { 
    return a + b; 
  }
  
};

int main() {
  struct kouzou khens; // いつもどおり構造体変数の作成
  int result = khens(10, 3); //  右辺がメンバにアクセスしてない事に注目

  printf("a+bの値は%d\n", result);
}
出力結果
a+bの値は13
(※ コンパイラは gcc-c++ でも clang++ でも同様の結果。2020年6月24日にFedora32上で確認。以降のコードも同様。)
(※ Windows の Visual Studio 2019 でも同様に動く。)

当然ですが、1つの構造体につき、operator() 宣言は1つまでしか使えないです。(2回以上使うと、コンパイル時にエラーになります。operator() のオーバーロード(多重呼び出し)などの理由で、エラーになります。)

operator()演算子で関数を定義すれば、構造体のメンバ関数の命名の手間が減りますが、しかし構造体そのものの命名の手間は残ります。


一般のメンバ変数を、operator() 演算子の関数のとなりに記述するのは可能です。

コード例
#include <iostream>
using namespace std;

struct kouzou {
  int operator()(int a, int b) { return a + b; }

  int nanika;
};

int main() {
  struct kouzou khen; // 構造体変数の作成
  int result = khen(10, 3);

  printf("a+bの値は%d\n", result);

  khen.nanika = 3;
  printf("nanikaの値は%d\n", khen.nanika);
}
出力結果
a+bの値は13
nanikaの値は3

operator による関数内関数[編集]

operator () 宣言子によって、メンバ関数名を省略しても、関数内関数として利用する事は可能です。

ただし、このoperator宣言は、演算子を拡張する機能をもつ命令であり、() という演算子を拡張していますので、あまり多用しないほうが安全でしょう。

コード例
#include <iostream>
using namespace std;


int main() {

  struct kouzou {
    int operator() (int a, int b) { 
      return a + b; 
    }
  };

  struct kouzou khen; // 構造体変数の作成
  int result = khen(10, 3);

  printf("a+bの値は%d\n", result);

}
出力結果
a+bの値は13
(※ コンパイラは gcc-c++ でも clang++ でも同様の結果。2020年6月24日にFedora32上で確認。以降のコードも同様。)
(※ Windows の Visual Studio 2019 でも同様に動く。)

ただし、原理的にはコレで動くものの、やや技巧的すぎるので、あまり多用しないほうが良いかもしれません。

なぜなら、仕事は一般に集団作業ですので、同僚など他の協力者が習得しづらい技法の利用は、メンテナンス性を低下させるので、なるべく利用をさけるべきだからです。