C言語のきほん|配列のアドレスを調べる

配列の中身だけじゃない。アドレスを見ると、C言語のしくみがぐっと見えてくる。

C言語で配列を学び始めると、つい 値 にばかり注目しがちです。たとえば、配列に何が入っているか、何番目の要素が何か、といったことはとても大切です。ですが、C言語をしっかり理解するためには、配列がメモリ上のどこに置かれているのか、つまり アドレス を見る視点も欠かせません。

配列は、メモリの中に連続して並べられます。この性質があるからこそ、添字を使って素早く要素へアクセスできます。そして、その連続性はアドレスを表示してみると、とてもよくわかります。先頭要素のアドレス、次の要素のアドレス、そのまた次の要素のアドレスを順番に見ていくと、配列が「バラバラ」ではなく「整然と並んでいる」ことがはっきり確認できます。

さらにC言語では、配列名そのものが特別な意味を持ちます。普通の変数名とは少し違い、配列名は多くの場面で先頭要素のアドレスとして扱われます。この考え方は、今後学ぶポインタ、文字列、関数への配列の受け渡し、2次元配列の扱いなどにもつながる、とても重要な土台です。

この記事では、1次元配列と2次元配列を例にしながら、配列名・アドレス演算子・添字の関係をやさしく整理していきます。値を見るだけでは気づきにくい、C言語らしいメモリの見え方を、ひとつずつ確認していきましょう。

配列のアドレスとは何か

まず押さえておきたいのは、配列の各要素はメモリ上に連続して配置されるという点です。

たとえば int 型の配列なら、int 型1個分の大きさずつ並んでいきます。環境によって異なることはありますが、多くの環境では int は4バイトなので、隣の要素のアドレスは4バイトずつ増えていきます。

ここで大切なのが、次の3つの見方です。

書き方意味
配列名先頭要素のアドレスとして扱われる
&配列名[添字]その要素のアドレス
配列名[添字]その要素の値

この3つを区別できるようになると、配列の見え方がかなりはっきりしてきます。

1次元配列のアドレスを調べる

最初は、1次元配列のアドレスを見てみましょう。元の例を少し変えて、よりシンプルで親しみやすい内容にしたプログラムを使います。

サンプルプログラム例

ファイル名:11_2_1.c

#include <stdio.h>

int main(void)
{
    /* 商品コードを入れる1次元配列 */
    int code[3] = {101, 102, 103};

    printf("配列codeの先頭要素のアドレス: %p\n", (void *)code);
    printf("code[0]の値: %d\tアドレス: %p\n", code[0], (void *)&code[0]);
    printf("code[1]の値: %d\tアドレス: %p\n", code[1], (void *)&code[1]);
    printf("code[2]の値: %d\tアドレス: %p\n", code[2], (void *)&code[2]);

    return 0;
}

実行結果例

配列codeの先頭要素のアドレス: 000000A1B2C3D4E0
code[0]の値: 101    アドレス: 000000A1B2C3D4E0
code[1]の値: 102    アドレス: 000000A1B2C3D4E4
code[2]の値: 103    アドレス: 000000A1B2C3D4E8

※ アドレスの値は実行環境によって異なります。

1次元配列の見方

この実行結果を見ると、配列名 code と &code[0] が同じアドレスになっています。ここがとても重要です。

配列名は先頭要素のアドレスを表す

次の行に注目します。

printf("配列codeの先頭要素のアドレス: %p\n", (void *)code);

配列名 code は、この場面では 先頭要素 code[0] のアドレス として扱われています。つまり、次の2つは同じ場所を指しています。

書き方指しているもの
codecode[0] のアドレス
&code[0]code[0] のアドレス

そのため、表示結果でも同じアドレスになります。

各要素のアドレスは連続している

続いて、各要素のアドレスを見てみると、次のように並んでいます。

要素アドレス例
code[0]101000000A1B2C3D4E0
code[1]102000000A1B2C3D4E4
code[2]103000000A1B2C3D4E8

4バイトずつ増えているのがわかります。これは int 型の要素が連続して配置されているためです。

値とアドレスは役割が違う

ここは混同しやすいところです。

書き方取り出せるもの
code[1]102 という値
&code[1]code[1] が置かれている場所のアドレス

つまり、添字だけなら中身、&を付けると場所、というイメージで覚えるとわかりやすいです。

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

この図では、1次元配列 code の各要素がメモリ上に横一列で並んでいる様子を表します。配列名 code は先頭要素 code[0] のアドレスを指し、各要素のアドレスが int 型のサイズ分だけ順番に増えていくことを視覚的に確認できます。配列は「値の集まり」であると同時に、「連続したメモリ領域」であることがつかみやすくなります。

なぜ %p で表示するときにキャストするのか

サンプルプログラムでは、アドレスを表示するときに (void *) を付けています。

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

これは、%p が void * 型の値を表示するための指定だからです。実際には配列やポインタの型はいろいろありますが、%p に渡すときは void * にしておくと、型の扱いが明確になります。

