C言語基礎|strcat と strncat の違い

連結は便利、でも油断は禁物!―strcat と strncat を使い分けて、バッファ事故をゼロにしよう。

文字列を作りながら表示したり、ユーザー入力をくっつけたりするとき、C言語では「連結」がよく登場します。
その代表が strcat と strncat です。

どちらも「末尾に追加する」点は同じですが、strcat は無制限に追加しようとするのに対して、strncat は追加する文字数に上限を設けられるのが大きな違いです。
つまり、使い分けのテーマはズバリ 安全性(バッファに収まるか) です。

strcat と strncat の基本(書式と役割)

strcat

  • ヘッダ:#include <string.h>
  • 形式:char *strcat(char * restrict s1, const char * restrict s2);

何をする命令なのか

  • s1 が指す文字列の末尾(\0 の位置)に、s2 の内容を \0 まで全部追加する
  • 追加後、s1 の末尾にも \0 が付く
  • 返り値は s1(先頭ポインタ)

strncat

  • ヘッダ:#include <string.h>
  • 形式:char *strncat(char * restrict s1, const char * restrict s2, size_t n);

何をする命令なのか

  • s1 の末尾に、s2 の先頭から最大 n 文字ぶん追加する
  • 追加のあと、必ず \0 を 1 つ付ける
  • 返り値は s1

一番大事な違い(表でスパッと理解)

観点strcatstrncat
追加する量s2 を \0 まで全部s2 の先頭から最大 n 文字
\0 の扱いs2 の \0 も含めて連結結果を終端追加後に必ず \0 を1個付ける
安全性バッファ不足で事故りやすい量を制御できるので比較的安全
典型用途サイズが確実に足りると分かっている場合入力や可変長データをつなぐ場合

重要ルール:strncat の n は「空き容量」ではない

ここが初心者がハマりやすいポイントです。

  • strncat の n は 追加する最大文字数
  • s1 の残り容量を渡すわけではありません

だから、安全にするには「残り容量(終端\0の分も考慮)」から n を計算します。

図でイメージ(末尾にくっつける)

例:s1 = "CAT"、s2 = "FISH"

連結前
s1: C A T \0 . . . .
s2: F I S H \0

strcat(s1, s2) の結果
s1: C A T F I S H \0

strncat で n=2 なら:

strncat(s1, s2, 2) の結果
s1: C A T F I \0

連結後の最大サイズ(安全計算の目安)

strncat(s1, s2, n) のあと、s1 の文字数(\0 を含む)は最大でこうなります。

  • 連結後の最大サイズ(\0込み)
    strlen(連結前の s1) + n + 1

つまり、s1 の配列サイズを cap とすると、安全にするには

  • strlen(s1) + n + 1 <= cap

を満たす必要があります。

サンプルプログラム

「挨拶メッセージを組み立てる」プログラム例です。

ポイントは、strcat の危険な使い方と、strncat の安全な使い方(n計算)を両方が確認できることです。

プロジェクト名:chap11-10-1 ソースファイル名:chap11-10-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

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

int main(void)
{
    char name[32];
    char msg[64] = "こんにちは、";
    const char *tail = "さん!";

    printf("名前を入力してください:");
    scanf("%31s", name);

    // 危険:msg の残り容量を考えず strcat すると、長い入力で溢れる可能性がある
    // strcat(msg, name);

    // 安全寄り:残り容量から「追加できる最大文字数」を計算して strncat
    size_t cap = sizeof(msg);
    size_t len = strlen(msg);

    if (len + 1 < cap) {
        size_t rest = cap - len - 1;     // 終端\0の分を引いた「追加できる文字数」
        strncat(msg, name, rest);
    }

    // tail も同様に安全に追加
    len = strlen(msg);
    if (len + 1 < cap) {
        size_t rest = cap - len - 1;
        strncat(msg, tail, rest);
    }

    printf("完成メッセージ:%s\n", msg);

    return 0;
}

この例のいいところ:

  • scanf は %31s にして入力長を制限
  • msg へ追加するときは 残り容量 rest を計算して strncat
  • strcat をコメントで「危険例」として残し、実務の勘どころを押さえています

返り値が便利(連結しながら表示できる)

strcat も strncat も s1 を返すので、次のように書けます。

printf("%s\n", strcat(msg, "追加"));

ただし、これは msg の容量に絶対余裕があるときだけにしましょう。
読みやすいけど、事故ったときに原因が見えにくくなりがちです。

誤った使い方(文字列リテラルに連結しない)

次はダメです。

char *s = "Soft";
strcat(s, "Bank");    // ダメ

理由は2つあります。

  • 文字列リテラルが書き換え可能な領域かどうかは処理系依存
  • そもそも "Soft" の後ろに書き込める空きがある保証がない。

連結するなら、必ず「書き換え可能で十分なサイズの配列」を用意します。

演習問題

演習11-5

次の仕様の関数 my_strcat と my_strncat を作成してください。

  • my_strcat(d, s) は d の末尾に s を \0 まで連結して d を返す
  • my_strncat(d, s, n) は s の先頭から最大 n 文字を連結し、最後に \0 を付けて d を返す

プロトタイプ:

#include <stddef.h>
char *my_strcat(char *d, const char *s);
char *my_strncat(char *d, const char *s, size_t n);

解答例

#include <stddef.h>

char *my_strcat(char *d, const char *s)
{
    char *t = d;

    while (*d != '\0')
        d++;

    while ((*d++ = *s++) != '\0')
        ;

    return t;
}

char *my_strncat(char *d, const char *s, size_t n)
{
    char *t = d;

    while (*d != '\0')
        d++;

    while (n > 0 && *s != '\0') {
        *d++ = *s++;
        n--;
    }

    *d = '\0';   // 追加後は必ず終端を付ける

    return t;
}

解説

  • どちらも最初に d の末尾(\0位置)まで進めてから追加します。
  • my_strncat は最大 n 文字だけ追加し、最後に必ず \0 を入れるのが特徴です。