C言語の基本|配列とアドレス

配列は、値が並ぶだけではなく、アドレスも規則正しく並ぶ。メモリ上の配置を見れば、配列とポインタの関係がすっきり見えてきます。

C言語で配列を学ぶときは、各要素にどのような値が入っているかに注目することが多いでしょう。

たとえば、次の配列には3つの整数が格納されています。

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

それぞれの値は、levels[0]、levels[1]、levels[2]のように添字を付けて取り出せます。

しかし、配列を理解するうえで大切なのは、値だけではありません。配列の各要素がメモリ上のどこに置かれているかという、アドレスの見方も重要です。

配列の要素は、メモリ上に連続して配置されます。そのため、各要素のアドレスを順番に表示すると、要素の型が使うバイト数に合わせて規則正しく増えていく様子を確認できます。

さらに、配列名は多くの式の中で先頭要素へのポインタに変換されます。この性質は、これから学ぶポインタ、関数への配列の受け渡し、文字列、2次元配列の理解にもつながります。

ここでは、1次元配列と2次元配列を例にしながら、次の内容を順番に見ていきます。

  • 配列要素がメモリ上に並ぶ仕組み
  • 配列の値とアドレスの違い
  • 配列名と先頭要素のアドレスの関係
  • %pを使ったアドレスの表示
  • 2次元配列の行とアドレスの関係
  • for文を使ったアドレスの確認

配列のアドレスとは

変数や配列の要素は、メモリ上のどこかに保存されます。その保存場所を表す情報がアドレスです。

普通の変数では、変数名と&変数名を使い分けます。

書き方表すもの
number変数numberに入っている値
&number変数numberのアドレス

配列の要素でも考え方は同じです。

書き方表すもの
levels[0]先頭要素に入っている値
&levels[0]先頭要素のアドレス
levels[1]2番目の要素に入っている値
&levels[1]2番目の要素のアドレス

添字だけを指定すると値を取り出せます。先頭に&を付けると、その要素が置かれているアドレスを取得できます。

配列要素は連続して配置される

配列の大きな特徴は、同じ型の要素がメモリ上に連続して並ぶことです。

次の配列を考えてみましょう。

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

int型が4バイトで、levels[0]のアドレスが0x1000だったと仮定すると、各要素は次のように配置されます。

要素アドレスの例
levels[0]400x1000
levels[1]550x1004
levels[2]700x1008

隣の要素へ進むたびに、アドレスが4バイトずつ増えています。これは、配列要素が連続して配置され、int型1個につき4バイト使っているためです。

ただし、int型の大きさや実際のアドレスは実行環境によって異なる場合があります。上のアドレスは、メモリ配置を説明するための例です。

図1:1次元配列の値とアドレスの並び

この図から分かること

1次元配列の各要素は、メモリ上に連続して配置されます。

int型が4バイトなら、levels[0]からlevels[1]へ進むとアドレスも4バイト進みます。levels[1]からlevels[2]へ進む場合も同じです。

また、配列名levelsは、多くの式の中で先頭要素levels[0]へのポインタに変換されます。そのため、levelsと&levels[0]を%pで表示すると、数値として同じ先頭位置が表示されます。

配列名と先頭要素のアドレス

1次元配列では、配列名が多くの式の中で先頭要素へのポインタに変換されます。

levels

このとき、levelsが指している先頭位置は、&levels[0]と同じです。

書き方指している位置
levels先頭要素levels[0]の位置
&levels[0]先頭要素levels[0]の位置

ただし、配列名そのものと&levels[0]を、どのような場面でも完全に同じものだと考えるのは正確ではありません。

levelsは配列名であり、多くの式で先頭要素へのポインタに変換されます。一方、&levels[0]はアドレス演算子によって明示的に取得した、先頭要素のアドレスです。

初めの段階では、次のように整理しておくと分かりやすいでしょう。

  • levels[0]は先頭要素の値
  • &levels[0]は先頭要素のアドレス
  • levelsは多くの式で先頭要素へのポインタになる

値とアドレスを混同しない

配列要素の値とアドレスは別の情報です。

たとえば、levels[1]に55が入っているとします。

levels[1]

この式で取り出せるのは値55です。

&levels[1]

こちらは、55が格納されている場所のアドレスです。

取り出せる情報表示方法
levels[1]値55%d
&levels[1]保存場所%p

値を表示するときとアドレスを表示するときでは、printfの書式指定子も異なります。

アドレスを%pで表示する

printfでアドレスを表示するときは、書式指定子%pを使います。

printf("%p\n", (void *)&levels[0]);

