第4週 2次元配列
INDEX

本日の目標

  • 2次元配列
  • 2次元配列とポインタの関係を理解することができる。
  • 2次元配列をポインタを使って操作することができる。
  • 関数間で2次元配列の受け渡しがができる。

予習・復習

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

本日の講義・演習予定

  1. 2次元配
  2. 2次元配列とポインタ
  3. 関数への2次元配列の受け渡し
  4. 演習問題
  5. 提出課題
内 容
  1. 2次元配列
  2. 2次元配列とポインタ
  3. 関数への2次元配列の受け渡し


2次元配列

2次元配列を宣言する

 1次元配列を一つの行として、複数の行からなる配列を2次元配列と言います。

2次元配列の宣言
要素のデータ型 配列名[行要素数][列要素数];

2次元配列宣言の例)
int a[2][3];

2次元配列の初期化
要素のデータ型 配列名[行要素数][列要素数] = {{値,値,・・・},{値,値,・・・},・・・};

2次元配列の初期化の例)
int a[2][3] = {{1,2,3},{4,5,6}};

2次元配列の初期化と各要素の参照の例)
array2d.c
#include <stdio.h>

int main(void)
{
	int a[2][3] = {{1,2,3},{4,5,6}};	//2次元配列の初期化
	int i,j;

	for(i=0; i<2; i++){
		for(j=0; j<3; j++){
			printf("a[%d][%d] = %2d\n",i,j,a[i][j]);
		}
	}
    
	return 0;
}

演習

  1. 次は、2行3列からなるint型の行列maとmbの各要素の値を入力して、その和を行列mcに格納した結果を表示するプログラムです。空欄を埋めてプログラムを完成させなさい。
  2. array2add.c
    #include <stdio.h>
    
    int main(void)
    {
    	int [[空欄ア]];
    	int i,j;
    
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			printf("[[空欄イ]] = ",i,j);
    			scanf("%d",[[空欄ウ]]);
    		}
    	}
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			printf("[[空欄エ]] = ",i,j);
    			scanf("%d",[[空欄オ]]);
    		}
    	}
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			mc[i][j] = [[空欄カ]];
    		}
    	}
    
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			printf("%4d",[[空欄キ]]);
    		}
    		printf("\n");
    	}
    
    	return 0;
    }
    
    実行結果の例
    ma[0][0] = 1
    ma[0][1] = 2
    ma[0][2] = 3
    ma[1][0] = 4
    ma[1][1] = 5
    ma[1][2] = 6
    mb[0][0] = 7
    mb[0][1] = 8
    mb[0][2] = 9
    mb[1][0] = 10
    mb[1][1] = 11
    mb[1][2] = 12
       8  10  12
      14  16  18
    



二次元配列とポインタ

二次元配列のメモリ配置

 二次元配列はメモリにどのように配置されるのでしょうか。例えば、int型の2行3列の配列aは下図のように0行目の要素を連続で配置し、続けて1行目の要素を連続して配置します。2次元の配列もメモリには1次元の構造として格納されるわけです。

array2_1

二次元配列の要素のアドレス
&配列名[添字][添字]

二次元配列は配列の配列

 C言語には厳密な意味で二次元配列(多次元配列)は存在しません。2行3列の配列の場合なら下図のように、二次元配列は0行目と1行目の2つの1次元配列を2つポインタからなる配列で指し示す構造をしています。さらに、配列名は二次元配列を示すポインタであり、開始アドレス示します。

array2_2

二次元配列を示すポインタ(二次元配列の開始アドレス)
配列名

配列とポインタ

配列のポインタ

 ポインタとしての配列名aの値を+1すると、そのアドレスは1行のあたりの要素数分だけ先に進みます。例えば、int型で2行3列の配列ならば1行が3つの要素からなっているので、4バイト(int型)×要素3個分=12バイト分だけ先に進みます。仮に配列の開始アドレスaが1000番地ならばa+1は100C番地となります。
 配列を指し示すためのポインタを利用する場合は、その示す先の配列のサイズに合わせて用意しなければなりません。以下のコード例では配列aを指し示すためのポインタが、int型で要素3個分のサイズを持つように、int (*p)[3]と宣言されています。
	int a[2][3] = {{11,12,13},{21,22,23}};
	int (*p)[3];

	p = a;

