閉じる

【C言語】aは&a[0]か?a+iと*(a+i)で学ぶ配列とポインタ

配列名とポインタは見た目がよく似ているため、初学者が最初につまずきやすいテーマです。

本記事では配列名aは先頭要素のアドレスに「見える」ことa+iや*(a+i)の正しい意味、そしてaと&aの違いを、動くサンプルコードと出力で丁寧に確認します。

安全なアクセスと型の意識を軸に、混乱しない考え方も整理します。

配列名とポインタの基本

配列名aは先頭要素へのアドレスに見える

C言語では、配列名aは多くの式コンテキストで「先頭要素へのポインタ」に暗黙変換(decay)されます

つまりaは多くの場面で&a[0]と同じアドレス値を持つポインタのように振る舞います

ただし例外があり、sizeofのオペランドや&演算子のオペランドなどでは配列として扱われます。

次のプログラムで、アドレスと型の違いを観察します。

C言語
#include <stdio.h>

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

    // a は多くの式の中では「int *」(先頭要素のポインタ)に見える
    printf("a        = %p  (先頭要素a[0]へのポインタに見える)\n", (void*)a);

    // &a[0] は明確に「先頭要素のアドレス」(int *) 
    printf("&a[0]    = %p  (先頭要素のアドレス)\n", (void*)&a[0]);

    // &a は「配列全体のアドレス」型は int (*)[5]
    printf("&a       = %p  (配列全体のアドレス: 型は int (*)[5])\n", (void*)&a);

    // 1要素先のアドレスと、配列丸ごと1個先のアドレスの差を比較
    printf("a + 1    = %p  (a[1] へのポインタ)\n", (void*)(a + 1));
    printf("&a + 1   = %p  (配列aを丸ごと1個進める: 要素5個分先)\n", (void*)(&a + 1));

    // sizeof での違い
    printf("sizeof(a)      = %zu (配列全体のバイト数)\n", sizeof(a));
    printf("sizeof(&a[0])  = %zu (ポインタのサイズ)\n", sizeof(&a[0]));
    printf("sizeof(&a)     = %zu (配列ポインタのサイズ)\n", sizeof(&a));

    return 0;
}

出力例(環境によりアドレスとポインタサイズは変わります):

a        = 0x7ffc9b2c8a40  (先頭要素a[0]へのポインタに見える)
&a[0]    = 0x7ffc9b2c8a40  (先頭要素のアドレス)
&a       = 0x7ffc9b2c8a40  (配列全体のアドレス: 型は int (*)[5])
a + 1    = 0x7ffc9b2c8a44  (a[1] へのポインタ)
&a + 1   = 0x7ffc9b2c8a54  (配列aを丸ごと1個進める: 要素5個分先)
sizeof(a)      = 20 (配列全体のバイト数)
sizeof(&a[0])  = 8  (ポインタのサイズ)
sizeof(&a)     = 8  (配列ポインタのサイズ)

アドレス値そのものは同じでも、a&aの「型」は別物である点が重要です。

aは&a[0]か? 等価に使える場面が多い

多くの式コンテキストでa&a[0]と同じint として扱われ、関数に渡す、ポインタ演算をする、といった使い方が等価になります。

例えば、どちらもintを受け取る関数に渡せます。

C言語
#include <stdio.h>
#include <stddef.h>

int sum(const int *p, size_t n) {
    int s = 0;
    for (size_t i = 0; i < n; ++i) s += p[i];
    return s;
}

int main(void) {
    int a[5] = {10, 20, 30, 40, 50};
    // a も &a[0] も実引数として等価に使える
    printf("sum(a, 5)       = %d\n", sum(a, 5));
    printf("sum(&a[0], 5)   = %d\n", sum(&a[0], 5));
    return 0;
}
実行結果
sum(a, 5)       = 150
sum(&a[0], 5)   = 150

ただしaは「配列」という実体であり、ポインタ変数ではありません

後述するようにa = ...と代入したりa++のようにインクリメントすることはできません。

aと&aは別物

