情報基礎演習II − 第9回


1.ポインタ


ポインタの正体

コンピュータは、 データをメモリに保存します (レジスタやディスクなんかはどうなるという突っ込みはしないように)。 変数名はデータが保存されているどこかのメモリに付けられた 名前です。
int a;

a = 100;
これはメモリ上に int 型のデータを格納する場所を一つ確保し、 a という名前を付けて、 そこに 100 を格納します。

実際には、 メモリのひとつひとつの「記憶単位」に番号が付けられています。 この番号のことを「番地(address)」と呼びます。 ポインタはこの番地のような、 データの格納場所を示す値をデータとして取り扱うデータ型です (なんちゅう説明だ…)。

図では変数 a は8番地のメモリの内容を指しています。 変数 a の格納場所を示すポインタを求めるには、 & という単項演算子を使います。

&a
なお、この場合 &a は定数です(変数 a の格納場所は固定だからね)。 これをポインタ定数と呼びます。


ポインタ変数

ポインタをデータとして格納する変数を、 ポインタ変数と呼びます。
int a;
int *pa;

a = 10;
pa = &a;
int *pa; という宣言によって、 pa という int 型のポインタ変数が用意されます。 これは「*pa」が int 型であるという宣言であって、 * が取れた pa が「int 型のポインタ変数」になります。

ポインタ変数が保持するデータはポインタ(番地)であり、 その場所に格納されているデータを取り出すためには、 * という単項演算子を用います。 * は掛け算と同じ記号ですが、 ポインタ型の変数に対する単項演算子として使用した場合は、 ポインタによって示されている場所に格納されているデータを取り出します。

int a, b;
int *pa;

a = 10;
pa = &a;

b = *pa;   /* b に 10 が代入される */
ポインタの値は、 オペレーティングシステムによってプログラムに割り当てられたメモリ空間内の、 どこか一箇所を指していなければなりません (どこも指していないということを明示するために 0 という値も用います)。 * 演算子によってその内容を操作しようとする場合は、 そのポインタが変数のようにデータを記憶する目的で割り当てられた メモリの領域を指していなければなりません。 もし不正な内容のポインタを * 演算子によって操作しようとすれば、 プログラムは正常に動かないか、異常終了します。

■課題37■

■課題38■

■課題39■



配列とポインタ

最初の要素のポインタを得るには、 次のようにします。 これは配列として確保されたメモリ領域の先頭を指すポインタです。
int a[10];
int *pa0, *pa1;

pa0 = &(a[0]); /* a[0] のポインタ */
配列の各要素はメモリ上に連続して配置されているので、 2つ目の要素 a[1] のポインタは、 次のようにして求めることができます。
pa1 = pa0 + 1; /* a[1] のポインタ */
従って、a[0] および a[1] の内容は、 それぞれ、次のようにして参照できます。

++ や -- という演算子もポインタ変数に使用することができます。

int a[10];
int *pa0, c;

pa0 = &(a[0]); /* a[0] のポインタ */
c = *pa0++;
* は ++ より優先順位が低いため、 上の例では c に a[0] の内容が入り、 その後 pa0 の値は &(a[1]) になります。 もし *pa(すなわち a[0])の内容を1増すなら、 ( ) を使う必要があります。
c = (*pa0)++;
この例では c に a[0] が入りますが、 その後 a[0] の内容は a[0] + 1 になり、 pa0 の内容は変化しません。 なお、
c = ++*pa0;
この場合は a[0] の内容が1増され、それが c に代入されます (* が ++ より pa0 に近いところにあるため)。

実は上記において宣言されている配列 a[10] の a は、 それ自体が配列として確保されたメモリ領域の先頭を指すポインタ定数、 すなわち &(a[0]) と等しい値を持ちます。 従って、

となります。すなわち、 [ ] は実はポインタに対する演算子であり、 ポインタ変数 pa0, pa1 に対しても、 次のように適用できます。
int a[10];
int *pa0, *pa1;

pa0 = &(a[0]); /* a[0] のポインタ */
pa1 = pa0 + 1; /* a[1] のポインタ */
上の例では、 最後の例から分かるとおり、 配列はその領域の先頭のポインタのみによって識別され、 その大きさや添え字の範囲といった情報は、 プログラム中では管理されていません。

従って、確保された領域からはずれるような 配列の添え字やポインタを使用しても、 コンパイルの際にはエラーにはなりませんし、 問題なく実行できるように見えることもあります。 しかしこれは非常に発見しにくいプログラムの異常動作の原因になりますし、 大抵は致命的なエラーを引き起こします。 ポインタを使う場合には細心の注意が必要です。

■課題40■

