静岡理工科大学 菅沼ホーム 全体目次 演習解答例 付録 索引

第6章 配列とポインタ

  1. 6.1 配列
    1. (プログラム例 6.1 ) 平均値と平均値以下の人の出力
    2. (プログラム例 6.2 ) 平方根の計算とファイルへの出力
    3. (プログラム例 6.3 ) 大文字から小文字への変換
  2. 6.2 配列とポインタ
    1. (プログラム例 6.4 ) 1 次元配列,ポインタ,及び,初期化
  3. 6.3 2 次元以上の配列とポインタ
    1. (プログラム例 6.5 ) 2 次元配列,ポインタ,及び,初期化
  4. 6.4 メモリの動的確保
    1. 6.4.1 malloc と free(Javaを除く)
      1. (プログラム例 6.6 ) メモリの動的確保( 1 次元配列)
      2. (プログラム例 6.7 ) メモリの動的確保( 2 次元配列)
    2. 6.4.2 new と delete(C++)
      1. (プログラム例 6.8 ) メモリの動的確保( 1 次元配列,new)
      2. (プログラム例 6.9 ) メモリの動的確保( 2 次元配列,new)
      3. (プログラム例 6.10 ) new 演算子と代入・初期化
  5. 演習問題6

  この章では,配列とポインタについて説明します.Java にはポインタという言葉(概念)はありませんが,int や double などの基本形以外はすべてポインタで表現されていると考えた方が妥当です.したがって,Java を勉強する場合にも,ポインタという考え方は重要だと思います.以下,Java を例外扱いせずに述べていきますが,ポインタやアドレスといった言葉を明示的に使用しているような箇所は,Java に対しては適用できません.

6.1 配列

  前節までに述べたプログラム,例えば,英語と数学の平均値を求めるプログラム(プログラム例 5.7 )においては,各科目の点数を常に変数 x と y に読み込んでいるため(つまり,同じ記憶場所に記憶していているため),変数 x と y には直前に入力されたデータだけが記憶されており,それ以前に入力されたデータはすべて失われます.しかし,プログラムによっては,すべての入力されたデータを以後の処理に利用するため記憶しておきたい場合があります.例えば,平均点を計算した後,平均点以下の人を調べるためには,各人の点数が記憶されていなければ,同じデータを再度入力し直さなければなりません.

  各人の点数を異なる変数,例えば,x1,y1,x2,y2,・・・,xn,yn に入力(記憶)することによって問題は解決できるかもしれません.しかし,この方法は,人数が少ないときは可能ですが,人数が多くなると膨大な数の変数を定義しなければならず事実上不可能になります.さらに,人数が前もってわかっていないような場合は,基本的に不可能となります.そこで,登場するのが,配列array )変数という考え方です.数学的な例えをすれば,今まで述べた変数はスカラー変数であり,配列変数はベクトルに対応します(行列等に対応する配列変数も存在しますが,それについては,6.3 節で述べます).配列変数を定義し,その各要素(ベクトルの各要素)に各人の点数を保存しておけば上の問題を解決できることになります.

  ある変数がスカラー(単純変数)でなく,配列変数であることを定義するためには,例えば,以下のように記述します.
	double x[10];
		
  このように定義することにより,変数 x は 10 個の要素を持った配列変数( 10 次元のベクトル)とみなされ,この場合は,10 個の倍精度実数を記憶するために 80( 8×10 )バイトの連続した領域が確保されます.なお,基本的に,要素の数は定数でなければなりません.ただし,const 宣言された変数 n を使用して,
	const int n = 10;   // n の値は変更できない
	double x[n];
		
のように,宣言することは可能です.例えば,下の例のように,入力するデータの数に応じて配列を宣言したいことがしばしば起こりますが,不可能ですので注意してください( C/C++ のバージョンによっては可能ですが,現段階では使用しない方がよいと思います).以下のような処理を行いたい場合は,後に述べるように,特別な関数または new 演算子を使用する必要があります.
	int n;
	printf("データの数を入力してください ");
	scanf("%d", &n);
	double x[n];   // 誤り
		
  Java の場合は,new 演算子を使用して配列を以下のように宣言します.そのため,3 行目のように,配列要素の数を変数とすることも可能です(後に述べるように,C++ においても同様の方法を利用できます).
	double x[] = new double [10];
	  // n の値を入力等によって決めた後
	int y[] = new int [n];
		
  配列の各要素を参照するためには,ベクトルの場合と同様,添え字を利用します.ただし,[] を使用して x[i] のように記述すると共に,添え字 i は,ベクトルとは異なり,上のように定義した場合,0 から 9 まで変化します.従って,
	x[5] = 3.0;
	y    = x[3];
		
と書くと,配列 x の 6 番目の要素( 5 番目でないことに注意)に値 3.0 が代入され,4 番目の要素に記憶されている値が変数 y に代入されます.このように,配列の参照や代入は,添え字を利用して各要素ごとに行えますので,たとえば,配列 x の 2 から 15 番目の要素を配列 y の先頭部分にコピー(代入)したい場合は,以下に示すように,繰り返し文を使用して簡単に実行できます.14 個の単純変数を他の 14 個の単純変数に代入する場合と比較してみて下さい.
	for (i1 = 1; i1 <= 14; i1++)   // i1 の値に注意
		y[i1-1] = x[i1];
		
  文字列も char 型の配列として取り扱われますが,数値を要素とする配列とは多少異なる点がありますので,説明を追加しておきます.3.2.2 節で述べましたように,
	char c = 'x';
		
