C言語/構造体

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

構造体[編集]

構造体の基本[編集]

構造体(こうぞうたい)は、複数の異なる型のデータを、 まとめて1つのデータ型として扱う方法である。

いっぽう、構造体をもちいずに配列だけを使った場合では、異なる型をまとめて扱うことは、不可能な仕様である。(『C言語/配列』)

しかし、C言語は、構造体を配列にすることは可能な仕様になっている。

このため、たとえば「商品名 = 牛乳、価格=200」・「商品名=オレンジ ジュース、価格=150」のように、文字型の商品名と数値型の価格という異なるデータ型のまざったデータを並べたい場合、まず構造体で「char 商品名、 int 価格」 のような内容の構造体をつくり、それを配列などで必要な個数(この場合は2個)ぶん以上を確保することで、異なる型のまざったデータを扱えるようになる。


より厳密には「構造体」とは、メンバオブジェクトの集合を順に割り付けたものである。[1]

なお、構造体のかわりに、本書のタイトルにもある「共用体」を用いても、異なる型のまざったデータを扱うことができる。


構造体の型枠の宣言[編集]

上述の牛乳などの商品リストの例では、たとえば

//例 構造体の宣言

int main(void)
{
	struct 
	{
		char syouhinmei[32]; // 商品名
		int kakaku; // 価格
	} syouhin_list;
}

のように宣言することになる。

struct は「ストラクト」と読む。英語で、なにかの構造のことを「structure」(ストラクチャー)という。なお、日常英語に struct (ストラクト)という語句は無い。(ストラクト struct はコンピュータ業界の専門用語。ふつうの英和辞典をしらべても、struct は無い。)

さて、構造体を使うには、まず構造体の型枠を宣言し、 次にその型枠を使って構造体を宣言しなければならない。 [2]

構造体の型枠の宣言の記述は次のようになっている。

struct タグ名
{
	データ型 メンバ名;
		:
		:
};

タグ名で、構造体の型枠に名前をつける。

構造体が持つ1つ1つのデータをメンバという。 使用するメンバの数だけ、 「データ型 メンバ名;」を含める。 構造体の型枠の宣言は、 単に構造体の型枠を作るに過ぎず、 メモリ上に領域は確保されない。

C++言語では構造体などのメンバに関数を含むこともでき「メンバ関数」などと呼ぶ。しかし、無印のC言語には、構造体に関数を含むことはできない(つまり、無印Cに「メンバ関数」の機能は無い)。

  • どうしても構造体と関数をグループにしたい場合

なお、どうしても無印C言語の構造体で、その構造体を関数と関連付けたい場合、たとえば、構造体の中で、

struct タグ名
{
		:
		:
	int kansuu_flag;
		:
		:
};

のように、関数を使うかどうかの判定用の数値を用意する。そして、構造体の外に、その関数の具体的な処理内容を書く。さらにmain関数などの内部においてIf文などで、kansuu_flagが(たとえば)1の場合にのみ、その関数を実行する、などの条件設定を行う。このようにして、ある構造体の中で、間接的に、関数と構造体を、グループ扱いすることができる。(なお、上述の kansuu_flag のように、ある処理を実行するか否かの判定をするための数値のことを、IT業界の用語で「フラグ」と、一般に言う。)


構造体変数[編集]

上述の飲料商品リストのコードのように構造体だけをつくっても、その構造体で示したパターンに従った変数は、まだ作られてはいない。

構造体で示したパターンに従った変数を、構造体変数と言う。下記のように宣言する。

//例 GCCコンパイラでの実行例
//例 構造体変数の宣言
#include <stdio.h>
#include <string.h>

int main(void)
{
	struct  syouhin_list
	{
		char syouhinmei[32]; // 商品名
		int kakaku; // 価格
	};


	struct syouhin_list gyuunyuu; // 構造体 syouhin の構造体変数 gyuunyuu を宣言
	struct syouhin_list orenji_jyuuusu; // 構造体 syouhin の構造体変数 orenji_jyuuusu を宣言

	strcpy(gyuunyuu.syouhinmei,  "牛乳"); // C言語では文字列の代入には strcpy( , ) を使わなければならない
	gyuunyuu.kakaku = 200;

	strcpy(orenji_jyuuusu.syouhinmei,"オレンジジュース");
	orenji_jyuuusu.kakaku = 150;

	printf("商品名 : %s , 価格 : %d \n", gyuunyuu.syouhinmei, gyuunyuu.kakaku);
	printf("商品名 : %s , 価格 : %d \n", orenji_jyuuusu.syouhinmei, orenji_jyuuusu.kakaku);
}
実行結果
商品名 : 牛乳 , 価格 : 200 
商品名 : オレンジジュース , 価格 : 150 
※ Visual Studio だと、strcpy_sを使っても、なぜか「牛乳」のデータが文字化けして失敗する。

