C言語のきほん|制御文で文字列を処理する

以下に、記事としてそのまま使いやすい形で、親しみやすくやわらかい口調でまとめます。

文字列は、1文字ずつ見ればもっとよく分かる。
制御文を使いこなして、文字列処理の基本をしっかり身につけよう。

C言語の文字列は、ほかの多くのプログラミング言語のような専用の文字列型ではなく、char型の配列として扱われます。
この仕組みは最初は少し独特に見えるかもしれませんが、そのぶん 1文字ずつ細かく調べたり、条件に応じて処理を変えたりしやすい という大きな特徴があります。

たとえば文字列を扱う場面では、

  • 文字数を数える
  • 特定の文字が何回出てくるか調べる
  • 先頭から順番に見て条件判定する
  • 前からと後ろから比べる
  • 複数の文字列の中から条件に合うものを探す

といった処理がよく出てきます。

このような処理では、for文や while文で1文字ずつ順番に見ていき、その中で if文を使って判定していくのが基本になります。
つまり、文字列処理では制御文がとても重要 なのです。

特にC言語では、文字列の終わりを表すナル文字 \0 があるため、ループでは「どこまで処理するか」を \0 で判断する場面がたくさん出てきます。
この感覚に慣れると、文字列をただの文章としてではなく、文字が順番に並んだ配列として自然に見られるようになります。

この記事では、制御文を使って文字列を処理する基本を、シンプルな例と一緒にじっくり見ていきます。
さらに後半では、実践問題として、文字の個数を数える処理、前後比較、複数文字列の比較、置換処理まで扱います。
文字列操作の基礎力をしっかり育てる内容なので、1つずつ丁寧に見ていきましょう。

文字列は1文字ずつ処理できる

C言語の文字列は char型配列なので、添字を使えば1文字ずつ取り出せます。

たとえば、次のような文字列があったとします。

char word[] = "Hello";

このとき、配列の中身は次のように並んでいます。

添字
word[0]'H'
word[1]'e'
word[2]'l'
word[3]'l'
word[4]'o'
word[5]'\0'

このように、文字列は「文字の並び」として保存されています。
そのため、for文を使えば先頭から順番に確認できますし、if文を組み合わせれば条件による判定もできます。

なぜ \0 まで処理するのか

文字列の長さを調べたり、中の文字を全部見たりするとき、C言語では \0 に出会うまで処理する という書き方が基本になります。

なぜなら、C言語では \0 が文字列の終わりを表すからです。

たとえば "Hello" という文字列を見ているとき、コンピュータは次のように判断します。

  1. H を読む
  2. e を読む
  3. l を読む
  4. l を読む
  5. o を読む
  6. \0 を見つける
  7. ここで終わりだと判断する

このため、文字列処理では次のような条件がよく出てきます。

str[i] != '\0'

これは「今見ている文字が終端文字ではない間、処理を続ける」という意味です。

サンプルプログラムで基本を確認しよう

元のサンプルでは文字列の長さを数えていましたが、ここでは別のシンプルな例として、
入力された文字列の中に母音 a が何個含まれているかを数えるプログラム
に変更して説明します。

表示メッセージも日本語にし、コメントも日本語にしています。

サンプルプログラム

ファイル名:10_14_1.c

#include <stdio.h>

int main(void)
{
    char text[100];
    int count_a = 0;

    printf("文字列を入力してください(最大99文字)> ");
    scanf("%99s", text);

    /* 文字列の先頭から順番に調べる */
    for (int i = 0; text[i] != '\0'; i++) {
        if (text[i] == 'a') {
            count_a++;
        }
    }

    printf("文字'a'は %d 個含まれています。\n", count_a);

    return 0;
}

実行結果例

文字列を入力してください(最大99文字)> banana
文字'a'は 3 個含まれています。

このサンプルプログラムの見方

このプログラムでは、文字列全体を1文字ずつ調べています。

入力部分

scanf("%99s", text);

ここでは、最大99文字まで入力できるようにしています。
配列の大きさは100なので、最後の \0 の分を考えて 99 を指定しています。

