Lecture 12 構造体と共用体

 構造体(structure)は,複数のデータを含み,ユーザが定義することで新しいデータ型が作れる機能である. データ(あるいは情報)は,それぞれ何らかの関連があることが多く,この様なデータをまとめて 管理できると大変に都合が良い.例えば,友人の連絡先データベースを作る場合,「氏名」「電話」 「住所」などの情報をまとめて一人分のデータとできれば便利である.Cでは,このような 「複合データ型(aggregate data type)」を, を構造体を用いて作成でき,これを「複合データ構造体(complex data structure)」などと呼ぶ.
 また,C++言語では,この構造体を拡張した「クラス(class)」により, オブジェクト指向という洗練されたプログラミングの概念を導入し, より堅牢で大規模なプログラムを作成することを可能にしている. このように,構造体は,単にデータをまとめるというその機能以上に, より新しいソフトウェアの設計概念を理解する上で重要な機能である. 深く理解するように取り組んでほしい.

構造体

構造体は,メンバと呼ばれる,互いに関連のある2つ以上の変数で構成される複合データ型である. 配列がどの要素も同じ型を持つのに対して,構造体はそれぞれのメンバが異なる型を持つことができる. 構造体を定義する一般的な形式は次の通り,
    struct タグ名 {
        型 メンバ 1;
        型 メンバ 2;
        型 メンバ 3;
        ・・・
        型 メンバ N;
    } 変数名;

例えば,住所録の場合,
    struct AddressRecord {
        char name[40];
        short postal_code1;
        short postal_code2;
        char address[80];
        char phone;
    } card;

Cの場合の説明

Cでは,この例の場合,AddressRecord は「タグ名」で,struct AddressRecord とすると「型名」になる.その後にある,card は struct AddressRecord 型の変数である.
* struct AddressRecord は,int などと同じ型名であって変数ではない.

C++の場合の説明 ... 拡張子が.cppの場合はこちら

C++では,Cと比べて構造体の解釈が拡張され,AddressRecordは「タグ名」ではなくて,「型名」として 使えるようになった.従って,変数(インスタンス)を作るときにも,
    struct AddressRecord card;
ではなくて,
    AddressRecord card;
でよく,typedef を使う必要もなくなった.



上の例は,型の宣言と,変数の宣言を同時に行なっている例であるが,通常はこの2つの宣言を別々に行うことが多い. 例えば,「型」の宣言:
    struct AddressRecord {  // 新しい「型」の宣言だけしている.
        char name[40];      // ヘッダファイルなどで宣言されることが多い
        short postal_code1;
        short postal_code2;
        char address[80];
        char phone_number[20];
    };
は,ヘッダファイル(.h)などで宣言されることが多い.

一方で,変数の宣言:
Cの場合>    struct AddressRecord card;	// cardという変数を宣言している.
C++の場合>  AddressRecord card;
は,ソースコード(.cpp)の中で行われる.
変数の宣言をして初めて,メモリ上に実体を格納する領域が作られる. そのメモリ配置は,
name 40バイト
postal_code1 2バイト
postal_code2 2バイト
address 80バイト
phone_number 20バイト


構造体のメンバにアクセスするには,変数名にピリオド(ドット演算子)をつけ,その後にメンバ名を書く.
    card.postal_code1 = 214;
    card.postal_code2 = 8571;
    strcpy(card.name,"理工太郎");
    strcpy(card.address,"川崎市多摩区東三田1-1-1");
    strcpy(card.phone_number,"044-934-xxxx");
その他の変数の要素へのアクセス方法は,一般の変数と同じである.
    scanf("%d",&card.postal_code1);
    for(int i=0; card.name[i], i++)
    	printf("%c",card.name[i]);

構造体の配列

 先の例では,一人分のアドレスを格納できる構造体 struct AddressRecord を宣言(定義)し, 一人分のデータを格納できる変数 card を一つ作った.だが実際には,アドレス帳には多数の連絡先を 記録したい.例えば,100人分の連絡先を記録するには,
    struct AddressRecord card[100];
