プログラム中で使用しているデータはコンピュータの中のどこかのメモリに 格納されている。例えば、プログラム中に、
int a; char c; a = 5; c = 'a';のような部分があったら、
アドレス(番地) | データ(2進数) |
---|---|
999 | -- |
1000 | 00000101 |
1001 | 00000000 |
1002 | 00000000 |
1003 | 00000000 |
1004 | 01100001 | 1005 | -- |
のようになっている箇所があるはずである (int型は4byte、char型は1byte使う)。
アドレス(番地)とは、コンピュータが持っているメモリに 順番に振られている番号のことで、「データをどこにしまっておいたか」を この番号で記憶している。C言語では、この番号を直接扱うことが出来る。
int *p; double *q;のように、「型名 * 変数名」で宣言する。この場合、 int *は「int型を指すためのポインタ変数」、 double *は「double型を指すためのポインタ変数」 という意味である。
また、(通常の)変数に対して、「&演算子」を使うことで その変数の格納されているアドレスを取り出すことが出来る。
int x; int *p; p = &x;とすると、通常の変数xの格納されているアドレスを ポインタ変数pに代入することになる。
また、ポインタ変数からそのポインタの指している部分の「中身」を 取り出すには、「*演算子」を使う。
int x; int *p; p = &x; /* p に変数xの存在するアドレスを代入 */ x = 1; printf("%d\n", *p); *p = 2; /* pの示す場所に2を代入 */ printf("%d\n", x); printf("%d\n", *p);上の例で分かるように、「*演算子」は代入文の左辺にも 使えることに注意。
int x; int *p; p = &x; printf("%p\n", p);のようにprintfの中で「%p」を使えば、番地そのものの値を 表示させることも出来る。
#include <stdio.h> int main(void) { printf("%lu\n", sizeof(int)); printf("%lu\n", sizeof(double)); printf("%lu\n", sizeof(float)); printf("%lu\n", sizeof(char)); printf("%lu\n", sizeof(int *)); printf("%lu\n", sizeof(double *)); return 0; } |
ある計算機でこれを実行したところ、
4 8 4 1 8 8となった。「ポインタ変数の大きさ」は、番地を格納するので、指し示す 先のデータの型によらず、その計算機のメモリ空間の大きさで決まる。 32bitの計算機なら32bit=4byte、64bitの計算機なら64bit=8byteが普通である。
#include <stdio.h> int main(void) { char c; int x; int *p; char *q; p = &x; printf("%p\n", p); q = &c; printf("%p\n", q); p++; printf("%p\n", p); q++; printf("%p\n", q); p += 2; printf("%p\n", p); return 0; } |
これをある計算機で実行したところ、
0x7ffff135c708 0x7ffff135c70f 0x7ffff135c70c 0x7ffff135c710 0x7ffff135c714となった。16進数で読みづらいが、説明の通りに動いていることを確認して欲しい。
int a[10];のような配列があったとき、「a」は 、その配列の先頭を表す 「定数ポインタ」であるとみなすことが出来る。例えば、
#include <stdio.h> int main(void) { int a[10]; int *p; p = &(a[0]); printf("%p\n", p); p = &(a[1]); printf("%p\n", p); p = &(a[9]); printf("%p\n", p); p = a; printf("%p\n", p); return 0; } |
これをある計算機で実行したところ、
0x7fffb77d4280 0x7fffb77d4284 0x7fffb77d42a4 0x7fffb77d4280となった。このように、「a」は、「a[0]のアドレス」に 等しく、「a[i]のアドレス」はaに i*4を足したもの になっている(4はsizeof(int))。また、
#include <stdio.h> int main(void) { int i; int a[10]; int *p; for (i=0; i<10; i++) { a[i] = i*i; } p = a; for (i=0; i<10; i++) { printf("%d ", *p); p++; } printf("\n"); return 0; } |
のように書くと、配列の要素を順に表示することが出来る。
実は、 「a[i]」 という通常の配列へのアクセスは、
「*(a + i)」と全く等価である。
malloc、freeによるメモリの確保、開放
次のプログラムは、自然数nを入力し、次にn個数値を入力してもらって、
そのn個の数値を逆順に出力するものである。
#include <stdio.h> int main(void) { int n; int i; scanf("%d", &n); int a[n]; for (i=0; i<n; i++) { scanf("%d", &(a[i])); } for (i=n-1; i>=0; i--) { printf("%d\n", a[i]); } return 0; } |
しかし、このプログラムはコンパイルできずエラーになってしまう (注意)。
このようなプログラムを書くには、mallocとfreeを用いれば良い。 これらを使うには、
#include <stdlib.h>が必要である。mallocは、OSから使用可能なメモリを 割り当ててもらう関数で、byte単位で大きさを指定する。 だから、例えばint10個分のメモリが欲しい場合は、
a = (int *)malloc(40);あるいは、
a = (int *)malloc(sizeof(int) * 10);のようにする。後者の方がスマートだろう。「(int *)」は、 ポインタのキャスト(型変換)である。mallocはそれが何を 指し示す用途に使われるか分からないので、「void *」という 特殊な型のポインタを返す。それで、intを指すポインタint * に変換してやる必要がある。
freeは、割り当てられたメモリ領域が不要になったとき、 OSにそれを返却する関数である。これをやらないと、プログラム終了時まで 返却されない。
先のプログラムは、malloc、free使って書き直すと、
#include <stdio.h> #include <stdlib.h> int main(void) { int n; int i; int *a; scanf("%d", &n); a = (int *)malloc(sizeof(int) * n); for (i=0; i<n; i++) { scanf("%d", &(a[i])); } for (i=n-1; i>=0; i--) { printf("%d\n", a[i]); } free(a); return 0; } |
のようになる。
なお、搭載するメモリ量の制限などにより、mallocは常に成功するわけでは無い。 失敗すると NULLポインタ(後述) が返ってくる。よって、
a = (int *)malloc(sizeof(int) * n); if (a == NULL) { printf("no memory\n"); return 1; }のようにちゃんとエラーチェックするのが行儀がよい。
これに対して、関数の中で書き換えると呼び出し元の値も書き変わるような 受渡し方を「参照渡し」(call by reference)と呼ぶ。C言語では、ポインタを使うと 参照渡しが実現出来る。
#include <stdio.h> void swap_int(int *x, int *y) { int tmp; tmp = *x; *x = *y; *y = tmp; } int main(void) { int a, b; a = 2; b = 3; swap_int(&a, &b); printf("%d %d\n", a, b); return 0; } |
scanfの中で引数の値の頭に「&」を 付けていたのは、実は参照渡しである。scanfは変数の値を 「書き換えてもらう」関数のため、そのようにする必要がある。
#include <stdio.h> #include <stdlib.h> int main(void) { printf("%p\n", NULL); return 0; } |
どのポインタとも一致しないポインタである。使うには、
#include <stdlib.h>が必要。ポインタ変数が「今現在どこも指していない」ことを表すのに 使われる。多くのコンパイラの実装では、「0番地」を指している。 (多くのOSにおいて0番地はシステムが使用していてユーザに開放されない 領域であるため、ユーザ使用領域と一致することは無いため。)