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

配列名は先頭を示し、ポインタは場所を自由にたどる。
配列とポインタの似ている点と違う点を理解して、C言語のメモリ操作をもっと正確に読み解こう。

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

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

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

ここで重要なのは、C言語では配列名が多くの場面で配列の先頭要素のアドレスとして扱われるという点です。そのため、ポインタ変数に配列名を代入すると、ポインタは配列の先頭を指す状態になります。

ただし、配列名とポインタ変数は完全に同じものではありません。配列名は、宣言された配列そのものを表す名前であり、別のアドレスを代入することはできません。ポインタ変数は、アドレスを保存するための変数なので、指す先をあとから変更できます。

この記事では、配列名とポインタ変数の関係、配列の先頭アドレス、ポインタを使った配列アクセス、配列名とポインタ変数の違い、ポインタのインクリメント、部分配列の扱い方まで、サンプルプログラムを使ってやさしく解説していきます。

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

配列を宣言すると、同じ型のデータを連続して保存できる領域が作られます。

たとえば、次のような配列を考えます。

int points[4] = { 12, 24, 36, 48 };

このpoints配列には、int型の値が4個入っています。

要素
points[0]12
points[1]24
points[2]36
points[3]48

C言語では、配列名pointsは、多くの式の中でpoints[0]のアドレスとして扱われます。つまり、pointsは配列の先頭要素を示す目印のように使えます。

そのため、次のようにポインタ変数へ代入できます。

int* ptr = NULL;
ptr = points;

このとき、ptrはpoints[0]を指します。

pointsと&points[0]は、多くの場面で同じ先頭要素のアドレスとして扱えます。

書き方意味
points配列の先頭要素のアドレスとして使える
&points[0]points[0]のアドレス
ptr = points;ptrに配列の先頭アドレスを代入する
ptr = &points[0];ptrに配列の先頭アドレスを代入する

配列名は、配列の中身そのものをすべて持っているように見えるかもしれません。しかし、式の中では先頭要素のアドレスとして使われる場面が多いです。

図:配列名は先頭要素のアドレスとして使える

この図から分かること

この図から分かるのは、配列名pointsが配列の先頭要素を示す情報として使えるということです。

ポインタ変数ptrにpointsを代入すると、ptrはpoints[0]を指します。配列の要素は連続して並んでいるため、ptrを使ってpoints[1]、points[2]、points[3]へ順番にアクセスできます。

配列名をポインタへ代入して要素を表示する

次のプログラムでは、配列名をポインタ変数に代入し、配列の添え字、ポインタ演算、ポインタの配列表現の3つを比較します。

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

#include <stdio.h>

int main(void) {
    int points[4] = { 12, 24, 36, 48 };
    int* ptr = NULL;
    int i;

    // 配列名を使って、配列の先頭アドレスをポインタに代入する
    ptr = points;

    // 配列アクセス、ポインタ演算、ポインタの配列表現を比較する
    for (i = 0; i < 4; i++) {
        printf("points[%d]=%d, *(ptr+%d)=%d, ptr[%d]=%d\n",
            i, points[i], i, *(ptr + i), i, ptr[i]);
    }

    return 0;
}

実行結果

points[0]=12, *(ptr+0)=12, ptr[0]=12
points[1]=24, *(ptr+1)=24, ptr[1]=24
points[2]=36, *(ptr+2)=36, ptr[2]=36
points[3]=48, *(ptr+3)=48, ptr[3]=48

このプログラムでは、ptr = points;によって、ptrにpoints配列の先頭アドレスを代入しています。

そのため、points[i]、*(ptr + i)、ptr[i]はいずれも同じ要素を表しています。

配列の表現アドレスの考え方ポインタ演算ポインタの配列表現
points[0]&points[0]*ptrptr[0]
points[1]&points[1]*(ptr + 1)ptr[1]
points[2]&points[2]*(ptr + 2)ptr[2]
points[3]&points[3]*(ptr + 3)ptr[3]

