C言語/構造体・共用体

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

構造体[編集]

構造体の基本[編集]

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

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

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

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


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

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


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

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

//例 構造体の宣言

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

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


構造体を使うには、まず構造体の型枠を宣言し、 次にその型枠を使って構造体を宣言しなければならない。 [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つのメンバの値を表示している。

構造体のコピー[編集]

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

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

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

//例 構造体のコピー
#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);
}

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


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

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

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を用いて、表示している。

構造体のネスト[編集]

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

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

//例 構造体のネスト
#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] 共用体は、メモリ上の一箇所を、 2つ以上のメンバで共有する方法である。 共用体の使い方は、構造体と似ているが、 共用体の全てのメンバは、 メモリ上の一箇所に格納されている点が異なる。 共用体のサイズは、 共用体のもっとも大きなメンバのサイズになる。

共用体の型枠の宣言[編集]

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

共用体の型枠の宣言は次のように記述する。

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

共用体の宣言[編集]

共用体の宣言は次のように記述する。

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

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

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

共用体のメンバへのアクセス[編集]

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

共用体の変数名.メンバ名
//例 共用体
#include <stdio.h>

int main(void)
{
	struct sbits
	{
		// リトルエンディアン
		unsigned char bit8:1;
		unsigned char bit7:1;
		unsigned char bit6:1;
		unsigned char bit5:1;
		unsigned char bit4:1;
		unsigned char bit3:1;
		unsigned char bit2:1;
		unsigned char bit1:1;
	};

	union
	{
		unsigned char c;
		struct sbits bits;
	}u;

	u.c=170;
	printf("u.cの値は%dです。\n", u.c);
	printf("u.cのビット表現は%d%d%d%d%d%d%d%dです。\n",
		u.bits.bit1,u.bits.bit2,u.bits.bit3,u.bits.bit4,
		u.bits.bit5,u.bits.bit6,u.bits.bit7,u.bits.bit8);
}

上の例では、 ビットフィールドと共用体を用いて、 ある値のビット表現を表示している。 このような使い方をする場合、エンディアン(複数バイトをどのような順で格納するかの形式)に注意せよ。

脚注[編集]

  1. ^ 1.0 1.1 『JISX3010:2003』p.24「6.2.5 型」
  2. ^ 構造体の型枠という用語はこの教科書独自のものであり、『JISX3010:2003』に出てくるわけではないので注意してください。
  3. ^ 共用体の型枠という用語はこの教科書独自のものであり、『JISX3010:2003』に出てくるわけではないので注意してください。

参考文献[編集]

  • 日本工業標準調査会『JISX3010 プログラム言語C』2003年12月20日改正