C言語基礎|間接演算子と添字演算子

*a[i] の正体は (a+i)。添字演算子は、間接演算子とポインタ算術の“省略記法”だよ。

配列を扱うとき、ふだんは a[2] みたいに書きますよね。
でもC言語では、これが実は

  • a + 2 で「2個先の要素を指すポインタ」を作り
  • *(a + 2) で「その要素そのもの」を取り出す

という2段階の考え方で成り立っています。

そして添字演算子 [] は、この考え方をサクッと書けるようにした“省略表現”です。
ここが分かると、ポインタ p でも p[2] が書ける理由まで一気に腑に落ちますよ。

まず結論:*(p+i) と p[i] は同じ要素を指す

p が配列の要素を指しているとき、次の対応が成り立ちます。

アクセス式(値を読む/書く式)の等価関係

アクセスしたい要素代表的な書き方同じ意味の書き方
i 個後ろの要素a[i]*(a + i)
i 個後ろの要素p[i]*(p + i)

ここで大事なのは、a も p も「要素を指すポインタとして扱える」という点です。

添字演算子 [] の正体(表記上の可換性)

添字演算子は、ざっくり言うとこうです。

p[i] は *(p+i) のこと

つまり p[i] は「p が指す要素から i 個進んだところを参照外ししてね」という意味なんです。

p+i と *(p+i) の役割

p + i  : i個後ろの要素へのポインタ(住所)
*(p+i) : その要素そのもの(値の箱)
  • p+i は「住所」
  • *(p+i) は「箱(値そのもの)」

この2段階を短く書けるのが p[i] です。

サンプルプログラム

4通りの書き方で同じ要素にアクセスできることを確認するプログラム例です。

プロジェクト名:chap10-11-1 ソースファイル名:chap10-11-1.c

#include <stdio.h>

int main(void)
{
    int a[5] = {7, 14, 21, 28, 35};
    int *p = a;   // p は a[0] を指す

    puts("同じ要素を4通りの書き方で読みます。");

    for (int i = 0; i < 5; i++) {
        printf("i=%d : a[i]=%d  *(a+i)=%d  p[i]=%d  *(p+i)=%d\n",
               i, a[i], *(a + i), p[i], *(p + i));
    }

    puts("次は p[2] に 999 を代入してみます。");
    p[2] = 999;

    printf("結果: a[2]=%d  *(p+2)=%d\n", a[2], *(p + 2));

    return 0;
}

さっきのコードを表で読み解く

4通りのアクセス式は「同じ要素の別名」

書き方意味i=2 のとき
a[i]配列の i 番目の要素a[2]
*(a+i)先頭から i 個後ろを参照外し*(a+2)
p[i]p から i 個後ろを参照外し(省略)p[2]
*(p+i)p から i 個後ろを参照外し*(p+2)

表のポイントは、「添字演算子は参照外しを含む」ということです。
p+i はポインタ(住所)だけど、p[i] は値(箱)にアクセスします。

a[2] を中心に図で整理

a が先頭要素を指すポインタとして解釈され、p も a[0] を指すとき、a[2] はこう表せます。

a[2] のエイリアス(別名)が増えていく

a は &a[0] と同じ意味になりやすい

a + 2   ----->  &a[2](3番目の要素の住所)
*(a+2)  ----->  a[2](要素そのもの)

p = a なので p も &a[0] を指す
p + 2   ----->  &a[2]
*(p+2)  ----->  a[2]
p[2]    ----->  *(p+2) と同じ -> a[2]

説明

  • a+2 と p+2 は「a[2] の住所」
  • (a+2)、(p+2)、p[2] は「a[2] そのもの(別名)」
  • つまりアクセス式は全部同じ要素に到達します

ポインタで配列っぽく書ける理由(p は配列そのもののように振る舞う)

p が a[0] を指しているとき、p は配列 a のように扱えます。

ポインタ p が配列っぽく振る舞う例

目的配列で書くポインタで書く
i番目の値を読むa[i]p[i]
i番目の値に書くa[i] = 10p[i] = 10
i番目の住所を得る&a[i]p + i

ここが「表記上の可換性」と呼ばれる感覚です。

有効なポインタは要素数+1個(番兵として使える)

配列 a が n 要素なら、要素は a[0]〜a[n-1] の n 個です。
でも、要素へのポインタとしては a+n(= &a[n])まで有効とされます。

配列要素と有効ポインタの範囲

種類有効範囲個数
要素(アクセスできる箱)a[0]〜a[n-1]n
要素へのポインタ(住所として有効)&a[0]〜&a[n]n+1

ポイントはこれです。

  • &a[n] は「末尾の1個後ろ」を指す正しいポインタとして作れる
  • ただし *(&a[n]) を使って値を読んだり書いたりするのはダメ(要素が存在しない)

添字はオフセット(0から始まる理由)

Cの添字 i は「先頭から何要素ぶん進むか」を表す オフセットです。
だから先頭は必ず 0 になります。

添字 = 何個進むか

a[0] : 0個進む
a[1] : 1個進む
a[2] : 2個進む

ポインタ同士の加算はできない(減算はできる)

  • p + i はOK(整数を足す)
  • p + q はNG(ポインタ同士の加算は意味がない)
  • 同じ配列内なら p - q はOK(何要素ぶん離れているかが分かる)

ポインタ演算の可否

可否意味
p + 1OK1要素先へ
p - 1OK1要素手前へ
p + qNG意味が定義されない
p - q条件付きでOK同じ配列内なら要素差

登場する命令(関数)と書式・何をする命令なのか

puts

  • 書式:puts(文字列);
  • 何をする?:文字列を表示して改行も付ける。

printf

  • 書式:printf(書式文字列, 引数1, 引数2, ...);
  • 何をする?:書式に従って表示する(%d で整数など)

使った表や図の説明(まとめ)

  • 4通りのアクセス表は a[i] と *(a+i) と p[i] と *(p+i) が同じになることの整理です。
  • 役割図は p+i が住所、*(p+i) が箱(値)という2段階を分けて理解するための図です。
  • a[2] の図は「別名(エイリアス)」が増えていく流れを見える化したものです。
  • 有効ポインタ範囲の表は &a[n] が番兵として使える理由を整理しています。