C言語編 第25章 配列と文字列

先頭へ戻る

この章の概要

この章の概要です。

配列

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

#include <stdio.h>

int main(void)
{
    char str[40];
    int num1;
    int num2;
    int num3;
    int num4;
    int num5;

    puts( "整数を5回入力して下さい。" );

    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\n" );

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

実行結果:

整数を5回入力して下さい。
3
7
4
1
9


9
1
4
7
3

逆順に出力するという要求に応えるためには、入力された整数をすべて保存しておく必要があります。値を保存するために用意する変数は、それぞれ個別のものになるため、for文を使ってソースコードをまとめることができず、上のような長々としたプログラムになってしまいます。

これまでにも何度か言及していますが、同じ意味合いのソースコードを複数書くべきではありません。何とか、まとめることを考えるべきです。

そこで、この章の主要テーマである、配列の出番です。まずは、配列を使ったプログラムをお見せしましょう。

#include <stdio.h>

#define INPUT_NUM	5		/* 入力回数 */

int main(void)
{
    char str[40];
    int num[INPUT_NUM];
    int i;

    puts( "整数を5回入力して下さい。" );

    for( i = 0; i < INPUT_NUM; ++i ) {
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%d", &num[i] );
    }


    /* 見やすいように、少し間をあけておく */
    printf( "\n\n" );

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

    return 0;
}

実行結果:

整数を5回入力して下さい。
3
7
4
1
9


9
1
4
7
3

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

配列は、変数宣言を行うことで使用できるようになります。配列の宣言は、次のように書きます。

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

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

C99 では、条件付きですが、要素数の指定に変数を用いることができます。

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

配列そのものの型は、配列型と呼ばれます。また、例えば int型の要素を持った配列のことを、int型の配列と表現することがあります。また、要素数も含めて配列型の一部です。つまり、要素数が 5 の int型の配列と、要素数が 10 の int型の配列は、別の型ということになります。

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

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

先ほどのサンプルプログラムでは、要素数が INPUT_NUM の int型の配列を宣言しています。INPUT_NUM は記号定数で、その値は 5 ですから、要素数は 5 ということになります。

配列の各要素を使うためには、[] で表される添字演算子を使います。[] の内側に、結果が整数になる式を入れることができ、これを添字(そえじ)と呼びます。

添字は、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つ以上の初期化子をカンマ区切りで並べます。

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

初期化子の方が多い場合は、コンパイルエラーです。

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

#include <stdio.h>

#define ARRAY_SIZE	10

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

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

    return 0;
}

実行結果:

0 1 2 3 4 5 6 7 8 9

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

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

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

ただ、この方法を使うと、後続の処理の中で、要素数を使いたいときに困ります。要素数は、次の方法で計算できることを覚えておきましょう。

配列の要素数 = 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 };
    int i;
    size_t size;

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

    return 0;
}

実行結果:

0 1 2 3 4 5 6 7 8 9

sizeof演算子が返す型は size_t型ですが、これは符号無し整数型です(第20章)。そのため、int型の変数 i との比較では、符号付き整数と符号無し整数の比較となり、混在してしまうため、非常に悩ましいところではあります。

まず、通常の算術型変換によって、符号無し整数型の方に暗黙的に変換されることを思い出して下さい(第21章)。int型と size_t型で比較すると、size_t型に合わせられることになるでしょう。そのため、まずありえなさそうではありますが、変数 i が負数になることがあれば、正しく動作しません。
あるいは、配列の要素数の方が極端に巨大で、int型で表現できない場合には、このループは終わらなくなるかも知れません。

このケースではあまり考えずに書かれているプログラムが多いですが、C言語でのプログラミングの一般論として、符号の有無の混在には、それなりに検討・検証が必要です。

C99 (要素指示子)

C99 では、要素指示子という機能が追加されています。これを使うと、配列の特定の要素を選んで初期値を与えることができます。

#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" );

    return 0;
}

実行結果:

10 0 0 0 0 0 0 0 0 99

配列array の初期化のところを見て下さい。[0] = 10, のように書けば、array[0] の初期値が 10 になります。同様に、[ARRAY_SIZE - 1] = 99 によって、array[ARRAY_SIZE - 1] の初期値が 99 になります。

この例の場合、array[1]~array[ARRAY_SIZE - 2] は、デフォルトの初期値で初期化されます。

C99 (可変長配列)

C99 (可変長配列)

C99 からは、配列を宣言するときの要素数の指定を変数にできる場面があります。 このような配列は、可変長配列と呼ばれます。 ただし、VisualStudio 2015/2017 は、この機能に対応していません。

まず1つ目の場面は、宣言しようとしている配列が、ブロックスコープかつ自動記憶域期間を持つ場合です。 例えば、普通のローカル変数は該当しますが、static が付いていると静的記憶域期間になるので該当しませんし、 グローバル変数はファイルスコープを持つので該当しません。

可変長配列は、その宣言を行っている箇所に処理が到達したときに、要素数が計算されて確定します。 計算結果は、0 より大きい正の整数にならなければなりません。