ポインタ変数ptrは、配列の先頭アドレスを持っています。そこからi個分進めることで、配列のi番目の要素へアクセスできます。

ポインタ変数は配列のように使える

ポインタ変数に配列の先頭アドレスを代入すると、ptr[i]のような配列風の書き方もできます。

これは、ptr[i]が*(ptr + i)と同じ意味として扱われるためです。

書き方意味
ptr[0]*(ptr + 0)
ptr[1]*(ptr + 1)
ptr[2]*(ptr + 2)
ptr[3]*(ptr + 3)

この関係から、配列とポインタがとても近い仕組みであることが分かります。

ただし、ポインタ変数が配列と同じ領域を持っているわけではありません。ptrはあくまでアドレスを保存しているだけです。

配列pointsは4個分のint型領域を持っています。
ポインタptrは、その先頭アドレスを保存しています。

この違いを意識しておくと、配列名とポインタ変数の違いも理解しやすくなります。

配列名とポインタ変数は似ているが同じではない

配列名は先頭アドレスとして使えるため、ポインタ変数と同じように見えることがあります。

しかし、配列名とポインタ変数には大きな違いがあります。

配列名は、宣言された配列の領域を表す名前です。その配列の先頭アドレスは固定されており、別の配列を代入することはできません。

一方、ポインタ変数はアドレスを保存するための変数です。別の配列の先頭アドレスを代入して、指す先を変更できます。

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

この違いを理解しておくと、配列名をポインタ変数と完全に同じものだと誤解せずに済みます。

配列同士の直接代入はできない

配列名はポインタのように見えることがありますが、代入で別の配列を指すように変更することはできません。

次のプログラムは、配列同士を直接代入しようとする例です。これはコンパイルエラーになります。

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

#include <stdio.h>

int main(void) {
    int first[3] = { 10, 20, 30 };
    int second[3] = { 0, 0, 0 };

    // 配列名に別の配列を代入することはできない
    // 次の行を有効にするとコンパイルエラーになる
    // second = first;

    printf("配列同士を直接代入することはできません。\n");
    printf("必要な場合は、要素を1つずつコピーします。\n");

    return 0;
}

実行結果

配列同士を直接代入することはできません。
必要な場合は、要素を1つずつコピーします。

上のプログラムでは、second = first;をコメントアウトしています。コメントを外すと、配列名に代入しようとしているためコンパイルエラーになります。

配列同士の値をコピーしたい場合は、要素を1つずつ代入します。

second[0] = first[0];
second[1] = first[1];
second[2] = first[2];

または、for文を使って順番にコピーします。

for (int i = 0; i < 3; i++) {
    second[i] = first[i];
}

配列名は、ポインタのように先頭アドレスとして使える場面はありますが、ポインタ変数のように自由に代入できる変数ではありません。

図:配列名は固定、ポインタ変数は指す先を変えられる

この図から分かること

この図から分かるのは、配列名とポインタ変数の柔軟性の違いです。

配列名firstやsecondは、それぞれ宣言された配列領域を表します。second = firstのように、配列名そのものへ別の配列を代入することはできません。

一方、ポインタ変数ptrはアドレスを保存する変数です。そのため、ptr = firstでfirstを指し、あとからptr = secondでsecondを指すように変更できます。

ポインタ変数は指す先を切り替えられる

ポインタ変数は、保存しているアドレスを変更できます。

たとえば、次のように2つの配列を用意し、ポインタが指す先を切り替えられます。

int morning[3] = { 100, 200, 300 };
int evening[3] = { 400, 500, 600 };
int* ptr = morning;

ptr = evening;

最初、ptrはmorningの先頭を指しています。
そのあと、ptr = evening;によって、ptrはeveningの先頭を指すようになります。

このように、ポインタ変数は複数の配列を切り替えて処理したいときに便利です。

ポインタをインクリメントして配列をたどる

ポインタ変数は、インクリメントすることで次の要素へ進めます。

