文字列を区切り文字ごとに分割する | Programming Place Plus C言語編 逆引き

トップページC言語編逆引き

このページの概要 🔗

このページの解説は C99 をベースとしています

以下は目次です。

目的 🔗

文字列が、ある特定の文字で区切られた状態になっているとき、その区切りごとに文字列を分割したいとします。

たとえば、“aaa/bb/cccc/dd” という文字があるとき、‘/’ で区切られていると考えれば、“aaa”、“bb”、“cccc”、“dd” という4つの文字列に分割できます。

仕様の面で難しいところがあります。このあと、方法① で、C言語に標準で用意されている方法を紹介しますが、その場合、次のような仕様になります。

  1. 対象の文字列が書き換えられる
  2. “a//b” を ‘/’ で分割したとき、“a” と “b” に分割される
  3. 区切り文字は複数指定できる

3番は便利なことなのでいいのですが、1と2が問題です。

まず1番ですが、“aaa/bb/cccc/dd” を ‘/’ で分割した結果、“aaa\0bb\0cccc\0dd” という状態に書き換えられます。何となく意味が分かると思いますが、要するに分割後の文字列を文字列だとみなせるようにするために、発見した区切り文字を、文字列の末尾に入れるヌル文字(‘\0’) で上書きしているのです。元の文字列が不要ならば何も問題ないのですが、元の文字列を残しておきたいのなら、あらかじめコピーを取っておく必要があります。

2番のほうは、区切り文字が連続して登場するときに、その隙間に空文字列(““) があるとはみなしてくれないということです。これは場面によっては何ら問題ないかもしれませんが、致命的な場合もあるでしょう。


関数化することを考えると、次のような仕様になります。

/*
    文字列を分割する

    引数
        s: 対象の文字列。書き換えが起こる。NULL不可
        separator: 区切り文字の並び。NULL不可
        result: 結果を格納する文字列配列。十分な要素数を確保すること
        result_size: 引数result の要素数

    戻り値
        分割後の文字列の個数
*/
size_t split(char* s, const char* separator, char** result, size_t result_size)

さきほどの問題点のうち、1番のほうは、呼び出し元でコピーしてもらえば対応できますし、気をきかせて関数内でコピーを取ると、コピーが不要な場合に余計なコストになってしまいますから、関数内でのコピーはしないことにします。これを反映して、第1引数は const char* ではなく char* になっています。

2番の問題は、関数の内部実装の問題ですから、とりあえず置いておきます。

第3引数 result は、文字列を指すポインタを要素とする配列を渡します。

char* result[8];  // 最大で8個に分割されると想定
split(s, separator, result, sizeof(result)/sizeof(result[0]));

こうではないことに注意してください。

char result[8][32];  // 最大32文字、最大8個に分割されると想定
split(s, separator, result, sizeof(result)/sizeof(result[0]));  // ?

この場合は result は二次元配列であり、仮引数は char** なので一致していません(第36章)。

方法①(strtok関数を使う) 🔗

標準ライブラリには、文字列分割の作業をサポートする strtok関数があります。サポートすると表現したように、この関数を呼び出せば分割が完了するという簡単なものではありません。

strtok関数は、次のように宣言されています。

char* strtok(char* restrict str, const char* restrict set);

第1引数が対象の文字列ですが、後述するようにヌルポインタを指定する場合もあります。第2引数が区切り文字の並びです。戻り値は、分割後の文字列を指すポインタで、これ以上分割できないときにはヌルポインタが返されます。

この関数の使い方は非常に特殊です。理解のためにまず、文字列を分割する作業を終えるまで、この関数は何度も繰り返して呼び出さなければならないことを知っておきましょう。

1回目の呼び出しでは、第1引数に対象の文字列を指定します。2回目以降は、ここをヌルポインタに変えます。第2引数はいつも同じものを指定してもいいですし、毎回変えても構いません。

2回目以降の呼び出しで、第1引数をヌルポインタに変えなければならないのは、前回の呼び出しで分割した位置の “続きから” 再開させるためです。strtok関数は第1引数がヌルポインタだったときを特別扱いして、“続きから” 分割を再開するのだと判断します。

つまり基本形はこうです。

char* p = strtok(s, separator);
while (p != NULL) {
    // p を使って何かする
    p = strtok(NULL, separator);
}

【上級】なお、strtok関数はその内部で静的変数を使用しています。そのため、一連の分割処理の最中に、ほかの分割処理を並行させると、互いに情報を書き換えあってしまい、正しく動作しません。

では、関数にまとめて試してみます。

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

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

/*
    文字列を分割する

    注意
        s の文字列内に、区切り文字が連続する箇所があるとき、その隙間は無視される。
        たとえば、s が "a//b"、separator が "/" のとき、分割結果は、
        "a" と "b" であり、戻り値は 2 を返す。

    引数
        s: 対象の文字列。書き換えが起こる。NULL不可
        separator: 区切り文字の並び。NULL不可
        result: 結果を格納する文字列配列。十分な要素数を確保すること
        result_size: 引数result の要素数

    戻り値
        分割後の文字列の個数
*/
size_t split(char* s, const char* separator, char** result, size_t result_size)
{
    assert(s != NULL);
    assert(separator != NULL);
    assert(result != NULL);
    assert(result_size > 0);

    size_t i = 0;

    char* p = strtok(s, separator);
    while (p != NULL) {
        assert(i < result_size);
        result[i] = p;
        ++i;

        p = strtok(NULL, separator);
    }

    return i;
}


