6日でできる 新C言語入門|ポインタと配列

配列は並び、ポインタは場所をたどる道しるべ。
配列の先頭アドレスとポインタ演算を理解して、C言語のデータ処理をもっと深く読み解こう。

C言語では、配列とポインタはとても深い関係を持っています。

配列は、同じ型のデータをメモリ上に連続して並べる仕組みです。たとえば、整数を4個並べた配列では、0番目、1番目、2番目、3番目の要素が順番に配置されます。

一方、ポインタはメモリ上の場所、つまりアドレスを保存する変数です。ポインタに配列の先頭要素のアドレスを入れると、そのポインタを使って配列の各要素へ順番にアクセスできます。

この関係が分かると、配列の添え字を使ったアクセスと、ポインタを使ったアクセスがなぜ似た動きをするのか理解しやすくなります。

たとえば、配列のi番目の要素にアクセスする書き方と、ポインタをi個分進めて値を取り出す書き方は、同じ要素を指すことがあります。

また、関数に配列を渡すときにも、ポインタの考え方が重要になります。C言語では、配列を関数に渡すとき、配列全体のコピーが渡されるわけではありません。実際には、配列の先頭要素のアドレスが渡されます。

この記事では、配列とポインタの基本的な関係、配列の先頭アドレス、ポインタ演算、配列名とポインタ変数の違い、関数へ配列を渡す仕組みを、サンプルプログラムと図を使ってやさしく解説していきます。

配列は同じ型のデータを連続して並べる仕組み

まず、配列の基本から確認しましょう。

配列は、同じ型の値をまとめて管理するための仕組みです。たとえば、4日分の歩数を保存したい場合、次のような配列を使えます。

#define LENGTH 4

int steps[LENGTH];

このsteps配列には、int型の値を4個保存できます。

配列の要素は、添え字を使ってアクセスします。

書き方意味
steps[0]0番目の要素
steps[1]1番目の要素
steps[2]2番目の要素
steps[3]3番目の要素

C言語の配列は、添え字が0から始まります。

LENGTHが4の場合、有効な添え字は0、1、2、3です。steps[4]は範囲外なので使ってはいけません。

配列の各要素にはアドレスがある

配列の各要素も、普通の変数と同じようにメモリ上に置かれています。

そのため、各要素にはアドレスがあります。

たとえば、steps配列の先頭要素のアドレスは、&steps[0]で取得できます。

int* pSteps = NULL;

pSteps = &steps[0];

このように書くと、pStepsはsteps配列の先頭要素を指すポインタになります。

書き方意味
steps[0]steps配列の0番目の値
&steps[0]steps配列の0番目のアドレス
pStepssteps配列の先頭要素のアドレス
*pStepspStepsが指している先の値

ポインタは配列そのものを丸ごと持っているわけではありません。配列の先頭要素の場所を持っているだけです。

ただし、配列は同じ型の要素が連続して並んでいるため、先頭要素の場所が分かれば、次の要素、その次の要素へと順番にたどれます。

図:配列の先頭アドレスとポインタの関係

この図から分かること

この図から分かるのは、ポインタが配列全体を持っているわけではなく、配列の先頭要素のアドレスを持っているということです。

pStepsに&steps[0]を代入すると、pStepsはsteps[0]を指します。配列の要素は連続して並んでいるため、ポインタ演算を使うことで、steps[1]、steps[2]、steps[3]にも順番にアクセスできます。

ポインタ演算で配列の要素へアクセスする

ポインタに配列の先頭アドレスを入れると、ポインタ演算で各要素へアクセスできます。

たとえば、pStepsがsteps[0]を指している場合、pSteps + 1はsteps[1]を指します。

ここで大切なのは、ポインタに1を足したとき、アドレスが単純に1だけ増えるわけではないということです。

int型を指すポインタなら、int型1個分だけ次へ進みます。double型を指すポインタなら、double型1個分だけ次へ進みます。

