構造体は、関連した複数のデータを扱うのに最適な方法です。そういうデータのかたまりを複数扱うことが多くあると思います。
構造体を配列で持てば、済む話かもしれませんが、それでは、配列を確保した以上のデータを扱う時に困りますよね。プログラムを変更する必要がでてきます。
そこで、構造体の中のメンバに自分自身のポインタを持たせることで、プログラム変更がいらないようにすることができます。そうすることで、配列でデータを扱うよりも、データの追加、削除が容易になります。
これが、構造体をリスト構造化するメリットです。デメリットはポインタの扱いになりますので、ややプログラムが複雑になるところです。
構造体とは何かを知りたい場合は、こちらの記事を読んでください。
———————————————————————————————
C言語のプログラムを3か月で身につけるための第4週でやるべきこと
———————————————————————————————
では、リスト構造化のプログラムを作る手順を書いていきます。
構造体を定義する
当たり前の話ですが、まずは、リスト構造にする構造体のメンバを定義しましょう。
ここでは、生徒の学籍番号、名前、5教科の点数を持つ構造体を作ってリスト構造化することにします。
構造体の宣言です。
struct student { int id; /* 学籍番号 */ char name[20]; /* 名前 */ int kokugo; /* 国語の点数 */ int suugaku; /* 数学の点数 */ int eigo; /* 英語の点数 */ int rika; /* 理科の点数 */ int syakai; /* 社会の点数 */ };
これでは、ただ単に構造体を定義しただけですね。これをリスト化するために、この構造体のポインタをメンバに追加します。
struct student { int id; /* 学籍番号 */ char name[20]; /* 名前 */ int kokugo; /* 国語の点数 */ int suugaku; /* 数学の点数 */ int eigo; /* 英語の点数 */ int rika; /* 理科の点数 */ int syakai; /* 社会の点数 */ struct student *next; /* 次の生徒のデータのポインタ */ };
何か気持ち悪いかもしれませんが、これは問題ありません。コンパイルエラーにもなりません。しかし、ポインタではなく、実体を持とうとするとコンパイルエラーになります。
なぜなら、構造体のメンバをコンパイラが解析している段階では、構造体の実体ができあがっていないからです。
リスト構造体の使い方
構造体を作成したら、次はそれを使っていきます。その方法を実際にプログラムを書いて解説していきます。
ここでは、生徒の各教科の情報を扱う構造体を宣言し、データの追加、削除、表示ができるプログラムを書いていますので、まずはざっと読んでみてください。
#include <stdio.h> #include <string.h> #include <strlib.h> struct student { int id; /* 学籍番号 */ char name[20]; /* 名前 */ int kokugo; /* 国語の点数 */ int suugaku; /* 数学の点数 */ int eigo; /* 英語の点数 */ int rika; /* 理科の点数 */ int syakai; /* 社会の点数 */ struct student *next; /* 次の生徒のデータのポインタ */ }; /* リストを追加するための関数 */ char addList(struct student* list, struct student* sp) { struct student* roop_list; /* 現リストのループ用 */ struct student* add_list; /* 追加リスト */ /* 追加リストがなかったら、何もしない */ if (sp == NULL) { return 0; } /* 追加リストの領域確保 */ add_list = (struct student*) malloc(sizeof(struct student))); /* 追加リストの領域確保ができなかったら何もしない */ if (add_list == NULL) { return 0; } else { /* 追加リストのデータをコピー */ add_list->id = sp->id; strcpy(add_list->name, sp->name); add_list->kokugo = sp->kokugo; add_list->suugaku = sp->suugaku; add_list->eigo = sp->eigo; add_list->rika = sp->rika; add_list->syakai = sp->syakai; add_list->next = NULL; /* 新規の追加リストなら、確保領域のアドレスをコピー */ if (list == NULL) { list = add_list; } else { /* 追加リストを一番最後に追加 */ roop_list = list; while (roop_list != NULL) { if (roop_list->next == NULL) { roop_list->next = add_list; break; } roop_list = roop_list->next; } } } return 1; } /* 指定リストを削除する関数 */ void deleteList(struct student* list, int pos) { struct student* p; int i; i = 0; while (list != NULL) { /* 次ポインタがNULLまで処理 */ if (pos == i) { /* 指定リストなら削除 */ if (pos == 0) { /* 先頭のリストを削除する場合 */ p = list->next; free(list); list = p; } else { /* 先頭以外のリストを削除する場合 */ p->next = list->next; free(list); } break; } p = list; list = list->next; i++; } } /* リストをすべて削除する関数 */ void deleteListAll(struct student* list) { struct student* p; while (list != NULL) { /* 次ポインタがNULLまで処理 */ p = list->next; free(list); list = p; } } void main(void) { struct student* data; /* 生徒のリスト情報 */ struct student* tmp; /* 生徒のリスト情報退避用 */ struct student add_data; /* 追加生徒の情報 */ int del_pos; /* 削除する生徒のリスト番号 */ int select; /* 操作選択 */ data = NULL; while (1) { printf("操作を選んでください。\n"); printf("0:表示\n"); printf("1:追加\n"); printf("2:削除\n"); printf("3:終了\n"); scanf("%d", select); swhitch (select) { case 0: tmp = data; while (data != NULL) { printf("学籍番号 = %d\n", data->id); printf("名前 = %s\n", data->name); printf("国語の点数 = %d\n", data->kokugo); printf("数学の点数 = %d\n", data->suugaku); printf("英語の点数 = %d\n", data->eigo); printf("理科の点数 = %d\n", data->rika); printf("社会の点数 = %d\n\n", data->syakai); data = data->next; } data = tmp; break; case 1: printf("学籍番号 = "); scanf("%d", &(add_data.id)); printf("名前 = "); scanf("%s", add_data.name); printf("国語の点数 = "); scanf("%d", &(add_data.kokugo)); printf("数学の点数 = "); scanf("%d", &(add_data.suugaku)); printf("英語の点数 = "); scanf("%d", &(add_data.eigo)); printf("理科の点数 = "); scanf("%d", &(add_data.rika)); printf("社会の点数 = "); scanf("%d", &(add_data.syakai)); /* リストに追加 */ addList(data, &add_data); break; case 2: printf("削除するリスト番号 = "); scanf("%d", &del_pos); /* 指定されたリストを削除 */ deleteList(data, del_pos); break; default: /* 使用メモリの開放 */ deleteListAll(data); exit; } } } }
確保された領域にデータを保持し、ポインタで関連付けをしていくイメージです。次のデータのポインタをメンバに持っておくことで、次のリストにアクセスすることができます。
注意して欲しいのは、リストの先へ進むことはできますが、リストの前に戻ることはできないので注意が必要です。
したがって、先頭のリストのポインタは常に覚えておく必要があります。
メイン関数で、何の操作を実行するかを選択させるようにしました。「終了」を選択しない限り、プログラムを終了しないようにwhile(1)で無限ループにしています。
ここで注意点ですが、この選択時に、誤って数値以外を入力してしまうと、プログラムが暴走してしまいます。他の”%d”で読み込んでいるscanf文全てで同じことが言えます。
これを暴走しないようにしようと思うと、一旦文字で受け取り、それを数値に変換するなどして対応することが考えられます。
0の表示を選択すると、今もっているリストデータを全て表示するようにしているだけです。何ももっていなければ、何も表示されず、操作選択に戻ります。
1の追加を選択すると、各種データを入力させる項目が出力され、入力が全て終わると、リストの一番最後に追加されるようにしています。一番最後の追加にしたので、addList関数は少々複雑になっています。
2の削除を選択すると、削除したいリスト番号を入力させるようにし、それが入力されると、指定されたリストを削除し、その前のリストにあるポインタを削除したリストの次のデータのポインタにするようにしています。
3の終了を選択すると、使用しているメモリを全て開放して、プログラムの実行を終了します。
始めにデータを追加しないと、削除も表示も意味がありません。プログラム実行終了時には、入力したデータは全て失われてしまいます。
ここから派生して、リスト構造をうまく利用するプログラムへ発展させましょう。例えば、ソーティングや、逆順にもリストをたどれるようにする、データをファイル保存するなど、色々やれることはあると思います。
リスト構造で逆順にもたどれるようにするには、逆順用のポインタをメンバに用意すればいい
先ほど、少し触れましたが、先ほどのリストは先頭から後ろへ順にたどっていくしかありませんでした。これでは少々不便ですよね。どうしたらいいかというと、リストの逆順にもたどれるようにしたらいいのです。
そうすると、いちいちリストの先頭のアドレスを覚えておく必要はなくなります。今のリスト情報から、たどれるからです。
どうやってやるかは、もうおわかりですね。もう1つ、逆順用のポインタをメンバに用意すればいいのです。
struct student { int id; /* 学籍番号 */ char name[20]; /* 名前 */ int kokugo; /* 国語の点数 */ int suugaku; /* 数学の点数 */ int eigo; /* 英語の点数 */ int rika; /* 理科の点数 */ int syakai; /* 社会の点数 */ struct student *prev; /* 前の生徒のデータのポインタ */ struct student *next; /* 次の生徒のデータのポインタ */ };
前のデータのポインタprevを追加しました。これに現在のリストの前にあるリストのポインタを覚えておけばいいのです。
先ほどのプログラム例で挙げたリストを追加する関数にポインタprevを設定する処理を追加します。
/* リストを追加するための関数 */ char addList(struct student* list, struct student* sp) { struct student* roop_list; /* 現リストのループ用 */ struct student* add_list; /* 追加リスト */ /* 追加リストがなかったら、何もしない */ if (sp == NULL) { return 0; } /* 追加リストの領域確保 */ add_list = (struct student*) malloc(sizeof(struct student))); /* 追加リストの領域確保ができなかったら何もしない */ if (add_list == NULL) { return 0; } else { /* 追加リストのデータをコピー */ add_list->id = sp->id; strcpy(add_list->name, sp->name); add_list->kokugo = sp->kokugo; add_list->suugaku = sp->suugaku; add_list->eigo = sp->eigo; add_list->rika = sp->rika; add_list->syakai = sp->syakai; add_list->prev = NULL; /* この部分を追加 */ add_list->next = NULL; /* 新規の追加リストなら、確保領域のアドレスをコピー */ if (list == NULL) { list = add_list; } else { /* 追加リストを一番最後に追加 */ roop_list = list; while (roop_list != NULL) { if (roop_list->next == NULL) { roop_list->next = add_list; add_list->prev = roop_list; /* この部分を追加 */ break; } roop_list = roop_list->next; } } } return 1; }
これだけで済みます。これで、list->prevでlistの前のデータ、list->nextでlistの次のデータへアクセスできるようになりました。
これで好きなようにリスト構造になっているデータへアクセスできるようになり、データの利用の幅が広がりましたね。
まとめ
リスト構造について、解説しました。構造体とポインタを駆使していく形なので、かなりややこしかったと思います。
しかし、配列でリストの間にデータを追加したり、データを削除したりするのは、手間がかかるのと、処理時間もかかってしまいます。
リスト構造にしておくと、ポインタの付け替えで済むので、圧倒的に有利なんですよ。
組み込みマイコンでは、あまりやることはないでしょうが、知っておいて損はないと思います。
私が、C言語の勉強の練習に使った中でも、一番ややこしい話です。
ですが、これが理解できるようになって、構造体、ポインタ、配列、関数という類の扱いが格段にできるようになりました。
だから、あなたもややこしいからと逃げずにやっていってほしいと思います。