C言語の基本|ポインタの配列で複数の文字列を扱う

複数の文字列は、文字そのものではなく、文字列への入口を並べるとすっきり管理できます。

C言語では、1つの文字列を配列やポインタで扱う方法を理解すると、その次に複数の文字列をまとめて管理する方法が見えてきます。

たとえば、曜日名、色名、メニュー項目、エラーメッセージなど、複数の文字列をひとまとめにして扱いたい場面はよくあります。

C言語では、複数の文字列を管理する代表的な方法として、次の2つがあります。

  • 2次元配列で管理する方法
  • ポインタの配列で管理する方法

2次元配列は、文字を行と列に並べて持つ方法です。
一方、ポインタの配列は、それぞれの文字列の先頭を指すポインタを配列として並べる方法です。

どちらも、配列名[i] の形で i 番目の文字列を扱えるため、最初はよく似て見えます。
しかし、内部で何を持っているのか、文字列の長さをどう扱うのか、文字列を書き換えられるのか、といった点には違いがあります。

たとえば、短い文字列と長い文字列が混在している場合、2次元配列では最も長い文字列に合わせて各行の大きさをそろえる必要があります。
そのため、短い文字列では使わない領域が増えることがあります。

一方、ポインタの配列では、それぞれの要素が別々の文字列の先頭アドレスを持ちます。
そのため、文字列ごとに長さが違っていても扱いやすいという特徴があります。

この記事では、ポインタの配列で複数の文字列を扱う方法について、次の内容を順番に見ていきます。

  • ポインタの配列とは何か
  • ポインタの配列の宣言
  • 2次元配列との違い
  • 複数の文字列を表示する方法
  • 文字列を1文字ずつ処理する方法
  • ポインタの配列が向いている場面

ポインタの配列とは

まずは、ポインタの配列という言葉の意味を整理してみましょう。

ポインタの配列とは、ポインタを要素として持つ配列です。

たとえば、3つの文字列をまとめて扱いたい場合は、次のように書けます。

const char *colors[3] = {"Red", "Blue", "Green"};

この宣言では、colors という配列の中に3つのポインタがあります。

それぞれのポインタは、次の文字列リテラルの先頭を指しています。

  • colors[0] は Red の先頭を指す
  • colors[1] は Blue の先頭を指す
  • colors[2] は Green の先頭を指す

表にすると、次のように整理できます。

要素持っている情報
colors[0]Red の先頭アドレス
colors[1]Blue の先頭アドレス
colors[2]Green の先頭アドレス

ここで重要なのは、colors の中に Red や Blue の文字そのものが入っているわけではないことです。

colors が持っているのは、それぞれの文字列が置かれている場所を示すアドレスです。

つまり、ポインタの配列は、複数の文字列そのものを並べるのではなく、複数の文字列への入口を並べて管理する仕組みだと考えると分かりやすくなります。

ポインタの配列の宣言を読む

次の宣言を見てみましょう。

const char *colors[3];

最初は少し複雑に見えますが、部分ごとに考えると理解しやすくなります。

char は文字を表す型です。

その前に付いている * は、char 型のデータを指すポインタであることを表します。

そして、

colors[3]

は、colors が3個の要素を持つ配列であることを表しています。

したがって、

const char *colors[3];

は、

char 型の文字列を指すポインタを3個持つ配列

と考えられます。

また、const が付いている点にも注目しましょう。

const char *colors[3] = {"Red", "Blue", "Green"};

この例では、各ポインタが文字列リテラルを指しています。

文字列リテラルは書き換えないものとして扱うため、const char * を使うことで、指している先の文字を変更しないことを明確にできます。

図1:ポインタの配列で複数の文字列を指す

この図から分かること

この図を見ると、colors の各要素が文字そのものを持っているのではなく、それぞれ別の文字列リテラルの先頭を指していることが分かります。

たとえば、colors[0] の中には Red という文字列そのものが格納されているのではありません。
colors[0] には、Red の先頭である R が置かれている場所のアドレスが入っています。

同じように、colors[1] は Blue、colors[2] は Green の先頭を指しています。