int main(void)
{
    char s1[] = "aaa/bb/cccc/dd";
    char s2[] = "a//b";
    char* result1[8];
    char* result2[8];
    size_t result_size;

    result_size = split(s1, "/", result1, SIZE_OF_ARRAY(result1));
    for (size_t i = 0; i < result_size; ++i) {
        printf("%zu: %s\n", i, result1[i]);
    }

    puts("----");

    result_size = split(s2, "/", result2, SIZE_OF_ARRAY(result2));
    for (size_t i = 0; i < result_size; ++i) {
        printf("%zu: %s\n", i, result2[i]);
    }
}

実行結果:

0: aaa
1: bb
2: cccc
3: dd
----
0: a
1: b

冒頭で書いたとおり、strtok関数を使った方法は、区切り文字が連続していたときに、その隙間を空文字列とは扱いません。そのため、実行結果の後半部分「0: a」と「1: b」のあいだがありません。

これが問題なら、自力で実装するしかありません。方法② で取り上げます。

方法②(自力で実装する) 🔗

冒頭で書いたとおり、区切り文字が連続していたときに、その隙間に空文字列があると解釈したいケースもあるでしょう。その場合には、strtok関数を使った方法① は使えません。ここでは、自力で文字列分割を行うことで解決を図ってみます。

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

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

/*
    文字列を分割する

    注意
        s の文字列内に、区切り文字が連続する箇所があるとき、その隙間に空文字列があるとみなす。
        たとえば、s が "a//b"、separator が "/" のとき、分割結果は、
        "a" と "" と "b" であり、戻り値は 3 を返す。

        また、s の末尾が区切り文字で終わる場合、最後に空文字列があるとみなす。
        たとえば、s が "a//"、separator が "/" のとき、分割結果は、
        "a" と "" と "" であり、戻り値は 3 を返す。

    引数
        s: 対象の文字列。書き換えが起こる。NULL不可
        separator: 区切り文字の並び。NULL不可
        result: 結果を格納する文字列配列。十分な要素数を確保すること
        result_size: 引数result の要素数

    戻り値
        分割後の文字列の個数
*/
size_t split(char* s, const char* separator, char** result, size_t result_size)
{
    assert(s != NULL);
    assert(separator != NULL);
    assert(result != NULL);
    assert(result_size > 0);

    size_t s_len = strlen(s);
    size_t start = 0;
    size_t end = 0;
    size_t i = 0;

    do {
        // 区切り文字でない文字が何文字続いているか調べ、
        // 変数 end に加算。加算後の位置に区切り文字がある。
        end = start + strcspn(&s[start], separator); 

        // 区切り文字をヌル文字で上書き
        s[end] = '\0';

        // 分割後の文字列の先頭アドレスを result へ格納
        assert(i < result_size);
        result[i] = &s[start];
        ++i;

        // 次に調べる位置を設定
        start = end + 1;

    } while (start <= s_len);   // s の末尾が区切り文字で終わっている場合、
                                // 最後に空文字列があるとみなさせるためには、
                                // start == s_len のときにループを続ける必要がある

    return i;
}


int main(void)
{
    char s1[] = "aaa/bb/cccc/dd";
    char s2[] = "a//b";
    char* result1[8];
    char* result2[8];
    size_t result_size;

    result_size = split(s1, "/", result1, SIZE_OF_ARRAY(result1));
    for (size_t i = 0; i < result_size; ++i) {
        printf("%zu: %s\n", i, result1[i]);
    }

    puts("----");

    result_size = split(s2, "/", result2, SIZE_OF_ARRAY(result2));
    for (size_t i = 0; i < result_size; ++i) {
        printf("%zu: %s\n", i, result2[i]);
    }
}

実行結果:

0: aaa
1: bb
2: cccc
3: dd
----
0: a
1:
2: b

自力で実装するために、strcspn関数の助けを借りました。

strcspn関数は、次のように宣言されています。

size_t strcspn(const char\* s1, const char\* s2);

第1引数の文字列を先頭から順に、第2引数に含まれている文字のいずれにも一致しない文字が何文字続くかを調べる関数です。

たとえば、第1引数が “abcdef”、第2引数が “df” なら、先頭の3文字 “abc” までは一致しないので 3 が返されます。第2引数は “df” という文字列とみなすのではなく、‘d’ と ‘f’ であることに注意してください。

strcspn関数を使って、区切り文字でない文字が何文字続いているかを調べれば、その直後に区切り文字があることが分かります。

もし区切り文字が1種類しかないのなら、シンプルにその区切り文字を探せばいいのですが、複数種類あってもいいという仕様なので、「いずれの区切り文字でもない文字」を探すかたちにしているということです。

区切り文字の場所が分かったら、そこを ‘\0’ で上書きします。これが分割後の文字列の終端を意味することになります。


参考リンク 🔗


更新履歴 🔗



逆引きのトップページへ

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る