第6週 構造体とポインタ
INDEX

本日の目標

  • 構造体をポインタを使って操作できる。
  • 構造体の配列をポインタを使って操作できる。

予習・復習

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

本日の講義・演習予定

  1. 構造体のポインタ
  2. 関数と構造体ポインタ
  3. 自己参照構造体
  4. 演習問題
  5. 提出課題
内 容
  1. 構造体のポインタ
  2. 関数と構造体ポインタ
  3. 自己参照構造体


構造体のポインタ

構造体をポインタで扱う

 構造体変数をポインタを使って指し示し、操作する方法を見ていきます。ポインタ変数を宣言すときには、基本型のポインタと同様に、その型には指し示す先の構造体変数と同じ型を記述して、変数名の前にはポインタ宣言子*(アスタリスク)を付けます。次に、ポインタを利用するために、ポインタ変数に指し示す先の構造体変数の先頭アドレスを代入します。構造体変数のメンバーをポインタを使って参照するときには、カッコで括ったポインタ変数とメンバをドット演算子で接続するように記述します。これは、まず、間接参照演算子*を使ってポインタが指し示す構造体を参照して、次にドット演算子.を使ってメンバにアクセスすることを示しています。ポインタ変数をカッコでくくるのは、間接参照演算子*よりもドット演算子の方が優先順位が高いからです。
struct_pointer.c
#include <stdio.h>

struct point2D {
	int x;
	int y;
};

int main(void)
{
	struct point2D point;
	struct point2D *p;	//構造体point2D型変数を指し示すポインタ

	point.x = 5;
	point.y = 7;

	p = &point;		//構造体変数pointの先頭アドレスを代入

	printf("座標 (%d, %d)\n", (*p).x,(*p).y);
	
	return 0;
}

 アロー演算子->(間接メンバ参照演算子)と呼ばれる演算子を使うことで、ポインタで直接、構造体のメンバを参照するように記述方法することもできます。以下の例のように、ポインタ名とメンバ名の間にアロー演算子->を記述します。
struct_pointer.c
#include <stdio.h>

struct point2D {
	int x;
	int y;
};

int main(void)
{
	struct point2D point;
	struct point2D *p;	//構造体point2D型変数を指し示すポインタ

	point.x = 5;
	point.y = 7;

	p = &point;		//構造体変数pointの先頭アドレスを代入

	printf("座標 (%d, %d)\n", p->x,p->y);
	
	return 0;
}

構造体変数を指し示すポインタ宣言
struct 構造体タグ名 *構造体ポインタ名

構造体変数のメンバーの参照
構造体変数名->メンバ名
ポインタを介して構造体のメンバーを参照するためには、アロー演算子(間接メンバー演算子とも言う)->を使って、構造体変数名とメンバ名をつなぎます。間接参照演算子を使って以下のように記述することもできます。
(* 構造体ポインタ名).メンバ名


構造体の配列をポインタで扱う

 ポインタを使わずに構造体の配列を扱うプログラム例を先に見ておきましょう。前回と同じ、2次元座標を表す構造体point2Dを一つの要素として複数の座標を配列で扱う例です。
例 struct_array.c
#include <stdio.h>

struct point2D {
	int x;
	int y;
};
typedef struct point2D point2D_t;

int main(void)
{
	point2D_t points[3] = {{2,3},{4,7},{6,9}};
	int i;

	for(i=0; i<3; i++){
		printf("座標%d (%d, %d)\n", i,points[i].x,points[i].y);
	}
	
	return 0;
}

 このプログラムを、構造体point2D型配列の操作をポインタを使って実現した例です。構造体の配列名をpointsとして、これを指し示すポインタをpとします。配列には構造体が連続して格納されており、ポインタpは先頭要素の先頭アドレスを示します。次の要素はp+1、その次の要素はp+2、i番目の要素はp+iとして示すことができます。i番目の要素が持つメンバーはアロー演算子を使って、例えばx座標ならば(p+i)->xとして参照することができます。
struct_array_pointer.c
#include <stdio.h>

