C言語の基本|C言語の文字列連結の基本

言葉と言葉を正しくつなごう。strcatとstrncatを理解すれば、C言語の文字列を自由に組み立てられる

C言語の文字列連結の基本

C言語で文字列を扱っていると、複数の文字列をつなげて、1つの文字列にしたい場面が出てきます。

例えば、名字と名前をつなげて氏名を作る、固定された案内文の後ろへ入力内容を追加する、ファイル名と拡張子を組み合わせるといった処理です。

ほかのプログラミング言語では、加算演算子を使って文字列をつなげられる場合があります。しかし、C言語では、文字列を表すchar型の配列同士を加算演算子で連結することはできません。

C言語で文字列を連結するときは、string.hで宣言されているstrcatやstrncatを利用できます。

2つの関数の主な違いは次のとおりです。

  • strcatは連結元の文字列全体を追加する
  • strncatは連結元の先頭から最大n文字を追加する

どちらの関数も、連結先の文字列を直接書き換えます。新しい配列を自動的に作る関数ではありません。

そのため、連結先の配列には、連結前の文字列、追加する文字列、新しいナル文字がすべて入るだけの領域が必要です。

領域が足りない状態で連結すると、配列の外側までデータを書き込むバッファオーバーフローが発生します。文字列連結では、関数の呼び出し方と同じくらい、配列サイズの確認が大切です。

ここでは、次の内容を順番に確認します。

  • 文字列を連結するという考え方
  • strcatが文字列を追加する仕組み
  • strncatで追加する長さを制限する方法
  • strcatとstrncatにおけるナル文字の扱い
  • 連結後に必要となる配列サイズ
  • バッファオーバーフローを防ぐための確認
  • 連結元と連結先が重なる場合の注意

文字列が配列の中でどのように変化するのかをイメージしながら、strcatとstrncatの基本を身につけていきましょう。

文字列を連結するとは

文字列の連結とは、ある文字列の末尾へ別の文字列を付け加え、1つの連続した文字列を作ることです。

例えば、次のような処理です。

連結先連結元連結後
BlueSkyBlueSky
GoodMorningGoodMorning
FileNameFileName
CLanguageCLanguage

連結先となる文字列の後ろへ、連結元となる文字列を追加します。

C言語の文字列には、最後にナル文字があります。連結するときは、連結先のナル文字が置かれている位置を見つけ、そこから連結元の文字を書き込んでいきます。

例えば、Blueという文字列は、配列の中で次のように格納されています。

'B'  'l'  'u'  'e'  '\0'

この末尾にSkyを連結すると、配列の内容は次のように変化します。

'B'  'l'  'u'  'e'  'S'  'k'  'y'  '\0'

もともとBlueの直後にあったナル文字の位置からSkyを書き込み、最後に新しいナル文字を置きます。

連結先の配列が書き換えられる

strcatとstrncatは、第1引数で指定した連結先の配列を直接書き換えます。

例えば、次の配列を用意します。

char destination[20] = "Blue";
char source[] = "Sky";

destinationには20要素分の領域がありますが、最初に格納されている文字列はBlueだけです。

destination
'B'  'l'  'u'  'e'  '\0'  空き  空き  空き  ...

sourceをdestinationへ連結すると、destinationの内容がBlueSkyへ変化します。

sourceの内容は変更されません。

配列連結前連結後
destinationBlueBlueSky
sourceSkySky

結果を格納する新しい配列が作られるわけではありません。destination自身の内容が書き換えられる点を押さえておきましょう。

図1:文字列連結でナル文字の位置が変わる仕組み

この図から分かること

文字列を連結するときは、連結先にあるナル文字の位置から連結元の文字を書き込みます。

BlueへSkyを連結すると、Blueの後ろにあったナル文字の位置へSが入り、その後ろにkとyが続きます。最後には新しいナル文字が置かれます。

連結後はBlueSkyという1つの文字列になります。連結先には、追加する文字列と新しいナル文字を格納できる空き領域が必要です。

C言語では加算演算子で連結できない

C言語の文字列はchar型の配列です。そのため、文字列を格納した配列同士を加算演算子でつなげることはできません。

次のような書き方はできません。

char first[] = "Blue";
char second[] = "Sky";
char result[20];

