第二週 構造体とポインタ・線形リスト
計算機序論2, 
www.kameda-lab.org 
2006/09/03
    構造体を使った不定長データ構造(線形リスト)
    
    構造体については講義や以前の課題で説明した通りです。
    今週は,構造体を使って,不定長(データの数や長さが可変であること)の
    データ構造を作ってみましょう.
    
    
    
    そのために最も簡単で良く使われる方法が
    線形リスト
    です.
    線形リストの構造は以下の図のようになります.
    
     
    
    次のデータへのポインタをメンバ変数として用意すると,
    これを使って,順にデータをたどることができるようになります.
    このメンバ変数が空アドレス(NULL)であれば、
    リストの最後まで到達したことになります。
    
    
    
    実際のサンプルプログラムを見てみよう
    
    以下のLINE構造体は、線分を表現するためのデータ構造体です。
    start 〜 widthは,線分自身のデータを表わし、
    nextは上で説明した「次のデータへのポインタ」です。
    見ればわかるように、3次元データにも対応できるように、
    このデータ書式は,z座標もすでに含んでいます.
    z座標に関する部分は,今回は使わず,4回目の演習以降で使います.
    
// 点のための構造体
struct POINT{
  float x;
  float y;
  float z;
  float w;
};
// 線分のための構造体 (この構造体の線形リストで各文字や図形を表す). 
// 次の線分へのポインタを持つ.最初の線分にアクセスできれば, 数珠繋ぎに
// 最後までアクセスできる.LINE->next → LINE, ....
struct LINE {
  struct POINT *start;  // 始点
  struct POINT *end;    // 終点
  float r;              // 赤色の強度
  float g;              // 緑色の強度
  float b;              // 青色の強度
  float width;          // 線の太さ
  struct LINE *next;    // 次の線分
};
    
    このようなデータ構造体を繋げて線形リストを作るわけですが,
    可変長のリストを作るためには,
    プログラムの中でデータ構造体を順次作っていかなければ
    なりません。
    そのために,malloc, calloc等を使って,メモリを確保していきます.
    以下のコードを良く見てください.
    ここでは,calloc関数に対して確保するデータの数【ここでは1つ】と
    サイズ【sizeof (struct LINE)】を与え,
    返ってきたデータをstruct LINE 型として,そこへのポインタを張ります。
    
// 新しい線分構造体を作り, 前準備をしておく
struct LINE *new_LINE(){
  struct LINE *newline = NULL;
  newline = (struct LINE *)calloc(1, sizeof(struct LINE)); 
  if (newline != NULL) {
    // 始点, 終点を格納するための構造体もメモリ上に確保する
    newline->start = new_POINT();
    newline->end   = new_POINT();
    newline->r = 0.0; newline->g = 0.0; newline->b = 0.0;
    newline->width = 1.0;
    newline->next  = NULL;
    if (newline->start == NULL || newline->end == NULL) {
      free_LINE(newline);
      newline = NULL;
    }
  }
  return (newline);
}
void ある関数 (引数) {
  .....(中略).....
  struct LINE *newline = NULL;
  .....(中略).....
    newline = new_LINE();
    .....(中略).....
    linked_list_end->next = newline;
}
    
    確保したデータが不要になったら,そのデータに対応するメモリを
    解放することもできます.
    free(ポインタ変数)とすれば,
    ポインタが参照している先のメモリを解放します.
    これが簡単にできるように,
    free_LINE(), free_FIGURE()
    等の関数を用意しています.
    ただし,一旦解放した部分にもう一度アクセスしようとすると
    エラーになるので,注意すること.
    
    