struct point2D {
	int x;
	int y;
};
typedef struct point2D point2D_t;

int main(void)
{
	point2D_t points[3] = {{2,3},{4,7},{6,9}};
	point2D_t *p;
	int i;

	p = points;

	for(i=0; i<3; i++){
		printf("座標%d (%d, %d)\n", i,(p+i)->x,(p+i)->y);
	}
	
	return 0;
}
 構造体point2Dのサイズはint型(4バイト)×2個分 の合計8バイトになります。したがって、構造体point2D型を示すポインタpは、+1することで8バイト分先のアドレス、すなわち、次の座標の先頭アドレスを示すことになるわけです。

mallocを使ったポインタ宣言


 変数や配列変数をポインタを使って扱う場合は、1)変数を宣言することでメモリに領域を確保する、2)変数を指し示すためのポインタを宣言する、3)変数領域をポインタで指し示すために変数の先頭アドレスをポインタへ代入する、という手順を踏みます。しかし、mallocという関数を利用するとこの手順を1回で済ますことができます。関数malloc()は引数で与えた分のメモリ領域を確保し、その先頭アドレスを指すポインタを戻り値します。ただし、処理が終了したら、関数free()を使ってその領域を解放しなければなりません。mallocを利用する一番のメリットは動的メモリ確保ですが、それについてはプログラミング4で詳しく学習していきます。
例 func_pstruct.c
#include <stdio.h>
#include <stdlib.h>
#define N 3

struct point2D {
	int x;
	int y;
};
typedef struct point2D point2D_t;

int main(void)
{
	//point2D_t型変数N個分の領域を確保
	point2D_t *p = (point2D_t *)malloc(sizeof(point2D_t)*N);
	int i;

	for(i=0; i<N; i++){
		printf("座標x> ");	scanf("%d", &(p+i)->x);
		printf("座標y> ");	scanf("%d", &(p+i)->y);
	}

	for(i=0; i<N; i++){
		printf("座標(%d,%d)\n",(p+i)->x,(p+i)->y);
	}

	//領域を解放
	free(p);
	
	return 0;
}


関数と構造体

関数間の構造体の受け渡し

 次のプログラムで定義されている関数print_pointsは、構造体であるpoint2D_t型の変数を引数で受け取ってその各メンバーの実体を表示する関数です。
例 func_pstruct.c
#include <stdio.h>
struct point2D {
	int x;
	int y;
};
typedef struct point2D point2D_t;

void print_point(point2D_t point);	//プロトタイプ宣言

int main(void)
{
	point2D_t point;

	point.x = 5;
	point.y = 7;

	print_point(point);

	return 0;
}

/* 座標の表示 */
void print_point(point2D_t point)
{
	printf("座標(%d, %d)\n", point.x, point.y);
}


関数間で構造体ポインタの受け渡し

 関数が仮引数で受け取る実体は、呼び出し側の実引数のコピーであり、関数内で変更されても元の実引数の実体が変わることはありません。先の関数print_point()も、point2D_t型変数の実体のコピーを引数として受け取ってそのメンバを表示する関数でした。一方、関数内で、実引数の実体そのものを変更したい場合もあります。このような場合は、引数としてポインタを利用することで実現することができます。次の例ではpoint2D_t型ポインタを引数としてそのメンバーである2次元座標の値を設定する関数set_point()を定義し ています。
例 struct_func_pointer.c
#include <stdio.h>

struct point2D {
	int x;
	int y;
};
typedef struct point2D point2D_t;

void set_point(point2D_t *pt, int x, int y);	//プロトタイプ宣言
void print_point(point2D_t point);

int main(void)
{
	point2D_t point;
	point2D_t *p;		//point2D_t型変数を指し示すポインタ
	//point2D_t *p = (point2D_t *)malloc(sizeof(point2D_t));

	point.x = 5;
	point.y = 7;

	print_point(point);
	set_point(&point,12,23);
	print_point(point);

	//free(p);		//領域解放
	return 0;
}

/* 座標設定 */
void set_point(point2D_t *pt, int x, int y)
{
	pt->x = x;
	pt->y = y;	
}

