C言語の配列とポインタの使い方~この2つの関係性について~

329, 2021-09-23

目次

ポインタと配列の使い方

C言語にはポインタ配列があります。
これら2つはいっしょに使うことが出来ます。
具体的にどう使うのか、その関係性についてこの記事で解説します。

結論から言うとポインタに配列を代入して使うこと多いです。
ポインタへの配列の代入方法、使用方法についても解説していきます。

関連記事
C言語のポインタのポインタを解説
プログラミングのポインタをわかりやすく解説【C言語】
C言語のポインタのメリットとは?コピーしますかメモリを共有しますか
C言語の配列の使い方

ポインタと配列の関係性は?

C言語のポインタと配列の関係はどういったものでしょうか?
この2つは切っても切れない関係性を持っています。
というのも、ポインタと配列は一緒に使うことが多いのです。

ポインタは配列を便利に扱うために存在します。
具体的にはどういうことなのでしょうか?

配列はポインタ変数に代入できる

配列変数はポインタ変数に代入することができます
つまり配列をポインタに保存できるわけです。
ということはポインタを通して配列を使うことが可能になるわけです。

    int a[] = {1, 2};
    int *b = a;  // ok

これは実際に可能で、ポインタから代入された配列にアクセスすることができます。

配列変数にポインタは代入できない

逆に配列変数にはポインタは代入できません
例えば↓のようなコードはだめです。

    int a[] = {1, 2};
    int *b;
    a = b;  // error: assignment to expression with array type

配列は配列型であってポインタ型ではありません。
そのためポインタや他の変数のアドレスを代入することはできません。

ポインタには配列のアドレスが保存される

ポインタには配列の何が保存されるのかと言うと、配列のアドレスが保存されます。
配列を参照することで配列のアドレスを取得することが可能です。
ポインタ変数には配列のアドレスを代入し、ポインタ変数はそのアドレスを参照して配列にアクセスします。
つまりポインタから配列の要素の参照や要素への代入などの処理は、配列のアドレスを参照して行っているということになります。

逆に言うと、この配列のアドレスが間違っているとポインタ変数は機能しません。
ちゃんと存在する配列のアドレス、それもメモリの範囲内のアドレスである必要があります。

配列のアドレスはどうやって確認するか?

ポインタ変数に代入する配列のアドレスはどうやって確認するのでしょうか?
その前に配列の仕組みですが、配列は連続したメモリ領域に確保されます。
つまり配列の先頭のアドレスがわかれば、配列の各要素にもアクセスできるということになります。
配列の先頭アドレスは、ただ単に配列変数を参照することで確認することができます。

配列の先頭アドレスを表示する

それでは配列の先頭アドレスを表示してみましょう。
↓のコードで確認できます。

#include <stdio.h>

int main(void) {
    int a[] = {1, 2};

    printf("先頭アドレス = %p\n", a);
    // 先頭アドレス = 0x7fff934198d8

    return 0;
}

↑のコードで出力される配列aのアドレスは、環境によって変わります。
printf()の出力指定子%pを使うとアドレスを出力できます。
↑のように配列の先頭アドレスは、ただ単に配列変数を参照するだけで確認できます。

配列の要素のアドレスを表示する

では次に配列の要素のアドレスを出力してみましょう。

#include <stdio.h>

int main(void) {
    int a[] = {1, 2};

    printf("先頭アドレス = %p\n", a);
    // 先頭アドレス = 0x7ffdffb0dfc8

    printf("要素[0]のアドレス = %p\n", &a[0]);
    // 要素[0]のアドレス = 0x7ffdffb0dfc8

    printf("要素[1]のアドレス = %p\n", &a[1]);
    // 要素[1]のアドレス = 0x7ffdffb0dfcc

    printf("(a + 1)のアドレス = %p\n", (a + 1));
    // (a + 1)のアドレス = 0x7ffdffb0dfcc

    return 0;
}

↑のように&a[0]とすると配列の先頭アドレス(0番目の要素)にアクセスできます。
そして&a[1]とすると配列の2つ目の要素のアドレスにアクセスできます。
(a + 1)とした場合は同様に2つ目の要素のアドレスにアクセス可能です。

つまり&a[1](a + 1)は同じ意味になります。
C言語ではこの2つの書き方をどちらも使うことが出来ます。

この方法で取得できるアドレスはポインタ変数に代入することが出来ます。
その場合は、たとえば配列の2番目の要素のアドレスをポインタに代入したとします。
そうするとそのポインタからは2番目の要素を基点に配列にアクセスできるようになります。

ポインタに配列を代入する

では実際にポインタ変数に配列を代入します。
↓のコードで確認できます。

    int a[] = {1, 2};
    int *b = a;  // ok

配列aのアドレスをポインタ変数bに代入しています。
こうすることでポインタ変数bから配列aにアクセスすることができるようになります。

配列とポインタの型は同じにしておく必要があります。
↑の例で言うと、配列aint型の配列です。
そのためポインタ変数bint型のポインタにしています。

例外としてはvoid型のポインタです。
void型のポインタはあらゆる型を代入することが出来ます。

配列の要素のアドレスをポインタに代入する

次に配列の要素のアドレスをポインタ変数に代入してみたいと思います。

    int a = {1, 2};
    int *b = &a[1];

