このページでは、sprintf
関数および snprintf
関数について解説していきます。
printf
関数はみなさんもうご存知ですよね!sprintf
関数や snprintf
関数は、一言でいえば printf
関数の「メモリ出力バージョン」になります。
printf
関数では指定した文字列が標準出力に出力されますが、sprintf
関数では文字列が配列等のメモリに出力されることになります。
メモリに出力された文字列は通常の文字列同様に扱うことができますので、sprintf
関数や snprintf
関数は「文字列を生成する関数」であると考えることもできます。
また、printf
関数では %c
や %d
などのフォーマット指定子を利用することで変数等の値を文字列に組み込むようなことができましたが、sprintf
や snprintf
関数でも同様のことが可能です。
これを利用して、文字列の生成や文字列の結合などを簡単に行うことができます。とにかく文字列を扱う際には便利な関数ですので、是非このページで sprintf
関数や snprintf
関数の使い方をマスターしていってください!
Contents
sprintf
関数
まずは、sprintf
関数について解説していきます!
sprintf
関数は printf
関数と似ている
お馴染みの printf
関数と対比しながら考えると分かりやすいと思いますので、printf
関数と対比しながら sprintf
関数について解説していきたいと思います。
まず、printf
関数は下記のような関数になります。
#include <stdio.h>
int printf(const char *format, ...);
各引数の意味合いや返却値は下記のようになります。
- 第1引数:文字列の出力フォーマット
- 第2引数以降:出力フォーマット内のフォーマット指定子に表示する変数や値
- フォーマット指定子の数だけ指定する
- 返却値:出力した文字数(バイト数)
printf
関数は、第1引数で指定された出力フォーマットに第2引数以降で指定された変数や値を組み込んだ文字列を標準出力に出力します(例えばターミナルなど)。
それに対し、sprintf
関数は下記のような関数になります。
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
各引数の意味合いや返却値は下記のようになります。
- 第1引数:文字列の出力先のアドレス
- 第2引数:文字列の出力フォーマット
- 第3引数以降:出力フォーマット内のフォーマット指定子に表示する変数や値
- フォーマット指定子の数だけ指定する
- 返却値:出力した文字数(バイト数)
引数としては、printf
関数に対して引数が1つ増えただけで、文字列の出力先のアドレスが第1引数に必要になります。他は printf
の引数と同様です。
そして、sprintf
関数は、第2引数で指定された出力フォーマットに第3引数以降で指定された変数や値を組み込んだ文字列を第1引数に指定されたアドレスに出力します。出力されるのが文字列ですので、最後には必ずヌル文字('\0'
)が付加されます。
要は、sprintf
関数と printf
関数の動作の違いは「出力先」だけということになります。
sprintf
関数:第1引数に指定されたアドレスに出力printf
関数:標準出力に出力
もしかしたら内部の詳細な動作としてはもっと違いがあるかもしれませんが、使用者側からしたら違いは出力先のみだと思います。
ただ、出力先の違いがあるために、sprintf
では printf
使用時には気にしなくて良かったことを追加で考慮しながら実装していく必要があります。
この辺りは sprintf 関数の注意点 で解説していきます。
スポンサーリンク
sprintf
関数の使用例
C言語での文字列の扱いは正直難しいですが、sprintf
を使うことで文字列の生成や連結などを比較的簡単に行うことが可能です。特に、printf
関数の時と同様の感覚で文字列を扱うことができる点が、この sprintf
関数の良いところだと思います。
次は、文字列の生成や結合を行う際の sprintf
関数の使用例を紹介していきたいと思います。
文字列の生成を行う例
例えば、下記のように printf
を実行すれば、x=7,y=1.6,z=Hi!
が標準出力に出力されることになりますが、
#include <stdio.h>
int main(void) {
int x = 7;
double y = 1.57;
char z[] = "Hi!";
printf("x=%d,y=%.1f,z=%s", x, y, z);
return 0;
}
下記のように sprintf
を実行すれば、x=7,y=1.6,z=Hi!
を配列に格納し、通常の文字列として利用することができるようになります。
#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[256];
int x = 7;
double y = 1.57;
char z[] = "Hi!";
sprintf(buffer, "x=%d,y=%.1f,z=%s", x, y, z);
size_t len = strlen(buffer);
printf("%s(len = %zu)\n", buffer, len);
return 0;
}
printf
関数と異なり、sprintf
では第1引数に出力先のアドレスを指定する必要がありますが、他に関しては printf
関数と同じように利用できているところが確認できると思います。
この出力先のアドレスとしては、上記の例のように事前に配列(上記の例だと buffer
)を用意し、その配列名を指定することが多いです。buffer
のように 配列名
だけを指定した場合、その配列の先頭アドレスとして扱われます。
上記の例では、配列 buffer
の先頭アドレスを sprintf
関数の第1引数に指定しているため、配列 buffer
の先頭から順番に1文字ずつ sprintf
の出力結果が格納されていくことになります。この時、出力結果の最後には文字列の終端を示すヌル文字 '\0'
が付加されます。
そのため、sprintf
関数実行後は、sprintf
関数の第1引数で指定したアドレスのデータを文字列として扱うことができます。
実際に上記の例においても、buffer
を strlen
関数の引数に指定したり、printf
関数に指定するフォーマット指定子 %s
に組み込んだりできてますよね(strlen
は引数に指定された文字列の長さを返却する関数です)。
文字列の結合を行う例
また、この sprintf
関数を利用すれば、文字列の結合も簡単に行えます。
例えば下記のように sprintf
関数を実行することで、
char buffer[256];
char str1[] = "abc";
char str2[] = "hij";
char str3[] = "xyz";
sprintf(buffer, "%s%s%s", str1, str2, str3);
printf("%s\n", buffer);
文字列 "abc"
と "hij"
と "xyz"
の文字列を結合した結果を文字列 buffer
として得ることができます。最後の printf
関数の実行結果は下記のようになります。
abchijxyz
また、下記のように sprintf
関数を実行することで、
char buffer[256];
char str1[] = "abc";
char str2[] = "hij";
char str3[] = "xyz";
sprintf(buffer, "%s/%s/%s", str1, str2, str3);
printf("%s\n", buffer);
文字列 "abc"
と "hij"
と "xyz"
の文字列を '/'
区切りで結合した結果を buffer
として得ることができます。最後の printf
関数の実行結果は下記のようになります。
abc/hij/xyz
こんな感じで、sprintf
関数を利用すれば、単に文字列を結合することも、途中で他の文字や文字列を挿入しながら結合するようなことも簡単に実現することができます。
sprintf
関数使用時の注意点
ここまで解説してきたように、sprintf
関数は printf
関数と同様の感覚で使用でき、そのため文字列の結合等も簡単に行うことができて便利です。
ただ、printf
関数には無い sprintf
関数ならではの注意点もありますので、その辺りについても解説しておきます。
文字列出力先のメモリが必要
まず、sprintf
関数では、文字列の出力先となるメモリが必要です。
printf
関数では文字列は標準出力に出力されますが、sprintf
関数ではメモリに出力されます。そのため、sprintf
関数を実行する前には、あらかじめ文字列の出力先となるメモリを用意しておく必要があります。そしてそのメモリのアドレスを、sprintf
関数の第1引数に指定します。
メモリやアドレスと聞くと難しそうに思う方もおられるかもしれませんが、ここまで紹介してきたソースコードのように char
型の配列を変数宣言しておけば、その配列を文字列の出力先となるメモリとして扱うことができます。
そして、その配列名を sprintf
関数の第1引数に指定すれば、sprintf
関数実行時に、その配列の先頭から文字列が出力されていくことになります。
また、配列にこだわる必要もなく、malloc
関数で事前にメモリを確保しておき、そのメモリを文字列の出力先として利用するのでも良いです。
char *dst = malloc(sizeof(char) * 256);
if (dst == NULL) {
return 0;
}
sprintf(dst, "%s/%s/%s", "abc", "efg", "hij");
printf("%s\n", dst);
free(dst);
malloc
関数を利用するためには stdlib.h
をインクルードしておく必要があります。malloc
関数の詳細は下記ページで解説していますので、詳しくは下記ページをご参照いただければと思います。
バッファオーバーランに注意!
sprintf
関数を利用するにあたって一番注意しなければならないのがバッファオーバーランです。
バッファオーバーランとは、あらかじめ用意していたバッファ(メモリ)の領域を超えてデータの書き込みを行なってしまい、他の変数等の値を変更してしまうようなことを言います。
これによって開発するプログラムに脆弱性が生まれる可能性があるので注意してください。
バッファオーバーランが発生しないよう、sprintf
関数の出力先とするメモリは、sprintf
関数が出力する文字列のサイズよりも大きくする必要があります。
例えば下記は、バッファオーバーランが発生する可能性のある sprintf
関数の使用例となります。私の環境だとバッファオーバーランがバッチリ発生します(環境によって正常にプログラムが動作する場合もあると思います)。
char str1[] = "abcdefghij";
char str2[] = "klmnopq";
char data[] = "0123456789";
char buffer[10];
char *p = buffer;
sprintf(p, "%s%s", str1, str2);
printf("buffer = %s\n", buffer);
printf("data = %s\n", data);
私の環境で実行すると、printf
関数での表示結果は下記のようになります。
buffer = abcdefghijklmnopq data = klmnopq
一番のポイントは、data
はソースコード上一切変更していないのにも関わらず、いつの間にか data
の中身が変わってしまっているという点です。元々 data
は "0123456789"
という文字列だったのに、printf
関数実行時には "klmnopq"
に変わってしまっています。
どういうことが起きているかを私の実行結果に基づいて解説すると、まず配列 buffer
と配列 data
は変数宣言によってメモリ上に配置されますが、私の環境で上記のソースコードを実行すると、ちょうど buffer
の後ろ側に data
が配置されるようになっていました(アドレスを表示すればどう配置されているかが確認できる)。
さらに、sprintf
関数を実行すれば、配列 buffer
の先頭から str1
と str2
を結合した文字列が出力されていくことになります(第1引数に指定している p
は配列 buffer
の先頭アドレスを指しています)。
ただ、str1
と str2
の結合結果が配列 buffer
のサイズである 10
を超えているため、配列 buffer
の領域を超えて文字列が出力されていくことになります。そして、配列 buffer
の後ろ側には配列 data
が配置されているため、配列 data
にまでまたがって文字列が出力されてしまうことになります。
つまり、配列 data
のデータが sprintf
関数によって上書きされています。なので、配列 data
の printf
結果が、sprintf
関数によって上書きされた結果である "klmnopq"
になってしまったというわけです。
また、文字列の終端はヌル文字 '\0'
で判断されるため、printf("buffer = %s\n", buffer)
実行時にも配列 buffer
を超え、次に見つかるヌル文字 '\0'
の前の文字まで出力されることになります。
例えば、配列 data
にパスワードなどが格納されていた場合、そのパスワードが書き換えられてしまうことになります。ログインシステムのプログラムであれば、不正ログインが行われる可能性もありますよね…。こういった重要なデータを書き換えてしまう(書き換えられてしまう)可能性がありますので、このバッファオーバーランには十分気をつける必要があります。
バッファオーバーランを防ぐための1つの方法は、snprintf
関数のような、出力する最大文字数を制御可能な関数を利用することです。これに関しては、後述の snprintf 関数 で紹介します。
もう1つの方法は、sprintf
の文字列出力先となるメモリを十分大きく確保しておくことです。
ヌル文字 '\0'
が付加されることを考慮する必要あり
では、先程の例(下記に再掲します)において、配列 buffer
のサイズ(要素数)は最低いくつにする必要があるでしょうか?
char str1[] = "abcdefghij";
char str2[] = "klmnopq";
char data[] = "0123456789";
char buffer[10];
char *p = buffer;
sprintf(p, "%s%s", str1, str2);
printf("buffer = %s\n", buffer);
printf("data = %s\n", data);
str1
が "abcdefghij"
の 10
文字で、str2
が "klmnopq"
の 7
文字だから 17
と考えると、残念ながらまだ sprintf
関数の文字列出力時に配列 buffer
の領域を超えてしまいます。
sprintf
関数では、文字列を出力する際に最後にヌル文字 '\0'
が付加されます。なので、そのヌル文字が付加されることを考慮して、配列やメモリのサイズを設定する必要があります。
そのため、上記の例の場合、buffer
のサイズとしては 17 + 1
文字分が必要ということになります。
最後にヌル文字 '\0'
が付加されることを忘れてサイズ設定してしまうとバッファオーバーラン等が発生する可能性もありますので、この点は注意しながら実装していくようにしましょう。この点に関しては、文字列を扱う他の関数においても同様の注意点となります。
また、上記では str1
や str2
の文字列が固定なので簡単に必要なサイズを計算することができますが、sprintf
に指定する文字列の長さが固定でないような場合は、特に必要になるサイズの計算が難しくなるので注意してください。
特にメモリ制限が厳しいような環境でなければ、とりあえず十分大きなサイズの配列を変数宣言しておくのが無難だと思います。
インクルードするのは stdio.h
最後の注意点は、sprintf
を使用する際にインクルードする必要があるヘッダーファイルが stdio.h
である点です。
文字列を扱う関数なので、string.h
をインクルードする必要がありそうな気もしますが、sprintf
関数に関してはインクルードが必要なのは stdio.h
です。
おそらくほとんどの場合 stdio.h
をインクルードすると思いますので、そこまで気にしなくてもいい注意点かもしれませんが、一応記載しておきました。
snprintf
関数
続いて、snprintf
関数について解説していきます。
sprintf 関数使用時の注意点 で、sprintf
関数ではバッファオーバーランに注意が必要と述べましたが、そのバッファオーバーランは sprintf
関数ではなく snprintf
関数を利用することで解決しやすくなります(snprintf
関数は C99 以降で規格化された関数)。
snprintf
と sprintf
は関数名が非常に似ていて違いが分かりにくいですが、snprintf
の場合は printf
の前に sn
が付いています(sprintf
の場合は s
のみ)。 混同しないように注意しましょう!
スポンサーリンク
snprintf
関数と sprintf
関数の違い
snprintf
関数は、sprintf
関数とほぼ動作は同じですが、sprintf
関数と異なり、出力する文字列の最大文字数を制御することが可能です。
その制御が行えるよう、snprintf
関数では sprintf
関数に比べ1つ引数が追加されています。
#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
snprintf
関数では、最大 size - 1
文字分のみの文字の出力を行います(size
は第2引数で指定される)。出力しようとしている文字の文字数が size
以上の場合、先頭から size - 1
文字のみが出力され、それより後ろ側の文字は無視されます。
さらに snprintf
関数は、出力した文字の後ろ側にヌル文字を付加します。つまり、snprintf
関数は、ヌル文字を含めて最大 size
文字分の文字しか出力しません。
このように、snprintf
関数では引数によって出力最大文字数を制御することができます。したがって、引数 size
に文字列の出力先となるメモリのサイズ、もしくはそれよりも小さいサイズを指定することで、バッファオーバーランを防ぐことができます。
例えば文字列の出力先となるメモリとして char buffer[BUF_SIZE]
を利用するのであれば、引数 size
に BUF_SIZE
を指定すれば、snprintf
関数が出力する文字列の最大文字数は「BUF_SIZE - 1
文字 + ヌル文字の 1
文字」となり、buffer
のサイズ内に出力された文字列が収まることになります。
snprintf
関数の使用例
バッファオーバーランに注意! でバッファオーバーランの発生する可能性のあるソースコードを示しましたが、sprintf
関数実行箇所を snprintf
関数に置き換えるだけで、バッファオーバーランを防ぐことができるようになります。
その例が下記となります。
char str1[] = "abcdefghij";
char str2[] = "klmnopq";
char data[] = "0123456789";
char buffer[10];
char *p = buffer;
snprintf(p, 10, "%s%s", str1, str2);
printf("buffer = %s\n", buffer);
printf("data = %s\n", data);
実行結果は下記の通りになります。
buffer = abcdefghi data = 0123456789
snprintf
関数の第2引数には 10
を指定していますので、snprintf
関数は最大で 9
文字 + ヌル文字しか出力しません。その結果、buffer
の printf
結果が 9
文字のみとなっていることが確認できると思います。
このため、もし配列 buffer
の後ろ側に変数等があったとしても、snprintf
関数でバッファオーバーランが発生してその変数が上書きされるようなことは起こりません。バッファオーバーランに注意! で紹介した結果だと data
が他の文字列に上書きされていましたが、上記の結果では元のままであることも確認できると思います。
snprintf
関数使用時の注意点
最後に、snprintf
関数使用時の注意点について解説しておきます。
必ずバッファオーバーランが防げるとは限らない
まず、snprintf
関数でバッファオーバーランが防げるのは、第2引数の指定が適切である場合のみです。snprintf
関数さえ使えばバッファオーバーランが防げるというわけではないので注意してください。
例えば snprintf
関数の出力先となるメモリのサイズよりも大きな値を第2引数に指定するとバッファオーバーランが起こり得ます。
防げるとしてもプログラムが正常に動作するとは限らない
また、snprintf
関数でバッファオーバーランを防げたとしても、それでプログラムが意図した通りに動作するとは限らないので注意してください。
snprintf
関数ではバッファオーバーランを防ぐために文字列の出力を途中でやめるため、取得した文字列が途切れた状態になってしまうことがあります。その状態の文字列で意図した通りの動作ができるかどうかは設計や仕様等によると思います。
なので、結局プログラムが意図した通りに動作できるよう、snprintf
関数の出力先となるメモリは十分な大きさで用意しておく必要があります。
まぁただ、バッファオーバーランは脆弱性につながる致命的なバグですので、文字列が途切れてしまうとしても、sprintf
よりも snprintf
を利用する方が安全であることは間違い無いと思います(もちろん第2引数はしっかり適切な値を指定する必要があります)。
スポンサーリンク
まとめ
このページでは、sprintf
関数および snprintf
関数について解説を行いました!
sprintf
関数は printf
関数の「メモリ出力バージョン」です。printf
関数では標準出力に文字列が出力されますが、sprintf
関数では配列等の指定したアドレスのメモリに出力できるため、sprintf
関数で文字列を生成し、それを後からプログラム内で利用することができて便利です。
また、sprintf
関数の使い方は printf
関数と非常に似ているため、使い慣れた printf
関数と同様の感覚でお手軽に文字列の生成や文字列の結合ができるメリットがあります。
ただ、sprintf
関数はバッファオーバーランが起きやすい関数でもあるので注意してください。snprintf
関数を利用することで、このバッファオーバーランを防ぐことができます(ただし第2引数には適切な値を指定する必要があります)。
文字列を生成したり文字列を結合したりする際にかなり便利な関数ですので、ぜひ sprintf
関数と snprintf
関数の使い方はマスターしておきましょう!