配列を指し示すポインタの宣言
型 (*ポインタ名)[1行当たりの要素数]

各行の開始アドレス

 列部分の添字がない配列の要素にアドレス演算子&を適用したものは、その配列の行を指し示すポインタになります。すわなち行の開始アドレスを示します。例えば、先の例では配列名aと0行目の開始アドレス&a[0]は同じアドレスを示すことになります。

二次元配列の各行のポインタ
&配列名[添字]

ポインタの配列

 一つのポインタを一つの要素として複数の要素からなる配列として扱うこともできます。

ポインタの配列の宣言
型 *配列名[要素数]

ダブルポインタ

 ポインタを指し示すポインタをダブルポインタと言います。

ダブルポインタの宣言
型 **配列名

プログラミング実験

  1. [実験1] 配列のメモリアドレスを表示する
  2.  次のプログラムを入力、コンパイル、実行して配列の各要素の先頭アドレスを調べて、メモリ配置図を描きなさい。
    apointer1.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[2][3] = {{1,2,3},{4,5,6}};
    	int i,j;
    
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			printf("a[%d][%d] = %2d (%p)\n",i,j,a[i][j], &a[i][j]);
    		}
    	}
        
    	return 0;
    }
    

  3. [実験2] 配列名が配列の開始アドレスであり配列を示すポインタであることを確認する
  4.  二次元配列の配列名が配列の開始アドレスを示していること、配列名に+1することで次の行の先頭アドレスを示すこと、すなわちポインタであることを次のプログラムを入力(追加)、コンパイル、実行することで確認しなさい。先のメモリ配置図に結果をわかりやすく追加記入しなさい。
    apointer2.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[2][3] = {{1,2,3},{4,5,6}};
    	int i,j;
    
    	printf("配列名a      -> (%p)\n",a);		//配列の開始アドレス(0行目の先頭アドレス)
    	printf("        a+1 -> (%p)\n",a+1);	//1行目の先頭アドレス
    	
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			printf("a[%d][%d] = %2d (%p)\n",i,j,a[i][j], &a[i][j]);
    		}
    	}
    
    	return 0;
    }
    
     配列名は配列の開始アドレスを示すポインタです。このポインタを+1するとその値が次の行の先頭アドレスを示すことがわかります。このことから、配列名は配列の開始アドレスを示すと同時に0行目の先頭アドレスを示していることがわかります。さらに、このポインタを+1すると4バイト×3要素分=12バイト分だけアドレスが増えることから、このポインタ(配列名)のサイズが1行当たりの要素数分(列数分)のサイズを持つことがわかります。

  5. [実験3] 行の先頭アドレス
  6.  二次元配列の要素の列を示す添字を省略しアドレス演算子を適用した&a[0]、&a[1]がそれぞれ0行目、1行目の先頭アドレスを示すことを、次のプログラムを入力、コンパイル、実行して確認しなさい。その結果を先のメモリ配置図にわかりやすく追加記入しなさい。
    apointer3.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[2][3] = {{1,2,3},{4,5,6}};
    	int i,j;
    
    	printf("配列名a      -> (%p)\n",a);	//配列の開始アドレス(0行目の先頭アドレス)
    	printf("          a+1 -> (%p)\n",a+1);	//1行目の先頭アドレス
    	
    	for(i=0; i<2; i++){
    		printf("%d行目の先頭アドレス %p :\n", i,&a[i]);
    		for(j=0; j<3; j++){
    			printf("a[%d][%d] = %2d (%p)\n",i,j,a[i][j], &a[i][j]);
    		}
    	}
    
    	return 0;
    }
    


  7. [実験4] ポインタ変数の型と配列へのポインタ
  8.  int型へのポインタpに配列の開始アドレスaを代入し、このポインタを使って要素を参照するプログラムです。しかし、入力(追加・修正)し、コンパイルすると警告(Warning)が発生します。なぜ、警告が出されたのかコンパイラのWarningを参考に考察しなさい。
    apointer4.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[2][3] = {{1,2,3},{4,5,6}};
    	int *p;		//ポインタ
    	int i,j;
    
    	printf("配列名a      -> (%p)\n",a);	//配列の開始アドレス(0行目の先頭アドレス)
    	printf("          a+1 -> (%p)\n",a+1);	//1行目の先頭アドレス
    
    	p = a;		//ポインタに配列の開始アドレスを代入
    	for(i=0; i<2; i++){
    		printf("%d行目の先頭アドレス %p :\n", i,&a[i]);
    		for(j=0; j<3; j++){
    			printf("a[%d][%d] = %2d (%p)\n", i, j, *(p+i*3+j), p+i*3+j);
    		}
    	}
    
    	return 0;
    }
    
     ポインタpは int *型(4バイト)で宣言されています。一方、配列名であるポインタaは行の先頭を示すポインタであり、12バイトのサイズを持つ int (*)[3]型でした。そのため、コンパイラは型が異なるとのWarningを発したわけです。
     では、この場合どのように修正したらよいでしょうか。各要素の参照を *(p+i*3+j)、先頭アドレスから1バイトずつ増加するようにして実現しています。これに合わせて、ポインタpは int *型のままにして、配列aの先頭要素のアドレス&a[0][0]を代入することにします。
    p = a ---修正---> p=&a[0][0]

  9. [実験5] 配列の要素へのポインタ
  10.  ポインタを int (*)[3]型に変更した場合の配列の参照方法について確認しましょう。配列へのポインタpは要素3個分の大きさなので、*(p+1)は1行目の先頭の要素、配列の先頭からは4つ目の要素を示すことになります。では、ポインタpを使って配列の先頭から2つ目の要素を示すためにはどうしたらよいでしょうか?
    apointer5.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[2][3] = {{1,2,3},{4,5,6}};
    	int (*p)[3];		//サイズがint型3個分のポインタ
    	int i,j;
    
    	printf("配列名a      -> (%p)\n",a);	//配列の開始アドレス(0行目の先頭アドレス)
    	printf("          a+1 -> (%p)\n",a+1);	//1行目の先頭アドレス
    
    	p = a;		//ポインタに配列の開始アドレスを代入
    	for(i=0; i<2; i++){
    		printf("%d行目の先頭アドレス %p :\n", i, p+i);
    		for(j=0; j<3; j++){
    			printf("a[%d][%d] = %2d (%p)\n", i, j, *(*(p+i)+j), *(p+i)+j);
    		}
    	}
    
    	return 0;
    }
    
     ポインタpの実体である*pはポインタpが示すアドレスになり、その型はint型(4バイト)になります。

  11. [実験6]ポインタの配列
  12.  ポインタを要素とする配列を使って、2つの配列を扱う方法を確認しましょう。次の例では、2個のint型ポインタからなる配列*p[]を使って、それぞれの要素に、一次元配列aとbを指し示すように初期化しています。こうすることで、ポインタpを使って2つの配列をあたかも2次元配列のように扱うことができます。
    apointer6.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[3] = {11,12,13};
    	int b[3] = {21,22,23};
    	int *p[2] = {a,b};		//int型ポインタ2個からなる配列を初期化
    	int i,j;
    
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			printf("p[%d][%d] = %2d (%p)\n",i,j,p[i][j],&p[i][j]);
    			//printf("p[%d][%d] = %2d (%p)\n",i,j,*(p[i]+j),p[i]+j)	//上記と同じ
    			//printf("p[%d][%d] = %2d (%p)\n",i,j,*(*(p+i)+j),*(p+i)+j); //上記と同じ
    		}
    	}
    
    	return 0;
    }
    

  13. [実験7]ダブルポインタはポインタへのポインタ
  14.  ダブルポインタはポインタ名の前に2つのアスタリスク*を付けて宣言する変数です。ダブルポインタはポインタを指し示す役割を担うことができます。すなわち、ポインタが格納されているアドレスを格納するための変数です。次のプログラムを入力、コンパイル、実行して、ダブルポインタの使い方の確認をしなさい。また、結果を簡単な図を使って示しなさい。
    dpointer.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a;
    	int *p;
    	int **pp;
    
    	a = 1000;
    	p = &a;
    	pp = &p;
    
    	printf(" a = %d (%p)\n", a,&a);
    	printf(" p-> %d (%p)(%p)\n", *p,p,&p);
    	printf("pp-> %d (%p)(%p)\n", **pp,pp,&pp);
    	return 0;
    }
    

