第2週 動的メモリー管理
INDEX

本日の目標

  • 動的にメモリを管理する仕組みを知る。
  • mallocを使って、必要に応じた分だけメモリ領域を確保し利用するプログラムが書ける。

予習・復習

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

本日の講義・演習予定

  1. 動的メモリ管理
  2. 構造体と動的メモリ管理
  3. 演習
  4. 演習問題
  5. 提出課題
内 容
  1. 動的メモリ管理
  2. 構造体と動的メモリ管理


動的メモリ管理

動的にメモリ領域を確保するわけ

 多くのデータを扱うために配列を利用します。例えば、定員40名の講義の試験結果を管理するために、次のように配列を宣言します。
 int score[40];
しかし、実際には受講希望者が10人しかいなければ30人分のメモリ領域が無駄になります。もし、1人多く履修を認めたとしても41人目の点数を扱うことはできません。変数のメモリ領域はコンパイルされた時点で確保され、以降変更することはできないからです。
 では、人数に応じてメモリ領域の大きさを変えることはできないのでしょうか?

動的にメモリ領域を確保するための関数

 プログラム実行時に必要な大きさのメモリ領域を確保するために4つの関数が用意されています。これらの関数を利用すれば、動的にメモリ領域の確保と解放が可能になります。確保したメモリ領域が不要になれば、その時点でその領域を解放することもできます。必要な時に必要な量だけメモリ領域を確保するという無駄のない利用が実現できます。

関 数説 明
void *malloc(size_t size)sizeバイト分のメモリ領域を確保する。
[戻り値]成功:確保した領域の先頭を指すポインタ 失敗:NULL
void* calloc(size_t n, size_t n)sizeバイト分のメモリ領域を確保する。
[戻り値]成功:確保した領域の先頭を指すポインタ 失敗:NULL
void* reallooc(void *ptr, size_t size)ptrで示すメモリ領域のサイズをsizeバイトに変更する
[戻り値]成功:確保した領域の先頭を指すポインタ 失敗:
void free(void *ptr)ptrで示すメモリ領域を解放する。

 さて、これらの関数の戻り値は、すべて(void *)となっており、いずれも特定の型を戻り値としないことを表しています。例えば、20バイト分のメモリ領域を確保したいとします。対象のデータ型がint *型ならば4バイトx5個分ですし、char *型ならば1バイトx20個分のメモリ領域ということになります。しかし、void *型では20バイト分のメモリブロックとしかわかりません。つまり、このままでは使えないので、いずれかの型に変更(キャスト)して使ってくださいということになります。
malloc

動的にメモリ領域を確保するプログラム例

 はじめに、配列を使って静的にメモリ領域を確保する例を確認しておきます。5人分の得点と最高点を求めるプログラムです。配列の領域はコンパイル時に確保されるため、プログラム実行中に人数を変更することはできません。
smemory.c
#include <stdio.h>
#define N 5

int main(void)
{
	int score[N];
	int i,max=0,sum=0;

	for(i=0; i<N; i++){
		printf("%d人目の点数>> ",i+1);
		scanf("%d",&score[i]);
	}

	for(i=0; i<N; i++){
		sum += score[i];
		if(score[i] > max) max = score[i];
	}

	printf("平均点 %.1f\n",(double)sum/N);
	printf("最高点 %d\n",max);
	return 0;
}

 次のプログラム例では人数を入力した後、その人数に応じたメモリ領域を確保しています。1人分の点数を格納するために必要なメモリサイズは、得点をint型で扱うことからsizeof(int)バイトとして求めることができます。N人分のメモリサイズならば、sizeof(int)*Nバイトとして求められます。関数malloc()を使って、N人分の点数を格納するためのメモリブロックを確保するためには、以下のように記述します。
int *p = malloc(sizeof(int)*N)
確保したメモリブロックは、ポインタpを使って指し示します。
 しかし、関数mallocの戻り値の型はvoid *型(汎用ポインタ型)のため、int型サイズN個分の領域といってもポインタpを使ってint型サイズ分ごとで領域にアクセスすることができません。次のように、ポインタpと同じint *型でキャストします。
int *p = (int *)malloc(sizeof(int)*N)

 以下に人数に応じてメモリ領域を確保するプログラム例を示します。
