C言語編 第26章 構造体

この章の概要

この章の概要です。

構造体

構造体は、複数の変数を1つにまとめたものです。 配列とは違い、含まれる要素(構造体の場合はメンバフィールドなどと呼びます)の型は異なっても構いません。

まずは使い方を確認しておきましょう。

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

構造体を表すキーワード struct を使います。 ここで注意してほしいことは、上のような形式で書いたとき、これはまだ「構造体を変数として宣言していない」という点です。 上の記述は、「新しい型を作った(型を定義した)」だけです。
つまり、標準として用意されている int型や double型のようなものに追加して、自分で新しい型を作り出しているのです。 この時点では、まだ型を作っただけに過ぎないので、実際に使うにあたっては、変数として宣言する必要があります。

また、「タグ名」というものが登場しています。 タグ(構造体タグ)は、新しく構造体として定義した型を識別するための名前です(タグとは名札のことです)。 これは実際に、構造体を使ったプログラムで確認した方が分かりやすいでしょう。

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

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
struct Student_tag {
	char  name[STUDENT_NAME_LEN];   /* 名前 */
	int   grade;                    /* 学年 */
	int   class;                    /* 所属クラス */
	int   score;                    /* 得点 */
};


void printStudentData(struct Student_tag student);

int main(void)
{
	struct Student_tag student;

	strcpy( student.name, "Saitou Takashi" );
	student.grade = 2;
	student.class = 3;
	student.score = 80;

	printStudentData( student );
	
	return 0;
}