文字列の実体は配列ですから、 これもポインタになります。

char *string;

string = "Hello!\n";
文字列定数の値は、 その文字列が格納されている位置を指すポインタ定数になります。 string[0] は 's'、string[5] は '!' になります。
int i;
char c;

c = "0123456789ABCDEF"[i];
上の例は、0〜15 の値を持つ整数 i を、 それに対応する16進数の文字 c に変換します。


ポインタの配列

ポインタ自身を配列の要素に使う場合もあります。
int *p[3];            /* ポインタの配列 */
int a[4], b[5], c[6];

p[0] = a;
p[1] = b;
p[2] = c;
こうすると、*(p[0] + 1)*(a + 1) すなわち a[1] になります。 さらに [ ] はポインタに対する演算子なので、 p[0][1] と書くこともできます。 すなわち2次元配列のように扱うことができます。
p[0][1] = 10;   /* a[1] = 10; */
p[1][2] = 20;   /* b[2] = 20; */
p[2][3] = 30;   /* c[3] = 30; */

ポインタ引数

ポインタを引数に使って関数を呼び出すためには、 仮引数をポインタ変数として宣言します。
#include <stdio.h>

void add10(int *a)
{
    *a += 10; /* 引数のポインタが指す内容に 10 を足す */
}

int main(void)
{
    int x;

    x = 20;

    add10(&x);

    printf("x = %d\n", x); /* 30 になっている */

    return 0;
}
引数に変数のポインタを使うことによって、 呼び出した関数から変数に値を戻すことができます。

■課題41■

配列の場合は、配列名がポインタ定数なので、 それをそのまま指定すればポインタを渡すことができます。

しかし、 ポインタとして渡されるのは配列の先頭の要素の位置だけなので、 配列の大きさはこの例のように別に渡してやる必要があります。

#include <stdio.h>

void alladd10(int a[], int n)
{
    int i;

    /* 配列の全ての要素に10を足す (/
    for (i = 0; i < n; i++) {
        a[i] += 10;
    }
}

int main(void)
{
    int x[20];

    alladd10(x, 20);

    return 0;
}
仮引数の次のいずれの形で宣言しても構いません。 前者はポインタとして宣言していますが(渡されるのはポインタなので)、 後者はサイズを指定しない配列として宣言しています。

■課題42■

ただし、2次元以上の配列の場合は、 最も左の添え字以外はサイズを省略できません。

#include <stdio.h>

void alladd10(int a[][3][2], int n)
{
    int i, j, k;

    /* 配列の全ての要素に10を足す (/
    for (i = 0; i < n; i++) {
        for (j = 0; j < 3; j++) {
            for (k = 0; k < 2; j++) {
                a[i][j][k] += 10;
            }
        }
    }
}

int main(void)
{
    int x[4][3][2];

    alladd10(x, 4);

    return 0;
}
これは、 多次元配列も メモリ上は1次元に展開されており、 それがどう分割して使用されているかを明らかにする必要があるからです。

ポインタの配列は、 次のようにして関数に渡すことができます。

#include <stdio.h>

void alladd10(int *a[], int *n, int m)
{
    int i, j, k;

    /* 配列の全ての要素に10を足す */
    for (i = 0; i < m; i++) {
        for (j = 0; j < n[i]; j++) {
            a[i][j] += 10;
        }
    }
}

int main(void)
{
    int *x[3], n[3];
    int a[4], b[5], c[6];

    x[0] = a;
    n[0] = 4;

    x[1] = b;
    n[1] = 5;

    x[2] = c;
    n[2] = 6;

    alladd10(x, n, 3);

    return 0;
}
ポインタの配列の仮引数の宣言は、 次のいずれでも構いません。

関数のポインタ

関数名も、 配列同様ポインタ定数です。 この場合、このポインタによって指し示される先の内容は、 データではなくプログラムです。
int add(int x, int y)
{
    return x + y;
}

int sub(int x, int y)
{
    return x - y;
}

int main(void)
{
    int x;
    int (*func)(int, int); /* 関数のポインタ変数の宣言 */

    func = add;
    x = (*func)(2, 3); /* 関数 add が実行される */

    func = sub;
    x = (*func)(2, 3); /* 関数 sub が実行される */
}
もちろん、関数のポインタの配列、なんてものも使えます。
int add(int x, int y)
{
    return x + y;
}

int sub(int x, int y)
{
    return x - y;
}

int main(void)
{
    int x;
    int (*func[2])(int, int) = { add, sub }; /* 関数のポインタ配列 */

    x = (*func[0])(2, 3); /* 関数 add が実行される */

    x = (*func[1])(2, 3); /* 関数 sub が実行される */
}

■課題43■