C言語の基本|ポインタで配列を指す

配列の先頭をポインタでつかめば、連続するデータを自在にたどれる。

これまでに、ポインタへ変数のアドレスを代入し、その変数の値を間接的に参照したり変更したりする仕組みを学んできました。今度はその考え方を広げて、ポインタで配列の要素を扱う方法を見ていきましょう。

配列は、同じ型のデータをメモリ上に連続して並べる仕組みです。一方、ポインタはメモリ上の位置を表すアドレスを保存します。配列の先頭要素をポインタで指せば、その位置を基準にして2番目、3番目と順番に要素をたどれます。

C言語では、配列の要素を array[i] のように添字で指定するだけでなく、*(p + i) で参照したり、p++ でポインタ自身を次の要素へ進めたりできます。最初は別々の仕組みに見える配列とポインタですが、メモリ上の並びを意識すると自然につながって見えてきます。

ここでは、次の内容を順番に確認します。

  • 配列の先頭要素をポインタで指す方法
  • *(p + i) で任意の要素を参照する方法
  • p++ でポインタを移動しながら要素を読む方法
  • *(p + 1) と *p + 1 の違い
  • ストッパーまで配列を処理する方法
  • ポインタを使って区間ごとの合計を求める方法

この関係を理解すると、配列が単なる箱の集まりではなく、メモリ上に連続したデータとして見えるようになります。文字列や関数への配列の受け渡しを学ぶときにも役立つ重要な土台です。

配列の先頭要素をポインタで指す

1つの変数を指すときは、その変数のアドレスをポインタへ代入します。配列の場合も基本は同じですが、最初の基準になるのは配列の先頭要素です。

たとえば、次の配列を考えてみましょう。

int levels[] = {40, 55, 70, 65};

levels の4つの要素は、メモリ上に順番に並びます。配列名 levels は、多くの式の中で先頭要素 levels[0] を指すポインタへ変換されます。そのため、次のように代入できます。

int *p;
p = levels;

この代入は、次の書き方と同じ先頭要素を指します。

p = &levels[0];

したがって、p に入るのは配列の全データではなく、levels[0] のアドレスです。*p と書けば、そのアドレスに格納されている値40を取り出せます。

書き方意味
levels多くの式で先頭要素を指すポインタへ変換される
&levels[0]先頭要素のアドレス
p = levels;p に先頭要素を指すポインタを代入する
*pp が指す先頭要素の値を取り出す

ポインタが配列全体を内部に保存するわけではありません。p はまず先頭要素を指し、その場所を基準として後ろの要素へ進みます。

図1:ポインタが配列の先頭要素を指す仕組み

この図から分かること

配列名 levels を p に代入すると、p は先頭要素 levels[0] を指します。p に保存されるのは0x1000という場所の情報であり、40、55、70、65という配列の値を丸ごと保存するわけではありません。*p はpが指す場所の中身を参照するため、結果は40になります。

配列をポインタで扱う基本の流れ

配列をポインタで扱う手順は、次の3段階に整理できます。

手順操作
1配列要素と同じ型のポインタを宣言するint *p;
2配列の先頭要素を指すポインタを代入するp = levels;
3指している要素を参照または変更する*(p + 2)

int 型の配列なら int *、double 型の配列なら double *を使います。配列要素の型とポインタの型を一致させることで、ポインタを1つ進めたときに何バイト先へ移動すればよいかをコンパイラが判断できます。

*(p + i) で要素を順番に参照する

*(p + i) は、ポインタpそのものを変更せず、式の中だけで位置をずらして配列要素を参照する書き方です。

意味は次の3段階に分けると読みやすくなります。

  • p は先頭要素を指す
  • p + i は先頭からi個先の要素を指す
  • *(p + i) はi個先の要素の値を取り出す

たとえばpが temperatures[0] を指している場合、*(p + 2) は temperatures[2] と同じ要素を表します。

5日分の最高気温をポインタで表示するプログラム

ファイル名:11_5_1.c

#include <stdio.h>

int main(void)
{
    int temperatures[] = {24, 27, 29, 26, 25};
    int *p = temperatures;
    int element_count = sizeof temperatures / sizeof temperatures[0];

    for (int i = 0; i < element_count; i++) {
        printf("%d日目の最高気温: %d度  アドレス: %p\n",
               i + 1, *(p + i), (void *)(p + i));
    }

    return 0;
}
実行結果の例
1日目の最高気温: 24度  アドレス: 0x7ffd1000
2日目の最高気温: 27度  アドレス: 0x7ffd1004
3日目の最高気温: 29度  アドレス: 0x7ffd1008
4日目の最高気温: 26度  アドレス: 0x7ffd100c
5日目の最高気温: 25度  アドレス: 0x7ffd1010