書き方指す場所値の取り出し
pStepssteps[0]*pSteps
pSteps + 1steps[1]*(pSteps + 1)
pSteps + 2steps[2]*(pSteps + 2)
pSteps + 3steps[3]*(pSteps + 3)

配列の添え字で書くsteps[i]と、ポインタを使う*(pSteps + i)は、同じ要素を表せる場面があります。

配列アクセスとポインタアクセスを確認するプログラム

次のプログラムでは、int型の配列とdouble型の配列を用意し、配列の添え字を使った表示とポインタを使った表示を比べます。

プロジェクト/ファイル名: Lesson53_1/main.c

#include <stdio.h>

#define LENGTH 4

int main(void) {
    int steps[LENGTH] = {3200, 4500, 5100, 2800};
    double temps[LENGTH] = {22.5, 24.0, 23.8, 21.9};
    int* pSteps = NULL;
    double* pTemps = NULL;
    int i;

    // 配列の先頭要素のアドレスをポインタに代入する
    pSteps = &steps[0];
    pTemps = &temps[0];

    // 配列アクセスとポインタアクセスを比較して表示する
    for (i = 0; i < LENGTH; i++) {
        printf("steps[%d] = %d, *(pSteps + %d) = %d\n",
            i, steps[i], i, *(pSteps + i));

        printf("temps[%d] = %.1f, *(pTemps + %d) = %.1f\n",
            i, temps[i], i, *(pTemps + i));
    }

    return 0;
}

実行結果

steps[0] = 3200, *(pSteps + 0) = 3200
temps[0] = 22.5, *(pTemps + 0) = 22.5
steps[1] = 4500, *(pSteps + 1) = 4500
temps[1] = 24.0, *(pTemps + 1) = 24.0
steps[2] = 5100, *(pSteps + 2) = 5100
temps[2] = 23.8, *(pTemps + 2) = 23.8
steps[3] = 2800, *(pSteps + 3) = 2800
temps[3] = 21.9, *(pTemps + 3) = 21.9

このプログラムでは、pStepsにsteps配列の先頭要素のアドレスを代入しています。

pSteps = &steps[0];

同じように、pTempsにtemps配列の先頭要素のアドレスを代入しています。

pTemps = &temps[0];

そのあと、for文で0番目から3番目まで順番に表示しています。

steps[i]は配列の添え字を使ったアクセスです。
*(pSteps + i)はポインタをi個分進めて、その場所の値を取り出すアクセスです。

どちらも同じ要素を表すため、同じ値が表示されます。

ポインタに足す数は要素数として考える

ポインタ演算では、pSteps + 1のように整数を足します。

ここで足している1は、1バイト進むという意味ではありません。次の要素へ進むという意味です。

たとえば、pStepsがint型を指している場合、pSteps + 1はint型1個分だけ先へ進みます。

pTempsがdouble型を指している場合、pTemps + 1はdouble型1個分だけ先へ進みます。

ポインタ指す型+1したときの意味
int*型のポインタint型int型1個分進む
double*型のポインタdouble型double型1個分進む
char*型のポインタchar型char型1個分進む

この仕組みによって、配列の要素を型に合わせて正しくたどれます。

もしポインタ演算が単純なバイト数だけで動いていたら、int型やdouble型の配列を正しく扱うのはとても大変になります。

図:ポインタ演算で配列を順番にたどるしくみ

この図から分かること

この図から分かるのは、ポインタ演算が配列の要素を順番にたどる仕組みであるということです。

pStepsはsteps[0]を指します。pSteps + 1はsteps[1]を指し、pSteps + 2はsteps[2]を指します。そこに*を付けることで、その場所に入っている値を取り出せます。

ポインタに足す数は、メモリの番地をそのまま1増やすという意味ではなく、指している型の要素数として進むと考えると分かりやすくなります。

配列名は先頭アドレスとして使える

C言語では、多くの場面で配列名は配列の先頭要素のアドレスとして扱われます。

たとえば、次の2つはよく似た意味になります。