/* 座標の表示 */
void print_point(point2D_t point)
{
	printf("座標(%d, %d)\n", point.x, point.y);
}


自己参照構造体

 メンバに自身と同じ型のポインタを持つ構造体を自己参照構造体と言います。自身と同じ型のメンバを有することで、同じ型のデータが連なったような構成など自由度の高いデータ構造を作ることができます。以下に自己参照構造体を使ったプログラム例を示します。
self_ref.c
#include <stdio.h>

struct node {
	int key;
	int value;
	struct node *next;		//自分自身へのポインタ
};

int main(void)
{
	struct node node[5];
	struct node *top,*np;
	int i;

	for(i=0; i<5; i++){
		node[i].key = i + 1000;
		node[i].value = i*10;
	}

	top = &node[0];		//ポインタtopは先頭ノードを示す
	node[0].next = &node[1];		//先頭ノードのnextは次のノードを示す
	node[1].next = &node[2];
	node[2].next = &node[3];
	node[3].next = &node[4];
	node[4].next = NULL;		//nextが示すノードがないのでNULLとする

	for(np = top; np!=NULL; np = np->next){	//NULLになるまで次のノードへ
		printf("%d ",np->value);
	}
	printf("\n");

	return 0;
}


expand_lessBack to TOP

演習問題

  1. 次の手順に従って、人間の体重と身長を入力してその一覧を表示するプログラムを完成させなさい。
    1. id(int型)、身長(int型)[cm]、体重(double型)[kg]の3つのメンバーからなる構造体personを定義しなさい。

    2. 構造体personをPerson型として型定義しさない。

    3. ヘッダファイル、stdio.hとstdlib.hをインクルードして、処理内容が空のmain関数を定義しなさい。

    4. 5人分のPerson型のデータを格納する領域を確保しなさい。
    5. Person *p = (Person *)malloc(sizeof(Person)*5);

       (最後にfree(p)すること。)
    6. 5人分のデータを入力できるようにしなさい。ただし、idについては、1001番から順番に1づつ増加させるものとします。

    7. n人分のデータを以下の実行例のように表示する関数print_info(Person *p, int n)を追加して、実行結果を確認しなさい。
    8. 身長(cm)> 181 体重(kg)> 76 身長(cm)> 172 体重(kg)> 69 身長(cm)> 156 体重(kg)> 82 身長(cm)> 166 体重(kg)> 55 身長(cm)> 175 体重(kg)> 66 ID | 身長 | 体重 | (cm) | (kg) -----+------+------ 1001 | 181 | 76.0 1002 | 172 | 69.0 1003 | 156 | 82.0 1004 | 166 | 55.0 1005 | 175 | 66.0

    9. データ一覧を以下の実行例のように身長順(降順)に並べ替える関数sort_height(Person *p, int n)を追加して、実行結果を確認しなさい。
    10. 身長(cm)> 181 体重(kg)> 72.5 身長(cm)> 176 体重(kg)> 68 身長(cm)> 155 体重(kg)> 90 身長(cm)> 188 体重(kg)> 82.5 身長(cm)> 165 体重(kg)> 58.2 ID | 身長 | 体重 | (cm) | (kg) -----+------+------ 1004 | 188 | 82.5 1001 | 181 | 72.5 1002 | 176 | 68.0 1005 | 165 | 58.2 1003 | 155 | 90.0

  2.  生徒のn人分の成績を入力して、成績順に並べ替えて、その一覧を表示するプログラムです。空欄を埋めてプログラムを完成させなさい。
  3. students.c
    #include <stdio.h>
    #include <stdlib.h>
    
    struct student {
    	int id;			//学籍番号
    	int nation; 	//国語
    	int math;		//数学
    	int english;	//英語
    	float avg;		//平均
    };
    typedef struct student student_t;
    
    void set_avg(student_t *s, int num);
    void sort(student_t *s, int num);
    
    int main(void)
    {
    	student_t *st_p;
    	int n,i;
    
    	printf("生徒の人数を入力してください> ");
    	scanf("%d",&n);
    	
    	st_p = (student_t *)malloc(sizeof(student_t)*n);
    
    	printf("点数を入力してください。\n");
    	for(i=0; i<n; i++){
    		[[空欄ア]]id = i+1001;
    		printf("学籍番号 %d\n",(st_p+i)->id);
    		printf("国語> ");
    		scanf("%d",[[空欄イ]]nation);
    		printf("数学> ");
    		scanf("%d",[[空欄ウ]]math);
    		printf("英語> ");
    		scanf("%d",[[空欄エ]]english);
    	}
    
    	set_avg(st_p,n);
    	sort(st_p,n);
    
    	puts("  No. | 国語 | 数学 | 英語 | 平均 ");
    	puts("------+------+------+------+------");
    	for(i=0; i<n; i++){
    		printf(" %4d |",[[空欄オ]]id);
    		printf("  %3d |",[[空欄カ]]nation);
    		printf("  %3d |",[[空欄キ]]math);
    		printf("  %3d |",[[空欄ク]]english);
    		printf(" %.1f\n",[[空欄ケ]]avg);
    	}
    
    	free(st_p);
    	return 0;
    }
    
    /* 平均値一覧の作成 */
    void set_avg(student_t *s, int num)
    {
    	int i;
    	
    	for(i=0; i<num; i++){
    		[[空欄コ]]avg = (float)([[空欄サ]]nation+[[空欄シ]]math + [[空欄ス]]english)/3;	
    	}
    }
    
    /* 成績順で並べ替え */
    void sort(student_t *s, int num)
    {
    	int i,j;
    	student_t tmp;
    
    	for(i=1; i<num; i++){
    		for(j=0; j<num-i; j++){
    			if([[空欄セ]]avg < [[空欄ソ]]avg){
    				tmp = [[空欄タ]];
    				[[空欄チ]];
    				[[空欄ツ]] = tmp;
    			}	
    		}
    	}
    }	
    

