読者です 読者をやめる 読者になる 読者になる

お茶漬けびより

学んだことを整理する場所です。主に、C++, Unreal Engine 4 (UE4) を扱います。

Re:ゼロから始めるポインタ入門 #1

C C++ 勉強

前回の続きです。

前回は、準備運動のようなものでしたが、 今回からはちゃんとポインタについて入門していきます。

まずポインタのことを知る前に、二つのことを知っておく必要があります。 16 進数とアドレスです。この二つを説明したあと、最後にポインタについて解説していきます。

16 進数

16 進数を知っている方は、読み飛ばして構いません。逆に知らない方は 2 進数のことを分かっておく必要があります。 2 進数については、前回に書いてあるので、そちらを参照してください。

16 進数とは、2 進数と 10 進数を知っているのであれば大方予想が着くと思いますが、16 の倍数で桁が増える表現方法です。 ですが、数字とは 0 ~ 9 の 10 個の数字です。残り 6 個はどうするかというと…… A ~ F を使います。10 進数では、9 の次が 10 で桁が増えますが、16 進数の場合は、9 の次は A になり桁は増えません。そこから B、C、D …… F と続きます。F は 10 進数で表すと値は何でしょう。数えてみましょう。 9、A(10)、B(11)、C(12)、D(13)、E(14)、F(15)。つまり F15 です。その次は基数の 16 なので、桁が一つ上がります。そう 10 になります。ところで 1010 進数の 10(十)なのか 16 進数 の 10(十六)なのか、2 進数の 10(二)なのか分かりづらいですね。 そこで、分かりやすいように手前に記号を付けます。2 進数の場合は、0b、 10 進数の場合は、0d、 16 進数の場合は 0x です。 b は binary(2 進数) の b。d は decimal(10 進数)の d。x は、Hexadecimal の x です(なんで h じゃないんだろう。hex が x に縮まったのかな……)。整理しましょう。

1000 という数字の手前に記号を付けます。説明のために数値は、10 進数に直しますが、数字だとややこしいので、漢字で解説します。
0b1000:これは、2 進数なので、ですね。
0d1000:これは、10 進数なので、です。
0x1000:これは、16 進数です。デカいですね。16 の 3 乗なので、四千九十六です。
どうでしょうか。整理できましたか? 16 進数は、慣れておくと便利なので、いろいろ自分で計算して練習してみましょう。

10 進数から 2 進数に変換し、2 進数から 16 進数への変換ができるのですが、これは、ネットや本に詳しく書かれているのでそちらを参照してください。

アドレス

では、アドレスについてです。前回、変数の話で値を保持するには器の大きさがあり、場所が必要だと書きました。この場所を管理するための番号があります。それがアドレスです。では、実際にアドレスを見てみましょう。アドレスを見るためには、変数の名前の前に &(アンパサンド) を付けます。

#include <stdio.h>

int main()
{
    int variable;

    printf("address of variable: %p\n", &variable);
    return 0;
}

上記を実行すると、以下のようにアドレスが表示されます。ただし、アドレスの値は毎回変わります。

address of variable: 0x7ffff409cbdc

これは、variable という器が 0x7ffff409cbdc の場所に保持されているということです。次に配列を作って、その配列のアドレスがどうなっているか見てみます。以下のプログラムを作成してください。

#include <stdio.h>
#define ARRAY_MAX (10)

int main()
{
    char array[ARRAY_MAX];
    int  aryi;

    for (aryi=0; aryi<ARRAY_MAX; aryi++) {
        printf("address of array[%d]: %p\n", aryi, &(array[aryi]));
    }
    return 0;
}

では、実行してみましょう。以下のようになりました。

address of array[0]: 0x7fffec172540
address of array[1]: 0x7fffec172541
address of array[2]: 0x7fffec172542
address of array[3]: 0x7fffec172543
address of array[4]: 0x7fffec172544
address of array[5]: 0x7fffec172545
address of array[6]: 0x7fffec172546
address of array[7]: 0x7fffec172547
address of array[8]: 0x7fffec172548
address of array[9]: 0x7fffec172549

