複数の文字列を区切り文字を入れながら連結する | Programming Place Plus C言語編 逆引き

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

このページの概要 🔗

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

以下は目次です。

目的 🔗

複数の文字列があるとき、それぞれを、任意の区切り文字を挟みながら連結したいとします。

たとえば、“aaa”、“bb”、“cccc”、“dd” という4つの文字列があり、区切り文字を ‘/’ とすると、“aaa/bb/cccc/dd” という結果が得られるようにしたいということです。「逆引き 文字列を区切り文字ごとに分割する」の逆方向の操作といえます。

「区切り文字」としていますが、文字列でもいいことにします。


結果の文字列をどこに作るかが問題になります。C言語としては、方向性は2つです。

  1. 連結後の文字数を予測して、十分な大きさの配列を呼び出し側で用意して、そこに結果を入れてもらう
  2. 関数側で動的にメモリを割り当てて対応する

1 の場合、予測を誤るとバッファオーバーフローの危険性があります。2 の場合は、効率が大きく落ちるでしょうし、解放忘れという別の危険性も生じます。

ここでは1の方法を採用して、次のように関数にします。

/*
    文字列を区切り文字を入れながら連結する

    引数
        str_array: 連結する文字列が入った配列
        str_array_size: str_array の要素数
        separator: 区切り文字の並び。NULL不可。空文字列は可能
        result: 結果の文字列を格納する配列。NULL不可
        result_size: result の要素数
    
    戻り値
        連結結果が result に格納できたとき true、
        要素数が不足して格納しきれなかったとき false。
*/
bool string_join(const char* const* str_array, size_t str_array_size, const char* separator, char* result, size_t result_size);

連結処理自体には不要ですが、バッファオーバーフローを検出するために、引数 result_size を導入しています。


仕様の面ではもう1点決めておかなければならないことがあります。それは、str_array が “” のような空文字列を含んでいたときの扱いについてです。たとえば、str_array が次のようになっていたとします。

const char* const str_array[] = {"a", "", "b"};

これを、“/” で区切りながら連結した結果は、次のどちらになることを望むでしょうか?

逆引き 文字列を区切り文字ごとに分割する」では、“a//b” を ‘/’ で分割したときに、“a” と “b” になる方法と、“a” と “” と “b” になる方法をそれぞれ取り上げています。今回は逆方向の操作なので、結果がきちんと対になるように考えるべきでしょう。

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

標準ライブラリには目的を達成するための良い関数がありませんから、自力で実装します。

まず、“a”, ““,”b” の連結結果が “a//b” になる方法で実装してみます。

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

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

/*
    文字列を区切り文字を入れながら連結する

    引数
        str_array: 連結する文字列が入った配列
        str_array_size: str_array の要素数
        separator: 区切り文字の並び。NULL不可。空文字列は可能
        result: 結果の文字列を格納する配列。NULL不可
        result_size: result の要素数
    
    戻り値
        連結結果が result に格納できたとき true、
        要素数が不足して格納しきれなかったとき false。
*/
bool string_join(const char* const* str_array, size_t str_array_size, const char* separator, char* result, size_t result_size)
{
    assert(str_array != NULL);
    assert(separator != NULL);
    assert(result != NULL);
    assert(result_size >= 1);

    const size_t sep_len = strlen(separator);

    size_t i = 0;
    size_t end = 0;

    // 区切り文字を入れながら連結する。
    // 連結する文字列が2つ以上なければ、区切り文字を入れる処理は発生しない。
    if (str_array_size >= 2) {

        // 最後の文字列の後ろには区切り文字を付けないので、
        // str_array_size - 1 まで繰り返す。
        while (i < str_array_size - 1) {

            // バッファオーバーフローしないことを確認する
            if (end + strlen(str_array[i]) + sep_len >= result_size) {
                return false;
            }

            end += sprintf(&result[end], "%s%s", str_array[i], separator);
            ++i;
        }
    }

    // 最後の文字列を連結
    {
        // バッファオーバーフローしないことを確認する
        if (end + strlen(str_array[i]) >= result_size) {
            return false;
        }

        strcpy(&result[end], str_array[i]);
    }

    return true;
}

int main(void)
{
    const char* const str_array1[] = {
        "aaa",
        "bb",
        "cccc",
        "dd"
    };
    const char* const str_array2[] = {
        "a",
        "",
        "b"
    };

    char result1[32];
    char result1_short[4];
    char result2[32];

    if (string_join(str_array1, SIZE_OF_ARRAY(str_array1), "/", result1, sizeof(result1))) {
        puts(result1);
    }

    if (string_join(str_array1, SIZE_OF_ARRAY(str_array1), "/", result1_short, sizeof(result1_short))) {
        puts(result1_short);
    }
    else {
        puts("failed.");
    }

    if (string_join(str_array2, SIZE_OF_ARRAY(str_array2), "/", result2, sizeof(result2))) {
        puts(result2);
    }
}

実行結果:

aaa/bb/cccc/dd
failed.
a//b