構造体変数を宣言するときは、上述の struct syouhin_list gyuunyuu ;

のように、

struct 呼び出し元の構造体名 変数名 ;

の書式で宣言する。


また、構造体変数に代入したい場合など、構造体変数にアクセスしたい場合には、「.(ドット演算子)」を用いて、

gyuunyuu.kakaku

のように記述する。

つまり書式は、

構造体名.構造体変数名

のようになる。


構造体の宣言[編集]

構造体の宣言の記述は次のようになっている。

struct タグ名 変数名のリスト;

タグ名で、構造体の型枠を指定する。 変数名のリストで、構造体に名前をつける。 複数の構造体を宣言する時は、変数名を「,(コンマ)」で区切る。 構造体の宣言で、タグ名が指す構造体の型枠を使って、メモリ上に領域を確保する。

なお、構造体の型枠の宣言 と 構造体の宣言 とを、同時に行うこともできる。 次のように記述する。

struct タグ名
{
	データ型 メンバ名;
		:
		:
} 変数名のリスト;

この場合、タグ名は省略することができる。

//例 構造体の宣言

int main(void)
{
	struct
	{
		int i;
		double d;
		char c;
		char str[32];
	} kouzoutai;
}

上の例では、int型の変数i、double型の変数d、 char型の変数c、char型の配列strの4つのメンバを持つ、 kouzoutaiという名前の構造体を宣言している。

また、構造体は、宣言と同時に、値のリストで初期化することもできる。 初期化の記述は次のようになっている。

struct タグ名 変数名 = {値のリスト};

値のリストは「,(コンマ)」で区切った定数のリストである。

//例 構造体の宣言と初期化
int main(void)
{
	struct
	{
		int i;
		double d;
		char c;
		char str[32];
	} kouzoutai = { 1234, 3.14, 'a', "Hello, World!" };
}

上の例では、iを1234、dを3.14、cを'a'、strを"Hello, World!"で初期化している。

構造体のメンバへのアクセス[編集]

構造体の各メンバにアクセスするには、 「.(ドット)演算子」を用いて、次のように記述する。

構造体の変数名.メンバ名
//例 構造体へのアクセス
#include <stdio.h>

int main(void)
{
	struct
	{
		int i;
		double d;
		char c;
		char str[32];
	} kouzoutai;

	printf("整数を入力してください。:");
	scanf("%d", &kouzoutai.i); // 整数入力を kouzoutai.i に格納する。

	printf("浮動小数点数を入力してください。:");
	scanf("%lf", &kouzoutai.d); // 浮動小数点数入力を kouzoutai.d に格納する。

	printf("文字(半角1文字)を入力してください。:");
	scanf(" %c", &kouzoutai.c); // 文字入力を kouzoutai.c に格納する。

	printf("文字列(半角31文字、全角15文字まで)を入力してください。:");
	scanf("%31s", kouzoutai.str); // 文字列入力を kouzoutai.str に格納する。

	printf("kouzoutaiのメンバの値は、%d %f %c %sです。\n", kouzoutai.i, kouzoutai.d, kouzoutai.c, kouzoutai.str);
}

上の例では、ユーザーからの4つの入力を、 構造体の4つのメンバに格納し、 その4つのメンバの値を表示している。

構造体のコピー[編集]

配列と異なり、構造体は一度にコピーすることができる。 構造体をコピーする時は、構造体の変数名のみを用いる。 すなわち、次のように記述する。

構造体の変数名 = 構造体の変数名;

ただし、代入される構造体(上コードの左辺)と代入する構造体(上コードの右辺)は、構造体の型が一致していなければいけない[3]。(仮にもし、違う構造体型の構造体どうしがコピーや代入を出来たとしても、正常動作するかアヤシイので、避けたほうが良い。)


右の構造体の全てのメンバが左の構造体の全てのメンバにコピーされる。 ただし、構造体のメンバにポインタを含む場合、 この方法ではポインタの値がコピーされ、 片方の構造体のポインタ先の値を変更すると、 もう片方の構造体のポインタ先の値まで変わってしまう、 いわゆる浅いコピー(シャローコピー)となるので注意が必要である。

//例 構造体のコピー
#include <stdio.h>

int main(void)
{
	struct
	{
		int i;
		double d;
		char c;
		char str[32];
	} kouzoutai1 = {1234,3.14,'a',"Hello, World!"}, kouzoutai2;

	kouzoutai2 = kouzoutai1;
	printf("kouzoutai2のメンバの値は、%d %f %c %sです。\n", kouzoutai2.i, kouzoutai2.d, kouzoutai2.c, kouzoutai2.str);
}

