このページでは、C言語における自己参照構造体について解説していきます!
構造体とはデータの型の1つで、下記ページで解説しているとおり「複数の配列や値をまとめて1つの型として管理する」型になります。
【C言語】構造体について初心者向けに分かりやすく解説例えば下記のような構造体は、char
型の配列、float
型の値2つをまとめて1つの型として管理を行う型となります。また、構造体の持つ各変数をメンバと呼びます。
struct PERSON {
char name[256];
float height;
float weight;
};
このように、複数の値をまとめて管理することで、データを単なる数値や文字ではなく、何かしらの物体や概念をとして扱うことができるようになります。要は、オブジェクトとして扱うことができます。上記の struct PERSON
型の変数の場合、name
(名前) ・ height
(身長) ・weight
(体重) を持っている型なので「人間」のようにして扱うことができます。この例に限らず、扱いたいものに合わせて構造体を定義してやれば、プログラム内で好きなものを扱うことができるようになります。
構造体に関しては上記ページで解説している通りなのですが、今回は構造体の中でも少し特別的な存在である「自己参照構造体」について解説していきます。具体的には、自己参照構造体の意味、自己参照構造体の定義の仕方、自己参照構造体を利用するメリットについて解説していきます。
Contents
自己参照構造体
まずは、自己参照構造体の意味合いについて解説していきます。
自己参照構造体とは、自分自身の型を参照する構造体のことになります。
例えば、前述で示した struct PERSON
であれば、struct PERSON
型のデータ(変数や配列の要素等)が他の struct PERSON
型のデータを参照できるように定義したものが自己参照構造体となります。
ここでいう「参照」とは、要は特定の変数が他の変数を指すことを言います。C言語で考えれば、特定の変数が他の変数を指すポインタのメンバを持つことと言い換えることができます。
今回説明するのは「自己参照」ですので、特定の変数が「同じ型の他の変数」を指すことを言います。
例えば前述で示した struct PERSON
は、他の struct PERSON
型の変数を指すためのメンバを持っていないため、自己参照構造体ではありません。
自己参照構造体の定義の仕方
ここまでの説明の通り、自己参照構造体は同じ型の他の変数をポインタで指すことができるように定義してやれば良いことになります。
スポンサーリンク
自己参照構造体の定義
つまり、自己参照構造体は、その構造体のメンバに「自身の型のポインタ」を持たせることで実現できます。
したがって、自己参照構造体は下記のように定義することができます。このように定義を行えば、メンバ名
のメンバによって、自分自身の型と同じ型の変数を指すことができるようになります。
struct 構造体名 {
// 他のメンバ
struct 構造体名 *メンバ名;
};
例えば下記のように struct PERSON
を変更すれば、この struct PERSON
は自己参照構造体となります。
struct PERSON {
char name[256];
float height;
float weight;
struct PERSON *person;
};
この struct PERSON
は確かに自己参照構造体ではあるのですが、メンバ名が person
になっているところはイマイチです。後述でも解説しますが、このメンバは同じ型の他のデータとの関係性を示すためののになります。したがって、このメンバには、名前からその関係性が理解できるようなメンバを付けてやるのが良いです。
間違った自己参照構造体の定義
自己参照構造体を定義する際によく起こる間違いが下記のような例になります。先ほどとの違いは person
がポインタではなく、単なる struct PERSON
になっている点になります。
struct PERSON {
char name[256];
float height;
float weight;
struct PERSON person;
};
このように、構造体の中で自分自身の型そのもののメンバを宣言するとコンパイル時にエラーが発生することになります。発生するエラーは下記のようなものになります。
error: field has incomplete type 'struct PERSON'
構造体の中で利用可能なメンバの型は、サイズが確定している型のみになります。
構造体の型のサイズが確定するのは最後の閉じ括弧 '}'
が記述された部分になります。上記のような struct PERSON
の定義では、最後の閉じ括弧前に struct PERSON
型が利用されており、サイズが確定していない型のメンバが宣言されているため上記のエラーが発生することになります。
ですが、ポインタはアドレスを格納する変数であるため、ポインタに必要なサイズはアドレス分であることが確定しています。そのため、struct PERSON *
に変更してやればエラーは発生しなくなります(図示しているバイト数はコンパイラや環境によって異なる可能性があるので注意してください)。
自己参照構造体で考えると少し複雑ですが、上記のような問題に関しては、別々の構造体で考えると何が起こっているかをシンプルに理解できます。
例えば下記のような構造体の定義をした場合も、先ほどと同様に field has incomplete type
というエラーが発生します。これに関しては、定義前の struct TEST
が struct PERSON
で利用されているのでエラーが発生することに納得していただけると思います。struct TEST
のサイズが確定していないため、struct PERSON
では struct TEST
のメンバ変数は利用できません。もちろん、構造体の定義の順番を逆にしてやれば上手くコンパイルできるようになります。
struct PERSON {
char name[256];
float height;
float weight;
struct TEST test;
};
struct TEST {
char name[256];
float height;
float weight;
};
また、上記の struct PERSON
の test
メンバの型を struct TEST
から struct TEST *
に変更してやれば、先ほどと同様にエラーが解決できることも確認することができます。
struct PERSON {
char name[256];
float height;
float weight;
struct TEST *test;
};
struct TEST {
char name[256];
float height;
float weight;
};
もちろん、struct TEST
の定義そのものがなければポインタに変更したとしても違うエラーが発生することになりますが、定義されるのであれば、ポインタの場合は構造体自体の型のサイズが確定する前に利用することが可能となります。前述で示した自己参照構造体の場合は少し現象が複雑にも思えますが、結局起きているのはここで説明した内容と一緒になります。
少し話が逸れましたが、要は自己参照構造体を定義する際には、その構造体に自分自身の型そのもののメンバを持たせるのではなく、そのポインタをメンバとして持たせる必要があります。
typedef
での型名の定義
また、通常の構造体の時同様に、自己参照構造体においても typedef
で新たな型名を定義することが可能です。例えば下記のように typedef
を利用すれば、struct PERSON
を person_t
という型名で扱うことができるようになります。
typedef struct PERSON {
char name[256];
float height;
float weight;
struct PERSON *person;
} person_t;
ただし、下記のように person_t
が定義される前に person_t
を利用するとコンパイル時にエラーが発生することになるので注意してください。
typedef struct PERSON {
char name[256];
float height;
float weight;
person_t *person;
} person_t;
自己参照構造体を定義する際に、メンバの型として typedef
で定義した型を利用したいのであれば、自己参照構造体を定義する前に typedef
を記述しておく必要があります。
typedef struct PERSON person_t;
struct PERSON {
char name[256];
float height;
float weight;
person_t *person;
};
スポンサーリンク
自己参照構造体の使い道・メリット
ここまでの説明で、自己参照構造体の定義の仕方については理解していただけたのではないでしょうか?
ですが、この自己参照構造体の使い道や、自己参照構造体を利用するメリットに関してはイマイチ理解できていない方が多いのではないかと思います。
ここからは、これらの自己参照構造体の使い道やメリットについて解説していきます。
他のオブジェクト(変数)との関係性を示すことができる
結論としては、この自己参照構造体を利用するメリットは他のオブジェクト(変数)との関係性を示すことができる点にあります。ですので、自己参照構造体は、他のオブジェクトとの関係性を示したい場合に利用します。
他のオブジェクトとの関係性
関係性という言葉が非常に曖昧なので、具体例で解説していきます。
例えば、ここまでも何回も登場してきた struct PERSON
の変数は「人間」というオブジェクトを表すことになります。皆さんもご存知の通り、人と人の間には様々な関係性があります。関係性の例としては、例えば親子もありますし、恋愛関係もあると思います。また、友達という関係もありますね!
こんな感じで、人と人との間には様々な関係があります。そして、こういった関係性は「人間」同士だけでなく、様々な「物」同士や「事」同士にも存在することになります。こういった「人間」「物」「事」をひっくるめてオブジェクトと呼んでいます。
で、こういったオブジェクト自体を表すのにC言語では良く構造体を利用します。そして、同じ種類のオブジェクト間の関係性を示す際に、自己参照構造体が利用されます。
関係性を示すためにポインタを利用する
また、こういった関係性を示す図によく用いられるのが「矢印」になります。下の図のように、どのオブジェクト同士が関係性を持っているかを示すために矢印がよく用いられます。
そして、C言語におけるポインタとは、アドレスを格納する変数であり、これは何かを指すための変数となります。イメージするなら、ポインタとは矢印です。この辺りは下記ページでも解説している通りです。
【C言語】ポインタを初心者向けに分かりやすく解説つまり、C言語では同じ種類のオブジェクト間の関係性を示すためにポインタを利用してやれば良いことになります。こういったイメージからも、自己参照構造体にはポインタのメンバが必要であることを理解していただけるのではないかと思います。
例えば、下記のように自己参照でない構造体を定義したとしても、struct PERSON
の各変数の間に関係性を持たせることはできません。
#include <string.h>
struct PERSON {
char name[256];
//struct PERSON *person;
};
int main(void) {
struct PERSON person1, person2, person3;
strcpy(person1.name, "YamadaTaro");
strcpy(person2.name, "YamadaHanako");
strcpy(person3.name, "YamadaSaburo");
}
つまり、struct PERSON
のそれぞれの変数、すなわち、それぞれの人間は独立していることになります。
ですが、下記のように自己参照構造体を定義した場合、各変数の間に関係性を持たせることができます。
#include <string.h>
struct PERSON {
char name[256];
struct PERSON *love;
};
int main(void) {
struct PERSON person1, person2, person3;
strcpy(person1.name, "YamadaTaro");
strcpy(person2.name, "YamadaHanako");
strcpy(person3.name, "YamadaSaburo");
person1.love = &person2;
person2.love = &person3;
person3.love = NULL;
}
この場合は、YamadaTaro
という人が YamadaHanako
という人を好きであり、さらに、YamadaHanako
は YamadaSaburo
という人が好きであるという関係性を持たせていることになります。
リスト構造や木構造における自己参照
また、今までに自己参照構造体を利用した経験がある人もおられるかもしれません。そういった方の中にはリスト構造や木構造の実装を行う際に利用したという人が多いのではないかと思います。
リスト構造や木構造については下記のページで解説していますので、ここでの各データ構造の詳細な説明は省略し、自己参照構造体の観点にのみ絞って説明を行います。
【C言語】リスト構造について分かりやすく解説【図解】 C言語で二分探索木(木構造・ツリー構造)をプログラミングこういったリスト構造や木構造を実装する際には自己参照構造体を利用することが多いです。そして、ここで自己参照構造体を利用するのも、今までの解説と同じで、同じ種類のオブジェクト間の関係性を示すことが目的となります。
例えば、上記のリスト構造の解説ページにおいては、リスト構造におけるノードを扱うために次のような struct NODE
を定義していました。
/* 会員情報を登録する構造体 */
struct NODE {
int number; /* 会員番号 */
char name[256]; /* 会員名 */
struct NODE *next; /* 次のノード */
};
そして、この NODE
構造体では number
(会員番号) と name
(会員名) と next
のメンバを持たせており、next
は struct NODE *
型なので、struct NODE
は自己参照構造体となります。
もし、この struct NODE
に next
がなかった場合、各ノードは単に会員番号と会員名を管理するだけのオブジェクトとなります。そして、各ノード間には関係性がなく、各ノードは独立して存在することになります。
それに対し、struct NODE
に next
があれば、単に会員番号と会員名を管理するだけでなく、特定のノードの次のノードも管理することができるようになります。例えば node1.next = &node2;
とすれば、node1
の次のノードが node2
であることを示すことができます。このようにして、各ノードを順序の関係性を含めて管理することができます。
そして、これによって、各ノードを特定の順序に並べるようなことが可能となります。例えば、各ノードを会員名のアルファベット順にソートすることもできます。また、新規会員が追加された場合に、特定のノードの next
を変更してやることで、新規会員追加後も会員名のアルファベット順にソートされた状態を維持するようなこともできます。
ノードの場合、人間などに比べてさらに抽象度の高いオブジェクトとなりますが、結局は自己参照構造体を定義する理由はオブジェクト間の関係性を示すためとなります。そして、この関係性を示すようにするのは、その関係性を示すことで新たな機能を実現するためとなります。
例えばリスト構造の場合はノードをソートすることができるようになって線形探索を実現することができますし、2分木の場合は各ノード間の親子関係を示すことで2分探索を実現することができることになります。
自己参照構造体の利用例
最後に、簡単なプログラムで自己参照構造体の利用例を示しておきたいと思います。
スポンサーリンク
利用例のソースコード
そのプログラムのソースコードが下記となります。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define PERSON_NUM 5 // 人の最大人数
// 人を表す構造体
struct PERSON {
char name[256]; // 名前
struct PERSON *love; // 好きな人
};
// 人を配列で管理
static struct PERSON persons[PERSON_NUM];
// nameの好きな人を表示する
void showLoveTo(char *name) {
printf("%sの好きな人:\n", name);
for (int i = 0; i < PERSON_NUM; i++) {
// nameを探索して好きな人を表示
if (strcmp(persons[i].name, name) == 0) {
if (persons[i].love == NULL) {
printf(" いません!\n");
} else {
printf(" %s\n", persons[i].love->name);
}
break;
}
}
printf("\n");
}
// nameのことが好きな人を全員表示する
void showLovedFrom(char *name) {
int loved_from_num = 0;
struct PERSON *person = NULL;
for (int i = 0; i < PERSON_NUM; i++) {
// nameを持つ要素を探索
if (strcmp(persons[i].name, name) == 0) {
person = &persons[i];
}
}
// personのことが好きな人を探索して表示
printf("%sのことが好きな人:\n", name);
for (int i = 0; i < PERSON_NUM; i++) {
// loveがpersonを指している要素を探索
if (persons[i].love == person) {
// 見つかったら表示
loved_from_num++;
printf(" %d : %s\n", loved_from_num, persons[i].name);
}
}
if (loved_from_num == 0) {
// nameのことが好きな人がいなかった場合
printf(" いません!\n");
}
printf("\n");
}
int main(void) {
char name[256];
// 各struct PERSONの情報を最初に設定
strcpy(persons[0].name, "Taro");
strcpy(persons[1].name, "Hanako");
strcpy(persons[2].name, "Jiro");
strcpy(persons[3].name, "Mika");
strcpy(persons[4].name, "Saburo");
persons[0].love = &persons[1];
persons[1].love = NULL;
persons[2].love = &persons[3];
persons[3].love = &persons[0];
persons[4].love = &persons[3];
while (1) {
// 名前の入力受付
printf("名前:");
scanf("%s", name);
int i;
for (i = 0; i < PERSON_NUM; i++) {
if (strcmp(name, persons[i].name) == 0) {
// 入力されたnameの好きな人・nameのことが好きな人を表示
showLoveTo(name);
showLovedFrom(name);
break;
}
}
// 入力されたnameが存在しない場合は終了
if (i == PERSON_NUM) break;
}
}
ソースコードの解説
簡単にソースコードの解説を行なっておきます。
自己参照構造体
まず、下記で「人」を表す構造体 struct PERSON
の定義を行っています。名前と好きな人を管理するため name
と love
メンバを持たせており、love
は struct PERSON *
型であるため、この構造体は自己参照構造体となります。
#define PERSON_NUM 5 // 人の最大人数
// 人を表す構造体
struct PERSON {
char name[256]; // 名前
struct PERSON *love; // 好きな人
};
// 人を配列で管理
static struct PERSON persons[PERSON_NUM];
また、この struct PERSON
型の配列を定義しており、この配列で5人の人を管理できるようにしています。
各 struct PERSON
の設定
そして、main
関数の前半部分で各配列の struct PERSON
の要素に対して name
と love
の設定を行なっています。
persons[0]
から persons[4]
の name
メンバに対し、strcpy
関数で文字列をコピーすることで名前の設定を行なっています。これにより、5人それぞれに名前が設定されることになりますが、それぞれ無関係・独立した状態になっています。
続いて、persons[0]
から persons[4]
の love
メンバを設定することで、それぞれの人の間に関係性が生まれます。イメージとしては、人同士の間に矢印が描かれることになります。そして、この矢印こそがポインタとなります。人は struct PERSON
によって表現されているのですから、このポインタは struct PERSON *
である必要があります(もしくは void *
)。
そして、このポインタの名前は love
なので、この矢印は、誰が誰を好きであるかの関係性を示すものとなります。矢印の根元側の人が、矢印の指す方向の人を好きであることを表しています。上記のソースコードの場合、例えば Taro
は Hanako
のことが好きですが、Hanako
は好きな人がいない、さらに Taro
は Mika
から好かれていることになります。
あくまでも love
はポインタですので、love
に代入する値はアドレスである必要があります。そして、このアドレスを取得するためにはアドレス演算子 &
が必要になるので注意してください。
persons[0].love = &persons[1];
persons[1].love = NULL;
persons[2].love = &persons[3];
persons[3].love = &persons[0];
persons[4].love = &persons[3];
アドレス演算子 &
については下記ページで詳しく説明していますので、詳細が知りたい方や &
の意味合いを知りたい方は是非下記ページを読んでみてください。
また、上記の2行目のように、関係性を持つ相手がいない場合は NULL
を指定しておいたほうが良いです。これにより、関係性を持つ相手がいないことを明示的に示すことができますし、不定値が格納されることを回避することができます。
このように、自己参照構造体における「自分自身の型のポインタ」で他の変数や配列の他の要素を指すことで、各変数や要素間、オブジェクト間に関係性を持たせることができます。
「特定の人の好きな人」の表示
そして、main
関数の後半部分で、各「人」の間の関係性を表示するようにしています。
scanf
関数で人の名前の入力受付を行い、その入力された名前の人を探索し、「その人の好きな人」と「その人のことが好きな人」の2種類の表示を行なっています。
前者を表示するのが showLoveTo
関数で、引数で指定された名前の人を表す要素を persons
配列から探索し、「その人が好きな人の “名前”」を表示しています。
この「引数で指定された名前の人を表す要素」を persons[i]
とすれば、presons[i]
の好きな人は persons[i].love
によって指されていることになります。なので、persons[i].love
によって指されている先の構造体のデータの name
メンバを表示してやることで、persons[i]
の好きな相手の名前を表示することができます。
ただ、この persons[i].love
はポインタであり格納されているのはアドレスになるため、*persons[i].love
が presons[i]
の好きな人(struct PERSON
型の実体)ということになります。
さらに、ここで表示したいのは presons[i]
の好きな人の “名前” ですので、printf
の引数に指定する表示する文字列としては (*persons[i].love).name
を指定する必要があります。
ただし、persons[i]
に好きな人がいない場合は persons[i].love
が NULL
となるようにしているため、この場合は *persons[i].love
を実行すると NULL
へのアクセスが発生してプログラムが異常終了することになります。そのため、persons[i].love == NULL
が成立する場合は *persons[i].love
を実行しないようにする必要があります。
また、(*persons[i].love).name
のようなポインタの指す構造体のメンバにアクセスする際はアロー演算子を利用することができ、(*persons[i].love).name
は persons[i].love->name
に書き換えることで簡潔に表現することが可能です。
要は、メンバの前側の変数が構造体を指すポインタである場合、アロー演算子 ->
を利用することができます。メンバの前側の変数が構造体の実体である場合は .
を利用してアクセスする必要があります。
自己参照構造体では構造体を指すポインタのメンバが必ず存在しますので、アロー演算子 ->
を利用する機会が多いです。下記ページでアロー演算子について解説していますので、この機会にアロー演算子についても理解を深めておいてください!
アロー演算子などが使われていて showLoveTo
関数の特に for
ループの中身の意味合いがパッと理解できないかもしれませんが、下記のように変数を分けて考えると理解しやすいのではないかと思います。
// nameを探索して好きな人を表示
if (strcmp(persons[i].name, name) == 0) {
struct PERSON src = persons[i]; // nameを名前にもつ人
struct PERSON *p_dst = persons[i].love; // srcの好きな相手
if (p_dst == NULL) {
printf(" いません!\n");
} else {
printf(" %s\n", p_dst->name);
}
break;
}
スポンサーリンク
「特定の人のことが好きな人」の表示
また、前述の通り、main
関数の後半部分では scanf
関数で人の名前の入力受付を行い、その入力された名前の人を探索し、「その人の好きな人」と「その人のことが好きな人」の2種類の表示を行なっています。
この後者側の表示を行なっているのが showLovedFrom
関数となります。この関数では「引数で指定された名前の人を表す要素を persons
配列から探索し、「その人のことが好きな人の “名前”」を表示しています。
この「引数で指定された名前の人を表す要素」を persons[i]
とすれば、presons[i]
のことが好きな人は、love
メンバによって presons[i]
のことを指していることになります。つまり、love
メンバが presons[i]
のアドレスと一致している構造体のデータの name
メンバを表示してやることで、persons[i]
のことを好きな人の名前を表示することができます。
そのため、showLovedFrom
関数では最初に「引数で指定された名前の人を表す要素」を特定し、そのアドレスを person
に格納するようにしています。これを行っているのが showLovedFrom
関数の前半の for
ループになります。
そして、後半のループでは、各要素 persons[i]
に対し、persons[i].love == person
が成立するかどうかを判断し、成立した場合に persons[i].name
を表示することで「引数で指定された名前を持つ人のことが好きな人の “名前”」を表示することを実現しています。
まとめ
このページではC言語における自己参照構造体について解説しました!
自己参照構造体とは、自分自身の型のポインタを持つ構造体のことになります。そして、この自己参照構造体のメリットは、同じ種類のオブジェクトの関係性を示すことができる点にあります。
今回紹介した人と人との恋愛関係を示すこともできますし、各ノード間の関係性を示すことでリスト構造や木構造などのデータ構造を実装することも可能です。
とにかく使い道は多いですし、こういった自分自身の型を参照する仕組みは、C言語の自己参照構造体だけでなく、クラス等でもよく利用されるので、是非この機会に自己参照のメリットについて理解しておいてください!
このサイトでは、自己参照構造体を利用する例としてリスト構造や木構造についても解説していますので、復習のために是非こちらも読んでみてください!
【C言語】リスト構造について分かりやすく解説【図解】 C言語で二分探索木(木構造・ツリー構造)をプログラミングオススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/