なお、可変長配列は、メモリのスタック領域を使用します。この点は、第34章で説明する malloc関数などを使った動的なメモリ割り当てとは異なっています。

可変長配列の使用例は次のようになります。

#include <stdio.h>

int main(void)
{
    /* データ件数を受け取る */
    char buf[40];
    int dataNum;
    puts( "データの件数を入力して下さい。" );
    fgets( buf, sizeof(buf), stdin );
    sscanf( buf, "%d", &dataNum );

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

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

    /* データを受け取る */
    for( int i = 0; i < dataNum; ++i ){
        puts( "データを入力して下さい。" );
        fgets( buf, sizeof(buf), stdin );
        sscanf( buf, "%d", &dataArray[i] );
    }

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

    return 0;
}

実行結果:

データの件数を入力して下さい。
5
データを入力して下さい。
35
データを入力して下さい。
-50
データを入力して下さい。
95
データを入力して下さい。
20
データを入力して下さい。
-45
0: 35
1: -50
2: 95
3: 20
4: -45

C99 では、変数宣言をブロックの先頭で行わなければならないというルールも無くなっているので(第22章参照)、 本当に必要な要素数が分かってから、可変長配列を宣言すれば良い訳です。

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

#include <stdio.h>

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

int main(void)
{
    char buf[40];
    int dataNum;
    puts( "データの件数を入力して下さい。" );
    fgets( buf, sizeof(buf), stdin );
    sscanf( buf, "%d", &dataNum );

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

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

    func( dataNum, array );
    
    return 0;
}

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

実行結果:

データの件数を入力して下さい。
5
999
999
999
999
999

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

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

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

文字列

文字列とは、'\0' という終端を表す文字(ヌル終端文字)で終わる文字の並びです。文字列リテラルは、"" で囲まれた 0文字以上の文字の並びです。"" であっても '\0' は存在しています。

文字列を変数で扱う場合には、文字型の配列を使います。

通常は char型の配列ですが、第47章で取り上げるように、ワイド文字というものを使うときには wchar_t型を用います。

文字型の配列に限り、次のような構文で初期化することが許可されます。

文字型 配列名[要素数] = 文字列リテラル;

{ } で囲む形式に代わって、文字列リテラルを指定します。すると、文字列リテラルに含まれている各文字を1要素として格納します。このとき、'\0' についても格納されますから、それを考慮した要素数が必要です。例えば、以下の2つの配列のうち、str2 の方には問題があります。

char str1[5] = "abcd";
char str2[5] = "abcde";

str1 の方は、'\0' を付加する余裕があるので問題ありませんが、str2 の方は余裕がないのでコンパイルエラーになるかも知れません。 ただし、C言語ではエラーとなる保証はありません。

C++ ではコンパイルエラーになります。

ただ、このような初期化を行うのであれば、要素数を省略してしまった方が安全です。

char str3[] = "abcd";

この場合は、初期値から要素数が判断されるので、きちんと '\0' も含めた、必要な要素数が確保されます。

また、文字型の配列でも、初期値を { } で囲む形で指定することは可能ですがお勧めしません。 1文字ずつ指定する場合、次のように書きます。

char str4[5] = { 'a', 'b', 'c', 'd' };
char str5[5] = { 'a', 'b', 'c', 'd', 'e' };

str4 の方は、要素数5 に対して、初期値の個数が 4つしかありません。 '\0' を格納する余裕があるので、問題なく "abcd\0" という文字列で初期化されます。

これに対して、str5 の方は、明示的な初期値だけで 5つの要素を使い切ってしまいます。 この場合、コンパイルエラーにならず、単に '\0' が無いまま初期化されます。 これは、文字列としての要件を満たしていないため危険であるといえます。


文字型の配列に、文字列を代入する場合には、以下のような構文が使えません。

str = "abcde";

これができない理由は、第32章で取り上げます。正しい方法は、代わりに strcpy関数を使うことです。

strcpy( str, "abcde" );

strcpy関数は、string.h をインクルードして使用します。第1引数に代入先の文字型配列を、第2引数に代入したい文字列を指定します。

strcpy関数は、代入先の配列が十分な要素数を持っているかどうかを確認しないことに注意が必要です。 いつものように、バッファオーバーフローを起こす危険性に注意しなければなりません。


練習問題

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

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

char str[] = "abcdef";

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

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

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


解答ページはこちら

参考リンク

更新履歴

'2018/5/18 「配列の初期化」の項を修正。
-- 明示的に初期化しなかったとき、静的記憶域期間を持つ場合は初期化されることを追記。

'2018/5/11 用語を統一(文字列定数 -> 文字列リテラル)

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/2/28 全面的に文章を見直し、修正を行った。
第34章から「C99 (可変長配列)」の項を移動してきた。内容も修正。
「範囲外アクセス」「配列のメモリイメージ」「配列を引数や戻り値にするには」の項を削除し、 これらの項の内容を「配列」の項に入れ込んだ。

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





前の章へ(第24章 プリプロセッサ)

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

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS