【C言語】strcat関数/strncat関数【完全解説|危険性と注意点】

strcat関数とstrncat関数の仕様と注意点について解説します。

strcat

strcat関数は複数の文字列を連結する関数です。

#include <string.h>
char *strcat(char *dst, const char *src);
// dst: 連結先の文字列(終端ナル文字を含む)
// src: 新たに追加する文字列(終端ナル文字を含む)
// 戻り値: `dst`を返す

strcat関数は、文字列dstの末尾に新たな文字列srcを追加します。いずれの文字列には終端ナル文字('\0')が含まれている必要があります。戻り値にはdstの値が返されます。両文字列の領域が重なり合う場合の動作は未定義となっています。

strcat関数は文字列dst'\0'が出現する位置に文字列srcを追加します。追加される文字には、終端のナル文字も含まれます。

char a[] = {'a', '\0', '-', '-'};
char b[] = {'b', 'c', '\0'};
strcat(a, b);
// a == {'a', 'b', 'c', '\0'}

strcat関数とバッファオーバーラン

strcat関数にはバッファオーバーランの危険性があります。連結先の文字列のバッファサイズを超えた過剰な書き込みが発生してしまう可能性があります。

char a[2] = {'\0', '\0'};
strcat(a, "abc");
// a == {'a', 'b', 'c', '\0'} // 2バイト分余計に書き込まれてしまう

対応策としては、書き込み可能なバッファサイズと連結後の文字列長を事前に計算・比較し、安全な場合にのみ連結を行うようにする事などが考えられます。

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

if (strlen(a) + strlen(b) < 4) {
   strcat(a, b);
}
// a == {'a', 'b', 'c', '\0'}

strncat

#include <string.h>
char *strncat(char *dst, const char *src, size_t len);
// dst: 連結先の文字列(終端ナル文字を含む)
// src: 新たに追加する文字列または配列
// len: 文字列`src`の長さ
// 戻り値: `dst`を返す

strncat関数は、文字列dstの末尾に新たな文字列srcを追加します。追加される文字列の長さはlenに指定された長さ、または文字列srcの長さのいずれか小さい方となります。追加後の文字列にはナル文字('\0')が付加されます(注意:dstには最大でlen + 1文字分の書き込みが発生する)

連結先の文字列は終端ナル文字を含んでいる必要があります。戻り値にはdstの値が返されます。両文字列の領域が重なり合う場合の動作は未定義となります。

char a[4] = {'a', '\0', '-', '-'};
char b[4] = {'b', 'c', 'd', '\0'};
strncat(a, b, 2);
// a == {'a', 'b', 'c', '\0'}
char s[5] = {'\0', '-', '-', '-', '-'};
strncat(s, "a", 3);
// s == {'a', '\0', '-', '-', '-'}(追加元の文字列の長さが優先される)
strncat(s, "bcd", 3);
// s == {'a', 'b', 'c', 'd', '\0'}(4文字目以降への書き込みが発生する)

strncat関数の危険性と対策

strncat関数はバッファオーバーランに注意して利用する必要があります。strncat関数は第三引数に指定された長さを超えた範囲で書き込みが発生してしまう場合があるためです。

char s[4] = {'\0', '-', '-', '-'};
strncat(s, "abcd", 3);
// s == {'a', 'b', 'c', '\0'}(4文字目への書き込みが発生)

char s[3] = {'\0'};
strncat(s, "abcd", 3); // バッファオーバーフローが発生する

第三引数で指定可能なlenは、文字列dstに書き込み可能なサイズを表すものではない点に注意が必要です。あくまで書き込み元の文字列srcから書き込む長さを指定するものであるため、誤用に注意が必要です。

この問題への対策としては、strncatの第三引数に指定する長さに対して、書き込み先のバッファのサイズよりも一文字分小さめのサイズを指定することなどが有効です。

char s[4] = {'\0', '-', '-', '-'};
strncat(s, "abcd", 3); // バッファサイズよりも短い長さを指定
// s == {'a', 'b', 'c', '\0'}

char s[4] = {'a', '\0', '-', '-'}; // strlen(s) == 1
strncat(s, "bcd", 3 - strlen(s));  // 連結時は残りのバッファサイズを考慮
// s == {'a', 'b', 'c', '\0'}

より堅実な方法は、書き込む文字列の長さがバッファサイズよりも小さい場合にのみ、実際の連結処理を行うようにすることです。

/* より安全で厳格な方法 */
char s[4] = {'a', '\0', '-', '-'};
const char *t = "bc";
if (strlen(s) + strlen(t) < 4) {
   strncat(s, t, strlen(t));
}

また、より安全なstrlcat関数やstrcat_s関数を用いることもできます。指定されたサイズは、書き込み対象の文字列に書き込みが可能なサイズとなります。

char s[] = {'\0', '-', '-', '-'};
strlcat(s, "abcd", 3); // strcat_s(s, 3, "abcd");
// s == {'a', 'b', '\0', '-'}
strlcat(s, "cdef", 4);
// s == {'a', 'b', 'c', '\0'}

ただし、これらの関数は利用できる環境が限られているため、移植性には難があります。snprintf関数による文字列連結も参考にしてみてください。

参考:文字列を連結する方法【strcat/snprintf/memcpyによる文字列結合】
広告