演習問題解答

OPEN ANSWER
  1. 回答例 ex6-1.c
  2. code/6/ex6-1.c
  3. 回答例 ex6-2.c
  4. code/6/ex6-2.c

expand_lessBack to TOP

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

本日の提出課題

生徒5人分の学籍番号、数学、英語、理科の得点データを入力した後に、各生徒の合計点から成績の良い順に並べ替えた結果一覧を表示する以下のプログラムkadai3-6.cを完成させなさい。生徒一人のIDと成績は構造体Seisekiを使って表します。関数sumof()は生徒一人分の数学、英語、理科の合計点を求めて構造体メンバのtotalに代入します。関数swap()は2つの構造体Seisekiが持つ値を交換する関数で、関数sort()から呼び出すことで利用します。関数sort()は合計点の高い順に成績を並べ替える関数です。
#include <stdio.h>
#define N 3 

typedef struct {
	int id;	//学籍番号
	int eng;	//英語
	int math;	//数学
	int sci;	//理科
	int total;	//合計
} Seiseki;

Seiseki data[N];

void sumof(Seiseki *x);			//一人分の合計を求める
void swap(Seiseki *a, Seiseki *b);		//成績の交換
void sort(Seiseki x[], int n);		//成績順で並べ替え

void printSeiseki(Seiseki *x){
	int i;

	printf("  ID. | 英語| 数学 | 理科 | 合計\n");
	printf("------+------+------+------|------\n");
	for(i=0; i<N; i++){
		printf(" %4d | %4d |",x[i].id, x[i].eng);
		printf(" %4d | %4d | %4d\n",x[i].math,x[i].sci,x[i].total);
	}
}

int main(void)
{
	int i;

	for(i=0; i<N; i++)
		scanf("%d %d %d %d", &data[i].id, &data[i].eng, &data[i].math, &data[i].sci);

	for(i=0; i<N; i++) sumof(&data[i]);
	printSeiseki(data);
	sort(data,N);
	printSeiseki(data);

	return 0;
}




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

  • [実行結果画面のハードコピーの取り方]
    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月30日(水)午後2時まで