【C言語】文字列を連結する【危険なstrcatと安全なsnprintf関数による文字列結合】

C言語の文字列型(const char *)や文字配列(char [])は、+演算子による文字列連結が行えません。C言語の文字列は配列やポインタで表現されているため、文字列だけ特別扱いというわけにはいかないのです。手動で結合用の新たな配列や領域を確保し、各文字列を手動で連結する必要があります。

// 連結演算子には対応していない
const char *a = "a", *b = "b";
char *c = a + b; // error: invalid operands to binary expression ('const char *' and 'const char *')

// 文字列リテラル同士であれば、
// 定数を並べるだけで連結が行われる(コンパイル時に結合される)
const char *c = "a" "b""c"; // c === "abc"
const char *q = "select * "
                "from user "
                "where 9 < user_id ";

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

目次

スポンサーリンク

snprintf関数による文字列結合

snprintf関数はフォーマット用の関数ですが、文字列の連結用途にも活用することができます。

// #include <stdio.h> // snprintf
const char *a = "a", *b = "b"; // 連結したい文字列

char s[3];                     // 結合先の配列
snprintf(s, 3, "%s%s", a, b);  // 結合

// s == {'a', 'b', '\0'}
printf("%s", s); // "ab"

第一引数に書き込み先の配列、第二引数に書き込み可能な長さを指定します。第三引数にはprintf関数と同等のフォーマット文字列を指定します。第四引数以降には実際に連結したい文字列を指定します。

なお第二引数に指定された長さを超えた書き込みは発生しません。また書き込み先のバッファは常にNULL文字で終端処理されます。

ちなみにsnprintf関数は様々な型のフォーマットが可能となっているため、数値との連結や、空白やカンマによる連結、3つ以上の文字列連結など、より柔軟な文字列連結が可能となっています。

char s[8];
snprintf(s, 8, "%s %d", "Shop", 99);
puts(s); // "Shop 99"

NULL終端されていない固定長の文字列を連結する方法

NULL終端されていない文字列を連結する場合には、フォーマット文字列中の変換指定子に対して配列の長さを明示する必要があります(例: %.2s)。

char a[2] = {'a', 'b'}; // NULL終端されていない固定長の文字列
char b[3] = {'c', 'd', 'e'};

char s[6] = {'-', '-', '-', '-', '-', '-'};
snprintf(s, 6, "%.2s%.2s", a, b);

puts(s); // "abcd" (5文字目はNULL終端される)
// s == {'a', 'b', 'c', 'd', '\0', '-'}