これはとても大事な書き方です。
入力フィールド幅を指定しないと、長すぎる入力で配列の範囲を超える危険があります。

ループ部分

for (int i = 0; text[i] != '\0'; i++) {

ここでは、text[i] が \0 になるまでループしています。
つまり、文字列の終わりまで1文字ずつ見ていくわけです。

判定部分

if (text[i] == 'a') {
    count_a++;
}

今見ている1文字が a なら、個数を1つ増やします。
このように、ループの中で if文を使うと、条件に合う文字だけを数えられます。

文字列処理でよく使う制御文

文字列を処理するときに特によく使う制御文を整理すると、次のようになります。

制御文主な役割
for文先頭から順番に1文字ずつ調べる
while文条件が成り立つ間くり返す
if文特定の文字かどうか判定する
if ~ else条件に応じて処理を分ける
if ~ else if ~ else複数条件で分岐する

文字列は配列なので、配列処理と同じようにループと相性が良いです。
そして、文字の内容によって処理を変えたいので if文ともよく組み合わさります。

先頭から \0 まで1文字ずつ見ていく流れ

文字列の長さを数える考え方

文字列の長さを求めるときも、基本の流れは同じです。
\0 に出会うまで、1文字ずつ数えればよいだけです。

たとえば、考え方は次のようになります。

文字count
1文字目を読む1
2文字目を読む2
3文字目を読む3
\0 に到達終了

C言語では通常、文字列の長さは strlen を使うことが多いですが、こうして自分で数える方法を学ぶと、文字列処理のしくみがよく分かります。
また、自分でループを書く方法なら、「大文字だけ数える」「数字だけ数える」といった応用もできます。

元の「指定した文字が何個含まれるかを数える問題」と似た内容として、
入力文字列の中に母音 e が何回含まれるかを数える問題 を作れます。

実践問題

入力した文字列中に文字 e がいくつ含まれるかを数えるプログラムを作成してください。

解答例

ファイル名:10_14_2.c

#include <stdio.h>

int main(void)
{
    char text[100];
    int count_e = 0;

    printf("文字列を入力してください(最大99文字)> ");
    scanf("%99s", text);

    for (int i = 0; text[i] != '\0'; i++) {
        if (text[i] == 'e') {
            count_e++;
        }
    }

    printf("文字'e'は文字列中に %d 回含まれています。\n", count_e);

    return 0;
}

実行結果例

文字列を入力してください(最大99文字)> reference
文字'e'は文字列中に 3 回含まれています。

解説

この問題では、文字列全体を先頭から順番に見て、e だったときだけ count_e を増やしています。
考え方はとても素直で、次の流れです。

手順内容
1文字列を入力する
2先頭から \0 まで1文字ずつ見る
3e ならカウントする
4最後に個数を表示する

この問題は、ループの中で1文字ずつ判定する 練習としてとても良い内容です。

実践問題

入力された文字列の先頭の文字と最後の文字が同じかどうかを判定して表示するプログラムを作成してください。

解答例

ファイル名:10_14_3.c

#include <stdio.h>

int main(void)
{
    char text[100];
    int length = 0;

    printf("文字列を入力してください(最大99文字)> ");
    scanf("%99s", text);

    for (int i = 0; text[i] != '\0'; i++) {
        length++;
    }

    if (text[0] == text[length - 1]) {
        printf("先頭の文字と最後の文字は同じです。\n");
    } else {
        printf("先頭の文字と最後の文字は異なります。\n");
    }

    return 0;
}

実行結果例1

文字列を入力してください(最大99文字)> level
先頭の文字と最後の文字は同じです。

実行結果例2

文字列を入力してください(最大99文字)> hello
先頭の文字と最後の文字は異なります。

解説

この問題では、まず文字列の長さを数えています。
最後の文字の位置は、長さが length なら length - 1 です。

たとえば hello なら、

文字添字
h0
e1
l2
l3
o4

このとき、先頭は text[0]、最後は text[4]、つまり text[length - 1] になります。

回文判定ほど複雑ではありませんが、
文字列の長さを使って末尾の位置を求める 練習になります。

実践問題

文字列 "Lion", "Elephant", "Cat", "Giraffe", "Tiger" の中から、最も短い文字列を見つけるプログラムを作成してください。

解答例

ファイル名:10_14_4.c

#include <stdio.h>

int main(void)
{
    char animals[][10] = {"Lion", "Elephant", "Cat", "Giraffe", "Tiger"};
    int min_index = 0;
    int min_length = 0;

    for (int j = 0; animals[0][j] != '\0'; j++) {
        min_length++;
    }

    for (int i = 1; i < 5; i++) {
        int length = 0;

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

        if (length < min_length) {
            min_length = length;
            min_index = i;
        }
    }

    printf("最も短い文字列は %s です。長さは %d 文字です。\n",
           animals[min_index], min_length);

    return 0;
}

実行結果例

最も短い文字列は Cat です。長さは 3 文字です。

解説

この問題では、文字列の数だけ外側のループ があり、
各文字列の長さを数える内側のループ があります。

つまり、二重の考え方になっています。

ループ役割
外側のループどの文字列を調べるか
内側のループその文字列が何文字か数える

最初の文字列 Lion の長さを初期値として使い、その後の文字列と比較しながら、より短いものが見つかったら更新しています。

実践問題

次の手順で文字列を変換するプログラムを作成してください。

  1. 文字列と、置き換え前の文字、置き換え後の文字を入力する
  2. 入力文字列の中で、置き換え前の文字をすべて見つけて、置き換え後の文字に変更する
  3. 結果を表示する

ただし、文字列中に空白は含まれないものとします。

解答例

ファイル名:10_14_5.c

#include <stdio.h>

int main(void)
{
    char text[200];
    char old_ch;
    char new_ch;

    printf("文字列を入力してください(最大199文字)> ");
    scanf("%199s", text);

    printf("置き換え前の文字を入力してください > ");
    scanf(" %c", &old_ch);

    printf("置き換え後の文字を入力してください > ");
    scanf(" %c", &new_ch);

    for (int i = 0; text[i] != '\0'; i++) {
        if (text[i] == old_ch) {
            text[i] = new_ch;
        }
    }

    printf("元の条件で変換した結果: %s\n", text);

    return 0;
}

実行結果例1

文字列を入力してください(最大199文字)> banana
置き換え前の文字を入力してください > a
置き換え後の文字を入力してください > o
元の条件で変換した結果: bonono

実行結果例2

文字列を入力してください(最大199文字)> hello
置き換え前の文字を入力してください > z
置き換え後の文字を入力してください > x
元の条件で変換した結果: hello

解説

この問題では、文字列を先頭から最後まで見ながら、
もし今見ている文字が old_ch と同じなら new_ch に置き換えています。

処理の流れを整理すると、次のようになります。

手順内容
1文字列を入力する
2置換前と置換後の文字を入力する
3先頭から順番に1文字ずつ調べる
4一致したら置き換える
5最後に結果を表示する

この問題は、文字の比較と代入を組み合わせた文字列処理 の練習になります。
元の文字列全体を別配列へ組み立てる置換問題への橋渡しとしても、とても良い内容です。

確認問題

次の項目について、正しいものには○、間違っているものには×をつけてください。

① 文字列リテラルで初期化した char配列には、終端文字 \0 が自動的に追加される。
② scanf で %s を使うと、空白を含む文章全体をそのまま読み取ることができる。
③ 文字列の長さを自分で数えるときは、\0 に出会うまでループすればよい。
④ char型配列の1つの要素には、文字列全体を格納できる。
⑤ 2次元配列の words[i] は、i 行目の文字列として扱える。
⑥ 配列は = 演算子を使って、あとから文字列全体を直接代入できる。
⑦ 文字列中の特定の文字を数える処理では、for文と if文の組み合わせがよく使われる。
⑧ "A" は1文字を表す文字定数である。
⑨ char str[3] = "ABC"; は、文字列として安全に扱うにはサイズ不足である。
⑩ 文字列の最後の文字を調べるには、長さを求めて length - 1 の位置を参照する考え方が使える。

解答と解説

① ○:文字列リテラルで初期化した char配列には、文字列の終わりを示す \0 が自動的に追加されます。たとえば ABC なら A、B、C、\0 の4要素が必要です。

② ×:%s は空白で入力を区切ります。そのため、空白を含む文章全体はそのまま読み取れません。

③ ○:C言語の文字列は \0 で終わるので、長さを数えるときは \0 に達するまで1文字ずつ数えれば求められます。

④ ×:char型配列の1要素に入るのは1文字です。文字列全体を表すには、複数の char が並んだ配列が必要です。

⑤ ○:2次元の char配列では、1行が1つの文字列になります。そのため words[i] は i 行目の文字列として扱えます。

⑥ ×:配列に対して、あとから = 演算子で文字列全体を直接代入することはできません。後から文字列を入れたいときは strcpy などを使います。

⑦ ○:文字列の中に特定の文字が何回出てくるかを調べるときは、for文で1文字ずつ見て、if文で一致判定する方法が基本です。

⑧ ×:"A" は文字列リテラルです。1文字を表す文字定数は 'A' のように書きます。

⑨ ○:ABC は3文字ですが、文字列として扱うには終端文字 \0 も必要なので、最低でも4要素必要です。str[3] では足りません。

⑩ ○:文字列の長さが length なら、最後の有効な文字の位置は length - 1 です。この考え方は先頭文字と末尾文字の比較などでよく使います。

解説補足 sizeof演算子

ここで、配列や型の大きさを調べるときによく使う sizeof演算子についても整理しておきましょう。
sizeof は、変数や型が何バイトの大きさを持つか を調べるための演算子です。

文字列処理や配列の学習では、配列全体の大きさや要素数を考える場面でとても役立ちます。

データ型の大きさを調べる

書き方は次のようになります。

size_t n = sizeof(int);

このように書くと、int型が何バイトかを調べられます。

使用例

ファイル名:10_14_6.c

#include <stdio.h>

int main(void)
{
    printf("char型  : %zu バイト\n", sizeof(char));
    printf("int型   : %zu バイト\n", sizeof(int));
    printf("double型: %zu バイト\n", sizeof(double));

    return 0;
}

実行結果例

実行環境によって違いはありますが、たとえば次のようになります。

char型  : 1 バイト
int型   : 4 バイト
double型: 8 バイト

式や変数の大きさを調べる

sizeof は、型名だけでなく、変数や配列にも使えます。

char c;
int numbers[5];

printf("変数cの大きさ: %zu バイト\n", sizeof(c));
printf("配列numbers全体の大きさ: %zu バイト\n", sizeof(numbers));

このようにすると、変数1つ分の大きさや、配列全体の大きさが分かります。

配列の要素数を求める考え方

配列の要素数を求めたいときは、次の形がよく使われます。

sizeof(配列) / sizeof(配列[0])

たとえば、

int numbers[5];

なら、

sizeof(numbers) / sizeof(numbers[0])

で 5 を求められます。

これは、

  • 配列全体の大きさ
  • 1要素分の大きさ

を割り算しているからです。

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

書き方意味
sizeof(int)int型の大きさ
sizeof(c)変数 c の大きさ
sizeof(arr)配列全体の大きさ
sizeof(arr) / sizeof(arr[0])配列の要素数

%zu を使う理由

sizeof の結果は size_t 型になります。
そのため、printf で表示するときは %zu を使います。

printf("%zu\n", sizeof(int));

ここも大切なポイントです。
整数だからといって %d を使うのではなく、sizeof の結果には %zu を使うと覚えておきましょう。

学習のポイント

制御文で文字列を処理するときは、次の考え方を意識すると理解しやすいです。

ポイント内容
文字列は char配列1文字ずつ見られる
\0 が終わりの印そこまで処理する
for文で順に調べる文字列全体を走査できる
if文で条件判定する特定文字の数え上げや置換ができる
長さを使うと後ろからも見られる末尾との比較に応用できる

このあたりが自然に使えるようになると、文字列処理の基礎がかなりしっかりしてきます。
その先で strlen や strcpy などのライブラリ関数を学ぶときにも、「中で何をしているのか」が見えやすくなります。