このページでは、C言語での「文字列の結合・連結」を行う方法を解説します。
例えば2つの文字列 "Hello "
と "World"
が存在する場合、これらを結合すれば "Hello World"
という文字列が生成されることになります。このような文字列の結合・連結方法について説明していきます。
他のプログラミング言語では文字列同士を +
演算すれば文字列を結合できるような場合もあるのですが、C言語の場合は基本的に標準ライブラリ関数を利用して文字列の結合を行うことになります。
そして、C言語の標準ライブラリ関数において文字列の結合を目的に用意されている関数は strcat
と strncat
になります。文字列を結合するだけであれば strcat
を利用すればよいです。ですが、strcat
は使い方を誤ると重大な問題がプログラムに発生することになります。それを解決するのが strncat
で、これ利用することでより安全に文字列の結合を行うことが出来ます。
また、文字列結合を目的とした関数ではないのですが、応用すれば sprintf
と snprintf
関数を利用して文字列を結合することも可能です。これらの関数に関しては下記ページで解説していますので、詳しくは下記ページを参照していただければと思います。このページでは、これらの関数に関しては結合を行うための使い方についてのみ説明します。
ということで、このページでは strcat
や strncat
の解説及び、これらを利用した文字列の結合について重点的に解説し、その後、sprintf
と snprintf
を利用した文字列の結合について紹介していきたいと思います。
strcat
による文字列の結合
では、strcat
関数と、この strcat
関数を利用した文字列の結合について説明していきます。
strcat
関数
まずは strcat
関数の紹介から行っていきます。strcat
は、まさにこのページの目的となっている2つの文字列を結合する関数になります。
この strcat
関数は string.h
で宣言されており、使用する際は string.h
を include
しておく必要があります。
strcat
関数の引数と返却値
strcat
関数の引数と返却値はそれぞれ下記のようになります。
第1引数 | char *dest |
結合する文字列1 |
第2引数 | const char *src |
結合する文字列2 |
返却値 | char * |
結合後の文字列(dest ) |
第1引数と第2引数に指定した文字列が結合され、その文字列が返却値として得られるという引数・返却値の構成になっています。非常に単純そうに思えますが、実は 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
関数で文字列を結合した結果は、前述のとおり返却値から得ることも出来ますし、第1引数 dest
からも得ることができます。
strcat
関数利用時の注意点
ここまでが strcat
関数の動作に関する説明になります。続いて、strcat
関数利用時の注意点について説明していきます。
配列には結合後の文字列のサイズが必要となる
まず、strcat
関数利用時に注意が必要になるのが、第1引数 dest
を先頭アドレスとする配列には下記のサイズが必要になるという点になります(文字列長には '\0'
は含まれません)。+ 1
は文字列を終端する '\0'
の分のサイズとなります。
dest の文字列長 + src の文字列長 + 1
つまり、dest
を先頭アドレスとする配列のサイズは dest の文字列長 + 1
だけでは不十分ということになります。もし、dest
を先頭アドレスとする配列のサイズが dest の文字列長 + 1
のような、上記よりも小さなサイズの場合、下の図のようにバッファーオーバーランが発生することになります。
そして、バッファーオーバーランが発生すると意図せず他の関係ない配列や変数が変更される可能性もありますし、スタックが破壊されてプログラムが異常終了する可能性もあります。意図せず他の配列や変数が変更されてしまうことでプログラムに脆弱性が生まれることになる可能性もありますので、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
で、ここで各配列のサイズに注目すると dest
と src
ともに変数宣言時には配列のサイズを指定せずに右辺の文字列で初期化しているため、この 文字列長 + 1
が配列のサイズに自動的に設定されることになります。
char dest[] = "Hello World!";
char src[] = "Good bye!";
したがって、Hello World!
の文字列長 12
に +1
した 13
が配列 dest
のサイズとなります。初期化後の配列 dest
を図示すると下の図のようになります。
そして、前述のとおり、strcat
関数を実行することで引数 dest
の終端 '\0'
以降が引数 src
の文字列に上書きされることになります。つまり、下の図のように引数 src
の2文字目の o
以降は配列外に格納されることになります。ここで意図せず何かしらのデータを上書きしているため、結果的にスタックメモリが破壊されてしまい、上記のような警告メッセージが表示されることになります。
このように、引数 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
関数実行後には元々の文字列は残っていないため、それが実現できないことになります。
#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
関数がその変更不可なデータを変更しようとすることが理由となります。
変更不可なデータの代表例は文字列リテラルです。ソースコードに下記のように "
(ダブルクォーテーションマーク) で囲ったデータは文字列リテラルとして扱われます。基本的に文字列として扱うことが出来るのですが、変数の配列と違って変更は不可です。
文字列リテラルの詳細は下記ページで解説していますので興味があれば是非読んで見てください。
C言語での “文字列リテラル” の扱い文字列リテラルは変更不可であるため、文字列リテラルを直接 strcat
関数の第1引数 dest
に指定して実行するとプログラムが異常終了することになります。また、文字列リテラルを指すポインタを strcat
関数の第1引数 dest
に指定した場合も、結局はそのポインタ変数の指す文字列リテラルが変更されることになるため strcat
関数実行時にプログラムが異常終了することになります。
例えば下記は文字列リテラルを指すポインタを strcat
関数の第1引数 dest
に指定する例で、コンパイルして実行するとおそらくプログラムが異常終了して最後の printf
の出力が行われないと思います。
#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";
に変更しただけです。
#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
の指す文字列リテラルが変更されることになってプログラムが異常終了します。
それに対し、後者の場合は dest
は文字列リテラルをコピーしただけの単なる配列であり変更可能です。なので、strcat
関数の第1引数に指定すると配列が変更されることになるだけでプログラムは正常に終了することになります。
ここまでの説明のとおり、strcat
関数を上手く使いこなすためには第1引数で指定されるアドレスが変更可能か否かをしっかり意識してプログラミングすることが重要となります。
スポンサーリンク
strncat
による文字列の結合
続いて strncat
関数と、この strncat
関数を利用した文字列の結合について説明していきます。
strncat
関数
strncat
関数は strcat
関数と同様に2つの文字列を結合する関数になります。後述で解説するように、strncat
関数は strcat 関数利用時の注意点 で挙げたバッファーオーバーランを防止することが可能な関数であり、strcat
関数よりも安全に文字列の結合を行うことが可能です。
この strncat
関数においても string.h
で宣言されており、使用する際は string.h
を include
しておく必要があります。
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
の求め方については後述で解説します。
#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
文字となります。
もし 第2引数 src の文字列長 + 1
が n
よりも小さいのであれば、第2引数 src
の文字列全体と '\0'
が上書きされることになります。この場合は strcat
関数と同じ結果が得られることになります。
最大で 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
を指定することを検討するのが良いです。
size_t n;
long sn;
sn = destの配列サイズ - destの文字列長 - 1;
if (sn > 0) {
n = sn;
} else {
n = 0;
}
例えば、strncat 関数による文字列結合の例 で示したソースコードを上記のように 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
ということになります。
つまり、strncat
関数では第3引数に dest の配列サイズ - dest の文字列長 - 1
以下のサイズを指定することでバッファーオーバーランを確実に防いで安全な文字列の結合が実現可能となります。逆に、このサイズを超えた場合はバッファーオーバーランが発生する可能性があります。
もちろん、dest の配列サイズ - dest の文字列長 - 1
よりも小さなサイズであればバッファーオーバーランを防ぐことが出来るため、第3引数 n
にはもっと小さな値を指定しても問題ありません。ですが、第2引数 src
が極力途切れることのないように第1引数 dest
に結合したいのであれば第3引数 n
には出来るだけ大きいサイズを指定した方が良いです。ということで、バッファーオーバーランを確実に防ぐことのできるサイズの最大値を第3引数 n
に指定するのが望ましく、それが dest 配列のサイズ - dest の文字列長 - 1
ということになります。
そして、DEST_SIZE
を dest
の配列サイズとした場合は、この第3引数 n
は下記の式で求めることが可能です。strlen
は文字列長を求める関数で、要は strlen
関数の引数に指定したアドレスか '\0'
までの長さを求める関数になります。
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
が負の値になった場合は n
に 0
を代入して 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
を求めておいてやるのが良いと思います。
ちょっと説明が長くなってしまいましたが、単に文字列を結合するだけの関数ではあるのですが、strcat
関数や strncat
関数は安全に使おうと思うと注意すべき点が多く、関数の使い方の難易度は結構高いです。まずは、strncat
関数の方がバッファーオーバーランを防ぎやすく、第3引数 n
の指定の仕方や第1引数の配列のサイズは余裕をもって確保しておくことが重要であることは覚えておきましょう!
スポンサーリンク
sprintf
・snprintf
による文字列の結合
最後に sprintf
や snprintf
関数を利用した文字列結合について説明しておきます。これらの関数自体については別途下記ページで解説していますので、詳細は下記ページを参照していただければと思います。
printf
による文字列の結合
sprintf
とは、一言でいえば printf
の配列出力版になります。printf
では文字列が標準出力に出力されますが、sprintf
では文字列が配列に出力されることになります。
そして、 printf
関数の第1引数に指定するフォーマットにおいて、%s
は第2引数以降で指定したアドレスのデータを文字列として出力する変換指定子となります。そのため、下記のように printf
関数を実行すれば "%s%s"
によって1つの文字列の直後に他の文字列を結合した結果が標準出力に出力されることになります。具体的には、下記を実行すれば標準出力に "Hello "
と "World"
を結合した結果の "Hello Word"
が出力されることになります。
printf("%s%s", "Hello ", "Word");
つまり、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引数に指定した配列に出力されます。
そして、前述のとおり printf
関数では第1引数に "%s%s"
を指定し、第2引数以降に結合したい文字列を2つ指定することで文字列の結合が可能です。これと同様に、sprintf
関数では第2引数に "%s%s"
を指定し、第3引数以降に結合したい文字列を2つ指定することで文字列の結合を行うことができ、さらに第1引数に指定した配列に結合結果が出力されます。つまり、これだけで文字列の結合が実現できることになります。
まとめると、sprintf
関数を下記のようにして実行することで2つの文字列の結合を実現できます。
- 第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
に “出力先の配列のサイズ” を指定してやればバッファーオーバーランも防ぐことが可能となります。
残りの引数に関しては sprintf
関数と同じです。
つまり、snprintf
関数で2つの文字列の結合を結合したいのであれば、下記のように引数を指定してやれば良いことになります。そして、下記のように引数を指定することでバッファーオーバーランも防ぐことが出来ます。
- 第1引数:結合後の文字列の出力先となる配列の配列名
- 第2引数:第1引数で指定した配列のサイズ
- 第3引数:
"%s%s"
- 第4引数:結合する文字列1
- 第5引数:結合する文字列2
sprintf
・snprintf
関数による文字列結合の例
ということで、次は sprintf
関数や snprintf
関数を利用した文字列結合のプログラムのソースコードの実例を示していきたいと思います。
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);
}
コンパイルして実行すれば、下記のように str1
と str2
とを結合した結果が出力されることを確認できると思います。
Hello + World! = Hello World!
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);
}
コンパイルして実行すれば、下記のように str1
と str2
の途中までを結合した結果が出力されることが確認できると思います。
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つの文字列の結合を行うプログラムのソースコード例となります。
#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
関数と同様に整数等を文字列中に埋め込むことも可能です。色々応用が効くと思いますので、是非いろんな文字列結合にトライしてみていただければと思います!
スポンサーリンク
strcat
や strncat
関数よりも使いやすい?
ここは主観も入るのですが、文字列の結合を行うのであれば strcat
関数や strncat
関数よりも sprintf
関数や snprintf
関数の方が楽に実現できるかなぁと思います。個人的には strcat
・strncat
関数は正直使いにくい…。
まず、strcat
関数や strncat
関数では第1引数が入力と出力の両方の役割を担っている点が関数の使い方を複雑にしていると思います…。sprintf
関数や snprintf
では出力が第1引数で、入力が他の引数と役割が分かれており、そのため使い方もシンプルだと思います。
また、strncat
関数も snprintf
関数も strcat 関数利用時の注意点 で説明した「バッファーオーバーラン」を防ぐ仕組みを持つ関数であり、その仕組みを利用するための引数が存在しますが、strncat
関数におけるその引数の指定の仕方は 第3引数 n の指定の仕方 で説明したとおり、少し複雑だと思います。snprintf
の場合は、単に 第1引数の配列のサイズ
を指定すればよいだけなのシンプルで使いやすいかと思います。
何より、皆さんが慣れ親しんでいる printf
関数と同様の使い方で文字列の結合が可能であるという点もポイントが高い。
こういった理由から、文字列の結合を行うのであれば、どちらかというと sprintf
関数 or snprintf
関数を利用するのが良いと思います。前述のとおり、3つ以上の文字列の結合も簡単に実現できますし。バッファーオーバーランを防ぐことが出来るという意味では snprintf
が一番オススメです。いずれにせよ、文字列の結合結果の出力先となる配列のサイズは余裕をもって大きめに確保しておくことを忘れないようにしましょう!
まとめ
このページでは、C言語における文字列の結合について解説しました!
C言語では文字列の結合には基本的に標準ライブラリ関数を利用します。文字列結合を行うことを目的に用意されているのは strcat
・strncat
関数で、これらを利用することで文字列の結合を実現できます。
特に文字列の結合を行う上で注意すべきはバッファーオーバーランであり、これは strncat
関数を利用し、引数を適切に指定することで防ぐことが出来ます。が、ちょっと引数の指定の仕方が複雑なので注意してください。
また、文字列結合を目的に用意されている関数ではないですが、sprintf
・snprintf
関数を利用することでも文字列の結合は実現可能です。どちらかと言うと、これらの関数の方が使い方がシンプルなので、文字列結合を行うにはこれらの関数を利用することも検討してみるのが良いと思います!