第二週 構造体とポインタ・線形リスト

計算機序論2, www.kameda-lab.org 2006/09/03

構造体を使った不定長データ構造(線形リスト)

構造体については講義や以前の課題で説明した通りです。 今週は,構造体を使って,不定長(データの数や長さが可変であること)の データ構造を作ってみましょう.

そのために最も簡単で良く使われる方法が 線形リスト です. 線形リストの構造は以下の図のようになります.

linear list

次のデータへのポインタをメンバ変数として用意すると, これを使って,順にデータをたどることができるようになります. このメンバ変数が空アドレス(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