C言語入門|配列アクセスとポインタアクセスが同じ理由

「えっ、p[0] と *p って同じ意味なの?」
C言語を学び進めていくと、ここで一度は思考が止まります。

配列アクセスとポインタアクセス。
見た目も、学ぶ順番も、用途も違うはずなのに、
なぜか結果はまったく同じ

でもこれは偶然でも裏技でもありません。
C言語の中では、ちゃんと一貫したルールに従って起きている、とても自然な出来事です。

今回は
「配列アクセスとポインタアクセスが同じ理由」を、
メモリの見方から、じっくりほどいていきましょう。

まずは動きを確かめてみよう

最初に、配列アクセスとポインタアクセスを両方使った簡単な例を見てみます。

プロジェクト名:10-4-1 ソースファイル名: sample10-4-1.c

#include <stdio.h>

int main(void)
{
    int numbers[3] = {7, 14, 21};
    int* p = numbers;

    printf("numbers[0]=%d\n", numbers[0]);
    printf("p[0]=%d\n", p[0]);
    printf("*p=%d\n", *p);

    return 0;
}

実行結果

numbers[0]=7
p[0]=7
*p=7

どの書き方でも、同じ値が表示されていますね。
ここからが本題です。

*p の意味をあらためて整理しよう

まずは、* 演算子の役割を確認します。

間接演算子 * のはたらき

int* 型のポインタ変数 p に、
仮に 3000 番地 が格納されているとします。

記述意味
p3000 番地
*p3000 番地を先頭とする int 型の変数(値を代入できる)

つまり *p とは、

「p に入っている番地を先頭とする変数を生み出す」

という操作でした。

今すぐ捨てるべき思い込み

ここで、これまで無意識に信じてきた常識を一度手放します。

捨てるべき常識

[] は、配列専用の要素アクセス演算子である。

たしかに今までは、
配列の要素にアクセスするために [] を使ってきました。

でも、それは
「配列でも使える」だけだったのです。

添え字演算子 [] の本当の正体

[] 演算子の正体は、次のようなものです。

添え字演算子の振る舞い

X* 型のポインタ変数 p にアドレス A が入っているとき、
p[N] は次の手順で評価されます。

手順内容
A 番地を先頭として X 型の変数が並んでいると仮定する。
その N 番目の場所に変数を生み出す。
その変数に化ける。

※ N は整数
※ p は void* 型ではない

図を使ってイメージすると

この後で、詳しく解説するので
ここでは、さっくりと、添え字によってアクセスされる記憶領域が変わるということだけを理解しておきましょう!

添え字によってアクセスされるメモリ領域

たとえば、int* 型のポインタ変数 p に 3000 番地が格納されている場合、p[0] と記述すると「3000 番地を先頭として並ぶ 0 番目の int 型変数」、つまり「3000 番地を先頭とする int 型変数」に化け、

p[1] と記述すると「3004 番地を先頭として並ぶ 1 番目の int 型変数」、つまり「3004 番地を先頭とする int 型変数」に化けます。

さらに、p[N] と記述すると「3000+4×N 番地を先頭として並ぶ N 番目の int 型変数」、つまり「3000+4×N 番地を先頭とする int 型変数」に化けるのです。

p[0] と *p が同じになる理由

int* 型のポインタ p に 3000 番地が入っているとき、

記述化けるもの
p[0]3000 番地を先頭とする int 型変数
*p3000 番地を先頭とする int 型変数

つまり、
p[0] も *p も、同じ番地に変数を生み出しているのです。

ここが重要ポイント

  • 演算子も [] 演算子も、
    任意の番地に変数を生み出す演算子

文脈は違っても、やっていることは非常によく似ています。

次の疑問 p[N] と *(p+N) はなぜ同じ?

ここまで理解できたら、自然と次の疑問が浮かびます。

「p[N] はわかるけど、*(p+N) とは同じにならないんじゃ?」

そこで登場するのが ポインタ演算です。

ポインタ演算という特別ルール

C言語では、ポインタに対する加算・減算は
型のサイズ単位で行われます。

ポインタ演算のルール

X* 型のポインタに加算や減算をすると、
アドレスは X 型のサイズ分ずつ変化する。

具体例

増加量
int*4 バイト
short*2 バイト
char*1 バイト

p[N] と *(p+N) が同じになる流れ

int* 型のポインタ p に 3000 番地が入っている場合を考えます。

記述評価結果
p[N]3000 + 4×N 番地を先頭とする int
p+N3000 + 4×N
*(p+N)3000 + 4×N 番地を先頭とする int

結果として、
p[N] と *(p+N) は完全に同じ場所を指すことになります。

2つのメモリアクセス手段の正体

ここまでの話をまとめると、次のようになります。

特殊構文のしくみまとめ

項目内容
* 演算子指定した番地に変数を生み出す。
[] 演算子指定した番地+オフセットに変数を生み出す。
ポインタ演算型サイズ単位で番地を移動する。

この3つが組み合わさることで、

配列アクセスとポインタアクセスは同じ動作になる

という結論にたどり着くのです。

なぜこの理解が重要なのか

この仕組みがわかると、

  • 配列とポインタの関係が一本につながる。
  • 関数引数での挙動が自然に理解できる。
  • 未定義動作やバグの原因を見抜きやすくなる。

といったメリットがあります。

ここは C言語理解の境界線 と言ってもいい場所です。