C++言語解説:1-8.構造体と共用体
2002-07-02

[概要] 構造体、Cの構造体との違い、ビットフィールド、共用体について学習します。

[構成]・構造体
     * 構造体とは
     * 構造体のコピー
     * 構造体へのポインタと参照
     * 構造体内部の構造体
   ・ビットフィールド
     * 単一ビットへのアクセス
   ・共用体
     * 共用体とは
     * 無名共用体


構造体

 構造体とは
 
  ・構造体(structure)は、様々なオブジェクトの集合体を指します。配列は同じ型のオ
   ブジェクトが集合したものでしたが、構造体は異なる型のオブジェクトが集合した
   ものです。
  
  ・構造体を構成するオブジェクトはメンバ(member)と呼ばれます。用途にもよります
   が要素やフィールドと呼ばれることもあります。
  
  ・構造体の宣言はList1のように行います。
 
   [List1.構造体の宣言(1)]
   ┌───────────────────────────────────┐
    struct 構造体名 { メンバリスト } 変数リスト;
    
    (例) struct SkiSpec {
         char c_name[40];
         int i_length;
       } skier1;
   └───────────────────────────────────┘
  
  ・各要素の後ろにはセミコロン(;)が付いています。要素の指定にセミコロンを付ける
   のは構造体ぐらいなので忘れないように注意して下さい。
   
  ・変数リストは省略できます。このときは構造体の宣言のみとなるので、後からオブ
   ジェクトを定義
します。
  
   [List2.構造体の宣言]
   ┌───────────────────────────────────┐
    struct SkiSpec {
       char c_name[40];
       int i_length;
    };
    
    SkiSpec skier1; //SkiSpec型構造体 skier1オブジェクトを定義
   └───────────────────────────────────┘
  
  ・また、オブジェクトが1つしか存在しないことが確実な場合、構造体宣言時に構造体
   名を省略できますが、あまり使う機会は無いでしょう。
  
  ・オブジェクトの定義において、いきなり
     SkiSpec skier1;
   と書いていますが、これはC++での記述方法です。C++では構造体を宣言することが
   新しい型を宣言していることと同義
だからです。Cの場合
     struct SkiSpec skier1;
   のようのstructキーワードが定義時も必要です。
 
  ・構造体のメンバへアクセスするには、List3のように構造体オブジェクト名にドット
   を付けます。


   [List3.構造体メンバへのアクセス]
   ┌───────────────────────────────────┐
     skier1.i_length = 170;
     strcpy(skier1.c_name, "1080Mogul");
     cout << skier1.c_name;
   └───────────────────────────────────┘
  
  ・構造体自身を配列にすることもできます。この場合は構造体名に添字を付けて操作
   を行います。

   [List4.構造体配列メンバへのアクセス]
   ┌───────────────────────────────────┐
     SkiSpec skier[4];
     
     skier[1].i_length = 180;
     skier[2].i_length = 170;
     strcpy(skier[1].c_name, "IdOne");
     strcpy(skier[2].c_name, "1080Mogul");
   └───────────────────────────────────┘
  
  ・構造体は、リストやツリーなどの連結情報を持つデータ構造実現や、多くのパラメ
   ータを関数間でやり取りするといった場合によく用いられます。データ構造につい
   ては本テキストの説明範囲外ですが、プログラムの実行速度や効率はアルゴリズム/
   データ構造と密接な関係がある