result = first + second;

配列同士に加算演算子を使用しても、文字列連結の処理にはなりません。

また、配列同士を代入演算子でそのままコピーすることもできません。

文字列を連結するときはstrcatやstrncatを使い、コピーするときはstrcpyなどを使用します。

strcat関数とは

strcatは、第2引数で指定した文字列全体を、第1引数で指定した文字列の末尾へ連結する関数です。

strcatを使うにはstring.hをインクルードします。

#include <string.h>

strcatの関数宣言は次のとおりです。

char *strcat(char *restrict s1, const char *restrict s2);

strcatの引数と戻り値を整理すると、次のようになります。

項目内容
関数名strcat
第1引数s1連結先となる書き換え可能な文字列
第2引数s2追加する文字列
戻り値連結先s1の先頭
必要なヘッダstring.h

第1引数s1の末尾に、第2引数s2の文字列全体が追加されます。

strcatの基本的な使い方

strcatは次のように使用します。

char destination[20] = "Hello";
char source[] = "World";

strcat(destination, source);

連結後のdestinationはHelloWorldになります。

連結前のdestination
'H'  'e'  'l'  'l'  'o'  '\0'

source
'W'  'o'  'r'  'l'  'd'  '\0'

連結後のdestination
'H'  'e'  'l'  'l'  'o'  'W'  'o'  'r'  'l'  'd'  '\0'

strcatはsourceの文字だけでなく、最後のナル文字まで含めてdestination側へ反映します。

strcatの処理の流れ

strcatは、次のような流れで文字列を連結します。

順番処理内容
1連結先s1の先頭からナル文字を探す
2ナル文字が見つかった位置を追加開始位置にする
3連結元s2の先頭から文字をコピーする
4s2のナル文字までコピーする
5連結先s1の先頭を戻り値として返す

連結先と連結元は、どちらもナル文字で正しく終わっている必要があります。

連結先にナル文字がなければ、strcatは追加開始位置を見つけられません。連結元にナル文字がなければ、どこまで追加すればよいのか判断できません。

strcatの戻り値

strcatは、連結後の文字列が格納されているs1の先頭を返します。

char destination[20] = "Spring";
char source[] = "Wind";

char *result = strcat(destination, source);

この場合、resultはdestinationの先頭を指します。

destinationとresultのどちらからでも、連結後のSpringWindを参照できます。

printf("%s\n", destination);
printf("%s\n", result);

学習の初めは、戻り値を受け取らずに使用することも多いでしょう。

strcat(destination, source);

ただし、戻り値として新しい文字列領域が作られるわけではありません。返されるのは、第1引数として渡した連結先の先頭です。

constが付いている理由

strcatの第2引数はconst char型へのポインタです。

const char *restrict s2

constは、strcatが連結元s2の内容を変更しないことを表します。

strcatはs2の内容を読み取り、s1の末尾へコピーします。変更されるのは第1引数s1です。

引数書き換え
s1連結結果が書き込まれる
s2内容は変更されない

第1引数には、書き換え可能で十分な大きさを持つchar型配列を指定する必要があります。

連結先は書き換え可能でなければならない

strcatは第1引数の内容を書き換えるため、連結先には書き換え可能な配列を指定します。

次のようにchar型の配列を用意します。

char destination[20] = "Hello";

一方、文字列リテラルを指すポインタを連結先にすることはできません。

char *destination = "Hello";
char source[] = "World";

strcat(destination, source);

文字列リテラルを書き換えようとした場合の動作は保証されません。

連結先には、次のような書き換え可能な配列を使用しましょう。

char destination[20] = "Hello";
char source[] = "World";

strcat(destination, source);

strcatに必要な配列サイズ

strcatで文字列全体を連結する場合、連結先には次の大きさが必要です。

現在の連結先の長さ
+ 連結元の長さ
+ ナル文字の1要素

strlenを使って表すと、次の式になります。

strlen(destination) + strlen(source) + 1

例えば、HelloへWorldを連結する場合を考えます。

内容長さ
Hello5
World5
ナル文字1
必要な合計11

連結先には、最低でも11要素が必要です。

char destination[11] = "Hello";

11要素あれば、HelloWorldとナル文字を格納できます。

