【C言語】文字列をコピーする方法【危険なstrcpy関数と安全な文字列複製】

C言語の文字列型(const char *)や文字配列(char [])は、代入演算子(=)による文字要素のコピーが行えません。いずれの型もポインタで表現されているため、代入演算子による処理はあくまでポインタのアドレスをコピーするものとなり、参照先の実体は同一のものとなります。

const char *s = "abc";
const char *t = s;      // 要素のコピーではなく、ポインタの値コピー
(void *)t == (void *)s; // true(同じアドレス)
*t = 'A';
*s == 'A'; // true(同じ参照先であるため書き換わってしまう)

// 配列へのコピー代入も不可
char a[4] = s; // error: array initializer must be an initializer list or string literal
a = s;         // error: array type 'char [4]' is not assignable

C言語で文字列連結を実現する方法としては、strcpy関数を用いる方法が知られていますが、この方法にはバッファオーバーランの危険性があるため注意して利用する必要があります。snprintf関数やstrlcpy関数の利用も検討してみてください。

目次

strcpy/strncpy関数による文字列コピー

strcpy関数を用いることで、文字列要素のコピーを実現することができます。strcpy関数は、第二引数に指定された文字列を第一引数に指定された配列に書き込みます。

// #include <string.h> // strcpy
char s[3];
strcpy(s, "ab");
// s == {'a', 'b', '\0'}

strcpy関数には、書き込み先の配列のサイズを超えた過剰な書き込みが発生する危険性(バッファオーバーラン)があるため、実際にstrcpy関数を用いる際には、配列のサイズと追加元の文字列の長さをチェックし、安全な場合にのみコピーを行うようにする必要があります。

char s[3];
const char *t = "ab";

if (strlen(t) < 3) {
   strcpy(s, t);
} else {
   /* 必要に応じてエラー処理や例外処理を行う */
   puts("3文字以上の文字列は受け付けられません");
   return -1;
}

なお、コピーする長さを指示することのできるstrncpy関数も存在しますが、ナル文字が書き込まれないケースが存在するため、注意が必要です。

// #include <string.h> // strncpy
char s[3];
strncpy(s, "abc", 3); // s ≒ {'a', 'b', 'c'}
s[2] = '\0';          // 手動で終端ナル文字を書き込む
参考:strncpy関数の問題点と脆弱性/安全な利用方法

snprintf/strlcpy関数による文字列コピー

snprintf関数は文字列のコピーの用途としても活用することができます。第二引数に指定された長さを超えた書き込みは発生しません。

// #include <stdio.h> // snprintf
char s[3] = {'-', '-', '-'};
snprintf(s, 3, "%s", "a");   // s ≒ {'a', '\0', '-'}
snprintf(s, 3, "%s", "ab");  // s ≒ {'a', 'b', '\0'}
snprintf(s, 3, "%s", "abc"); // s ≒ {'a', 'b', '\0'}

同等の処理は、strlcpy関数やstrcpy_s関数で実現することもできます。こちらは標準関数ではないため、利用できる環境が限られています。

// #include <string.h> // strlcpy
char s[3] = {'-', '-', '-'};
strlcpy(s, "a", 3);   // s ≒ {'a', '\0', '-'}
strlcpy(s, "ab", 3);  // s ≒ {'a', 'b', '\0'}
strlcpy(s, "abc", 3); // s ≒ {'a', 'b', '\0'}

なお、snprintf関数やstrlcpy関数によるコピー方法では、指定された長さを超えた分の文字列は書き込まれなくなる点に注意する必要があります。バッファーオーバーランの危険性は回避できても、書き込まれるべき文字列が書き込まれなくなることにより、別の新たなバグに繋がってしまう危険性があります。

参考:snprintf関数/strlcpy関数の問題点と危険性

固定長の文字列をコピーする

ヌル終端されていない固定長の文字配列をコピーする場合には、長さ指定が可能なmemcpy関数を用います。memcpyの第三引数にはコピーしたい長さを指定することができます。

