文字列の途中に文字列を挿入する | Programming Place Plus C言語編 逆引き

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

このページの概要

以下は目次です。

目的

文字の配列があるとき、その途中の部分に、ほかの文字列を挿入したいとします。

たとえば、“abcde” が格納された文字の配列に “xyz” を挿入して、“abxyzcde” のような文字列を得たいということです。

残念ながら、C言語には文字列の挿入に関する標準ライブラリ関数はありません

当然ですが、文字列の挿入を行うと全体の文字数が増えます。そのため、同じ配列内に挿入後の文字列ができあがることを望むのならば、あらかじめ十分な要素数を与えておかなければなりません。

char str1[] = "abcde";    // 要素数は 6 しかない。"xyz" を挿入できない
char str2[10] = "abcde";  // 要素数は 10。"abxyzcde" を格納できるので挿入可能

「挿入できない」といってもコンパイルはできてしまうでしょうし、その結果はバッファオーバーフローであり、未定義の動作です。必ず避けなければなりません。

次のコードのように、別の配列を用意しておいて、そちらに、挿入後の文字列を格納することも考えられますが、

char str1[] = "abcde";

char str2[10];
// str2 へ "ab" と "xyz" と "cde" を格納する

事前に str2 の要素数を決定できるのなら、そもそも str1 の要素数を多く確保して、str1 だけで完結させればいいともいえるかもしれません。str1 を書き換えられない事情があれば意味がありますが。

変化形として、str2 を動的にメモリ確保する方法があります。この場合、実行時に挿入後の長さを決められます。

char str1[] = "abcde";

char* str2;
// 動的に "abxyzcde" が使う大きさ以上のメモリを割り当てて、str2 にメモリアドレスを与える
// その後、"ab" と "xyz" と "cde" を格納する

なお、文字列の “途中” に挿入することがメインテーマですが、実際には、先頭に挿入することも、末尾に挿入(つまり連結)することも、同じ操作で行えるべきでしょう。

ただし、文字列の末尾への連結には、より簡単な方法があるので、挿入先が文字列の末尾だと分かっているのなら、専用の方法を使えばいいです。詳細は「逆引き 文字列を連結する」を参照してください。

最初に挙げた、要素数が固定された配列を使う方法を方法①で、動的にメモリを割り当てる方法を方法② で取り上げます。

方法①(あらかじめ十分な要素数を確保しておき、挿入する)

事前に、挿入後の文字列の長さが予想できるのならば、あらかじめそれを見越した大きさの配列を用意し、その配列内で挿入作業を行えます。

文字列の挿入は、以下の手順で行います。

  1. 挿入先の文字配列の挿入位置を起点に、挿入する文字列の長さの分だけ、文字列をうしろへずらす
  2. 空いた範囲内へ、挿入する文字列をコピーする

実際のプログラム例は次のようになります。文字列の挿入は汎用的な機能なので、関数化しています。

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

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

/*
    文字列を挿入する

    引数
        str1:      挿入先の文字列。ヌルポインタは不可
        str1_size: str1 の要素数
        pos:       挿入位置。str1 の先頭から何文字目か
        str2:      挿入する文字列。ヌルポインタは不可
    戻り値
        str1 を返す
*/
char* str_insert(char* str1, size_t str1_size, size_t pos, const char* str2)
{
    assert(str1 != NULL);
    assert(str2 != NULL);

    size_t len1 = strlen(str1);
    size_t len2 = strlen(str2);

    // 挿入を行っても、str1 の範囲をはみ出さないか?
    assert(len1 + len2 < str1_size);

    // str1 の終端よりも後ろを挿入位置に指定していないか?
    assert(len1 >= pos);


    // 挿入位置 (str1[pos]) から、挿入文字数 (len2) の範囲を空ける
    // 末尾の '\0' も含めて、後ろへずらす
    memmove(&str1[pos + len2], &str1[pos], len1 - pos + 1);

    // 空いた領域へ、挿入する文字列 (str2) をコピー
    memcpy(&str1[pos], str2, len2);

    return str1;
}

int main(void)
{
    char str[10] = "abcde";

    puts(str_insert(str, SIZE_OF_ARRAY(str), 2, "xyz"));
}

実行結果:

abxyzcde

str_insert関数の4つの引数のうち、第2引数 str1_size については、挿入処理を行ううえでは必須ではなく、関数内の assert でのみ使用しています。この引数は、挿入先の配列の要素数を指定するものですが、呼び出し側で正しい情報を渡してやることで、関数内でバッファオーバーフローを検知できます。

最初に挙げた手順1「挿入先の文字配列の挿入位置を起点に、挿入する文字列の長さの分だけ、文字列をうしろへずらす」は、memmove関数を使って行います。領域に重なりがあるので、memcpy関数を使ってはいけません(第34章)。

ここでは、末尾の ‘\0’ も含めてずらさなければならないので、memmove関数の第3引数に渡す値の計算に注意してください。

手順2「空いた範囲内へ、挿入する文字列をコピーする」では、memcpy関数を使っています。今度は、strcpy関数を使ってはなりません。strcpy関数を使うと、コピー後の文字列の終端に ‘\0’ が付加されてしまうので、str1 の内容は「abxyz\0cde\0」になってしまいます。

memcpy関数の代わりに、strncpy関数を使うことはできます。

方法②(メモリ領域を動的に確保して、挿入後の文字列を生成する)

挿入先の文字配列の内容を変えてはならない場合や、プログラムを記述する時点では、挿入後の文字列の長さがわからない場合には、挿入結果を格納する配列を別に用意しなければなりません。