aは「先頭要素へのポインタに見える」一方、&aは「配列全体のアドレス」で、型はint (*)[N](配列ポインタ)です。

数値としては同じアドレスを指しますが、ポインタ演算の単位が違います

a + 1は要素1個分進みますが、&a + 1は配列丸ごと1個分進みます。

型が違うため、代入や関数引数の一致性にも影響します

int **int (*)[N]は全く別物ですので、混同しないようにしましょう。

a+iと*で学ぶ配列アクセス

a+iはi個先の要素のアドレス

ポインタ演算ではa + iは「i個先の要素のアドレス」を表します。

進む幅は型のサイズに比例し、intなら4バイト環境で4、doubleなら8バイト環境で8のように自動的に調整されます。

C言語
#include <stdio.h>

int main(void) {
    int a[5] = {10, 20, 30, 40, 50};
    for (int i = 0; i < 5; ++i) {
        printf("i=%d: &a[%d]=%p, a+%d=%p\n",
               i, i, (void*)&a[i], i, (void*)(a + i));
    }
    return 0;
}
実行結果
i=%d: &a[0]=0x7ffc8f3f1a40, a+0=0x7ffc8f3f1a40
i=%d: &a[1]=0x7ffc8f3f1a44, a+1=0x7ffc8f3f1a44
i=%d: &a[2]=0x7ffc8f3f1a48, a+2=0x7ffc8f3f1a48
i=%d: &a[3]=0x7ffc8f3f1a4c, a+3=0x7ffc8f3f1a4c
i=%d: &a[4]=0x7ffc8f3f1a50, a+4=0x7ffc8f3f1a50

*は中身 a[i]と同じ意味

*(a + i)a[i]と同じ意味です。

[]演算子は実は*(ptr + index)の糖衣構文です。

C言語
#include <stdio.h>

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

    for (int i = 0; i < 5; ++i) {
        int via_index = a[i];       // 添字でアクセス
        int via_ptr   = *(a + i);   // ポインタ算術 + 間接参照でアクセス
        printf("a[%d]=%d, *(a+%d)=%d\n", i, via_index, i, via_ptr);
    }

    // 参考: i[a] も a[i] と同じ (式としては可能だが可読性が低いので非推奨)
    printf("a[2]=%d, 2[a]=%d (同じ)\n", a[2], 2[a]);
    return 0;
}
実行結果
a[0]=10, *(a+0)=10
a[1]=20, *(a+1)=20
a[2]=30, *(a+2)=30
a[3]=40, *(a+3)=40
a[4]=50, *(a+4)=50
a[2]=30, 2[a]=30 (同じ)

範囲外アクセスは未定義動作に注意

配列の有効範囲外を指すポインタを間接参照することは未定義動作です。

例えば要素数5の配列にa[5]*(a + 5)でアクセスするのはダメです。

動くように見えても、クラッシュやデータ破壊を引き起こします。

安全のため、常にインデックスの範囲チェックを行いましょう。

C言語
#include <stdio.h>
#include <stddef.h>
#include <stdbool.h>

bool safe_at(const int *a, size_t n, size_t i, int *out) {
    if (i >= n) return false; // 範囲外は拒否
    *out = a[i];
    return true;
}

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

    // 正しいアクセス
    if (safe_at(a, 5, 4, &v)) printf("a[4]=%d\n", v);

    // 間違い(未定義動作の例) — 実際にアクセスしない安全なデモ
    size_t bad = 5;
    printf("bad index=%zu -> 未定義動作なのでアクセスしません\n", bad);
    return 0;
}
実行結果
a[4]=50
bad index=5 -> 未定義動作なのでアクセスしません

配列とポインタの違い

配列名は代入不可 ポインタ変数は代入可

配列名は「非修飾の左辺値」ではないため代入できません

一方、ポインタ変数は代入やインクリメントが可能です。

C言語
#include <stdio.h>