と宣言することにより,変数 c には 1 文字だけ記憶できます.この例では,その変数を「x」という文字で初期化しています(「'」が使われていることに注意).複数の文字,つまり,文字列を記憶するためには,複数の char 型変数を必要とします.従って,配列が重要な役割を果たします( Java においては,String クラスを使用するのが一般的です).

  例えば,9 個の文字( 10 個ではない)からなる文字列を,配列を使用して記憶するためには,
	char z[10];
		
という宣言が必要になります.なぜなら,配列を利用して文字列を扱う場合,文字列の最後には文字列の終わりであることを示す「\0」( NULL 文字)という文字を入れておく必要があるからです.そのため,(文字列の長さ+1)以上の大きさの配列を定義しておく必要があります.NULL 文字を使用しないで記憶することも可能ですが,printf や文字列を操作する関数(7.0.2 節参照)などを使いにくくなります.

  最後に,char 型変数に対する入出力について述べておきます.以下の例では,変数 z に対して,「 char z[10]; 」の宣言がされているものとします.なお,NULL 文字を使用せずに文字列を扱いたい場合は,%s を使用した入出力方法を利用できませんので,1 文字ずつ入力することになります.
	scanf("%c", &z[2]);     // z の 3 番目の要素に 1 文字入力する
	scanf("%s", z);         // 9 文字以下の文字列を入力する
	                        // (文字列の最後に \0 が付加される)
	printf("%c\n", z[2]);   // z の 3 番目の要素を出力する
	printf("%s\n", z);      // 文字列を出力する
		
(プログラム例 6.1 ) 平均値と平均値以下の人の出力 

  最初の例として,プログラム例 5.7 と似た問題を取り上げます.n 人いるクラスで試験を行ったとします.各人の点数を入力し,平均値を計算した後,平均値以下の人の番号と点数を出力するプログラムです.

01	/********************************/
02	/* 平均値の計算と平均値以下の人 */
03	/*      coded by Y.Suganuma     */
04	/********************************/
05	#include <stdio.h>
06
07	int main()
08	{
09		double mean = 0.0;
10		double x[50];
11		int n, i1;
12	/*
13	         データの数の読み込み
14	*/
15		printf("人数は? ");
16		scanf("%d", &n);
17
18		if (n <= 0 || n > 50)
19			printf("人数が不適当です\n");
20	/*
21	         データの読み込み
22	*/
23		else {
24			for (i1 = 0; i1 < n; i1++) {
25				printf("%d 番目の人の点は? ", i1+1);
26				scanf("%lf", &(x[i1]));
27				mean += x[i1];
28			}
29	/*
30	         平均値の計算と出力
31	*/
32			mean /= n;
33			printf("   平均値は=%f\n", mean);
34	/*
35	         平均値以下の人を調べ,出力
36	*/
37			for (i1 = 0; i1 < n; i1++) {
38				if (x[i1] <= mean)
39					printf("      %d番 %f点\n", i1+1, x[i1]);
40			}
41		}
42
43		return 0;
44	}
		
18,19 行目

  このプログラムは,10 行目で宣言しているように,50 個のデータしか記憶できません.従って,入力された人数が 50 人より多いとき,エラーメッセージを出し何も行わないようにするため,これらの行があります.同時に,0 以下の値も避けています.もし,プログラム内に
	x[55] = 3.6;
			
のような記述をしても,コンパイルやリンク時に何のエラーメッセージも出力されず,実行されてしまいます.しかし,このような文を実行することは,配列変数 x に用意された記憶領域の外にデータを書き込むことになり,場合によってはプログラム自身を破壊してしまいます.従って,最初に用意された領域外にデータが書き込まれることを避けるために 18 行目の if 文が必要になります.配列変数を使用するときは,領域外に書き込まれることがないよう,十分注意して下さい.

26 行目

 (i1 + 1)番目の人の点数が,配列変数 x の(i1 + 1)番目の要素に読み込まれます.

37 ~ 40 行目

  平均点以下の人を調べるための for ループです.全員に対しチェックする必要がありますので,再度,for 文を使う必要があります.

(プログラム例6.2) 平方根の計算とファイルへの出力 

  次のプログラムは,1 から 100 までの値に対する平方根を 1 おきに計算し,それらを一旦配列変数 x 及び y に入れた後(配列変数 x には 1 から 100,y にはその平方根),ファイルに出力するプログラムです.

  なお,このプログラムでは,出力するファイル名をプログラムで固定せず,キーボードから入力しています.

01	/****************************/
02	/* 平方根の計算             */
03	/*      coded by Y.Suganuma */
04	/****************************/
05	#include <stdio.h>
06	#include <math.h>
07
08	int main()
09	{
10		double data, x[100], y[100];
11		int i1;
12		char f_name[50];
13		FILE *out;
14	/*
15	     ファイル名の入力
16	*/
17		printf("出力ファイル名は? ");
18		scanf("%s", f_name);
19	/*
20	     平方根の計算
21	*/
22		data = 0.0;
23
24		for (i1 = 0; i1 < 100; i1++) {
25			data  += 1.0;
26			x[i1]  = data;
27			y[i1]  = sqrt(data);
28		}
29	/*
30	     出力
31	*/
32		out = fopen(f_name, "w");
33
34		for (i1 = 0; i1 < 100; i1++)
35			fprintf(out, "%f %f\n", x[i1], y[i1]);
36
37		return 0;
38	}
		
6 行目

  27 行目において,平方根を計算する関数 sqrt を使用しているため,このヘッダファイルが必要になります.同様に,三角関数,対数等,数学関係の関数を使用するときもこのヘッダファイルが必要になります.C++ の場合は,<cmath> を使用しても構いません.C の場合と同じ関数が使用されます.ただし,<cmath> をインクルードした時の関数名は std 名前空間内に所属するようになりますが,<math.h>をインクルードした時はグローバル関数になります.

10 行目

  平方根を計算する値は 1 から 100 までですので,変数 data や x に対する型宣言は int で構わないように見えますが,関数 sqrt に渡してやるデータ(カッコ内)は,必ず double である必要があります.このように,各関数に渡してやるデータは,その関数によって型が決まっています.必ず,定められた型を使用して下さい.

12 行目

  ファイル名は,一般に,文字列です.ファイル名を入力するために,配列変数 f_name の宣言をしています.この配列変数には,49 文字以下の文字列を記憶できます.なぜなら,文字列の場合,文字列の最後に文字列の最後であることを示す NULL 文字「\0」が付加されるため,この NULL 文字も文字数として数える必要があるからです.

18 行目

  ファイル名を入力しています.「%s」を使用すること,及び,変数の前に「&」が付かないことに注意して下さい.

26,27 行目

  変数 data の値を x[i1] に保存し,かつ,その値に対する平方根を計算し,y[i1] に保存しています.

34,35 行目

  if 文と同じように,for 文で繰り返す文が 1 文だけのときは,「{」と「}」を省略できます.

(プログラム例 6.3 ) 大文字から小文字への変換 

  文字( char 型)は,1 バイトの整数とみなすこともできます.実際,整数と同様の取り扱いが可能です.以下に示すプログラムでは,この性質を利用して,大文字を小文字に変換しています.アスキーコードでは,大文字は 65 ~ 90,小文字は 97 ~ 122 までです.したがって,文字を整数として扱い,32 を加えてやれば,大文字から小文字への変換が可能です.

/****************************/
/* 大文字から小文字への変換 */
/*  	coded by Y.Suganuma */
/****************************/
#include <stdio.h>

int main()
{
	int k = 0;
	char c[50];
/*
	 データ数の入力
*/
	printf("大文字の文字列を入力してください(49字以内) ");
	scanf("%s", c);
/*
	 小文字へ変換
*/
	while (c[k] != '\0') {
		c[k] += 32;
		k++;
	}
/*
	 出力
*/
	printf("%s\n", c);

	return 0;
}
		

6.2 配列とポインタ 

  4.1 節で述べた( 4.1 節を読んでない人は,読んでからこの節を読んで下さい)ポインタと配列とは非常に深い関係があります.例えば,
	int x[4] = {100, 200, 300, 400};
	int *y;   // int y[4]; と宣言した場合は,y = x; は実行できない
	char *c;
		
のような宣言がなされていたとして,以下の説明を行います.このとき,配列変数 x に対しては,図 6.1 の左に示すようなイメージを浮かべると理解しやすいと思います.つまり,x がポインタ変数であり,この変数の値が,連続した領域にある 4 つの変数 x[0],x[1],x[2],及び,x[3] の先頭アドレスを指しているというイメージです.従って,ポインタ変数 x の値を同じポインタ変数である y に代入,つまり,
	y = x; または y = &(x[0]);
		
という文を実行してやれば,図 6.1 の右に示すように,変数 x と変数 y はほとんど同じものになります.例えば,y[2] は x[2] と同じ箇所を参照することになります.ただし,厳密には,全く同じではありません.例えば,「y++」などの演算は可能ですが,「x++」は不可能です.x と y を全く同じように扱いたい場合は,x に対する定義部分を.後に述べる new 演算子を使用して,以下のように修正してやる必要があります.このようにすれば,変数 x は,図 6.1 の左と同じ形になり,x と y を全く同じように扱えます.
	int *x = new int [4];
	for (int i1 = 0; i1 < 4; i1++)
		x[i1] = 100 * (i1 + 1);
	int *y;   // int y[4]; と宣言した場合は,y = x; は実行できない
	char *c;
		
  また,先頭アドレスではなく,例えば,y = &x[2] とすることにより,y を x の後半 2 つのデータに対応する 2 個の要素を持った配列として使用することも可能です.つまり,y[0] と x[2],及び,y[1] と x[3] が同じものになります(右図参照).

  上で述べたように,変数 y はポインタですので,間接演算子を使用して配列の各要素を参照することも可能です.たとえば, *y という方法で,y[0] ( x[0] でもある.以下,同様)を参照できます.それでは,ポインタを使用して y[1] 以下を参照するにはどのようにすれば良いでしょうか.まず,
	y++;
	*y = ・・・;
		
のように,y の値を 1 だけ増加させれば,ポインタ変数 y は y[1] のアドレスを指すようになります.4.1 節でも述べたように,y は int 型のポインタ変数ですので,単純に 1 だけは増加しません.図 6.1 のような場合であれば,int 型変数の大きさ( 4 バイト)だけ増加し,24 という値になり,y[1] の記憶場所を指すことになります.y[2] 以降を参照する場合も,同様に,ポインタ変数を 1 ずつ増加させていけば可能です.

  しかし,この方法では,ポインタ変数 y の値自身が変化してしまい,後に同じ配列を参照したいとき困ることになります.もちろん,他のポインタ変数,例えば z にポインタの値を代入した後に上の操作を行えば問題ありませんが,通常,y[0],y[1],y[2],・・・ を,
	*y     = ・・・;
	*(y+1) = ・・・;
	a      = *(y+2);
	 ・・・
		
のように参照する方法の方がよく使用されます.

  ポインタ変数の値を実際 1 だけ増加させたいような場合は,char 型のポインタ変数を使用すれば可能です.例えば,
	c = (char *)x;
	c++;
		
とすれば,変数 c の値は 1 だけ増加し,図 6.1 の場合,21 になります.

  配列に対する初期化も,単純変数と同様,可能です.しかし,配列表現の場合とポインタ表現の場合とでは,微妙な違いがありますので注意して下さい.例えば,以下のような宣言により初期化を行ったとします.
	int x1[4] = {1, 2};
	int x2[] = {1, 2};    // Java においても可能
	int *x3 = {1, 2};     // 誤り
	char c1[15] = {"test data"};   // char c1[15] = "test data"; でも可
	char c2[] = {"test data"};   // char c2[] = "test data"; でも可
	char *c3 = {"test data"};   // char *c3 = "test data"; でも可
	int x1[4] = {0};    // すべての要素が 0 で初期設定される
		
  1 行目の場合は,配列変数 x1 に対し,4 つの値が入る領域がとられ,その最初の 2 つが 1 と 2 に初期設定されます.2 行目の宣言に対しては,同様に配列変数 x2 の 2 つのデータに対する初期設定が行われますが,2 つのデータが入る領域しか確保されません.このように,初期設定する場合は,「[]」内の数字を書かなくても,構いません.しかし,3 行目の表現は許されません(ポインタ変数 x3 そのものを初期化するものとみなされる).

  文字型の場合も,基本的には,上で述べた int 型に対する場合と同じです(ただし,{} は無くても良い).4 行目では,15 字入る領域が確保され,最初の 10 文字に "test data\0" が保存されます.5 行目では,同じように 10 文字が記憶されますが,領域として 10 文字分しか確保されません.また,文字列に対しては,6 行目の表現も可能です.

  すべての要素を 0 で初期設定したい場合だけは,最後の行のような表現が許されます.初期設定したい値が 0 でないときは,1 行目のように,最初の要素だけが初期設定されます.

(プログラム例6.4) 1次元配列,ポインタ,及び,初期化 

01	/****************************/
02	/* 配列とポインタ           */
03	/*      coded by Y.Suganuma */
04	/****************************/
05	#include <stdio.h>
06
07	int main()
08	{
09		int i1;
10		int *x1;                         /* intに対するポインタ */
11		int x2[6] = {1, 2, 3, 4, 5, 6};  /* 6つのデータ領域 */
12		int x3[]  = {1, 2, 3, 4};        /* 4つのデータ領域 */
13		char c1[15] = {"test data"};     /* 15個のデータ領域 */
14		char c2[] = {"test data"};       /* 10個のデータ領域 */
15		char *c3 = {"test data"};        /* 文字列を指す1つのポインタ */
16		char *c4;                        /* 文字列を指す1つのポインタ */
17	/*
18	     初期設定された値と確保された領域のサイズ
19	*/
20		for (i1 = 0; i1 < 6; i1++)
21			printf("%d ", x2[i1]);
22		printf("(%dバイト)\n", sizeof(x2));
23		for (i1 = 0; i1 < 4; i1++)
24			printf("%d ", x3[i1]);
25		printf("(%dバイト)\n", sizeof(x3));
26		printf("%s (%dバイト)\n", c1, sizeof(c1));
27		printf("%s (%dバイト)\n", c2, sizeof(c2));
28		printf("%s (%dバイト)\n", c3, sizeof(c3));
29	/*
30	     要素の参照と変更
31	*/
32		x1      = x2;   // x1 = &x2[0]の意味
33		x1[1]   = -1;
34		*(x1+2) = -2;
35		x2[3]   = -3;
36		*(x2+4) = -4;
37		for (i1 = 0; i1 < 6; i1++)
38			printf("%d ", x1[i1]);
39		printf("\n");
40		for (i1 = 0; i1 < 6; i1++)
41			printf("%d ", x2[i1]);
42		printf("\n");
43
44	//	c1 = "test1 data    ";
45	//	c2 = "test2 data";
46		c3 = "test3 data";   // 文字列のアドレスを代入
47		c4 = "test4 data";   // 文字列のアドレスを代入
48		printf("%s (%dバイト)\n", c3, sizeof(c3));
49		printf("%s (%dバイト)\n", c4, sizeof(c4));
50
51		return 0;
52	}
		
22,25 行目

  int 型配列のために確保された領域の大きさを出力しています.int 型は 4 バイトであるため,それぞれ,24 および 16 が出力されます.

26 ~ 28 行目

  宣言および初期設定の方法により sizeof の出力が異なることに注意してください.特に,28 行目に対しては,文字列の長さではなく,ポインタ変数として使用される領域の大きさが出力されます.

32 行目

  配列 x2 の先頭アドレスをポインタ変数 x1 に代入しています.このようにすることによって,33 から 36 行目に見るように,x1 と x2 を全く同様に使用できます.また,先頭アドレスではなく,例えば x1 = &x2[2] とすることにより,x1 を x2 の後半 4 つのデータに対応する 4 個の要素を持った配列として使用することも可能です.

44 ~ 47 行目

  44,45 行目のように,配列に代入することはできません.ただし,46,47 行目のように,char に対するポインタとして宣言されていれば,文字列(のアドレス)を代入することが可能です.

  プログラム例6.4 を実行すると,以下のような結果が得られます.
	1 2 3 4 5 6 (24バイト)
	1 2 3 4 (16バイト)
	test data (15バイト)
	test data (10バイト)
	test data (4バイト)
	1 -1 -2 -3 -4 6 
	1 -1 -2 -3 -4 6 
	test3 data (4バイト)
	test4 data (4バイト)
			

6.3 2 次元以上の配列とポインタ

  2 次元以上の配列に対しても,1 次元の場合と同様に,ポインタを介して参照等が可能です.例えば,2 次元配列の場合,以下のような宣言がなされていたとします.このとき,配列とポインタの関係は,概念的に,図 6.2 の左側のような関係になります.
	int a[3][2];   // int a[][] = new int [3][2];   Javaの場合
	int x[3][2] = {{100, 200}, {300, 400}, {500, 600}};   // int x[][] = {{100, 200}, {300, 400}, {500, 600}};   Javaの場合
	int **y, *z, *w[3];
		

  まず,実際にデータが記憶される場所として,x[0][0] から x[2][1] の連続した 6 個の領域がとられます(この例では,それらが,20 番地からの領域であるとしています.次節に示すようなメモリを動的に確保する場合や Java の場合は,配列の配列として処理されるため必ずしも連続的な領域になる保証はありません.).添え字が最も右側から変化していることに注意して下さい(この点は,3 次元以上の配列の場合も同様です).次に,x[0] から x[2] のポインタを要素とする配列があり,その各要素は,配列の各行の値が保存されている場所の先頭を指しています.変数 x は,このポインタ配列の先頭を指していることになります.したがって,以下に示すような記述を行えば,変数 x は,ポインタ変数 y (ポインタ配列に対するポインタ)と同等のものになり,
	y    = &(w[0]);
	w[0] = x[0];
	w[1] = x[1];
	w[2] = x[2];
		
の記述により,例えば,x[1][1],y[1][1],及び,w[1][1] は,すべて同じ場所を指すことになります.

  配列に対し連続的な領域が確保されていますので,図 6.2 の右に示すように,2 次元の配列を 1 次元の配列として処理することも可能です.例えば,
	z = (int *)x;   // z = &x[0][0]; でも良い
		
のような処理をすれば,z[3] と x[1][1] が同じ場所を指すことになります.一般に,x[i][j] と z[(i×k) + j] は,同じ場所を指すことになります.ただし,k は,配列変数 x の宣言における列の数です(上の例では 2 ).さらに,
	int *z = &x[1][0]; // int *z = x[1]; でも良い
		
のような処理をすれば,配列 x の 2 行目以降を 1 次元配列として扱うことができます.

  また,初期化も,以下のような方法で可能です.列の数を必ず記述しなければならない点に注意してください.誤りと書いたものを除き,各宣言とも正しく初期設定されると共に,各行の右側に書かれたような領域が確保されます.1 次元の配列の場合と同様,配列とポインタでは多少の違いが出てきます( Java の場合は多少異なります).
	int x1[][] = {{1, 2}, {3, 4}, {5, 6}};     // 誤り.Javaはこの方法で初期化
	int x1[3][2] = {{1, 2}, {3, 4}, {5, 6}};   // 3行2列
	int x2[][2] = {{1, 2}, {3, 4}};            // 2行2列
	int x3[3][] = {{1, 2}, {3, 4}};            // 誤り
	char c1[5][10] = {"zero", "one", "two", "three"};   // 5行10列
	char c2[][10] = {"zero", "one", "two", "three"};    // 4行10列
	char *c3[] = {"zero", "one", "two", "three"};       // 4つのポインタ
	char c4[5][] = {"zero", "one", "two", "three"};     // 誤り
		
(プログラム例 6.5 ) 2 次元配列,ポインタ,及び,初期化 

/****************************/
/* 2次元配列とポインタ      */
/*      coded by Y.Suganuma */
/****************************/
#include <stdio.h>

int main()
{
	int x1[3][2] = {{1, 2}, {3, 4}, {5, 6}};          /* 3行2列 */
	int x2[][2] = {{1, 2}, {3, 4}};                   /* 2行2列 */
	char c1[5][10] = {"zero", "one", "two", "three"}; /* 5行10列 */
	char c2[][10] = {"zero", "one", "two", "three"};  /* 4行10列 */
	char *c3[] = {"zero", "one", "two", "three"};     /* 4つのポインタ */
	int *px, *px1;
	char *pc;
/*
	 初期設定された値と確保された領域のサイズ
*/
	printf("%d (%dバイト)\n", x1[1][1], sizeof(x1));
	printf("%d (%dバイト)\n", x2[1][1], sizeof(x2));
	printf("%s (%dバイト)\n", c1[3], sizeof(c1));
	printf("%s (%dバイト)\n", c2[3], sizeof(c2));
	printf("%s (%dバイト)\n", c3[3], sizeof(c3));
/*
	 要素の参照
*/
	px = (int *)x1;
	px1 = x1[2];
	pc = &(c1[2][0]);
	printf("%d %d %d %d\n", x1[2][1], px[2*2+1], *(px+2*2+1), px1[1]);
	printf("%c%c%c %s\n", c1[2][0], pc[1], *(pc+2), pc);

	return 0;
}
		

  このプログラムを実行すると,以下のような結果が得られます.
	4 (24バイト)
	4 (16バイト)
	three (50バイト)
	three (40バイト)
	three (16バイト)
	6 6 6 6
	two two
		
  3 次元以上の配列の対しても,ポインタ配列が増えていく以外,2 次元配列と同様です.なお,初期設定等の際,要素数を省略できるのは,一番左側の要素数だけです.例として,

int x[4][3][2]; // 初期設定等で省略できるのは 4 という値だけ

と宣言した場合,24 個の int 型のデータが入る連続した領域が確保され,その概念図は図 6.3 のようになります.

この場合,変数 x の最も右側の添え字から順に変化していく点に注意して下さい.また,ポインタの概念からすると,変数 x は,

int ***y;

と宣言されたポインタ変数 y と同等のものになります.さらに,2 次元配列の場合と同様,

int *z = (int *)x; // int *z = &x[0][0][0];

という記述により,3 次元配列を 1 次元配列として参照することも可能です.上で定義した 3 次元配列の場合,x[i][j][k] と z[((i*3)+j)*2+k] が同じ場所を指すことになります.

6.4 メモリの動的確保

6.4.1 malloc と free(Javaを除く)

  クラスの人数が 50 人であるとします.各人の国語,数学,及び英語の試験の点を処理するため,以下のような配列変数を定義したとします.
	int a[50][3];
		
クラスの人数や対象とする科目数が減少するような場合は特に問題はありませんが,増加する場合は,同じプログラムで処理できなくなります.

  このような場合に対処する 1 つの方法は,printf 等のようにシステムが所有している関数 callocmalloc を利用することです.これらの関数を利用することにより,各行毎に異なった型,サイズの配列を定義したり,入力データ等によって配列のサイズを動的に変化させたりすることが可能になります.また,確保したメモリがプログラム実行の進展に従い不足したような場合は,realloc 関数によってメモリ領域を変更することもできます.

  なお,これらの関数を使用して確保されたメモリは,プログラムが終了するまで存在します.確保されたメモリを開放したい場合は,free 関数を使用する必要があります.ただし,領域を開放してもその領域をプログラムが有効に利用してくれるとは限りません.なぜなら,C/C++ においてはガーベッジコレクションを行ってくれないからです( Java では,行ってくれます).したがって,頻繁に領域の確保と解放を繰り返すと,未使用の領域がふくれあがり,頻繁にスワッピングが起こったり,または,メモリが不足するような状態が発生します( realloc の場合も同様).できるだけ,プログラムの開始時に領域をまとめて確保し,プログラム終了時に解放されるようなプログラムを書いた方が安全だと思います.

(プログラム例 6.6 ) メモリの動的確保( 1 次元配列)

  次のプログラムは,入力されたデータ数に応じた配列を確保し,入力されたデータをその配列に保存し,それらの和を求めています.

/**********************************/
/* メモリの動的確保(1次元配列) */
/*      coded by Y.Suganuma       */
/**********************************/
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int i1, n, sum = 0, *x;
/*
		  データの数
*/
	printf("データの数は? ");
	scanf("%d", &n);
/*
		  領域の確保
*/
	x = (int *)malloc(n * sizeof(int));   // 1次元配列
/*
		  データの入力と和の計算
*/
	for (i1 = 0; i1 < n; i1++) {
		printf("   %d 番目のデータを入力してください ", i1+1);
		scanf("%d", &x[i1]);
		sum += x[i1];
	}

	printf("和 = %d\n", sum);
/*
		  領域の開放
*/
	free(x);

	return 0;
}
		