この書き方に含まれる要素を整理すると、次のようになります。

要素役割
printf値や文字列を画面へ表示する
%pアドレスを表示する
&levels[0]levels[0]のアドレスを取得する
(void *)voidポインタへ変換する

%pに対応する引数はvoidポインタとして渡します。そのため、アドレスを(void *)で変換しています。

配列名を表示する場合も同じです。

printf("%p\n", (void *)levels);

voidポインタとは

C言語には、指す対象の型に応じてさまざまなポインタ型があります。

ポインタ型主に指すもの
int *int型のデータ
char *char型のデータ
double *double型のデータ
void *型を特定しない汎用的なポインタ

voidポインタは、特定のデータ型に結び付けない汎用的なポインタです。

printfの%pでは、表示するポインタをvoidポインタとして渡します。

(void *)levels
(void *)&levels[0]

ここで行っているのは、アドレスの値そのものを変更する処理ではありません。printfへ渡すポインタの型を、%pに合うvoidポインタへ変換しています。

アドレスの表示形式

アドレスは、16進数のような形式で表示されることが一般的です。

0x000000a1b2c3d4e0

ただし、%pによる具体的な表示形式は処理系によって異なります。0xが付く環境もあれば、付かない環境もあります。英字の大文字と小文字が異なることもあります。

アドレスを確認するときは、数値そのものではなく、次の関係に注目しましょう。

  • 配列名と先頭要素のアドレスが同じ位置を示すか
  • 隣り合う要素のアドレスが型の大きさ分だけ離れているか
  • 配列要素が連続して配置されているか

1次元配列の値とアドレスを表示する

センサー基準値と各要素のアドレスを表示するプログラム

ファイル名:11_2_1.c

#include <stdio.h>

int main(void)
{
    /* 3個のセンサー基準値を保存する配列 */
    int levels[3] = {40, 55, 70};

    printf("配列levelsの先頭位置: %p\n", (void *)levels);

    printf("levels[0]の値: %d\tアドレス: %p\n",
           levels[0], (void *)&levels[0]);

    printf("levels[1]の値: %d\tアドレス: %p\n",
           levels[1], (void *)&levels[1]);

    printf("levels[2]の値: %d\tアドレス: %p\n",
           levels[2], (void *)&levels[2]);

    return 0;
}
実行結果の例

アドレスは実行環境によって変わります。次は、int型が4バイトの場合の表示例です。

配列levelsの先頭位置: 0x000000a1b2c3d4e0
levels[0]の値: 40    アドレス: 0x000000a1b2c3d4e0
levels[1]の値: 55    アドレス: 0x000000a1b2c3d4e4
levels[2]の値: 70    アドレス: 0x000000a1b2c3d4e8

配列の宣言を確認する

次の文では、int型の要素を3個持つ1次元配列を宣言しています。

int levels[3] = {40, 55, 70};
要素
levels[0]40
levels[1]55
levels[2]70

3つの要素は、メモリ上に連続して配置されます。

配列名を表示する部分

最初のprintfでは、配列名levelsを%pで表示しています。

printf("配列levelsの先頭位置: %p\n", (void *)levels);

この式の中では、levelsが先頭要素へのポインタに変換されます。そのため、表示される位置は&levels[0]と一致します。

各要素の値とアドレスを表示する部分

次のprintfでは、levels[0]の値とアドレスを表示しています。

printf("levels[0]の値: %d\tアドレス: %p\n",
       levels[0], (void *)&levels[0]);

%dに対応するのはlevels[0]の値です。%pに対応するのは&levels[0]のアドレスです。

2番目以降も同じ考え方です。

levels[1]

これは値55です。

&levels[1]

これは値55が保存されている場所です。

実行結果から確認できること

実行結果では、配列名levelsと&levels[0]が同じ位置を示しています。

配列levelsの先頭位置
levels[0]のアドレス

この2つが同じアドレスになっている点に注目してください。

また、int型が4バイトなら、各要素のアドレスは4バイトずつ進みます。

要素前の要素からの間隔
levels[0]先頭
levels[1]4バイト
levels[2]さらに4バイト

アドレスの差は16進数で読む

実行結果例では、アドレスの末尾が次のように変化しています。

d4e0
d4e4
d4e8

16進数でも、e0からe4までは4、e4からe8までも4の差です。

ただし、アドレスを読むときに、すべての16進数計算を暗算する必要はありません。大切なのは、要素が型の大きさに応じた一定間隔で並んでいることです。

2次元配列のアドレス

2次元配列は、行と列を持つ表のように見えます。

int matrix[2][2] = {
    {21, 22},
    {31, 32}
};

