[C言語] 文字列の置換を実装してみる


C言語でごにょごにょするとまだまだ理解が浅いなーと思わされます。
今回は文字列の置換部分を、色々な記事を参考に自作してみました。
（だいぶコピペ部分あるけど・・）

ちなみに文字列の連結部分も、理解を深めるために記事を参考に実装。
またそれ以外にもヒープ領域に新しく領域を作ってコピーを返す形のものも作ってみました。

まずは今回のソースを。

## ソースコード

```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/**
 *  文字列を連結する
 * 
 *  引数で受け取る文字列は「配列型」を期待
 *  逆にポインタ型で定義するとエラーが発生するので注意
 */
char* str_concat(char *str1, const char *str2) {

    // str1の最初のアドレスを保持しておく
    char *top = str1;
    
    // 文字列終端までポインタを進める
    while (*(str1++) != '\0');
    
    // `\0`分を消すため、ポインタをひとつ戻す（連結のため）
    str1 -= 1;

    // 終端直前から、str2の文字を追加していく
    do {
        *(str1++) = *str2;
    } while (*(str2++) != '\0');

    return str1;
}

/**
 *  mallocを使ってヒープに領域を確保するパターン
 */
char* str_concat2(const char *str1, const char *str2, char **result) {

    // 連結後の文字列分の領域を確保する（+1は終端文字（\0）分）
    size_t size = sizeof(char) * (strlen(str1) + strlen(str2) + 1);
    char *work = (char*)malloc(size);
    if (work == NULL) {
        printf("Cannot allocate memory.\n");
        return NULL;
    }
    
    // 確保した領域の最初のアドレスを保持しておく
    char *top = work;

    // str1をコピー
    strcpy(work, str1);
    
    // 追加した文字数分、ポインタを進める
    work += strlen(str1);
    
    // str2をコピー
    strcpy(work, str2);

    // 確保した先頭アドレスを結果に格納
    *result = top;

    return top;
}

/**
 *  文字列を置換する
 *
 *  @param src 評価する文字列
 *  @param target  置き換え対象となる文字列
 *  @param replace 置き換える文字列
 *  @param result  置換後の文字列を返すポインタ
 */
void str_replace(const char *src, const char *target, const char *replace, char **result) {

    char *temp = (char*)malloc(sizeof(char) * 1000);
    if (temp == NULL) {
        printf("Cannot allocate memory.\n");
        return;
    }

    char *top = temp;

    // 操作できるようにコピーする
    char *work = (char*)malloc(sizeof(char) * strlen(src));
    strcpy(work, src);

    char *p;
    while ((p = strstr(work, target)) != NULL) {
        // 検知した位置に文字列終端文字を挿入
        *p = '\0';
        
        // 検知した位置＋対象文字数分の位置にポインタを移動
        p += strlen(target);

        // 該当文字列以降をいったんtempに退避
        strcpy(temp, p);

        // 前半文字列と置き換え文字列を連結する
        str_concat(work, replace);
        str_concat(work, temp);
    }
    
    free(temp);

    *result = work;
}


int main() {
    char src[100] = "1. this is a test.";
    char *replace = "t";
    char *target  = "|";
    char *result;

    str_replace(src, replace, target, &result);
    printf("%s\n", result); // => "1. |his is a |es|."
    
    free(result);

    return 0;
}
```

`str_concat2` がコピーを作るタイプのものです。
（ちなみにメモリの使用状況とかは適当なので、実際に使う場合はそのあたりの制御を追加しないとなりません）


## 解説

置換処理は `str_replace` 関数です。
引数に取るのは `置換対象の文字列` `置換したい文字列` `置換する文字列` `結果を返すポインタ` となっています。

今回の実装をやるにあたって少しハマったのが文字列定数の扱いです。
以下のように書くとコンパイル時に定数化され、それがread onlyの領域に書き込まれます。
それを操作すると `bus error` などのエラーになります。

そのあたりについてはこちらの記事（[C言語の配列とポインタの違いのお話など](http://tamacona.hatenablog.com/entry/2013/05/13/005722)）が分かりやすく解説してくれていました。

なので、今回の実装では受け取った文字列をいったんコピーしたのち、そのコピーに対して処理をする、という実装になっています。


### 処理の流れ

処理の大まかな流れは、まず、置き換えたい文字列の位置を探し出し、置き換えたい文字列を抜かした前と後ろでふたつの文字列に分割します。
そうした上で、「前部分＋置き換える文字列＋後部分」と連結することによって置換を実現しています。

C言語の場合、文字列の終端は `\0` が現れる箇所になります。
なので、以下のようにすることで文字列の終わりを強制的に書き込んでやり、その仕組みを利用する形になっています。

```c
*p = '\0';
```

文字列の連結については自作の関数を使っています。


### 図解

簡単に図解すると以下のイメージです。

`This_is_a_test.` という文字列の `is` を `was` に置き換える例です。
（`^` がポインタの位置です）

まず、`is` が検知された位置に文字列終端文字 `\0` を入れます。

```
This_\0s_a_test.
      ^
```

すると前半の `This_` と後半の `s_a_test.` に分割されます。（`i` は `\0` に置き換わっている）
仮に`work` を `printf` で出力すると `This_` とだけ表示されます。

次に、検索対象の文字列 `is` （つまり2文字分）先に進めます。

```
This_\0s_a_test.
        ^
```

これを一時的に用意した `temp` 変数にコピーしておきます。（コピーを実行すると、今回の例では `_a_test.` がコピーされる）

あとは用意された情報をうまく連結してやれば目的の文字列が得られます。

変数としては `work` `replace` `temp` の順番にくっつけてやればいいわけです。

ここで、`work` は事前に `\0` が仕込まれているため、文字列としては `This_` になっている点に注意してください。

変数の中身を見てみると、 `This_` `was` `_a_test.` となっているわけですね。
これを連結してやればまさに目的の文字列が得られる、というわけです。

　◆

これ書いていて文字列置換、意外とポインタの学習に向いているんじゃ、って思った(  ´⊇`)