こんにちは、ナナです。
C言語は、文字の扱いが苦手な言語です。そのため、文字列を扱うプログラムを作るときは初心者の方ほど慎重に行うことが必要です。
そんなC言語には、標準ライブラリ関数として「sprintf関数」というものがあります。
「sprintf関数」は非常に優秀な関数です。使い方をマスターすれば「こんな形の文字列を作りたい!」という願いを叶えてくれます。
例えば、次のように様々な変数に格納された情報を組み合わせて、文字列を作り出すことができます。
「sprintf関数」の存在を知らないがゆえに、時間を掛けて複雑なプログラムを作り、結果バグを生み出すということは非常にもったいないことです。
本記事では次の悩みを解消する内容となっています。
それでは、「sprintf関数」の使い方を学びましょう。
sprintf系の標準ライブラリ関数を紹介
sprintf関数には、セキュア版も含めて2種類の関数が存在します。
#include <stdio.h>
int sprintf(char * buf, const char * format, ...);
int sprintf_s(char * buf, size_t size, const char * format, ...);
sprintf関数は「s(string:文字列)」「print(出力する)」「f(format:書式)」の3つの要素から構成されている「書式指定可能な文字列出力関数」です。
第3引数以降は「可変個引数」と呼ばれるいくつでも引数を追加できる構成になっています。
「sprintf_s関数」はセキュア版となっており、安全性が高められています。
sprintf関数の仕様
sprintf関数は、第3引数以降で指定された情報を第2引数のフォーマットに従い文字列へ変換し、第1引数で指定したメモリ領域にコピーしてくれます。
includeファイル | stdio.h |
関数仕様 | int sprintf(char * buf, const char * format, …); |
引数1 | 生成した文字列をコピーするメモリへのポインタ |
引数2 | 生成したい文字列の出力フォーマット |
引数3以降 | 出力フォーマットで指定された変数を並べる |
戻り値 | 生成した文字列の文字数(ヌル文字を除く) |
特記事項 | 第3引数以降は出力フォーマットと変数の型と数を合わせる必要がある。 |
出力フォーマットで指定できる、基本的な変換指定子は次のものです。
変換指定子 | 変数のデータ型 | 説明 |
---|---|---|
%c | char | 1文字のアスキーコードを出力する |
%s | char * | 文字列を出力する |
%d | 整数型 | 10進数で出力する |
%x | 整数型 | 16進数(英小文字)で出力する |
%X | 整数型 | 16進数(英大文字)で出力する |
%f | 浮動小数点型 | 浮動小数点を出力する |
%p | ポインタ型 | 番地を16進数(英大文字)で出力する |
出力したい形式で変換指定子を選んで、フォーマット文字列の中に埋め込みます。使い方は次に解説しましょう!
sprintf関数のプログラム使用例
まずは「sprintf関数」から利用してみましょう。ただし、最初に伝えておかなければならないことがあります。
Visual Studio 2017 環境では、ノンセキュア版である「sprintf関数」の使用は推奨されておらず、使用すると、次のようなエラーが発生します。
error C4996: ‘sprintf’: This function or variable may be unsafe. Consider using sprintf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
sprintf関数は安全ではないためsprintf_s関数を使うように促しています。ただし、「
_CRT_SECURE_NO_WARNINGS」を定義することで使用することができます。
このエラーを回避するためにはマクロ定義を利用して「_CRT_SECURE_NO_WARNINGS」を定義することで使用可能になります。
ただし、次のようにstdio.hのインクルードよりも手前でマクロ定義をする必要があることに注意しましょう。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char moji[32];
char data1[] = "モノづくり";
char data2 = 'C';
int data3 = 2020;
// moji配列に指定フォーマットで文字列を生成
sprintf(moji, "%s %c 言語塾 %d年", data1, data2, data3);
// 生成した文字を画面に表示
printf("%s", moji);
return 0;
}
モノづくり C 言語塾 2020年
このように「sprintf関数」は、変数として管理している「文字列」「文字」「数」といった情報を、目的のフォーマットに合わせて文字列を作り出すことができます。
変数に入れておいた情報を、フォーマットの指定で組み合わせて文字列を作ることができるのが「sprintf関数」です。
実際に使ってみると、すごく便利なんですよ!
csv形式の文字列データ作成
カンマ区切りの文字情報の並びを「CSV(Comma Separated Value)」と呼びます。
CSV形式のデータはフォーマットが決まっていることがほとんどですので、「sprintf関数」を使うことで目的の文字列を作り出すことが簡単にできます。
次のプログラムはテーブル上に用意された選手情報を、カンマ区切りで文字列として生成しています。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 個人情報の管理データ
typedef struct
{
char name[32]; // 氏名
int year; // 誕生日(年)
int month; // 誕生日(月)
int day; // 誕生日(日)
int height; // 身長
double weight; // 体重
} S_PERSON;
// テニス選手のテーブル情報
const S_PERSON player[] =
{
{"錦織圭", 1989, 12, 29, 178, 75.2},
{"フェデラー", 1981, 8, 8, 185, 85.4},
{"ジョコビッチ",1987, 5, 22, 188, 77.7},
{"大阪なおみ", 1997, 10, 16, 180, 69.5},
};
int main(void)
{
int i;
char moji[64];
for (i = 0; i < sizeof(player) / sizeof(player[0]); i++)
{
// 選手情報から文字列を生成
sprintf(moji, "%s, %d年%d月%d日, %dcm, %.1fkg",
player[i].name, player[i].year, player[i].month, player[i].day, player[i].height, player[i].weight);
// 生成した文字を画面に表示
printf("%s\n", moji);
}
return 0;
}
錦織圭, 1989年12月29日, 178cm, 75.2kg
フェデラー, 1981年8月8日, 185cm, 85.4kg
ジョコビッチ, 1987年5月22日, 188cm, 77.7kg
大阪なおみ, 1997年10月16日, 180cm, 69.5kg
「文字列」「整数」「浮動小数点」といった様々な変数情報を、カンマ区切りのCSVデータとして出力できましたね。
このようにテーブル構造のデータ群を、CSV形式のような規定のフォーマットにして出力するときに、「sprintf関数」はものすごく便利なものなのです。
「printf関数」と「sprintf関数」の関係性と使い分け
今まで皆さんは「sprintf関数」によく似た「printf関数」をたくさん使ってきたことでしょう。
そうなんです。この2つの関数は兄弟のような似た者同士の関数なのです。
この2つの関数の違いとは、いったい何なんでしょう?それを解説していきますよ!
「printf関数」と「sprintf関数」の違いとは
この2つの関数は、先頭に「s」が付くかどうかの違いであり、「s」とは「string(文字列)」を示すための文字でしたね。
「print」と「f」とは、「フォーマットに従い出力する!」という意味です。
関数名 | 文字列の出力先 |
---|---|
printf関数 | ディスプレイ(標準出力)に出力 |
sprintf関数 | 文字列領域(メモリ)に出力 |
この2つの関数は「フォーマットに従い出力する」という共通点がありますが、その出力先が異なるのです。
皆さんは、作成した文字列をどこに出力したいのですか?
画面に表示したければ「printf関数」、メモリに出力したければ「sprintf関数」を使うのです。
「sprintf_s関数」の使い方と「sprintf関数」との違い
ここまで「sprintf関数」を扱ってきましたが、冒頭で述べた通りこの関数の使用は推奨されていません。それは、バッファオーバーランを発生させやすく、危険性があるためです。
sprintf関数の危険性と使う時の注意点
例えば、次のようなプログラムは、moji配列の領域をオーバーランして破壊してしまいます。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char hello[] = "Hello";
char moji[5];
// "Hello World"の文字列をmojiにコピー
sprintf(moji, "%s World", hello);
return 0;
}
sprintf関数で生成した文字列は”Hello Wolrd”とヌル文字を入れて12文字の領域が必要ですが、moji配列は5文字分の領域しか確保されていません。
そのため、このプログラムは最後まで動かすと、スタック破壊の例外が発生します。
「sprintf関数」を使う時は、くれぐれも出力先のメモリ領域は余裕をもってサイズ確保を行っておきましょう。必ず設計根拠を元にサイズを見積もるのです!
スタック破壊は絶対に起こしてはならないバグです。必ずシステムが飛びます。メモリが破壊されるというのはものすごく危険なことなのです。
sprintf_s関数の使い方
sprintf関数との違いは、新しく第2引数に第1引数のメモリサイズを指定することです。
#include <stdio.h>
int sprintf_s(char * buf, size_t size, const char * format, ...);
このサイズを指定することで、コピー先のメモリ領域が不足していることを即座に検知することができます。
#include <stdio.h>
int main(void)
{
char hello[] = "Hello";
char moji[5];
// 第2引数のサイズには第1引数のサイズを設定する
sprintf_s(moji, sizeof(moji), "%s World", hello);
return 0;
}
実行すると、sprintf_s関数を実行した直後に、アサートと呼ばれるメッセージが表示されます。
扱うときに注意するのは、メモリ領域が足りないからといって「戻り値が異常になる」といったものではなく、アサートメッセージにより即座に問題があったことを教えてくれるということです。
sprintf関数の場合は、スタックメモリの場合は関数のreturn時に異常が発生しますが、sprintf_s関数の場合は呼び出し直後にアサートが発生することです。
そのため、問題の特定が用意になります。
sprintf関数のまとめ
- 「sprintf関数」はフォーマットに従い文字列をメモリ上へ生成するサービスを提供する
- フォーマットに変換指定子を配置することで、変数の目的の形式して文字列化できる
- 文字列の出力先のメモリ領域は十分なサイズを確保した上で呼び出す必要がある
- 「sprintf関数」よりも「sprintf_s関数」を使った方が安全