dmemory.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int *p;
	int i,max=0,sum=0;
	int num;

	printf("何人分の点数ですか?> ");
	scanf("%d",&num);

	p = (int *)malloc(num*sizeof(int));	//領域確保
	if(p==NULL){	//領域確保に失敗した場合
		fprintf(stderr,"Error: Malloc failed");
		return EXIT_FAILURE;
	}
	for(i=0; i<num; i++){
		printf("%d人目の点数>> ",i+1);
		scanf("%d",p+i);
	}

	for(i=0; i<num; i++){
		sum += *(p+i);
		if(*(p+i) > max) max = *(p+i);
	}

	printf("平均点 %.1f\n",(double)sum/num);
	printf("最高点 %d\n",max);

	free(p);	//領域解放

	return 0;
}

メモリブロックを拡張する

 関数reallocを使って、一度確保したメモリ領域のサイズを変更(拡張)する例を見ていきましょう。次の例では、はじめに100バイト分の領域を確保したのち、その領域を倍の200バイトに拡張しています。
realloc.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	char *p1,*p2;

	p1 = (char *)malloc(100*sizeof(char));		//領域確保
	if(p1==NULL){			//領域確保に失敗
		fprintf(stderr,"Error: P1 Malloc failed");
		return EXIT_FAILURE;
	}

	p2 = (char *)realloc(p1,200*sizeof(char));	//2倍に拡張
	if(p2==NULL){			//領域確保に失敗
		fprintf(stderr,"Error: P2 Malloc failed");
		return EXIT_FAILURE;
	}

	p1 = p2;	//ポインタp1で指し示す

	free(p1);	//p1とp2は同じ領域を示しているので

	return 0;
}

 次は、はじめに2000バイト分と、800バイト分の2つのメモリ領域を確保した後、2000バイトの領域を2800バイトに拡張するプログラム例です。
realloc.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int *p1,*p2,*p3;
	int i,max=0,sum=0;
	int num=5;

	p1 = (int *)malloc(500*sizeof(int));		//領域確保
	if(p1==NULL){			//領域確保に失敗
		fprintf(stderr,"Error: P1 Malloc failed");
		return EXIT_FAILURE;
	}
	printf("メモリブロックp1 (%p)\n",p1);

	p2 = (int *)malloc(200*sizeof(int));		//領域確保
	if(p2==NULL){			//領域確保に失敗
		fprintf(stderr,"Error: P2 Malloc failed");
		return EXIT_FAILURE;
	}
	printf("メモリブロックp2 (%p)\n",p2);

	p3 = (int *)realloc(p1,700*sizeof(int));	//領域確保
	if(p3==NULL){			//領域確保に失敗
		fprintf(stderr,"Error: P3 Malloc failed");
		return EXIT_FAILURE;
	}
	p1 = p3;
	printf("拡張したメモリブロックp1 (%p)\n",p1);

	free(p1);
	free(p2);
	
	return 0;
}

 ポインタp1が示す領域を拡張しようとすると、ポインタp2が示す領域にぶつかってしまうため、拡張することができません。このような場合は、別の空き領域に新たに拡張したサイズ分だけの領域(ポインタp3で示す)が確保されます。さらに、自動的に元の領域であるp1のデータが新しい領域p3へコピーmemcopyされます。ポインタp1のアドレスをポインタp3のアドレスに変更することで拡張を完了します。
realloc

構造体と動的メモリ管理

 構造体へのポインタを利用する場合も、基本形と同様に2つの方法があります。一つは、あらかじめ宣言された構造体変数のメモリアドレスをポインタ変数へ代入して指し示す方法です。もう一つは、関数malloc()を使って構造体を格納するために必要な大きさのメモリ領域を確保し、その領域をポインタ変数で指し示す方法です。後者の場合は、基本形と同様にプログラムの実行途中でも必要に応じて領域を追加したり、解放したりすることができるのでメモリを効率的に利用することができます。
 構造体が占有するメモリサイズはその構造体のメンバによって異なってくるため、sizeof演算子を使って構造体のサイズを求めます。
 次のプログラム例は、学生のIDと名前をメンバとする構造体Studentを定義して、入力された学生数に応じてメモリ領域を確保しています。構造体Studentは1つのint型(4Byte)変数と32個の要素を持つchar型(1Byte)配列のメンバを持つことから、そのサイズは36Byteとなります。
struct_malloc.c
#include <stdio.h>
#include <stdlib.h>

struct student{
	int id;
	char name[32];
};
typedef struct student Student;

int main(void)
{
	Student *sp;
	int num,i;

	printf("学生数> ");
	scanf("%d",&num);

	if((sp=(Student *)malloc(sizeof(Student)*num))==NULL){
		fprintf(stderr,"Error: Memory allocation failure.\n");
		return EXIT_FAILURE;
	}

	for(i=0; i<num; i++){
		(sp+i)->id = 1000+i;
		printf("名前> ");
		scanf("%s",(sp+i)->name);
	}

	for(i=0; i<num; i++)
		printf("%d: %s\n",(sp+i)->id,(sp+i)->name);
	
	free(sp);

	return 0;
}