実際に表示されるアドレスは、実行環境や実行するタイミングによって変わります。この例では int 型が4バイトの環境を想定し、違いが分かりやすい値で示しています。

このプログラムでは、pをtemperaturesの先頭に合わせています。for文のiが0のときは*(p + 0)、iが1のときは*(p + 1)となり、順番に各日の最高気温を読み取ります。

要素数は、配列全体の大きさを先頭要素1個の大きさで割って求めています。

int element_count = sizeof temperatures / sizeof temperatures[0];

この計算は、temperaturesが配列として宣言されているこの場所で行うから利用できます。表示するアドレスには%pを使い、引数を(void *)へ変換しています。

iポインタ式対応する配列要素
0*ptemperatures[0]24
1*(p + 1)temperatures[1]27
2*(p + 2)temperatures[2]29
3*(p + 3)temperatures[3]26
4*(p + 4)temperatures[4]25

ポインタに1を足すと次の要素へ進む

ポインタに1を足したとき、アドレスが必ず1バイトだけ増えるわけではありません。ポインタが指す型1個分だけ進みます。

ポインタの型p + 1で進む量
char *char 型1個分
int *int 型1個分
double *double 型1個分

たとえば int 型が4バイトの環境では、int *型のpに対するp + 1は、アドレスとして4バイト先を表します。double 型が8バイトの環境なら、double *型のポインタは8バイト先へ進みます。

このように型の大きさを自動的に考慮するため、プログラマーはバイト数を直接足さなくても、次の配列要素へ正しく移動できます。

図2:p + iで配列要素を参照する流れ

この図から分かること

pが先頭要素を指しているとき、p + 1、p + 2、p + 3は、それぞれ1個先、2個先、3個先の要素を指します。さらに*を付けると、その場所に格納された値を取り出せます。どの式を評価してもpに保存された先頭アドレス自体は更新されません。

*(p + 1) と *p + 1の違い

見た目がよく似ている次の2つの式は、括弧の位置によって意味が変わります。

処理の順序意味
*(p + 1)p + 1を計算してから間接参照する次の要素の値を取り出す
*p + 1*pで値を取り出してから1を足す現在の要素の値に1を加える

たとえば、配列の先頭要素が24、次の要素が27なら、結果は次のようになります。

*(p + 1)  /* 結果は27 */
*p + 1    /* 結果は25 */

は加算の+より優先順位が高いため、p + 1 ではpが先に評価されます。一方、(p + 1)では括弧内の p + 1が先に評価されます。ポインタを含む式は、どこまでがアドレスの計算で、どこで値を取り出しているのかを区切って読むことが大切です。

p++でポインタ自身を次の要素へ進める

*(p + i)はpの位置を保ったまま要素を選びます。これに対してp++は、pに保存されている位置そのものを次の要素へ進めます。

来場者数をポインタの移動で順番に表示するプログラム

ファイル名:11_5_2.c

#include <stdio.h>

int main(void)
{
    int visitors[] = {36, 42, 51, 47};
    int *p = visitors;
    int element_count = sizeof visitors / sizeof visitors[0];

    for (int i = 0; i < element_count; i++) {
        printf("第%d会場の来場者数: %d人  アドレス: %p\n",
               i + 1, *p, (void *)p);
        p++;
    }

    return 0;
}
実行結果の例
第1会場の来場者数: 36人  アドレス: 0x7ffd2000
第2会場の来場者数: 42人  アドレス: 0x7ffd2004
第3会場の来場者数: 51人  アドレス: 0x7ffd2008
第4会場の来場者数: 47人  アドレス: 0x7ffd200c

最初のpはvisitors[0]を指します。1回目のループで*pを表示したあと、p++によってpはvisitors[1]を指すようになります。同じ動きを繰り返し、最後まで順番に読み進めます。

ループ回数pが指す要素*pの値p++後に指す位置
1visitors[0]36visitors[1]
2visitors[1]42visitors[2]
3visitors[2]51visitors[3]
4visitors[3]47配列末尾の1つ先

最後のp++を実行すると、pは配列末尾の1つ先を指します。その位置を比較やループ終了の判定に使うことはできますが、*pで値を読み取ってはいけません。配列の有効範囲外を間接参照すると、未定義動作になります。

*(p + i) とp++を使い分ける

どちらの方法でも配列を先頭から順番に読めますが、処理後のpに違いがあります。

方法p自体の位置向いている処理
*(p + i)変化しない先頭位置を残したまま任意の要素を参照する処理
p++次の要素へ移動する先頭から終端まで順番に読み進める処理