図2:strcatで必要になる配列サイズ

この図から分かること

HelloとWorldは、それぞれ5文字です。連結後のHelloWorldは10文字になりますが、C言語の文字列として扱うには、最後にナル文字が必要です。

そのため、連結先には最低でも11要素必要です。

文字数だけを計算して10要素にすると、ナル文字を格納する領域がありません。文字列連結では、現在の文字列、追加する文字列、ナル文字の3つを合わせて配列サイズを考えます。

strcatを実行する前にサイズを確認する

連結先が配列として宣言されている場所では、sizeofで配列全体の大きさを調べられます。

if (sizeof(destination) >=
    strlen(destination) + strlen(source) + 1) {
    strcat(destination, source);
}

この条件式では、次の2つを比較しています。

左辺右辺
destination全体の大きさ連結後に必要となる大きさ

配列全体の大きさが必要サイズ以上であれば、連結結果を格納できます。

char型の大きさは1バイトなので、char型配列に対するsizeofの結果は、その配列の要素数と一致します。

ただし、配列が関数の引数としてポインタへ変換された後は、sizeofで元の配列全体の大きさを取得できません。配列の実体が分かる場所でサイズを確認することが大切です。

strncat関数とは

strncatは、第2引数で指定した文字列の先頭から、最大n文字を第1引数の末尾へ連結する関数です。

strncatを使うにはstring.hをインクルードします。

#include <string.h>

strncatの関数宣言は次のとおりです。

char *strncat(char *restrict s1,
              const char *restrict s2,
              size_t n);

strncatの引数と戻り値を整理すると、次のようになります。

項目内容
関数名strncat
第1引数s1連結先となる書き換え可能な文字列
第2引数s2追加する文字列
第3引数n連結元から追加する最大文字数
戻り値連結先s1の先頭
必要なヘッダstring.h

strncatの基本的な使い方

Helloの末尾へWorldの先頭3文字を連結する場合は、次のように書きます。

char destination[20] = "Hello";
char source[] = "World";

strncat(destination, source, 3);

連結後のdestinationはHelloWorになります。

連結先
Hello

連結元
World

先頭から3文字
Wor

連結後
HelloWor

sourceの残りのldは追加されません。

strncatは最大n文字を追加する

strncatは、連結元から必ずn文字を追加するとは限りません。

連結元のナル文字へ到達した場合は、n文字より前でも追加を終了します。

連結元の長さn実際に追加する長さ
533
353
555

strncatが実際に追加する長さは、連結元の文字列長とnのうち、小さいほうです。

ただし、配列サイズを簡単かつ安全側で確認する場合は、最大でn文字が追加されると考えられます。

strncatは末尾にナル文字を付ける

strncatは、連結元から最大n文字を追加した後、結果の末尾へナル文字を付けます。

char destination[20] = "Hello";
char source[] = "World";

strncat(destination, source, 3);

連結後の配列は次のようになります。

'H'  'e'  'l'  'l'  'o'  'W'  'o'  'r'  '\0'

strncatで3文字を追加する場合は、追加する3文字に加えて、ナル文字を格納する1要素が必要です。

strncpyとの違い

strncatとstrncpyは名前が似ていますが、ナル文字の扱いが異なります。

関数主な処理ナル文字の扱い
strncpy文字列を指定した要素数でコピーするコピー元がn文字以上の場合は付かないことがある
strncat文字列の末尾へ最大n文字を追加する連結結果の末尾へナル文字を付ける

strncpyでは、コピー後の配列がナル文字で終わらない場合があります。

一方、strncatは、最大n文字を追加した後にナル文字を付けます。

ただし、strncatを使えば自動的に安全になるわけではありません。追加する文字とナル文字が入るだけの領域を、連結先に用意しておく必要があります。

図3:strncatで指定した範囲だけ連結する流れ

この図から分かること

strncatでnに3を指定すると、連結元Worldの先頭からW、o、rの3文字だけが追加されます。

残りのlとdは追加されません。追加した3文字の後ろには、新しいナル文字が置かれます。

連結先には、現在のHello、追加するWor、最後のナル文字がすべて収まる領域が必要です。

strncatに必要な配列サイズ

strncatで最大n文字を追加する場合、連結先には次の大きさを用意します。