typedefによるコードの短縮[編集]

typedefを構造体に用いて、コードを短縮することができる。

//例 typedefを用いない構造体の宣言

struct sKouzoutai
{
	int i;
	double d;
	char c;
	char str[32];
};

int main(void)
{
	struct sKouzoutai kouzoutai;//構造体の宣言には「struct タグ名」が必要
}

上の例は typedef を用いて、下の例のように記述することができる。

//例 typedefを用いた構造体の宣言

typedef struct
{
	int i;
	double d;
	char c;
	char str[32];
} sKouzoutai;

int main(void)
{
	sKouzoutai kouzoutai; // 構造体の宣言には「構造体の別名」が必要
}

構造体の応用[編集]

構造体を引数に持つ関数[編集]

構造体を関数の引数にしたい場合、下記のように行う。Windowsの Visual C++ での構造体配列のコードの例を下記に示す。

#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct  syouhin_list {
	char syouhinmei[32]; // 商品名
	int kakaku; // 価格
};


void kansuu(struct syouhin_list nomimono)  // このように、引数では struct から宣言する必要がある
{
	printf("商品名 : %s , 価格 : %d \n", nomimono.syouhinmei, nomimono.kakaku);
}

int main(void) {
	struct syouhin_list gyuunyuu; // 構造体 syouhin の構造体変数 gyuunyuu を宣言
	struct syouhin_list orenji_jyuuusu; // 構造体 syouhin の構造体変数 orenji_jyuuusu を宣言

	strcpy_s(gyuunyuu.syouhinmei, 20, "牛乳"); // C言語では文字列の代入には strcpy( , ) を使わなければならない
	gyuunyuu.kakaku = 200;

	strcpy_s(orenji_jyuuusu.syouhinmei,20, "オレンジジュース");
	orenji_jyuuusu.kakaku = 150;

	kansuu(gyuunyuu); // 引数は構造体変数だけで良い。通常の関数と同様に、型は不要。
	kansuu(orenji_jyuuusu); // 引数は構造体変数だけで良い

	system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
	
	return 0;
}


  • 細かなノウハウ1

構造体を呼び出す側の gyuunyuu.kakaku では、int や char などの型をつけないように気をつけよう。

実務では、いったん構造体を使わずにモデルとなる(非構造体の)通常の変数を作ってから、あとから手作業で構造体に置き換える場合もよくある。この置き換えの時、呼び出し側の場所でよく int や char などの型宣言が行われている場合があるが、構造体では型宣言は既に構造体の宣言の場所で行われているので不要だし、それに呼び出し側で型宣言するとコンパイルエラーになるので、どちらせにせよ呼び出し側からは重複する型宣言を除去する必要がある。


  • 細かなノウハウ2

構造体の宣言は、main関数よりも上でも可能だが、つまり、

struct  syouhin_list {
	char syouhinmei[32]; // 商品名
	int kakaku; // 価格
};

のような宣言はmain関数よりも上で行えるが、

しかし、構造体の各要素の初期値代入(たとえば

gyuunyuu.kakaku = 200;

など)

は一般のコンパイラ(たとえばWindowsならVisual Studio 2019)ではmain関数よりも上では不可能である。


構造体の配列[編集]

たとえば、学校での生徒の成績表のような、文字型と数値型が混ざるデータ構造を作りたい場合、つまり、異なる型を組み合わせたデータ構造をつくる場合には、構造体(または共用体)を要素とした配列をつくる必要がある。このような、配列と構造体を組み合わせたデータ構造は、ネット上では「構造体配列」または「構造体の配列」などと紹介されている。

※ かならずしも配列から構造体を呼び出さなくても良いが、実務上は、もし成績の構造体だけをつくっても、それを配列にしないと、実用的には不便である。なぜなら、配列でない個別の変数(構造体変数)に代入していく方式だと、たとえば成績表なら、クラスの人数ぶんだけ構造体変数を用意したりするハメになりかねない。たとえば1クラス当たり35人の学級なら、35個の構造体変数を用意するのは煩雑である。
よって成績表をつくるためには、まず構造体の機能で1人ぶんの氏名と成績とのデータの結びつけを作り、それを配列で生徒数のぶんだけ複製するのである。

学校の成績表なら、まだクラスの生徒数がせいぜい数十名なので、構造体変数を数十個書くという方法でも、なんとか対応できる。だが、もし、大企業による提供サービスの登録者の会員名簿とかだと、会員数が数万名とかになるので、もはや構造体変数を1個ずつ書いていく方式は、不可能である。


Windowsの Visual C++ での構造体配列のコードの例を下記に示す。

(※ 他のコンパイラだとエラーになる場合もある。)