(プログラム例 6.7 ) メモリの動的確保( 2 次元配列)

  このプログラムでは,n 行 m 列の 2 次元配列を利用し,その中に n クラスにおける各学生の点数を入力しています.各クラスの学生数は m 人とし,クラス毎に異なっても構いません.また,各クラスごとの平均も計算し,配列に入れています.

/**********************************/
/* メモリの動的確保(2次元配列) */
/*      coded by Y.Suganuma       */
/**********************************/
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int i1, i2, m, n, *mean, **ten;
/*
		  データの数
*/
	printf("クラスの数は? ");
	scanf("%d", &n);
/*
		  領域の確保
*/
	ten  = (int **)malloc(n * sizeof(int *));   // ポインタの1次元配列(配列の配列)
	mean = (int *)calloc(n, sizeof(int));   // 1次元配列
/*
		  データの入力と和の計算
*/
	for (i1 = 0; i1 < n; i1++) {
		printf("%d 番目のクラスの人数は? ", i1+1);
		scanf("%d", &m);
		ten[i1] = (int *)calloc(m, sizeof(int));   // 1次元配列の先頭アドレスを代入
		for (i2 = 0; i2 < m; i2++) {
			printf("   %d 番目の学生の点数は? ", i2+1);
			scanf("%d", &ten[i1][i2]);
			mean[i1] += ten[i1][i2];
		}
		mean[i1] /= m;
	}