たとえば、ptrが配列の先頭を指している場合、ptr++を実行すると、次の要素を指すようになります。

次のプログラムでは、ポインタを1つずつ進めながら配列の値を表示します。

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

#include <stdio.h>

int main(void) {
    int sales[5] = { 120, 180, 150, 210, 170 };
    int* ptr = sales;
    int i;

    // ポインタを進めながら配列の要素を表示する
    for (i = 0; i < 5; i++) {
        printf("%d ", *ptr);

        // 次の要素を指すようにポインタを進める
        ptr++;
    }

    printf("\n");

    return 0;
}

実行結果

120 180 150 210 170

このプログラムでは、ptrが最初にsales[0]を指しています。

ループの中で*ptrを表示したあと、ptr++で次の要素へ進みます。

ループ回数ptrが指す要素表示される値
1回目sales[0]120
2回目sales[1]180
3回目sales[2]150
4回目sales[3]210
5回目sales[4]170

ポインタを進めると、現在どの要素を指しているのかが変わります。そのため、ループのあとにptrを使い続ける場合は、元の先頭アドレスを失っていないか注意が必要です。

ポインタのインクリメントは型のサイズ分進む

ptr++を実行すると、ptrは次の要素を指します。

ここで注意したいのは、アドレスが単純に1だけ増えるわけではないことです。int型を指すポインタなら、int型1個分だけ進みます。

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

この仕組みによって、ポインタは配列の要素を正しく順番にたどれます。

ポインタ変数を配列のように使う

ポインタ変数に配列の先頭アドレスを代入すると、ポインタ変数に対して添え字を使えます。

次のプログラムでは、double型の配列をポインタ変数で扱います。

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

#include <stdio.h>

int main(void) {
    double weights[3] = { 1.25, 2.50, 3.75 };
    double* ptr = weights;
    int i;

    // ポインタ変数を配列のように使って表示する
    for (i = 0; i < 3; i++) {
        printf("ptr[%d]=%.2f  *(ptr+%d)=%.2f\n",
            i, ptr[i], i, *(ptr + i));
    }

    return 0;
}

実行結果

ptr[0]=1.25  *(ptr+0)=1.25
ptr[1]=2.50  *(ptr+1)=2.50
ptr[2]=3.75  *(ptr+2)=3.75

ptrはweightsの先頭を指しています。

そのため、ptr[0]はweights[0]と同じ要素を表します。ptr[1]はweights[1]、ptr[2]はweights[2]を表します。

ポインタの配列表現ポインタ演算対応する配列要素
ptr[0]*(ptr + 0)weights[0]
ptr[1]*(ptr + 1)weights[1]
ptr[2]*(ptr + 2)weights[2]

ポインタに添え字を使えることから、ポインタと配列がとても近い関係にあることが分かります。

ただし、ptrが有効な配列の先頭や要素を指していない場合、ptr[i]を使うのは危険です。ポインタがどこを指しているのかを常に確認しましょう。

図:ポインタ変数は配列の一部からでもアクセスできる

この図から分かること

この図から分かるのは、ポインタは配列の先頭だけでなく、途中の要素を指すこともできるということです。

ptrに&stock[2]を代入すると、ptr[0]はstock[2]を表します。そこからptr[1]、ptr[2]と進むことで、stock[3]、stock[4]へアクセスできます。ポインタは、配列の一部を扱いたいときにも役立ちます。

配列変数をポインタのように使う

配列名は、多くの場面で先頭要素のアドレスとして扱われるため、ポインタのような書き方ができます。

たとえば、weights配列がある場合、次のような書き方で要素にアクセスできます。

weights[0]

*weights

*(weights + 1)

weightsは先頭要素のアドレスとして使えるため、weightsはweights[0]を表します。(weights + 1)はweights[1]を表します。

書き方意味
weights[0]0番目の要素
*weights0番目の要素
*(weights + 1)1番目の要素
*(weights + 2)2番目の要素

