文字列をコピーする | Programming Place Plus C言語編 逆引き

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

このページの概要

以下は目次です。

目的

文字列をコピーして、同じ内容をもった文字の配列を作りたいとします。

C言語では、配列は代入できませんから、他の手段が必要になります。文字列リテラルも配列である(第32章)ことに注意してください。

#include <stdio.h>

#define ARRAY_SIZE (5)

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

    str2 = str1;  // エラー
    str2 = "XYZ"; // エラー
}

文字でない配列の場合は、ほかの手法があります。「配列をコピーする」のページを参照してください。

方法①(strcpy関数を使う)

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

strcpy関数は、第2引数の文字列を、第1引数で指定した配列へコピーします。

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

#define ARRAY_SIZE (5)

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

    strcpy(str2, str1);
    puts(str2);

    strcpy(str2, "XYZ");
    puts(str2);
}

実行結果:

xyz
XYZ

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

もちろん、確実に問題なく動作するケースもあります。コピー元が文字列リテラルであり、コピー先の配列の要素数が十分にあれば問題は起きません。コピー元が変数の場合は、そこにどんな文字列が入りうるかを慎重に検討しなければならず、危険度が上がります。

C99規格の時点で、標準で使える方法で解決するには、きちんと配列の大きさを確認するしかありません。次のプログラムでは、strcpy関数を呼び出す前に、文字数の比較を行い、コピー先のほうが短いときには assert が失敗するようにしています。

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

#define ARRAY_SIZE (5)

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

    assert(strlen(str1) < strlen(str2));
    strcpy(str2, str1);
    puts(str2);

    assert(strlen("xxxxxx") < strlen(str2));
    strcpy(str2, "xxxxxx");
    puts(str2);
}

実行結果:

xyz
(アサートで停止)

安全になるものの、strlen関数の呼び出しのコストが上乗せされるため、処理速度への影響は大きくなります(assertマクロであれば、リリース時のコストは消えますが)。

また、strncpy関数を使う方法が紹介されることもありますが、この関数は仕様が非常に複雑であり、かえって危険だと思われます。

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

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

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

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

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

strcpy_s関数は、第3引数の文字列を、第1引数の配列へコピーします。第2引数には、第1引数の配列の大きさを指定します。第1引数、第3引数はいずれもヌルポインタ以外でなければなりません。

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

#define ARRAY_SIZE (5)

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

    if (strcpy_s(str2, sizeof(str2), str1) == 0) {
        puts(str2);
    }

    if (strcpy_s(str2, sizeof(str2), "XYZ") == 0) {
        puts(str2);
    }

    if (strcpy_s(str2, sizeof(str2), "xxxxxxxx") == 0) {
        puts(str2);
    }
}

実行結果:

xyz
XYZ
(停止)

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

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

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

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

方法③(1文字ずつコピーする)

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

#include <stdio.h>

#define ARRAY_SIZE (5)

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

    int i;
    for (i = 0; str1[i] != '\0'; ++i) {
        str2[i] = str1[i];
    }
    str2[i] = '\0';

    puts(str2);
}

実行結果:

xyz

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

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


参考リンク

  1. strcpy_s, wcscpy_s, _mbscpy_s, _mbscpy_s_l | Microsoft Docs
    • Visual Studio 2017 の strcpy_s関数のマニュアル


更新履歴

’2019/8/26 新規作成。



逆引きのトップページへ

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

Programming Place Plus のトップページへ



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