【C言語】sprintf 関数と snprintf 関数(お手軽に文字列を生成する関数)

sprintf関数とsnprintf関数の解説ページアイキャッチ

このページでは、sprintf 関数および snprintf 関数について解説していきます。

printf 関数はみなさんもうご存知ですよね!sprintf 関数や snprintf 関数は、一言でいえば printf 関数の「メモリ出力バージョン」になります。

printf 関数では指定した文字列が標準出力に出力されますが、sprintf 関数では文字列が配列等のメモリに出力されることになります。

printfとsprintfの違い

メモリに出力された文字列は通常の文字列同様に扱うことができますので、sprintf 関数や snprintf 関数は「文字列を生成する関数」であると考えることもできます。

また、printf 関数では %c%d などのフォーマット指定子を利用することで変数等の値を文字列に組み込むようなことができましたが、sprintfsnprintf 関数でも同様のことが可能です。

これを利用して、文字列の生成や文字列の結合などを簡単に行うことができます。とにかく文字列を扱う際には便利な関数ですので、是非このページで sprintf 関数や snprintf 関数の使い方をマスターしていってください!

sprintf 関数

まずは、sprintf 関数について解説していきます!

sprintf 関数は printf 関数と似ている

お馴染みの printf 関数と対比しながら考えると分かりやすいと思いますので、printf 関数と対比しながら sprintf 関数について解説していきたいと思います。

まず、printf 関数は下記のような関数になります。

printf関数

#include <stdio.h>

int printf(const char *format, ...);

各引数の意味合いや返却値は下記のようになります。

  • 第1引数:文字列の出力フォーマット
  • 第2引数以降:出力フォーマット内のフォーマット指定子に表示する変数や値
    • フォーマット指定子の数だけ指定する
  • 返却値:出力した文字数(バイト数)

printf 関数は、第1引数で指定された出力フォーマットに第2引数以降で指定された変数や値を組み込んだ文字列を標準出力に出力します(例えばターミナルなど)。

printf関数の説明図

それに対し、sprintf 関数は下記のような関数になります。

sprintf関数

#include <stdio.h>

int sprintf(char *str, const char *format, ...);

各引数の意味合いや返却値は下記のようになります。

  • 第1引数:文字列の出力先のアドレス
  • 第2引数:文字列の出力フォーマット
  • 第3引数以降:出力フォーマット内のフォーマット指定子に表示する変数や値
    • フォーマット指定子の数だけ指定する
  • 返却値:出力した文字数(バイト数)

引数としては、printf 関数に対して引数が1つ増えただけで、文字列の出力先のアドレスが第1引数に必要になります。他は printf の引数と同様です。

そして、sprintf 関数は、第2引数で指定された出力フォーマットに第3引数以降で指定された変数や値を組み込んだ文字列を第1引数に指定されたアドレスに出力します。出力されるのが文字列ですので、最後には必ずヌル文字('\0')が付加されます。

sprintf関数の説明図

要は、sprintf 関数と printf 関数の動作の違いは「出力先」だけということになります。

  • sprintf 関数:第1引数に指定されたアドレスに出力
  • printf 関数:標準出力に出力

もしかしたら内部の詳細な動作としてはもっと違いがあるかもしれませんが、使用者側からしたら違いは出力先のみだと思います。

ただ、出力先の違いがあるために、sprintf では printf 使用時には気にしなくて良かったことを追加で考慮しながら実装していく必要があります。

この辺りは sprintf 関数の注意点 で解説していきます。

スポンサーリンク

sprintf 関数の使用例

C言語での文字列の扱いは正直難しいですが、sprintf を使うことで文字列の生成や連結などを比較的簡単に行うことが可能です。特に、printf 関数の時と同様の感覚で文字列を扱うことができる点が、この sprintf 関数の良いところだと思います。

次は、文字列の生成や結合を行う際の sprintf 関数の使用例を紹介していきたいと思います。

文字列の生成を行う例

例えば、下記のように printf を実行すれば、x=7,y=1.6,z=Hi! が標準出力に出力されることになりますが、

printfの使用例

#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! を配列に格納し、通常の文字列として利用することができるようになります。

sprintfの使用例

#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 関数実行後は、sprintf 関数の第1引数で指定したアドレスのデータを文字列として扱うことができます。