を宣言すればよい.このそれぞれの要素にアクセスするには,
    card[i].postal_code1 = 101;
    card[i].postal_code2 = 8301;
    strcpy(card[i].name,"明治二郎");
    strcpy(card[i].address,"東京都千代田区神田駿河台1-1");
    strcpy(card[i].phone_number,"03-3296-xxxx");
などとすれば良い.

構造体へのポインタ

 構造体へのアクセスは,多くの場合,ポインタを用いて行われる.構造体へのポインタの宣言は, その他の変数のポインタの宣言と変わらない.
   struct AddressRecord card[100];
   struct AddressRecord *cardp = card;
このポインタにアクセスするには,アロー演算子を用いる.
    cardp->postal_code1 = 102;
    cardp++;    // ポインタをインクリメント(次のカードへ行く)

ビットフィールド

 構造体の特殊な利用例として,ビットフィールドがある.これはバイトやワードの内部の ビット1つまたは複数のビットにまたがる場所に名前をつけてアクセスすることができる. 機械やロボットの制御を行うときには大変良く用いられる.
   型 名前:サイズ;
例えば以下の例は,学生の年組番号と出欠を2バイトで表した例である.
    struct BitF {
        unsigned grade: 3;  // 1-4年生(最大8通り)
        unsigned class: 4;  // 5/6組 (最大16通り)
        unsigned number:8;  // 1-99番(最大256通り)
		unsigned shusseki:1;// 1:出席,0:欠席
    };
通常なら少なくとも4バイトは使っている(intを使えば16バイト)ところを, 非常にコンパクトに表すことができる. ビットフィールドは更に,機械のスイットのOn/OFFなどを制御するのにも非常に役立つ.

共用体

 共用体は,同じメモリ領域を複数の変数でシェア(共有)するものである.
    union タグ名 {
        型 メンバ1;
        型 メンバ2;
        型 メンバ3;
        ・・・
        型 メンバN;
    } 変数;
例えば以下のように用いる.
   union SharedMemory {
       long int li;
       short    s[2];
       float   f;
       char c[4];
   };
          
   union SharedMemory shm;
この変数 shm は,サイズが4バイトであり,その同じ領域を,longとしても floatとしても, また,shortを2つ,あるいはcharを4つとして利用することができる. 要素へのアクセス方法は、構造体と同じである。


Quiz 12

課題は,数多くのトレーニングを積むことで,個人の実力を養成する目的で出しているものです.その趣旨を理解し「自分の為」になるよう適切に実施しなさい.

