Comments
Description
Transcript
行列とポインタ
IT入門B2 ー 行列とポインタ ー 授 業 内 容 • 配列とポインタ • 行列とポインタ • 動的なメモリの確保 • 演習 配列とポインタ n次元ベクトルxをプログラム上で表現する際は,配列を利用. double x[n]; と宣言することで,double型でn個分の記憶領域がメモリ上に 連続して確保される: 0xbfff660 0xbfff668 0xbfff670 … x[0] x[1] x[2] … … x[n-1] … メモリには住所のように番地(アドレス)が振られている. 各要素のアドレスはアドレス演算子(&)を用いて, &x[0], &x[1], &x[2], … , &x[n-1] により参照できる. 配列とポインタ 要素x[i]のアドレスは &x[i] により参照できる. &x[2] &x[0] 0xbfff660 0xbfff668 0xbfff670 … x[0] x[1] x[2] … … x[n-1] … 一方,配列名xは先頭要素へのポインタとして利用できる. ポインタの演算を用いることにより,各要素のアドレスは x+i(i=0,1,2,…,n-1) でも参照できる. x x+2 x+1 x+(n-1) … x[0] x[1] x[2] … x[n-1] … 配列とポインタ ポインタに対して間接参照演算子(*)を適用することにより, ポインタの指すアドレスに格納された値を参照できる. ポインタ x+i に対しては,*(x+i) とすることで各要素の値 を参照できる. アドレスの参照 x … *x x+2 x+1 x[0] x[1] *(x+1) x[2] *(x+2) 値の参照 x+(n-1) … x[n-1] *(x+(n-1)) … 行列とポインタ 行列をプログラム上で表現する場合は,2次元配列を利用する. 例えば,n次の正方行列Aに対しては double a[n][n]; と宣言することで,double型でn×n個分の記憶領域がメモリ 上に確保される: a : a[0][0] a[0][1] a[0][2] … a[0][n-1] a[1][0] a[1][1] a[1][2] … a[1][n-1] a[2][0] a[2][1] a[2][1] … a[2][n-1] … a[n-1][0] a[n-1][1] a[n-1][2] … a[n-1][n-1] 行列とポインタ メモリ … double a[n][n]; a[0] → このときaのn×n個の要素の領域は, 右のようにメモリ上に連続して確保 される. aは, a[0], a[1], … , a[n-1] から構成される. a[1] → 各要素a[i] (i=0,1,…,n-1) は 第i行の先頭要素を指すポインタ a[n-1] → である. a[0][0] a[0][1] … 第0行 a[0][n-1] a[1][0] a[1][1] … 第1行 a[1][n-1] … a[n-1][0] … a[n-1][n-1] … 第 n-1 行 行列とポインタ メモリ double a[n][n]; … 行列aの第i行j列の要素(a[i][j]) を,ポインタと間接参照演算子を 用いて参照する場合は, *(a[i] + j) となる. a[i][j] ⇔ *(a[i] + j) *(a[0]) a[0][0] *(a[0]+1) a[0][1] … *(a[0]+n-1) a[0][n-1] … *(a[i]+0) a[i][0] *(a[i]+1) a[i][1] … *(a[i]+j) a[i][j] … 行列とポインタ メモリ double a[n][n]; に対してポインタ変数 ai を 導入する: … *ai a[0][0] *(ai+1) a[0][1] double *ai; 配列名aは先頭要素へのポイン *(ai+n-1) タとして利用できるから, ai = a; により,aの第0行の各要素が *ai, *(ai+1), …, *(ai+n-1) で参照できる. ポインタ ai +1 +1 … a[0][n-1] a[1][0] a[1][1] … a[1][n-1] a[2][0] … +1 行列とポインタ メモリ 次の行に処理が移るときは, … a[0][0] ai += n; a[0][1] によりポインタaiを更新すれば, aiは常に 「第i行の先頭要素a[i][0]へ のポインタ」 +n … a[0][n-1] として機能し,第i行の各要素が *ai a[1][0] *(ai+1) a[1][1] *ai, *(ai+1), …, *(ai+n-1) で参照できる. ポインタ ai … *(ai+n-1) a[1][n-1] a[2][0] … +1 +1 +n (行列)×(ベクトル)の計算 以前作成した(行列)×(ベクトル)を計算するプログラム を,ポインタの表現を用いて書き直して見よう. double x[n]; double a[n][n]; double *ai; ベクトルxの各要素: *(x), *(x + 1), … , *(x + j), … , *(x + (n-1)) 行列aの各要素: aiが第i行の先頭要素へのポインタであるとして, *(ai + 0),*(ai + 1), … ,*(ai + j), … ,*(ai + (n-1)) 次の行(第i+1行)へ移るときには, ai += n; (行列)×(ベクトル)の計算 2次元配列の表現を用いた プログラム例 ポインタの表現を用いた プログラム例 for(i = 0; i < n; i++){ y[i] = 0.0; for(j = 0; j < n; j++){ y[i] += a[i][j]*x[j]; } } ai = a; for(i = 0; i < n; i++){ *(y+i) = 0.0; for(j = 0; j < n; j++){ *(y+i) += *(ai+j) * *(x+j); } ai += n; } 演 習 第2回の授業で作成した行列 A とベクトル x の積( A⋅ x )を計 算するプログラムを,ポインタの表現を用いたプログラムに 書き直し,以下の場合について積を計算してみよう: ⎛2 4 6 ⎞ ⎛ − 33 ⎞ A = ⎜⎜ 3 8 7 ⎟⎟, x = ⎜⎜ 9 ⎟⎟ ⎜ 5 7 21⎟ ⎜ 6 ⎟ ⎝ ⎠ ⎝ ⎠ (行列)×(ベクトル)の計算 これまでのプログラムでは, double a[3][3], x[3]; のように配列の大きさを宣言 してから利用. • 次元nに依存している. • 次元nや行列A, ベクトルxの 内容が変わるたびにプログ ラムの修正,コンパイル, 実行,を行わなければなら ない. #include <stdio.h> int main(void) { int i, j; double x[3] = {-33.0, 9.0, 6.0}; double y[3]; double a[3][3] = {{2.0, 4.0, 6.0}, {3.0, 8.0, 7.0}, {5.0, 7.0, 21.0}}; … } 次元nや行列A, ベクトルxの内容に依存しないプログラムに変 更してみよう. (行列)×(ベクトル)の計算 行列Aやベクトルxの内容変更への対応 ここでは,scanf()などの関数を利用して,実行時に行列,ベ クトルの各要素をキーボードから入力することにする. 次元nの変更への対応 動的なメモリ確保を行う関数malloc()を利用する. プログラムの実行中に行列やベクトルの大きさに応じたメモ リ領域を確保することができる. 動的なメモリ確保 malloc(size) : • 引数sizeで指定されたサイズのメモリ領域を確保して, その先頭へのポインタを返す関数. • メモリの確保に失敗した場合は,NULLを返す. • malloc()により確保したメモリ領域は,使い終わったら 関数free()により開放する必要がある. • malloc()を利用するにはヘッダファイル stdlib.h を インクルードする必要がある. 動的なメモリ確保 基本的な使い方: double *x; x = (double *)malloc(n*sizeof(double)); メモリ … x double型 double型 double型のサイズのn個分の領域を確保し, その領域の先頭へのポインタをxに代入. double型 n個 … sizeof演算子 いろいろな型のサイズ(バイト数)を求める ことができる. sizeof(double) → 8 (バイト) sizeof(int) → 4 (バイト) sizeof(char) → 1 (バイト) double型 double型 double型 … 動的なメモリ確保 基本的な使い方: x = (double *)malloc(n*sizeof(double)); malloc()により得られたデータ型をdoubleへのポインタ型に変換する. キャスト演算子 (型名)式 : 式で与えられているデータ型を型名へ変換する. 例 int a, b; double c, d; a b c d = = = = 1.0; 3.0; a / b; → (double)a / b; → 0.000000 0.333333 動的なメモリ確保 基本的な使い方: x = (double *)malloc(n*sizeof(double)); • メモリの確保に失敗した場合は,以降の処理を中止する必要がある. • 確保した領域は,使い終わった段階で開放する必要がある. x = (double *)malloc(n*sizeof(double)); if (x == NULL) { printf(Cant allocate memory.¥n); exit(1); } /* 必要な処理 free(x); */ 動的なメモリ確保 メモリ … double *x; x = (double *)malloc(n*sizeof(double)); x double型 double型 double型 ポインタxがdouble型の要素をn個持つような 配列名と同じように扱うことができる. n個 … double型 x + i double型 → i番目の要素へのポインタ *(x + i) → i番目の要素 scanf("%lf", x + i); double型 … → i番目の要素をキーボードから入力 printf("%.2f¥n", *(x + i)); → i番目の要素を出力 動的なメモリ確保 メモリ 行列の場合 double *a, *ai; … a a = (double *)malloc(n*n*sizeof(double)); if (a==NULL) { printf("Can't allocate memory.¥n"); exit(1); } ポインタaがdouble型の要素をn×n個持つような 配列名と同じように扱うことができる. double double double n×n個 … double double double ポインタ変数aiを導入し, … ai = a; とすると,ポインタの表現による行列の場合と同様に第0行の各要素が *(ai + j)で参照でき,次の行へ処理が移るときに ai += n; とすれば,aiが常に行列の第i行の先頭へのポインタとして機能する. 動的なメモリ確保 行列の場合のプログラムの流れ double *a, *ai; a = (double *)malloc(n*n*sizeof(double)); if (a==NULL) { printf("Can't allocate memory.¥n"); exit(1); } ai = a; for (i=0; i<n; i++) { for(j=0; j<n; j++) { printf("a[%d][%d] = ", i, j); scanf("%lf", ai + j); } ai += n; } 行列aの各要素にキーボードから データを入力 演 習 キーボードから次元nを入力した後,n×n行列Aとn次元ベクトルxのデータ を要素ごとにキーボードから入力し,A⋅ x を計算するプログラムを完成さ せよう. $ ./a.out n = 3 a[0][0] = 2.0 a[0][1] = 4.0 a[0][2] = 6.0 a[1][0] = 3.0 … x[2] = 6.0 A*x = 6.0 15.0 24.0 まとめ 以上から,プログラムの実行時にメモリを動的に確保することで,行列や ベクトルの次元に依存しないプログラムを作成することができた. 実際には,行列やベクトルのデータの入出力を行う部分について,行列, ベクトルのデータを記述したファイルを用意しておき, ・ このファイルからデータを読み込む ・ 計算結果を別のファイルに書き込む といったファイル入出力の関数を利用することにより,より実用的なプロ グラムが作成できる.