まとめ

変数宣言とその意味
int p int型サイズの変数
int *p int型サイズの変数を指し示すポインタ変数
int **p int型サイズの変数を指し示すポインタ変数へのポインタ変数
int p[N] int型サイズの変数をN個持つ配列
int *p[N] int型サイズの変数を指し示すポインタ変数をN個持つ配列
int (*p)[N] int型サイズN個分の要素からなる配列を指し示すポインタ変数
Nは任意の自然数

添字演算子のポインタへの読み替え
 配列の行と列の位置を示すために使われる角括弧を添字演算子と言います。但し、配列の宣言の角括弧とは異なります。この添字演算子は以下の例のようにポインタを含む形式に変換することができます。
a[2] -> *(a+2)
a[2][3] -> (*(a+2))[3] -> *((*(a+2))+3)   -(カッコの省略)--> *(*(a+2) + 3)

array2_3

関数への二次元配列の受け渡し

 関数が引数を使って二次元配列を受け渡しする際の引数の記述方法を見てみましょう。int型の2行3列の二次元配列を例にその記述方法を示します。1)と2)記述方法は異なりますが、いずれも(int *)[3]型で、配列の先頭アドレスと1行のサイズの情報を引き渡すことができます。3)は(int *)型で、配列の先頭アドレスはわかりますが1行のサイズを引き渡すことができません。呼び出し側は引数の型に合わせて、実引数を設定しなければならない点に注意が必要です。