pSteps = &steps[0];

pSteps = steps;

stepsは配列名ですが、式の中では先頭要素のアドレスとして扱われるため、pStepsに代入できます。

つまり、stepsは多くの場面で&steps[0]と同じように使えます。

書き方意味
steps配列の先頭要素のアドレスとして扱われる
&steps[0]配列の0番目の要素のアドレス
pSteps = steps;pStepsに先頭アドレスを代入
pSteps = &steps[0];pStepsに先頭アドレスを代入

ただし、配列名とポインタ変数は完全に同じものではありません。

配列名とポインタ変数の違い

配列名は先頭アドレスのように使える場面がありますが、ポインタ変数そのものではありません。

特に大きな違いは、配列名には別のアドレスを代入できないという点です。

次のように、ポインタ変数は指す先を変えられます。

int morning[3] = {1000, 2000, 3000};
int evening[3] = {1500, 2500, 3500};
int* p = morning;

p = evening;

このように、pは最初morningを指し、あとでeveningを指すように変更できます。

一方、配列名そのものに別の配列を代入することはできません。

morning = evening;

このような書き方はできません。

配列名とポインタ変数の違いを整理すると、次のようになります。

項目配列名ポインタ変数
先頭アドレスとして使えるか使える使える
別のアドレスを代入できるかできないできる
配列本体の領域を持つか持つ持たない
指す先を切り替えられるかできないできる

配列名は、宣言した配列そのものを表す名前です。
ポインタ変数は、アドレスを保存するための変数です。

似ている場面はありますが、同じものとして扱わないようにしましょう。

配列を関数に渡すと先頭アドレスが渡る

C言語では、配列を関数に渡すとき、配列全体がコピーされるわけではありません。

関数に渡されるのは、配列の先頭要素のアドレスです。

そのため、関数側ではポインタとして受け取れます。

たとえば、次のような関数を考えます。

void printArray(int* arr, int len);

この関数は、int型の配列の先頭アドレスと、配列の要素数を受け取る形です。

配列を渡すときは、次のように書けます。

printArray(scores, 5);

scoresは配列名ですが、関数呼び出しの中では先頭要素のアドレスとして渡されます。

配列を関数で表示するプログラム

次のプログラムでは、配列を関数に渡し、関数側で要素を順番に表示します。

プロジェクト/ファイル名: Lesson53_2/main.c

#include <stdio.h>

// 配列の内容を順番に表示する関数
void printArray(int* arr, int len) {
    int i;

    for (i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }

    printf("\n");
}

// 配列の合計値を返す関数
int sumArray(int* arr, int len) {
    int i;
    int total = 0;

    for (i = 0; i < len; i++) {
        total += arr[i];
    }

    return total;
}

int main(void) {
    int scores[5] = {88, 72, 95, 64, 81};
    int total;

    // 配列の先頭アドレスと要素数を関数に渡す
    printArray(scores, 5);

    // 配列の合計を関数で求める
    total = sumArray(scores, 5);

    printf("合計点: %d\n", total);

    return 0;
}

実行結果

88 72 95 64 81
合計点: 400

このプログラムでは、printArray関数とsumArray関数にscores配列を渡しています。

printArray(scores, 5);

total = sumArray(scores, 5);

scoresを渡すと、関数側では配列の先頭アドレスとして受け取ります。

printArray関数の引数はint* arrです。

void printArray(int* arr, int len)

arrはscores配列の先頭を指しています。そのため、関数の中でarr[i]と書くことで、scores[i]と同じように要素へアクセスできます。

配列を関数に渡すときは要素数も渡す

配列を関数に渡すときに大切なのは、要素数も一緒に渡すことです。

関数側では、配列の先頭アドレスは分かります。しかし、配列に何個の要素があるのかは自動では分かりません。

そのため、lenのような引数で要素数を渡します。

引数役割
arr配列の先頭アドレス
len配列の要素数