#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct seisekihyou {
	char seitomei[100];
	int syusseki_bangou;
	int kokugo_tensuu;
	int sugaku_tensuu;
};

int main(void)
{
	struct seisekihyou student[2]; // 構造体配列の宣言

	strcpy_s( student[0].seitomei, 10 ,"山田"); 
	student[0].kokugo_tensuu = 80;
	student[0].sugaku_tensuu = 70;

	strcpy_s(student[1].seitomei, 10 , "佐藤");
	student[1].kokugo_tensuu = 60;
	student[1].sugaku_tensuu = 90;

	int i = 0;

	for (i = 0; i<2; i = i + 1) 
	{
		printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].seitomei, student[i].kokugo_tensuu, student[i].sugaku_tensuu);
	}

	system("pause");// 「続行するには何かキーを押してください . . .」の待機命令

	return 0;
}
※ 上記のコードの動作確認として、 Visual Studio 2017 のCommunity 無料版でのC++で動作を確認ずみである。


実行結果
名前: 山田, 国語: 80点, 数学: 70点
名前: 佐藤, 国語: 60点, 数学: 90点


いっぽう、gccコンパイラの場合、strcpy_s ではなく strcpy にしないと行けないので、上記のコードは使えないので、gccで動作させるには下記のようなコードになる。

// gcc での構造体配列の例

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

struct seisekihyou {
	char seitomei[100];
	int syusseki_bangou;
	int kokugo_tensuu;
	int sugaku_tensuu;
};

int main(void)
{
	struct seisekihyou student[2]; // 構造体配列の宣言

	strcpy( student[0].seitomei ,"山田"); 
	student[0].kokugo_tensuu = 80;
	student[0].sugaku_tensuu = 70;

	strcpy(student[1].seitomei, "佐藤");
	student[1].kokugo_tensuu = 60;
	student[1].sugaku_tensuu = 90;

	int i = 0;

	for (i = 0; i<2; i = i + 1) 
	{
		printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].seitomei, student[i].kokugo_tensuu, student[i].sugaku_tensuu);
	}

	return 0;
}
※ 上記のコードの動作確認として、Fedora 28 (Linuxの一種)上での gcc で動作を確認ずみ。


構造体の配列の書式[編集]

構造体の配列を宣言する時は、 変数名の後に「[要素数]」をつける。 すなわち、次のように記述する。

struct タグ名 配列の変数名[要素数];

構造体の配列のメンバにアクセスする時は、 変数名の後に「[添字]」をつける。 すなわち、次のように記述する。

配列の変数名[添字].メンバ名

「[]」と「.」とは、 同じ優先順位の演算子で、 左から右へと評価されるため、 次のような意味になる。

(配列の変数名[添字]).メンバ名
//例 構造体の配列
#include <stdio.h>

int main(void)
{
	struct
	{
		int i;
		double d;
		char c;
		char str[32];
	} kouzoutai[4] =
		{
			{12, 1.2, 'a', "abc"},
			{34, 3.4, 'b', "def"},
			{56, 5.6, 'c', "ghi"},
			{78, 7.8, 'd', "jkl"},
		};

	int n;
	for(n=0; n<4; ++n)
		printf("kouzoutai[%d]のメンバの値は、%d %f %c %sです。\n",
		n, kouzoutai[n].i, kouzoutai[n].d, kouzoutai[n].c, kouzoutai[n].str);
}

上の例では構造体の配列に初期値を与え、 そのメンバの値を表示している。


構造体のネスト[編集]

構造体のメンバに、構造体を指定することができる。 これを構造体のネストと呼ぶ。

ネストされた構造体にアクセスする時は、 ネストの最も外側から内側に向かって、 参照しなければならない。

//例 構造体のネスト
#include <stdio.h>

int main(void)
{
	struct sKouzoutai1
	{
		int i;
		double d;
		char c;
	};
	
	struct sKouzoutai2
	{
		int i;
		double d;
		char c;
		struct sKouzoutai1 k;
	}kouzoutai;

	kouzoutai.i=1234;
	kouzoutai.d=1.23;
	kouzoutai.c='a';
	kouzoutai.k.i=5678;
	kouzoutai.k.d=5.67;
	kouzoutai.k.c='b';

	printf("%d %f %c\n",kouzoutai.i,kouzoutai.d,kouzoutai.c);
	printf("%d %f %c\n",kouzoutai.k.i,kouzoutai.k.d,kouzoutai.k.c);
}

上の例では、kouzoutaiのメンバに、 kという構造体がネストされていて、 それらのメンバに値を代入したり、 メンバの値を表示している。

構造体配列のネスト[編集]

ある学級の生徒のデータベースを作りたいなら、前節のように構造体配列を使えばいいだろう。

だが、1つの学年に学級が幾つもある場合、別別に構造体を作るのは避けたい。