/*
		  平均の出力
*/
	printf("各クラスの平均点は以下の通りです\n");
	for (i1 = 0; i1 < n; i1++)
		printf(" %d", mean[i1]);
	printf("\n");
/*
		  領域の開放
*/
	for (i1 = 0; i1 < n; i1++)
		free(ten[i1]);
	free(ten);
	free(mean);

	return 0;
}
		

6.4.2 new と delete(C++)

  C++ では,new 演算子及び delete 演算子を使用して,malloc や free 関数と同じように,メモリの動的確保及び解放を行うことが可能です.なお,Java においては,配列を扱う際には常に new 演算子を使用します.

  new 演算子は,データ型を与えると,例えば,
	int *pi = new int;   // 2 で初期設定したい場合は,new int (2);
		
のように,その型のデータを記憶するのに必要な大きさのメモリを確保し,必要ならば初期設定し,そのメモリ領域へのポインタを返します.指定した大きさのメモリを確保できないときは,0 を返します.ただし,配列の場合は,初期設定することはできません.

  delete 演算子は,new 演算子によって確保されたメモリを解放します.意図的に解放しない限り,確保されたメモリはプログラム終了時まで存在しますので,必要な場合は,例えば,
	delete pi;
		
のように,delete 演算子によって解放して下さい.また,new によって配列を確保した場合は,次のような delete の使用方法をして下さい.ガーベッジコレクションが行われない点は同様ですので,前節で述べましたように,確保と解放には十分注意してください.なお,Java においては,自動的にガーベッジコレクションが実行され,また,delete 演算子も存在しません.
	double *dp = new double [10];  // 「10」の部分は変数でも構わない
	  ・・・・・
	delete [] dp;
		
  2 次元以上の配列も同様にして確保できます.例えば,2 行 3 列(行や列の数は変数でも構わない)の配列を確保するためには,以下のように記述します.この結果,概念的には,右図に示すような領域が確保されます.
	double** pd = new double* [2];
	for (i1 = 0; i1 < 2; i1++)
		pd[i1] = new double [3];
		
