C言語のきほん|ポインタで配列を指す

配列の先頭をつかめば、ポインタで並んだデータを自由にたどれる。

前の内容では、ポインタを使って1つの変数を指すしくみを見てきました。
変数のアドレスをポインタに入れて、その値を間接的に参照したり変更したりする、という流れでしたね。

ここからは、その考え方を少し広げて、ポインタで配列を指す方法を学んでいきます。

配列は、同じ型のデータがメモリ上に順番に並んでいる仕組みです。
そしてポインタは、メモリ上の場所、つまりアドレスを扱うための仕組みです。
この2つはとても相性がよく、C言語では配列をポインタで操作する場面がたくさん出てきます。

たとえば、配列の先頭要素をポインタで指すことができれば、そこから1つ先、2つ先、3つ先へと順番にたどっていけます。
このため、配列を扱うときには array[i] のような添字だけでなく、*(p + i) や p++ のような書き方もよく使われます。

最初は、配列とポインタは別々のものに見えるかもしれません。
でも、配列名が先頭要素のアドレスとして使えることや、ポインタに 1 を足すと次の要素へ進めることがわかると、両者のつながりがとても自然に見えてきます。

ここでは、

  • 配列をポインタで指す基本の考え方
  • *(p + i) で順番に要素を参照する方法
  • p++ でポインタ自身を更新しながら進む方法
  • *(p + 1) と *p + 1 の違い
  • 実践問題とチャレンジ問題

まで、表や図のイメージも交えながら、ていねいに整理していきます。

ポインタで配列を指す感覚がつかめると、C言語の配列操作がぐっと立体的に見えてきます。
この先の文字列、関数、メモリ操作の理解にもつながるので、ここでしっかり土台を作っていきましょう。

ポインタで配列を指すとは

変数をポインタで指すときは、1つの変数のアドレスをポインタに代入しました。
配列を指す場合も基本は同じです。違うのは、配列では先頭要素のアドレスを基準にするという点です。

たとえば、次のような配列があるとします。

int scores[] = {70, 85, 90, 78};

この配列は、メモリ上に 70、85、90、78 が順番に並んでいます。
そして配列名 scores は、多くの場面で先頭要素 scores[0] のアドレスとして扱われます。

そのため、次のように書けます。

int *p;
p = scores;

これは、次の書き方と同じ意味です。

p = &scores[0];

つまり、p は配列 scores の先頭要素を指すことになります。

この関係を表にすると、次のようになります。

書き方意味
scores先頭要素のアドレス
&scores[0]先頭要素のアドレス
p = scores;ポインタ p が先頭要素を指す
*p先頭要素の値

ここで大切なのは、ポインタが配列全体を丸ごと持つわけではないということです。
まずは先頭要素を指し、そこから順番に後ろの要素へ進んでいく、というイメージで考えるとわかりやすいです。

図:ポインタは配列の先頭要素を指す

この図では、配列 data の要素が横一列に並び、data[0]、data[1]、data[2]、data[3] と続いています。
ポインタ p には配列 data の先頭要素のアドレスが入っていて、data[0] を指しています。

この図で見てほしいのは、ポインタが最初から配列全部を一度に扱うのではなく、先頭を起点にしていることです。
そこからポインタ演算を使うことで、次の要素、その次の要素へと進めるようになります。

配列をポインタで扱う3つの流れ

配列をポインタで扱うときも、基本の流れはシンプルです。

  1. ポインタ型の変数を宣言する
  2. ポインタに配列の先頭要素のアドレスを代入する
  3. ポインタが指す配列要素の値を参照または変更する

表で整理すると、次のようになります。

手順内容
1ポインタを宣言するint *p;
2先頭要素のアドレスを代入するp = scores;
3要素を参照する*(p + 2)

変数1個を指す場合との違いは、配列では並んだ要素を順番にたどることができる点です。
そのため、ポインタ演算がとても重要になります。

*(p + i) で順番に要素を指す

まずは、ポインタ自身の中身は変えずに、式の中で位置だけをずらしながら要素を取り出す方法を見てみましょう。
これが *(p + i) という書き方です。