実際に上記の例においても、bufferstrlen 関数の引数に指定したり、printf 関数に指定するフォーマット指定子 %s に組み込んだりできてますよね(strlen は引数に指定された文字列の長さを返却する関数です)。

文字列の結合を行う例

また、この sprintf 関数を利用すれば、文字列の結合も簡単に行えます。

例えば下記のように sprintf 関数を実行することで、

sprintf関数での文字列結合1

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 関数を実行することで、

sprintf関数での文字列結合2

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 関数の詳細は下記ページで解説していますので、詳しくは下記ページをご参照いただければと思います。

malloc解説ページのアイキャッチ【C言語】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 が配置されるようになっていました(アドレスを表示すればどう配置されているかが確認できる)。

bufferとdataの配置位置

さらに、sprintf 関数を実行すれば、配列 buffer の先頭から str1str2 を結合した文字列が出力されていくことになります(第1引数に指定している p は配列 buffer の先頭アドレスを指しています)。

ただ、str1str2 の結合結果が配列 buffer のサイズである 10 を超えているため、配列 bufferの領域を超えて文字列が出力されていくことになります。そして、配列 buffer の後ろ側には配列 data が配置されているため、配列 data にまでまたがって文字列が出力されてしまうことになります。

バッファオーバーランが発生する様子

つまり、配列 data のデータが sprintf 関数によって上書きされています。なので、配列 dataprintf 結果が、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' が付加されることを忘れてサイズ設定してしまうとバッファオーバーラン等が発生する可能性もありますので、この点は注意しながら実装していくようにしましょう。この点に関しては、文字列を扱う他の関数においても同様の注意点となります。

また、上記では str1str2 の文字列が固定なので簡単に必要なサイズを計算することができますが、sprintf に指定する文字列の長さが固定でないような場合は、特に必要になるサイズの計算が難しくなるので注意してください。

特にメモリ制限が厳しいような環境でなければ、とりあえず十分大きなサイズの配列を変数宣言しておくのが無難だと思います。

インクルードするのは stdio.h

最後の注意点は、sprintf を使用する際にインクルードする必要があるヘッダーファイルが stdio.h である点です。

文字列を扱う関数なので、string.h をインクルードする必要がありそうな気もしますが、sprintf 関数に関してはインクルードが必要なのは stdio.h です。

おそらくほとんどの場合 stdio.h をインクルードすると思いますので、そこまで気にしなくてもいい注意点かもしれませんが、一応記載しておきました。

snprintf 関数

続いて、snprintf 関数について解説していきます。

sprintf 関数使用時の注意点 で、sprintf 関数ではバッファオーバーランに注意が必要と述べましたが、そのバッファオーバーランは sprintf 関数ではなく snprintf 関数を利用することで解決しやすくなります(snprintf 関数は C99 以降で規格化された関数)。

snprintfsprintf は関数名が非常に似ていて違いが分かりにくいですが、snprintf の場合は printf の前に sn が付いています(sprintf の場合は s のみ)。 混同しないように注意しましょう!

スポンサーリンク

snprintf 関数と sprintf 関数の違い

snprintf 関数は、sprintf 関数とほぼ動作は同じですが、sprintf 関数と異なり、出力する文字列の最大文字数を制御することが可能です。

その制御が行えるよう、snprintf 関数では sprintf 関数に比べ1つ引数が追加されています。

snprintf関数

#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 に文字列の出力先となるメモリのサイズ、もしくはそれよりも小さいサイズを指定することで、バッファオーバーランを防ぐことができます。

snprintf関数の動作

例えば文字列の出力先となるメモリとして char buffer[BUF_SIZE] を利用するのであれば、引数 sizeBUF_SIZE を指定すれば、snprintf 関数が出力する文字列の最大文字数は「BUF_SIZE - 1 文字 + ヌル文字の 1 文字」となり、buffer のサイズ内に出力された文字列が収まることになります。

snprintf 関数の使用例

バッファオーバーランに注意! でバッファオーバーランの発生する可能性のあるソースコードを示しましたが、sprintf 関数実行箇所を snprintf 関数に置き換えるだけで、バッファオーバーランを防ぐことができるようになります。

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

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 文字 + ヌル文字しか出力しません。その結果、bufferprintf 結果が 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 関数の使い方はマスターしておきましょう!

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