第5週 構造体
INDEX

本日の目標

  • 対象とするデータを構造体として表現することができる。
  • 構造体を使った簡単なプログラムをつくることができる。

予習・復習

 以下のスライドを利用して、予習と復習をしよう。復習では、自分の理解度を確認するために、実際にプログラムを作成し、意図する結果が得られるか確認しよう。
  1. 構造体

本日の講義・演習予定

  1. 構造体
  2. 構造体を配列で利用する
  3. 構造体の中の構造体
  4. 構造体を戻り値とする関数
  5. 共用体
  6. 列挙体
  7. 演習問題
  8. 提出課題
内 容
  1. 構造体
  2. 構造体を配列で利用する
  3. 構造体の中の構造体
  4. 構造体を戻り値とする関数
  5. 共用体
  6. 列挙体

構造体

構造体とは

 構造体とは、データ型の異なる複数のデータを一つにまとめて、一つの変数として扱うことのできる仕組みです。新しいデータ型を作ることができる仕組みとも言えます。

複数のデータを一つにまとめる

 例えば、2次元座標はx座標とy座標の2つのデータを持ちます。3次元座標ならばz座標を加えた3つのデータを持ちます。また、日付ならば年、月、日の3つのデータを持ちます。曜日を加えると4つのデータが必要となります。複数のデータから構成される情報を一つの名前で扱うための方法には配列があります。例えば、2次元座標ならばint point[2]として、x座標をpoint[0]で、y座標をpoint[1]として表すことができます。しかし、例えばクラスの生徒の出席番号と氏名と身長を配列で扱うことはできません。配列で複数のデータを扱うためには全てのデータ型が同じでなければならないからです。構造体ならば、異なるデータ型を含む場合でも一つの変数として表現することができるのです。

構造体の定義

 構造体を利用するためには、まず構造体を定義しておき、次にその構造体をデータ型とする構造体変数を宣言する必要があります。構造体の定義は以下に示すように、先頭にstructと、構造体タグ名と呼ばれる構造体を区別するための名前を記述します。その後に、構造体を構成するメンバを波カッコ{}で括ります。メンバにはchar型やint型などの基本データ型に加え、配列、さらには構造体を含めることができます。
[定義]
struct 構造体タグ名
{
   メンバ名1;
   メンバ名2;
   ・・・
};
 

[構造体定義の例] x座標とy座標から2次元座標。
struct point {
	int x;
	int y;
};

struct_coordinates
Fig5-1.構造体struct dateのイメージ

構造体の宣言

 構造体を型とする変数の宣言は、struct 構造体タグ名の直後に変数名を付けます。これをオブジェクト名とも呼びます。
struct 構造体タグ名 オブジェクト名 [, オブジェクト名, ・・・];

 [構造体変数の宣言の例] 構造体struct dateを型とする変数の宣言の例です。
struct point p;

構造体変数の参照

 構造体変数の各メンバの値を参照するためには、構造体変数名とメンバ名の間をドット(.)を使って接続します。
構造体変数名.メンバ名

 次の例では、構造体変数名をpとして、そのメンバーであるx,yを参照しています。
#include <stdio.h>

struct point{
	int x;
	int y;
};

int main(void)
{	
	struct point p;
	
	p.x = 5;
	p.y = 8;
	
	printf("座標(%d,%d)\n", p.x, p.y);

	return 0;
}

構造体変数の初期化

 構造体変数を初期化したい時には、変数名の右辺に構造体の各メンバに代入したい値を波括弧の中にカンマで区切って記述します。
 次の例では、構造体変数pointのメンバxとyの値をそれぞれ5と8で初期化しています。
#include <stdio.h>

struct point{
	int x;
	int y;
};

int main(void)
{	
	struct point p = {5, 8};		//構造体変数の初期化
		
	printf("座標(%d,%d)\n", p.x, p.y);

	return 0;
}

構造体変数の演算

構造体変数を使った演算では、構造体変数が持つメンバに対して個別に演算をする必要があります。以下の例のように同じ構造体の変数同士の同じ演算であっても直接演算をすることはできません。
#include <stdio.h>

