ポインタと配列

配列の先頭要素のポインタ

ポインタと配列は別物ですが、ちょっとした関係性があります。
まずは以下のコードを見てください。

#include <stdio.h>

int main()
{
    int arr[] = { 11, 22, 33, 44 };
    int *pointer;
    
    pointer = arr;
    printf("%d\n", *pointer);

    pointer = &(arr[0]); //↑と同じ
    printf("%d\n", *pointer);

    pointer += 1; //arr[1]と同じ
    printf("%d\n", *pointer);

    getchar();
}
11
11
22

8行目、ポインタ変数pointerに配列を代入しています。
配列の名前の後にいつもの角括弧[](添字演算子という)が付けられておらず、配列名そのままで指定しています。

配列は、[]を省いて記述すると配列の先頭要素のポインタを返すという決まりがあります。
配列の先頭要素のポインタなので、そのままポインタ変数に代入できますし、ポインタ変数pointerの値を表示するとちゃんと「11」が表示されます。

11行目、今度はいつも通り添字演算子をつかって先頭要素([0]番目)を指定した上で、アドレス演算子を使ってアドレスを取り出しています。
これは8行目と同じ意味になりますので、やはり値は「11」になります。

14行目では、ポインタ変数pointerに「1」を加算しています。
配列の要素を示すポインタ変数に値を加算すると、指し示す先が配列の次の要素に移動するという特徴があります。
つまりこれは、「arr[1]」と同じ意味になります。

ポインタと配列2

ただし、この図は実は正確ではありません。
「次のアドレス」の具体的な場所は、ポインタ変数のデータ型により異なるためです。

ポインタの型(ポインタ演算)

配列の要素を示すポインタ変数に1加算すると、配列の次の要素を指します。
これは「ポインタ変数に1を加算するとアドレスが1増えるではないことに注意してください。
(ポインタ変数に値を増減する操作をポインタ演算)といいます。

変数にはデータ型があります。
データ型が異なると、その値を保存するためのメモリ上のサイズが異なります。
char型ならば1バイト、short型ならば2バイトのメモリ上の領域が必要になります。
今までの図では説明のため簡略化していましたが、実際にはこのようになります。
データ型のサイズとメモリ

ポインタ変数は、メモリ上の場所(アドレス)の情報だけではなく、データ型も記憶しています。
つまり、「int *pointer」ならば「メモリ上の場所は○○で、そこでは4バイト消費している」という情報を持っていることになります。
(int型のサイズは環境によって異なります。ここでは32bit(=4バイト)を前提に進めます)

int型配列を宣言(&初期化)した時、メモリ上には4バイトごとに連続した位置に値が配置されます。
(本当かどうか気になる人は確認プログラムを作ってみてください)
int型配列のメモリ上の配置

配列の先頭要素のアドレスが「1000」であるとき、先頭要素を指すポインタ変数に「1」を加算すると、そのポインタ変数が保存するアドレスは「1004」になります。
ポインタ変数に加算するときに実際に増加する値

つまり、ポインタ変数の演算時に実際に増減する値は「増減させたい値×データ型サイズ」である、ということです。

これはインクリメント/デクリメントの場合でも同様です。

int arr[] = { 11, 22, 33, 44 };
int *pointer;
pointer = arr;

//次の要素を指す
//つまりアドレスにプラス4バイト
pointer++;

//前の要素を指す
//つまりアドレスにマイナス4バイト
pointer--;

これはポインタの演算時の特徴です
実際にアドレス位置を数値指定して動かすことはまずありませんが、上記のようなポインタ演算はしょっちゅう行われます。

ちなみに「ポインタ自体のサイズ」はint型です。
char型のポインタ変数でもlong型のメモリ変数でも、ポインタ変数ひとつを使用するために消費するメモリはint型ひとつです。
この性質は、巨大なデータを高速に処理する場合に役に立つことがあります。

ポインタの配列的な記述

以下のコードは一見奇妙に見えるかもしれませんが、有効なコードです。

#include <stdio.h>

int main()
{
    int arr[] = { 11, 22, 33, 44 };
    int *pointer;
    pointer = arr;

    for (int i = 0; i < 4; i++)
    {
        printf("%d\n", pointer[i]);
        //printf("%d\n", *(pointer + i));
    }

    getchar();
}

「pointer」はint型ポインタ変数として宣言していますが、11行目のprintf関数内ではポインタ変数に対して角括弧[]を使用しています。
これは、「*(pointer + i)」と同じ意味となります。
丸括弧がないと、「ポインタ変数が指す値 + i」という意味になってしまいますので注意してください。

//以下の二つは全く同じ
*(pointer + 1);
pointer[1];

//これは意味が変わるので注意
*pointer + 1;

角括弧(添字演算子)は配列にしか指定できないという決まりはなく、アドレスを増減するという役割を持ちます。
「arr[3]」は内部的に「*(arr + 3)」と変換される、という決まりがあるだけです。

実は「3[arr]」のように、配列名と添え字とを逆に書いてもエラーにはなりません。
「*(3 + arr)」となるだけだからです。
(ややこしいので普通はこんな書き方はしませんが)

関数の仮引数の配列

この関係性は、関数の引数に配列を指定する場合によく利用されています。
例えば以下のようなコードです。

#include <stdio.h>

double average(const int arr[], int length)
{
    if (length <= 0)
        return 0.0;

    int total = 0;
    for (int i = 0; i < length; i++)
    {
        total += arr[i];
    }

    return (double)total / length;
}

int main()
{
    int numbers[] = { 60, 75, 82 };
    printf("%f", average(
            numbers,
            sizeof(numbers) / sizeof(numbers[0])));
    getchar();
}

これは配列と関数の項で使用したサンプルコードと全く同じものです。

「const int arr[]」で引数に配列を指定しているのですが、これは実は内部的に「const int *arr」に置き換えられています。


double average(const int arr[], int length)
{
}

//↓

double average(const int *arr, int length)
{
}

引数の指定で角括弧を用いて記述しても、ポインタ形式で記述したのとまったく同じことなのです。
(だから引数に配列の要素数を書いても無視される)

11行目の「total += arr[i]」というコードも、実際には「total += *(arr + i)」と記述したのと同じことです。
配列と関数の項では、知らないうちにポインタを利用していたことになります。

「*(ポインタ変数 + n)」という書き方よりも「ポインタ変数[n]」という配列的な書き方の方が書きやすくコードの意味がわかりやすいため、このような書き方が許されています。
このような簡便な記述方法をシンタックスシュガー(糖衣構文)と言います。
(呼び方は別に覚えなくても良い)

配列が関数内で書き換えられる理由

このことが分かれば、配列と関数の項の最後で紹介した「配列は関数の引数に指定すると関数内で書き換えられる」という理由が分かります。

関数に配列を渡したつもりでも、実際に渡されていたのは配列自身ではなく、「配列の先頭要素を示すポインタ」です。
関数の呼び出し側の実引数に角括弧を付けないのも、配列の先頭要素のアドレスを渡していたからです。

受け取ったのはポインタですから、ポインタが指し示す先のデータに直接アクセスができるため、関数内では配列の値を書き換えることができるのです。

関数側から配列の要素数を知る方法がないのも、引数で受け取ったのは配列の先頭要素を示すポインタに過ぎないからです。