見た目は次のような2行2列です。

行\列0列目1列目
0行目2122
1行目3132

しかし、メモリ上では次の順番で連続して配置されます。

matrix[0][0]
matrix[0][1]
matrix[1][0]
matrix[1][1]

2次元配列は、1次元配列の集まりとして考えると分かりやすくなります。

  • matrixは2つの行を持つ配列
  • matrix[0]は0行目の1次元配列
  • matrix[1]は1行目の1次元配列
  • matrix[0][1]は0行目の中にある1つの値

図2:2次元配列がメモリ上に並ぶ順序

この図から分かること

2次元配列は、見た目では行と列を持つ表です。しかし、メモリ上では各要素が行の順番に連続して配置されます。

matrix[0]は0行目の先頭位置を表し、matrix[1]は1行目の先頭位置を表します。

1行にint型要素が2個あり、int型が4バイトなら、次の行の先頭位置は8バイト先になります。

2次元配列でアドレスになるもの

2次元配列では、1次元配列よりも見方が増えます。

書き方表すもの
matrix先頭行側の位置
matrix[0]0行目の先頭要素の位置
matrix[1]1行目の先頭要素の位置
matrix[0][1]0行1列目の値
&matrix[0][1]0行1列目のアドレス

matrixは、多くの式の中で先頭行へのポインタに変換されます。

matrix[0]は0行目を表す1次元配列です。多くの式の中では、0行目の先頭要素へのポインタに変換されます。

数値として表示される先頭位置には、次の関係があります。

書き方表示される位置
matrix配列全体の先頭側
matrix[0]0行目の先頭
&matrix[0][0]0行0列目のアドレス

これらを%pで表示すると、数値として同じ先頭位置になります。ただし、それぞれの式が持つ型や意味は同じではありません。

行の先頭位置

2次元配列では、配列名[行番号]がその行を表す配列になります。

matrix[0]

この式は0行目の配列です。多くの式では、matrix[0][0]へのポインタに変換されます。

matrix[1]

こちらは1行目の配列で、多くの式ではmatrix[1][0]へのポインタに変換されます。

行の先頭アドレスは、次のように明示的に取得することもできます。

&matrix[0][0]
&matrix[1][0]

2次元配列の要素のアドレス

個々の要素の値を取り出すときは、行と列の添字を指定します。

matrix[0][1]

この式が表す値は22です。

その要素のアドレスを取得する場合は、先頭に&を付けます。

&matrix[0][1]
表すもの
matrix[0][1]値22
&matrix[0][1]値22が置かれている場所

1次元配列と同じように、添字だけなら値、&を付けるとアドレスと考えられます。

2次元配列の値とアドレスを表示する

2行2列の測定値とアドレスを表示するプログラム

ファイル名:11_2_2.c

#include <stdio.h>

int main(void)
{
    /* 2行2列の測定値を保存する配列 */
    int matrix[2][2] = {
        {21, 22},
        {31, 32}
    };

    printf("matrixの先頭位置: %p\n", (void *)matrix);
    printf("matrix[0]の先頭位置: %p\n", (void *)matrix[0]);

    printf("matrix[0][0]の値: %d\tアドレス: %p\n",
           matrix[0][0], (void *)&matrix[0][0]);

    printf("matrix[0][1]の値: %d\tアドレス: %p\n",
           matrix[0][1], (void *)&matrix[0][1]);

    printf("matrix[1]の先頭位置: %p\n", (void *)matrix[1]);

    printf("matrix[1][0]の値: %d\tアドレス: %p\n",
           matrix[1][0], (void *)&matrix[1][0]);

    printf("matrix[1][1]の値: %d\tアドレス: %p\n",
           matrix[1][1], (void *)&matrix[1][1]);

    return 0;
}
実行結果の例

次はint型が4バイトの場合の表示例です。実際のアドレスは実行環境によって異なります。

matrixの先頭位置: 0x000000b2c3d4e500
matrix[0]の先頭位置: 0x000000b2c3d4e500
matrix[0][0]の値: 21    アドレス: 0x000000b2c3d4e500
matrix[0][1]の値: 22    アドレス: 0x000000b2c3d4e504
matrix[1]の先頭位置: 0x000000b2c3d4e508
matrix[1][0]の値: 31    アドレス: 0x000000b2c3d4e508
matrix[1][1]の値: 32    アドレス: 0x000000b2c3d4e50c

先頭位置が一致している部分

実行結果では、次の3つが数値として同じ位置を示しています。

  • matrix
  • matrix[0]
  • &matrix[0][0]