snprintf関数の特性により、書き込み先のバッファには自動的にナル文字('\0')が書き込まれるため注意してください。NULL終端が不要な場合には、memcpy関数による連結が必要となります(# 固定長の文字列を連結する場合(memcpy関数による連結))。

結合元の文字列の長さを動的に指定する

配列のサイズを動的に指定したい場合には、変換指定子の幅に*を指定し(%.*s)、実際の幅をsnprintf関数の実引数に指定します。

char a[] = {'C', 'C'}; // sizeof(a) == 2
char b[] = "CURRY&";   // strlen(b) == 6

char s[9];
snprintf(s, 9, "%.*s %.*s", 2, a, (int)strlen(b), b);

puts(s); // "CC CURRY"

snprintf関数の安全性・注意点

snprintf関数の第三引数には書き込み先文字列のバッファサイズを指定することができるため、万が一想定外の長さの文字列を指定してしまっても、余計な書き込みによるバッファオーバーランが発生しません。

char s[3];
snprintf(s, 3, "%s%s", "a", "bcd"); // 余分な長さの文字列を追加しても
printf("%s", s); // "ab"            // バッファオーバーフローが発生しない

ただし、文字列連結時におけるsnprintf関数の利用は、あくまで万が一の場合の予防策に過ぎません。超過した分の文字列は暗黙の内に切り捨てられる事になるため、このことが逆に別の新たな問題やバグを引き起こすことに繋がる可能性もあります。

const char *cmd = "rm *.txt"; // テキストファイルのみを削除するコマンド
char s[5];
snprintf(s, 5, "%s", cmd);
puts(s); // "rm *" // すべてのファイルを削除するコマンド

この場合、「# stncat関数とstrncat関数の危険性・安全策」で紹介する例のように、連結後の長さと連結先のバッファサイズを事前にチェックすることが求められます。またその際には、処理の目的を明確化するために、snprintf関数ではなくstncat/strncat関数を用いて文字列連結を行うことをオススメします。

strcat関数による文字列連結

strcat関数strncat関数による文字列連結はバッファオーバーランの危険性があるため注意して利用する必要があります(後に解説)。

// #include <string.h> // strcat
char s[3] = {'\0'}; // s == {'\0', '\0', '\0'} // ゼロ初期化
strcat(s, "a");     // s == {'a' , '\0', '\0'}
strcat(s, "b");     // s == {'a' , 'b' , '\0'}
puts(s); // "ab"

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

// #include <string.h> // strncat
char s[4] = {'\0', '_', '_', '_'};
strncat(s, "abcd", 3); // バッファサイズよりも短い長さを指定する
// s == {'a', 'b', 'c', '\0'}

strcat関数は、文字列の末尾(ナル文字'\0'が最初に出現する位置)に新たな文字列を追加する関数です。結合用の空配列を用いる場合には事前にゼロ初期化が必要となります。ゼロ初期化を行わない場合には、先頭要素をゼロ初期化するか(s[0] = '\0';)、またはstrcpy関数を用いて文字列をコピーする必要があります。

固定長の文字列を連結する場合(memcpy関数による連結)

NULL終端されていない固定長の文字配列を連結する場合には、strcat関数やstrcpy関数を用いることができないため、代わりにmemcpy関数を用いる必要があります。

char a[2] = {'A', 'a'};
char b[2] = {'B', 'b'};
char s[5];           // s == {'_', '_', '_', '_', '_'}(不定な値)
memcpy(s    , a, 2); // s == {'A', 'a', '_', '_', '_'}
memcpy(s + 2, b, 2); // s == {'A', 'a', 'B' ,'b', '_'}

s[4] = '\0'; // 必要に応じてNULL終端する
puts(s); // "AaBb"

memcpy関数の第一引数には書き込み先のバッファを指定します。第二引数には書き込む文字列、第三引数には書き込みたい長さを指定します。二回目以降の書き込みではs + 2&s[2]という形で、書き込み開始位置のオフセットを明示する必要があります。

なおmemcpy関数はNULL終端を行わないため、必要に応じてナル文字('\0')による終端処理を行います。

stncat関数とstrncat関数の危険性・安全策

strcat関数はバッファオーバーランの危険性があるため注意して利用する必要があります。より安全な対策方法としては、連結後のサイズを事前に計算し、安全な長さの場合にのみ実際の連結処理を行うようにすることです。

char a[4] = {'a', '\0'};
const char *b = "bc";

if (strlen(a) + strlen(b) < 4) {
   strcat(a, b);
} else {
   printf("余計な長さの文字列を連結しようとしています");
   printf("十分なバッファサイズが確保されていません");
}
参考:strcat関数 #strcat関数とバッファオーバーラン

なおstrncat関数はstrcat関数の安全版では無い点に注意が必要です。第三引数は書き込み可能な長さではなく、書き込み元の文字列の長さを表します。strncat関数を用いる場合には、余計に追加される終端NULL文字の分を考慮し、第三引数の長さを一文字分小さめに指定したり、strcat関数の時と同様に連結後の文字列長さを事前計算する必要があります。

参考:strncat関数 #strncat関数の危険性と対策

std::stringを介した連結

C++スタイルの文字列型(std::string)を介することで、文字列同士の連結を実現することもできます。メモリの動的確保が発生する場合があるため、処理効率は悪いですが、自前で連結するよりは安全な方法となります。

const char *a = "a";
const char *b = "b";
std::string s = std::string(a) + b;
const char *c = s.c_str();
puts(c); // "ab"
char a[] = {'-', 'z'};
// 固定長配列の連結にも対応(第二引数に配列のサイズを指定)
std::string s = std::string{'a'} + std::string(a, 2);
puts(s.c_str()); // "a-z"
// より効率的な連結
s.append(a, 2);
puts(s.c_str()); // "a-z-z"

c_strはC言語スタイルの文字列を取得するための関数です。c_str関数の戻り値の有効期限はstd::string型の一時変数の寿命に依存するため注意が必要です。またc_strの結果を書き換えることはできません。std::stringオブジェクトの値や状態を変更した場合、以前取得したc_strの値は不定なものとなります。

参考:C++の文字列をC言語の文字列に変換/コピーする方法

広告

広告