さらに、実用的な事を考えれば、1つの学校に、幾つもの学年があるわけであるので、それに対応したい。

この事から、構造体配列を、構造体でネストする必要性が生じてくる。

まず、前節の構造体配列の内容を、さらにネスト化してみよう(表示結果はまだ同じ)。


コード例(windows版)
// windows版 gcc
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct seisekihyou {
	char seitomei[100];
	int syusseki_bangou;
	int kokugo_tensuu;
	int sugaku_tensuu;
};


struct gakkyuu {
        struct seisekihyou kumi;
};


int main(void)
{        
	struct gakkyuu student[2]; // 構造体配列の宣言

	strcpy_s( student[0].kumi.seitomei, 10 ,"山田"); 
	student[0].kumi.kokugo_tensuu = 80;
	student[0].kumi.sugaku_tensuu = 70;

	strcpy_s(student[1].kumi.seitomei, 10 , "佐藤");
	student[1].kumi.kokugo_tensuu = 60;
	student[1].kumi.sugaku_tensuu = 90;

	int i = 0;

	for (i = 0; i<2; i = i + 1) 
	{
		printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].kumi.seitomei, student[i].kumi.kokugo_tensuu, student[i].kumi.sugaku_tensuu);
	}

	system("pause");// 「続行するには何かキーを押してください . . .」の待機命令

	return 0;
}


実行結果
名前: 山田, 国語: 80点, 数学: 70点
名前: 佐藤, 国語: 60点, 数学: 90点
(Windows7上で、gccコンパイラで確認。)

である。(※ 得点は最初の例と同じまま)

とりあえず、構造体配列をネスト化することは、これで可能である事が実験できた。

なお、Linux版gccでは、コードは下記のようになる。strcpy()関数がwindowsと違っている(windowsでは、ここは strcpy_s() 関数になっている 。また system("pause"); がコメントアウト)。


Linux版のコード例
// linux版gcc
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct seisekihyou {
	char seitomei[100];
	int syusseki_bangou;
	int kokugo_tensuu;
	int sugaku_tensuu;
};


struct gakkyuu {
        struct seisekihyou kumi;
};


int main(void)
{        
	struct gakkyuu student[2]; // 構造体配列の宣言

	strcpy( student[0].kumi.seitomei,"山田"); 
	student[0].kumi.kokugo_tensuu = 80;
	student[0].kumi.sugaku_tensuu = 70;

	strcpy(student[1].kumi.seitomei , "佐藤");
	student[1].kumi.kokugo_tensuu = 60;
	student[1].kumi.sugaku_tensuu = 90;

	int i = 0;

	for (i = 0; i<2; i = i + 1) 
	{
		printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].kumi.seitomei, student[i].kumi.kokugo_tensuu, student[i].kumi.sugaku_tensuu);
	}

	// system("pause");// 「続行するには何かキーを押してください . . .」の待機命令

	return 0;
}
(※ 上記コードの動作確認については、Fedora33 にて 2011年1月24日 に動作確認ずみ。)

上記コードの動作結果はwindows版と同じであるので、説明を省略する。

※ 以降、Linux版の例については、本節の最後の例を除いて省略する。つまり、上記の最初の例と、最後の例だけLinux版コードを紹介する。それ以外の場合については、その紹介例から容易にコードを作れる。


しかし、私たちが作成したいのは、これを2学級以上に対応させる事である。(上記コードは、まだ1学級ぶんしか対応していない。

これを2学級以上に対応させるには、単に下記のように

struct gakkyuu {
        struct seisekihyou kumi[2];
};

のように、呼び出し元のほうの構造体で配列変数を使えばいいだけである。

論より証拠、下記のコードは実行できる。

コード例(windows版)
// windows版
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct seisekihyou {
	char seitomei[100];
	int syusseki_bangou;
	int kokugo_tensuu;
	int sugaku_tensuu;
};


struct gakkyuu {
        struct seisekihyou kumi[2];
};


int main(void)
{        
	struct gakkyuu student[2]; // 構造体配列の宣言

	strcpy_s( student[0].kumi[0].seitomei, 10 ,"山田gggg");  // 前コードと区別のため、文字を追加している
	student[0].kumi[0].kokugo_tensuu = 80;
	student[0].kumi[0].sugaku_tensuu = 70;

	strcpy_s(student[1].kumi[0].seitomei, 10 , "佐藤");
	student[1].kumi[0].kokugo_tensuu = 60;
	student[1].kumi[0].sugaku_tensuu = 90;

	int i = 0;

	for (i = 0; i<2; i = i + 1) 
	{
		printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].kumi[0].seitomei, student[i].kumi[0].kokugo_tensuu, student[i].kumi[0].sugaku_tensuu);
	}

	system("pause");// 「続行するには何かキーを押してください . . .」の待機命令

	return 0;
}
実行結果
名前: 山田gggg, 国語: 80点, 数学: 70点
名前: 佐藤, 国語: 60点, 数学: 90点

