配列 | Programming Place Plus C言語編 第25章

トップページC言語編

このページの概要

以下は目次です。


配列

標準入力から5つの整数を受け取り、それを受け取った順番の逆になるように出力したいとします。この章までの知識だけで、このようなプログラムを書くと、次のようになるでしょう。

#include <stdio.h>

int main(void)
{
    puts("Please enter an integer 5 times.");

    char str[40];
    int num1;
    int num2;
    int num3;
    int num4;
    int num5;

    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &num1);

    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &num2);

    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &num3);

    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &num4);

    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &num5);


    // 逆順に出力
    printf("\n");
    printf("%d\n", num5);
    printf("%d\n", num4);
    printf("%d\n", num3);
    printf("%d\n", num2);
    printf("%d\n", num1);
}

実行結果:

Please enter an integer 5 times.
3  <-- 入力された内容
7  <-- 入力された内容
4  <-- 入力された内容
1  <-- 入力された内容
9  <-- 入力された内容

9
1
4
7
3

逆の順番で出力するという目的を実現するためには、まず整数の入力が5個とも終了しなければなりません。そのため、入力された整数をいったんどこかに保存しておく必要があります。5つの整数を保存する保存のため、用意する変数の個数も5個ということになります。

実現はできるものの、別個の変数が5個あるという状態は、さきほどのサンプルプログラムのように、長々としたコードになりがちです。また、入力する整数を5個から10個に変更したいとなったら、プログラムはさらに巨大になるでしょう。

これまでにも何度か書いたとおり、同じ意味合いのコードを何度も繰り返し書くべきではありません。そこで、この章の主要テーマである、配列 (array) を使います。まずは、配列を使ったプログラムをお見せしましょう。

#include <stdio.h>

#define INPUT_NUM   5       // 入力回数

int main(void)
{
    puts("Please enter an integer 5 times.");

    int num[INPUT_NUM];

    for (int i = 0; i < INPUT_NUM; ++i) {
        char str[40];

        fgets(str, sizeof(str), stdin);
        sscanf(str, "%d", &num[i]);
    }

    // 逆順に出力
    printf("\n");
    for (int i = INPUT_NUM - 1; i >= 0; --i) {
        printf("%d\n", num[i]);
    }
}

実行結果:

Please enter an integer 5 times.
3  <-- 入力された内容
7  <-- 入力された内容
4  <-- 入力された内容
1  <-- 入力された内容
9  <-- 入力された内容

9
1
4
7
3

配列は、同じ型の要素を連続的に並べたものです。要素 (element) というのは、配列を構成している1つ1つの変数のことです。また、連続的に並んでいるというのは、実際にコンピュータのメモリ上でも、隙間なく並んでいるということを意味しています。

配列自体も変数なので、配列を使うには、まず宣言を書きます。配列の宣言は、次のように書きます。

要素の型 配列名[要素数];

配列を宣言するときには、その配列に含まれている要素の個数を指定しなければなりません。要素数は 0 より大きい整数型でなければならず、通常は、定数式である必要もあります。

条件付きで、要素数を変数で指定する方法があります。これは、後の項で取り上げます

宣言時に初期化子を与えて、明示的に初期化することもできます。これについては、後の項で取り上げます

配列そのものは、配列型 (array type) という型です。また、たとえば int型の要素を持った配列のことを、int型の配列のように表現します。なお、要素数の指定も配列型の構成要素です。つまり、要素数が 5 の int型の配列と、要素数が 10 の int型の配列は、別の型とみなされます。

第26章で登場する構造体型と合わせて、集成体型と呼ぶこともあります。

実のところ、これまでの章のプログラムでも配列は登場しています。fgets関数で文字列を受け取るときに使っていた char str[40]; のような変数がそれです。これは、要素数が 40 の char型の配列という意味です。つまり、char型の要素が 40個並んだものですから、40文字分の文字を取り扱える変数を作ったことになります。

配列の要素を、1つの変数として取り扱うためには、[] で表される添字演算子 (subscript operator) を使います。[] の内側には、結果が整数になる式を指定でき、これを添字(そえじ) (subscript) と呼びます。

添字は、0以上かつ、その配列の要素数未満の整数でなければなりません。この範囲外の添字を使って、配列の要素にアクセスする行為は未定義の動作なので、避けなければなりません。たとえば、要素数 5 の配列であれば、添字として使える範囲は 0~4 です。配列の一番末尾にある要素を表す添字は、「要素数 - 1」であることを確認してください。