以下の問それぞれに対応するプログラムを作成しなさい.
    1. 以下に示す生徒一人分の情報を構造体として作成し,一人一人の内容を表示するプログラムを作成せよ


      解答例 Q12-1


      • 成績レコード型の構造体を作成.main関数の中でインスタンス( =実体のこと.ここではdata[]) を作成して初期化している.構造体もこのように初期化できる.



    2. 上記に加えて平均値と成績を求めるプログラムを作成せよ

      解答例 Q12-2


      • 成績リスト構造体とインスタンスの配列(grade_list[])も作って成績(A-F)と得点のマッピングを決めている. こうすることで,コードの中の if文 には直接数値を書かなくて良くなり, 後からマッピングの関係を変更することも容易になる(16-27行および41-42行).
      • 構造体の配列の中身を初期化することもできる(21-27行)
      • CSVファイルを書き出す関数と,画面に表示する関数は非常に似ていることがわかる. コードを上手に書くと,この2つの関数を一つに集約することもできる(11章参照). 是非トライしてみて欲しい.



    3. 名前,出身地,年齢,性別を格納する構造体を作り,適当なデータを作成して 関数に構造体引数として渡し,関数の中で内容を画面に出力するプログラムを作成せよ.
      // main関数の例
      int main(void)
      {
          AddressBook data[] = {
              "Aoyama Taro", "Tokyo", 20, 'M',
              "Kawasaki Jiro", "Kanagawa", 21, 'M',
              "Morioka Miki", "Iwate", 22, 'F',
              "Kawagoe Shiro", "Saitama", 23, 'M',
              "Narashino Goro", "Chiba", 24, 'M',
              NULL, NULL, 0, 0};
      
          print_data(data);
      }
      

      解答例 Q12-3


      • アドレス帳の構造体を作成,main関数でインスタンス生成と初期化を行なっている. 実際はこの様なレコードデータはファイルにしておき(前の課題のCSVファイルなどがそれに当たる), ファイルから読み込むのが良い.
      • レコードデータの最後に名前の無いダミーデータを挿入して,これをデータの終わりとして判断させている. ちょうど,文字列の最後が0で終わるのと同じ考えである. 【注】 !strcmp(ab[i].name,"") とする代わりに,単に !ab[i].name[0] でもよい (なぜなら空の文字列の場合,先頭には0が入っているからである).



    4. 上記のプログラムに,CSV形式でファイルに保存,および読み込む関数を付加せよ. 少なくとも5名以上のデータを格納したCSVファイルを作り,実行時引数でファイル名を指定して読み込み, キーボードから名前を入力すると該当する人を見つけてそのデータ全体を画面に出力するプログラムを書け.

      解答例 Q12-4



      入力用データベースファイルの例:【address_data.csv】
      • 前の課題12-3に手を加えて作成している.
      • 名前や誕生地で検索できるようにし,対応するデータがなければ繰り返し尋ねるようにしている.
      • ただし,このプログラムでは,同じ名前や同じ誕生地の人がいた場合,最初の人しか検索できない. プログラムを工夫して,同じ名前や同じ誕生地の人すべてが検索結果として表示されるようにしてみよ.



    5. 2次元点群 (x,y) を保持する構造体 point_t を定義せよ.また,以下の3つの関数を作成せよ.
        1. 点の座標を表示する関数
        2. 点群を乱数生成する関数
        3. 点群の重心点を計算する関数
        4. 点群全体を(dx,dy)並行移動する関数
      #include <stdio.h>
      #include <stdlib.h>
      
      struct point_t{
          //ここに記述
      };
      
      void disp(point_t p);
      void initialize(point_t *cloud, int n);
      point_t calc_centroid(point_t *cloud, int n);
      void shift(point_t *cloud, int n, double dx, double dy);
      
      int main(int argc, const char * argv[])
      {
          const int N = 100;
          point_t cloud[N]; // 全点群(ポイントクラウドといいます
                            // を格納する point_t型変数の配列
      
          initialize(cloud,N); // 好きな値に初期化します
      
          point_t centroid = calc_centroid(cloud,N);
             // ここで全点群の重心を計算し結果を返します。
      
          disp(centroid); // 座標を表示します。
      
          shift(cloud, N, 2, 3); // 全点の座標を移動します。
      
          centroid = calc_centroid(cloud,N);
          disp(centroid);
         
          return 0;
      }
      
      【実行例】
      > 12-5
      ( 0.0257288, 0.0266681)
      
      ( 2.0257288, 3.0266681)
      
      

      解答例 Q12-5




Assignment(課題)12

【課題提出における注意】
  • すべてのプログラムは,必ずコンパイル&実行し,正しく機能することをチェックしてから提出すること.
  • プログラムは正しくインデントさせること.


Assignment 12のページ



作成したプログラムを一つのファイル(*.zip)にしてOh-o! Meijiシステムレポート第12回に提出する. 提出するすべてのファイルには,年組番号 氏名を記入すること.締め切りは日曜日よる0時までとする.

【注意】すべてのファイル名は半角のみで構成すること.
【お願い】提出するファイルは,フォルダに入れないでそのまま圧縮してください.