struct point{
	int x;
	int y;
};

int main(void)
{	
	struct point a = {2,3}, b = {4,5};

	a.x += b.x;
	a.y += b.y;
	// a += b; とはできない!
	
	printf("(%d, %d)\n", a.x, a.y);

	return 0;
}

構造体変数のコピー

 同じ構造体型の変数同士のコピーならば、左辺の構造体変数へ右辺の構造体変数を代入すればその各メンバーの値もコピーされます。
 次の例では、キーボードから入力した座標の値を構造体変数aに格納した後に構造体変数bにコピーしています。最後に、構造体変数aとbを表示して、各メンバーの値がコピーされたことを確認しています。
#include <stdio.h>

struct point{
	int x;
	int y;
};

int main(void)
{	
	struct point a,b;

	printf("座標A(x,y)> ");
	scanf("%d,%d", &a.x, &a.y);

	b = a;		//構造体のコピー

	printf("座標a(%d,%d)\n", a.x, a.y);
	printf("座標b(%d,%d)\n", b.x, b.y);

	return 0;
}

typedefを使って構造体に別名をつける

 構造体変数を宣言する場合は、構造体変数名の前にstructと構造体タグ名を付けなければならず、基本データ型の変数宣言に較べて記述が長くなります。そこで、typedefを使って構造体に別の名前を付けて、これを新しい型名として利用できるようにします。
  1. 構造体宣言後に別名宣言
  2.  次の例では、構造体struce pointを宣言した後に、typedefを使ってこの構造体に新しい型名Pointを付けます。先頭文字Pが大文字です。main関数では新しく定義された型名Pointを使って変数pointを宣言しています。
    ・・
    struct point {
    	int x;
    	int y;
    };
    typedef struct point Point;
    
    int main(void)
    {
    	Point point;
    	・・・
    
    

  3. 構造体宣言と同時に新しい型名を定義する
  4.  構造体宣言にtypedefを直接使って、以下の例のように別の型名を定義することができます。
    typedef struct point {
    	int x;
    	int y;
    } Point;
    

  5. 構造体タグ名と同じ型名を定義する
  6.  構造体宣言に直接typedefを使う場合、新しく定義する型名には構造体タグ名と同じ名前を使うことができます。
    typedef struct Point {
    	int x;
    	int y;
    } Point;
    

  7. 構造体タグ名を省略する
  8.  構造体宣言に直接typedefを使う場合、構造体タグ名を省略して型名を定義することができます。
    typedef struct {
    	int x;
    	int y;
    } Point;
    

  9. 複数の型名を定義する
  10.  typedefは複数の別名を定義することができます。次の例ではメンバxとyを持つ構造体を、位置を示すPointとx方向とy方向への移動量を示すVectorの2つの意味を持たせようと定義しています。
    typedef struct {
    	int x;
    	int y;
    } Point, Vector;
    

異なるデータ型からなる構造体の例

 音楽ライブラリを管理するためのプログラムを作成することにします。楽曲を表す情報には、曲名、演奏時間、データサイズなどがあります。これらをまとめて構造体songで表すと、
struct song{
	 char name[128];		//曲名
	 int time;			//演奏時間(秒)
	 double size;			//データサイズ(MB)
}
となります。タイトルには文字列を格納するためにchar型配列を、演奏時間にはint型を、データサイズには小数点を含む数を扱えるようにdouble型としました。ある楽曲Aを構造体songを使って表せば、「Aの曲名」、「Aの演奏時間」、「Aのデータサイズ」のようにAという名前でひとくくりにして管理することができるようになります。(文字列の扱い方についてはプログラミング4で詳しく学びます。)
構造体のイメージ
構造体
 以下に、構造体songを定義して利用する例を示します。
例 song.c
#include <stdio.h>

struct song{
	char name[128];
	int time;
	double size;
};
typedef struct song Song;

int main(void)
{
	Song sakura= {"SAKURA", 281, 12.5};
	
	printf("曲名: %s  演奏時間: %d(sec) サイズ:%.2f(MB) ¥n", sakura.name, sakura.time, sakura.size);	
	
	return 0;
}


[補]typedefによる識別子の付け方  ソースコードの中で基本型とは別によく見かける型名に, time_tやsize_tなど語尾に_tが付いたものがあります。これらは標準ライブラリの中でtypedefを使って定義されていることを示しています。例えば、time_t型はtime.hで、size_t型はstddef.hでtypedefを使って定義されています。この標準ライブラリで利用されているルールを真似てユーザプログラムにおいて語尾に_tを付けるケースが見られますが、必ずしも_tを付けなければならないということではありません。


構造体を配列で利用する

 先に定義した構造体pointは1つの2次元座標を表します。複数の座標をまとめて扱うためには、構造体変数を配列として宣言して、1つの座標を1つの要素とすることで実現することができます。

構造体の配列の初期化

 次は、構造体pointの配列を使って4つの二次元座標を扱うプログラム例です。構造体変数の初期化は、要素ごとに構造体定義でのメンバの記述順に合わせて、x座標、y座標の順に波カッコ内にカンマで区切って記述していきます。また、各要素のメンバの参照は配列名[添字].メンバ名となります。
points.c
#include <stdio.h>
#define N 4

typedef struct point{
	int x,y;
} Point;

int main(void)
{
	Point p[N] = {{0,0}, {0,5}, {5,5}, {5,0}}			 		//構造体変数の初期化
	int i;
	
	for(i=0; i<N; i++)
		printf("座標%d(%d, %d)¥n", i, p[i].x, p[i].y);		

	return 0;
}

キーボードから構造体の配列要素のメンバーの値へ入力する

 構造体の配列の各要素のメンバーの値をscanfを使ってキーボードから読み込む例です。
struct_array_input.c
#include <stdio.h>
#define N 4

typedef struct point{
	int x, y;
} Point;

int main(void)
{
	Point p[N];
	int i;

	for(i=0; i<N; i++){
		printf("座標%d/n",i+1);
		printf("x>> ");		scanf("%d", &p[i].x);
		printf("y>> ");	scanf("%d", &p[i].y);
	}

	for(i=0; i<N; i++)
		printf("座標%d(%d, %d)¥n", i, p[i].x, p[i].y);		

	return 0;

}


構造体の中の構造体

構造体をメンバとする構造体

 2次元直交座標系における直線を表す構造体lineを考えましょう。直線を表すためには始点と終点の2つの座標が必要であることから、構造体には2組のx座標とy座標、合計4つのメンバを定義すれば良いことがわかります。ですが、構造体lineとは別に2次元座標を表すための構造体pointを定義しておけば、構造体lineではstruct point型の2つメンバを定義すればよいことになります。
 さらに、ここでは、直線が実線か点線かを示すための属性を与えました。データ型をchar型として、0xffなら直線を、0x00なら線なしとして、その中間を16段階(8bit)で点線のパターンを表すことができます。
struct_member.c
#include <stdio.h>

typedef struct {
	int x, y;
} Point;

struct line{
	int id;				//ID
	Point start;			//始点
	Point end;			//終点
	char attribute;			//属性
};
typedef struct line Line;

int main(void)
{
	Point a = {0,0};
	Point b = {200,350};
	Line line = {1000,a,b,0xff};
	
	printf("ID   : %d\n",line.id);
	printf("始点: (%d,%d)\n", line.start.x, line.start.y);
	printf("終点: (%d,%d)\n", line.end.x, line.end.y);
	printf("属性: 0x%02x\n", line.attribute);

	return 0;
}	

構造体の中で構造体を定義

 次のプログラムも先と同様に2次元座標系における直線を構造体lineとして定義している例です。先の例と異なる点は、構造体の中で構造体を定義していることです。
struct_in_struct.c
#include <stdio.h>

struct line{
	int id;
	struct point {
		int x;
		int y;
	} start,end;		//始点,終点
	char attribute;			//属性
};
typedef struct line Line;

int main(void)
{
	Line line = {1000,0,0,100,200,0xff};
	
	printf("id  : %d\n",way.id);
	printf("始点: (%d,%d)\n",line.start.x,line.start.y);
	printf("終点: (%d,%d)\n",line.end.x,line.end.y);
	printf("属性: 0x%02x\n",line.attribute);

	return 0;
}



構造体を関数に渡す、関数の戻り値にする

構造体を関数で扱う

 以前、関数はその戻り値として一つの値のみしか返せないと説明しました。しかし、その戻り値の型を構造体とすることで、それに含まれる複数の値を返すことができるようになります。以下は、2次元座標系におけるx成分とy成分からなるベクトルを構造体vectorとして定義して、2つのベクトルを渡してその和を戻り値とする関数addの例です。
struct_division.c
#include <stdio.h>
typedef struct vector {
	int x,y;
} Vector;

Vector add(Vector a, Vector b)
{
	Vector v;

	v.x = a.x + b.x;
	v.y = a.y + b.y;

	return v;
}

int main(void)
{
	Vector p1 = {7,5};
	Vector p2 = {10,20};
	Vector p3;
	
	p3 = add(p1, p2);

	printf("合成ベクトル( %d,  %d)\n", p3.x, p3.y);

	return 0;
}

構造体の配列を関数で扱う

 構造体の配列も関数に引数として渡して、処理することができます。次の例はN個の2次元ベクトルを渡してその和を戻り値とする関数sumof()の例です。
struct_division.c
#include <stdio.h>
#define N 3

typedef struct vector {
	int x,y;
} Vector;

Vector sumOf(Vector a[])
{
	Vector s = {0,0};
	int i;

	for(i=0; i<N; i++){
		s.x += a[i].x;
		s.y += a[i].y;
	}

	return s;
}

int main(void)
{
	Vector v[N] = {{3,-1},{2,6},{-3,2}};
	Vector d;
	
	d = sumOf(v);
	printf("(%d,%d)\n", d.x, d.y);

	return 0;
}


共用体

 共用体も構造体と同様に複数のデータを一つにまとめる仕組みを提供します。構造体と異なる点は、一つのデータの塊に複数の意味を持たす役割をします。下記のプログラム例は、2つの整数を入力して積と商を求めるプログラムです。得られる積と商を格納するためにそれぞれ整数型と実数型の変数を用意する必要があります。これを構造体に似た共用体を使って実現しています。
union.c
#include <stdio.h>

union answer{
	int seki;		//積
	double syo;		//商
};

int main(void)
{
	int a,b;
	union answer ans; 

	printf("積と商を求めます。2つの整数を入力してください。¥n");	
	printf(">> ");
	scanf("%d,%d", a,b);	

	ans.seki=a*b;
	printf("積:%d¥n",ans.seki);
	
	ans.syo=a/b;
	printf("商:%f¥n",ans.syo);

	return 0;
}

 共用体では、下図のようにメンバの中で一番大きいサイズに合わせてメモリを確保し、残りのメンバはこの一部を共有します。例えばint型の変数なら2Byte、double型の変数なら8Byte、合計10Byteのメモリサイズを必要とするところを、double型変数一つ分の8Byteのみで済んでしまいます。メモリを無駄に使いたくないという側面と、複数のデータを一つのデータとしてまとめて扱いたいという要望を実現できる仕組みです。例えばネットワーク通信で一つのパケットに複数の情報をまとめて送りたいといった場面が想定されます。
union


列挙型

 列挙型とは整数0、1、2、、、に、定数名を付けて利用することができる型です。

列挙型の宣言
	enum タグ名{
		定数名,		// 0
		定数名,		// 1
		定数名,		// 2
		・・・
	}

次の例では、0,1,2にそれぞれ「Zero」、「One」、「Two」という名前を付けたNumberという列挙型を宣言しています。定数名はint型の数値をとして扱われます。
enum.c
#include <stdio.h>

enum Number{
	Zero,
	One,
	Two
};

int main(void)
{
	enum Number num;

	num = One;
	printf("%d\n",num);
	
	return 0;
}

また、定数名には値を設定することもできます。次の例では、 「Ten」は10、「Eleve」は11として扱われます。
enum_c.c
enum Number{
	Zero,
	One,
	Two,
	Ten = 10,
	Eleven
};

また、最後の定数名を0とすると、すべての定数が0となります。


expand_lessBack to TOP

演習問題

  1. x軸とy軸が直交する点を原点(0,0)とする2次元直交座標系において、2つの座標を入力してその座標間の距離を求めて表示するプログラムを作成しなさい。ただし、座標を渡してその座標間距離を戻り値とする関数distanceを定義して利用すること。
  2. (ヒント)次の手順に従ってプログラムを完成させてみよう。
    1. x座標とy座標をメンバとする2次元座標を表す構造体pointを定義する。さらにtypedefを使って名前をPointとして再定義する。
    2. 2つの座標を入力してその座標間の距離を表示するプログラムを作成する。(距離:\(d=\sqrt{(x_2-x_1)^2 + (y_2 - y_1)^2}\) )
    3. 距離を求める処理を関数double distance(Point a, Point b)として定義する。

  3. x軸とy軸が直交する点を原点(0,0)とする2次元直交座標系を移動する自動車のプログラムを、以下の各設問に解答しながら完成させなさい。
    1. 車名、現在位置、ガソリン残量をメンバーとする構造体carを定義しなさい。ただし、距離を表わす単位はKm、ガソリン残量はリットルとする。また、現在位置は上記の演習問題1で定義した座標を表わす構造体pointを利用すること。
    2. 構造体carを引数としてその各メンバーの値を表示する関数infoを定義しなさい。
    3. 移動先の座標を渡して移動できた時には1をそうでない場合は0を戻り値とする関数moveを定義しなさい。燃費は20Km/Lとし、ガソリンが足りない場合は移動できないものとする。移動距離は演習問題1で定義した関数distanceを利用できるものとする。
    4. はじめに車名とガソリンを設定しておき、スタート位置は原点とする。ガソリンは40Lで満タンとする。ガソリンの残量が1L以下になるまで繰り返し移動先の座標を入力できるものとする。

  4. 2つの複素数を入力して、その四則演算の結果を表示するプログラムを作成しなさい。複素数は実数部と虚数部からなる構造体Complexを定義して扱うこと、また、加減乗除それぞれの演算についてそれぞれ二つの複素数を引数としてその演算結果を戻り値(複素数)とする加算関数cadd、減算関数csub、乗算関数cmul、除算関数cdivを定義して利用すること。また、複素数を表示するための関数printComplexを定義して、わかりやすい表示を実現すること。

  5. 2つの整数を入力して、その除算における商と剰余を表示するプログラムを作成しなさい。除算における商と剰余は以下に示す構造体divisionを定義して扱うこと。また、除算の結果は二つの整数aとbを引数として、その商と剰余を持つ構造体divisionを戻り値とする関数divide(int a, int b)を定義して、利用すること。
  6. 	struct  division{
    		int syo;	//商
    		int amari;	//剰余
    	}
    
  7. スタックはデータを格納する配列とその書き込み位置を示す変数からなる構造体として表すことができます。第1週で提示されているサンプルプログラムstack.cを、次のスタックを表す構造体mystackを使って書き直したプログラムを作成しなさい。動作を確認するためのmain関数を含めること。
  8. 	struct mystack {
    		int top;
    		int data[STACKSIZE];
    	}
    

演習問題解答

OPEN ANSWER
  1. 回答例 ex5-1.c
  2. code/5/ex5-1.c
  3. 回答例 ex5-2.c
  4. code/5/ex5-2.c
  5. 回答例 ex5-3.c
  6. code/5/ex5-3.c
  7. 回答例 ex5-4.c
  8. code/5/ex5-4.c
  9. 回答例 ex5-5.c
  10. code/5/ex5-5.c

expand_lessBack to TOP

今週の確認テスト テスト(PDF)

本日の提出課題

 2台の自動車があり、1台は燃料がなくなって停止してしまい、もう1台はその自動車を助けに向かおうとしています。停止している自動車の位置を入力して、満タンにした救援車でたどり着くことができるかを調べる以下のプログラムkadai3-5.cを完成させなさい。救援車両の位置は2次元直行座標系における原点にあり、燃料は50リットル給油されているものとします。関数distance()は2つの車両間の距離を求める関数です。2点\((x_1, y_1)\)と\((x_2, x_2)\)の間の距離は\(\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}\)で求めることができます。平方根の計算には標準数学関連ライブラリmath.hで定義されている関数sqrt()を利用するものとします。