また,解放するときは,以下のようになります.
	for (i1 = 0; i1 < 2; i1++)
		delete [] pd[i1];
	delete [] pd;
		
  なお,realloc 関数に対応するような演算子はありません.new と delete 演算子の詳細な使用方法については,プログラム例を参考にして下さい.

(プログラム例 6.8 ) メモリの動的確保( 1 次元配列,new ) 

  プログラム例 6.6 を new と delete を使用して書いた例です.

/**************************************/
/* メモリの動的確保(1次元配列,new) */
/*      coded by Y.Suganuma           */
/**************************************/
#include <stdio.h>

int main()
{
/*
		  データの数
*/
	int n;
	printf("データの数は? ");
	scanf("%d", &n);
/*
		  領域の確保
*/
	int *x = new int [n];   // 1次元配列
/*
		  データの入力と和の計算
*/
	int sum = 0;
	for (int i1 = 0; i1 < n; i1++) {
		printf("   %d 番目のデータを入力してください ", i1+1);
		scanf("%d", &x[i1]);
		sum += x[i1];
	}

	printf("和 = %d\n", sum);
/*
		  領域の開放
*/
	delete [] x;

	return 0;
}
		

(プログラム例 6.9 ) メモリの動的確保( 2 次元配列,new ) 

  プログラム例 6.7 を new と delete を使用して書いた例です.