// #include <string.h> // memcpy
char s[3] = {'-', '-', '-'};
char t[3] = {'a', 'b', 'c'};
memcpy(s, t, 2); // s == {'a', 'b', '-'}
s[2] = '\0';     // 必要に応じて終端ナル文字を書き込む

またsnprintf関数を用いて固定長の文字列を書き込むこともできます。その際には変換指定子に対して長さ指定を行います(例: %.2s)。

char s[3] = {'-', '-', '-'};
snprintf(s, 3, "%.2s", "abc");
// s == {'a', 'b', '\0'} // ナル文字が強制的に付加される

snprintf(s, 3, "%.*s", 1, "ABC"); // 長さを動的に指定することも可能
// s == {'A', '\0', '\0'}

ただし、コピー元の文字列にナル文字が含まれている場合には、ナル文字が出現した時点でコピーが完了してしまうため注意が必要です(マルチバイト文字列やバイナリを扱う場合など)。

char s[4] = {'-', '-', '-', '-'};
char t[3] = {'a', '\0', 'c', 'd'};
snprintf(s, 4, "%.3s", t);
// s == {'a', '\0', '-', '-'}('c'が書き込まれない)

同じ理由でstrncpy関数を用いることができません。代わりに先程紹介したmemcpy関数を用いる必要があります。

char s[4] = {'-', '-', '-', '-'};
char t[3] = {'a', '\0', 'c', 'd'};
memcpy(s, t, 4);  // s ≒ {'a', '\0', 'c', 'd'}
strncpy(s, t, 4); // s ≒ {'a', '\0', '\0', '\0'}

部分文字列のコピーを行う

文字列の一部をコピーする場合には# 固定長の文字列をコピーする際と同様にmemcpy関数を用います。

// #include <string.h> // memcpy
char s[4] = {'-', '-', '-', '-'}; // コピー先の文字配列
char t[4] = {'a', 'b', 'c', 'd'}; // コピー元の文字配列

memcpy(s, t + 1, 2); // s ≒ {'b', 'c', '-', '-'}
memcpy(s + 2, t, 2); // s ≒ {'b', 'c', 'a', 'b'}

コピー元の文字列の読み取り開始位置を指定する場合には、t + 1という形でポインタのオフセットを明示する必要があります。また書き込み開始位置を指定する場合も同様に、s + 2&s[2]という形でオフセットを明示します。

strdup関数による便利な文字列複製

標準関数ではないため移植性には難がありますが、POSIX規格のstrdup関数でメモリ確保とコピーを一度に行うこともできます。

#include <string.h> // strdup
const char *s = "abc";
char* d = strdup(s);    // メモリ確保 & コピー
if (d != NULL) puts(d); // "abc"
free(d);                // メモリ解放

strdup関数はコピー処理やメモリの動的確保、エラー処理等を簡略化する目的に利用することができます。

strdup関数

strdup関数は受け取った文字列を動的確保された新たなメモリ領域にコピーし、その領域を指したポインタを戻り値として返します。動的確保が失敗した際にはNULLが返されます。

#include <string.h>
char *strdup(const char *s) {
   size_t n = strlen(s) + 1;
   void *t = malloc(n);
   if (t == NULL) return NULL;
   return memcpy(t, s, n);
}

可変長配列へのコピー

多用は危険ですが、可変長配列などによる配列の自動割り当てが行える環境であれば、以下のような方法を用いることもできます。

const char *s = "abc";
char vla[strlen(s) + 1]; // 可変長配列(実行時に動的に割り当てられる)
strcpy(vla, s);
// vla == {'a', 'b', 'c', '\0'}
可変長配列に巨大なサイズを指定した場合にはスタックオーバーフローが発生します。
可変長配列(variable-length array)はC言語の機能であり、C++言語(C++17)では正式な機能ではなく、あくまでコンパイラの独自拡張として提供されている場合があるため注意してください。
広告