#include <stdio.h>
#include <math.h>
#define Nenpi 10.0 	// 燃費[km/L]

typedef struct {
	double x,y;	//位置の座標
} Position;

typedef struct {
	Position p;	// 位置
	double fuel;	// 燃料の残量
} Car;

/* 移動距離 */
double distance(Car car1, Car car2)
{
	//ここを埋めて完成させる
}

int main(void)
{
	Car rescue = {{0,0},50};	//救援車 <-訂正(再提出不要)
	Car car;			//救援対象車
	double d;			//距離
	double remain;		//残燃料

	car.fuel = 0.;		//燃料が0に
	printf("救援を求めている車の現在位置(x,y)> ");
	scanf("%lf,%lf",&car.p.x, &car.p.y);

	d = distance(car, rescue);	//移動距離
	remain = [空欄ア];		//ここを埋めて完成させる。救援車の残燃料 
	if(remain > 25)
		printf("往復分の燃料があります。(残燃料: %f リットル)\n",remain);
	else if (remain> 0)
		printf("往復分の燃料はありません。(残燃料: %f リットル)\n",remain);	
	else
		printf("燃料が足りません。\n");

	return 0;
}


  • [提出方法]
    • 電子メールの添付ファイルとして提出してください。宛先は指定のアドレスです。
    • 表題は、課題5とします。
    • メール本文には、プログラムを完成させるまでに苦労した点などを記述すること。
    • 添付ファイルは下記の2点です。
    • 1) ソースコード・ファイル kadai3-5.c
      2) 実行結果画面(ウインドウのみ)のハードコピーの画像ファイル kadai3-5.jpg

  • [実行結果画面のハードコピーの取り方]
    1. [メニュー]→[アクセサリ]→Snipping Toolを起動する。ただし、Snipping Toolのウインドウが実行画面に重ならないように、必要に応じてウインドウを移動すること。
    2. タイトルバーを含めた実行結果画面をマウスドラッグして選択して、キャプチャする。
    3. メニューから「ファイル」→「名前をつけて保存」を選択する。ファイルの種類にはJPG形式を選択して、保存先フォルダを選び、ファイル名を付けて「保存」する。
    ※Windows10(creators update以降)では、Windows+Shift+S キーを押して、キャプチャしたい矩形領域をドラッグして、クリップボードにコピーします。アプリ「切り取り&スケッチ」が起動したら、画像をJPG形式で保存します。

  • [評価について]
  • プログラムの内容の評価とは別に以下の要件を満たすことを評価の前提とする。
    1. ソースコードがC言語で記述されていること。
    2. 提出されたソースコードファイルをコンパイルし、ワーニングやエラーが出力されないこと。scanfをscanf_sとすべきワーニングについては除外する。
    3. 提出されたソースコードファイルから生成される実行形式ファイルを実行した結果、所定の出力結果が得られること。
    4. 提出されたソースコードは、インデントや適当な改行が施された見やすい状態であること。
    5. 変数の利用目的、処理や判定の意味など必要と思われる解説を簡潔にコメント文として付けること。
    6. 提出された実行結果の画面コピーは、コマンドプロンプトのウインドウのみとすること。

  • [提出期限]
  • 2019年 10月23日(水)午後4時まで