特に後者の場合は、動的メモリ割り当てによって作成するしかありません。


挿入先の文字配列を動的に作ったあとは、方法① で作った文字列挿入関数を使うことも可能です。ただし、この方法だと無駄があります。

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

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

/*
    文字列を挿入する

    引数
        str1:      挿入先の文字列。ヌルポインタは不可
        str1_size: str1 の要素数
        pos:       挿入位置。str1 の先頭から何文字目か
        str2:      挿入する文字列。ヌルポインタは不可
    戻り値
        str1 を返す
*/
char* str_insert(char* str1, size_t str1_size, size_t pos, const char* str2)
{
    assert(str1 != NULL);
    assert(str2 != NULL);

    size_t len1 = strlen(str1);
    size_t len2 = strlen(str2);

    // 挿入を行っても、str1 の範囲をはみ出さないか?
    assert(len1 + len2 < str1_size);

    // str1 の終端よりも後ろを挿入位置に指定していないか?
    assert(len1 >= pos);


    // 挿入位置 (str1[pos]) から、挿入文字数 (len2) の範囲を空ける
    // 末尾の '\0' も含めて、後ろへずらす
    memmove(&str1[pos + len2], &str1[pos], len1 - pos + 1);

    // 空いた領域へ、挿入する文字列 (str2) をコピー
    memcpy(&str1[pos], str2, len2);

    return str1;
}

int main(void)
{
    char str[] = "abcde";

    size_t result_size = strlen(str) + strlen("xyz") + 1;
    char* result = malloc(sizeof(char) * result_size);
    strcpy(result, str);

    puts(str_insert(result, result_size, 2, "xyz"));

    free(result);
}

実行結果:

abxyzcde

まず、malloc関数を使って、挿入先の文字配列を生成します。この大きさは、「挿入先の文字列の長さ+挿入する文字列の長さ+1」です。最後の +1 は、末尾の \0’ の分です。

メモリ割り当て後にはまだ中身の文字列が入っていないので、まず str の内容をコピーします。 あとは自作の str_insert関数を使えば、文字列を挿入できます。

この方法の無駄は、str_insert関数の前に行う文字列のコピーで、str に格納されていた文字列のすべてをコピーしていることです。この方法だと、

  1. strcpy関数で “abcde” をコピー
  2. memmove関数で “cde” をコピーして “abcdecde” になる
  3. memcpy関数で、挿入する文字列 “cde” をコピーして “abxyzcde” になる

というように文字列の状態が変わっていきます。しかし、次のようにすれば、コピーする文字数の合計量を減らせます。

  1. 挿入位置より手前の文字列 “ab” だけをコピー
  2. 1 の文字列の末尾へ、挿入する文字列をコピーして “abxyz” にする
  3. 2 の文字列の末尾へ、挿入先の残りの文字列 “cde” をコピーして “abxyzcde” になる

この方法で実装すると、次のようになります。

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

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

/*
    文字列を挿入した結果を、動的に確保した文字配列へ格納する

    引数
        str1:      挿入先の文字列。ヌルポインタは不可
        pos:       挿入位置。str1 の先頭から何文字目か
        str2:      挿入する文字列。ヌルポインタは不可
    戻り値
        関数内で malloc関数で確保されたメモリ領域を指すポインタ。
        この領域には、str1 の pos番目に str2 を挿入した結果が格納されている。
        メモリの割り当てに失敗した場合は、ヌルポインタを返す。
*/
char* str_alloc_and_insert(const char* str1, size_t pos, const char* str2)
{
    assert(str1 != NULL);
    assert(str2 != NULL);

    size_t len1 = strlen(str1);
    size_t len2 = strlen(str2);

    // str1 の終端よりも後ろを挿入位置に指定していないか?
    assert(len1 >= pos);


    size_t result_size = len1 + len2 + 1;
    char* result = malloc(sizeof(char) * result_size);
    if (result == NULL) {
        return NULL;
    }

    // 挿入位置より手前の部分だけをコピー
    memcpy(result, str1, pos);

    // 挿入する文字列を末尾へコピー
    memcpy(&result[pos], str2, len2);

    // 挿入先の残りの文字列を末尾へコピー
    // 終端の '\0' も併せてコピーする
    strcpy(&result[pos + len2], &str1[pos]); 

    return result;
}

int main(void)
{
    char str[] = "abcde";

    char* result = str_alloc_and_insert(str, 2, "xyz");
    if (result != NULL) {
        puts(result);
        free(result);
    }
}

実行結果:

abxyzcde

str_alloc_and_insert関数にまとめました。

str_insert関数と違い、第1引数は挿入先の文字列の内容を使うだけであって、直接変更するわけではありません。それを表すため、型が char* から const char* に変わっています。

str_insert関数の第2引数にあった、挿入先の要素数の指定はなくなりました。動的に適切な大きさのメモリを割り当てるので、バッファオーバーフローの危険性は(str_alloc_and_insert関数が気を付ければ)なくなったからです。

また、戻り値は、関数内で動的に割り当てた領域を指すポインタが返されるようになっています。 free関数を呼ぶ責任が呼び出し元にあることに注意しなければなりません。

関数内では、malloc関数でメモリの割り当てを行った後、3回に渡って文字列のコピーを行います。末尾の ‘\0’ が必要なのは、最後のコピーだけなので、1回目と2回目のコピーでは memcpy関数を使っています。


参考リンク


更新履歴

’2019/10/21 新規作成。



逆引きのトップページへ

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

Programming Place Plus のトップページへ



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