int main(void) {
    int a[3] = {1, 2, 3};
    int b[3] = {4, 5, 6};
    int *p = a;     // OK: p はポインタ変数、a は先頭要素へのポインタに見える
    p = b;          // OK: p に別の配列の先頭アドレスを代入
    p++;            // OK: 次の要素へ

    // a = b;       // コンパイルエラー: 配列名は代入できない
    // a++;         // コンパイルエラー: 配列名はインクリメントできない
    (void)p;
    return 0;
}

「aはポインタのように振る舞う」が「a自身がポインタ変数ではない」ことを区別して覚えましょう。

sizeofの結果が違う

sizeof(a)は配列全体のバイト数sizeof(p)はポインタのサイズです。

64ビット環境ではポインタは通常8バイトです。

C言語
#include <stdio.h>

void inside(int arr[5]) {
    // 関数引数の配列形は int * に退化する(型は int*)
    printf("inside: sizeof(arr) = %zu (ポインタのサイズ)\n", sizeof(arr));
}

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

    printf("outside: sizeof(a)  = %zu (配列全体のサイズ)\n", sizeof(a));
    printf("outside: sizeof(p)  = %zu (ポインタのサイズ)\n", sizeof(p));
    printf("outside: sizeof(&a) = %zu (配列ポインタのサイズ)\n", sizeof(&a));

    inside(a); // a は関数に渡ると int* として扱われる
    return 0;
}
実行結果
outside: sizeof(a)  = 20 (配列全体のサイズ)
outside: sizeof(p)  = 8  (ポインタのサイズ)
outside: sizeof(&a) = 8  (配列ポインタのサイズ)
inside: sizeof(arr) = 8  (ポインタのサイズ)

関数引数では配列はポインタに見える

関数引数でint arr[]int arr[5]と書いても、実際の引数の型はint *に退化(decay)します

そのため、sizeof(arr)で配列の大きさは取得できません。

サイズが必要なときは必ず個数nを別引数で渡すのが定石です。

また、2次元配列などでは退化の扱いがさらに重要になりますが、本記事の範囲を超えるため別記事で扱います。

まず覚える書き方と考え方

インデックス表記a[i]を基本にする

初学者はまずa[i]を基本の書き方にしましょう。

*(a + i)と同じ意味ですが、可読性が高くバグを生みにくいからです。

ポインタの足し算は「アドレスを動かす」ために必要な時だけ使います。

&と*の対応でアドレスを理解する

&は「アドレスを取る」、*は「アドレスから中身を取り出す」という対応で覚えると混乱しにくいです。

例えば

  • p = &a[i]は「i番目の要素のアドレスをpに入れる」
  • *pは「pが指す要素の値」

この対応が分かれば、a[i] == *(a + i)&a[i] == a + iが自然に理解できます。

型を意識する

「何のポインタか」という型がポインタ演算と代入可否を決めます

特にa&a[0]&aは同じアドレス値に見えても型が違います。

次の表で要点を整理します。

型(代表例)意味演算の単位
aint[5] (式中では多くの場合 int*)先頭要素のアドレスに退化要素1個分
&a[0]int*先頭要素のアドレス要素1個分
&aint (*)[5]配列全体のアドレス配列1個分
a + iint*i個先の要素のアドレス要素1個分
*(a + i)inti個先の要素の値なし
a[i]inti番目の要素の値なし
sizeof(a)size_t配列全体のバイト数なし
sizeof(&a[0])size_tポインタのサイズなし

アドレス値が同じでも、型が違えば意味が違う点を常に意識しましょう。

これがバグ回避の近道です。

まとめ

本記事では配列名aは多くの場面で&a[0]に等価なポインタに見えること、a + iはi個先の要素のアドレス、*(a + i)はその中身でa[i]と同じであることを、動作と出力で確認しました。

さらにa&aは型が異なり、ポインタ演算の単位も違うため、混同は禁物です。

配列とポインタの違いは代入可否、sizeofの結果、関数引数での退化に表れます。

最初はa[i]を基本にし、必要なときだけ&*でアドレスと中身を対応づけて考えましょう。

最後にもう一度強調しますが、範囲外アクセスは未定義動作です。

サイズは別引数で渡し、型の違いを意識して安全なコードを書くことが、配列とポインタを使いこなす最短ルートです。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!