処理後にも先頭位置が必要なら、*(p + i)を使うと分かりやすくなります。p++を使いたい場合は、先頭を指すポインタとは別に、走査用のポインタを用意する方法もあります。

int *first = visitors;
int *current = first;

currentだけを進めれば、firstは先頭を指したまま残せます。

配列の添字とポインタ式の対応

p = array;とした場合、配列の添字とポインタ式は次のように対応します。

配列要素要素のアドレス要素を指すポインタ値の参照
array[0]&array[0]p または p + 0*p
array[1]&array[1]p + 1*(p + 1)
array[2]&array[2]p + 2*(p + 2)
array[3]&array[3]p + 3*(p + 3)

また、配列名そのものを使って次のようにも書けます。

書き方意味
*arrayarray[0]の値
*(array + 1)array[1]の値
*(array + 2)array[2]の値

配列の添字とポインタ式は、同じメモリ上の要素を異なる形で指定しています。ただし、配列とポインタが完全に同じものという意味ではありません。配列名は多くの式で先頭要素を指すポインタへ変換されますが、配列そのものは要素を格納する領域です。また、配列名にarray++のような演算を行って先頭位置を変更することはできません。

実践問題:ストッパーまでの値を集計する

次の条件で、ポインタを使って測定値を集計してみましょう。

  • int 型の配列を用意する
  • 最後の-1をデータの終わりを表すストッパーにする
  • 添字を使わずにポインタを進める
  • 有効な測定値の合計値、平均値、最大値、最小値を求める

使用する配列は次のとおりです。

{22, 19, 25, 28, 21, 24, 20, -1}
ストッパーまでの室温データを集計するプログラム

ファイル名:11_5_3.c

#include <stdio.h>

int main(void)
{
    int room_temperature[] = {22, 19, 25, 28, 21, 24, 20, -1};
    int *p = room_temperature;
    int sum = 0;
    int count = 0;
    int max = *p;
    int min = *p;

    while (*p != -1) {
        sum += *p;

        if (*p > max) {
            max = *p;
        }

        if (*p < min) {
            min = *p;
        }

        count++;
        p++;
    }

    if (count > 0) {
        printf("合計値: %d度\n", sum);
        printf("平均値: %.2f度\n", (double)sum / count);
        printf("最大値: %d度\n", max);
        printf("最小値: %d度\n", min);
    } else {
        printf("集計できる測定値がありません。\n");
    }

    return 0;
}
実行結果の例
合計値: 159度
平均値: 22.71度
最大値: 28度
最小値: 19度

while (*p != -1)は、pが指している値が-1ではない間、処理を繰り返す条件です。ループ内では次の順番で処理します。

  1. *pをsumへ加算する
  2. *pが現在のmaxより大きければ最大値を更新する
  3. *pが現在のminより小さければ最小値を更新する
  4. countを1増やす
  5. p++で次の要素へ進む

pが最後の-1を指すとwhile文の条件が偽になり、-1を集計せずにループを終了します。平均値を求める式では、sumを(double)へキャストし、整数同士の除算にならないようにしています。

countが0より大きいことを確認しているのは、配列の先頭がストッパーだった場合の0除算を避けるためです。ただし、このプログラムではmaxとminを*pで初期化しているため、実際に利用するときは先頭に少なくとも1つの有効な測定値があるという条件も必要です。

図3:ストッパーまでポインタを進める処理

この図から分かること

pは配列の先頭から始まり、有効な値を1つ処理するたびにp++で次へ進みます。-1に到達した時点で条件が偽になるため、処理はそこで終了します。配列の要素数をループ条件に使う代わりに、特別な値を終端の目印として利用する方法です。

実践問題:2地点間の距離を表にする

次は、8つの給水所を結ぶサイクリングコースを考えます。隣り合う給水所間の距離をdouble型の配列へ格納し、任意の2地点間の距離をポインタで合計します。

距離の配列は次のとおりです。

{0.8, 1.5, 2.2, 1.1, 2.6, 1.7, 2.0}

配列の各要素は、次の区間に対応します。

配列要素区間距離
route[0]S1からS20.8 km
route[1]S2からS31.5 km
route[2]S3からS42.2 km
route[3]S4からS51.1 km
route[4]S5からS62.6 km
route[5]S6からS71.7 km
route[6]S7からS82.0 km
給水所間の距離をポインタで合計して表示するプログラム

ファイル名:11_5_4.c

#include <stdio.h>