ただし、weights++のように配列名そのものを進めることはできません。配列名はポインタ変数ではないため、値を変更できないからです。

ポインタ変数ならptr++で指す先を進められますが、配列名は固定された配列領域を表す名前です。

ポインタで部分配列を扱う

ポインタを使うと、配列の途中から処理を始めることもできます。

次のプログラムでは、配列の3番目の要素から右側だけを表示します。

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

#include <stdio.h>

int main(void) {
    int stock[6] = { 10, 20, 30, 40, 50, 60 };
    int* ptr = &stock[2];
    int i;

    // stock[2]から右側の要素を表示する
    for (i = 0; i < 4; i++) {
        printf("%d ", *(ptr + i));
    }

    printf("\n");

    return 0;
}

実行結果

30 40 50 60

このプログラムでは、ptrに&stock[2]を代入しています。

つまり、ptrはstock[2]を指しています。

そのため、(ptr + 0)はstock[2]、(ptr + 1)はstock[3]、(ptr + 2)はstock[4]、(ptr + 3)はstock[5]を表します。

書き方対応する要素
*(ptr + 0)stock[2]30
*(ptr + 1)stock[3]40
*(ptr + 2)stock[4]50
*(ptr + 3)stock[5]60

このように、ポインタは配列の途中を指すことができます。配列の一部分だけを処理したい場合に便利です。

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

配列とポインタは便利ですが、扱いを間違えると危険なアクセスにつながります。

配列の範囲外へ進まない

ポインタ演算で配列の範囲外へ進むと、存在しない要素へアクセスしてしまいます。

たとえば、stock配列の要素数が6個なら、有効な添え字は0から5です。stock[6]へアクセスしてはいけません。

配列名に代入しない

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

配列の内容をコピーしたい場合は、各要素を1つずつコピーします。

ポインタがどこを指しているか意識する

ポインタ変数は指す先を変更できます。便利な反面、今どの要素を指しているのか分からなくなると、バグにつながります。

ポインタのインクリメント後に先頭を失わないようにする

ptr++でポインタを進めると、ptrは次の要素を指すようになります。元の先頭アドレスが必要な場合は、別のポインタに保存しておくと安心です。

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

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

内容ポイント
配列は連続した領域同じ型の要素が順番に並ぶ
配列名は先頭アドレスとして使える多くの式で先頭要素を示す
ポインタはアドレスを保存する配列の先頭や途中の要素を指せる
ptr[i]と*(ptr + i)は対応するポインタ変数も配列風に使える
配列名には再代入できない配列名は固定された配列領域を表す
ポインタ変数は再代入できる指す先を切り替えられる
ポインタ演算では型のサイズ分進むint型ならint型1個分進む

配列とポインタは似ている部分がありますが、役割は異なります。

配列はデータを並べる領域です。
ポインタはその場所を保存してたどるための変数です。

この違いを意識すると、ポインタを使った配列処理を安全に書きやすくなります。

配列とポインタの違いが分かるとC言語の理解が深まる

C言語では、配列名が先頭アドレスとして使えるため、配列とポインタはとても近い関係にあります。

配列名をポインタ変数に代入すると、ポインタは配列の先頭を指します。そして、ポインタ演算や添え字を使って、配列の各要素にアクセスできます。

points[i]、*(ptr + i)、ptr[i]は、同じ要素を表せる場面があります。この関係が分かると、配列処理や関数への配列渡しが理解しやすくなります。

一方で、配列名とポインタ変数は完全に同じではありません。配列名は固定された配列領域を表すため、別のアドレスを代入できません。ポインタ変数はアドレスを保存する変数なので、指す先を自由に切り替えられます。

また、ポインタを使えば、配列の途中から処理を始めることもできます。部分配列のように扱えるため、柔軟なデータ処理ができます。

配列はデータの並び、ポインタはその場所をたどる道しるべ。この2つの関係をしっかり理解すると、C言語の配列処理、文字列処理、関数へのデータ受け渡しがより深く理解できるようになります。