アドレスのとこを見て下さい。各アドレスの下の位から 1~3 つ目まで見れば問題ありません。値が 1 ずつ増えてるのが分かります。char 型は、サイズが 1 Byte、つまり 8 bit です。つまり、このアドレスの単位は Byte のようです。試しに型を変えてみましょう。char 型から int 型にしてみると以下のようになりました。

address of array[0]: 0x7fffe297dc40
address of array[1]: 0x7fffe297dc44
address of array[2]: 0x7fffe297dc48
address of array[3]: 0x7fffe297dc4c
address of array[4]: 0x7fffe297dc50
address of array[5]: 0x7fffe297dc54
address of array[6]: 0x7fffe297dc58
address of array[7]: 0x7fffe297dc5c
address of array[8]: 0x7fffe297dc60
address of array[9]: 0x7fffe297dc64

4 ずつ増えていますね。(long)int 型は 4 Byte なので、アドレスの単位は Byte で正しそうです。

アドレスは場所を管理するための番号です。先ほどのプログラムの結果からアドレスは 型のサイズずつ管理しているということですね。

配列の結果を見ると分かりますが、変数は他の変数が占有している場所を利用して値を保持することはできません。中には共有する変数もありますが……今回は無視しましょう。基本的に変数が確保した場所は、その変数専用の場所になります。ですので、その変数が占有しているアドレスに訪れるとその変数の場所にたどり着くことができます。つまり、変数の場所が分かれば、その変数の中にある値を見たり、別の値を変数の中に入れたりできるわけです。そのための方法がポインタです。

ポインタの話に行く前に、一つ遊んでみましょう。何をするかというと足し算です。 以下を実行して下さい。

#include <stdio.h>
#define ARRAY_MAX (3)

int main()
{
    int array[ARRAY_MAX];
    int aryi;

    for (aryi=0; aryi<ARRAY_MAX; aryi++) {
        array[aryi] = aryi;
        printf("address of array[%d]: %p\n", aryi, &(array[aryi]));
    }
    printf("\n");

    printf("array[0] + 1\n");
    printf("%p + %d = %p\n", &(array[0]), 1, (&array[0]) + 1);

    return 0;
}

結果は以下のようになりました。

address of array[0]: 0x7fffe8de9180
address of array[1]: 0x7fffe8de9184
address of array[2]: 0x7fffe8de9188

array[0] + 1
0x7fffe8de9180 + 1 = 0x7fffe8de9184

array[0] のアドレス(0x7fffe8de9180)に 1 を加えると結果は、0x7fffe8de9184 になりました。なんと、4 も増えていますね。次に array の型を int から char にしてみましょう。 結果は、以下のようになりました。

address of array[0]: 0x7fffd307cf50
address of array[1]: 0x7fffd307cf51
address of array[2]: 0x7fffd307cf52

array[0] + 1
0x7fffd307cf50 + 1 = 0x7fffd307cf51

今度は、1 増えています。つまり型によって値の増え方が変わるというわけです。これで中途半端なアドレスを、簡単には指定できないようになってるわけですね。ちなみに、 printf 文内の足し算の処理を以下のようにすると、中途半端なアドレスが作れます。arrayint 型です。

(char*)(&array[0]) + 1

ですが、危険なのと特に意味はないので使う機会はないでしょう。

ポインタ

ポイント(point)という言葉があります。これは、点や先という意味もありますが、向けるや指さすという意味もあります。なのでポインタ(pointer)は、向ける者や指さす者という感じでしょうか。何に指さすかというとアドレスの中にあるモノです。ポインタが指さす先には、何かがあるわけです。実際にポインタを使ってみましょう。

#include <stdio.h>

#define ARRAY_MAX (3)