となる。(※ 得点は最初の例と同じまま)


では、実際に2学級目のデータを入れたコードを作成してみて、実行して試してみよう。


コード例
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct seisekihyou {
	char seitomei[100];
	int syusseki_bangou;
	int kokugo_tensuu;
	int sugaku_tensuu;
};


struct gakkyuu {
        struct seisekihyou kumi[2];
};


int main(void)
{        
	struct gakkyuu student[2]; // 構造体配列の宣言

	strcpy_s( student[0].kumi[0].seitomei, 10 ,"山田gggg"); 
	student[0].kumi[0].kokugo_tensuu = 80;
	student[0].kumi[0].sugaku_tensuu = 70;

	strcpy_s(student[1].kumi[0].seitomei, 10 , "佐藤");
	student[1].kumi[0].kokugo_tensuu = 60;
	student[1].kumi[0].sugaku_tensuu = 90;

	int i = 0;

	for (i = 0; i<2; i = i + 1) 
	{
		printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].kumi[0].seitomei, student[i].kumi[0].kokugo_tensuu, student[i].kumi[0].sugaku_tensuu);
	}


        // 2学級目
	strcpy_s( student[0].kumi[1].seitomei, 10 ,"安部"); 
	student[0].kumi[1].kokugo_tensuu = 78;
	student[0].kumi[1].sugaku_tensuu = 65;

	strcpy_s(student[1].kumi[1].seitomei, 10 , "小泉");
	student[1].kumi[1].kokugo_tensuu = 50;
	student[1].kumi[1].sugaku_tensuu = 100;

	// int i = 0; // 宣言済みなのでコメントアウト

	for (i = 0; i<2; i = i + 1) 
	{
		printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].kumi[1].seitomei, student[i].kumi[1].kokugo_tensuu, student[i].kumi[1].sugaku_tensuu);
	}


	system("pause");// 「続行するには何かキーを押してください . . .」の待機命令

	return 0;
}
実行結果
名前: 山田gggg, 国語: 80点, 数学: 70点
名前: 佐藤, 国語: 60点, 数学: 90点
名前: 安部, 国語: 78点, 数学: 65点
名前: 小泉, 国語: 50点, 数学: 100点

このように、たしかに2学級目も、データベースを作成できている。


for文を使って短くすれば、下記のようになる。

コード例
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct seisekihyou {
    char seitomei[100];
    int syusseki_bangou;
    int kokugo_tensuu;
    int sugaku_tensuu;
};


struct gakkyuu {
    struct seisekihyou kumi[2];
};


int main(void)
{        
    struct gakkyuu student[2]; // 構造体配列の宣言


    // 1学級目
    strcpy_s( student[0].kumi[0].seitomei, 10 ,"山田gggg"); 
    student[0].kumi[0].kokugo_tensuu = 80;
    student[0].kumi[0].sugaku_tensuu = 70;

    strcpy_s(student[1].kumi[0].seitomei, 10 , "佐藤");
    student[1].kumi[0].kokugo_tensuu = 60;
    student[1].kumi[0].sugaku_tensuu = 90;


    // 2学級目
    strcpy_s( student[0].kumi[1].seitomei, 10 ,"安部test"); // 前コードとの実行結果の区別のため、文字を追加
    student[0].kumi[1].kokugo_tensuu = 78;
    student[0].kumi[1].sugaku_tensuu = 65;

    strcpy_s(student[1].kumi[1].seitomei, 10 , "小泉");
    student[1].kumi[1].kokugo_tensuu = 50;
    student[1].kumi[1].sugaku_tensuu = 100;

    int i = 0;

    for(int temp = 0; temp < 2 ; temp = temp +1 ){
        for (i = 0; i<2; i = i + 1) 
        {
            printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].kumi[temp].seitomei, student[i].kumi[temp].kokugo_tensuu, student[i].kumi[temp].sugaku_tensuu);
        }
    } // for temp の終わり

    system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
    return 0;
}
実行結果
名前: 山田gggg, 国語: 80点, 数学: 70点
名前: 佐藤, 国語: 60点, 数学: 90点
名前: 安部test, 国語: 78点, 数学: 65点
名前: 小泉, 国語: 50点, 数学: 100点


上述のような方法だと、確保される1学級あたりの要素数(生徒数に相当)は、どの学級も同じである。

一つの学級だけ多くの要素数を確保しようと思っても、不可能である。