1)2次元配列の開始アドレスを受け取る。列数がわかるので、
void func(int a[2][3])
{
	・・・
}

行数を省略した記述も可能です。1行のサイズが
void func(int a[][3])
{
	・・・
}

3) 2次元配列の開始アドレスを受け取る
void func(int (*a)[3])
{
	・・・
}

4) 2次元配列の先頭要素アドレスを受け取る(但し、行サイズがわからない)
void func(int *a, int row, int col)
{
	・・・
}


演習

  1. 次のプログラムは2行3列の配列の各要素を2倍した値を別の配列に格納した結果を表示するプログラムです。空欄を埋めて完成させなさい。この際、空欄には複数の記述方法が当てはまることを確認しなさい。関数twiceは、2行3列の配列aを受け取って、その各要素を2倍した結果を配列bに格納する関数です。
  2. twice.c
    #include <stdio.h>
    
    void twice([[空欄ア]], [[空欄イ]]);
    void print_array(int a[][3]);
    
    int main(void)
    {
    	int a[2][3]  = {{0,1,2},{3,-1,5}};
    	int b[2][3];
    
    	twice(a,b);
    	print_array(a);
    	print_array(b);
    
    	return 0;
    }
    
    void twice([[空欄ア]], [[空欄イ]])
    {
    	int i,j;
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			b[i][j] = a[i][j] * 2;
    		}
    	}
    }
    
    void print_array(int a[][3])
    {
    	int i,j;
    	
    	puts("\n------------");
    	for(i=0; i<2; i++){
    		for(j=0; j<3; j++){
    			printf("%3d",a[i][j]);
    		}
    		printf("\n");
    	}
    	puts("------------");
    }
    

expand_lessBack to TOP

