ポインタと配列
配列とポインタは同じ人物の2つの見方のようなものです——角度が違えば姿は異なりますが、その下にあるのは同じ連続したメモリ領域です。
配列名は最初の要素のアドレス
ほとんどの式において、配列名は自動的に最初の要素へのポインタに変換されます。
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]であり、その歩幅は配列全体です。
printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(p));
64ビットシステムでは、それぞれ20(5 x 4バイト)と8(ポインタのサイズ)を出力します。
ポインタによる配列走査
ポインタは配列要素を指せるため、ポインタ演算で配列全体を走査できます。
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のイディオムです。
添字記法も使えます。
int i;
for (i = 0; i < len; i++) {
printf("%d ", *(arr + i));
}
または:
int *p = arr;
int i;
for (i = 0; i < len; i++) {
printf("%d ", p[i]);
}
p[i]と*(p + i)は完全に等価です。添字演算子はポインタ演算の糖衣構文です。
ポインタ演算
ポインタは限られた演算操作をサポートしています。
整数との加算と減算
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つのポインタを減算すると、間の要素数が得られます。
int *q = &arr[4];
printf("%ld\n", (long)(q - p));
出力:2。pはarr[2]を、qはarr[4]を指しており、差は2要素です。
ポインタの比較
同じ配列内のポインタは<、<=、>、>=で比較でき、相対位置を判定できます。
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)のため)。ただし後者を書く人はいません。
しかし、ポインタと配列には根本的な違いがあります。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
arrは配列。sizeof(arr)= 20。再代入不可pはポインタ。sizeof(p)= 8(64ビット)。別のものを指すよう再代入可能
例
ポインタで配列を反転します。
#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;
}
7 6 5 4 3 2 1
双ポインタテクニック:leftは先頭から末尾へ、rightは末尾から先頭へ移動し、指す要素を入れ替えて、両者が出会うまで続けます。
配列の関数仮引数としての本質
配列を関数に渡すと、コンパイラは最初の要素のアドレスだけを渡し、配列の長さの情報は失われます。これが「配列からポインタへの崩れ」です。
void func(int arr[]) {
}
仮引数int arr[]はint *arrと等価です。関数内では次の通りです。
sizeof(arr)はポインタのサイズを返し、配列のサイズではないsizeofで要素数を計算できない- 長さは別の仮引数として渡す必要がある
void print_array(int *arr, int len) {
int i;
for (i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
4つの等価なシグネチャ:
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);
角括弧内の数値はコンパイラに無視され、「これはポインタである」ことを示すだけです。
例
ポインタでソート済み配列を重複排除します。
#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;
}
1 2 3 4 5
dstは重複排除後のデータの書き込み位置を追跡し、srcは元の配列を走査します。関数は新しい長さを返します。これは追加の配列を必要としないインプレース重複排除アルゴリズムです。
❓ よくある質問
arr = p;は不正です。配列名は定数ポインタです。しかしp = arr;は有効です。ポインタ変数には代入できるからです。arrと&arrの違いは何ですか?arrは最初の要素のアドレスで、型はint *、歩幅はsizeof(int)です。&arrは配列全体のアドレスで、型はint (*)[5]、歩幅はsizeof(int[5])です。数値は同じですが型が異なります。p[-1]は有効ですか?*(p-1)と等価です。pが配列の途中を指していれば、p[-1]は前の要素に安全にアクセスできます。ただし配列の境界外へのアクセスは未定義動作です。📖 まとめ
- 配列名は式の中では最初の要素へのポインタに崩れます。ただしsizeofと&の場合を除きます
arr[i]と*(arr+i)は完全に等価です。添字はポインタ演算の糖衣構文です- ポインタ演算は歩幅単位で行われます。歩幅 = sizeof(指している型)
- 配列を関数の仮引数として渡すとポインタに崩れます。長さは別途渡す必要があります
- 双ポインタテクニックは反転や重複排除などの問題を効率的に解けます
📝 練習問題
- 追加の配列を使わず、ポインタで配列を1つ左に回転させる(最初の要素を最後に移動する)関数を書いてください。
- targetと等しい最初の要素へのポインタを返す関数
int *find(int *arr, int len, int target)を書いてください。見つからなければNULLを返します。 - ソート済み配列を対象に、ポインタを使った二分探索を実装してください。見つかった要素へのポインタを返し、見つからなければNULLを返します。