現在の連結先の長さ
+ 最大追加文字数n
+ ナル文字の1要素

strlenを使って表すと、次の式になります。

strlen(destination) + n + 1

例えば、Helloへ最大3文字を追加する場合は、次の大きさが必要です。

内容長さ
Hello5
最大追加文字数3
ナル文字1
必要な合計9

連結先の配列が9要素以上であれば、HelloWorとナル文字を格納できます。

if (sizeof(destination) >= strlen(destination) + 3 + 1) {
    strncat(destination, source, 3);
}

連結元がnより短い場合、実際に必要な領域は小さくなります。しかし、最大でn文字追加されると考えて確認すれば、安全側の判定になります。

strcatとstrncatの違い

2つの関数の違いを整理すると、次のようになります。

比較項目strcatstrncat
連結する範囲連結元の文字列全体連結元の先頭から最大n文字
ナル文字結果の末尾に付く結果の末尾に付く
戻り値連結先の先頭連結先の先頭
主な用途文字列全体を追加する追加する長さを制限する
主な注意点連結元全体とナル文字の領域が必要最大n文字とナル文字の領域が必要

関数を選ぶ基本的な考え方は次のとおりです。

  • 連結元全体を追加する場合はstrcatを使う
  • 連結元の先頭部分だけを追加する場合はstrncatを使う
  • どちらの場合も連結前に配列サイズを確認する
  • 第1引数には書き換え可能な文字列を指定する
  • 連結先と連結元はナル文字で終わっている必要がある

strncatのnは日本語の文字数とは限らない

strncatの第3引数nは、char型の要素数を表します。一般的な環境では、バイト数として考えられます。

半角英数字は1文字を1バイトで表すことが多いため、nと画面上の文字数が一致しやすくなります。

一方、UTF-8で日本語を扱う場合、1文字が複数バイトで表現されます。日本語文字の途中で連結を止めると、不完全なバイト列になる可能性があります。

例えば、strncatで日本語文字列から3を指定して連結した場合、常に日本語3文字が追加されるわけではありません。

文字列nの考え方
半角英数字画面上の文字数と一致しやすい
UTF-8の日本語nは日本語の文字数とは限らない
マルチバイト文字文字の途中で分割する可能性がある

基本動作を確認するプログラムでは、半角英数字を使うと結果を理解しやすくなります。

バッファオーバーフローに注意する

連結先の領域が不足した状態でstrcatやstrncatを実行すると、配列の外側までデータが書き込まれます。

これをバッファオーバーフローといいます。

例えば、次の配列にはHelloとナル文字だけが入ります。

char destination[6] = "Hello";

この配列には空き領域がないため、Worldを連結できません。

char source[] = "World";

strcat(destination, source);

この処理を実行すると、destinationの範囲を越えて書き込むため、動作は保証されません。

バッファオーバーフローによって、次のような問題が発生する可能性があります。

  • ほかの変数の値が壊れる
  • 文字列が正しく表示されない
  • プログラムが異常終了する
  • 実行するたびに異なる結果になる
  • セキュリティ上の問題につながる

連結前に確認する4つの長さ

文字列を連結する前には、次の4つを確認します。

確認項目内容
連結先の現在の長さすでに何文字使っているか
連結元の長さ全体で何文字あるか
実際に追加する長さstrcatでは全体、strncatでは最大n文字
連結先の配列サイズ連結後の文字列とナル文字が収まるか

strcatでは、次の条件を確認します。

sizeof(destination) >=
strlen(destination) + strlen(source) + 1

strncatで最大n文字を追加する場合は、次の条件を確認できます。

sizeof(destination) >= strlen(destination) + n + 1

最後の1は、連結結果を終端するナル文字のために必要です。

strcatとstrncatを使うプログラム

次のプログラムでは、最初にstrcatで文字列全体を連結し、その後にstrncatで別の文字列の先頭3文字だけを追加します。

半角英数字を使用することで、strlenの結果とstrncatで指定する長さを確認しやすくしています。

文字列全体と先頭部分を順番に連結するプログラム

ファイル名:12_6_1.c

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