/*
	生徒のデータを出力する。
	引数:
		student: 出力するデータを集めた構造体変数。
*/
void printStudentData(struct Student_tag student)
{
	printf( "name: %s\n", student.name );
	printf( "grade: %d\n", student.grade );
	printf( "class: %d\n", student.class );
	printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

構造体の定義を、関数の外側、つまりグローバルな場所で行っていますが、実際には関数内に書いても構いません

複数のソースファイルで同じ構造体を使うのなら、ヘッダファイルに構造体の定義を書きます。 ただしその場合、複数の箇所から #include で取り込まれると問題が起きることがあります。 この問題は第24章で取り上げたように、インクルードガードを行うことで対処できます。

タグ名の末尾に「_tag」と付けていますが、これは必須ではありません。 タグ名は、変数や関数の名前と同様の制限しかないので、自由に付けられます。

構造体を実際に使うには、main関数の最初のところで行っているように、変数として宣言します。 なお、「構造体を宣言する」という表現を使うと、型を作る段階と、変数を宣言する段階との区別が付きにくいので、 前者は「構造体型を定義する」、後者は「構造体変数を宣言する」のように明確に表現するべきです
構造体の理解が進まない人の多くは、この2つの段階の区別が付いていないように思います。

構造体変数を宣言するには、

struct タグ名 構造体変数名;

のように記述します。 structキーワードとタグ名を両方セットで書かないといけない点に注意して下さい。

C++ の場合、structキーワードが省略可能ですが、C言語では不可です。

続いて、構造体メンバをどうアクセスするかですが、これにはドット演算子を使います。

構造体変数名.メンバ名;

構造体メンバもまた、変数そのものですから、

student.score = 80;
printf( "%d\n", student.score );

のような感じで使えます。

最後に、構造体変数をそのまま引数や戻り値に使える点も重要です。 ここでも、仮引数や戻り値の型の指定は、「struct 構造体タグ名」というセットで行います。

構造体変数をそのまま渡すことは確かに可能ですが、実際には、構造体はデータ量が多いため、処理効率的にはあまり良くありません。 そこで、ポインタを利用するのが一般的です。これについては、第33章で取り上げます。 それでも、int型のメンバが 2個の構造体のように、比較的小さいものなら、そのまま渡しても特に問題ありません。

構造体の初期化

構造体変数を宣言したときに、同時に初期値を与えて初期化することも可能です。 その場合、次のように記述します。

struct タグ名 構造体変数名 = { 1つ目のメンバの初期値, 2つ目のメンバの初期値, … };

この構文を使って、最初のサンプルプログラムを書き換えると、次のようになります( main関数だけ抜粋)。

int main(void)
{
	struct Student_tag student = { "Saitou Takashi", 2, 3, 80 };

	printStudentData( student );
	
	return 0;
}

便利ではありますが、あとからメンバを間に挟み込むようなことをすると、初期値がずれてしまう点には注意が必要です。

C99 では、この問題への対処として、要素指示初期化子というものが導入されています。

C99 (指示付きの初期化子)

C99 では、指示付きの初期化子という機能が追加されています。 これを使うと、特定のメンバを選んで初期値を与えることができます。

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

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
struct Student_tag {
	char  name[STUDENT_NAME_LEN];   /* 名前 */
	int   grade;                    /* 学年 */
	int   class;                    /* 所属クラス */
	int   score;                    /* 得点 */
};


void printStudentData(struct Student_tag student);

int main(void)
{
	struct Student_tag student = { 
		.name = "Saitou Takashi",
		.grade = 2,
		.class = 3,
		.score = 80
	};

	strcpy( student.name, "Saitou Takashi" );
	student.grade = 2;
	student.class = 3;
	student.score = 80;

	printStudentData( student );
	
	return 0;
}

/*
	生徒のデータを出力する。
	引数:
		student: 出力するデータを集めた構造体変数。
*/
void printStudentData(struct Student_tag student)
{
	printf( "name: %s\n", student.name );
	printf( "grade: %d\n", student.grade );
	printf( "class: %d\n", student.class );
	printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

構造体変数 student の初期化のところを見て下さい。 「.name = "Saitou Takashi"」という記述によって、name というメンバのところに初期値 "Saitou Takashi" が与えられます。 このように、「.メンバ名 = 初期値」という構文が使えるようになりました。

VisualC++ 2013/2015、clang 3.7 のいずれも対応しています。

メンバ全体をまとめた操作

構造体変数を、そのまま引数や戻り値で受け渡しできるのは、同じ型の構造体変数はそのまま代入できるからです。 引数や戻り値の受け渡しという処理は、代入処理に他なりません。
試してみると…、

int main(void)
{
	struct Student_tag student1 = { "Saitou Takashi", 2, 3, 80 };
	struct Student_tag student2;
	
	student2 = student1;

	printStudentData( student1 );
	printStudentData( student2 );
	
	return 0;
}

これはうまく動作してくれます。

ところで、ここに少し不思議な点があります。 struct Student_tag という構造体のメンバには、char型の配列が含まれています。 これまでに何度か見てみたように、文字列をそのまま代入することはできず、strcpy関数(⇒リファレンス)の力を借りる必要があったはずです。

実は、構造体同士の代入は、中身が何であれ成功します。 メンバとして配列が含まれていれば、その配列の要素をそのままコピーしてくれるので、問題なく動作します。
だからといって、文字列をいつも構造体のメンバとして宣言すべしと言っている訳ではありません。 文字列の代入が必要なら、strcpy関数を使えばいいだけのことです。 ここでは、構造体の代入操作の性質を説明しているだけです。

メンバとしてポインタが含まれている場合、ポインタの複製ができます。 その場合、同じ場所を指すポインタが2つ存在するという状況になることに注意が必要です。 なお、ポインタについては、第31章で取り上げます。


代入の他によく行う操作は、比較でしょうか。 2つの構造体変数のメンバが一致しているかどうかを調べたいとします。 しかし、「if(student1 == student2)」のような比較は、(出来ても良さそうなものですが)出来ません。 これはコンパイルエラーになります。

構造体同士を比較するための唯一の手段は、メンバの一致を1つ1つ調べることです。 面倒な作業ですし、後からメンバが増えたときに、忘れずに比較処理を修正する必要もあります。

memcmp関数(⇒リファレンス)というもので一致を調べようとする人がいますが、それは正しくありません。

タグが省略される場合

ここから少し混乱させられる部分です。 構造体に関するこれまでの部分が曖昧なら、少し整理してから読み進めて下さい。

構造体を定義する際、タグ名を指定しましたが、実はタグ名を省略できる場合があります。 次のように書いた場合です。

struct {
	型 メンバ名;
	型 メンバ名;
	  :
} 構造体変数名;

これは、構造体の型の定義と、構造体変数の宣言とを同時に行っており、この場合はタグ名が不要になります。 ついでに初期値を与えても構いません。

struct {
	型 メンバ名;
	型 メンバ名;
	  :
} 構造体変数名 = { 1つ目のメンバの初期値, 2つ目のメンバの初期値, … };

ただ、この方法の場合、構造体の型の名前を表現できないため、他の場所で型の名前が必要な場合には使えません。 例えば、関数に構造体変数を渡そうにも、型名が表現できないと、仮引数の宣言が記述できません。
この方法を使った場合、この章の最初のサンプルは次のように書き換えることになります。

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

int main(void)
{
	#define STUDENT_NAME_LEN 32			/* 生徒の名前データの最大長 */

	/* 生徒のデータ */
	struct {
		char  name[STUDENT_NAME_LEN];   /* 名前 */
		int   grade;                    /* 学年 */
		int   class;                    /* 所属クラス */
		int   score;                    /* 得点 */
	} student;

	strcpy( student.name, "Saitou Takashi" );
	student.grade = 2;
	student.class = 3;
	student.score = 80;

	printf( "name: %s\n", student.name );
	printf( "grade: %d\n", student.grade );
	printf( "class: %d\n", student.class );
	printf( "score: %d\n", student.score );
	
	return 0;
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

typedef

タグ名を省略すると、型として表現できなくなってしまいましたが、 そこで更に typedef というキーワードを持ち出せば状況が変わります。
typedef は、新たな型名を作り出すキーワードで、 構造体とは直接的には関係しませんが、構造体とセットで使われることは多いです。

先に構造体とは関係しない場面での使い方を見ておきましょう。 まず、typedef の構文は次のようになります。

typedef 既存の型名 新しい型名;
既存の型名 typedef 新しい型名;

いずれでも構いませんが、ほとんどの場合、前者の方が使われています。 既存の型名というのは、例えば、int や double といったものですが、自分で定義した構造体や、配列を指定することもできます。 新しい型名は自由に付けられます。

1つの利用法は、環境によってサイズが異なってしまう型への対応です。 例えば、int型が 32bit の環境ばかりとは限りません。 そこで必ず 32bit であることが保証された型を、typedef を利用して作り出します。

typedef int int32;   /* int型が 32bit の環境ならこちら */
typedef long int32;  /* long型が 32bit の環境ならこちら */

環境に合わせて、どちらか一方だけが有効になるようにしておき( #if とか #ifdef とかで切り分けられるかも知れません)、 あとはプログラム中で、32bit の整数が必要なときには、常に int32 という型名を使うようにすれば良くなります。

現実にはそんなに簡単にはいかないことが多いでしょうが、こういう手法自体は有効です。 また、C99 には stdint.h という新しい標準ヘッダに、同様の趣旨の定義が存在します。

新しい型名は、所詮、新しい「名前」に過ぎず、新しい「型」が出来た訳ではありません。 もし、まったく別の型が作られたのであれば、上記の例で、int32型の変数を int型の変数には代入できないことになりますが、実際には代入できます。 両者は、同じ型の別の名前なのです。

#include <stdio.h>

int main(void)
{
	typedef int int32;

	int   a = 99;
	int32 b = a;
	int   c = b;

	printf( "%d\n", a );
	printf( "%d\n", b );
	printf( "%d\n", c );
	
	return 0;
}

実行結果:

99
99
99


構造体の例に戻ります。 まずは、最初のサンプルを typedef を使ったものに書き換えます。

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

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
typedef struct {
	char  name[STUDENT_NAME_LEN];   /* 名前 */
	int   grade;                    /* 学年 */
	int   class;                    /* 所属クラス */
	int   score;                    /* 得点 */
} Student;


void printStudentData(Student student);

int main(void)
{
	Student student;

	strcpy( student.name, "Saitou Takashi" );
	student.grade = 2;
	student.class = 3;
	student.score = 80;

	printStudentData( student );
	
	return 0;
}

/*
	生徒のデータを出力する。
	引数:
		student: 出力するデータを集めた構造体変数。
*/
void printStudentData(Student student)
{
	printf( "name: %s\n", student.name );
	printf( "grade: %d\n", student.grade );
	printf( "class: %d\n", student.class );
	printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

typedef を使った場合の構造体の定義は次のように書きます。

typedef struct タグ名(省略可) {
	型 メンバ名;
	型 メンバ名;
	  :
} 型名;

typedef なので、新しい型名を記述する必要があり、typedef の構文上、新しい型名は最後に書くことになります。 typedef を使わない場合、この場所には構造体変数名が来るので分かりづらいですが、typedef を使うとこうなります。
また、typedef があれば、タグ名は省略できるようになります。 タグ名を書けば従来通り、struct Student_tag のように型名を指定することもできます。
両方とも書く場合の名前の衝突に備えて、タグ名の方には「_tag」を付けている訳ですが、 実は両者の名前が同じでも文法上は許されます。単に、分かりにくいだけなので、やめた方が無難ですが。

typedef の構文と照らし合わせると、

struct {
	char  name[STUDENT_NAME_LEN];
	int   grade;
	int   class;
	int   score;
}

ここまでが「既存の型名」で、最後の「Student」が「新しい型名」です。 この辺、ちゃんと整理して理解しておくべきです。
また、次のように分けて書いても構いません。

struct Student_tag {
	char  name[STUDENT_NAME_LEN];
	int   grade;
	int   class;
	int   score;
};
typedef struct Student_tag Student;


typedef したことにより、struct Student_tag と書いていた箇所は、全て単なる Student だけで済むようになります。 両者はまったく同じものを表すことになり、混在しても構文上は問題ありません(分かりにくいので混在は避けるべきです)。

自己参照

構造体のメンバとして、構造体変数を持たせることも可能ですが、 自分自身と同じ型は不可です。

struct Student_tag {
	int num;
	struct Student_tag other_student;  /* エラー */
};

これは実現したい場面もあるので、ポインタを使った解決策が存在します。 これについては、第36章で取り上げます。

構造体のネスト

構造体型を入れ子にして、次のように記述することが可能です。

struct Student_tag {
	struct Score_tag {
		int  math;
		int  english;
	};
	
	char  name[STUDENT_NAME_LEN];
};

内側にある構造体のメンバへは、ドット演算子を使って普通にアクセスできます。

struct Student_tag student;
student.math = 75;
student.english = 85;

また、内側の構造体型の変数を宣言することができます。

struct Score_tag score;
score.math = 75;
score.english = 85;

このような使い方をすると、最早、Student とは無関係に、特定を保持する構造体としての意味だけになってしまうので、 入れ子にする意味がありません。 そもそも、構造体を入れ子にすることの価値は、あまり無いと思います。

C++ では、内側の構造体(クラス)が新たなスコープを形成します(C++編【言語解説】第24章)。 C++ においては、入れ子の構造体(クラス)には、それなりの価値があります。


練習問題

問題@ 平面上にある点の座標(x,y) を表現できる Point構造体を作成して下さい。 座標は int型で表現するものとします。

問題A 問題@の Point構造体について、次のような関数を作成して下さい。

問題B 問題@の Point構造体を使って、四角形を表現できる Rect構造体を作成して下さい。


解答ページはこちら

参考リンク

更新履歴

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/12/23 「構造体のネスト」の内容を修正し、元あった内容を「自己参照」の項へ移動。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++ 2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/2/1 VisualC++ 2013 に対応。

'2014/1/14 VisualC++ 2008 の対応終了。

'2014/1/11 clang 3.0 に対応。

'2013/4/21 「C99 (指示付きの初期化子)」の項を追加。

'2009/8/30 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