/**************************************/
/* メモリの動的確保(2次元配列,new) */
/*      coded by Y.Suganuma           */
/**************************************/
#include <stdio.h>

int main()
{
/*
		  クラスの数
*/
	int n;
	printf("クラスの数は? ");
	scanf("%d", &n);
/*
		  領域の確保
*/
	int **ten = new int * [n];   // ポインタの1次元配列(配列の配列)
	int *mean = new int [n];   // 1次元配列
/*
		  データの入力と和の計算
*/
	for (int i1 = 0; i1 < n; i1++) {
		mean[i1] = 0;
		int m;
		printf("%d 番目のクラスの人数は? ", i1+1);
		scanf("%d", &m);
		ten[i1] = new int [m];   // 1次元配列の先頭アドレスを代入
		for (int i2 = 0; i2 < m; i2++) {
			printf("   %d 番目の学生の点数は? ", i2+1);
			scanf("%d", &ten[i1][i2]);
			mean[i1] += ten[i1][i2];
		}
		mean[i1] /= m;
	}
/*
		  平均の出力
*/
	printf("各クラスの平均点は以下の通りです\n");
	for (int i1 = 0; i1 < n; i1++)
		printf(" %d", mean[i1]);
	printf("\n");
/*
		  領域の開放
*/
	for (int i1 = 0; i1 < n; i1++)
		delete [] ten[i1];
	delete [] ten;
	delete [] mean;

	return 0;
}
		