「連結する1個目の文字列」「区切り文字」「連結する2個目の文字列」「区切り文字」「連結する3個目の文字列」・・・となるように連結すればいいのですが、必ずしも、文字列連結の代表的な関数である strcat関数を使う必要はありません。特に、strcat関数はその内部で、連結先の文字列の末尾を探す処理が走ることに注意が必要です。考えて使わないと効率が非常に悪くなります。

strcat関数による文字列連結については、「逆引き 文字列を連結する」を参照。

そこで、連結先の文字列の末尾を自力で管理しつつ(変数 end)、その位置に連結するようにします。すべてを自力で書いてもいいのですが、ここでは sprintf関数を使いました。この関数は、実際に書き込まれた文字数を返すので、末尾の位置を管理する作業に都合がいいためです。

少し面倒なのは、連結後の文字列の末尾には区切り文字が付かない点です。(「連結する〇個目の文字列」「区切り文字」)のペアを書き出す処理をループさせると、最後の最後に余計な区切り文字が書き出されてしまいます。そこで、最後の連結だけはループの外に出して、strcpy関数でコピーしています。

なお、バッファオーバーフローのチェックのコードも入っています。このチェックは、sprintf関数や strcpy関数を呼び出す前に行わなければ意味がないことに注意してください。


次に、“a”, ““,”b” の連結結果を “a/b” にする実装を挙げます。

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

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

/*
    文字列を区切り文字を入れながら連結する

    引数
        str_array: 連結する文字列が入った配列
        str_array_size: str_array の要素数
        separator: 区切り文字の並び。NULL不可。空文字列は可能
        result: 結果の文字列を格納する配列。NULL不可
        result_size: result の要素数
    
    戻り値
        連結結果が result に格納できたとき true、
        要素数が不足して格納しきれなかったとき false。
*/
bool string_join(const char* const* str_array, size_t str_array_size, const char* separator, char* result, size_t result_size)
{
    assert(str_array != NULL);
    assert(separator != NULL);
    assert(result != NULL);
    assert(result_size >= 1);

    const size_t sep_len = strlen(separator);

    size_t i = 0;
    size_t end = 0;

    // 区切り文字を入れながら連結する。
    // 連結する文字列が2つ以上なければ、区切り文字を入れる処理は発生しない。
    if (str_array_size >= 2) {

        // 最後の文字列の後ろには区切り文字を付けないので、
        // str_array_size - 1 まで繰り返す。
        while (i < str_array_size - 1) {

            // 空文字列は連結しない
            if (str_array[i][0] != '\0') {

                // バッファオーバーフローしないことを確認する
                if (end + strlen(str_array[i]) + sep_len >= result_size) {
                    return false;
                }

                end += sprintf(&result[end], "%s%s", str_array[i], separator);
            }

            ++i;
        }
    }

    // 最後の文字列を連結
    if (str_array[i][0] != '\0') {

        // バッファオーバーフローしないことを確認する
        if (end + strlen(str_array[i]) >= result_size) {
            return false;
        }

        strcpy(&result[end], str_array[i]);
    }

    // 1文字も連結されない場合に、結果が空文字列になるようにする
    if (end == 0) {
        result[end] = '\0';
    }

    return true;
}

int main(void)
{
    const char* const str_array1[] = {
        "aaa",
        "bb",
        "cccc",
        "dd"
    };
    const char* const str_array2[] = {
        "a",
        "",
        "b"
    };

    char result1[32];
    char result1_short[4];
    char result2[32];

    if (string_join(str_array1, SIZE_OF_ARRAY(str_array1), "/", result1, sizeof(result1))) {
        puts(result1);
    }

    if (string_join(str_array1, SIZE_OF_ARRAY(str_array1), "/", result1_short, sizeof(result1_short))) {
        puts(result1_short);
    }
    else {
        puts("failed.");
    }

    if (string_join(str_array2, SIZE_OF_ARRAY(str_array2), "/", result2, sizeof(result2))) {
        puts(result2);
    }
}

実行結果:

aaa/bb/cccc/dd
failed.
a/b

必要なことは、連結しようとする前に、その文字列が空文字列でないかどうかを確認することです。空文字列かどうかは、文字列の 0文字目が ‘\0’ かどうかを調べれば分かります。

もう1つ注意が必要なのは、すべての連結前の文字列が空文字列だったというケースです。

const char* const str_array[] = {
    "",
    "",
    ""
};

これを連結させると、1回も連結が起こらないことになるので、結果の文字列もまた空文字列になるのが正しいです。最初の実装のままだと、連結後の文字列の末尾の ‘\0’ を付加しているのは、最後に呼び出す strcpy関数ですから、この呼び出しがスキップされてしまうと、‘\0’ が付加されないままになります。

そこで、1文字も連結されなかった場合にだけ、‘\0’ を付加する処理を追加しています。


参考リンク 🔗


更新履歴 🔗



逆引きのトップページへ

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

Programming Place Plus のトップページへ



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