C言語の配列はそもそも、標準的な方法では可変長配列を確保していない(C99で可変長配列のサポートとあるが、高度なプログラムのための限定的なものであり、様々な制約があり、決して初心者むけではない。)。それ以前からも malloc 等の関数を使って動的にメモリを確保することとポインタとを組み合わせることで、可変長配列のような機能を実装する事もできるが、コードが難しくなり、メンテナンスが難しくなるだろう。[4] [5]特に、日本語環境などのメモリ確保領域を計算して管理するのは、かなり困難であろう。

このため、構造体配列でも、mallocなどを用いない簡単な方法では、可変長配列を実装することは無理そうである。

よって、あきらめて、すべての構造体配列の1つの配列あたりの要素数を同じにするのが良い。

どうしても異なる要素数の構造体配列が必要なら、ネストとは別々に新規の構造体配列として、その配構造体列を分けるべきだろう。


なお、Linux版のコード例を、本節の最後の例なので紹介する。

Linux版のコード例
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。

struct seisekihyou {
    char seitomei[100];
    int syusseki_bangou;
    int kokugo_tensuu;
    int sugaku_tensuu;
};


struct gakkyuu {
    struct seisekihyou kumi[2];
};


int main(void)
{        
    struct gakkyuu student[2]; // 構造体配列の宣言


    // 1学級目
    strcpy( student[0].kumi[0].seitomei, "山田gggg"); 
    student[0].kumi[0].kokugo_tensuu = 80;
    student[0].kumi[0].sugaku_tensuu = 70;

    strcpy(student[1].kumi[0].seitomei,  "佐藤");
    student[1].kumi[0].kokugo_tensuu = 60;
    student[1].kumi[0].sugaku_tensuu = 90;


    // 2学級目
    strcpy( student[0].kumi[1].seitomei, "安部test"); // 前コードとの実行結果の区別のため、文字を追加
    student[0].kumi[1].kokugo_tensuu = 78;
    student[0].kumi[1].sugaku_tensuu = 65;

    strcpy(student[1].kumi[1].seitomei,  "小泉");
    student[1].kumi[1].kokugo_tensuu = 50;
    student[1].kumi[1].sugaku_tensuu = 100;

    int i = 0;

    for(int temp = 0; temp < 2 ; temp = temp +1 ){
        for (i = 0; i<2; i = i + 1) 
        {
            printf("名前: %s, 国語: %d点, 数学: %d点\n", student[i].kumi[temp].seitomei, student[i].kumi[temp].kokugo_tensuu, student[i].kumi[temp].sugaku_tensuu);
        }
    } // for temp の終わり

    // system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
    return 0;
}
※(動作確認ずみ。Fedora33上の gcc および ./a.out にて2021年1月24日に動作確認。)

実行結果はwindows版と同じであるので省略。

二重配列の構造体変数[編集]

構造体配列は、二重配列であっても良い。つまり、構造体変数には、二重配列を宣言する事も可能である。

よって構造体のネストを使わなくても、下記コードのように二重配列でも代用できる。

コード例
#include <stdio.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。
#include <string.h>

struct seisekihyou {
    char seitomei[100];
    int syusseki_bangou;
    int kokugo_tensuu;
    int sugaku_tensuu;
};

int main(void) {
    struct seisekihyou student[3][2]; // 構造体配列の宣言   [組][組内番号]

    strcpy_s(student[0][0].seitomei, 10, "山田123"); // 上記コードとの区別のため
    student[0][0].kokugo_tensuu = 80;
    student[0][0].sugaku_tensuu = 70;

    strcpy_s(student[0][1].seitomei, 10, "佐藤");
    student[0][1].kokugo_tensuu = 60;
    student[0][1].sugaku_tensuu = 90;

    strcpy_s(student[1][0].seitomei, 10, "安部");
    student[1][0].kokugo_tensuu = 78;
    student[1][0].sugaku_tensuu = 65;

    strcpy_s(student[1][1].seitomei, 10, "小泉");
    student[1][1].kokugo_tensuu = 50;
    student[1][1].sugaku_tensuu = 100;

    for (int kumi = 0; kumi <= 1; kumi = kumi + 1) {
        for (int i = 0; i < 2; i = i + 1) {
            printf("名前: %s, 国語: %d点, 数学: %d点\n",
                   student[kumi][i].seitomei, student[kumi][i].kokugo_tensuu,
                   student[kumi][i].sugaku_tensuu);
        }
    }

    system("pause"); // 「続行するには何かキーを押してください . . .」の待機命令


    return 0;
}
実行結果
名前: 山田123, 国語: 80点, 数学: 70点
名前: 佐藤, 国語: 60点, 数学: 90点
名前: 安部, 国語: 78点, 数学: 65点
名前: 小泉, 国語: 50点, 数学: 100点