演習問題

  1. 次の5人の生徒の試験結果を2次元配列で表し、各生徒の合計点を含む結果一覧を表示しなさい。
  2. No.英語数学
    18865
    29286
    37880
    47065
    58878
    実行結果例
    No.| 英語 数学 | 合計
    ---+-----------+-----
     1 |  88   65  | 153
     2 |  92   86  | 178
     3 |  78   80  | 158
     4 |  70   65  | 135
     5 |  88   78  | 166
    

  3. N行N列の単位行列を求めるプログラムを作成しなさい。ただし、N行N列の配列を単位行列に設定する関数void set_identity(int a[][N])を定義して利用しなさい。Nの値はプリプロセッサで定義できるものとする。

  4. 2つの3行3列の正方行列の和を求めるプログラムを作成しなさい。ただし、2つの3行3列の配列aとbの和cを求める関数plus()を定義して利用すること。
  5. \[ c = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \\ \end{bmatrix} + \begin{bmatrix} b_{11} & b_{12} & b_{13} \\ b_{21} & b_{22} & b_{23} \\ b_{31} & b_{32} & b_{33} \\ \end{bmatrix} \]

  6. 次式で示されてるm行n列の行列Aとベクトルxの積Axを求めるプログラムを作成しなさい。行列とベクトルの積を求める関数mv_product()を定義して利用すること。ただし、行列Aは3行3列であるとして作成して構わないものとする。
  7. \[ Ax = \begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{1n} \\ \cdot & \cdot & \ddots & \cdot \\ a_{m1} & a_{m2} & \cdots & a_{mn} \\ \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \\ \end{bmatrix} = \begin{bmatrix} a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n \\ a_{21}x_1 + a_{22}x_2 + \cdots + a_{2n}x_n \\ \vdots \\ a_{m1}x_1 + a_{m2}x_2 + \cdots + a_{mn}x_n \\ \end{bmatrix} \]

  8. 3行3列の配列の要素を右に90度回転させた配置にするプログラムを作成しなさい。ただし、n行n列でも利用できるような方法で実現しなさい。

  9. 下記の実行例のように2次元配列を表示するプログラムを作成しなさい。ただし、配列名(文字)name、二次元配列a、行数row、列数colを引数とする関数void print_array(char name, int *a[][N])を定義して利用すること。
     [a] ---------
      2   0  -1
      1   5   3
     ----------
    


OPEN ANSWER
  1. 演習問題1 回答例 ex4-1.c
  2. code/4/ex4-1.c

  3. 演習問題2 回答例 ex4-2.c
  4. code/4/ex4-3.c

  5. 演習問題3 回答例 ex4-3.c
  6. code/4/ex4-4.c

  7. 演習問題4 回答例 ex4-4.c
  8. code/4/ex4-5.c

  9. 演習問題5 回答例 ex4-5.c
  10. code/4/ex4-6.c

  11. 演習問題6 回答例 ex4-6.c
  12. code/4/ex4-2.c


expand_lessBack to TOP

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

本日の提出課題

 以下の生徒5人分の国語、数学、英語の得点データから各生徒の合計を求めて、合計順に並べ替えた結果一覧を表示するプログラムkadai3-4.cを完成させなさい。得点データは5行5列からなる2次元配列で与えられており、各行が生徒一人分の情報を表し、0列目には学籍番号が、1列目から3列目には各科目の得点が初期値で格納されています。4列目には合計得点が格納されますが、初期値では0点となっており、関数setTotal()を使って設定するものとします。関数swap()は値を交換する関数です。関数sort()は各生徒の合計点の高い順に成績を並べ替える関数です。並べ替えの処理は関数swap()を呼び出すことで実現するものとします。
#include <stdio.h>
#define N 5		//学生数

/* 合計点を求めて設定する */
void setTotal(int m[][5])
{
	//ここを埋めて完成させる		
}

/* 値の交換 */
void swap(int *a, int *b)
{
	//ここを埋めて完成させる	
}

/* 成績一覧の並べ替え(合計点の高い順) */
void sort(int m[][5])
{
	//ここを埋めて完成させる
}

/* 成績一覧の表示 */
void printScore(int m[][5]){
	int i;
	
	printf("  No. | 国語 | 数学 | 英語 | 合計\n");
	printf("------+------+------+------|------\n");
	for(i=0; i<N; i++){
		printf(" %4d | %4d |",m[i][0], m[i][1]);
		printf(" %4d | %4d | %4d\n",m[i][2],m[i][3],m[i][4]);
	}
}

int main(void)
{
	int score[N][5] = { {1001,65,60,65,0},	//得点データ
				{1002,80,86,80,0},
				{1003,100,100,95,0},
				{1004,30,38,30,0},
				{1005,70,76,70,0}};
	setTotal(score);	//合計点を求めて得点データにセットする
	sort(score);		//成績の並べ替え(合計点順)
	printScore(score);	//一覧表示

	return 0;
}



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