このように、ポインタの配列では、文字列そのものではなく、文字列の場所をまとめて管理します。

2次元配列との違い

複数の文字列は、2次元配列でも管理できます。

たとえば、次のように書けます。

char colors[3][6] = {"Red", "Blue", "Green"};

これは、3行6列の char 型配列を用意し、各行に1つずつ文字列を格納しています。

一方、ポインタの配列では次のようになります。

const char *colors_p[3] = {"Red", "Blue", "Green"};

どちらも3つの文字列を扱っていますが、内部の構造は異なります。

項目2次元配列ポインタの配列
配列が持つもの文字そのもの文字列の先頭アドレス
各文字列の領域同じ列数を確保する文字列ごとに異なる長さでよい
短い文字列の空き領域発生しやすい発生しにくい
文字列の変更可能文字列リテラルなら行わない
向いている用途編集する文字列読み取り中心の文字列

見た目では似ていますが、2次元配列は文字を自分の配列の中に持ちます。

ポインタの配列は、文字列が置かれている場所を指します。

この違いを理解しておくことが大切です。

2次元配列では各行の大きさがそろう

たとえば、次の2次元配列を考えてみましょう。

char foods[3][13] = {"Bread", "Pie", "Cheesecake"};

この配列では、すべての行に13文字分の領域が用意されます。

最も長い Cheesecake を格納するためには、文字列の最後に必要な \0 も含めて十分な列数を用意しなければなりません。

すると、Bread や Pie のような短い文字列でも、同じ13文字分の領域を持つことになります。

イメージすると、次のようになります。

格納される内容のイメージ
0行目B r e a d \0 \0 \0 ...
1行目P i e \0 \0 \0 \0 ...
2行目C h e e s e c a k e \0 ...

2次元配列では、すべての行の大きさが固定されます。

そのため、文字列の長さに大きな差があると、短い文字列では使用しない領域が増えます。

ポインタの配列では文字列ごとの長さをそろえなくてよい

同じ文字列をポインタの配列で管理すると、次のように書けます。

const char *foods_p[3] = {"Bread", "Pie", "Cheesecake"};

foods_p の各要素には、それぞれの文字列リテラルの先頭アドレスが格納されます。

つまり、

  • foods_p[0] は Bread の先頭を指す
  • foods_p[1] は Pie の先頭を指す
  • foods_p[2] は Cheesecake の先頭を指す

という構造になります。

それぞれの文字列は、同じ長さにそろえる必要がありません。

そのため、文字数のばらつきが大きい文字列をまとめて扱うときは、ポインタの配列が使いやすいことがあります。

ただし、ポインタの配列では、文字列とは別にポインタ自体を保存する領域が必要です。

そのため、必ずポインタの配列のほうがメモリ使用量が少なくなるわけではありません。

文字列の長さや個数によって変わるため、単純にどちらが常に優れているとは言えない点も覚えておきましょう。

図2:2次元配列とポインタの配列の違い

この図から分かること

2次元配列では、それぞれの行が同じ大きさになります。

そのため、長い文字列に合わせて列数を決めると、短い文字列の後ろに使われない領域ができます。

一方、ポインタの配列では、各要素が別々の文字列を指しています。

そのため、文字列ごとに長さが違っていても、すべて同じ大きさの行を用意する必要はありません。

この違いから、

  • 2次元配列は文字そのものをまとめて持つ
  • ポインタの配列は文字列への参照をまとめて持つ

という違いが見えてきます。

2次元配列とポインタの配列で文字列を表示する

ここでは、2次元配列とポインタの配列を使って、複数の文字列を表示するプログラムを見てみましょう。

今回は、3つの駅名を管理する例にします。

ファイル名:11_8_1.c

2次元配列とポインタの配列で駅名を表示するプログラム
#include <stdio.h>

