C プログラミング
ポインタなんて恐くない!
-藤田 悟
目標
• C 言語プログラムとメモリ、ポインタの関係を
深く理解する。
– C言語プログラムは、メモリを素のまま利用できま
す。これが原因のエラーが多く発生します。
– メモリマップをよく頭にいれて、ポインタの動きを
理解できれば、C言語もこわくありません。
ディレクトリの作成と移動
• 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
を用いる。
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 が、何番地のメモ
リに格納されていたかを確認して、メモリマッ
プを作成せよ
7fff3c4f92ac
7fff3c4f92ad
7fff3c4f92ae
7fff3c4f92af
7fff3c4f92a8
7fff3c4f92a9
7fff3c4f92aa
7fff3c4f92ab
x と y は、どちらが小さいアドレスにある?
x と y のサイズとメモリは一致しているか?
PCが4Gバイトのメモリを持っているとすると、
4G = 0x100000000 (16進数で、0が8個)
表示は7fff から始まっているけれど、本当は
そんなに大きなメモリはない
0
7fffffffffff
4バイト境界のメモリマップ
• 1バイトずつだと、縦に長くなりすぎるので、4
バイトずつ横に並べて、メモリマップを書く
7fff3c4f92ac
7fff3c4f92a8
0
7ffffffffffc
00
00
00
00
00
00
00
00
変数 x
変数 y
なぜか、上下が逆!!
演習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;
printf("%d %lx %d¥n", x, &x, sizeof(x));
printf("%ld %lx %d¥n", y, &y, sizeof(y));
printf("%f %lx %d¥n", f, &f, sizeof(f));
printf("%f %lx %d¥n", d, &d, sizeof(d));
printf("%d %lx %d¥n", c, &c, sizeof(c));
}
7fff3c4f92ac
7fff3c4f92a8
0
7ffffffffffc
7fff3c4f92a4
7fff3c4f92a0
7fff3c4f929c
7fff3c4f9298
7fff3c4f9294
7fff3c4f9290
7fff3c4f928c
変ですねぇ…
x
y
f
d
c
配列のメモリを見る
• 配列の宣言は、int array[4]; のように行う
• 配列要素のメモリアドレスは、&array[0],
&array[1]のようにして、得られる
• 配列の先頭アドレスは、&array でわかる
• 配列の全体のサイズは、 sizeof array
• 配列の一要素のサイズは、sizeof array[0]
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);
}
ポインタ変数
• アドレスを格納するための変数をポインタ変数と
呼ぶ。
– 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”;
h
o
s
e
i ¥0
univ
演習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すると、変数の型に合わせて、数バ
イトずつアドレスが増加する
• ローカル変数は、関数呼び出しごとにメモリが割
り当てられる
– ローカル変数で配列を確保すると大変なことに…
• ディレクトリを変更します。
• 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]); }
} }