トラック9・🔤文字列

文字列処理の定石パターン

読む目安 約13分・ゴール:文字列を1文字ずつ走査する型を身につけ、数える・削る・逆順にするが書ける

アプリでこのトピックを学ぶ(無料・登録不要)

このトピックの要点

文字列処理も、配列と同じで「型」で覚えます。土台になるのは、このfor文です。

for (int i = 0; s[i] != '\0'; i++) {
    // s[i] に対する処理
}

条件が i < n ではなく s[i] != '\0' になっているのがポイントです。長さを知らなくても、終わりの印に出会うまで進めばよい。これがCの文字列の走査の基本形です。

配列のときは i < n と要素数で止めました。文字列で要素数がなくても止まれるのは、終わりの印(ヌル文字)が配列の中に書かれているからです。裏を返せば、印のない配列を回すと止まりません。この形が安全に動くのは「渡された文字列が正しく終端されている」という前提があってこそです。

定石1: 数える

#include <stdio.h>

int count_char(const char s[], char target)
{
    int cnt = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == target) {
            cnt++;
        }
    }
    return cnt;
}

int main(void)
{
    printf("%d\n", count_char("banana", 'a'));   // 3
    return 0;
}

特定の文字を数える、条件に合う文字を数える——if の中身を変えるだけで応用が利きます。

定石2: 入力の改行を取り除く

fgets で読み込むと、末尾に改行 '\n' が付いてきます。これを消すのが定番の処理です。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char line[64];

    if (fgets(line, sizeof(line), stdin) != NULL) {
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n') {
            line[len - 1] = '\0';        // 改行の位置を終端に置き換える
        }
        printf("[%s]\n", line);
    }
    return 0;
}

'\0' を上書きするだけで文字列が短くなる、というのがCらしい発想です。長さの情報がどこにも無いので、印の位置がすべてを決めます。len > 0 の確認を入れているのは、空文字列のときに line[-1] を踏まないためです。

定石3: 逆順にする

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "stressed";
    int n = (int)strlen(s);

    for (int i = 0; i < n / 2; i++) {
        char t = s[i];
        s[i] = s[n - 1 - i];
        s[n - 1 - i] = t;
    }
    printf("%s\n", s);      // desserts
    return 0;
}

両端から1つずつ入れ替え、真ん中まで進んだら終わり。i < n にすると2回入れ替えて元に戻ってしまうので、n / 2 が正解です。ここでも '\0' は動かさないことに注目してください。終端の位置は変わらないので、文字列としての長さもそのままです。

定石4: 状態を覚えながら走査する

空白で区切られた単語の数を数えたいとき、空白の個数を数えるだけでは正しくありません。空白が連続していたり、先頭や末尾に空白があったりするからです。そこで「いま単語の中にいるか」を int in_word のような変数で覚えておき、空白でない文字が空白の次に現れた回数を数えます。走査しながら状態を持つ、という発想は文字列処理でくり返し登場します。1文字だけを見て判断できないときは、たいてい状態変数の出番だと考えてください。

罠・注意

ビット「'\0' まで回す」——この1行が書ければ、文字列処理はもう怖くないよ

まとめ: 走査は s[i] != '\0'、短くするのは '\0' の書き込み、入れ替えは両端から。配列で覚えた型が、そのまま文字列でも生きます。

アプリでこのトピックを学ぶ(無料・登録不要)

確認問題

確認問題 1

文字列の中の 'a' を数えるループです。空欄を埋めてください(cnt欄はその周が終わった時点の値)。

#include <stdio.h>

int main(void)
{
    char s[] = "banana";
    int cnt = 0;

    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == 'a') {
            cnt++;
        }
    }
    printf("%d\n", cnt);
    return 0;
}
is[i]cnt
0'b'0
1'a'1
2'n'1
3'a'?
4'n'?
5'a'?
6'\0'3 (ループ終了)
答えを見る

空欄の答え(上から順に):2、2、3

banana には 'a' が3つあります。i=3 で2個目、i=5 で3個目を数え、'n' の周では増えません。i=6 で s[6] が '\0' になり、条件が偽になってループが終わります。文字数を知らなくても末尾まで走査できるのが、この形の利点です。

確認問題 2

文字数を表示したいのですが、5ではない大きな数が出たり、実行のたびに結果が変わったりします。原因の行はどれですか。

1#include <stdio.h>
2#include <string.h>
3
4int main(void)
5{
6    char s[5] = "hello";
7
8    printf("%zu\n", strlen(s));
9    return 0;
10}
答えを見る

バグのある行:6行目

"hello" は5文字なので '\0' を含めて6バイト必要ですが、配列は5個しかありません。終わりの印が入らないため、strlen は配列の外まで読み続けます。6行目を char s[6] = "hello"; または char s[] = "hello"; に直します。

char s[5] = "hello"; はコンパイルが通ってしまいますが、'\0' が入りません。終端のない文字列を strlen や printf に渡すと、たまたま見つかった0まで読み進める境界外アクセスになります。配列の大きさは必ず「文字数 + 1」で確保します。

「文字列」の目次

  1. 文字列とはchar配列である
  2. ヌル終端 '\0' の掟
  3. 文字列リテラルと初期化
  4. string.h の主要関数
  5. 安全な文字列操作——バッファサイズの意識
  6. 文字の判定と変換(ctype.h)
  7. 文字コードの基礎——ASCIIとUTF-8
  8. 文字列処理の定石パターン

もっと先へ:ポインタ・メモリ・ファイル入出力・セキュアコーディングを含む全26トラックと、段位検定・模試のフルセットは完全版に収録しています。

完全版の販売ページは準備中です。

ほかのトラック

📚 姉妹教材:手を動かして覚える Linux 教材 — Linuxとインフラの仕組みを地図で学ぶ

Web版