要素数を渡さないまま関数側で適当にループすると、配列の範囲外にアクセスしてしまう危険があります。

配列を関数へ渡すときは、先頭アドレスと要素数をセットで渡すと考えると安全です。

図:配列を関数に渡すと先頭アドレスが渡る

この図から分かること

この図から分かるのは、配列を関数に渡したとき、配列全体がコピーされているわけではないということです。

main関数のscores配列を関数に渡すと、printArray関数側のarrはscoresの先頭要素を指します。そのため、arr[0]、arr[1]のように書くことで、元の配列の要素にアクセスできます。

また、関数側では要素数が自動では分からないため、lenのような長さ情報を一緒に渡すことが大切です。

関数の引数では配列形式でも書ける

関数の引数では、次の2つの書き方がよく使われます。

void printArray(int* arr, int len);

void printArray(int arr[], int len);

この2つは、関数の引数としてはほぼ同じ意味で扱えます。

どちらも、int型の並びの先頭アドレスを受け取るという意味です。

書き方意味
int* arrint型配列の先頭アドレスを受け取る
int arr[]int型配列を受け取るように見えるが、実際には先頭アドレスを受け取る

学習段階では、配列を関数に渡すと先頭アドレスが渡ると覚えておくと分かりやすいです。

ポインタと配列で注意したいこと

ポインタと配列を扱うときは、いくつか注意点があります。

範囲外アクセスをしない

配列の有効な添え字を超えてアクセスしてはいけません。

LENGTHが4なら、有効な添え字は0から3です。

steps[4] = 1000;

このような書き方は範囲外アクセスになります。

NULLのままポインタを使わない

ポインタ変数をNULLで初期化した場合、有効なアドレスを代入してから使います。

int* pSteps = NULL;

pSteps = steps;

pStepsがNULLのまま*pStepsを使うと危険です。

配列名に代入しようとしない

配列名はポインタ変数のように再代入できません。

steps = otherSteps;

このような書き方はできません。

関数へ配列を渡すときは要素数も渡す

関数側では、配列の長さが自動では分かりません。

そのため、配列の先頭アドレスと要素数を一緒に渡します。

printArray(scores, 5);

配列とポインタの関係を整理する

ポインタと配列の関係を整理すると、次のようになります。

内容ポイント
配列は連続した領域同じ型の要素がメモリ上に順番に並ぶ
配列名は先頭アドレスとして使える多くの場面で&配列名[0]と同じように扱える
ポインタは配列の先頭を指せる先頭から順に要素をたどれる
ポインタ演算で次の要素へ進める型のサイズ分だけ進む
関数に配列を渡すと先頭アドレスが渡る配列全体のコピーではない
要素数は別に渡す必要がある範囲外アクセスを防ぐため

配列とポインタは、見た目は違いますが、配列の先頭アドレスを使う場面では密接につながっています。

ポインタと配列を理解するとC言語の見通しがよくなる

ポインタと配列は、C言語の中でも特に重要な関係を持っています。

配列は、同じ型の要素を連続して並べる仕組みです。ポインタは、メモリ上の場所を保存する変数です。配列の先頭要素のアドレスをポインタに代入すると、ポインタ演算によって配列の各要素へ順番にアクセスできます。

steps[i]のような配列の添え字を使う書き方と、*(pSteps + i)のようなポインタを使う書き方は、同じ要素を表せる場面があります。

ただし、配列名とポインタ変数は完全に同じものではありません。配列名は先頭アドレスとして使える場面がありますが、別のアドレスを代入することはできません。一方、ポインタ変数は別の配列や変数のアドレスを代入できます。

また、配列を関数に渡すときは、配列全体がコピーされるのではなく、先頭アドレスが渡されます。そのため、関数側ではポインタとして受け取り、要素数も一緒に渡して処理するのが基本です。

この考え方を理解すると、配列処理、文字列処理、関数へのデータ受け渡し、動的メモリ確保など、C言語のさまざまなテーマがつながって見えるようになります。