int main(void)
{
    char message[30] = "Hello";
    char separator[] = ", ";
    char name[] = "World";

    /* separator全体を連結する */
    if (sizeof(message) >=
        strlen(message) + strlen(separator) + 1) {
        strcat(message, separator);
        printf("strcatの結果: %s\n", message);
    } else {
        puts("strcatを実行するための領域が不足しています。");
    }

    /* nameの先頭3文字を連結する */
    if (sizeof(message) >= strlen(message) + 3 + 1) {
        strncat(message, name, 3);
        printf("strncatの結果: %s\n", message);
    } else {
        puts("strncatを実行するための領域が不足しています。");
    }

    return 0;
}
実行結果の例
strcatの結果: Hello, 
strncatの結果: Hello, Wor

文字列を順番に連結するプログラムの解説

必要なヘッダをインクルードする部分

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

stdio.hはprintfとputsを使うために必要です。

string.hはstrlen、strcat、strncatを使うために必要です。

連結先と連結元を用意する部分

char message[30] = "Hello";
char separator[] = ", ";
char name[] = "World";

messageは連結先です。30要素の配列を用意し、最初にHelloを格納しています。

separatorとnameは連結元です。

配列内容役割
messageHello連結先
separatorカンマと空白strcatで全体を追加する
nameWorldstrncatで先頭部分を追加する

messageには30要素あるため、今回の連結結果を十分に格納できます。

strcatの前にサイズを確認する部分

if (sizeof(message) >=
    strlen(message) + strlen(separator) + 1) {

sizeof(message)はmessage全体の大きさである30を返します。

strlen(message)はHelloの長さである5、strlen(separator)はカンマと空白の長さである2を返します。

必要な大きさは次のとおりです。

5 + 2 + 1 = 8

messageには30要素あるため、連結できます。

strcatで文字列全体を追加する部分

strcat(message, separator);
printf("strcatの結果: %s\n", message);

strcatによって、separatorの内容全体をmessageの末尾へ追加します。

連結後のmessageは次の文字列になります。

Hello,

カンマの後ろには空白が1文字入っています。

strncatの前にサイズを確認する部分

if (sizeof(message) >= strlen(message) + 3 + 1) {

strcatを実行した後のmessageの長さは7です。

nameから最大3文字を追加し、最後にナル文字を置くため、必要な大きさは次のようになります。

7 + 3 + 1 = 11

messageは30要素あるため、問題なく連結できます。

strncatで先頭3文字を追加する部分

strncat(message, name, 3);
printf("strncatの結果: %s\n", message);

nameに格納されているWorldの先頭から3文字をmessageへ追加します。

追加される文字はW、o、rです。

連結後のmessageは次の文字列になります。

Hello, Wor

strncatは追加した3文字の後ろへナル文字を置くため、messageは正しく終端された文字列になります。

連結元と連結先が重なる場合

strcatとstrncatでは、連結先と連結元が重なっている場合の動作は保証されません。

例えば、同じ配列の一部分を連結元として、その配列自身の末尾へ追加するような処理には注意が必要です。

char text[20] = "ABCDE";

strcat(text, text + 2);

この例では、連結先textと連結元text+2が同じ配列内で重なっています。このような使い方の動作は保証されません。

学習の初めは、次のように別の配列を使用するのが基本です。

char destination[20] = "ABC";
char source[] = "DEF";

strcat(destination, source);

連結元と連結先を別の領域として用意することで、重なりによる問題を避けられます。

文字列連結で確認したいこと

strcatとstrncatを使う前に、次の項目を確認しましょう。

  • string.hをインクルードしているか
  • 連結先が書き換え可能な配列か
  • 連結先がナル文字で正しく終わっているか
  • 連結元がナル文字で正しく終わっているか
  • 連結先の現在の長さを確認したか
  • 追加する文字列の長さを確認したか
  • 新しいナル文字の1要素を計算に含めたか
  • 連結先に十分な空き領域があるか
  • strncatのnを日本語の文字数と誤解していないか
  • 連結元と連結先の領域が重なっていないか

文字列を連結できるだけではなく、配列の範囲内で正しく連結できることが大切です。

strcatとstrncatは便利な関数ですが、連結先の配列サイズを自動的には確認しません。現在の文字列長、追加する長さ、ナル文字の領域を連結前に確認する習慣をつけましょう。