C プログラミング - ポインタなんて恐くない! - 藤田悟 fujita_s@hosei.ac.jp
目標 C 言語プログラムとメモリ ポインタの関係を深く理解する C 言語プログラムは メモリを素のまま利用できます これが原因のエラーが多く発生します メモリマップをよく頭にいれて ポインタの動きを理解できれば C 言語もこわくありません
1. ポインタ入門編
ディレクトリの作成と移動 mkdir chapter1 cd chapter1
前提知識 : printf C 言語のコンソール出力の基本形 printf( 書式, 変数 1, 変数 2, ); 書式には 表示したい文字列と 書式文字列 (%d など ) を書く 書式制御文字列と同じ数だけの変数を後ろに続ける %d : int の表示 %ld : long の表示 %f : float, double の表示 %x : 整数の 16 進数表示 %lx: long 整数の 16 進数表示 %s : 文字列表示 %c : 文字表示 n : 改行
prog0.c #include <stdio.h> // 入出力を使うときに宣言 // argc は引数の数 argv は引数の文字列の配列 int main(int argc, char *argv[]) { int x = 10; printf( %d n, x); コンパイル : gcc prog0.c o prog0 実行 :./prog0
変数の大きさ 変数のアドレス 変数を格納するためには メモリが必要 メモリには 1 バイト単位に アドレス ( 番地 ) がある int x; という変数に対して &x と指定すると 変数 x のアドレス ( ポインタアドレス ) がわかる アドレスを画面に表示するには printf( %lx n, &x); // %lx は long の 16 進数表示 変数を格納するためには メモリが何バイト必要か 変数が何バイトか知るには sizeof x を用いる printf( %d n, sizeof x);
prog1.c int の変数 x, y の値と アドレスと 大きさをまとめて表示する #include <stdio.h> int main(int argc, char *argv[]) { int x = 112; int y = 115; // sizeof x は sizeof(x) のように書いても良い printf("%d %lx %d n", x, &x, sizeof x); printf("%d %lx %d n", y, &y, sizeof y);
演習 1: メモリマップを完成させよう! prog1.c を実行して x や y が 何番地のメモリに格納されていたかを確認して メモリマップを作成せよ 0 7fff3c4f92a8 7fff3c4f92a9 7fff3c4f92aa 7fff3c4f92ab x と y は どちらが小さいアドレスにある? x と y のサイズとメモリは一致しているか? 7fff3c4f92ac 7fff3c4f92ad 7fff3c4f92ae 7fff3c4f92af 7fffffffffff PC が 4G バイトのメモリを持っているとすると 4G = 0x100000000 (16 進数で 0 が 8 個 ) 表示は 7fff から始まっているけれど 本当はそんなに大きなメモリはない
4 バイト境界のメモリマップ 1 バイトずつだと 縦に長くなりすぎるので 4 バイトずつ横に並べて メモリマップを書く 0 7fff3c4f92a8 7fff3c4f92ac 00 00 00 00 00 00 00 変数 y 00 変数 x なぜか 上下が逆!! 7ffffffffffc
演習 2: prog2.c int x; に加えて long y; float f; double d; char c; を宣言して 値 メモリアドレス サイズを表示して メモリマップを作成せよ char c の値は int で表示してよい (%d を使う ) x, y, f, d, c は どの順番に格納されるのか ゴミの値が入っていないか? ゴミとは 初期値が 0 でない変な値
prog2.c #include <stdio.h> 変ですねぇ int main(int argc, char **argv) { int x; long y; float f; double d; char c; 7fff3c4f9298 printf("%d %lx %d n", x, &x, sizeof(x)); 7fff3c4f929c printf("%ld %lx %d n", y, &y, sizeof(y)); printf("%f %lx %d n", f, &f, sizeof(f)); 7fff3c4f92a0 printf("%f %lx %d n", d, &d, sizeof(d)); 7fff3c4f92a4 printf("%d %lx %d n", c, &c, sizeof(c)); 7fff3c4f92a8 7fff3c4f92ac 0 7fff3c4f928c 7fff3c4f9290 7fff3c4f9294 c d f y x 7ffffffffffc
配列のメモリを見る 配列の宣言は int array[4]; のように行う 配列要素のメモリアドレスは &array[0], &array[1] のようにして 得られる 配列の先頭アドレスは &array でわかる 配列の全体のサイズは sizeof array 配列の一要素のサイズは sizeof array[0] &array のサイズは sizeof &array
prog3.c #include <stdio.h> int main(int argc, char **argv) { int x; int array[4]; printf("%d %lx %d n", x, &x, sizeof(x)); printf("%d %lx %d n", array[0], &array[0], sizeof array[0]); printf("%d %lx %d n", array[1], &array[1], sizeof array[1]); printf("%d %lx %d n", array[2], &array[2], sizeof array[2]); printf("%d %lx %d n", array[3], &array[3], sizeof array[3]); // & の位置や sizeof の位置に注意!! 書式の %lx にも注意!! printf("%lx %lx %d %d n", array, &array, sizeof array, sizeof &array); 演習 3: メモリマップを描け
ポインタ変数 アドレスを格納するための変数をポインタ変数と呼ぶ int *xp; // 整数へのポインタを格納する変数 ポインタ変数へのアドレスの代入には 元の型の変数のアドレスを代入する int *xp; int x; xp = &x; // x のアドレスを xp に代入 演習 4: ポインタ変数の sizeof と 上記のプログラムで格納されたポインタ変数の値を確認し メモリマップを作成せよ
演習 4 prog4.c #include <stdio.h> int main(int argc, char **argv) { int x; int *xp; xp = &x; x = 3; printf("x = %d %lx n", x, &x); printf( xp = %lx %lx n, xp, &xp); // &xp は変数 xp のアドレス
ポインタ変数を使った値の書き換え int x = 3; int *xp; xp = &x; という状況で xp は x の変数のアドレスが入っている ここで *xp = 5; と実行すると xp が指し示すアドレスの先のデータが 5 に書き換わる すなわち x の値が書き換わる
prog5.c #include <stdio.h> int main(int argc, char *argv[]) { int x = 3; int *xp; xp = &x; printf("x = %d %lx n", x, &x); printf( xp = %lx %lx n", xp, &xp); *xp = 5; printf("x = %d %lx n", x, &x); printf( xp = %lx %lx n", xp, &xp); 演習 5: メモリマップを作成せよ
配列要素のアドレスをポインタ変数に格納 配列要素のアドレスは &array[0] のようにして得られる ポインタ変数の宣言は int *xp; したがって xp = &array[0]; のように書くと 配列の第 0 要素のアドレスが xp に代入される *xp = 12; のように書いて 配列の中身をすべて 12 に書き換え メモリマップで確認しよう
演習 6: prog6.c #include <stdio.h> int main(int argc, char *argv[]) { int array[4]; int *xp; printf("%lx %lx n", xp, &xp); int i; for(i = 0; i < 4; i++) { xp = &array[i]; printf("before: %d %lx %d n", array[i], &array[i], sizeof array[i]); *xp = 12; printf("after: %d %lx %d n", array[i], &array[i], sizeof array[i]);
ポインタ変数のインクリメント ポインタ変数に 1 を足すと ポインタの指し示す 型 の大きさ分だけアドレスが足される int *xp; の時 xp++ はアドレスに 4 を足す long *xp; の時 xp++ はアドレスに 8 を足す ポインタ変数を加算しながら 指し示すアドレスの内容を書き換えてみよう *xp = 12; xp++; まとめて書くと *xp++ = 12; と書くことができる
演習 7: prog7.c #include <stdio.h> int main(int argc, char *argv[]) { int array[4]; int *xp; printf("%lx %lx n", xp, &xp); int i; xp = &array[0]; for(i = 0; i < 4; i++) { printf("before: %d %lx n", array[i], xp); *xp = 12; xp++; printf("after: %d %lx n", array[i], xp);
文字列は char の配列 C 言語には Java のような String 型はなく 文字 (char: 8bits=1byte) の配列として扱う 文字列の長さもわからないので char の値に 0 が来たら 文字列の終了とみなす char univ[] = hosei ; univ h o s e i 0 0 と書いたのは 文字の 0 と区別するため 文字の 0 は整数の 0x20 であるが 0 は整数の 0
演習 8: prog8.c #include <stdio.h> int main(int argc, char *argv[]) { char univ[] = "hosei"; int i = 0; char *cp; cp = univ; printf("%c %d %x %lx %lx n", *cp, *cp, *cp, cp, &cp); for(i = 0; i < 6; i++) { printf("%c %d %x %lx n", *cp, *cp, *cp, cp); cp++;
整数はどのように格納される? int *xp の値や x の値は アドレスにどのような順番で格納されているのだろうか これを確認するためには 1 バイトずつデータを見なければならない 1 バイトのデータは char 型です 特に 符号なしの 1 バイトとしたい場合 unsigned char と書きます
演習 9: prog9.c #include <stdio.h> int main(int argc, char *argv[]) { int x = 0x12345678; long lx = 0x123456789abcdef0; unsigned char *cp; printf("%lx n", &cp); int i = 0; cp = (unsigned char *)&x; for(i = 0; i < 4; i++) { printf("%x %lx n", *cp, cp); cp++; cp = (unsigned char *)&lx; for(i = 0; i < 8; i++) { printf("%x %lx n", *cp, cp); cp++;
演習 10: prog10.c 下記のプログラムを実行して 何が起こっているかを メモリマップに示して説明せよ #include <stdio.h> int main(int argc, char *argv[]) { long lx = 0x6965736f68; char *cp; cp = (char *)&lx; printf("%s n", cp);
関数呼び出し 関数呼び出しをしたとき 変数はどのようにメモリにマッピングされているか 特に 再帰呼び出しした時にメモリマップはどうなるのか 関数から戻ってきたときに 関数内で使っていたローカル変数はどうなるのか 別の関数が呼び出された時のローカル変数領域になり 上書きされる ローカル変数領域へのポインタを 呼び出し元の関数に引き渡すと 大きなエラーになる
演習 11: prog11.c #include <stdio.h> int fact(int x) { int result; printf("x = %d %lx n", x, &x); printf("result = %d %lx n", result, &result); if(x == 0) { result = 1; else { result = x * fact(x - 1); return result; int main(int argc, char **argv) { int x; x = 2; printf("x = %d %lx n", x, &x); int y = fact(x); printf("y = %d %lx n", y, &y); printf("%d n", y);
グローバル変数 malloc() 関数内のローカル変数ではなく 関数の外に定義するグローバル変数 malloc() で動的に確保されるメモリは どこに確保されるのであろうか
演習 12: prog12.c #include <stdio.h> #include <stdlib.h> // malloc を使うときに必要 int ex; int main(int argc, char **argv) { int x; char *cp = "hosei"; char cparray[] = "hosei"; char *mp = (char *)malloc(8); printf("x = %d %lx n", x, &x); printf("ex = %d %lx n", ex, &ex); printf("cp = %lx %lx n", cp, &cp); printf("cparray = %lx %lx n", cparray, &cparray); printf("mp = %lx %lx n", mp, &mp);
ここまででのまとめ
C 言語はメモリが命 ポインタはメモリのアドレス ( 番地 ) である int は 4 バイト long は 8 バイトで 変数宣言すると 適当に隙間も作りながらメモリが割り当てられる ポインタ変数は 64bitsOS の時 64bits(8 バイト ) ポインタ変数を +1 すると 変数の型に合わせて 数バイトずつアドレスが増加する ローカル変数は 関数呼び出しごとにメモリが割り当てられる ローカル変数で配列を確保すると大変なことに
2. ポインタ応用編
ディレクトリを変更します cd.. mkdir chapter2 cd chapter2
2 次元配列 unsigned char array[3][3]; と宣言したら どのようなメモリが割り当てられるのか array の値と sizeof は? array[0] の値と sizeof は? array[0][0] の値と sizeof は? &array の値はと sizeof は? &array[0] の値と sizeof は? &array[0][0] の値と sizeof は? &array[0][1] の値は? &array[1][0] の値は? &array[1] の値は?
演習 1: prog1.c #include <stdio.h> int main(int argc, char *argv[]) { unsigned char array[3][3]; int i; int j; for(i = 0; i < 3; i++) { for(j = 0; j < 3; j++) { printf("%d %lx n", array[i][j], &array[i][j]); printf("array = %lx %d n", array, sizeof array); printf("array[0] = %lx %d n", array[0], sizeof array[0]); printf("array[0][0] = %lx %d n", array[0][0], sizeof array[0][0]); printf("&array = %lx %d n", &array, sizeof &array); printf("&array[0] = %lx %d n", &array[0], sizeof &array[0]); printf("&array[0][0] = %lx %d n", &array[0][0], sizeof &array[0][0]); printf("&array[0][1] = %lx %d n", &array[0][1], sizeof &array[0][1]); printf("&array[1][0] = %lx %d n", &array[1][0], sizeof &array[1][0]); printf("&array[1] = %lx %d n", &array[1], sizeof &array[1]);
演習 2: prog2.c #include <stdio.h> int main(int argc, char *argv[]) { unsigned char array[3][3]; unsigned char *cp; // char 配列をアクセスし 1 バイトごとに増えるポインタ編巣 unsigned char (*cpp)[3]; // char 配列 3 個分ずつ増えるポインタ変数 unsigned char (*cppp)[3][3]; // char 配列 3*3 個分ずつ増えるポインタ変数 cp = array[0]; printf("cp = %lx n", cp); cp++; printf("cp = %lx n", cp); cpp = array; printf("cpp = %lx n", cpp); cpp++; printf("cpp = %lx n", cpp); cppp = &array; printf("cppp = %lx n", cppp); cppp++; printf("cppp = %lx n", cppp);
配列とポインタ prog3.c で何が起きているのか 演習 3: プログラムを実行し メモリマップを作成した上で 動作を説明せよ
prog3.c #include <stdio.h> int main(int argc, char *argv[]) { unsigned char array[3][3]; unsigned char *cp; unsigned char (*cpp)[3]; unsigned char (*cppp)[3][3]; cp = array[0]; cp++; *cp = 'h'; cp[1] = 'o'; cpp = array; cpp++; *cpp[0] = 's'; cpp[0][2] = 'e'; cppp = &array; (*cppp)[2][2] = 'i'; int i, j; for(i = 0; i < 3; i++) { for(j = 0; j < 3; j++) { printf("%c %lx n", array[i][j], &array[i][j]);
演習 4 30 バイトの unsigned char の配列を calloc() で確保し 0 を含む 3 の倍数番目の要素を 0xff に書き換えよ #include <stdlib.h> unsigned char *cp = (unsigned char *)calloc(30, 1); 3 の倍数番目の要素に どのようにして 値をセットするか?
演習 5 30 バイトの unsigned char の配列を calloc() で確保し 0 を含む 3 の倍数番目の要素を 1 3 の倍数 +1 番目の要素を 2 3 の倍数 +2 番目の要素を 3 にセットせよ 別途 10 バイトの unsigned char の配列を calloc() で用意し 上記の 30 バイトの配列から 3 バイトずつ加算をした結果を格納せよ 結果的には 値 6 の入った 10 バイトの配列ができる