expand_lessBack to TOP

演習問題

以下のコードにおいては、malloc()によるメモリ領域確保におけるエラー処理の記述を省略しています。環境によってはこの処理を必須とする場合があります。必要に応じて追加をしてください。
  1. 次は、n角形の頂点数(但し、n≧3)とその座標を入力して面積を求めるプログラムです。空欄を埋めて完成させなさい。面積は頂点の座標を\(P_i(x_i, y_i)\)とすると、次式で求められるます。 \[ S=\frac{1}{2}\left|\sum_{i=1}^n(x_i y_{i+1} - x_{i+1}y_i)\right|\]
  2. #include <stdio.h>
    #include <stdlib.h>
    
    // 座標
    struct point2d {
    	int x;
    	int y;
    };
    typedef struct point2d Point;
    
    int main(void)
    {
    	Point *p;
    	int i,n,sum=0;
    
    	printf("多角形の頂点数> ");
    	scanf("%d", &n);
    	p = [[空欄ア]];	//領域確保
    
    	printf("反時計回りに座標を入力してください。\n");
    	for(i=0; i<n; i++){
    		printf("座標%d(x,y)> ",i);
    		scanf("%d,%d", [[空欄イ]], [[空欄ウ]]);
    	}
    
    	for(i=0;i<n-1;i++){
    		sum += (p+i)->x * (p+i+1)->y - [[空欄エ]] *[[空欄オ]];
    	}
    	sum += (p+n-1)->x * p->y - (p+n-1)->y * p->x;	 //最初の座標と
    
    	printf("面積: %f\n",(float)sum/2);
    
    	[[空欄カ]];	//領域解放
    	
    	return 0;
    }
    

  3. 次は、n角形の頂点数(但し、n≧3)とその座標を入力して面積を求めるプログラムです。空欄を埋めて完成させなさい。面積は頂点の座標を\(P_i(x_i, y_i)\)とすると、次式で求められるます。 \[ S=\frac{1}{2}\left|\sum_{i=1}^n(x_i y_{i+1} - x_{i+1}y_i)\right|\]
  4. #include <stdio.h>
    #include <tstdlib.h>
    
    // 座標
    struct point2d {
    	int x;
    	int y;
    };
    typedef struct point2d Point;
    
    //平面図形
    struct polygon{
    	int nvertices;	//頂点数
    	Point *p;		//各頂点の座標
    };
    typedef struct polygon Shape;
    
    int main(void)
    {
    	Shape shape;
    	int i,n,sum=0;
    
    	printf("多角形の頂点数> ");
    	scanf("%d", &n);
    	[[空欄ア]] = (Point *)malloc(sizeof(Point)*n);		//領域確保
    
    	printf("反時計回りに座標を入力してください。\n");
    	for(i=0; i<n; i++){
    		printf("座標%d(x,y)> ",i+1);
    		scanf("%d,%d",[[空欄イ]], [[空欄ウ]]);
    	}
    
    	for(i=0;i<n-1;i++){
    		sum += (shape.p+i)->x * (shape.p+i+1)->y - [[空欄エ]] * [[空欄オ]];
    	}
    	sum += (shape.p+n-1)->x * shape.p->y - (shape.p+n-1)->y * shape.p->x;
    
    	printf("面積: %f\n",(float)sum/2);
    
    	[[空欄カ]];
    	
    	return 0;
    }
    

  5. 次の図形の頂点数とその座標を入力して図形を扱うプログラムです。空欄を埋めて完成させなさい。2次元図形を表す構造体Shapeは、そのメンバに図形の頂点数nverticesと各頂点の座標を指すポインタpを持ちます。このポインタpが指し示す記憶領域の大きさは、図形の頂点数によって変わるため、頂点数の入力後にその領域を確保しています。2次元図形shapeは頂点数が3以上ならば多角形を扱いますが、座標が1つだけならば点を、2つならば直線も扱えるものとします。
  6. #include <stdio.h>
    #include <stdlib.h>
    
    //座標
    struct point2d {
    	int x;
    	int y;
    };
    typedef struct point2d Point;
    
    //平面図形
    struct polygon {
    	int id;
    	int nvertices;	//頂点数
    	Point *p;	//各頂点の座標
    };
    typedef struct polygon Shape;
    
    int main(void)
    {
    	Shape shape;
    	int i,n;
    
    	do{
    		printf("頂点数> ");
    		scanf("%d",&n);
    	}while(n<1);
    	shape.nvertices = n;
    	shape.p = [[空欄ア]] ;	//領域確保
    
    	shape.id = 1001;
    	for(i=0; i<n; i++){
    		printf("頂点%dの座標(x,y) > ",i+1);
    		scanf("%d,%d", [[空欄イ]]->x, [[空欄イ]]->y);
    	}
    
    	switch(shape.nvertices){
    		case 1: printf("点"); break;
    		case 2: printf("直線"); break;
    		default: printf("%d角形",n); break;
    	}
    	printf("(%d): ",shape.id);
    	for(i=0; i<n; i++){
    		printf("(%d,%d) ", [[空欄ウ]]->x, [[空欄ウ]]->y);	//頂点座標
    	}
    	printf("\n");
    	
    	[[空欄エ]];
    
    	return 0;
    }
    

  7. 次プログラムは、先の図形を扱うプログラムを、複数の図形を扱えるように拡張したものです。空欄を埋めて完成させなさい。
  8. #include <stdio.h>
    #include <stdlib.h>
    
    //座標
    struct point2d {
    	int x;
    	int y;
    };
    typedef struct point2d Point;
    
    //平面図形
    struct polygon {
    	int id;
    	int nvertices;	//頂点数
    	Point *p;		//各頂点の座標	
    };
    typedef struct polygon Shape;
    
    int main(void)
    {
    	Shape *ps;			//図形群へのポインタ
    	int i,j,ns,np;		//図形数、頂点数
    
    	printf("図形の数> ");
    	scanf("%d",&ns);
    	ps = [[空欄ア]];	//領域確保
    	
    	for(i=0; i<ns; i++){
    		(ps+i)->id = 1001+i;
    		printf("\nID:%d ----------\n", (ps+i)->id);
    		printf("何角形> ");
    		scanf("%d",&np);
    		 (ps+i)->nvertices = np;
    		[[空欄イ]] = (Point *)malloc(sizeof(Point)*np);	//領域確保	
    
    		for(j=0; j<np; j++){
    			printf("座標%d(x,y) > ",j+1);
    			scanf("%d,%d", [[空欄ウ]]->x, [[空欄ウ]]->y);
    		}
    	}
    
    	for(i=0; i<ns; i++){
    		printf("%d: ",(ps+i)->id);
    		switch((ps+i)->nvertices){
    			case 1: printf("点"); break;
    			case 2: printf("直線"); break;
    			default: printf("%d角形 ",(ps+i)->nvertices); break;
    		}
    		for(j=0; j<(ps+i)->nvertices; j++)
    			printf("(%d,%d) ", [[空欄エ]]->x, [[空欄エ]]->y);		//頂点座標
    		printf("\n");
    		[[空欄オ]];	//各頂点のための領域解放
    	}
    	
    	[[空欄カ]];	//図形のための領域解放
    
    	return 0;
    }
    

  9. 以下の実行例のように1月から12月までの月名を表す英単語(半角文字)を入力して、その結果を表示するプログラムを作成しなさい。ただし、入力した英単語(文字列)の文字数をカウントして、英単語を格納するために必要なメモリ領域を関数mallocを利用して確保するものとします。12ヶ月分の各領域を示すために12個の要素からなるポインタの配列をあらかじめ用意しておきます。
  10. [実行結果例]
    1月> January
    2月> February
    ・・・
    