ここでは、4日分の読書ページ数を配列に入れて表示する例にします。

ファイル名:11_5_1.c

#include <stdio.h>

int main(void)
{
    int pages[] = {18, 25, 32, 20};   /* 4日分の読書ページ数 */
    int *p;                           /* int型の要素を指すポインタ */

    p = pages;                        /* 配列の先頭要素のアドレスを代入 */

    for (int i = 0; i < 4; i++) {
        printf("%d日目の読書量: %dページ\tアドレス: %p\n",
               i + 1, *(p + i), (void *)(p + i));
    }

    return 0;
}

このプログラムでは、p は pages[0] を指しています。
そして for 文の中で *(p + i) を使うことで、順番に各要素を表示しています。

対応関係は次のようになります。

ポインタ式対応する配列要素
*ppages[0]18
*(p + 1)pages[1]25
*(p + 2)pages[2]32
*(p + 3)pages[3]20

つまり、*(p + i) は「p が指している場所から i 個先の要素の値」を表します。

*(p + i) の読み方

*(p + i) は、慣れないうちは少し読みにくく感じるかもしれません。
でも、意味を分けて考えるとすっきりします。

まず p は先頭要素のアドレスです。
次に p + i は、そこから i 個先の要素のアドレスです。
最後に *(p + i) は、そのアドレスにある値を取り出すという意味です。

流れで書くと、こうなります。

  1. p は先頭要素を指す
  2. p + i で i 個先の要素の場所へ進む
  3. * でその場所の中身を取り出す

この考え方がわかると、配列の添字とポインタ式の関係も見えやすくなります。

配列の書き方ポインタの書き方
pages[0]*p
pages[1]*(p + 1)
pages[2]*(p + 2)
pages[3]*(p + 3)

ポインタに 1 を足すと何が起こるか

ここでとても大事なのが、ポインタに 1 を足しても 1バイトだけ増えるわけではないという点です。

int *p なら、p + 1 は int 型1個分だけ先へ進みます。
もし int 型が 4バイトなら、アドレスとしては 4バイト先へ進むことになります。

つまり、ポインタ演算では、単なる数値の足し算ではなく、型の大きさを考慮して進むわけです。

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

この性質があるからこそ、配列の次の要素、その次の要素へ自然に進めます。

図2 (p + i) で配列要素を順番に参照する

この図では、横一列に並んだ配列 values の各要素に対して、ポインタ p が先頭要素を指しています。
そこから p + 1、p + 2、p + 3 が順番に次の要素の位置を表し、*(p + 1)、*(p + 2)、*(p + 3) によって値を取り出す様子を示します。

この図のポイントは、ポインタ自身の中身は変えずに、式の中で位置をずらしていることです。
先頭位置をそのまま保ちながら、順番に要素を見ていくイメージがつかみやすくなります。

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

これはとてもよく出てくる大切なポイントです。
見た目は似ていますが、意味はまったく違います。

意味
*(p + 1)次の要素の値を取り出す
*p + 1先頭要素の値に 1 を足す

たとえば、先頭要素が 18、次の要素が 25 の場合を考えてみましょう。

  • *(p + 1) は 25
  • *p + 1 は 19

になります。

つまり、括弧の位置がとても大切です。

これは演算子の優先順位が関係しています。
*(p + 1) は、先に p + 1 を計算してから、その場所の値を取り出しています。
一方で *p + 1 は、まず *p で先頭要素の値を取り出してから、そこに 1 を足しています。

ポインタでは、括弧の有無で意味が大きく変わることがあるので、丁寧に読む習慣が大切です。

p++ で順番に要素を指す

次はもう1つの方法です。
今度は、ポインタそのものを進めながら配列をたどります。
このとき使うのが p++ です。

元の例を別の内容に変えて、今度は商品の在庫数を順番に表示するプログラムにしてみます。

ファイル名:11_5_2.c

#include <stdio.h>