構造体へのポインタ[編集]

構造体へのポインタを宣言する時は、 変数名の前に「*」をつける。 すなわち、次のように記述する。

struct タグ名 *ポインタの変数名;

ポインタの指す構造体のメンバにアクセスする時は、 変数名の前に「*」をつけ、それらを「()」で囲む。 すなわち、次のように記述する。

(*ポインタの変数名).メンバ名

「*」と「.」とでは、「.」の方が優先順位が高い演算子であるため、 「()」が必要である。

また「->」(アロー演算子)を用いて、 次のように記述でき、 こちらが一般的に使われる。

ポインタの変数名->メンバ名
//例 構造体へのポインタ
#include <stdio.h>
#include <string.h>

int main(void)
{
	struct
	{
		int i;
		double d;
		char c;
		char str[32];
	}kouzoutai, *pkouzoutai;

	pkouzoutai=&kouzoutai;

	pkouzoutai->i=1234;
	pkouzoutai->d=3.14;
	pkouzoutai->c='a';
	strcpy(pkouzoutai->str,"Hello, World!");

	printf("kouzoutaiのメンバの値は、%d %f %c %sです。\n",
		pkouzoutai->i, pkouzoutai->d, pkouzoutai->c, pkouzoutai->str);
}

上の例では、構造体へのポインタを使って、 構造体のメンバに値を代入したり、 その値を表示したりしている。


なお、strcpyは、文字列をコピーするための命令。strcpyを使うには、string.hのインクルードが必要である。

また、文字列の宣言は、(上記の char str[32]; のように)配列として宣言するのが一般的である。

構造体のポインタを引数にした関数[編集]

(アロー演算子を使わずに、そのまま)大きな構造体を引数に指定すると、 実引数から仮引数へその構造体全体をコピーするため、 時間がかかってしまう。

高速化したい場合、構造体を引数として呼び出す際に、構造体をコピーせずに(引数として)参照できるようにすれば良いので、そのためにはポインタを活用する必要がある。

構造体へのポインタを引数に指定すると、引数のコピーをせずに元々の構造体を参照するので、コピーの時間が省略されるしメモリも節約されるので、高速に処理できる。

このような処理において、構造体を指すポインタのメンバにアクセスする場合には、(前の節で上述した)アロー演算子を使う。

//例 引数に大きな構造体を指定する
#include <stdio.h>

//この構造体が非常に大きな構造体であるとする
typedef struct
{
	int i;
	double d;
	char c;
	char str[32];
} sKouzoutai;

void function(sKouzoutai *kouzoutai) //構造体へのポインタを引数に指定する
{
	printf("kouzoutaiのメンバの値は、%d %f %c %sです。\n", kouzoutai->i, kouzoutai->d, kouzoutai->c, kouzoutai->str);
}

int main(void)
{
	sKouzoutai kouzoutai = {1234, 3.14, 'a', "Hellol. World!" };
	function(&kouzoutai) ;
}

function(sKouzoutai *kouzoutai) がポインタで呼び出せるようにするために、渡す側 function(&kouzoutai) はアドレスを渡している。

メンバに同じ型へのポインタを持つ構造体[編集]

構造体のメンバに、自分自身と同じ型へのポインタを指定することができる。 このような構造体を自己参照構造体と呼ぶ。

//例 自己参照構造体
#include <stdio.h>

int main(void)
{
	struct sKouzoutai
	{
		int i;
		double d;
		char c;
		char str[32];
		struct sKouzoutai *next;
	};
	
	struct sKouzoutai a={123, 1.23, 'a', "abc"},
			b={456, 4.56, 'b', "def"},
			c={789, 7.89, 'c', "ghi"};
	struct sKouzoutai *p;	

	a.next=&b;
	b.next=&c;
	c.next=NULL;

	for(p=&a; p; p=p->next)
		printf("%d %f %c %s\n",
			p->i, p->d, p->c, p->str);
}

上の例では、 3つの構造体a、b、cのメンバを、 ポインタpを用いて、表示している。

脚注[編集]

  1. ^ 『JISX3010:2003』p.24「6.2.5 型」
  2. ^ 構造体の型枠という用語はこの教科書独自のものであり、『JISX3010:2003』に出てくるわけではないので注意してください。
  3. ^ Wisdomソフト『7.3.1 ポインタにキャストする』、文「異なる構造体形は型に互換性がないため、代入は認められないのです。」
  4. ^ teratail(webサイト)『C 入れ子構造の構造体にて配列を可変長にしたい』、投稿 2016/04/29 11:56 2020年1月23日に閲覧して確認.
  5. ^ https://stackoverflow.com/questions/32311269/can-we-have-a-struct-element-of-type-variable-length-array 2021年4月13日に閲覧して確認.