int main(void)
{
    double route[] = {0.8, 1.5, 2.2, 1.1, 2.6, 1.7, 2.0};
    int station_count = 8;

    printf("        ");
    for (int i = 1; i <= station_count; i++) {
        printf("S%-6d", i);
    }
    printf("\n");

    for (int start = 0; start < station_count; start++) {
        printf("S%-6d", start + 1);

        for (int end = 0; end < station_count; end++) {
            if (end <= start) {
                printf("%-7s", "---");
            } else {
                double sum = 0.0;
                double *p = route + start;

                for (int k = start; k < end; k++) {
                    sum += *p;
                    p++;
                }

                printf("%-7.1f", sum);
            }
        }

        printf("\n");
    }

    return 0;
}
実行結果の例
S1     S2     S3     S4     S5     S6     S7     S8
S1     ---    0.8    2.3    4.5    5.6    8.2    9.9    11.9
S2     ---    ---    1.5    3.7    4.8    7.4    9.1    11.1
S3     ---    ---    ---    2.2    3.3    5.9    7.6    9.6
S4     ---    ---    ---    ---    1.1    3.7    5.4    7.4
S5     ---    ---    ---    ---    ---    2.6    4.3    6.3
S6     ---    ---    ---    ---    ---    ---    1.7    3.7
S7     ---    ---    ---    ---    ---    ---    ---    2.0
S8     ---    ---    ---    ---    ---    ---    ---    ---

routeに入っている値は各給水所の位置ではなく、隣り合う2地点の間の距離です。たとえばS2からS5までの距離は、S2からS3、S3からS4、S4からS5の3区間を足して求めます。

1.5 + 2.2 + 1.1 = 4.8

開始地点がstartで表されているため、走査用ポインタは次の式で対応する区間へ移動させます。

double *p = route + start;

内側のfor文では、startからendの直前までsum += *p;を行い、p++で次の区間へ進みます。endの位置まで足さないのは、地点番号と区間の数に1つ差があるためです。S2からS5ならroute[1]、route[2]、route[3]の3要素を合計します。

end <= startの範囲を---としているため、この表は出発地点より後ろにある給水所までの距離だけを表示します。この問題では、ポインタによる配列の走査と、どの範囲の要素を合計すべきかという考え方を同時に確認できます。

配列をポインタで扱うときの注意点

ポインタを使うと配列を柔軟に処理できますが、指している範囲や型をプログラマー自身が正しく管理する必要があります。

配列の範囲外を間接参照しない

p++を繰り返すと、やがて配列の末尾へ到達します。末尾の1つ先を指すポインタを作ることはできますが、そこには読み取ってよい配列要素がありません。*pで値を参照したり、*p = 値;で書き込んだりしてはいけません。

ループでは、次のどちらかを明確に決めておきましょう。

  • 要素数を数えて、その回数だけ処理する
  • ストッパーなどの終端条件を用意する

配列要素とポインタの型を合わせる

int型配列にはint *、double型配列にはdouble *を使います。型が合っているからこそ、p + 1が次の要素を正しく指し、*pが適切な大きさのデータを読み取れます。

配列の宣言対応するポインタの例
int values[4];int *p = values;
double prices[4];double *p = prices;
char word[10];char *p = word;

pの元の位置が必要か確認する

p++を使うとp自身が移動します。あとで配列の先頭へ戻る必要があるなら、次のいずれかの方法が使えます。

  • *(p + i)を使い、pを移動させない
  • 先頭を保存するポインタと、走査用のポインタを分ける
  • 必要な処理の前にp = array;で先頭を指し直す

括弧を含めて式を正確に読む

(p + 1)とp + 1のように、括弧の有無で処理対象が変わります。ポインタ式を読むときは、まずアドレスを計算している部分を確認し、そのあとどの位置に*を適用しているかを見ると理解しやすくなります。

ポインタで配列をたどるために覚えておきたいこと

覚えておきたいこと内容
配列名と先頭要素配列名は多くの式で先頭要素を指すポインタへ変換される
p = array;pがarray[0]を指す
*ppが現在指している要素の値を参照する
*(p + i)pを動かさず、i個先の要素を参照する
p++p自身を次の要素へ進める
*(p + 1)次の要素の値を参照する
*p + 1現在の要素の値に1を加える

配列を添字で見るとarray[0]、array[1]、array[2]という並びになります。ポインタで見るとp、(p + 1)、*(p + 2)という並びです。表現方法は異なりますが、どちらも同じ配列要素を参照しています。

最初は、紙に配列の箱を横一列に描き、その下へアドレスを書き加えてみると理解しやすくなります。pがどの箱を指しているのか、p + 1でどこへ進むのか、*を付けたときにどの値を取り出すのかを1つずつ追ってみてください。メモリ上の動きを思い浮かべられるようになると、配列とポインタを組み合わせたコードを落ち着いて読めるようになります。