(プログラム例 6.10 ) new 演算子と代入・初期化 

  以下に示す例においては,new 演算子と代入・初期化との関係を,様々な型の変数に対して行っています.クラスに関する説明がまだですので,その部分に関しては,クラスの説明が終了した後,再度見直してみてください.Java に対するプログラム例においても同様です.

01	/****************************/
02	/* new 演算子と代入・初期化 */
03	/*      coded by Y.Suganuma */
04	/****************************/
05	#include <stdio.h>
06	
07	/*****************/
08	/* クラスComplex */
09	/*****************/
10	class Complex
11	{
12		public:
13			double x, y;
14			Complex ()
15			{
16				x = 0.0;
17				y = 0.0;
18			}
19	};
20	
21	/********/
22	/* main */
23	/********/
24	int main()
25	{
26						// 単純変数
27	//	int p1 = 0;
28	//	int p4 = p1;
29	//	p4 = 1234;
30	//	printf("%d\n", p1);   // 0 を出力
31	//	printf("%d\n", p4);   // 1234 を出力
32		int *p1 = new int;
33		int *p4 = p1;
34		*p4 = 1234;   // p1 が示す値も 1234 になる
35		printf("%d\n", *p1);
36		printf("%d\n", *p4);
37						// 複数データ(配列と同等)
38	//	int p2[4];
39	//	int p5[4];
40		int *p2 = new int [4];
41		int *p5;
42		p5 = p2;   // p5 = &p2[0] と同じ
43		p5[2] = 200;   // p2[2] の値も 200
44		printf("%d\n", *(p2+2));
45		printf("%d\n", *(p5+2));
46		printf("%d\n", p2[2]);   // このように記述しても良い
47						// クラスのオブジェクト
48	//	Complex p3;
49	//	Complex p6 = p3;
50	//	p6.x = 30;   // p3.x の値は変化しない
51	//	p6.y = 40;   // p3.y の値は変化しない
52	//	printf("%f %f\n", p3.x, p3.y);
53	//	printf("%f %f\n", p6.x, p6.y);
54		Complex *p3 = new Complex();
55		Complex *p6 = p3;
56		p6->x = 30;   // p3->x の値も 30
57		p6->y = 40;   // p3->y の値も 40
58		printf("%f %f\n", p3->x, p3->y);
59		printf("%f %f\n", p6->x, p6->y);
60		printf("%f %f\n", (*p3).x, (*p3).y);   // このように記述しても良い
61						// 領域の開放
62		delete p1;
63		delete [] p2;
64		delete p3;
65	
66		return 0;
67	}
		
10 行目~ 19 行目

  クラスの定義です.ここでは,2 つの double 型変数 x,y から構成される新しい Complex 型の変数が定義されたという程度で理解しておいて下さい.

27 行目~ 31 行目

  int 型の変数 p1,p4 を定義し,p1 は 0 で初期設定し,それを p4 に代入(初期設定)しています.29 行目において,p4 に 1234 を代入していますので,p1 の値は 0,p4 の値は 1234 になっているはずです.p1 と p2 は異なる変数であり,代入は,代入する値をコピーして実行されるわけですから,当然の結果と言えます.なお,27 行目~ 31 行目を実行する(のコメントを外す)ためには,32 行目~ 36 行目,及び,62 行目を削除する(コメントにする)必要があります.

32,33 行目

  27 行目~ 31 行目とは異なり,p1,p4 はポインタとして定義してあります.32 行目において,int 型のデータが入る領域が確保され,そのアドレス(右図においては 20 番地)が変数 p1 に代入されます.「 * 」は,変数 p1 が,int 型のデータが入る領域を指すアドレスが記憶される変数(ポインタ)であることを表しています.33 行目において,p1 の値がコピーされて p4 に代入されますが,p1 はアドレスですので p1 に記憶されているアドレスが p4 に代入されます.その結果,p1 と p4 は,全く同じ領域を指すことになります(右図参照).

34 行目

  この文は,変数(ポインタ)が指すアドレスに 1234 を代入するという意味になります.p1 の値と p4 の値は等しいので,この記述と「 *p1 = 1234; 」は同じ結果になり,いずれの場合も,確保した領域に値 1234 が記憶されます.従って,35,36 行目の結果からも明らかなように,いずれの変数を使用しても同じ値が出力されます(出力結果の 1,2 行目).

40 行目~ 46 行目

  40 行目において,4 個分の int 型データが入る領域が確保され,その先頭アドレス(下の図では 20 番地)が変数 p2 に代入されます(下の左図).42 行目において,p2 の値のコピーが p5 に代入されますので,p2 と p5 は同じ場所を指すことになります(下の右図).そのため,43 行目のように,p5[2] の値を変更すると p2[2] の値も変化します(出力結果の 3 行目~ 5 行目).

  C/C++ における配列変数の宣言方法は,38,39 行目のようになりますが,new 演算子を使用した 40 行目のような宣言方法でも,少なくとも使用する立場からすれば,変数 p2 は配列変数と同等のものになります.ただし,40 行目を 38 行目のように宣言しても問題ありませんが,41 行目の p5 を 39 行目のように宣言すると,42 行目が実行できなくなります(コンパイルエラー).なお,38 行目を生かすためには,40行目,63行目を削除する必要があります.

  配列内の各要素の参照は,43,46 行目のように添え字を使用するか,または,44,45 行目のように間接演算子を使用して行います.なお,添え字は 0 から始まるため,p5[2] は,最初から見て 3 番目のデータであることを表します.