int main(void)
{
    int stock[] = {12, 8, 15, 10};   /* 商品の在庫数 */
    int *p;                          /* int型の要素を指すポインタ */

    p = stock;                       /* 配列の先頭要素を指す */

    for (int i = 0; i < 4; i++) {
        printf("%d番目の在庫数: %d個\tアドレス: %p\n", i + 1, *p, (void *)p);
        p++;                         /* 次の要素へ進む */
    }

    return 0;
}

このプログラムでは、最初に p が stock[0] を指しています。
1回目のループで *p を表示したら、p++ によって p は stock[1] を指すようになります。
次のループでは stock[1]、さらにその次は stock[2]、最後は stock[3] を指します。

この方法の特徴は、ポインタ自身の位置が更新されることです。

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

どちらも配列を順番にたどれますが、考え方が少し違います。

方法特徴
*(p + i)ポインタ p 自体は動かさず、式の中で位置をずらす
p++ポインタ p 自体を次の要素へ進める

*(p + i) は、先頭位置をそのまま残しておきたいときに使いやすいです。
p++ は、先頭から順番に読み進めていく処理に向いています。

どちらもC言語ではよく使うので、両方の考え方に慣れておくことが大切です。

図:p++ でポインタを進めながら配列をたどる

この図では、配列 items の要素が横に並んでいて、最初はポインタ p が items[0] を指しています。
その後、p++ を1回行うごとに、p が items[1]、items[2]、items[3] へと順番に移動していきます。

この図で見てほしいのは、*(p + i) のように式の中で位置を変えるのではなく、ポインタ自身が次の要素へ動いていくところです。
配列を先頭から1つずつ読んでいくイメージをつかみやすい図です。

配列名と &array[0] の関係

配列では、配列名そのものが先頭要素のアドレスとして使えます。
これはとても重要な性質です。

たとえば、次の2つは同じ意味になります。

p = array;
p = &array[0];

どちらも、ポインタ p に配列の先頭要素のアドレスを代入しています。

この関係を表で整理すると、次のようになります。

書き方意味
array先頭要素のアドレス
&array[0]先頭要素のアドレス
*array先頭要素の値
*(array + 2)3番目の要素の値

この性質があるからこそ、配列名をそのままポインタに代入できるのです。

配列とポインタの対応を表で見る

配列 array に対して 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)

この表を見ると、配列の添字とポインタ式がきれいに対応していることがわかります。
配列を添字で見るか、ポインタの移動として見るかの違いだけで、見ている対象は同じです。

実践問題

元の問題と同じように、ストッパーの値までポインタを進めながら処理する実践問題を、別テーマで1つ作ります。

以下の手順でプログラムを作成してください。

① 任意の要素数の int 型配列を用意します。
② 配列の最後の要素には、終了を表すストッパーとして -1 が格納されているものとします。
③ ポインタを使って配列を先頭から順に調べ、次の値を求めてください。

  • 合計値
  • 平均値
  • 最大値
  • 最小値

配列の例は、次のようにします。

{14, 27, 35, 18, 42, 31, 9, -1}

解答例

ファイル名:11_5_3.c

#include <stdio.h>

int main(void)
{
    int data[] = {14, 27, 35, 18, 42, 31, 9, -1};  /* 最後の -1 はストッパー */
    int *p = data;                                 /* 先頭要素を指すポインタ */
    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++;
    }

    printf("合計値: %d\n", sum);
    printf("平均値: %.2f\n", (double)sum / count);
    printf("最大値: %d\n", max);
    printf("最小値: %d\n", min);

    return 0;
}

解説

このプログラムでは、p を配列 data の先頭に合わせたあと、while 文で *p が -1 になるまで処理を続けています。

while (*p != -1)

は、今 p が指している要素がストッパーではない間だけ処理を行うという意味です。

ループの中では、

  • sum に値を足す
  • max と min を更新する
  • count を増やす
  • p++ で次の要素へ進む

という流れになっています。

この問題のポイントは、添字 i を使わずに、ポインタそのものを進めながら配列全体を処理しているところです。
配列の長さを別に数で持たなくても、ストッパーの値を使って終わりを判断できるのが特徴です。

図:ストッパーまでポインタを進めながら処理する

