文字列を置換する | Programming Place Plus C言語編 逆引き

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

このページの概要

以下は目次です。

目的

文字列の中に特定の文字列があったら、それをほかの文字列に置き換えたいとします。

最初に見つけたものだけを置換するという用途もあり得ますが、ここでは、一致する文字列はすべて置換する方向性にします。たとえば、“abcdabcd” という文字列があるとして、“abc” を “xyz” に置き換えると、“xyzdxyzd” になります。

置き換える文字列と、置き換えた後の文字列の文字数が異なる可能性を考慮しなければなりません。“abc” を “!” に置き換えたら “!d!d” になりますし、“abc” を “?????” に置き換えたら “?????d?????d” になります。

C言語では、配列の要素数を変更することができませんから、文字数が増減することは大問題です。 解決策は大きく分ければ2択です。

  1. 置換後の文字数を予測して、十分な大きさの配列にしておく。あるいは十分な大きさの配列を別途用意して、そちらに結果を入れる
  2. malloc関数や realloc関数を使って、動的にメモリを割り当てて対応する

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

ここでは1の方法を採用し、対象の文字列を直接書き換える方向性にします。よく使われる操作ですから、次のように関数にします。

/*
    文字列を置換する

    引数
        s: 置換対象の文字列。NULL不可
        before: 置き換える文字列。NULL不可。"" の場合は何もしない
        after: 置き換え後の文字列。NULL不可

    戻り値
        s を返す
*/
char* replace(char* s, const char* before, const char* after);


int main(void)
{
    char s[16] = "abcdabcd";

    replace(s, "abc", "?????");
    puts(s);  // "?????d?????d"
}

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

標準ライブラリには、この目的に合った関数がありません。

自力で実装します。

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

/*
    文字列を置換する

    引数
        s: 置換対象の文字列。NULL不可
        before: 置き換える文字列。NULL不可。"" の場合は何もしない
        after: 置き換え後の文字列。NULL不可

    戻り値
        s を返す
*/
char* replace(char* s, const char* before, const char* after)
{
    assert(s != NULL);
    assert(before != NULL);
    assert(after != NULL);

    const size_t before_len = strlen(before);
    if (before_len == 0) {
        return s;
    }

    const size_t after_len = strlen(after);
    char* p = s;

    for (;;) {

        // 置換する文字列を探す
        p = strstr(p, before);
        if (p == NULL) {
            // 見つからなければ、これ以上置換するものはないので終了する
            break;
        }

        // 置換対象にならない位置を計算
        const char* p2 = p + before_len;

        // 置換対象にならない位置(p2) 以降の文字列を、
        // 置換の影響を受けない位置に移動
        memmove(p + after_len, p2, strlen(p2) + 1);

        // 置換する
        memcpy(p, after, after_len);

        // 探索開始位置をずらす
        p += after_len;
    }

    return s;
}


int main(void)
{
    char s[32] = "abcdabcd";

    replace(s, "abc", "?????");
    puts(s);  // "?????d?????d"
}

実行結果:

?????d?????d

置換する文字列を探すには、strstr関数を使います。発見されなくなるまで(ヌルポインタが返されるようになるまで)、forループを繰り返しています。

置換する文字列が発見されたあとの流れは、次の2つです。

  1. 置換対象にならない後続部分を、置換後の文字数を考慮して移動させておく
  2. 置換する

memmove関数を使っているところが1、memcpy関数を使っているところが2にあたります。

コードだけでを眺めても難しいかもしれませんので、s が “abcdefg”、begin が “abc”、after が “?????” と仮定して、流れを説明します。

まず、strstr関数が s の中から “abc” の開始位置を探します。その結果は &s[0] で、変数 p に代入されます。

次に、さきほどの流れ1のために、「置換対象にならない後続部分」の位置を計算します。“abcdefg” の “abc” を置換するので、置換対象にならない位置とは “d” のことです。これは、p + strlen(before) のようにして求められます。この結果を変数 p2 に入れています。

そして、memmove関数を使って、置換の影響を受けない部分の文字列を移動します。これは、置換前後で文字数が増減するからです。短くなるなら、詰め直さないと空きが(しかもそこにはゴミが入っている)できてしまいますし、長くなるなら、後ろに退避しておかないと、置換処理によって上書きされて、一部分が消えてしまいます。

置換した結果は “?????defg” になるので、置換の影響を受けない部分とは &s[5] 以降です。それはつまり、p + after_len のことです。したがって、memmove(p + after_len, p2, strlen(p2) + 1); とすればいいことになります。第3引数の + 1 は終端の ‘\0’ のためです。

次に、実際の置換処理です。置換対象の開始位置は変数 p に入っていますし、置換後の文字列は after ですから、after から p へコピーすればいいだけです。すでに影響範囲の退避は済んでいるので、かまわず memcpy(p, after, after_len); としてコピーします。

これで1回分の置換が終わります。s が指す文字列の中に、まだほかにも before の文字列が含まれているかもしれませんから、for文は次の周回に進みますが、その前に、変数 p の位置を進めておきます(p += after_len;)。

変数 p を進めておかないと、strstr関数の内部処理が前回と同じところから行われるので無駄になるだけでなく、場合によっては for文から抜け出せなくなります。たとえば、置換したい文字列が “abc” で、置換後の文字列が “abcabc” の場合、strstr関数が置換後の “abcabc” に含まれている “abc” を発見するため、それをまた “abcabc” に置換して・・・という無限ループに陥るからです。


参考リンク


更新履歴



逆引きのトップページへ

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

Programming Place Plus のトップページへ



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