C言語基礎|文字列の配列

文字列がたくさん登場したら、配列で並べる?それともポインタで束ねる?――選び方でメモリ効率も自由度も変わる!

文字列が1つだけなら、char 配列や char * で十分でした。
でも現実のプログラムでは「メニュー一覧」「エラーメッセージ一覧」「曜日名」「コマンド名」みたいに、文字列の集まりを扱うことがよくあります。

そこで登場するのが 文字列の配列です。
実は文字列の表現には「配列による文字列」と「ポインタによる文字列」がありましたよね。
文字列の集まりも同じで、次の2通りで実現できます。

  • 配列による文字列の配列(2次元配列:文字の箱を並べる)
  • ポインタによる文字列の配列(ポインタ配列:文字列の先頭住所を並べる)

この違いを、表と図でしっかり整理していきましょう。

文字列の配列は2通りある

2つの実現方法の全体比較

実現方法宣言例正体文字列の長さメモリの特徴よくある用途
配列による文字列の配列char a[][6] = {"DOG", "CAT", "BIRD"};char の2次元配列(固定長の箱×個数)最大長に合わせて固定すべて連続配置、短い文字列は空きが出る書き換える予定がある文字列群、固定サイズで扱いたい
ポインタによる文字列の配列const char *p[] = {"RED", "GREEN", "BLUE"};ポインタの配列(住所札の集合)バラバラでOK文字列本体は別領域、並びは連続とは限らない表示専用の定数文字列、差し替えや並び替えをしたい

この表の説明

  • 左は「箱を並べる」ので 整列して管理しやすいけど空きが出やすい
  • 右は「住所札を並べる」ので 効率よく可変長を扱えるけど配置は散らばる
    という違いです。

サンプルプログラム

プロジェクト名:chap11-3-1 ソースファイル名:chap11-3-1.c

#include <stdio.h>

int main(void)
{
    char  animal[][6] = {"DOG", "CAT", "BIRD"};          // 配列による文字列の配列(最大5文字+終端)
    const char *color[] = {"RED", "GREEN", "BLUE"};      // ポインタによる文字列の配列

    puts("【配列で持つ文字列一覧】");
    for (int i = 0; i < 3; i++)
        printf("animal[%d] = %s\n", i, animal[i]);

    puts("【ポインタで持つ文字列一覧】");
    for (int i = 0; i < 3; i++)
        printf("color[%d] = %s\n", i, color[i]);

    return 0;
}

実行結果(例)

【配列で持つ文字列一覧】
animal[0] = DOG
animal[1] = CAT
animal[2] = BIRD
【ポインタで持つ文字列一覧】
color[0] = RED
color[1] = GREEN
color[2] = BLUE

配列による文字列の配列(2次元配列)

どういう構造?

宣言はこうでした。

  • 形式:char 変数名[行数][列数];
  • 今回:char animal[][6]

ここでの「列数 6」は、1つの文字列を入れる箱の大きさです。
"DOG" は 3文字ですが、終端の '\0' が必要なので 最低4
今回は余裕を持って 6 にしています(最大5文字+'\0')。

2次元配列(箱が連続して並ぶ)

この図の説明

  • 各行 animal[i] が「固定長の文字列ボックス」
  • 短い文字列だと末尾に 未使用領域が残る
  • その代わり、全体は きっちり連続配置されます。

メモリ量のイメージ

  • 占有サイズ:行数×列数 バイト
    例:3×6 = 18 バイト(char は1バイト想定)

ポインタによる文字列の配列(ポインタ配列)

どういう構造?

宣言はこうでした。

  • 形式:char *変数名[要素数];
  • 今回:const char *color[]

この配列の中身 color[i] は、文字列そのものではなく 先頭文字へのポインタです。
つまり「文字列の住所札が3枚並んでいる」状態です。

ポインタ配列(住所札+文字列本体)

この図の説明

  • color の配列領域:ポインタが3つ並ぶ。
  • 文字列本体:別の場所(静的領域)に置かれることが多い。
  • 文字列同士が隣り合うとは限らないので、前後関係を当てにできません

メモリ量のイメージ

  • ポインタ配列:sizeof(char *) × 要素数
  • 文字列本体:各リテラルのバイト数(終端 '\0' 含む)の合計
    ※ sizeof(char *) は環境依存(64bit環境なら8バイトが多い)です。

2つの [ ] で2次元っぽく見える理由

ポインタ配列 color は「2次元配列ではない」のに、color[i][j] のように書けます。

color[i][j] の意味

表記実際に起きていること
color[i]i番目の文字列の先頭を指すポインタ
color[i][j]i番目の文字列の j文字目(= *(color[i] + j))

この表の説明

  • 最初の [i] で「どの文字列か」を選ぶ
  • 次の [j] で「その文字列の何文字目か」を選ぶ
    という2段階のアクセスになっています。

どちらを選ぶ?判断のコツ

選び方の実務目線

やりたいことおすすめ理由
文字列を書き換えたい2次元配列配列の中身は自分の領域なので安全に編集しやすい。
文字列の長さがバラバラで無駄を減らしたいポインタ配列必要な長さだけ確保され、空きが出にくい。
文字列の並び替えをしたいポインタ配列住所札を並べ替えるだけで済む。
サイズが固定で扱いやすくしたい2次元配列1行の長さが一定で処理が単純になる。

登場する命令(関数)の書式と役割

puts

  • 書式:puts(文字列);
  • 何をする命令?:文字列を表示して、最後に改行を付ける
  • 特徴:printf よりシンプル(書式指定はできない)

printf

  • 書式:printf(書式文字列, 引数1, 引数2, ...);
  • 何をする命令?:書式に従って表示する
  • 今回の主役:%s(char へのポインタを受け取り、'\0' まで表示)

演習問題

演習11-2

サンプルプログラムの for 文の 3 を埋め込みにせず、計算で求めるように書きかえよ。

解答例(配列要素数を計算)

#include <stdio.h>

int main(void)
{
    char  animal[][6] = {"DOG", "CAT", "BIRD"};
    const char *color[] = {"RED", "GREEN", "BLUE"};

    int animal_count = (int)(sizeof(animal) / sizeof(animal[0]));
    int color_count  = (int)(sizeof(color)  / sizeof(color[0]));

    puts("【配列で持つ文字列一覧】");
    for (int i = 0; i < animal_count; i++)
        printf("animal[%d] = %s\n", i, animal[i]);

    puts("【ポインタで持つ文字列一覧】");
    for (int i = 0; i < color_count; i++)
        printf("color[%d] = %s\n", i, color[i]);

    return 0;
}

解説(ここが大事)

  • sizeof(animal) は「2次元配列全体のバイト数」
  • sizeof(animal[0]) は「1行分(char[6])のバイト数」
  • 割り算で「行数」が出ます

同様に color も、配列全体 ÷ 1要素(ポインタ1個)で要素数が出ます。
※この方法は「配列として見えている範囲」で有効です。関数の引数にすると配列はポインタに変換されるので注意です。