int main(void)
{
    /* 2次元配列で駅名を管理する */
    char stations[3][8] = {"Tokyo", "Ueno", "Shibuya"};

    /* ポインタの配列で駅名を管理する */
    const char *stations_p[3] = {"Tokyo", "Ueno", "Shibuya"};

    printf("2次元配列で管理した文字列を表示します\n");

    for (int i = 0; i < 3; i++) {
        printf("%s\n", stations[i]);
    }

    printf("\nポインタの配列で管理した文字列を表示します\n");

    for (int i = 0; i < 3; i++) {
        printf("%s\n", stations_p[i]);
    }

    return 0;
}
実行結果の例
2次元配列で管理した文字列を表示します
Tokyo
Ueno
Shibuya

ポインタの配列で管理した文字列を表示します
Tokyo
Ueno
Shibuya

2次元配列を表示している部分

まずは、次の部分を見てみましょう。

for (int i = 0; i < 3; i++) {
    printf("%s\n", stations[i]);
}

stations は2次元配列です。

そのため、

stations[i]

は、i 行目の先頭を表します。

たとえば、

stations[0]

は、Tokyo が入っている0行目の先頭です。

stations[1]

は、Ueno が入っている1行目の先頭です。

printf の %s は、渡された先頭アドレスから \0 が見つかるまで文字を読み続けます。

そのため、

printf("%s\n", stations[i]);

と書くことで、各行に格納された文字列を表示できます。

ポインタの配列を表示している部分

次に、ポインタの配列を見てみましょう。

for (int i = 0; i < 3; i++) {
    printf("%s\n", stations_p[i]);
}

stations_p[i] は、i 番目のポインタ要素です。

各要素には、文字列リテラルの先頭アドレスが格納されています。

たとえば、

stations_p[0]

には、Tokyo の先頭アドレスが入っています。

そのため、

printf("%s\n", stations_p[0]);

とすると、Tokyo が表示されます。

両者を比べると、次のようになります。

書き方意味
stations[i]2次元配列の i 行目の先頭
stations_p[i]i 番目のポインタが持つ文字列の先頭アドレス

どちらも printf の %s に渡せるため、実行結果は同じです。

しかし、2次元配列では行の先頭を渡し、ポインタの配列ではポインタに格納されたアドレスを渡しているという違いがあります。

ポインタの配列で文字列を1文字ずつ処理する

ポインタの配列で管理している文字列は、文字列単位で表示するだけでなく、1文字ずつ読み取ることもできます。

ここでは、3つの動物名を1文字ずつ表示してみましょう。

ファイル名:11_8_2.c

ポインタの配列で動物名を1文字ずつ表示するプログラム
#include <stdio.h>

int main(void)
{
    const char *animals[3] = {"Tiger", "Dog", "Panda"};

    printf("文字列を1文字ずつ表示します\n");

    for (int i = 0; i < 3; i++) {
        for (int j = 0; *(animals[i] + j) != '\0'; j++) {
            printf("%c", *(animals[i] + j));
        }

        printf("\n");
    }

    return 0;
}
実行結果の例
文字列を1文字ずつ表示します
Tiger
Dog
Panda

animals[i] で文字列を選ぶ

このプログラムでは、外側の for 文で処理する文字列を順番に切り替えています。

for (int i = 0; i < 3; i++)

animals の各要素は、次の文字列を指しています。

要素指している文字列
animals[0]Tiger
animals[1]Dog
animals[2]Panda

したがって、

animals[i]

は、i 番目の文字列の先頭アドレスを表します。

i が0なら Tiger、i が1なら Dog、i が2なら Panda の先頭を指します。

animals[i] + j で文字列の中を進む

次に、内側の for 文を見てみましょう。

for (int j = 0; *(animals[i] + j) != '\0'; j++)

animals[i] は、選ばれた文字列の先頭アドレスです。

そこに j を加えることで、文字列の中を1文字ずつ進めます。

たとえば、animals[0] が Tiger を指している場合を考えてみます。

指している位置
animals[0] + 0T
animals[0] + 1i
animals[0] + 2g
animals[0] + 3e
animals[0] + 4r
animals[0] + 5\0

そして、

*(animals[i] + j)

とすることで、その場所に格納されている文字を取り出せます。

*(animals[i] + j) の意味

式を3段階に分けて整理すると、次のようになります。