いずれも、2次元配列の最初の要素が置かれている位置から始まります。

ただし、型の見方は異なります。

基本的な見方
matrix1行分の配列へのポインタとして扱われる
matrix[0]int型要素へのポインタとして扱われる
&matrix[0][0]int型要素のアドレス

表示される数値が同じでも、型まで同じとは限らない点が大切です。

行の先頭アドレスの間隔

matrixは1行にint型要素を2個持っています。

int型が4バイトなら、1行分の大きさは次のようになります。

4バイト × 2要素 = 8バイト

そのため、matrix[0]とmatrix[1]の先頭アドレスは8バイト離れます。

先頭アドレスの例
matrix[0]0x000000b2c3d4e500
matrix[1]0x000000b2c3d4e508

同じ行の中では4バイトずつ進み、次の行へ移るときも、要素の並びは途切れません。

2次元配列は行優先で並ぶ

C言語の2次元配列は、行優先で配置されます。

今回のmatrixでは、次の順番です。

matrix[0][0]
matrix[0][1]
matrix[1][0]
matrix[1][1]

0行目のすべての要素が並び、その直後に1行目の要素が続きます。

この配置方法によって、見た目が表であっても、メモリ上では1本の連続した領域として扱えます。

1次元配列と2次元配列の違い

1次元配列と2次元配列では、配列名や添字を付けた式の意味が少し異なります。

観点1次元配列2次元配列
配列名先頭要素側を表す先頭行側を表す
配列名[添字]1つの値1行分の配列
要素の値data[i]table[row][col]
要素のアドレス&data[i]&table[row][col]
行の先頭位置なしtable[row]
よく使う添字1つ行と列の2つ

2次元配列は、1次元配列を複数まとめたものだと考えると理解しやすくなります。

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

配列のすべての要素のアドレスを確認するときは、for文が便利です。

基本形は次のようになります。

for (int i = 0; i < 要素数; i++) {
    printf("%p\n", (void *)&配列名[i]);
}

添字iを0から要素数の1つ手前まで変化させることで、先頭要素から最後の要素までのアドレスを順番に表示できます。

図3:配列の種類によるアドレス指定の違い

この図から分かること

1次元配列では、data[i]が1つの値を表し、&data[i]がその要素のアドレスを表します。

2次元配列では、table[row]が1行分の配列です。多くの式の中では、その行の先頭要素へのポインタに変換されます。

個々の値を取り出す場合はtable[row][col]、その値のアドレスを取得する場合は&table[row][col]を使います。

12個のfloat型要素のアドレスを表示する

float型配列の全要素のアドレスを表示するプログラム

ファイル名:11_2_3.c

#include <stdio.h>

int main(void)
{
    /* 12個の電圧値を保存する配列 */
    float voltage[12];

    printf("配列voltageの各要素のアドレス一覧\n");

    for (int i = 0; i < 12; i++) {
        printf("voltage[%d]のアドレス: %p\n",
               i, (void *)&voltage[i]);
    }

    return 0;
}
実行結果の例

次はfloat型が4バイトの場合の表示例です。

配列voltageの各要素のアドレス一覧
voltage[0]のアドレス: 0x000000f0aabb1000
voltage[1]のアドレス: 0x000000f0aabb1004
voltage[2]のアドレス: 0x000000f0aabb1008
voltage[3]のアドレス: 0x000000f0aabb100c
voltage[4]のアドレス: 0x000000f0aabb1010
voltage[5]のアドレス: 0x000000f0aabb1014
voltage[6]のアドレス: 0x000000f0aabb1018
voltage[7]のアドレス: 0x000000f0aabb101c
voltage[8]のアドレス: 0x000000f0aabb1020
voltage[9]のアドレス: 0x000000f0aabb1024
voltage[10]のアドレス: 0x000000f0aabb1028
voltage[11]のアドレス: 0x000000f0aabb102c

for文の役割

for文では、添字iを0から11まで変化させています。