この図では、配列 data の要素が横に並び、最後に -1 が置かれています。
ポインタ p は先頭から順番に右へ進み、各要素を読み取りながら、最後に -1 に到達したところで処理を終了します。

この図のポイントは、配列の終わりを要素数ではなくストッパーで判断していることです。
文字列の終端文字の考え方とも少し似ていて、ポインタで順番に進んでいくイメージをつかみやすいです。

実践問題

ハイキングコースのチェックポイントを題材にします。

以下のプログラムを作成してください。

① 8個のチェックポイントがあります。
② 隣り合うチェックポイント間の距離が double 型の配列に格納されています。
③ 配列の値は次の通りとします。

{1.2, 2.4, 1.8, 3.1, 2.0, 2.7, 1.9}

④ ポインタを使って、各チェックポイント間の距離を求め、表形式で表示してください。

たとえば、チェックポイント1からチェックポイント4までの距離は
1.2 + 2.4 + 1.8 = 5.4
のように計算します。

解答例

ファイル名:11_5_4.c

#include <stdio.h>

int main(void)
{
    double distance[] = {1.2, 2.4, 1.8, 3.1, 2.0, 2.7, 1.9};  /* 隣接地点間の距離 */
    int point_count = 8;                                       /* チェックポイント数 */

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

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

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

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

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

        printf("\n");
    }

    return 0;
}

解説

この問題では、distance 配列に入っているのは「各地点そのもの」ではなく、隣り合う地点どうしの距離です。
そのため、ある地点から別の地点までの距離を求めるには、その間にある値を順に足していく必要があります。

たとえば P2 から P5 までの距離なら、

  • P2→P3
  • P3→P4
  • P4→P5

の距離を合計します。

この解答例では、開始位置に応じて

double *p = distance + start;

としてポインタを移動させ、そのあと

sum += *p;
p++;

を繰り返して距離を合計しています。

この問題のよいところは、ポインタで配列を順番に読む練習だけでなく、
どこからどこまでの値を足せばよいかを考える練習にもなるところです。

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

ポインタで配列を扱うときは便利ですが、注意したいこともあります。

範囲外に進めない

p++ を続けて配列の最後を越えてしまうと、配列の外のメモリを指してしまいます。
これは危険なので、必ず正しい範囲や終端条件を意識することが大切です。

型を合わせる

int 型の配列なら int *、double 型の配列なら double * を使います。
ポインタの型と配列要素の型は一致させる必要があります。

元の位置を残したいかを意識する

p++ を使うと、ポインタ自身の位置が変わります。
あとで先頭位置が必要になるなら、別のポインタを使うか、*(p + i) の書き方を使うほうが扱いやすいことがあります。

図:配列とポインタ式の対応関係

この図では、配列 arr の各要素と、それに対応するポインタ式を並べて示します。
たとえば arr[0] と *p、arr[1] と *(p + 1)、arr[2] と *(p + 2) のように対応づけることで、添字によるアクセスとポインタによるアクセスが同じ対象を表していることが見えてきます。

この図のポイントは、配列とポインタがまったく別のものではなく、同じメモリ上の並びを違う書き方で見ているということです。
ここが理解できると、配列操作の見え方がかなり変わってきます。

ポインタで配列を指す感覚をつかもう

ポインタで配列を指す学習では、まず次のポイントをしっかり押さえることが大切です。

覚えたいこと内容
配列名は先頭要素のアドレスとして使えるp = array; と書ける
*(p + i) で i 個先の要素を参照できる添字と対応している
p++ でポインタ自身を次の要素へ進められる順番に読む処理で便利
*(p + 1) と *p + 1 は違う括弧の位置で意味が変わる

ここがわかると、今まで array[i] として見ていた配列を、メモリ上に順番に並んだデータの流れとして見られるようになります。

C言語では、配列とポインタはとても深く結びついています。
その関係が見えてくると、コードがただの記号の並びではなく、メモリの上でどう動いているかまで感じ取れるようになります。
ここをしっかり理解しておくと、この先の学習がかなり楽になります。