// 線分構造体のメモリを解放
void free_LINE (struct LINE *line) {
  if (line == NULL)
    return;
  
  free (line->start);
  free (line->end);
  free (line);
}
    
    データを読み込みながら線形リストを作る
    
    データはファイルに格納します.
    プログラム中にデータを長々と書くのは初心者しか使わないテクニックです.
    プログラムが煩雑になるし,
    データを変更する度にコンパイルし直さなければならないですから、
    はやくそういうことはしないような習慣をつけましょう.
    
    
    今回の演習では、ファイル中のデータは,以下のような書式とします.
    
    
    "#" で始まる行は,そこがコメント行として扱われる【読み捨てられる】
    
    
    "L" で始まる行は線分のデータを表わす.
    始点(x, y, z),終点(x, y, z),色(r, g, b),太さの順に書く.
    
    
    "F" はひとまとまりの図形がそこで終わることを表わす.
    
# 文字Yのデータ
L        50 350 0       100 200 0       1.0 0.5 0.5     4.0
L       150 350 0       100 200 0       1.0 0.5 0.5     3.5
L       100 200 0       100  50 0       1.0 0.5 0.5     5.0
F
# 文字Kのデータ
L       250 350 0       250  50 0       0.0 1.0 0.2     6.0
L       350 350 0       250 150 0       0.0 1.0 0.2     4.2
L       290 230 0       350  50 0       0.0 1.0 0.2     4.4
F
# 以上
    
    load_LINE(), attach_new_FIGURE() 等は,ファイルからデータを
    読み込みながら線形リストを作っていく関数です.
    大まかには,順次データを読み込みながら,
    構造体を作り(メモリを確保し),前のデータとつなげて行きます.
    この部分はかなりわかりにくいので,各自,その働きを理解し,
    コメント文を付加してください【レポートの課題の一つです!】.
    
// LINE構造体の生成, 読み込み
// 線分データをコマンドラインから読み込む
void load_LINE (char *command) {
  struct LINE *newline = NULL;
  int number_of_element = 0;
  char tag[CMDSTRLEN];
  newline = new_LINE();
  if (newline == NULL) {
    fprintf(stderr, "Memory allocation failed at load_LINE()\n");
    return;
  }
  // ここで値を代入している!
  number_of_element = 
    sscanf(command, "%128s  %f %f %f  %f %f %f  %f %f %f  %f", 
	   tag,
	   &newline->start->x, &newline->start->y, &newline->start->z,
	   &newline->end  ->x, &newline->end  ->y, &newline->end  ->z,
	   &newline->r, &newline->g, &newline->b, 
	   &newline->width);
  if (number_of_element != 11) {
    fprintf(stdout, "load_LINE: format error (%d elements found)\n", 
	    number_of_element);
    return;
  }
  if (Lines_on_reading == NULL) {
    Lines_on_reading = newline;
  } else {
    Line_at_end->next = newline;
  }
  Line_at_end = newline;
}
    
    
    次に,以下の関数を良く見てみましょう.
    まず,引数 "line"はポインタ変数であり,
    このポインタの先に実際の構造体(LINE)があります.
    関数が呼び出されるときは,最初の線分データへのポインタを
    含む変数figureが渡されます.
    その最初の線分データを描画した後,ポインタの先を次の線分にします.
    これをやるのが下記で日本語表記した部分です.
    線分リストの終りかどうかをチェックは、ポインタが次に繋がっているか
    どうかで判定できます.
// 図形(線分リスト)を描画
void draw_FIGURE (struct FIGURE *figure) {
  struct LINE *line = NULL;
  if (figure == NULL) return;
  line = figure->first_line;
  変数lineがNULLでない【=まだ次がある】うちは
    その変数lineによって線を描画し、
    さらにその次の線を変数lineにセットして
  どんどん線形リスト内を辿っていく
}
    
    関数の戻り値を構造体とする
    
    上記の関数【new_LINE()】のように,戻値【関数の値】を構造体への
    ポインタとしている部分がいくつかあります.
    以前は,このような動作が保証されていない処理系【Cコンパイラ】が
    あり,安易には使えませんでしたが,現在はほぼ標準になったと考えて
    良いでしょう.
    構造体へのポインタを使いこなすと,プログラムが簡潔になるため,
    中級テクニックとして是非とも身に付けておきましょう.
    
    
Yoshinari Kameda: 2004/10/02-
Yuichi Nakamura: Sun Aug 11 21:08:05 JST 2002