学習の最初の段階では見落とされがちですが、C言語では表示形式と渡す値の型をきちんと合わせることが大切です。こうした書き方に慣れておくと、あとでポインタを扱うときにも安心です。

*(void )

ポインタを「voidポインタ型」にキャスト(型変換)するための記述です。

C言語では、%p を使って アドレス(ポインタ)を表示する場合、引数は void型ポインタである必要があります。
そのため、code を (void *) で voidポインタに変換してから printf に渡しているのです。

ここで順番に整理してみましょう。

%p の仕様

printf の %p は ポインタのアドレスを表示する書式指定子です。

書式意味
%pポインタのアドレスを表示

なぜ voidポインタにするのか

C言語ではポインタにはいろいろな種類があります。

ポインタ型指しているもの
int *int型変数
char *文字
double *double型
void *どんな型でも指せるポインタ

void * は 汎用ポインタ(generic pointer)と呼ばれます。

つまり

どんなポインタでも一時的に入れられる型です。

そのため %p では ポインタ型を統一するために void * を使う仕様になっています。

2次元配列のアドレスを調べる

次は2次元配列です。2次元配列では、配列名そのものだけでなく、配列名[行番号] もアドレスとして扱われるのが大きなポイントです。

こちらも内容を変えたシンプルな例で見てみましょう。

サンプルプログラム例

ファイル名:11_2_2.c

#include <stdio.h>

int main(void)
{
    /* 2日分の売上個数を入れる2次元配列 */
    int sales[2][2] = {{8, 12}, {15, 10}};

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

    return 0;
}

実行結果例

salesの先頭要素のアドレス: 000000B2C3D4E500
sales[0]の先頭要素のアドレス: 000000B2C3D4E500
sales[0][0]の値: 8     アドレス: 000000B2C3D4E500
sales[0][1]の値: 12    アドレス: 000000B2C3D4E504
sales[1]の先頭要素のアドレス: 000000B2C3D4E508
sales[1][0]の値: 15    アドレス: 000000B2C3D4E508
sales[1][1]の値: 10    アドレス: 000000B2C3D4E50C

※ アドレスの値は実行環境によって異なります。

2次元配列では何がアドレスになるのか

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

書き方意味
sales配列全体の先頭、つまり sales[0][0] 側を指す
sales[0]0行目の先頭要素のアドレス
sales[1]1行目の先頭要素のアドレス
&sales[0][1]0行1列目の要素のアドレス
sales[0][1]0行1列目の値

配列名 sales は先頭要素側を指す

次の行では、sales 全体の先頭側の場所を表示しています。

printf("salesの先頭要素のアドレス: %p\n", (void *)sales);

このアドレスは、sales[0] の先頭要素のアドレスと一致します。さらに、それは &sales[0][0] とも同じ場所です。

つまり、2次元配列では次の関係が見えてきます。

書き方指している場所
sales先頭側の行
sales[0]0行目の先頭要素
&sales[0][0]0行0列目の要素

表示結果では、これらが同じ先頭位置から始まっていることが確認できます。

sales[行番号] はその行の先頭要素のアドレス

ここが2次元配列らしいポイントです。

printf("sales[0]の先頭要素のアドレス: %p\n", (void *)sales[0]);
printf("sales[1]の先頭要素のアドレス: %p\n", (void *)sales[1]);

sales[0] は 0行目の先頭要素のアドレス、sales[1] は 1行目の先頭要素のアドレスです。

つまり、2次元配列では 行ごとにまとまりがある と考えると理解しやすいです。行の先頭位置を見たいなら 配列名[行番号] を使います。

要素のアドレスは &配列名[行][列] で取得する

各要素の値そのものは sales[0][1] のように書きますが、その要素が置かれている場所を見たいなら &sales[0][1] のように書きます。

この違いも1次元配列と同じで、添字だけなら値、&を付けるとアドレスです。

2次元配列の並び方を整理する

2次元配列は見た目では表のように見えますが、メモリ上では結局 連続した並び です。

今回の例では、

  • sales[0][0]
  • sales[0][1]
  • sales[1][0]
  • sales[1][1]

の順に並びます。

表にすると次のようになります。

要素アドレス例
sales[0][0]8000000B2C3D4E500
sales[0][1]12000000B2C3D4E504
sales[1][0]15000000B2C3D4E508
sales[1][1]10000000B2C3D4E50C

ここでも int 型1個分ずつ、アドレスが増えているのがわかります。行が分かれて見えても、メモリ上では順番に並んでいるのがポイントです。

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

この図では、2行2列の配列 sales がメモリ上でどのように並んでいるかを表します。見た目は表のようでも、実際のメモリでは sales[0][0] から sales[1][1] までが連続して配置されます。また、sales[0] が0行目の先頭、sales[1] が1行目の先頭を指すことも同時に確認できます。

1次元配列と2次元配列の違いを整理しよう

ここまでの内容を表でまとめると、次のようになります。

