共用体

共用体(union)

共用体はひとつの変数に複数の型の意味を持たせるための仕組みである。 宣言は構造体のそれに似た以下の書式となる。

union タグ {
  メモリ共用する変数宣言の並び(メンバ)...
};

具体例を示す。

union-simple.c

#include <stdio.h>
#include <string.h>

union US {
  int i;
  float f;
  char chr[10];
};

int main() {
  union US uv;
  union US fv;
  strncpy(uv.chr, "foo!", sizeof uv.chr);
  printf("文字列 %s の2進並びは,\n", uv.chr);
  printf("10進数で %d です。\n", uv.i);
  fv.f = 3.14159265;
  printf("float %f の2進並びは,\n", fv.f);
  printf("10進数で %ld です。\n", fv.i);
  return 0;
}

上記プログラム中にある

union US {
  int i;
  float f;
  char chr[10];
};

が共用体宣言で,これにより union US で宣言した変数は int でも float でも chr[] でもアクセスできる。どの型でアクセスするかは union宣言中のメンバ変数名で決まる(リスト参照)。

実用上は,宣言と同時にunionの型を typedef で定義することが多い。

union-typedef.c

#include <stdio.h>
#include <string.h>

typedef union {
  int i;
  float f;
  char chr[10];
} US;

int main() {
  US uv;
  US fv;
  ...以下略...
}

実際には以下に示すように, 複数の構造体を切り替えて使う場合に用いることが多い。

構造体値の保存

数学と英語の得点を集計するプログラムが実用段階に入ったとする。

 struct tokuten {
   char name[20];		/* 最大20バイトで足りるかな */
   int  shusseki;		/* 出席番号は int */
   int  mathpt;			/* 数学の得点も int */
   int  engpt;			/* 英語の得点も int */
 };				/* セミコロンが必要!! */

このように定義された構造体に代入された値を、 次回の処理で利用できるようファイルに保存したい。どうしたらよいか。

fwrite()fread()

メモリ上の連続したデータを書き出したり、逆にファイルにある 連続データをメモリに読み込むにはそれぞれ fwrite(), fread() を利用する。

効率を重視するプログラムでは open/read/wrire/close システムコールを用いることが多いが、ここでは既習の fopen/fclose から利用できる fread/fwrite を利用して説明を進める。

fread(), fwrite() は,

size_t fread(ポインタ, ブロックサイズ, ブロック数, ストリーム);
size_t fwrite(ポインタ, ブロックサイズ, ブロック数, ストリーム);

の形式で利用し,読み書きに成功すると成功したブロック数を返す。 実質的に「ブロックサイズ×ブロック数」のバイトデータの入出力が 行なわれる。fread() では指定したバイト数の読み込み前に ファイル終端に達した場合は,完全に読めたブロック数が返される。

メモリイメージの保存/読み込みの例示プログラム mem-rw.c を利用し、 以下の手順で動作の仕組を確認せよ。

  1. コンパイルする。
    gcc -o mem-rw mem-rw.c
    
  2. 適当なデータを入力しファイルに保存させる。
    ./mem-rw save
    (以下,問い合わせに答える)
    
  3. データファイルの中味を一応確認
    less memslot.bin
    
  4. データを構造体に読み込む
    ./mem-rw
    

共用体の利用

英数2科目得点処理だけでなく, 国語の得点処理もするよう拡張する必要が出たとする。 単純に考えると,構造体宣言を以下のように拡張することになる。

typedef struct {
  char name[20];
  int  shusseki;
  int  mathpt;
  int  engpt;
  int  japt;
} TOKUTEN;

ところが,構造体を変更すると古いデータファイルが利用できなくなる。 古い2科目用のデータファイルも,新しい3科目用のデータファイルも 同じプログラムで読み取り処理できるようにするにはどうしたらよいか。

こうした問題を回避するには,構造体設計の初期段階から, 複数の構造体を判別する印を入れたものを設計する。 初期の構造体をそのまま使うのではなく,構造体のバージョンを 組み込んだ構造体を定義してそれを利用する。

typedef struct {
  char name[20];
  int  shusseki;
  int  mathpt;
  int  engpt;
} TOKUTEN;

typedef struct {
  char version[4];
  TOKUTEN me;		/* 数学と英語用の構造体 */
} TKDATA;

構造体のファイルへの入出力はこれを用いて行なうようにする。 mem-rw.c をこの構造体を使うように改めたものを union-rw.c に示す。

さて,もう1科目,国語の点を処理する必要が出た。 構造体を拡張したものを作る。

typedef struct {
  char name[20];
  int  shusseki;
  int  mathpt;
  int  engpt;
  int  japt;		/* 新バージョンでこれを追加 */
} TOKUTEN3;

バージョンを含んだ構造体 TKDATA 中の,実際に得点を含む構造体宣言を,新旧構造体の union とする。