OPEN ANSWER
  1. 解答1
    ア. (Point *)malloc(sizeof(Point)*n)
    イ.&(p+i)->x
    ウ. &(p+i)->y
    エ. (p+i)->y
    オ. (p+i+1)->x
    カ.free(p)
  2. 解答2
    ア.shape.p
    イ.&(shape.p+i)->x
    ウ. &(shape.p+i)->y
    エ. (shape.p+i)->y
    オ. (shape.p+i+1)->x
    カ.free(shape.p)
  3. 解答3
    ア. (Point *)malloc(sizeof(Point)*n)
    イ. &(shape.p+i)
    ウ. (shape.p+i)
    エ. free(shape.p)
  4. 解答4
    ア.(Shape *)malloc(sizeof(Shape)*ns)
    イ. (ps+i)->p
    ウ.&((ps+i)->p+j)
    エ. ((ps+i)->p+j)
    オ. free((ps+i)->p)
    カ. free(ps)
  5. monthnames.c
  6. #include <stdio.h>
    #include <stdlib.h>
    #define N 12	//12ヶ月
    
    int str_len(char s[])
    {
    	int len=0;
    
    	for(len=0; s[len]!='\0'; len++);
    
    	return len;
    }
    
    int main(void)
    {
    	char *p[N] = {NULL};		//N個のポインタからなる配列
    	char str[16];
    	int len,i,j;
    
    	printf("月名の英単語を入力してください\n");
    	for(i=0; i<N; i++){
    		printf("%d月> ",i+1);
    		scanf("%s",str);
    		len = str_len(str);		//文字数
    		p[i] = (char *)malloc(sizeof(char)*(len+1));	//文字数分の領域確保,NULL文字分を加える
    		if(p[i] == NULL){
    			fprintf(stderr,"Error: Memory allocation failure.\n");
    			for(j=0; j<i; j++) free(p[j]);	//確保済領域の解放 
    			return EXIT_FAILURE;
    		}
    		for(j=0; j<=len; j++) p[i][j] = str[j];
    		getchar();		//改行文字の読み込み
    	}
    
    	for(i=0; i<N; i++) printf("%s\n",p[i]);	//結果表示
    
    	for(i=0; i<N; i++) free(p[i]);	//12個の領域を解法
    
    	return 0;
    }
    