int main()
{
    int array[ARRAY_MAX];
    int aryi;

    for (aryi=0; aryi<ARRAY_MAX; aryi++) {
        array[aryi] = aryi;
        printf("address of array[%d]: %p\n", aryi, &(array[aryi]));
    }
    printf("\n");

    int* array_ptr;
    array_ptr = &array[0];

    printf("array_ptr has address of array[0]: %p\n", array_ptr);
    printf("array_ptr points %d\n", *array_ptr);

    return 0;
}

結果は以下のようになりました。

address of array[0]: 0x7ffff5947b90
address of array[1]: 0x7ffff5947b94
address of array[2]: 0x7ffff5947b98

array_ptr has address of array[0]: 0x7ffff5947b90
array_ptr points 0

array_ptr = &array[0] によって、ポインタ(array_ptr)に array[0] のアドレスを教えて、そのあとポインタが指し示す場所を見に行っています。*array_ptrで指している場所を見に行けるわけです。

アドレスの最後に足し算をしましたが、これを使えば配列の中を自由に見に行けます。 以下を実行してみましょう。

#include <stdio.h>

#define ARRAY_MAX (3)

int main()
{
    int array[ARRAY_MAX];
    int aryi;

    for (aryi=0; aryi<ARRAY_MAX; aryi++) {
        array[aryi] = aryi;
        printf("address of array[%d]: %p, %d\n", aryi, &(array[aryi]), array[aryi]);
    }
    printf("\n");

    int* array_ptr;
    array_ptr = &array[0];

    for (aryi=0; aryi<ARRAY_MAX; aryi++) {
        printf("array_ptr has address of array[%d]: %p\n", aryi, array_ptr + aryi);
        printf("array_ptr points %d\n", *(array_ptr+aryi));
    }
    return 0;
}

結果は、以下のようになりました。

address of array[0]: 0x7fffed9f3780, 0
address of array[1]: 0x7fffed9f3784, 1
address of array[2]: 0x7fffed9f3788, 2

array_ptr has address of array[0]: 0x7fffed9f3780
array_ptr points 0
array_ptr has address of array[1]: 0x7fffed9f3784
array_ptr points 1
array_ptr has address of array[2]: 0x7fffed9f3788
array_ptr points 2

配列の中を先頭から順番に見れているようです。実は、配列の各要素を指定するときのarray[i]は、*(array + i)の略なんですね。配列は、ポインタというわけです。[] を使わずに array とすると、配列 array の先頭のアドレスを指します。先頭のアドレスとは、&array[0] のことです。つまり array&array[0] は同じ意味です。整理しましょう。

以下は、int array[3] とした場合の説明です。

  • 配列は、ポインタの別の表現方法。
  • &array[0]array は同じアドレスを指す。
  • 配列の要素([] 内の値)は、配列の先頭からどれだけアドレスを増やすかを示すモノ。
  • array[i]*(array + i) は同じ意味。

確認してみましょう。

#include <stdio.h>
#define ARRAY_MAX (3)

int main()
{
    int array[ARRAY_MAX];
    int aryi;
    
    if (&array[0] == array) {
        printf("&array[0] == array\n");
    }

    if (&array[2] == array + 2) {
        printf("&array[2] == array + 2\n");
    }

    array[1] = 100;
    if (array[1] == *(array + 1)) {
        printf("array[1] == *(array + 1)\n");
    }

    return 0;
}

結果は、以下のようになりました。

&array[0] == array
&array[2] == array + 2
array[1] == *(array + 1)

おわりに

今回は、ポインタを入門してみました。配列のくだりはあまり理解しなくても問題ありませんが、 配列とポインタを頭の中で変換できるとポインタの理解が深まる気がします。 ポインタを理解するには、実際にコードを書くしかないと思います。 最低限以下を覚えておけば、ポインタで使われるテクニックはすべて理解可能だと思っています。

  • ポインタの宣言は、int * pointer のように行う。
  • 変数の保持されている場所のアドレスは、& で見ることができる。
  • ポインタの中には、アドレスを入れる。
  • ポインタの手前に * を付けると、アドレス先の中身を見に行く。