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

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

このページの概要 🔗

以下は目次です。

目的 🔗

文字の配列があるとき、その末尾にほかの文字列を連結したいとします。

#include <stdio.h>

#define ARRAY_SIZE (10)

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

    // str に "xyz" を連結したい

    puts(str);  // "abcxyz" を出力してほしい
}

細かくいうと、str の元の文字列には “abc\0” のように、終端にヌル文字があります(第8章)。連結する “xyz” の最初の文字 ‘x’ が、str の終端のヌル文字を上書きするように連結したいということです。

“xyz” の終端にも ‘\0’ があるので、これが連結後の終端文字になります。結果として “abcxyz\0” という文字列になればいいわけです。

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

もっとも一般的な方法は strcat関数を使うことです。

strcat関数は、第2引数の文字列を、第1引数で指定した文字列の終わりに連結します。

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

#define ARRAY_SIZE (10)

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

    // str に "xyz" を連結する
    strcat(str, "xyz");

    puts(str);
}

実行結果:

abcxyz

この方法で問題なのは、連結先の配列の要素数が足りているのかどうかが確認されないことです。要素数が不足していた場合、配列の末尾を越えて書き込み続ける、バッファオーバーフローが起こります。これは未定義動作につながってしまい危険です。

もちろん、確実に問題なく動作するケースもあります。連結後の文字数がきちんと収まることがわかっていれば問題は起きません。特に連結元の文字列が変数に格納されている場合には、そこにどんな文字列が入りうるかを慎重に検討しなければならず、危険度が上がります。

C99規格の時点で、標準で使える方法で解決するには、きちんと配列の大きさを確認するしかありません。次のプログラムでは、strcat関数を呼び出す前に、連結後の文字数を調べて、連結先の文字配列に収まらないことが分かったときには、assert が失敗するようにしています。

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

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

#define ARRAY_SIZE (10)

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

    // str に "xyz" を連結する
    assert(strlen(str) + strlen("xyz") + 1 <= SIZE_OF_ARRAY(str));
    strcat(str, "xyz");
    puts(str);

    // str2 に str3 を連結する
    char str2[ARRAY_SIZE] = "123";
    char str3[] = "4567890";
    assert(strlen(str2) + strlen(str3) + 1 <= SIZE_OF_ARRAY(str2));
    strcat(str2, str3);
    puts(str2);
}

実行結果:

abcxyz
(アサートで停止)

文字列の長さを調べるときには strlen関数を使います(第32章)。sizeof を使うと、文字列がポインタで表現されているときに間違った値を取得してしまいます。

また、strlen関数で得られる長さには、終端のヌル文字の分が含まれないことにも注意してください(sizeof で文字配列を調べた場合には、ヌル文字の分も含まれます)。そのため、assert に与えた式の左辺側で +1 しています。

【上級】ワイド文字列を使う場合には、sizeof で取得できる値が sizeof(wchar_t) の倍数になる点にも注意が必要です。もはや sizeof をそのまま使うのでは文字数は得られません(sizeof(wchar_t) で除算しなければならない)。ワイド文字列の長さを取得する場合には、wcslen関数を使います。

一方、assert に与えた式の右辺側では、SIZE_OF_ARRAYマクロを使って、配列の要素数を取得しています。必要なのは、連結先の配列の要素数であって、連結先の文字列の長さではないからです。このチェックを行う時点で、連結先の文字列の長さは 3 ですが、要素数は 10 です。欲しいのは後者の値です。

このように連結前にチェックを入れておけば安全になるものの、strlen関数の呼び出しのコストが上乗せされるため、処理速度への影響は大きくなります(assertマクロを使っていれば、リリース版では何もなくなるので、コストも消えます)。

また、strncat関数を使う方法が紹介されることもありますが、バッファオーバーフローを防ぐためには、結局自力で計算を行わなければならず、安全性が向上しているとは言いがたいものです。

方法②(strcat_s関数を使う)[C11] 🔗

C11規格からは、安全性を高めた strcat_s関数を使える可能性があります。

strcat_s関数は、__STDC_LIB_EXT1__ が定義されている処理系でのみ使用できます。もし使えるのであれば、strcat関数を使うのはやめて、strcat_s関数に置き換えたほうがいいでしょう。

Visual Studio 2017 に実装されている strcat_s関数は、Microsoft の独自仕様になっており、ここで取り上げる C11 標準の仕様とは異なっています(参考リンク 1 参照

使用する際には、<string.h> をインクルードする前に、「#define __STDC_WANT_LIB_EXT1__ 1」という記述が必要です。

strcat_s関数は、第3引数の文字列を、第1引数の配列の末尾へ連結します。第2引数には、第1引数の配列の要素数を指定します。第1引数、第3引数はいずれもヌルポインタ以外でなければなりません。

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <string.h>

#define ARRAY_SIZE (10)

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

    // str に "xyz" を連結する
    if (strcat_s(str, sizeof(str), "xyz") == 0) {
        puts(str);
    }

    // str2 に str3 を連結する
    char str2[ARRAY_SIZE] = "123";
    char str3[] = "4567890";
    if (strcat_s(str2, sizeof(str2), str3) == 0) {
        puts(str);
    }
}

実行結果:

abcxyz
(停止)

strcat_s関数が処理を成功とみなすのは、第3引数に指定した文字列が、その終端文字まですべて連結できた場合に限られ、その場合にだけ 0 を返します。途中で連結先の領域が尽きた場合は失敗となり、0以外の値を返します。

また、実行時制約と呼ばれる、実引数に対するチェックが行われ、違反が見つかるとエラーを通知します。具体的には、以下の場合にエラーになります。

Visual Studio 2017 の strcpy_s関数は、この仕様が入っていません。代わりに、独自のパラメータ検証が行われ、第1引数か第3引数がヌルポインタの場合や、第2引数が連結後の文字数に対して小さすぎる場合にエラーになります。

実行時制約違反になると、実行時制約ハンドラと呼ばれる関数が呼び出されます。デフォルトではどのような処理を行う関数になっているかは処理系定義ですが、set_constraint_handler_s関数を使って、独自の関数が呼び出されるように切り替えられます。

方法③(1文字ずつ連結する) 🔗

基本的には strcat_s関数や strcat関数を使った方が良いですが、1要素ずつ手動で連結することも考えられます。こちらの方法の場合は、1文字ずつ何らかの加工を加える等、処理を追加できる余地が生まれます。

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

#define ARRAY_SIZE (10)

int main(void)
{
    char str[ARRAY_SIZE] = "abc";
    char str2[] = "xyz";

    size_t i, j;
    for (i = strlen(str), j = 0; str2[j] != '\0'; ++i, ++j) {
        str[i] = str2[j];
    }
    str[i] = '\0';

    puts(str);
}

実行結果:

abcxyz

for文の内側で文字をコピーするとき、何らかの変換を加える余地があります。

バッファオーバーフローへの対策が行われていないことに注意してください。


参考リンク 🔗

  1. strcat_s、wcscat_s、_mbscat_s、_mbscat_s_l | Microsoft Docs
    • Visual Studio 2017 の strcat_s関数のマニュアル


更新履歴 🔗

 新規作成。



逆引きのトップページへ

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

Programming Place Plus のトップページへ



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