意味
animals[i]i 番目の文字列の先頭アドレス
animals[i] + ji 番目の文字列の j 文字目のアドレス
*(animals[i] + j)i 番目の文字列の j 文字目

つまり、この処理では、

  • i でどの文字列を読むか決める
  • j でその文字列の何文字目を読むか決める
  • * で実際の文字を取り出す

という流れになっています。

\0 が見つかるまで繰り返す

内側の for 文では、次の条件を使っています。

*(animals[i] + j) != '\0'

C言語の文字列では、最後に \0 が格納されています。

そのため、この条件は、

現在の文字が \0 ではない間、処理を続ける

という意味になります。

たとえば Tiger では、T、i、g、e、r と順番に読み、最後の \0 に到達したところで内側の for 文が終了します。

図3:ポインタの配列で文字列を1文字ずつ参照する

この図から分かること

この図では、ポインタの配列から文字を取り出すまでの流れを二段階で確認できます。

まず animals[i] を使って、どの文字列を処理するのかを決めます。

そのあと、animals[i] + j で、その文字列の中を1文字ずつ進みます。

最後に * を使うことで、その位置にある文字を読み取ります。

つまり、ポインタの配列を使った文字列処理では、

  • i で文字列を選択する
  • j で文字列の中を移動する

という考え方が重要になります。

ポインタの配列が向いている場面

ポインタの配列は、特に文字列の長さにばらつきがある場合に便利です。

たとえば、次のような文字列をまとめて扱うことを考えてみましょう。

  • OK
  • Warning
  • CriticalError

2次元配列で扱う場合は、最も長い CriticalError に合わせて、すべての行の大きさを決める必要があります。

そのため、OK のような短い文字列では使わない領域が多くなります。

一方、ポインタの配列では、それぞれの文字列を別々に指せるため、各文字列の長さをそろえる必要がありません。

読み取り専用の文字列一覧にも向いている

ポインタの配列は、内容を書き換えず、表示や比較に使う文字列の一覧にも向いています。

たとえば、次のような用途です。

  • メニュー項目
  • 曜日名
  • 月名
  • 色名
  • 固定メッセージ
  • エラーメッセージ

こうした文字列は、プログラムの中で内容を変更せず、決められた文字列を読み出して使うことが多くあります。

そのような場合は、

const char *messages[3];

のようなポインタの配列を使うと、読み取り専用であることも分かりやすくなります。

文字列を書き換えたい場合は2次元配列が向いている

一方、文字列の中身を変更したい場合は、2次元配列のほうが扱いやすいことがあります。

たとえば、

char names[3][10] = {"Tom", "Ken", "Amy"};

のように文字列を2次元配列に格納しておけば、

names[0][0] = 'B';

のように文字を書き換えることができます。

すると、Tom の先頭文字 T が B に変わるため、文字列は Bom になります。

これに対して、

const char *names[3] = {"Tom", "Ken", "Amy"};

では、各要素が文字列リテラルを指しているため、指している先の文字列を書き換えないようにします。

したがって、文字列の内容を編集する可能性があるかどうかも、2次元配列とポインタの配列を選ぶ基準になります。

2次元配列とポインタの配列の使い分け

使い分けの目安を整理すると、次のようになります。

条件向いている方法
文字列の長さにばらつきが大きいポインタの配列
読み取り専用の文字列をまとめたいポインタの配列
文字列の内容を書き換えたい2次元配列
各行の大きさを固定して扱いたい2次元配列
固定メッセージや一覧を管理したいポインタの配列
配列の中に文字そのものを持たせたい2次元配列

どちらが必ず優れているということではありません。

重要なのは、

文字そのものを配列に持たせたいのか、それとも文字列への参照を並べたいのか

という違いです。

ポインタの配列は、1本のポインタで1つの文字列を指す考え方を、そのまま複数の文字列へ広げたものです。

1本のポインタで1つの文字列を指せるなら、そのポインタを配列に並べれば、複数の文字列をまとめて管理できます。

この考え方を意識すると、

const char *names[3];

という宣言も、単なる難しい記号の並びではなく、

文字列を指すポインタを3個並べたもの

として自然に読めるようになります。