【C言語】文字列の結合・連結(strcat・sprintf)

C言語における文字列結合の方法についての解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、C言語での「文字列の結合・連結」を行う方法を解説します。

例えば2つの文字列 "Hello ""World" が存在する場合、これらを結合すれば "Hello World" という文字列が生成されることになります。このような文字列の結合・連結方法について説明していきます。

2つの文字列を結合・連結する様子

他のプログラミング言語では文字列同士を + 演算すれば文字列を結合できるような場合もあるのですが、C言語の場合は基本的に標準ライブラリ関数を利用して文字列の結合を行うことになります。

そして、C言語の標準ライブラリ関数において文字列の結合を目的に用意されている関数は strcatstrncat になります。文字列を結合するだけであれば strcat を利用すればよいです。ですが、strcat は使い方を誤ると重大な問題がプログラムに発生することになります。それを解決するのが strncatで、これ利用することでより安全に文字列の結合を行うことが出来ます。

また、文字列結合を目的とした関数ではないのですが、応用すれば sprintfsnprintf 関数を利用して文字列を結合することも可能です。これらの関数に関しては下記ページで解説していますので、詳しくは下記ページを参照していただければと思います。このページでは、これらの関数に関しては結合を行うための使い方についてのみ説明します。

sprintf関数とsnprintf関数の解説ページアイキャッチ 【C言語】sprintf 関数と snprintf 関数(お手軽に文字列を生成する関数)

ということで、このページでは strcat や strncat の解説及び、これらを利用した文字列の結合について重点的に解説し、その後、sprintf と snprintf を利用した文字列の結合について紹介していきたいと思います。

strcat による文字列の結合

では、strcat 関数と、この strcat 関数を利用した文字列の結合について説明していきます。

strcat 関数

まずは strcat 関数の紹介から行っていきます。strcat は、まさにこのページの目的となっている2つの文字列を結合する関数になります。

この strcat 関数は string.h で宣言されており、使用する際は string.hinclude しておく必要があります。

strcat 関数の引数と返却値

strcat 関数の引数と返却値はそれぞれ下記のようになります。