expand_lessBack to TOP

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

回答

[答]
1. (ア) sizeof(char)
(イ) sizeof(int)
(ウ) sizeof(double)
(エ) free(c)
(オ) free(a)
(カ) free(x)

2. (ア) (int *)malloc(sizeof(int) * 5)
(イ) *(a+i)
(ウ) free(a)

3. (ア) (Person *)malloc(sizeof(Person)*n)
(イ) (p+i)->id
(ウ) (p+i)->name
(エ) (p+i)->id
(オ) (p+i)->name

本日の提出課題

     N冊分の書籍の書名を入力して、その書名の文字数に応じた記憶域を動的に確保し、その領域に書名を格納した結果を表示する下記のプログラムkadai4-2.cを完成させなさい。ただし、書名データは文字列配列dataとしてあらかじめ用意されているが、このデータを作成する際に本来の書名とは関係のない数字が紛れ込んでしまっているため、書名から数字を排除する必要があります。ここでは、元の文字列srcと、その数字を排除した文字列dstを引数として、文字列dstの文字数を戻り値とする関数scrapeを定義して利用するものとします。

    [プログラム(その2)]
    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    #include <stdlib.h>
    #define NUM 10	//冊数
    
    /* 書名データ */
    const char data[][32] = {
    		"Al8gorithms",
    		"Artif1icial Intel99ligence",
    		"Autom3ata",
    		"Co7mput6er Architecture",
    		"Deep Learning8",
    		"Desi4gn 7Patterns",
    		"M3achine Learning",
    		"1Numerical 1Analysis",
    		"Object Orie65nted design",
    		"Programm3ing Langu98ages"};
    
    /* 文字列から数字を取って、文字数を返す */
    int scrape(char dst[], const char src[])
    {
    	int i,len=0;
    	
    	for(i=0; src[i]!='\0'; i++){
    		if([空欄]) continue;	//数字ならば以下の処理を飛ばして継続
    		/*
    		 ここを埋めて完成させる
    		*/		
    	}
    	dst[len] = '\0';
    	
    	return len;
    }
    
    int main(void)
    {
    	char *books[NUM]={NULL};	//各書名を示すNUM個のポインタからなる配列
    	char str[32]={0};
    	int i,j,len;
    
    	for(i=0; i<NUM; i++){
    		len = scrape(str,data[i]);			//文字数と文字列の取り出し
    		[ 空欄ア ];		//書名を格納するための領域確保
    		if(books[i]==NULL){                 //エラー処理
    			fprintf(stderr,"Error: Memory Allocation failure.\n");
    			for(j=0; j<i; j++) free(books[j]);  //確保済領域の解放
    			return EXIT_FAILURE;
    		}
    		for(j=0; j<=len; j++)
    			[ 空欄ウ ] = str[j];		//書名の格納(複数行可)
    	}
    
    	for(i=0; i<NUM; i++)
    		printf("%s\n",books[i]);	//書名の表示
    	
    	for(i=0; i<NUM; i++)
    		[ 空欄エ ]				//NUM冊分の領域解法
    
    	return 0;
    }
    

    [ヒント:10個の文字列を指し示すポインタを要素とする配列]
    array_points_string


  • [提出方法]
    • 電子メールの添付ファイルとして提出してください。宛先は指定のアドレスです。
    • 表題は、課題2とします。
    • 本文中には、本課題のプログラムを作成するためのポイントと思われる点について簡単に説明しなさい。
    • 添付ファイルは下記の2点です。
    • 1) ソースコード・ファイル kadai4-2.c
      2) 実行結果画面(ウインドウのみ)のハードコピーの画像ファイル kadai4-2.jpg

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

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

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