ポインタ


ポインタとは

ポインタとは、「メモリ空間を指す=メモリ空間の番地を格納する」ための変数 である。CPUの内部では数値以上に多用されているとても重要なデータだが、 普通のプログラミング言語では、ポインタを直接触れないように隠蔽している。 が、それを生で見せてしまうところが、良くも悪くもC言語の特徴であると言える。

プログラム中で使用しているデータはコンピュータの中のどこかのメモリに 格納されている。例えば、プログラム中に、

    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」を使えば、番地そのものの値を 表示させることも出来る。

データの「大きさ」

少し話はそれるが、 メモリ上にデータを格納するためには当然メモリを消費する。 そのデータの大きさは、sizeof演算子で調べられる。 単位はbyte。 (printfの中の"%lu"はunsigned long intに対するフォーマット指定。)

#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;
}
sizeof.c

ある計算機でこれを実行したところ、

4
8
4
1
8
8
となった。「ポインタ変数の大きさ」は、番地を格納するので、指し示す 先のデータの型によらず、その計算機のメモリ空間の大きさで決まる。 32bitの計算機なら32bit=4byte、64bitの計算機なら64bit=8byteが普通である。

ポインタ変数の加算、減算

ポインタに対して、++, --や、整数の加減が出来る。 このとき、ポインタ変数宣言時に指定された型の大きさ に従って、動く。例えば、「a++;」とされると、aがdoubleを 指すポインタなら8byte、intを指すポインタなら4byte増える。

#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;
}
pointer1.c

これをある計算機で実行したところ、

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;
}
pointer2.c

これをある計算機で実行したところ、

0x7fffb77d4280
0x7fffb77d4284
0x7fffb77d42a4
0x7fffb77d4280
となった。このように、「a」は、「a[0]のアドレス」に 等しく、「a[i]のアドレス」はai*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;
}
pointer3.c

のように書くと、配列の要素を順に表示することが出来る。

実は、 「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;
}
malloc1.c

しかし、このプログラムはコンパイルできずエラーになってしまう (注意)。

このようなプログラムを書くには、mallocfreeを用いれば良い。 これらを使うには、

#include <stdlib.h>
が必要である。mallocは、OSから使用可能なメモリを 割り当ててもらう関数で、byte単位で大きさを指定する。 だから、例えばint10個分のメモリが欲しい場合は、
    a = (int *)malloc(40);
あるいは、
    a = (int *)malloc(sizeof(int) * 10);
のようにする。後者の方がスマートだろう。「(int *)」は、 ポインタのキャスト(型変換)である。mallocはそれが何を 指し示す用途に使われるか分からないので、「void *」という 特殊な型のポインタを返す。それで、intを指すポインタint * に変換してやる必要がある。

freeは、割り当てられたメモリ領域が不要になったとき、 OSにそれを返却する関数である。これをやらないと、プログラム終了時まで 返却されない。

先のプログラムは、mallocfree使って書き直すと、

#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;
}
malloc2.c

のようになる。

なお、搭載するメモリ量の制限などにより、mallocは常に成功するわけでは無い。 失敗すると NULLポインタ(後述) が返ってくる。よって、

    a = (int *)malloc(sizeof(int) * n);
    if (a == NULL) {
        printf("no memory\n");
        return 1;
    }
のようにちゃんとエラーチェックするのが行儀がよい。

関数の引数の参照渡し

関数の引数の受渡しは、一般に「値渡し」(call by value)と呼ばれる方法で行われる。 引数の値は関数の側の変数にコピーされて、関数の中で書き換えても 呼び出し元の変数の値は変化しない。

これに対して、関数の中で書き換えると呼び出し元の値も書き変わるような 受渡し方を「参照渡し」(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;
}
swapint.c

scanfの中で引数の値の頭に「&」を 付けていたのは、実は参照渡しである。scanfは変数の値を 「書き換えてもらう」関数のため、そのようにする必要がある。

NULLポインタ

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("%p\n", NULL);

    return 0;
}
nullpointer.c

どのポインタとも一致しないポインタである。使うには、

#include <stdlib.h>
が必要。ポインタ変数が「今現在どこも指していない」ことを表すのに 使われる。多くのコンパイラの実装では、「0番地」を指している。 (多くのOSにおいて0番地はシステムが使用していてユーザに開放されない 領域であるため、ユーザ使用領域と一致することは無いため。)
ポインタ