for (int i = 0; i < 12; i++) {

配列voltageの要素数は12なので、使用できる添字は0から11までです。

条件をi <= 12にすると、存在しないvoltage[12]のアドレスを求めようとするため注意が必要です。

&voltage[i]で各要素のアドレスを取得する

for文の中では、&voltage[i]を使っています。

(void *)&voltage[i]

voltage[i]では要素の値を表します。&voltage[i]では、その要素が置かれているアドレスを表します。

i表示するアドレス
0&voltage[0]
1&voltage[1]
2&voltage[2]
11&voltage[11]

float型の大きさとアドレスの間隔

実行結果例では、アドレスが4バイトずつ進んでいます。これは、例の環境でfloat型が4バイトだからです。

float型が何バイトかは、sizeofで確認できます。

printf("%zu\n", sizeof(float));

アドレスの間隔は、次の関係になります。

次の要素までの間隔 = sizeof(float)

環境によってfloat型の大きさが異なる場合は、アドレスの間隔もその大きさに合わせて変わります。

2次元配列の各行の先頭アドレスを表示する

2次元配列では、外側のfor文で行番号を変化させると、各行の先頭位置を順番に確認できます。

基本形は次のとおりです。

for (int row = 0; row < 行数; row++) {
    printf("%p\n", (void *)配列名[row]);
}

配列名[row]はその行を表す配列です。printfの引数として使う場面では、その行の先頭要素へのポインタに変換されます。

4行6列の各行の先頭アドレスを表示する

short型2次元配列の各行の先頭アドレスを表示するプログラム

ファイル名:11_2_4.c

#include <stdio.h>

int main(void)
{
    /* 4行6列の画素データを保存する配列 */
    short pixels[4][6];

    printf("2次元配列pixelsの各行の先頭要素のアドレス一覧\n");

    for (int row = 0; row < 4; row++) {
        printf("pixels[%d]の先頭要素のアドレス: %p\n",
               row, (void *)pixels[row]);
    }

    return 0;
}
実行結果の例

次はshort型が2バイトの場合の表示例です。

2次元配列pixelsの各行の先頭要素のアドレス一覧
pixels[0]の先頭要素のアドレス: 0x000000ccdd8800
pixels[1]の先頭要素のアドレス: 0x000000ccdd880c
pixels[2]の先頭要素のアドレス: 0x000000ccdd8818
pixels[3]の先頭要素のアドレス: 0x000000ccdd8824

配列の宣言を確認する

pixelsは、short型の要素を4行6列持つ2次元配列です。

short pixels[4][6];
項目
行数4
列数6
全要素数24

各行にはshort型の要素が6個あります。

行番号をfor文で変化させる

for文では、rowを0から3まで変化させています。

for (int row = 0; row < 4; row++) {
rowの値表示する行
0pixels[0]
1pixels[1]
2pixels[2]
3pixels[3]

pixels[row]は、row行目を表す配列です。printfへ渡すときは、その行の先頭要素へのポインタに変換されます。

各行のアドレスが12バイトずつ進む理由

short型が2バイトなら、1行分の大きさは次のようになります。

2バイト × 6要素 = 12バイト

そのため、各行の先頭アドレスは12バイトずつ離れます。

アドレス末尾の例前の行からの間隔
pixels[0]8800先頭
pixels[1]880c12バイト
pixels[2]881812バイト
pixels[3]882412バイト

16進数の0Cは10進数の12です。そのため、8800から880Cへ進むと、12バイト先の位置になります。

pixels[row]と&pixels[row][0]の関係

各行の先頭位置は、次の2つの書き方で確認できます。

pixels[row]
&pixels[row][0]

pixels[row]はrow行目の配列です。多くの式の中で、その行の先頭要素へのポインタに変換されます。

&pixels[row][0]は、row行0列目の要素のアドレスを明示的に取得しています。

書き方意味
pixels[row]row行目の先頭要素側
&pixels[row][0]row行0列目の明示的なアドレス

%pで表示すると、数値として同じ位置になります。

配列のアドレスを確認するときの注意点

注意点内容
アドレスの表示%pを使う
%pへ渡す値(void *)で変換する
実際のアドレス実行環境や実行時によって変わる
型の大きさsizeofで確認する
配列の範囲添字は0から要素数の1つ手前まで
1次元配列&array[i]で要素のアドレスを取得する
2次元配列array[row]で行の先頭側を表せる
2次元配列の個別要素&array[row][col]でアドレスを取得する

コードを読むときの確認手順

配列とアドレスを扱うコードでは、次の順番で確認すると理解しやすくなります。

  • 配列の型を確認する
  • 1要素が何バイトか確認する
  • 1次元配列か2次元配列か確認する
  • 添字だけなら値、&付きならアドレスと考える
  • 配列名がどの要素の先頭側を表しているか確認する
  • 2次元配列では1行に何要素あるか確認する
  • for文の範囲が配列の要素数や行数と一致しているか確認する
  • 表示されたアドレスの間隔を型や1行分の大きさと比べる

配列の値だけでなくアドレスも確認できるようになると、配列がメモリ上に連続して並ぶ仕組みが見えてきます。

この考え方は、ポインタ演算、関数への配列の受け渡し、文字列、2次元配列の処理を学ぶときの大切な土台になります。