【C言語】
strcat非推奨理由と安全な文字列連結方法
(sprintfの使い方解説)

この記事の概要

この記事では、C言語のstrcat関数が非推奨とされる理由と、それに代わる安全な文字列連結方法を紹介します。

sprintfやsnprintfを使った効率的な連結方法、バッファオーバーフローを防ぐための注意点、さらにitoa関数の自作がなぜ危険かについても詳しく説明しています。

安全で信頼性の高いコードを書くための実践的なアドバイスが詰まった内容です。

strcatを使わないで文字列連結する(推奨sprintf版)

//strcatを使わないで文字列連結する
#include <stdio.h> 
#include <string.h>

void join(char *output,char *s1, char *s2,char *s3,char *s4)
{
    sprintf(output,"%s,%s,%s,%s",s1,s2,s3,s4);
}

int main(void) 
{
    char output[1024];
    join(output,"Jugemu","Jugemu","Gokono","Surikire");
    puts(output);
}

実行結果をイメージしやすいsprintfを使いましょう。

➡実行結果

./a.out 
Jugemu,Jugemu,Gokono,Surikire

strcatを使って文字列連結するな(非推奨コード)

//strcatを使って文字列連結する
#include <stdio.h> 
#include <string.h>
void join(char *output,char *s1, char *s2,char *s3,char *s4)
{
    strcpy(output,s1);
    strcat(output,",");
    
    strcat(output,s2);
    strcat(output,",");
    strcat(output,s3);
    strcat(output,",");
    strcat(output,s4);
}
int main(void) 
{
    char output[1024];
    join(output,"Jugemu","Jugemu","Gokono","Surikire");
    puts(output);
}

strcpy+strcatをダラダラ書くと読みにくく
実行結果をイメージしにくいです。


標準関数を使わないで文字列連結を自作するな(非推奨コード)

#include <stdio.h>

void string_join(char *output,const char *s1,const char *s2)
{
    int j = 0;
    for(int i = 0;s1[i] != '\0';i++){
        output[j++] = s1[i];
    }
    for(int i = 0;s1[i] != '\0';i++){
        output[j++] = s2[i];
    }
    output[j] = '\0';//お尻の終端文字
}
int main(void)
{
    char output[1024];

    string_join(output,"Hello, ","world");
    puts(output);
}

学生や新入社員教育の課題によく出てくる課題なので
参考に乗せます。

しかし、実際の製品では
標準関数を使えば簡単に書ける事を
わざわざ自作するのは止めましょう。

※自作するとデバッグするのが大変です。
ちょっとバグってるのわかりますか?


■sprintfにはバッファオーバーフローの危険性がある

#include <stdio.h>
int main(void){
    char    dst[32] ;
    char *src = 
    "Jugemu Jugemu," 
    "Gokō no Surikire," 
    "Kaijarisuigyo no Suigyōmatsu," 
    "Ungyōmatsu," 
    "Fūraimatsu," 
    "Kuunerutokoro ni Sumutokoro," 
    "Yaburakōji no Burakōji," 
    "Paipo Paipo," 
    "Paipo no Shūringan," 
    "Shūringan no Gūrindai," 
    "Gūrindai no Ponpokopī no Ponpokona no Chōkyūmei,"
    "Chōkyūmei no Chōsuke";

    sprintf(dst,"%s\n",src);
    puts(dst);
}

出力バッファは充分に大きくとりましょう。
このコードでは
領域破壊で異常終了するかもしれません。


■sNprintfには尻切れの危険性がある

#include <stdio.h>
int main(void){
    char    dst[32] ;
    char *src = 
    "Jugemu Jugemu," 
    "Gokō no Surikire," 
    "Kaijarisuigyo no Suigyōmatsu," 
    "Ungyōmatsu," 
    "Fūraimatsu," 
    "Kuunerutokoro ni Sumutokoro," 
    "Yaburakōji no Burakōji," 
    "Paipo Paipo," 
    "Paipo no Shūringan," 
    "Shūringan no Gūrindai," 
    "Gūrindai no Ponpokopī no Ponpokona no Chōkyūmei,"
    "Chōkyūmei no Chōsuke";

    snprintf(dst,sizeof(dst),"%s",src);
    puts(dst);
}

➡実行結果

./a.out
Jugemu Jugemu,Gokō no Surikire

寿限無落語が途中で終わってしまいました。
お客に怒られないでしょうか?

意味のある文字列を
途中でぶった切っても良い状況って
筆者には余り思いつきません。

例えば
”やっぱり買うのを止めた”を途中で切ると
”やっぱり買う”になり
意味が反対になってしまいます。

それでもsnprintfは安全でしょうか?


自作itoa(INT_MIN)はバグるのでsprintfを使う

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>
#include <string.h>

// itoa 関数の実装
char* itoa(int num) {
    // 変数の初期化
    static char result[256]; // 最大 12 桁の整数を扱えるように
    int index = 0;
    bool is_negative = false;

    // 負の数を扱う
    if (num < 0) {
        is_negative = true;
        num = -num;
    }

    // 各桁を文字に変換して結果に追加
    while (num > 0) {
        int digit = num % 10;
        result[index++] = '0' + digit;
        num /= 10;
    }

    // 負の符号を追加
    if (is_negative) {
        result[index++] = '-';
    }

    // 文字列を反転
    for (int i = 0; i < index / 2; ++i) {
        char temp = result[i];
        result[i] = result[index - i - 1];
        result[index - i - 1] = temp;
    }

    // 文字列の終端を設定
    result[index] = '\0';

    return result;
}
int     main(void){
    //INT_MAXは動く
    int max = INT_MAX;
    printf("%d\n%s\n",max,itoa(max));

    //INT_MINでバグる   
    int min = INT_MIN;
    printf("%d\n%s\n",min,itoa(min));
}

生成AIにitoa関数を作ってもらいました。


➡暴走結果の例

./a.out
2147483647
2147483647
-2147483648
-

自作 itoa(INT_MIN)で暴走しました。
ネットで紹介されている
自作itoaもほぼ全滅です。


➡sprintfを使った修正例

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

int     main(void){
    char    ascii[256];
     
    //INT_MAXは動く
    int max = INT_MAX ;
    sprintf(ascii,"%d",max);
    printf("%d\n%s\n",max,ascii);

    //INT_MINも動く
    int min = INT_MIN ;
    sprintf(ascii,"%d",min);
    printf("%d\n%s\n",min,ascii);
}

itoa関数を自作しないで
標準関数のsprintfを使いました。


➡期待する結果が得られる

./a.out
2147483647
2147483647
-2147483648
-2147483648

つまり、
itoa関数を自作しないで
sprintfを使えという話でした。

参考:

C-FAQ 13.1

■sprintfは入力出力引数を同じにしてはいけない

#include <stdio.h>
int main(void){
    char    dst[16] = "abcd";
    char    src[16] = "1234";

    sprintf(dst,"/%s/%s/",src,dst);
    puts(dst);
}

dstが入力と出力の引数に使われているので
未定義の動作(バグ)となります。

参考:

軽率にも次のようなコードを使っているプログラムがある。