48 行目~ 53 行目

  Complex クラスのインスタンス(オブジェクト) p3 を生成すると共に,そのオブジェクトで,変数 p6 を初期化しています.クラス内の変数 x,y は,すべて,0 で初期化されます.27,28 行目と同様,p3 の値のコピーが p6 に代入されるだけですので,p6 の値を変更( 50,51 行目)しても,変数 p3 の値は変化しません.48 行目~ 53 行目を実行する(のコメントを外す)ためには,54 行目~ 60 行目,及び,64 行目を削除する(コメントにする)必要があります.

54 行目~ 60 行目

  Complex クラスのインスタンス(オブジェクト)を記憶できる領域が確保され,そのアドレスが変数 p3 に代入されます( 54 行目).40 行目~ 46 行目と同様,この例においても,p3 と p6 が同じ場所を指しているため,どちらの変数を使用しても同じ結果になります(出力結果の 6 行目~ 8 行目).

62 行目~ 64 行目

  確保した領域を必要としなくなった場合は,delete 演算子によって,確保されたメモリを解放してやる必要があります.ただし,C/C++ においては,ガーベッジコレクションが行われないため,頻繁に大きなサイズの new や delete を繰り返すと,残されたゴミのためメモリが圧迫されることになります.

  このプログラムを実行すると,以下に示すような結果が得られます.
1234
1234
200
200
200
30.000000 40.000000
30.000000 40.000000
30.000000 40.000000
			

演習問題6

[問1] n (入力)次元のベクトル a の各要素を配列変数の各要素に入力した後,その大きさ(|a|)を出力するプログラムを書け.

[問2] n (入力)次元のベクトル ab の各要素を配列変数の各要素に入力した後,ベクトル ab の和(c = a + b)を出力するプログラムを書け.

[問3] n 次元のベクトル ab の内積を計算し出力するプログラムを書け.なお,n,および,各ベクトルの要素の値はキーボードから入力するものとする(ベクトルに対しては,配列変数を利用するものとする).また,ベクトル ab の内積 x の定義は以下の通りである.
x = a11 + a22 +・・・+ ann

[問4]配列 a に n (入力)個のデータ,また,配列 b に n 個のデータを入力した後,もし,a[i] が b[i] より大きければ,a[i] と b[i] を入れ換える操作を行うプログラムを書け.このとき,この操作を行った回数,及び,入れ換えを行った a[i],b[i] の値を出力するものとする.

[問5] n (入力)人の英語,及び,数学の点を入力し,各科目の平均点を計算した後(平均点も出力),どちらかの科目が平均点以下の人の数を出力するプログラムを書け.

[問6]平面上の n 個の点の座標(xi, yi)を読み込み,原点(0, 0)からの平均距離を計算した後,平均距離,原点から平均距離以上離れたすべての点の座標,及び,その数を出力するプログラムを書け.

[問7]今,ある物質を構成する n (未知数)個の成分の各質量がファイルに保存されていたとする.これらの成分を入力した後,各成分の全体に対する%を別のファイルへ出力するプログラムを書け.

[問8] n (入力)人のテストの点を入力し,0~10 点,11~20 点,・・・,91~100 点の範囲に入る人数を計算した後,下に示すようなグラフを出力するプログラムを書け(「*」の数は人数と同じとする).
     RANGE    NUM
           0- 10    2   **
          11- 20    5   *****
       ・・・・
          91-100    3   ***
		
[問9]各月の売り上げ( 0~9,整数)を入力し,以下のようなグラフを出力するプログラムを書け.
                *
                *                        *
            *   *         ・・・・・       *  *
            *   *   *                 *  *
          --------------  ・・・・・  ---------
       1   2   3     ・・・・・      11 12
		
[問10]ある店舗の毎日の売上の記録 xi(i = 1~30 )がある(入力データとして読み込む).ある与えられた日(入力データ)を基準にした前後 7 日間(例えば,5 日が与えられたときは,2 日から 8 日)の売上高の平均を計算し,出力するプログラムを書け.ただし,前後の日にちが 0 以下になったり 31 以上になった場合は,1 または 30 までの平均を計算する(例えば,2 日が与えられたときは,1 日から 5 日までの平均を計算)ものとする.また,0 以下または 31 以上の日にちが与えられたときは,再入力をうながすものとする( goto 文は使用しない).

[問11] n (入力)個の正の整数値を入力し,末尾の桁が 0,1,・・・,9 のものの個数とその割合(%)を出力するプログラムを書け.

[問12]ある学年に n (入力)クラスあり,各クラスには m 人(入力,クラス毎に異なる)の生徒がいるものとする.全員に対しある試験を実施したとき( 2 次元の配列に読み込む),全体における最高点,全体の平均点,及び,その平均点以上の各クラスにおける人数を出力するプログラムを書け.

[問13]平面上の n (入力)個の点の座標を入力した後,すべての点の間の距離を計算し,その距離が最大になる点の組( 2 つの点の座標),及び,その距離を出力するプログラムを書け.

[問14] n (入力)人の試験の点を入力し,上位 m(入力)人の平均点を計算するプログラムを書け.ただし,m << n ( m は n に比較して十分小さい)とし,すべての( n 人の)データを配列に保存し,並べ替えるようなことはしないものとする.

[問15]今,ある物質を構成する成分の数 n と各成分の質量がファイルに保存されていたとする.これらの成分を動的確保した配列に入力した後,各成分の全体に対する%を出力するプログラムを書け.

[問16]ある学年に n (入力)クラスあり,各クラスには m 人(入力,クラス毎に異なる)の生徒がいるものとする.全員に対しある試験を実施したとき,各人の名前,点数を動的確保した配列に保存した後,学年平均点以下の人の名前とその点数を出力するプログラムを書け.

静岡理工科大学 菅沼ホーム 全体目次 演習解答例 付録 索引