【上級】厳密には、添字が負数であることが許されないわけではなく、配列の範囲外をアクセスすることに問題があります。たとえば、array[5] のメモリアドレスを指すポインタ p があるとき、p[-3] のような指定は有効です(ポインタは第31章で登場します)。

配列を使うことで、複数の変数を1箇所に集めるることによって、for文で処理をまとめられたという点は注目すべきです。for文を使って、添字を変化させながら、配列の要素へのアクセスを繰り返すことで、配列の各要素を順番にアクセスできます。配列と for文を組合せたこのような処理は、使用頻度が非常に高い、重要な考え方です。確実に理解してください。


添字は式になっていても構わないので、以下のような指定も可能です。

int index = 5;
array[index + 3];   // array[8] を意味する

なお、引数や戻り値に配列を使いたいときもありますが、配列を直接的に、関数と受け渡しすることはできません。これが必要なときには、ポインタという機能を使います。ポインタについては第31章で説明することにします。また、実際に配列を受け渡す例は、第33章で取り上げます。

配列の初期化

配列に、明示的に初期値を与えなかった場合の状態は、配列でない変数の場合と同じです。つまり、自動記憶域期間を持つのなら不定値であり、静的記憶域期間を持つのなら暗黙的に初期化されます第22章)。

配列の宣言と同時に初期値を与えるには、次のように書きます。

要素の型 配列名[要素数] = {初期化子並び};

{} の内側に、1つ以上の初期化子, で区切りながら並べます。

【C++プログラマー】C言語では {} の内側を空にはできません。すべての要素を 0 で初期化するには int a[] = {0}; のように、最低1つは 0 を書く必要があります。