観点1次元配列2次元配列
配列名先頭要素のアドレス先頭の行側を指す
配列名[添字]要素の値行の先頭要素のアドレス、またはさらに添字を続けて要素の値
&配列名[添字]要素のアドレス行の指定後、さらに要素のアドレスを取得できる
よく見る形data[2], &data[2]table[1], table[1][2], &table[1][2]

特に初心者のうちは、2次元配列で 配列名[行番号] が値ではなくアドレスになる ところで少し戸惑いやすいです。ですが、2次元配列は「1次元配列の集まり」と考えると納得しやすくなります。

つまり、

  • sales は 行の集まり
  • sales[0] は 0行目という1次元配列
  • sales[0][1] は その1次元配列の中の1つの値

というイメージです。

配列のアドレスを見る意味

配列のアドレスを調べることには、次のような学習上の大きな意味があります。

学べること内容
配列の連続性要素がメモリ上に連続して並ぶことを確認できる
配列名の性質配列名が先頭要素のアドレスとして扱われることがわかる
ポインタ理解の土台配列とポインタの関係を学ぶ準備になる
2次元配列の構造理解行ごとの先頭位置や要素の並び方が見えてくる

C言語は、メモリの見え方を意識すると一気に理解が深まる言語です。配列のアドレスを表示して確かめる作業は、単なる確認ではなく、C言語らしい考え方を体に覚えさせるための大事な練習でもあります。

実践問題

要素数12の float 型の1次元配列 score を宣言し、for文を使って先頭要素から最後の要素までのアドレスを順に表示するプログラムを作成してください。

最初に 配列scoreの各要素のアドレス一覧 と表示してください。

解答例

ファイル名:11_2_3.c

#include <stdio.h>

int main(void)
{
    /* 得点を入れる1次元配列 */
    float score[12];
    int i;

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

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

    return 0;
}

実行結果例

配列scoreの各要素のアドレス一覧
score[0]のアドレス: 000000F0AABB1000
score[1]のアドレス: 000000F0AABB1004
score[2]のアドレス: 000000F0AABB1008
...
score[10]のアドレス: 000000F0AABB1028
score[11]のアドレス: 000000F0AABB102C

※ アドレスの値は実行環境によって異なります。

解説

この問題では、1次元配列の各要素のアドレスを順番に表示しています。使っているのは &score[i] です。score[i] だけだと要素の値になりますが、&score[i] にするとその要素が置かれている場所を取得できます。

また、配列の要素は連続して配置されるので、float 型のサイズ分ずつアドレスが増えていきます。多くの環境では float は4バイトなので、4ずつ増えていく形になります。

この問題のポイントは次の2つです。

ポイント内容
&score[i] を使う各要素のアドレスを取得するため
for文で順に処理する配列全体をまとめて確認できるため

配列の添字とアドレスの関係を身につける練習として、とてもよい問題です。

実践問題:2次元配列

要素数4×6の short 型配列 table を宣言し、for文を使って先頭行から最後の行までの先頭要素のアドレスを順に表示するプログラムを作成してください。

表示メッセージは日本語にし、最初に 2次元配列tableの各行の先頭要素のアドレス一覧 と表示してください。

解答例

ファイル名:11_2_4.c

#include <stdio.h>

int main(void)
{
    /* 表データを入れる2次元配列 */
    short table[4][6];
    int row;

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

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

    return 0;
}

実行結果例

2次元配列tableの各行の先頭要素のアドレス一覧
table[0]の先頭要素のアドレス: 000000CCDD8800
table[1]の先頭要素のアドレス: 000000CCDD880C
table[2]の先頭要素のアドレス: 000000CCDD8818
table[3]の先頭要素のアドレス: 000000CCDD8824

※ アドレスの値は実行環境によって異なります。

解説

この問題では、2次元配列の 各行の先頭要素のアドレス を表示しています。ここで使うのは &table[row][0] でもよいですが、2次元配列では table[row] 自体がその行の先頭要素のアドレスとして扱われます。

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

printf("table[%d]の先頭要素のアドレス: %p\n", row, (void *)table[row]);

この問題で確認したいのは、1次元配列と2次元配列ではアドレスの取り方の見え方が少し違うという点です。

配列の種類書き方意味
1次元配列&score[i]各要素のアドレス
2次元配列table[row]その行の先頭要素のアドレス
2次元配列&table[row][col]個々の要素のアドレス

さらに、short 型が2バイトであれば、1行は 6 × 2 = 12バイト になるので、各行の先頭アドレスは12バイトずつ離れて並ぶことになります。実行結果例で 0C ずつ増えているように見えるのは、そのためです。

学習のコツ

配列のアドレスを学ぶときは、いきなり難しく考えすぎなくて大丈夫です。まずは次の3つを何度も見比べるのがおすすめです。

見比べたいもの意識すること
配列名 と &配列[0]同じ先頭位置を指しているか
配列[添字] と &配列[添字]値とアドレスの違い
2次元配列名[行] と &2次元配列名[行][0]行の先頭位置との関係

このあたりが自然に読めるようになると、配列とポインタの話がかなり理解しやすくなります。配列のアドレス確認は地味に見えて、C言語の核心に近づく大切な一歩です。