第1引数 char *dest 結合する文字列1
第2引数 const char *src 結合する文字列2
返却値 char * 結合後の文字列(dest

第1引数と第2引数に指定した文字列が結合され、その文字列が返却値として得られるという引数・返却値の構成になっています。非常に単純そうに思えますが、実は strcat 関数の使い方は結構難しいです。

スポンサーリンク

strcat 関数による文字列結合の例

その難しさの説明を行う前に、まずは strcat 関数を利用した文字列結合の例を示しておきます。

その例が下記となります。

strcat関数による文字列結合
#include <stdio.h> // printf
#include <string.h> // strcat

int main(void) {
    char dest[256] = "Hello ";
    char src[256] = "World!";
    char *ret;
    ret = strcat(dest, src);

    printf("ret = %s\n", ret);
}

このソースコードをコンパイルして実行すれば下記が出力されます。ret の出力結果が dest"Hello "src"World!" を結合した文字列となっていることが確認できると思います。

ret = Hello World!

もちろん、結果の ret を文字列として扱い、strlen で文字列長を求めたり strtok 関数で文字列を分離したりと、他の str 系の関数への入力として用いることも可能です。

strcat 関数の動作

続いて strcat 関数の動作について説明していきます。

(おさらい)文字列とは

本題に入る前に、まずはC言語での文字列の扱いについておさらいしておきます。おさらいが不要という方は次の strcat 関数での結合の動作 までスキップしてください。

C言語には文字列という型が存在しません。そのため、下記のようなデータを疑似的に文字列と考えるのが一般的になっています。

  • '\0' で終端した char 型のデータ系列の先頭アドレス

このページでは、「データ系列 == 配列」と考えていきたいと思います。例えば malloc 関数という関数で取得したメモリの可能性もありますし、リテラルの可能性もありますが、まずはそれは置いておきましょう。

例えば "Hello" という文字列は下図のような配列で扱うことが可能です。'\0' はヌル文字(NULL 文字)と呼ばれます。また、C言語において、' (シングルクォーテーション) で囲まれたものは文字として扱われます。ただ、今後は図のスペース都合上 ' は省略させていただきますのでご了承ください。

文字列の構成を表す図

そして、この配列の先頭のアドレスを格納したポインタ変数を str とすれば、この str が文字列として扱われることになります。また、C言語においてはソースコードに配列名のみを指定した場合は「配列の先頭アドレス」として扱われることになるため、この配列名も文字列として扱うことが可能です。

ここで覚えておいていただきたいのが、C言語で扱う文字列は必ずヌル文字('\0')で終端されるという点と、これはどんな配列においても共通に言えることですが、配列外へのデータの格納など、配列外へのアクセスをしてはいけないという点になります。

strcat 関数での結合の動作

で、ここで strcat に話を戻すと、まず第1引数と第2引数には、上記のような '\0' で終端した char 型の配列の先頭アドレス or そのアドレスを格納したポインタ変数を指定する必要があることになります。

そして、strcat を実行すると、第1引数で指定された文字列の '\0' 以降を第2引数で指定された文字列の '\0' までのデータで上書きされることになります。この上書きにより、結果的に2つの文字列が結合されることになります。

つまり、第1引数 dest で指定した文字列自体が第2引数 src と結合した文字列に変化することになります。

strcat関数によって文字列が結合される様子を示す図

したがって、strcat 関数で文字列を結合した結果は、前述のとおり返却値から得ることも出来ますし、第1引数 dest からも得ることができます。

strcat関数の返却値と第1引数との関連性を示す図

strcat 関数利用時の注意点

ここまでが strcat 関数の動作に関する説明になります。続いて、strcat 関数利用時の注意点について説明していきます。

配列には結合後の文字列のサイズが必要となる

まず、strcat 関数利用時に注意が必要になるのが、第1引数 dest を先頭アドレスとする配列には下記のサイズが必要になるという点になります(文字列長には '\0' は含まれません)。+ 1 は文字列を終端する '\0' の分のサイズとなります。

dest の文字列長 + src の文字列長 + 1

つまり、dest を先頭アドレスとする配列のサイズは dest の文字列長 + 1 だけでは不十分ということになります。もし、dest を先頭アドレスとする配列のサイズが dest の文字列長 + 1 のような、上記よりも小さなサイズの場合、下の図のようにバッファーオーバーランが発生することになります。

strcat関数の実行によってバッファーオーバーランが発生する様子

そして、バッファーオーバーランが発生すると意図せず他の関係ない配列や変数が変更される可能性もありますし、スタックが破壊されてプログラムが異常終了する可能性もあります。意図せず他の配列や変数が変更されてしまうことでプログラムに脆弱性が生まれることになる可能性もありますので、dest を先頭アドレスとする配列のサイズには十分に注意する必要があります。

バッファーオーバーランによって他の関係ない配列のデータが上書きされてしまう様子

例えば下記の例を考えてみましょう!これは、strcat 関数の実行によって "Hello World!""Good bye!" とを結合して "Hello World!Good bye!"という文字列を得ることを期待したプログラムのソースコードになります。

バッファーオーバーランの発生
#include <stdio.h> // printf
#include <string.h> // strcat

int main(void) {
    char dest[] = "Hello World!";
    char src[] = "Good bye!";
    char *ret;

    printf("size of dest : %ld\n", sizeof(dest));
    printf("size of src  : %ld\n", sizeof(src));

    ret = strcat(dest, src);

    printf("ret = %s\n", ret);
}

私の PC で上記プログラムを実行すると、下記のような出力が得られました。確かに文字列の結合には成功してはいるものの、最後の行で示す通りスタックメモリが破壊されていることが検知されています。これは、バッファーオーバーランが発生して配列外のデータが意図せず上書きされていることが原因で発生しています(PC 等の環境によっては正常に終了する場合もあると思います)。

size of dest : 13
size of src  : 10
ret = Hello World!Good bye!
*** stack smashing detected ***: terminated

で、ここで各配列のサイズに注目すると destsrc ともに変数宣言時には配列のサイズを指定せずに右辺の文字列で初期化しているため、この 文字列長 + 1 が配列のサイズに自動的に設定されることになります。

destとsrcの変数宣言
char dest[] = "Hello World!";
char src[] = "Good bye!";

したがって、Hello World! の文字列長 12+1 した 13 が配列 dest のサイズとなります。初期化後の配列 dest を図示すると下の図のようになります。

変数宣言時に自動的に配列のサイズが決定される様子

そして、前述のとおり、strcat 関数を実行することで引数 dest の終端 '\0' 以降が引数 src の文字列に上書きされることになります。つまり、下の図のように引数 src の2文字目の o 以降は配列外に格納されることになります。ここで意図せず何かしらのデータを上書きしているため、結果的にスタックメモリが破壊されてしまい、上記のような警告メッセージが表示されることになります。

strcat関数実行によってバッファーオーバーランが発生する様子

このように、引数 dest に指定するアドレスの配列のサイズが小さいと、何かしらの問題が発生してプログラムを意図したとおりに動作させられなくなってしまいます。

この問題の解決方法は単純で、これは引数 dest に指定するアドレスの配列のサイズを 結合後の文字列の文字列長 + 1 以上にすることで解決できます。

例えば下記のように配列 dest のサイズを十分大きく設定してやれば、先ほど表示された警告メッセージが表示されなくなり、正常にプログラムが終了したことが確認できるようになります。

バッファーオーバーランの発生
#include <stdio.h> // printf
#include <string.h> // strcat

int main(void) {
    char dest[256] = "Hello World!";
    char src[] = "Good bye!";
    char *ret;

    printf("size of dest : %ld\n", sizeof(dest));
    printf("size of src  : %ld\n", sizeof(src));

    ret = strcat(dest, src);

    printf("ret = %s\n", ret);
}

下記のようなサイズを指定せずに配列を初期化する変数宣言方法は配列のサイズが自動的に設定されて便利ではありますが、配列のサイズとしては右辺の文字列を格納するのための最低限のサイズしか確保されないため、特に strcat 関数を使用際には不適切なサイズとなってしまうので注意が必要です。

変数宣言時の配列の初期化
char dest[] = "Hello World!";
char src[] = "Good bye!";

strcat 関数実行によって元々の dest の文字列は存在しなくなる

続いて注意点の2つ目を説明していきます。

ここまで説明してきた通り、strcat 関数では第1引数 dest の文字列が変更されることになります。より具体的には、前述で示した通り第1引数 dest の文字列の '\0' 以降が第2引数 src の文字列で上書きされることになります。

そのため、第1引数 dest の文字列は strcat 関数実行後に第2引数 src の文字列と結合した文字列に変化することになります。

したがって、strcat 関数実行によって元々の dest の文字列は存在しなくなることになります。

例えば下記のように strcat 関数実行後に dest の元々の文字列を printf で表示したいとしても、strcat 関数実行後には元々の文字列は残っていないため、それが実現できないことになります。

destが上書きされてしまう例
#include <stdio.h> // printf
#include <string.h> // strcat

int main(void) {
    char dest[256] = "Hello";
    char src[] = "World";
    char *ret;

    ret = strcat(dest, src);

    printf("%s + %s = %s\n", dest, src, ret);
}

本来であれば最後の printf では下記を出力したいところなのですが、

Hello + World = HelloWorld

実際には下記のように dest を出力すると結合後の文字列が出力されてしまい、意図しない結果が得られることになります。

HelloWorld + World = HelloWorld

元々の文字列を表示したいのであれば、下記のように dest の文字列を strcat 関数実行前にコピーして退避しておく必要があります。

事前に文字列をコピーして退避
#include <stdio.h> // printf
#include <string.h> // strcat

int main(void) {
    char dest[256] = "Hello";
    char src[] = "World";
    char *ret;
    char dest_copy[256];

    // destの退避
    strcpy(dest_copy, dest);

    ret = strcat(dest, src);

    printf("%s + %s = %s\n", dest_copy, src, ret);
}

退避してやれば解決する単純な問題ではあるのですが、つい陥りがちな問題となりますので、第1引数 dest の文字列が変更されることを理解したうえで strcat 関数は利用するようにした方が良いです。

dest には変更不可なデータのアドレスを指定してはダメ

前述でも説明したように、strcat 関数は第1引数 dest で指定したアドレスのデータを変更します。つまり、第1引数 dest で指定したアドレスのデータは変更可能なものでなければなりません。strcat 関数利用時の注意点の3つ目はこの点になります。

変更不可なデータのアドレスを指定して strcat 関数を実行した際にはプログラムが異常終了することになります。これは、strcat 関数がその変更不可なデータを変更しようとすることが理由となります。

strcat関数の第1引数に変更不可な文字列を指定した場合にプログラムが異常終了することを示す図

変更不可なデータの代表例は文字列リテラルです。ソースコードに下記のように " (ダブルクォーテーションマーク) で囲ったデータは文字列リテラルとして扱われます。基本的に文字列として扱うことが出来るのですが、変数の配列と違って変更は不可です。

文字列リテラルの説明図

文字列リテラルの詳細は下記ページで解説していますので興味があれば是非読んで見てください。

C言語における文字列リテラルの扱いの解説ページのアイキャッチ C言語での “文字列リテラル” の扱い

文字列リテラルは変更不可であるため、文字列リテラルを直接 strcat 関数の第1引数 dest に指定して実行するとプログラムが異常終了することになります。また、文字列リテラルを指すポインタを strcat 関数の第1引数 dest に指定した場合も、結局はそのポインタ変数の指す文字列リテラルが変更されることになるため strcat 関数実行時にプログラムが異常終了することになります。

例えば下記は文字列リテラルを指すポインタを strcat 関数の第1引数 dest に指定する例で、コンパイルして実行するとおそらくプログラムが異常終了して最後の printf の出力が行われないと思います。

destに文字列リテラルを指すポインタを指定する例
#include <stdio.h>
#include <string.h>

int main(void) {
    char *dest = "Hello";
    char src[] = "World";
    char *ret;

    ret = strcat(dest, src);

    printf("ret = %s\n", ret);
}

より具体的には、上記のプログラムを実行すると、下の図のように例外が発生したり Segmentation Fault が発生したりしてプログラムが異常終了すると思います。

文字列リテラルを変更しようとして例外が発生したときの警告画面

これは、下記のように変更してやることで簡単に解決することができます。char *dest = "Hello"; の行を char dest[256] = "Hello"; に変更しただけです。

destに文字列リテラルをコピーした配列を指定する例
#include <stdio.h>
#include <string.h>

int main(void) {
    char dest[256] = "Hello";
    char src[] = "World";
    char *ret;

    ret = strcat(dest, src);

    printf("ret = %s\n", ret);
}

これも文字列リテラルを変更しようとしているようにも思えますが、この場合は正常にプログラムが終了することになります。

char *dest = "Hello"; と char dest[256] = "Hello"; はそれぞれ dest の変数宣言を行なっており、これらの printf("%s", dest) での出力結果も全く同じものになります。

ですが、これらの dest は全く異なる変数となります。前者の場合は dest は文字列リテラルそのものを指すポインタ変数であり、前述のとおり、この変数の指す文字列は変更不可になります。なので、strcat 関数の第1引数に指定すると、dest の指す文字列リテラルが変更されることになってプログラムが異常終了します。

strcat関数がポインタ変数の指す文字列リテラルを変更する様子

それに対し、後者の場合は destは文字列リテラルをコピーしただけの単なる配列であり変更可能です。なので、strcat 関数の第1引数に指定すると配列が変更されることになるだけでプログラムは正常に終了することになります。

strcat関数が文字列リテラルがコピーされた配列を変更する様子

ここまでの説明のとおり、strcat 関数を上手く使いこなすためには第1引数で指定されるアドレスが変更可能か否かをしっかり意識してプログラミングすることが重要となります。

スポンサーリンク

strncat による文字列の結合

続いて strncat 関数と、この strncat 関数を利用した文字列の結合について説明していきます。

strncat 関数

strncat 関数は strcat 関数と同様に2つの文字列を結合する関数になります。後述で解説するように、strncat 関数は strcat 関数利用時の注意点 で挙げたバッファーオーバーランを防止することが可能な関数であり、strcat 関数よりも安全に文字列の結合を行うことが可能です。

この strncat 関数においても string.h で宣言されており、使用する際は string.hinclude しておく必要があります。

strncat 関数の引数と返却値

strncat 関数と strcat 関数の違いは引数にあります。下記がstrncat 関数の引数を示す表であり、strcat 関数に比べて引数 n を指定できるようになっています。

第1引数 char *dest 結合する文字列1
第2引数 const char *src 結合する文字列2
第3引数 size_t n 文字列1に結合する最大サイズ
返却値 char * 結合後の文字列(dest

strncat 関数による文字列結合の例

次は strncat 関数を利用した文字列結合の例を示しておきます。

その例が下記となります。第3引数 n の求め方がてきとうになっていますが、この n の求め方については後述で解説します。

strncat関数による文字列結合
#include <stdio.h> // printf
#include <string.h> // strncat

int main(void) {
    char dest[256] = "Hello ";
    char src[256] = "World!";
    char *ret;
    size_t n;

    n = 256;
    ret = strncat(dest, src, n);

    printf("ret = %s\n", ret);
}

このソースコードをコンパイルして実行すれば下記が出力されます。ret の出力結果が dest"Hello "src"World!" を結合した文字列となっていることが確認できると思います。

ret = Hello World!

スポンサーリンク

strncat 関数の動作

続いて strncat 関数の動作について説明していきます。

strncat 関数での結合の動作

strcat 関数では第1引数 dest の文字列の '\0' 以降に第2引数 src の文字列が上書きされることで2つの文字列の結合が実現されていました。この点は strncat 関数でも同じです。

ですが、strncat 関数と strcat 関数とでは上書きされるサイズが異なります。

strcat 関数の場合、この上書きでは第2引数 src に指定した文字列の 文字列長 + 1 のサイズ分の上書きされることになります。+1'\0' の上書きになります。つまり、第2引数 src の先頭から文字の上書きが行われ、次に '\0' が見つかるまでこの上書きが繰り返し行われることになります。そして、その後に '\0' の上書きが行われます。したがって、もし第2引数の src のアドレス以降に '\0' が存在しなければ延々と上書きが繰り返し実施されることになりバッファーオーバーランが発生します。

それに対し、strncat 関数の場合、この上書きは最大で第3引数 n のサイズ分だけしか行われないようになっています。この n の内訳は下記のようになっています。

  • n - 1 文字:src の先頭から n - 1 文字
  • 1 文字:'\0'

したがって、strncat 関数では第1引数 dest の文字列の '\0' 以降に第2引数 src の文字列が最大で n - 1 文字のみが上書きされ、さらに最後に '\0' が上書きされることになります。つまり、strncat 関数の実行によって上書きされる文字列の最大文字列長は n 文字となります。

strncat関数の引数nの役割を示す図

もし 第2引数 src の文字列長 + 1n よりも小さいのであれば、第2引数 src の文字列全体と '\0' が上書きされることになります。この場合は strcat 関数と同じ結果が得られることになります。

strncat関数の引数nの役割を示す図2

最大で n 文字分の上書きしか行われないため、strncat 関数の場合は第3引数 n を上手く利用してやることで strcat 関数利用時の注意点 で挙げた strcat 関数でのバッファーオーバーランを防ぐことが可能です。しかし、逆に第3引数 n の指定を誤ると意図したサイズの上書きが行われずに上手く文字列結合が出来ないことになります(文字列が途中で途切れる)。ということで、strncat 関数においては第3引数 n  の指定が非常に重要になります。

では、具体的に第3引数 n には何を指定すればよいでしょうか?

ここからは、その第3引数 n に指定すべき値について説明していきたいと思います。

第3引数 n の指定の仕方

strncat 関数を利用する目的は「2つの文字列の “全体” を結合したい」であることが一番多いと思います。この場合、結論としては第3引数 n には下記を指定してやるのが理想です。ただ、これは理想であって、実際には第1引数 dest の配列サイズが分からない場合があり、その場合は下記のように n を求めることはできません。ですが、まずは strncat 関数を利用時は下記の式で n を指定することを検討するのが良いです。

nの理想的な計算式
size_t n;
long sn;

sn = destの配列サイズ - destの文字列長 - 1;
if (sn > 0) {
    n = sn;
} else {
    n = 0;
}

例えば、strncat 関数による文字列結合の例 で示したソースコードを上記のように n を求めるように変更すれば次のようになります。

nへの指定
#include <stdio.h> // printf
#include <string.h> // strncat

#define DEST_SIZE (10)

int main(void) {
    char dest[DEST_SIZE] = "Hello ";
    char src[256] = "World!";
    char *ret;
    size_t n;
    long sn;

    sn = DEST_SIZE - strlen(dest) - 1;
    if (sn > 0) {
        n = sn;
    } else {
        n = 0;
    }

    ret = strncat(dest, src, n);

    printf("ret = %s\n", ret);
}

このソースコードの場合、dest のサイズである DEST_SIZE が小さいため strcat 関数を実行して文字列結合を行うとバッファーオーバーランが発生しますが、strncat 関数の場合は dest の後ろ側に結合される文字列の長さが  3 文字のみとなるためバッファーオーバーランを防ぐことが出来ます。 また、最後の printf で出力される結果は下記のようになります。

ret = Hello Wor

src の文字列が途中で途切れているのが気になる方もおられるかもしれませんが、strncat 関数を利用する場合だけでなく、C言語で文字列や配列を扱う上で一番に注意すべきことはバッファーオーバーランになると思います。なので、この結果では文字列が途切れていることよりも、バッファーオーバーランを防ぐことが出来ていることに注目した方が良いです。

文字列結合結果が途切れていることに対する印象が人によって異なることを示す図

そして、上記のように n を計算して第3引数に指定してやることでバッファーオーバーランを防ぐことが出来ています。そして、これは上記の例だけでなく、n を前述のように求めてやることで確実にバッファーオーバーランを防ぐことができます。

次は、その理由について説明しておきます。

前述のとおり、strncat 関数では第1引数 dest の文字列の '\0' 以降に第2引数 src の文字列の上書きが行われます。第1引数 dest のアドレスから '\0' までのサイズは第1引数 dest の文字列長となりますので、strncat 関数で結合可能なサイズは dest の配列サイズ - dest の文字列長 となります。ただし、 strncat 関数では必ず最後に '\0' での上書きが行われますので '\0' を除いてて結合可能なサイズは dest の配列サイズ - dest の文字列長 - 1 ということになります。

引数nの求め方の説明図

つまり、strncat 関数では第3引数に dest の配列サイズ -  dest の文字列長 - 1 以下のサイズを指定することでバッファーオーバーランを確実に防いで安全な文字列の結合が実現可能となります。逆に、このサイズを超えた場合はバッファーオーバーランが発生する可能性があります。

もちろん、dest の配列サイズ -  dest の文字列長 - 1 よりも小さなサイズであればバッファーオーバーランを防ぐことが出来るため、第3引数 n にはもっと小さな値を指定しても問題ありません。ですが、第2引数 src が極力途切れることのないように第1引数 dest に結合したいのであれば第3引数 n には出来るだけ大きいサイズを指定した方が良いです。ということで、バッファーオーバーランを確実に防ぐことのできるサイズの最大値を第3引数 n に指定するのが望ましく、それが dest 配列のサイズ -  dest の文字列長 - 1 ということになります。

そして、DEST_SIZEdest の配列サイズとした場合は、この第3引数 n は下記の式で求めることが可能です。strlen は文字列長を求める関数で、要は strlen 関数の引数に指定したアドレスか '\0' までの長さを求める関数になります。

nの求め方
size_t n;
long sn;

sn = DEST_SIZE - strlen(dest) - 1;
if (sn > 0) {
    n = sn;
} else {
    n = 0;
}

ちょっとややこしいのが、n の型が size_t である点になります。size_t は符号無しの型になり、n = DEST_SIZE - strlen(dest) - 1 のように直接 n に代入すると右辺が負の値になった場合に符号無しとして扱われるため、とんでもなく大きな値が n に代入されてしまうことになります。そして、それを strncat 関数の第3引数に指定してしまうとバッファーオーバーランを防ぐ効果が無くなってしまうので注意が必要となります。

DEST_SIZE - strlen(dest) - 1 が負の値になるということは、dest の配列よりも文字列長が大きい or dest の配列と文字列長が同じ場合であることを意味しており、つまりは dest の配列内に '\0' が存在しないということになります。なので、そもそも dest は文字列のデータとして破綻しています。

ということで、DEST_SIZE - strlen(dest) - 1 が負の値になる場合は dest の配列サイズに問題があるのですが、そういった場合でも strncat 関数でのバッファーオーバーランを防ぐために、念のため DEST_SIZE - strlen(dest) - 1 が負の値になった場合は n0 を代入して strncat 関数で文字列結合が行われないようにしています。

第1引数 dest の配列サイズ

ここまでの解説のとおり、第3引数 n を求めるようにしてやれば少なくとも strncat 関数でバッファーオーバーランが発生することを防ぐことが出来ます。

ただし、バッファーオーバーランを防ぐことは出来たとしても、文字列結合時に第2引数 src の文字列が途切れてしまう可能性は残ります。この可能性を極力小さくするためには、結局は第1引数 dest の配列サイズを大きくしておく必要があります。

前述のとおり、第3引数 n は第1引数 dest に結合する文字列の最大サイズの指定となります。この引数の値を大きくしてやれば、第1引数 dest に結合可能な文字列長が長くなることになり、結合する src の文字列が途切れることを防ぐことが出来ます。

そして、バッファーオーバーランを防ぐためには destの配列サイズ - destの文字列長 - 1 以下の値を n に指定する必要がありますので、結局 n を大きくするためには destの配列サイズ を大きくするか destの文字列長 を小さくするしか方法はありません。文字列長に関しては、例えばユーザーからの入力によって文字列が動的に決定することも多くてコントロールしづらいため、n を大きくするためには destの配列サイズ を大きくしておく必要があるという結論になります。

ということで、strncat 関数を利用する際には(strcat 関数も同様ですが)、dest の配列サイズは余裕をもって確保しておくことが重要です。dest の配列サイズを大きくするだけでバッファーオーバーランを防ぐ効果もあるのですが、それだけでは防止策として確実ではないため、結合後の文字列が dest の配列サイズを超えてしまうことを防ぐために前述の計算方法で n を求めておいてやるのが良いと思います。

destの配列サイズを大きくすることとnを指定することの役割の違いを示す図

ちょっと説明が長くなってしまいましたが、単に文字列を結合するだけの関数ではあるのですが、strcat 関数や strncat 関数は安全に使おうと思うと注意すべき点が多く、関数の使い方の難易度は結構高いです。まずは、strncat 関数の方がバッファーオーバーランを防ぎやすく、第3引数 n の指定の仕方や第1引数の配列のサイズは余裕をもって確保しておくことが重要であることは覚えておきましょう!

スポンサーリンク

sprintfsnprintf による文字列の結合

最後に sprintfsnprintf 関数を利用した文字列結合について説明しておきます。これらの関数自体については別途下記ページで解説していますので、詳細は下記ページを参照していただければと思います。

sprintf関数とsnprintf関数の解説ページアイキャッチ 【C言語】sprintf 関数と snprintf 関数(お手軽に文字列を生成する関数)

printf による文字列の結合

sprintf とは、一言でいえば printf の配列出力版になります。printf では文字列が標準出力に出力されますが、sprintf では文字列が配列に出力されることになります。

そして、 printf 関数の第1引数に指定するフォーマットにおいて、%s は第2引数以降で指定したアドレスのデータを文字列として出力する変換指定子となります。そのため、下記のように printf 関数を実行すれば "%s%s" によって1つの文字列の直後に他の文字列を結合した結果が標準出力に出力されることになります。具体的には、下記を実行すれば標準出力に "Hello ""World" を結合した結果の "Hello Word" が出力されることになります。

printfによる文字列の結合
printf("%s%s", "Hello ", "Word");

つまり、printf 関数を利用することで文字列の結合は非常に簡単に実現できます。ですが、出力先が標準出力になってしまっており、配列などの変数での文字列結合結果の取得が出来ません。

printfでの文字列結合結果が標準出力に出力される様子

sprintf 関数での文字列結合

そこで活躍するのが sprintf 関数で、前述のとおり sprintf 関数は printf 関数の配列出力版になります。

sprintf 関数の引数や返却値は下記のようになります。sprintf 関数は、これも printf 同様に stdio.h で宣言されていますので利用時は stdio.h をインクルードしておく必要があります(string.h ではないことに注意してください)。

第1引数 char *str 出力先の配列(のアドレス)
第2引数 const char *format 出力する文字列のフォーマット
第3引数以降 ... 可変個引数
返却値 int 出力された文字列の文字列長

ちょうど、printf 関数の第1引数以降が snprintf 関数の第2引数以降に相当しています。前述のとおり、printf 関数では出力先が標準出力になりますが、snprintf 関数の場合は第1引数に指定した配列に出力されます。

sprintfでの文字列結合結果が配列に出力される様子 

そして、前述のとおり printf 関数では第1引数に "%s%s" を指定し、第2引数以降に結合したい文字列を2つ指定することで文字列の結合が可能です。これと同様に、sprintf 関数では第2引数に "%s%s" を指定し、第3引数以降に結合したい文字列を2つ指定することで文字列の結合を行うことができ、さらに第1引数に指定した配列に結合結果が出力されます。つまり、これだけで文字列の結合が実現できることになります。

まとめると、sprintf 関数を下記のようにして実行することで2つの文字列の結合を実現できます。

文字列結合時のsprintf関数の引数
  • 第1引数:結合後の文字列の出力先となる配列の配列名
  • 第2引数:"%s%s"
  • 第3引数:結合する文字列1
  • 第4引数:結合する文字列2

もちろん、この結合した文字列は '\0' で終端されますので、sprintf 関数の出力先に指定された配列は、sprintf 関数実行後に普通の文字列として扱うことが可能です。

スポンサーリンク

snprintf 関数での文字列結合

また、snprintf 関数の引数や返却値は下記のようになります。snprintf 関数も stdio.h で宣言されているため、利用時は stdio.h をインクルードしておく必要があります。

第1引数 char *str 出力先の配列(のアドレス)
第2引数 size_t size 出力データの最大サイズ('\0' を含む)
第3引数 const char *format 出力する文字列のフォーマット
第4引数以降 ... 可変個引数
返却値 int 出力された文字列の文字列長

snprintf 関数では第2引数 size が追加されており、snprintf 関数実行時には文字列長が size - 1 以下の文字列が出力されることになります。そして、その文字列の後ろ側に '\0' が出力されます。したがって、引数 size に “出力先の配列のサイズ” を指定してやればバッファーオーバーランも防ぐことが可能となります。

引数sizeの意味合いを示す図

残りの引数に関しては sprintf 関数と同じです。

つまり、snprintf 関数で2つの文字列の結合を結合したいのであれば、下記のように引数を指定してやれば良いことになります。そして、下記のように引数を指定することでバッファーオーバーランも防ぐことが出来ます。

文字列結合時のsnprintf関数の引数
  • 第1引数:結合後の文字列の出力先となる配列の配列名
  • 第2引数:第1引数で指定した配列のサイズ
  • 第3引数:"%s%s"
  • 第4引数:結合する文字列1
  • 第5引数:結合する文字列2

sprintfsnprintf 関数による文字列結合の例

ということで、次は sprintf 関数や snprintf 関数を利用した文字列結合のプログラムのソースコードの実例を示していきたいと思います。

sprintf 関数による文字列結合の例

まず、sprintf 関数を用いた文字列結合を行うプログラムのソースコードの例は下記となります。

sprintf関数を用いた文字列結合
#include <stdio.h> // printf/sprintf

#define STR_SIZE (256)

int main(void) {
    char str[STR_SIZE];
    char str1[] = "Hello ";
    char str2[] = "World!";

    sprintf(str, "%s%s", str1, str2);

    printf("%s + %s = %s\n", str1, str2, str);
}

コンパイルして実行すれば、下記のように str1str2 とを結合した結果が出力されることを確認できると思います。

Hello  + World! = Hello World!

snprintf 関数による文字列結合の例

続いて、snprintf 関数を用いた文字列結合を行うプログラムのソースコードの例は下記となります。

snprintf関数を用いた文字列結合
#include <stdio.h> // printf/snprintf

#define STR_SIZE (10)

int main(void) {
    char str[STR_SIZE];
    char *str1 = "Hello ";
    char str2[] = "World!";

    snprintf(str, STR_SIZE, "%s%s", str1, str2);

    printf("%s + %s = %s\n", str1, str2, str);
}

コンパイルして実行すれば、下記のように str1str2 の途中までを結合した結果が出力されることが確認できると思います。

Hello  + World! = Hello Wor

途中で途切れているのは第2引数 size に STR_SIZE (10) と指定しているからになります。この指定により第1引数に出力されるデータのサイズは必ず size 以下となります。今回は 10 を指定しているため、結合後の文字列長は 9 以下となり、結合後の文字列の最後に '\0' が追加されて全部で 10 文字のデータが出力されることになります。この例のように、第2引数 size を指定することでバッファーオーバーランを防ぐことが可能となります。そして、前述の通り、第2引数 size には基本的には出力先の配列のサイズを指定します。

3つ以上の文字列の結合も簡単に実現可能

既にお気づきの方も多いかと思いますが、sprintf 関数や snprintf 関数では2つだけでなく、3つ以上の文字列の結合も簡単に実現できます。sprintf 関数であれば、例えば第2引数に "%s%s%s%s" を指定し、第3引数以降に4つの文字列を指定すれば、4つの文字列の結合が可能となります。

下記は4つの文字列の結合を行うプログラムのソースコード例となります。

sprintf関数を用いた4つの文字列の結合
#include <stdio.h> // printf/sprintf

#define STR_SIZE (256)

int main(void) {
    char str[STR_SIZE];
    char str1[] = "Hello ";
    char str2[] = "World!";

    sprintf(str, "%s%s%s%s", str1, str2, "Good ", "Bye!");

    printf("%s\n", str);
}

こんな感じで、3つ以上の文字列の結合も行えますし、printf 関数と同様に整数等を文字列中に埋め込むことも可能です。色々応用が効くと思いますので、是非いろんな文字列結合にトライしてみていただければと思います!

スポンサーリンク

strcatstrncat 関数よりも使いやすい?

ここは主観も入るのですが、文字列の結合を行うのであれば strcat 関数や strncat 関数よりも sprintf 関数や snprintf 関数の方が楽に実現できるかなぁと思います。個人的には strcatstrncat 関数は正直使いにくい…。

まず、strcat 関数や strncat 関数では第1引数が入力と出力の両方の役割を担っている点が関数の使い方を複雑にしていると思います…。sprintf 関数や snprintf では出力が第1引数で、入力が他の引数と役割が分かれており、そのため使い方もシンプルだと思います。

strcat関数とsprintf関数との引数の入力出力の違いを示す図

また、strncat 関数も snprintf 関数も strcat 関数利用時の注意点 で説明した「バッファーオーバーラン」を防ぐ仕組みを持つ関数であり、その仕組みを利用するための引数が存在しますが、strncat 関数におけるその引数の指定の仕方は 第3引数 n の指定の仕方 で説明したとおり、少し複雑だと思います。snprintf の場合は、単に 第1引数の配列のサイズ を指定すればよいだけなのシンプルで使いやすいかと思います。

何より、皆さんが慣れ親しんでいる printf 関数と同様の使い方で文字列の結合が可能であるという点もポイントが高い。

こういった理由から、文字列の結合を行うのであれば、どちらかというと sprintf 関数 or snprintf 関数を利用するのが良いと思います。前述のとおり、3つ以上の文字列の結合も簡単に実現できますし。バッファーオーバーランを防ぐことが出来るという意味では snprintf が一番オススメです。いずれにせよ、文字列の結合結果の出力先となる配列のサイズは余裕をもって大きめに確保しておくことを忘れないようにしましょう!

まとめ

このページでは、C言語における文字列の結合について解説しました!

C言語では文字列の結合には基本的に標準ライブラリ関数を利用します。文字列結合を行うことを目的に用意されているのは strcatstrncat 関数で、これらを利用することで文字列の結合を実現できます。

特に文字列の結合を行う上で注意すべきはバッファーオーバーランであり、これは strncat 関数を利用し、引数を適切に指定することで防ぐことが出来ます。が、ちょっと引数の指定の仕方が複雑なので注意してください。

また、文字列結合を目的に用意されている関数ではないですが、sprintfsnprintf 関数を利用することでも文字列の結合は実現可能です。どちらかと言うと、これらの関数の方が使い方がシンプルなので、文字列結合を行うにはこれらの関数を利用することも検討してみるのが良いと思います!

同じカテゴリのページ一覧を表示

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です