ポインタと配列

配列とポインタは同じ人物の2つの見方のようなものです——角度が違えば姿は異なりますが、その下にあるのは同じ連続したメモリ領域です。

配列名は最初の要素のアドレス

ほとんどの式において、配列名は自動的に最初の要素へのポインタに変換されます。

C
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%p\n", (void *)arr);
printf("%p\n", (void *)&arr[0]);
printf("%p\n", (void *)p);

3つのアドレスはすべて同じです。arr&arr[0]は等価で、どちらも最初の要素のアドレスです。

例外があります:sizeof(arr)は配列全体のサイズを返し、ポインタのサイズではありません。&arrは配列全体へのポインタで型はint (*)[5]であり、その歩幅は配列全体です。

C
printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(p));

64ビットシステムでは、それぞれ20(5 x 4バイト)と8(ポインタのサイズ)を出力します。

ポインタによる配列走査

ポインタは配列要素を指せるため、ポインタ演算で配列全体を走査できます。

C
int arr[] = {10, 20, 30, 40, 50};
int *p;
int len = sizeof(arr) / sizeof(arr[0]);

for (p = arr; p < arr + len; p++) {
    printf("%d ", *p);
}
printf("\n");

arr + lenは最後の要素の次の位置を指し、ループの終了番兵として機能します。これは非常に一般的なCのイディオムです。

添字記法も使えます。

C
int i;
for (i = 0; i < len; i++) {
    printf("%d ", *(arr + i));
}

または:

C
int *p = arr;
int i;
for (i = 0; i < len; i++) {
    printf("%d ", p[i]);
}
💡 ヒント: p[i]*(p + i)は完全に等価です。添字演算子はポインタ演算の糖衣構文です。

ポインタ演算

ポインタは限られた演算操作をサポートしています。

整数との加算と減算

C
int arr[] = {10, 20, 30, 40, 50};
int *p = arr + 2;

printf("%d\n", *p);
printf("%d\n", *(p + 1));
printf("%d\n", *(p - 1));

出力:30、40、20。p+1はint1つ分前進、p-1はint1つ分後退します。

ポインタ同士の減算

同じ配列内の2つのポインタを減算すると、間の要素数が得られます。

C
int *q = &arr[4];
printf("%ld\n", (long)(q - p));

出力:2。pはarr[2]を、qはarr[4]を指しており、差は2要素です。

ポインタの比較

同じ配列内のポインタは<<=>>=で比較でき、相対位置を判定できます。

C
int *start = arr;
int *end = arr + len;
while (start < end) {
    printf("%d ", *start);
    start++;
}
⚠️ 注意: ポインタ演算が意味を持つのは同じ配列内のみです。異なる配列にまたがるポインタの減算や比較は未定義動作です。

ポインタの歩幅まとめ

歩幅(p+1で移動するバイト数)
char * 1
int * 4
double * 8
int (*)[5] 20

歩幅 = sizeof(指している型)

ポインタと配列添字の等価性

次の2つのアクセス形式は完全に等価です。

等価な形式
arr[i] *(arr + i)
&arr[i] arr + i
p[i] *(p + i)
&p[i] p + i

コンパイラは添字演算をポインタ演算に変換します。arr[3]3[arr]はCでは両方とも有効です(*(arr+3) == *(3+arr)のため)。ただし後者を書く人はいません。

しかし、ポインタと配列には根本的な違いがあります。

C
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

ポインタで配列を反転します。

C
#include <stdio.h>

void reverse(int *arr, int len) {
    int *left = arr;
    int *right = arr + len - 1;

    while (left < right) {
        int temp = *left;
        *left = *right;
        *right = temp;
        left++;
        right--;
    }
}

int main(void) {
    int data[] = {1, 2, 3, 4, 5, 6, 7};
    int len = sizeof(data) / sizeof(data[0]);
    int i;

    reverse(data, len);

    for (i = 0; i < len; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");
    return 0;
}
▶ 試してみよう
TEXT
7 6 5 4 3 2 1

双ポインタテクニック:leftは先頭から末尾へ、rightは末尾から先頭へ移動し、指す要素を入れ替えて、両者が出会うまで続けます。

配列の関数仮引数としての本質

配列を関数に渡すと、コンパイラは最初の要素のアドレスだけを渡し、配列の長さの情報は失われます。これが「配列からポインタへの崩れ」です。

C
void func(int arr[]) {
}

仮引数int arr[]int *arrと等価です。関数内では次の通りです。

C
void print_array(int *arr, int len) {
    int i;
    for (i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

4つの等価なシグネチャ:

C
void func(int *arr, int len);
void func(int arr[], int len);
void func(int arr[5], int len);
void func(int arr[100], int len);

角括弧内の数値はコンパイラに無視され、「これはポインタである」ことを示すだけです。

ポインタでソート済み配列を重複排除します。

C
#include <stdio.h>

int unique(int *arr, int len) {
    if (len == 0) return 0;

    int *dst = arr;
    int *src = arr + 1;
    int *end = arr + len;

    while (src < end) {
        if (*src != *dst) {
            dst++;
            *dst = *src;
        }
        src++;
    }

    return (int)(dst - arr + 1);
}

int main(void) {
    int data[] = {1, 1, 2, 2, 2, 3, 4, 4, 5};
    int len = sizeof(data) / sizeof(data[0]);
    int new_len = unique(data, len);
    int i;

    for (i = 0; i < new_len; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");
    return 0;
}
▶ 試してみよう
TEXT
1 2 3 4 5

dstは重複排除後のデータの書き込み位置を追跡し、srcは元の配列を走査します。関数は新しい長さを返します。これは追加の配列を必要としないインプレース重複排除アルゴリズムです。

❓ よくある質問

Q 配列名に代入できますか?
A いいえ。arr = p;は不正です。配列名は定数ポインタです。しかしp = arr;は有効です。ポインタ変数には代入できるからです。
Q arr&arrの違いは何ですか?
A arrは最初の要素のアドレスで、型はint *、歩幅はsizeof(int)です。&arrは配列全体のアドレスで、型はint (*)[5]、歩幅はsizeof(int[5])です。数値は同じですが型が異なります。
Q 関数内で配列にsizeofを使えないのはなぜですか?
A 配列を仮引数として渡すとポインタに崩れ、sizeofはポインタのサイズ(4または8)を返し、配列のサイズは返しません。長さは別途渡す必要があります。
Q p[-1]は有効ですか?
A 構文的には有効で、*(p-1)と等価です。pが配列の途中を指していれば、p[-1]は前の要素に安全にアクセスできます。ただし配列の境界外へのアクセスは未定義動作です。

📖 まとめ

📝 練習問題

  1. 追加の配列を使わず、ポインタで配列を1つ左に回転させる(最初の要素を最後に移動する)関数を書いてください。
  2. targetと等しい最初の要素へのポインタを返す関数int *find(int *arr, int len, int target)を書いてください。見つからなければNULLを返します。
  3. ソート済み配列を対象に、ポインタを使った二分探索を実装してください。見つかった要素へのポインタを返し、見つからなければNULLを返します。
100%