C言語基礎|添字演算子のオペランド

*p[i] は (p+i)。だから i[p] も同じ意味。Cの添字は“左右入れ替えOK”な2項演算子だよ。

添字演算子 [] は、配列アクセスのための“特別な記法”に見えますよね。
でもC言語では、[] はれっきとした 2項演算子で、仕組みはとても素直です。

ポイントはこの2つです。

  • 添字演算子の正体は *(p+i)
  • p+i と i+p が同じなので、p[i] と i[p] も同じになる

「え、i[p] って書けるの?」となるところが、この節の面白さです。
ただし、読みにくいので実務では基本使いません(仕組み理解のための知識として持つのが◎)。

添字演算子 [] は2項演算子(オペランドは2つ)

添字演算子は、次の2つのオペランドを取ります。

  • 片方:Type 型オブジェクトへのポインタ(Type * の値)
  • もう片方:整数型(添字)

そして結果は Type 型の要素そのもの(エイリアス)です。

添字演算子のオペランドと結果

形式左オペランド右オペランド生成されるもの
p[i]Type *(要素へのポインタ)整数 iType(要素そのもの)
i[p]整数 iType *(要素へのポインタ)Type(要素そのもの)

「順序が任意」というのが、この記事の核心です。

なぜ p[i] と i[p] が同じなの?(正体を展開すると一発)

まず、添字の正体を思い出します。

  • p[i] は *(p+i)

そして、加算は可換なので

  • p+i と i+p は同じ

だから

  • *(p+i) と *(i+p) は同じ
  • よって p[i] と i[p] は同じ

等価関係の流れ

p[i]
  = *(p + i)
  = *(i + p)
  = i[p]

8通りの“同じ要素アクセス”を整理

配列名 a も多くの式で先頭要素へのポインタとして扱えるので、同じ要素を指す書き方が増えます。

同じ要素を表す8通り(p が a[0] を指すとき)

種類書き方
配列の添字a[i] / i[a]
配列のポインタ算術 + 間接*(a+i) / *(i+a)
ポインタの添字p[i] / i[p]
ポインタ算術 + 間接*(p+i) / *(i+p)

この表は「[] は *( + ) の省略」という理解を、見える形にしたものです。

サンプルプログラム(内容・メッセージを変更した例)

ここではよりシンプルな、p[i] と i[p] が同じ要素になるプログラム例です。

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

#include <stdio.h>

int main(void)
{
    int a[4] = {5, 6, 7, 8};
    int *p = a;   // p は a[0] を指す

    puts("p[i] と i[p] が同じ値になることを確認します。");

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

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

    printf("a[2]=%d  *(p+2)=%d\n", a[2], *(p + 2));
    return 0;
}

プログラムの中身を表で分解して理解する

各式が意味するもの

意味ポイント
pa[0] を指すポインタp は住所(アドレス)
p+ii 個後ろの要素へのポインタi 要素ぶん進む
*(p+i)その要素そのもの参照外し
p[i]*(p+i) と同じ添字は省略表現
i[p]*(i+p) と同じオペランド順は任意

でも i[p] は使うべき?(理解用の知識)

i[p] が動くのは仕様として正しいですが、読み手に優しくないです。
実務では p[i] を使うのが自然です。

おすすめ表記

目的おすすめ理由
要素アクセスp[i]読みやすい・一般的
仕組みの確認*(p+i)ポインタ算術を理解しやすい
ネタ・理解確認i[p]仕様理解の確認用(普段は避ける)

配列の受け渡し:関数引数の配列 v[] は実はポインタ

ここからが超重要な実戦ポイントです。
関数の仮引数で

  • int v[]
  • int v[10]

みたいに書けても、Cの規則で *最終的に int v として扱われます
要素数を書いても、仮引数としては“無視される”イメージです。

関数引数の配列宣言は最終的にポインタ扱い

書き方見た目実際の扱い
void f(int v[])配列っぽいv は int *
void f(int v[10])要素数指定っぽいv は int *(10は無視される)
void f(int *v)ポインタv は int *

この表は「配列を渡す=先頭要素へのポインタを渡す」というカラクリを整理しています。

サンプル(配列を受け取る関数)を、別の例に変更して解説

先頭 n 個を 1,2,3,... にする関数を用いたプログラム例です。

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

#include <stdio.h>

void fill_1toN(int v[], int n)
{
    for (int i = 0; i < n; i++)
        v[i] = i + 1;
}

int main(void)
{
    int a[5] = {0, 0, 0, 0, 0};

    puts("配列の先頭から順に 1,2,3,... を入れます。");
    fill_1toN(a, 5);

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

    return 0;
}

関数呼び出し時のイメージ

呼び出し側: a(= &a[0] 相当)を渡す
            + 要素数 n も渡す

呼び出し先: v はポインタとして受け取り
            v[i] で要素を書き換える

なぜ要素数 n を渡す必要がある?

受け渡しているのは配列そのものではなく、先頭要素へのポインタだけだからです。
関数側だけでは「何個ある配列なのか」が分からないので、n を別引数にします。

演習問題

演習10-4:添字と同じ値を入れる関数

要素型が int 型で要素数が n の領域を受け取り、全要素に添字と同じ値を代入する関数 set_index を作成せよ。
関数プロトタイプ:
void set_index(int *v, int n);

解答例

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

#include <stdio.h>

void set_index(int *v, int n)
{
    for (int i = 0; i < n; i++)
        v[i] = i;
}

int main(void)
{
    int a[6] = {0};

    puts("各要素に添字と同じ値を入れます。");
    set_index(a, 6);

    for (int i = 0; i < 6; i++)
        printf("a[%d] = %d\n", i, a[i]);

    return 0;
}

解説
v は配列ではなくポインタとして受け取ります。
でも v[i] は *(v+i) の省略なので、配列のようにアクセスできます。
呼び出し側で a を渡すと、a は先頭要素へのポインタとして渡るので、変更がそのまま a に反映されます。

演習10-5:途中から2要素だけ書き換えるとどうなる?

次の呼び出しを行うと、配列 a はどう変化するか。実行して確認し、理由を説明せよ。
fill_1toN(&a[2], 2);

ここで fill_1toN は先頭 n 個に 1..n を代入する関数とする。

解答例(確認用コード)

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

#include <stdio.h>

void fill_1toN(int v[], int n)
{
    for (int i = 0; i < n; i++)
        v[i] = i + 1;
}

int main(void)
{
    int a[6] = {10, 20, 30, 40, 50, 60};

    puts("部分配列っぽく、途中から2要素だけ書き換えます。");
    fill_1toN(&a[2], 2);

    for (int i = 0; i < 6; i++)
        printf("a[%d] = %d\n", i, a[i]);

    return 0;
}

想定される結果(例)

  • a[2] が 1 に
  • a[3] が 2 に
    他の要素はそのまま

解説
&a[2] は「a[2] を指すポインタ」です。
それを v として受け取るので、関数内の v[0] は a[2]、v[1] は a[3] を意味します。
つまり配列の途中から“2要素ぶん”を操作したことになります。
このテクニックは便利ですが、n を間違えると配列範囲外を書き換えるので要注意です。

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

puts

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

printf

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