プログラミング言語Ⅰ(実習を含む。), 計算機言語Ⅰ・計算機言語演習Ⅰ, 情報処理言語Ⅰ(実習を含む。) C言語入門 第12週 プログラミング言語Ⅰ(実習を含む。), 計算機言語Ⅰ・計算機言語演習Ⅰ, 情報処理言語Ⅰ(実習を含む。)
動的配列
動的配列の基本 malloc で確保し free で解放する 講義資料 第6週p.59, 第9週pp.30-34. unsigned char *img; int w, h; // 動的にサイズを決める fprintf(stderr, "w = ?"); scanf("%d", &w); fprintf(stderr, "h = ?"); scanf("%d", &h); if ((img = malloc(3 * w * h)) == NULL) { // img の動的確保 fprintf(stderr, "Error: in %s line %d: malloc failed\n", __FILE__, __LINE__); exit(EXIT_FAILURE); } // ここで img に対する処理を行う free(img); // 使い終わったimgの解放
動的配列の例 1週目の bmptest.c と同じグラデーション 講義資料 第6週p.59, 第9週pp.30-34. dynamic_array_test.c 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void print_ppm(unsigned char *img, int w, int h, int q); int main() { unsigned char *img; int w, h, x, y; fprintf(stderr, "w = ? "); scanf("%d", &w); fprintf(stderr, "h = ? "); scanf("%d", &h); if ((img = malloc(3 * w * h)) == NULL) { fprintf(stderr, "Error: in %s line %d: malloc failed\n", __FILE__, __LINE__); exit(EXIT_FAILURE); } for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { img[3 * (w * y + x) + 0] = 255; // R img[3 * (w * y + x) + 1] = 255 * x / w; // G img[3 * (w * y + x) + 2] = 255 * y / h; // B print_ppm(img, w, h, UCHAR_MAX); free(img); return EXIT_SUCCESS; malloc で確保したメモリは 普通に1次元配列のように使える a.bmp mintty + bash + GNU C $ gcc dynamic_array_test.c print_ppm.c && ./a | convert - a.bmp w = ? 255 h = ? 255
動的配列によるカラー画像 1次元配列を3次元配列のように使う 講義資料 第6週pp.10-30,57-59. dynamic_array_test.c unsigned char *img; を unsigned char img[h][w][3]; と同じように使う 18 19 20 21 22 23 24 for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { img[3 * (w * y + x) + 0] = 255; // R img[3 * (w * y + x) + 1] = 255 * x / w; // G img[3 * (w * y + x) + 2] = 255 * y / h; // B } img[3 * (w * y + x) + 0] x img R G B ... : ⋱ y h w*3
テキスト形式 PPM 画像の書き出し 第6週p.53.のプログラムを関数化した例 講義資料 第6週pp.47-53. print_ppm.c 関数のプロトタイプ宣言 #include <stdio.h> void print_ppm(unsigned char *img, int w, int h, int q) { int c, x, y; printf("P3\n"); printf("%d %d\n", w, h); printf("%d\n", q); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { printf(" "); for (c = 0; c < 3; c++) { printf(" %3d", img[3 * (w * y + x) +c]); } printf("\n"); // 作った関数の本体(並括弧の中身)を取り除き // 末尾に ; (セミコロン)を付ける void print_ppm(unsigned char *img, int w, int h, int q); x c R G B ... : ⋱ y
テキスト形式 PPM 画像の読み込み 読み込んだパラメータに応じて大きさを変更 講義資料 第6週pp.47-53. read_ppm_p3_core.c 実用上は、これ+エラー処理が必要 詳細は read_ppm_p3.c 参照 unsigned char *read_pnm_p3(int *w, int *h, int *q) { char s[10]; unsigned char *img; int y, x, c; scanf("%9s%d%d%d", s, w, h, q); if ((img = calloc(*h * *w * 3, 1)) == NULL) { return NULL; } for (y = 0; y < *h; y++) { for (x = 0; x < *w; x++) { for (c = 0; c < 3; c++) { scanf("%d", &img[3 * (*w * y + x) + c]); return img; パラメータの読み込み パラメータに応じて 動的配列の確保 配列に格納する値の 読み込み 確保し値を読み込んだ 動的配列を返す
添え字0~N-1の範囲を自由変更出来ないか? ポインタの応用: 配列の添え字調整 C言語における配列の宣言 型名 変数名[要素数]; 例: 要素数N個でint型の配列a int a[N]; 利用可能な要素は a[0]~a[N-1] 添え字0~N-1の範囲を自由変更出来ないか? ↓ ポインタを利用すれば可能
ポインタの応用: 配列の添え字調整 1元配列とメモリ上の配置 ⋮ a[-1] a[ 0] a[ 1] a[ 2] a[ 3] ⋮ p[-1] ... 確保されたメモリ 確保されたメモリ外 p[0] が a のどこに対応するか 調整してやれば良い = = int a[3]; int *p; として p = &a[0] とした場合 p = &a[1] とした場合 a[ 0]~a[ 2] を p[-1]~p[ 1] として使える
ポインタの応用: 配列の添え字調整 オフセットを与えてポインタを格納する array_offset_test1.c p は a[1] のアドレスを指す つまり p[0] が a[1] 結果、以下の要素が対応する a[ 0], a[ 1], a[ 2] p[-1], p[ 0], p[ 1] int a[3] = {2,3,5}; int *p = a + 1; int i; for (i = 0; i < 3; i++) { printf("%p: a[% d] = %d\n", &a[i], i, a[i]); } for (i = -1; i <= 1; i++) { printf("%p: p[% d] = %d\n", &p[i], i, p[i]); mintty + bash + GNU C $ gcc array_offset_test1.c && ./a 0x22aab0: a[ 0] = 2 0x22aab4: a[ 1] = 3 0x22aab8: a[ 2] = 5 0x22aab0: p[-1] = 2 0x22aab4: p[ 0] = 3 0x22aab8: p[ 1] = 5
ポインタの応用: 配列の添え字調整 2次元配列の場合 多次元配列も 結局は1次元の メモリアドレスに 割り振られる 2次元配列とメモリ上の配置 ⋮ a[ 0][ 0] a[ 0][ 1] a[ 0][ 2] a[ 1][ 0] a[ 1][ 1] a[ 1][ 2] a[ 2][ 0] a[ 2][ 1] a[ 2][ 2] ⋮ a[ 0][ 0] a[ 0][ 1] a[ 0][ 2] a[ 0][ 3] a[ 0][ 4] a[ 0][ 5] a[ 0][ 6] a[ 0][ 7] a[ 8][ 8] ⋮ a[ 1][-3] a[ 1][-2] a[ 1][-1] a[ 1][ 0] a[ 1][ 1] a[ 1][ 2] a[ 1][ 3] a[ 1][ 4] a[ 1][ 5] ⋮ a[ 2][-6] a[ 2][-5] a[ 2][-4] a[ 2][-3] a[ 2][-2] a[ 2][-1] a[ 2][ 0] a[ 2][ 1] a[ 2][ 2] ... 確保されたメモリ 添え字の範囲外だが 確保されたメモリ内 確保されたメモリ外 = 添え字の範囲外でも メモリ上で連続であれば アクセスは可能
ポインタの応用: 配列の添え字調整 2次元配列の場合 2次元配列とポインタのオフセット ⋮ a[ 0][ 0] a[ 0][ 1] a[ 0][ 2] a[ 1][ 0] a[ 1][ 1] a[ 1][ 2] a[ 2][ 0] a[ 2][ 1] a[ 2][ 2] ⋮ (*p)[-1][-1] (*p)[-1][ 0] (*p)[-1][ 1] (*p)[ 0][-1] (*p)[ 0][ 0] (*p)[ 0][ 1] (*p)[ 1][-1] (*p)[ 1][ 0] (*p)[ 1][ 1] int [3][3] へのポインタ p を作り p が a[ 1][ 1] を指すよう調整すれば a[ 0][ 0]~ a[ 2][ 2] を (*p)[-1][-1]~(*p)[ 1][ 1] として使える! = int a[3][3]; int (*p)[3][3]=(int (*)[3][3])&a[1][1];
ポインタの応用: 配列の添え字調整 2次元配列の場合 2次元配列とポインタのオフセット p = a とした時の (*p)[-1][-1]~(*p)[1][1]の範囲 ⋱ ⋮ ⋰ ... a[-1][-1] a[-1][ 0] a[-1][ 1] a[-1][ 2] a[-1][ 3] a[ 0][-1] a[ 0][ 0] a[ 0][ 1] a[ 0][ 2] a[ 0][ 3] a[ 1][-1] a[ 1][ 0] a[ 1][ 1] a[ 1][ 2] a[ 1][ 3] a[ 2][-1] a[ 2][ 0] a[ 2][ 1] a[ 2][ 2] a[ 2][ 3] a[ 3][-2] a[ 3][ 0] a[ 3][ 1] a[ 3][ 2] a[ 3][ 3] ... 確保されたメモリ 添え字の範囲外だが確保されたメモリ内 確保されたメモリ外 p = &a[1][1] とした時の (*p)[-1][-1]~(*p)[1][1]の範囲
ポインタの応用: 配列の添え字調整 2次元配列の場合 オフセットを与えてポインタを格納する array_offset_test2_1.c a[1][1]は int[3][3]型ではなく int型なのでキャストが必要 int a[3][3] = { { 2, 3, 5}, { 7,11,13}, {17,19,23}, }; int (*p)[3][3] = (int (*)[3][3]) &a[1][1]; int x, y; printf("sizeof(a) = %d\n", sizeof(a)); printf("sizeof(*p) = %d\n", sizeof(*p)); for (y = 0; y < 3; y++) { for (x = 0; x < 3; x++) { printf("%p: a[% d][% d] = %d\n", &a[y][x], y, x, a[y][x]); } for (y = -1; y <= 1; y++) { for (x = -1; x <= 1; x++) { printf("%p: (*p)[% d][% d] = %d\n", &(*p)[y][x], y, x, (*p)[y][x]); int [3][3] へのポインタは int (*)[3][3]
ポインタの応用: 配列の添え字調整 2次元配列の場合 オフセットを与えてポインタを格納する array_offset_test2_2.c int a[3][3] = { { 2, 3, 5}, { 7,11,13}, {17,19,23}, }; int (*p)[3] = (int (*)[3]) &a[1][1]; int x, y; printf("sizeof(a) = %d\n", sizeof(a)); printf("sizeof(*p) = %d\n", sizeof(*p)); for (y = 0; y < 3; y++) { for (x = 0; x < 3; x++) { printf("%p: a[% d][% d] = %d\n", &a[y][x], y, x, a[y][x]); } for (y = -1; y <= 1; y++) { for (x = -1; x <= 1; x++) { printf("%p: p[% d][% d] = %d\n", &p[y][x], y, x, p[y][x]); int [3][3] へのポインタは int (*)[3][3] だが int (*)[3] でも格納可能 使う時は *p としなくて良いので 後者の方が使い易い? 1次元配列でも int [3] への ポインタではなく int 型への ポインタを 使っていた
ポインタの応用: 配列の添え字調整 2次元配列の場合 mintty + bash + GNU C mintty + bash + GNU C $ gcc array_offset_test2_1.c && ./a sizeof(a) = 36 sizeof(*p) = 36 0x22aa90: a[ 0][ 0] = 2 0x22aa94: a[ 0][ 1] = 3 0x22aa98: a[ 0][ 2] = 5 0x22aa9c: a[ 1][ 0] = 7 0x22aaa0: a[ 1][ 1] = 11 0x22aaa4: a[ 1][ 2] = 13 0x22aaa8: a[ 2][ 0] = 17 0x22aaac: a[ 2][ 1] = 19 0x22aab0: a[ 2][ 2] = 23 0x22aa90: (*p)[-1][-1] = 2 0x22aa94: (*p)[-1][ 0] = 3 0x22aa98: (*p)[-1][ 1] = 5 0x22aa9c: (*p)[ 0][-1] = 7 0x22aaa0: (*p)[ 0][ 0] = 11 0x22aaa4: (*p)[ 0][ 1] = 13 0x22aaa8: (*p)[ 1][-1] = 17 0x22aaac: (*p)[ 1][ 0] = 19 0x22aab0: (*p)[ 1][ 1] = 23 $ gcc array_offset_test2_2.c && ./a sizeof(a) = 36 sizeof(*p) = 12 0x22aa90: a[ 0][ 0] = 2 0x22aa94: a[ 0][ 1] = 3 0x22aa98: a[ 0][ 2] = 5 0x22aa9c: a[ 1][ 0] = 7 0x22aaa0: a[ 1][ 1] = 11 0x22aaa4: a[ 1][ 2] = 13 0x22aaa8: a[ 2][ 0] = 17 0x22aaac: a[ 2][ 1] = 19 0x22aab0: a[ 2][ 2] = 23 0x22aa90: p[-1][-1] = 2 0x22aa94: p[-1][ 0] = 3 0x22aa98: p[-1][ 1] = 5 0x22aa9c: p[ 0][-1] = 7 0x22aaa0: p[ 0][ 0] = 11 0x22aaa4: p[ 0][ 1] = 13 0x22aaa8: p[ 1][-1] = 17 0x22aaac: p[ 1][ 0] = 19 0x22aab0: p[ 1][ 1] = 23 sizeof(*p)は異なる int (*p)[3][3]; だと sizeof(*p) は sizeof(int[3][3]) int (*p)[3]; だと sizeof(int[3])
ポインタの応用: 配列の添え字調整 2次元配列の場合 配列のサイズが必要になるのが欠点 array_offset_test2_2.c int a[3][3] = { { 2, 3, 5}, { 7,11,13}, {17,19,23}, }; int (*p)[3] = (int (*)[3]) &a[1][1]; int x, y; printf("sizeof(a) = %d\n", sizeof(a)); printf("sizeof(*p) = %d\n", sizeof(*p)); for (y = 0; y < 3; y++) { for (x = 0; x < 3; x++) { printf("%p: a[% d][% d] = %d\n", &a[y][x], y, x, a[y][x]); } for (y = -1; y <= 1; y++) { for (x = -1; x <= 1; x++) { printf("%p: p[% d][% d] = %d\n", &p[y][x], y, x, p[y][x]); 最後の次元以外は要素数が既知でないと 配列へのポインタを作れない つまり動的配列に対して使えない
ポインタの応用: 配列の添え字調整 2次元配列の場合 動的配列の場合は自力でアドレスを計算する array_offset_test3.c int a[3][3] = { { 2, 3, 5}, { 7,11,13}, {17,19,23}, }; int *p = &a[1][1], w = 3, h = 3; int x, y; for (y = 0; y < 3; y++) { for (x = 0; x < 3; x++) { printf("%p: a[% d][% d] = %d\n", &a[y][x], y, x, a[y][x]); } for (y = -1; y <= 1; y++) { for (x = -1; x <= 1; x++) { printf("%p: p[w * % d + % d] = %d\n", &p[w * y + x], y, x, p[w * y + x]); ポインタを1次元配列として用いる 但しアドレスを計算し易いオフセットを あらかじめ設定しておく アドレスは自力で計算
確認問題 以下の状況で9行目の時点の *p と p の値を16進数で答えよ。 第9週講義資料p.16. hoge.c 7 8 9 int a = 0x12345678; int *p = &a; printf("&a = %p\n", &a); mintty + bash + GNU C $ gcc hoge.c && ./a &a = 0x22aac4
確認問題 int 型へのポインタ変数 a, b を宣言する場合正しいのは以下のうちどれか? 第9週講義資料p.17. hoge.c
演習: strtosign.c 文字列sの先頭1文字を見て、「-」なら-1、「+」なら+1、それ以外なら+1を返す関数 strtosign を作成せよ int strtosign(const char *s, char **endp); 引数 s: 文字列 endp: NULL 以外の時、*endp に符号の識別子(つまり'-','+')の次の文字へのポインタを返す 戻り値 文字列sの先頭2文字に応じて、2、8、10、16の何れかを返す。 endp が NULL 以外の時、*endp に符号の識別子(つまり'-','+')の次の文字へのポインタを返す strtosign_test.c と共にコンパイルして動作を確認せよ 第13週へ移動+改訂
演習: strtobase.c 文字列sの先頭2文字を見て、0なら8進数、0bなら2進数、0xなら16進数、それ以外なら10進数と判別する関数 strtobase を作成せよ int strtobase(const char *s, char **endp); 引数 s: 文字列 endp: NULL 以外の時、*endp に基数判別の識別子(0, 0b, 0x)の次の文字へのポインタを返す 戻り値 文字列sの先頭2文字に応じて、2、8、10、16の何れかを返す。 endp が NULL 以外の時、*endp に基数判別の識別子(0, 0b, 0x)の次の文字へのポインタを返す strtobase_test.c と共にコンパイルして動作を確認せよ 第13週へ移動+改訂
参考文献 [1] B.W.カーニハン/D.M.リッチー著 石田晴久 訳、プログラミング言語C 第2版 ANSI 規格準拠、共立出版(1989)