↑の場合、配列aの添え字1の要素(先頭から2番目)のアドレスをポインタ変数bに代入しています。
こうするとポインタ変数bには配列aの2番目の要素のアドレスが保存されます。
そのため配列bにアクセスしてb[0]のように参照すると、それは配列aの2番目の要素になります。

ポインタに代入した配列はどのように使うか?

配列を代入したポインタ変数はどのように使うのでしょうか?
なにかポインタ変数に代入することで使い方が変わるのでしょうか?
具体的に見ていきたいと思います。

基本的には配列と同じ使い方

配列をポインタ変数に代入した場合、そのポインタはほとんど配列と同じ振る舞いになります。
つまり添え字による要素の参照や要素への代入なども配列と同じように行えます。
注意点としてはsizeof演算子の挙動が変わるということです。

sizeofの挙動に注意

配列を代入したポインタ変数は、厳密に言うと配列型ではなくてポインタ型です。
ポインタ型を通して配列にアクセスしてるわけです。

配列をsizeofした場合、その結果は配列全体のバイト数になります。
しかしポインタ変数をsizeofした場合はポインタ変数のバイト数になります。

つまり配列全体のバイト数を取得しようとしてポインタをsizeofした場合、予想外の結果が返ってくることになります。
このように配列とポインタのsizeofの挙動は違いますので注意が必要です。

要素の参照

配列を代入したポインタから、配列の要素を参照したい場合です。
これは配列の場合と同じく角カッコと添え字を使って参照します。

#include <stdio.h>

int main(void) {
    int a[] = {1, 2};
    int *b = a;

    printf("%d\n", b[0]);  // 1
    printf("%d\n", b[1]);  // 2

    return 0;
}

↑の場合、配列aのアドレスをポインタ変数bに代入しています。
そしてポインタ変数bに角カッコと添え字でアクセスしています。
その結果、配列aの各要素にアクセスすることができて、要素の値を参照できています。

要素への代入

配列を代入したポインタから、配列の要素に代入する場合です。
これも配列と同じように書くことが出来ます。

#include <stdio.h>

int main(void) {
    int a[] = {1, 2};
    int *b = a;

    b[0] = 100;

    printf("b[0] = %d\n", b[0]);
    // b[0] = 100

    printf("a[0] = %d\n", a[0]);
    // a[0] = 100

    return 0;
}

↑の場合、ポインタ変数bには配列aのアドレスが代入されています。
b[0] = 100;とやってポインタ変数を通じて配列の1番目の要素の値を上書きします。
その結果、配列aの1番目の要素の値が書き変わります。

このようにポインタ変数を使うと配列の値を間接的に書き換えることができます。

sizeofによるサイズの確認方法

さきほども書きましたが配列とポインタのsizeofの挙動は違います。
ここが配列とポインタを扱う上でC言語のややこしいところです。
逆に言うとここを理解すれば配列とポインタを扱う上でややこしいバグを生まなくて済みます。

配列は配列全体のサイズを確認できる

sizeofで配列を参照するとその配列全体のサイズを得ることができます。

#include <stdio.h>

int main(void) {
    int a[100];

    printf("a[] size = %d bytes\n", sizeof(a));
    // a[] size = 400 bytes

    return 0;
}

↑の場合、配列aは要素数100のint型の配列です。
つまりsizeof(int) * 100が配列全体のサイズです。
結果は400バイトになります。

ポインタは配列のサイズを確認できない

ではポインタはどうでしょうか?
配列を代入したポインタ変数のサイズを確認してみます。

#include <stdio.h>

int main(void) {
    int a[100];
    int *b = a;

    printf("sizeof *b = %d bytes\n", sizeof(b));
    // sizeof *b = 8 bytes

    return 0;
}

筆者の環境では8バイトと出力されました。
これはポインタ変数のサイズが出力されています。
つまり、配列を代入したポインタをsizeofした場合、配列全体のサイズではなく、ポインタ変数のサイズが取得できるわけです。
ここは間違えやすいので注意が必要です。

番兵を持った配列とポインタ

配列を代入したポインタはsizeofで配列全体のサイズを得ることができません。
そのためポインタから配列の長さを得て、for文で回すなどと言ったこともできません。
ポインタをfor文で回したい場合、配列に番兵を持たせておくと可能になります。

番兵とは配列内に保存する終端要素のことです。
たとえば↓のように使います。

#include <stdio.h>

int main(void) {
    int a[] = {1, 2, -1};
    int *b = a;

    for (int *p = b; *p != -1; p += 1) {
        printf("%d\n", *p);
    }

    return 0;
}

↑の場合、配列aに保存されている-1という要素が番兵です。
for文でポインタを回す時に、この-1をチェックしてfor文の終了条件にします。
こうするとポインタをインクリメントしていって配列の各要素にアクセスすることができるようになります。

番兵を使った場合、ポインタ変数単体でforループを回すことができるようになります。
番兵を使わない場合は配列の長さを別で取得しておく必要があります。

関連記事
C言語の配列の要素数を得る方法

constを付けた配列とポインタ

配列とポインタにはconstを付けることができます。
constを付けた配列はconstを付けたポインタに保存可能です。

    const int a[] = {1, 2};
    const int *b = a;

おわりに

今回はC言語の配列とポインタの使い方とその関係性について見てきました。
ポインタは配列にアクセスできますが、sizeofの挙動が変わるなど一部注意が必要になります。

配列とポインタはマブダチ

バグりそうなやつは大体友達