typedef struct {
  char version[4];
  union {
    TOKUTEN	me;		/* 数学と英語用の構造体 */
    TOKUTEN2	mej;		/* 数学と英語と国語用の構造体 */
  };
} TKDATA;

これを利用して,データのバージョン切り替えを行なうには TKDATA型の変数の version メンバを利用する。たとえば

TKDATA v;

で宣言した変数 v を旧バージョンの構造体として 扱いたい場合は v.me で,新バージョンの構造体として 扱いたい場合は v.mej でアクセスすればよい。それを 切り替える判断のために,v.version を用いる。

新旧両バージョンのデータ処理ができるようにしたプログラムを示す。

union-rw2.c

/*
 * union-rw2.c
 * 第1引数が "save" ならデータファイルの作成、
 * 第2引数が "2" ならバージョン2のデータファイルの作成
 * それ以外ならロード
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DATAFILE "memslot.bin"
#define BUFSZ 20

typedef struct {
  char name[20];
  int  shusseki;
  int  mathpt;
  int  engpt;
} TOKUTEN;

typedef struct {
  char name[20];
  int  shusseki;
  int  mathpt;
  int  engpt;
  int  japt;                    /* 国語の得点新設 */
} TOKUTEN2;

typedef struct {
  char version[4];
  union {
    TOKUTEN  me;                /* 数学と英語用の構造体 */
    TOKUTEN2 mej;               /* 数学と英語と国語用の構造体 */
  };
} TKDATA;

/*
 * TKDATA* のデータを出力する。
 */
void dispdata(TKDATA *td)
{
  TOKUTEN *d = &td->me;
  printf("%10.10s の得点 |\n", d->name);
  printf("%17.17s | %3d\n", "数学", d->mathpt);
  printf("%17.17s | %3d\n", "英語", d->engpt);
  if (0 == strcmp(td->version, "v2")) {
    printf("%17.17s | %3d\n", "国語", td->mej.engpt);
  }
}

/*
 * TKDATA* のデータを所定のデータファイルにバイナリのまま書き込む
 */
int save(TKDATA *td)
{
  TOKUTEN2 *t = &td->mej;
  char tmp[sizeof(t->name)+1];
  FILE *fp;
  while (t->name[0] == '\0') {
    /* データの入力と構造体への代入 */
    fputs("名前: ", stderr);
    fgets(tmp, sizeof tmp, stdin);
    sscanf(tmp, "%s", t->name);
    fputs("出席番号: ", stderr);
    fgets(tmp, sizeof tmp, stdin);
    t->shusseki = atoi(tmp);
    fputs("数学得点: ", stderr);
    fgets(tmp, sizeof tmp, stdin);
    t->mathpt = atoi(tmp);
    fputs("英語得点: ", stderr);
    fgets(tmp, sizeof tmp, stdin);
    t->engpt = atoi(tmp);
    if (strcmp(td->version, "v2") == 0) {
      fputs("国語得点: ", stderr);
      fgets(tmp, sizeof tmp, stdin);
      t->japt = atoi(tmp);
    }
  }
  fputs("これから保存するデータ:\n", stderr);
  dispdata(td);
  if (NULL != (fp=fopen(DATAFILE, "w"))) {
    /* ここで実際にファイルに書き込んでいる */
    fwrite(td, sizeof *td, 1, fp);
    fclose(fp);
    fprintf(stderr, "%s に書き込み完了\n", DATAFILE);
    return 0;
  }
  return 1;
}

/*
 * 所定のデータファイルのバイナリデータを TKDATA* に読み込む
 */
int load(TKDATA *td)
{
  FILE *rfp;
  int sz = sizeof *td;
  int rc=1;
  if (NULL != (rfp=fopen(DATAFILE, "r"))) {
    /* freadでバイナリデータを読む */
    fread(td, sz, 1, rfp);
    if (!ferror(rfp)) {         /* fread()が失敗していなかったら... */
      printf("%sからロードしたデータ(%s):\n", DATAFILE, td->version);
      dispdata(td);
      rc = 0;
    } else {                    /* fread()が失敗していたら */
      fputs("読み込み失敗\n", stderr);
    }
  }
  fclose(rfp);
  return rc;
}

int main(int argc, char *argv[])
{
  int rc;
  TKDATA tkdata;
  memset(&tkdata, 0, sizeof tkdata);
  if (argc > 1 && 0 == strcmp(argv[1], "save")) {
    if (argc > 2 && 0 == strcmp(argv[2], "2"))
      strncpy(tkdata.version, "v2", sizeof tkdata.version);
    else
      strncpy(tkdata.version, "v1", sizeof tkdata.version);
    rc = save(&tkdata);
  } else
    rc = load(&tkdata);
  return rc;
}

目次