ので、関連書籍を読む必要があるでしょう。(*1)


 構造体のコピー
 
  ・構造体を関数の引数としたときは値渡しとなります。これが配列とは異なる性質と
   言えます。List5に例を示します。

   [List5.関数へ構造体を渡す]
   ┌───────────────────────────────────┐
    #include <iostream>
    #include <cstring>
    using namespace std;
    
    struct SkiSpec {
       char c_name[40];
       int i_length;
    };
    
    void SubFunc(SkiSpec skier);
    
    int main(){
      SkiSpec skier;
      
      strcpy(skier.c_name, "1080Mogul");
      skier.i_length = 170;
      
      SubFunc(skier);
      
      cout << "main : " << skier.c_name << endl;
      cout << "main : " << skier.i_length << endl;
      
      return 0;
    }
    
    void SubFunc(SkiSpec skier)
    {
      strcpy(skier.c_name, "IdOne");
      skier.i_length = 168;
      
      cout << "sub : " << skier.c_name << endl;
      cout << "sub : " << skier.i_length << endl;
      
      return;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     sub : IdOne
     sub : 168
     main : 1080Mogul
     main : 170
   └───────────────────────────────────┘
  
  ・この例でもわかるように、メンバに配列データがあったとしても値渡し(コピー)
   れていきます。ただし、構造体自身が配列の場合は別です。ある意味、構造体に大
   きなデータを持たせていると、コピーのオーバーヘッドが生じることになります。
   
  ・値渡しで配列データも含めて全てのデータがコピーされるということは、代入演算
   子によるコピーはというと、これも可能です。型が一致していれば、代入演算子に
   よるメンバデータのコピーができます。


   [List6.構造体の代入]
   ┌───────────────────────────────────┐
    SkiSpec skier1, skier2;
    
    skier1.i_length = 170;
      :
    skier2 = skier1; //代入でメンバ値もコピーされる
   └───────────────────────────────────┘
  

 構造体へのポインタと参照
 
  ・構造体は通常の変数と同様に関数間でやり取りを行う場合は値渡しとなるため、
   照渡しを行う場合はポインタを使用
します。
  
  ・構造体のアドレス取得も&演算子です。ポインタを介して構造体メンバへアクセスす
   るにはアロー演算子(->)を使用します。
  
  ・参照の場合は変数と同様に別名として振る舞うのでアロー演算子は必要ありません。

   [List7.構造体へのポインタと参照]
   ┌───────────────────────────────────┐
    #include <iostream>
    #include <cstring>
    using namespace std;
    
    struct SkiSpec {
       char c_name[40];
       int i_length;
    };
    
    void SubFunc1(SkiSpec* skier);
    void SubFunc2(SkiSpec& skier);
    
    int main(){
      SkiSpec skier;
      
      strcpy(skier.c_name, "1080Mogul");
      skier.i_length = 170;
      
      cout << "(0) : " << skier.c_name << endl;
      cout << "(0) : " << skier.i_length << endl;

      SubFunc1(&skier);
      
      cout << "(1) : " << skier.c_name << endl;
      cout << "(1) : " << skier.i_length << endl;
      
      SubFunc2(skier);
      
      cout << "(2) : " << skier.c_name << endl;
      cout << "(2) : " << skier.i_length << endl;

      return 0;
    }
    
    void SubFunc1(SkiSpec* skier) //ポインタ
    {
      strcpy(skier->c_name,"IdOne");
      skier->i_length = 168;
      
      return;
    }

    void SubFunc2(SkiSpec& skier) //参照
    {
      strcpy(skier.c_name,"Assault");
      skier.i_length = 172;
      
      return;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     (0) : 1080Mogul
     (0) : 170
     (1) : IdOne
     (1) : 168
     (2) : Assault
     (2) : 172
   └───────────────────────────────────┘


 構造体内部の構造体
 
  ・構造体のメンバに構造体が含まれる場合、ドット演算子(.)をつなげてメンバへアク
   セス
します。
  
  ・また、ポインタで参照渡しした場合ですが、アロー演算子は構造体ポインタが呼び
   出すものなので、内部の構造体はドット演算子を使用します。

   [List8.構造体内部の構造体]
   ┌───────────────────────────────────┐
    #include <iostream>
    #include <cstring>
    using namespace std;
    
    struct CurveSpec {
      int i_radius;
      int i_weight;
    };

    struct SkiSpec {
      char c_name[40];
      int i_length;
      CurveSpec cspec; //構造体中の構造体
    };
    
    void SubFunc(SkiSpec* skier);
    
    int main(){
      SkiSpec skier;
      
      strcpy(skier.c_name, "1080Mogul");
      skier.i_length = 170;
      skier.cspec.i_radius = 25;
      skier.cspec.i_weight = 60;
      
      SubFunc(&skier);
      
      cout << "main : " << skier.c_name << endl;
      cout << "main : " << skier.i_length << endl;
      cout << "main : " << skier.cspec.i_radius << endl;
      cout << "main : " << skier.cspec.i_weight << endl;
      
      return 0;
    }
    
    void SubFunc(SkiSpec* skier) //ポインタ
    {
      skier->cspec.i_radius = 20;
      skier->cspec.i_weight = 55;
      
      return;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     main : 1080Mogul
     main : 170
     main : 20
     main : 55
   └───────────────────────────────────┘


ビットフィールド

 単一ビットへのアクセス
 
  ・C/C++には1ビット単位でデータへアクセスする機能があり、それはビットフィール
   ド(bit field)
と呼ばれます。構造体メンバ変数名の後ろに「:ビット幅」を付ける
   ことでアクセスビット幅が決まります。ビットフィールドは構造体メンバの一種な
   のです。
  
  ・ビットフィールドのメンバはsigned int / unsigned int / bool / enumとして宣言
   する必要があります。幅1bitのデータについては符号を持てないのでunsigned /
   bool
にしなければいけません。
  
  ・Cではboolが無いので、従来からのコード通常はunsignedとなります。intも省略
   (*2)する傾向にあるのでビットフィールドの宣言は
     struct 型名 {
       unsigned 変数名:ビット幅;
          :
     };
   の書式になることが多いようです。
  
   [List9.ビットフィールド使用例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    struct Bitaccess {
      unsigned b_d1:1;
      unsigned b_d2:1;
      unsigned b_d3:1;
      unsigned b_d4:1;
    };

    int main(){
      Bitaccess x;
      
      x.b_d1 = x.b_d3 = 1;
      x.b_d2 = x.b_d4 = 0;
      
      cout << x.b_d1 << " ";
      cout << x.b_d2 << " ";
      cout << x.b_d3 << " ";
      cout << x.b_d4 << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
  
  ・List9はあまり良い例ではありません。ビットフィールドは低レベルI/Fの制御とし
   て後述の共用体とセットで使用するケースが多く、あるデータのbitを直接書き換え
   るような用途を想定しています。システム制御系では多い使い方と言えます。
  
  ・ポインタのアドレスはバイト単位なので、ビットフィールドのメンバにはアドレス
   の概念がありません。
また添字によるアドレス計算もできないので配列もありませ
   ん。
  

共用体

 共用体とは
 
  ・共用体(union)は構造体の1種ですがメンバは全て同じアドレスを共有することが特
   徴です。より少ないメモリ環境を有効利用したい場合に用います。
  
  ・例えば
   ┌───────────────────────────────────┐
     struct Calc {
       int i_type;   //データ型判別
       double d_result; //doubleデータ用
       int i_result;  //intデータ用
     };
   └───────────────────────────────────┘
   の構造体があったとします。そしてi_typeによって
   ┌───────────────────────────────────┐
     Calc cal;
     
     cal.i_type = 1; //double
     if (cal.i_type) cal.d_result = 10.5;
     else cal.i_result = 10;
   └───────────────────────────────────┘
   の使い分けをしたいと想定します。
  
  ・この場合、d_resultとi_resultが同時に使用されることは無いので、メモリスペー
   スが無駄
になっています。このとき共用体をList10のように使用します。

   [List10.共用体の使用例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    union Result { //d_resultとi_resultは同じアドレスを共有
      double d_result;
      int i_result;
    };
    
    struct Calc {
      int i_type;
      Result v;
    };
    
    int main()
    {
      Calc cal;
      
      cin >> cal.i_type;
      
      if (cal.i_type) {
        cal.v.d_result = 10.5;
        cout << cal.v.d_result;
      }
      else {
        cal.v.i_result = 10;
        cout << cal.v.i_result;
      }
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:\tmp>prog
     0
     10
     C:\tmp>prog
     1
     10.5
   └───────────────────────────────────┘


 無名共用体
 
  ・List10の例では、共用体を入れたために、共用体内のメンバ指定を追加する必要が
   出てしまいました(cal.v.d_resultのvの部分)。
ただし、単一変数(オブジェクト)で
   あれば共用体は変数(オブジェクト)名を省略
できます。これを無名共用体
   (anonymous union)
と呼びます。
  
  ・無名共用体の場合、メンバをそのまま指定することができます。List11に使用例を
   示します。

   [List11.無名共用体の使用例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    struct Calc {
      int i_type;
      union {  //無名共用体
        double d_result;
        int i_result;
      };
    };
    
    int main()
    {
      Calc cal;
      
      cin >> cal.i_type;
      
      if (cal.i_type) {
        cal.d_result = 10.5; //メンバ直接指定
        cout << cal.d_result;
      }
      else {
        cal.i_result = 10;  //メンバ直接指定
        cout << cal.i_result;
      }
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:\tmp>prog
     0
     10
     C:\tmp>prog
     1
     10.5
   └───────────────────────────────────┘
  
  ・構造体を使う側から見れば、List11は通常の構造体向けのコーディングと何も変わ
   りません。
しかし構造体内部で無名共用体を用いることで、外側のコーディング変
   更無しにメモリスペースの節約が実現されており、これは低レベルな多態性と言え
   ます。
  
  ・今後のテキストで説明する内容ですが、注意点として共用体のメンバにコンストラ
   クタ、デストラクタ、コピーコンストラクタ(代入演算子オーバーロードに付随)を
   持つクラスを入れることはできません。


(*1)データ構造はアルゴリズムと密接な関連があります。連結をポインタで示す際、デー
  タの本体+ポインタという形で構造体が多用されます。また、これらのコーディング
  作業を軽減するためにSTL(Standard Template Library)が生み出されたとも考えられ
  ます。言語知識以外の領域ですが、言語のfeaturingを決める理由とも言える部分なの
  で、これについては別途学習をお勧めします。

(*2)unsigned, short, longの型修飾子がある場合、intについては型名を省略できます。

[Revision Table]
 |Revision |Date    |Comments
 |----------|-----------|-----------------------------------------------------
 |1.00   |2002-07-02 |初版
 |1.01   |2002-07-06 |リンク追加
[end]
Copyright(C) 2002 Altmo