【C言語】文字列を連結・結合する【strcatの危険性とsnprintfの安全性】

C言語で文字列の連結を実現する方法としては、strcat/strncat関数を用いる方法が知られています。ただしバッファオーバーフローの危険性があるため注意して利用する必要があります。

複数の文字列を新たな文字列として結合したい場合にはsnprintf関数の利用が最適です。

目次

strcat関数/strncat関数による文字列連結

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

// #include <string.h> // strcat
char a[4] = {'a', '\0'}; // a: {'a', '\0', '\0', '\0'}
char b[3] = "bc";        // b: {'b', 'c', '\0'}
strcat(a, b);            // a: {'a', 'b', 'c', '\0'}
puts(a); // "abc"
// #include <string.h> // strncat
char a[4] = {'a', '\0'}; // a: {'a', '\0', '\0', '\0'}
strncat(a, "bcdefg", 2); // a: {'a', 'b', 'c', '\0'}
puts(a); // "abc"

strcat関数は、第一引数に指定された文字列の末尾(ナル文字'\0'が最初に出現する位置)に、第二引数に指定された新たな文字列を追加する関数です。strncat関数の場合は第三引数に追加する文字列の長さを指定することができます。

結合用の空配列を用いる場合には事前にゼロ初期化が必要となります。

char s[3] = {'\0'}; // s: {'\0', '\0', '\0'} // ゼロ初期化
strcat(s, "a");     // s: {'a' , '\0', '\0'}
strcat(s, "b");     // s: {'a' , 'b' , '\0'}
char s[4] = {'\0', '_', '_', '_'};
strncat(s, "ab", 1); // s: {'a', '\0', '_', '_' }
strncat(s, "cd", 2); // s: {'a', 'c' , 'd', '\0'}

ゼロ初期化を行わない場合には、配列の先頭要素をゼロ初期化するか(s[0] = '\0';)、またはstrcpy関数を用いて文字列を先頭位置へとコピーする必要があります。

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

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

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

// #include <string.h> // 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[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);
} else {
   printf("余計な長さの文字列を連結しようとしています");
   printf("十分なバッファサイズが確保されていません");
}

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

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

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"
         // バッファサイズ9が優先され'&'は切り捨てられる

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関数を用いて文字列連結を行うのも良いでしょう。

加算演算子(+)による結合には対応していない

なお、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 *')

// 結合代入演算子にも対応していない
char s[] = "a";
s += "b"; // error: Invalid operands to binary expression ('char [2]' and 'const char *')

なお文字列リテラル同士であれば、両定数を並べるだけで連結が行われます。ただしこの連結はあくまでコンパイル時に行われるものです。

const char *c = "a" "b" "c"; // c === "abc"
const char *q = "select * "
                "from user ";

なお、C++の文字列型(std::string)は+演算子や+=演算子に対応しているため、文字列の連結や結合を行うことが可能となっています。次項を参考にしてください。

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(); // const char *型の文字列として参照
puts(c);                   // "ab"(C言語のAPIで処理できる)
char a[] = {'-', 'z'};

// 固定長配列の結合にも対応(第二引数に配列のサイズを指定)
std::string s = std::string{'a'} + std::string(a, 2);
puts(s.c_str()); // "a-z"

// より効率的な連結処理
std::string t = "a";
t.append(a, 2);
puts(t.c_str()); // "a-z"(連結先のオブジェクトが直接書き換わる)
参考:C++ 文字列の連結と結合

c_strはC言語スタイルの文字列を参照するための関数です。c_str関数の戻り値の有効期限はstd::string型の一時変数の寿命に依存するため注意が必要です。またc_strの結果を書き換えることはできません。std::stringオブジェクトの値や状態を変更した場合、c_strで取得していた文字列は不定なものとなります。const char *型の連結結果を永続的に保持したい場合は、文字列を手動でコピーする必要があります。

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