指定した初期化子の方が、指定した「要素数」よりも少ない場合は、不足した部分にデフォルトの初期値が与えられます。デフォルトの初期値は、静的記憶域期間の変数に与えられるデフォルトの初期値の規則と同様で、大ざっぱにいえば 0 です(第22章1

int array[5] = {3, 4, 5};  // 3,4,5,0,0 で初期化

反対に、指定した「要素数」より初期化子の方が多いという記述は許されておらず、コンパイルエラーになります2

int array[5] = {0, 1, 2, 3, 4, 5};  // コンパイルエラー


次のサンプルプログラムは、配列の宣言時に初期化子を与えている例です。

#include <stdio.h>

#define ARRAY_SIZE  10

int main(void)
{
    int array[ARRAY_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (int i = 0; i < ARRAY_SIZE; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

実行結果:

0 1 2 3 4 5 6 7 8 9

すべての要素に明示的に初期値を与える場合には、要素数の指定を省略できます

要素の型 配列名[] = {初期化子,};

この場合、{} の中に記述した初期値の個数が、要素数であるとみなされます。要素数の指定を間違えないので、この方法の方が安全です。

ただ、この方法を使うと、後続の処理の中で、要素数を使いたいときに困ります。たとえば、for文を使いたいとき、繰り返し回数=要素数であることが多いはずです。このようなときに備えて、次の方法で要素数を計算できることを覚えておくといいです。

配列の要素数 = sizeof(配列) / sizeof(配列の要素);

sizeof演算子に配列名を指定した場合、配列全体の大きさになります。また、添字演算子を使って1つの要素だけを sizeof に渡した場合は、その要素単体の大きさになります。ですから、「全体の大きさ / 1つの大きさ」によって、要素数が求められるということです。

この計算は非常によく使うので、関数形式マクロを作っておくことが多いです。第28章の練習問題で取り上げています。

次のプログラムは、要素数を求めて使用する例です。

#include <stdio.h>

int main(void)
{
    int array[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    const size_t size = sizeof(array) / sizeof(array[0]);

    for (int i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

実行結果:

0 1 2 3 4 5 6 7 8 9

sizeof演算子が返す型は size_t型ですが、これは符号無し整数型です(第19章)。そのため、int型の変数 i との比較では、符号付き整数と符号無し整数の比較となることが悩ましい問題です。

このプログラムでは、変数i が負数になりえないので大丈夫ですが、int型と size_t型の比較は、第21章で解説したような問題を起こす可能性があります。たとえば、size_t型が unsigned int型である処理系では、int型と unsigned型の比較になるので、通常の算術変換(第21章)により、int型の側が unsigned int型に変換されてから比較されます。そのため、int型の側が負数だった場合、巨大な正の整数になってしまい、想定外の比較結果を生み出します。

【上級】あるいは、配列array の要素数が、int型で表現できないほど極端に巨大な場合、変数i の値が、変数size の値にまで到達できないため、for文を終わらせることができないことになります。

問題にならないことも多いため、あまり考えずに書かれているプログラムが多いですが、C言語でのプログラミングの一般論として、符号の有無の混在には注意を払わなければなりません。たとえば、変数i も size_t型にする意味はあります。

#include <stdio.h>

int main(void)
{
    int array[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    const size_t size = sizeof(array) / sizeof(array[0]);

    for (size_t i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

実行結果:

0 1 2 3 4 5 6 7 8 9

可変長配列

配列の要素数の指定に定数式以外、つまり変数を使うことができる場面があります。このような配列は、可変長配列 (variable length array、VLA) と呼ばれます。ただし、Visual Studio 2015/2017/2019/2022 は、この機能に対応していません。

【C89/95 経験者】この機能は C99 で追加されたものです。

【C11】この機能はオプションとなり、提供しない処理系も許されるように変更されました。可変長配列を提供しない場合、1 に置換される STDC_NO_VLA という事前定義マクロが定義されます。3

【C++ プログラマー】この機能は C++ では使えません。

可変長配列の要素数は、その宣言を行っている箇所に処理が到達したときに、式を評価して決定します。1度確定した要素数は変化しませんが、同じ宣言を何度も通過する場合、そのたびに異なる要素数になる可能性はあります。なお、要素数は 0 より大きい正の整数にならなければなりません。

可変長配列の要素数が 0以下の場合、具体的にどうなるかは定義されていません。

可変長配列が使える1つ目の場面として、宣言しようとしている配列が、ブロックスコープ(第22章)かつ自動記憶域期間(第21章)を持つ場合です。たとえば次のように使います。

#include <stdio.h>

int main(void)
{
    puts("Please enter the number of data.");

    char buf[40];
    fgets(buf, sizeof(buf), stdin);
    int data_num;
    sscanf(buf, "%d", &data_num);

    // データ件数が 0件以下なら終了
    if (data_num <= 0) {
        return 0;
    }

    // 件数に合わせた大きさの可変長配列を宣言
    int data_array[data_num];

    // データを受け取る
    for (int i = 0; i < data_num; ++i) {
        puts("Please enter the integer.");
        fgets(buf, sizeof(buf), stdin);
        sscanf(buf, "%d", &data_array[i]);
    }

    // 結果を出力
    for (int i = 0; i < data_num; ++i) {
        printf("%d: %d\n", i, data_array[i]);
    }
}

実行結果:

Please enter the number of data.
5  <-- 入力された内容
Please enter the integer.
35  <-- 入力された内容
Please enter the integer.
-50  <-- 入力された内容
Please enter the integer.
95  <-- 入力された内容
Please enter the integer.
20  <-- 入力された内容
Please enter the integer.
-45  <-- 入力された内容
0: 35
1: -50
2: 95
3: 20
4: -45

このように、本当に必要な要素数が分かってから、可変長配列を宣言すればいいです。

可変長配列が使用できる2つ目の場面は、関数プロトタイプスコープを持つ場合です。つまりは、関数の他の仮引数を要素数の指定に使えます。

#include <stdio.h>

void print_array(int n, int array[n]);

int main(void)
{
    puts("Please enter the number of data.");

    char buf[40];
    fgets(buf, sizeof(buf), stdin);
    int data_num;
    sscanf(buf, "%d", &data_num);

    // データ件数が 0件以下なら終了
    if (data_num <= 0) {
        return 0;
    }

    int array[data_num];
    for (int i = 0; i < data_num; ++i) {
        array[i] = 999;
    }

    print_array(data_num, array);
}

void print_array(int n, int array[n])
{
    for (int i = 0; i < n; ++i) {
        printf("%d\n", array[i]);
    }
}

実行結果:

Please enter the number of data.
5  <-- 入力された内容
999
999
999
999
999

前に書いたとおり、配列は直接的に引数としては使えません。これは可変長配列であっても同様です。ですから実は、仮引数に可変長配列を指定しても、実質的にそこで指定した要素数は意味を成していません。そこで、添字の部分を * としても、可変長配列であることを表現できます。これができるのは、関数プロトタイプスコープの場合だけなので、関数宣言では可能ですが、関数定義では不可能です。

// 関数宣言
void print_array(int n, int array[*]);

// 関数定義
void print_array(int n, int array[n])
{
}

要素指示子

要素指示子 (designated initializer) という機能を使うと、配列の特定の要素を選んで初期値を与えられます。この機能は配列以外でも使える場面がありますが、ここでは配列についての解説にとどめます。

【C89/95 経験者】この機能は C99 で追加されたものです。

【C++ プログラマー】C++20 で、{} を使った集成体初期化の際に、要素指示子を使えるようになりましたが、配列に対しては使えません。4

次のサンプルプログラムは、要素指示子の使用例です。

#include <stdio.h>

#define ARRAY_SIZE  10

int main(void)
{
    int array[ARRAY_SIZE] = {
        [0] = 10,
        [ARRAY_SIZE - 1] = 99
    };

    for (int i = 0; i < ARRAY_SIZE; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

実行結果:

10 0 0 0 0 0 0 0 0 99

配列array の初期化のところを見てください。[0] = 10 のように、どの要素に対する初期化子であるのかを明示的に書くことによって、array[0] に対して 10 を与えることができます。[ARRAY_SIZE - 1] = 99 のように計算式を使うことも可能です。

初期値が明示されていない array[1]array[ARRAY_SIZE - 2] の範囲の要素については、静的記憶域期間の場合に与えられるデフォルトの初期値の規則と同様に、0 に相当する値で初期化されます

要素指示子による初期化と、通常の初期化は同時に使用できます。

#include <stdio.h>

#define ARRAY_SIZE  10

int main(void)
{
    int array[ARRAY_SIZE] = {
        [0] = 10,
        11,
        12,
        [5] = 50,
        51
    };

    for (int i = 0; i < ARRAY_SIZE; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

実行結果:

10 11 12 0 0 50 51 0 0 0

要素指示子による初期化の次に書いた要素指示子を使わない初期化は、続きの要素に対する初期化とみなされます。そのため、11array[1] の初期化子であり、51array[6] に対する初期化子です。

配列の要素数を明示的に指定しなかった場合の要素数は、初期値を与えた要素の中で一番大きい添字から決定されます。

#include <stdio.h>

int main(void)
{
    int array[] = {
        [0] = 10,
        11,
        12,
        [5] = 50,
        51
    };

    printf("%zu\n", sizeof(array) / sizeof(array[0]));
}

実行結果:

7

この場合、51 を与えた array[6] の添字が一番大きいので、要素数は 7 になります。

要素指示子を使うと、同じ要素に対して、複数回初期値を与えるコードが書けてしまう落とし穴があります。

#include <stdio.h>

int main(void)
{
    int array[] = {
        [5] = 50,
        [4] = 40,
        51          // [5] = 51 を意味している
    };

    size_t size = sizeof(array) / sizeof(array[0]);

    for (size_t i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

実行結果:

0 0 0 0 40 51

51 という初期化子は、要素指示子による array[4] の初期化の直後にありますから、array[5] に対するものとみなされます。そのため、array[5] にはすでに 50 を与えているはずですが、上書きされて 51 になります。

これは初期化の指定を、添字の昇順に並ぶように行えば問題は起こりませんが、添字を記号定数で与えていると、ぱっと見で気付きにくいので注意が必要です。

複合リテラル

複合リテラル (compound literal) を使うと、配列型のリテラルを記述できます。この機能を解説するには、ポインタという別の機能の前提知識が必要ですので、第32章であらためて取り上げます。

【C89/95 経験者】この機能は C99 で追加されたものです。


練習問題

問題① 要素数10 の int型配列に、2 から始まる 2 のべき乗を順番に格納し、それを逆の順番で表示するプログラムを作成してください。

問題② 次のように文字型の配列を定義します。

char str[] = "abcdef";

この文字列を 1文字ずつ改行しながら出力するプログラムを作成してください。

問題③ 問題②と同じ内容で、文字型の配列の初期値を次のように変えた場合、どうなるでしょう?

char str[] = "abc\0def";

問題④ 次のような配列があります。

#define ARRAY_SIZE 5
int values[ARRAY_SIZE] = {13, 27, 75, 27, 48};

この配列の中に、同じ値が重複して含まれているかどうかを調べるプログラムを作成してください。

問題⑤ 次のような配列があります。

#define ARRAY_SIZE 5
int values1[ARRAY_SIZE] = {-17, 8, 29, -5, 13};
int values2[ARRAY_SIZE] = {64, -5, 17, -22, -38};

2つの配列の両方に同じ値が含まれているかどうかを調べるプログラムを作成してください。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第24章 複数ファイルによるプログラム)

次の章へ (第26章 構造体)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る