scanf
scanf(スキャンエフ)は、C言語の標準ライブラリに属し、ヘッダーファイル stdio.h
で宣言されている、書式(format)付き入力のための関数である。
標準入力(大抵はキーボード)からの入力を、書式に従って変数に読み込む機能を持つ。標準出力関数のprintfと対比させて考えると分かりやすい。
ユーザーからの入力を受ける、ごく基本的な機能を持つにもかかわらず、後述するように異常入力(エラー)に配慮すると相応の手間がかかるため、サンプルプログラムや入門書を除いてはあまり使われない。
このファミリーの関数には、入力ストリームを指定できる fscanf や、メモリ上の文字列ストリームを入力対象とする sscanf などがある。
形式
[編集]<stdio.h>
内で以下のように宣言されている。
int scanf(const char *format, ...);
C99以降は以下のようにrestrict
型修飾子[1]を伴う(en:restrictも参照のこと)。
int scanf(const char *restrict format, ...);
printf と同様、第1引数のformat
は、それに続く可変長の実引数の変換方法(書式)を指定する。また戻り値は入力(スキャン)に成功した入力項目の数が返される。
利用例を以下に示す。
#include <stdio.h>
int main(void)
{
int x; /* スキャン結果を格納する変数。 */
printf("x = ? "); /* 入力を促すプロンプト。 */
/* スキャンと結果の確認。 */
if (scanf("%d", &x) == 1) {
printf("スキャン結果 = %d\n", x);
}
else {
printf("スキャン失敗\n");
}
return 0;
}
変換指定
[編集]scanf の変換指定は次の形式をとる。
% [代入抑止][最大フィールド幅][長さ修飾子]変換指定子
printfとよく似ているが、scanfの場合は変換結果を受け取る実引数としてポインタを渡す必要がある。型の不一致があった場合の動作は未定義である。
代入抑止
[編集]フラグ | 意味 |
---|---|
* | フォーマットに合わせて入力を読み込むが実引数に代入はされない。 |
例えば
char c;
scanf("%*c%c", &c);
というコードがあり、 "ab"という入力があった場合 1文字目の 'a' は無視され 2文字目の 'b' という文字が代入される。
長さ修飾子
[編集]修飾子 | 意味 | 導入バージョン |
---|---|---|
なし | 実引数はsigned int* 型、unsigned int* 型、char* 型、float* 型、またはvoid** 型 |
全バージョン |
hh | 実引数はsigned char* 型またはunsigned char* 型 |
C99以降 |
h | 実引数はsigned short* 型またはunsigned short* 型 |
全バージョン |
l(エル) | 実引数はsigned long* 型、unsigned long* 型、wchar_t* 型、またはdouble* 型 |
wchar_t についてはC95以降
|
ll(エルエル) | 実引数はsigned long long* 型またはunsigned long long* 型 |
C99以降 |
j | 実引数はintmax_t* 型またはuintmax_t* 型 |
C99以降 |
z | 実引数はsize_t* 型 |
C99以降 |
t | 実引数はptrdiff_t* 型 |
C99以降 |
L | 実引数はlong double* 型 |
全バージョン |
Cではchar
型は文字型であり、さらにC++では文字型も整数型の一種であるが、符号付きかそれとも符号無しかは実装定義である。char
型、signed char
型、unsigned char
型は、いずれもsizeof演算子の適用結果は1
であり、char
型の値の内部表現はsigned char
型またはunsigned char
型のいずれかと等しいが、型システム上ではすべて異なる型として扱われる[2][3]。一方short
型、int
型、long
型、long long
型は、signed
/ unsigned
を省略した場合、符号付きとして扱われ、signed
を指定した場合と同じ型になる。
変換指定子
[編集]指定子 | 意味 | 導入バージョン |
---|---|---|
d,i | 10進符号付き整数 | 全バージョン |
u | 10進符号無し整数 | 全バージョン |
o | 8進符号無し整数 | 全バージョン |
x,X | 16進符号無し整数 | 全バージョン |
e,E | 浮動小数点数 | 全バージョン |
f,F | 浮動小数点数 | 全バージョン |
g,G | 浮動小数点数 | 全バージョン |
a,A | 浮動小数点数 | C99以降 |
c | 文字 | 全バージョン |
s | 文字列 | 全バージョン |
p | ポインタの値。対応する実引数はvoid** になる。 |
全バージョン |
n | 整数変数に出力済み文字数を格納 | 全バージョン |
% | '%'の入力 | 全バージョン |
[...] | [ ]内で囲まれた文字だけを取得し、 それ以外の文字が現れた場所以降は入力を終了する(下記参照)。 |
全バージョン |
[ ] は例えば
char str[256];
scanf("%[abc]", str);
というコードがあり、入力に "babaacdeabfghijabcef" という文字列が入った場合、str には "babaac" という文字列のみが入力され、残りの文字列は入力されずに終了する。strに代入されなかった、"deabfghijabcef"は入力ストリームに残る形となる。また[^ ... ]とした場合は逆に[ ]内の文字が入ってくるまで文字を読み込む。 例えば、
char str[256];
scanf("%[^abc]", str);
という場合、"ghetbceajk"と入力すると、str には"ghet"が代入される。上記と同様に、入力されなかった文字列は入力ストリームに保持される。
コード例
[編集]ソース:
#include <stdio.h>
#include <limits.h>
int main(void)
{
int n1, n2, nadd, nsub, nmul, ndiv, nmod;
printf("1つ目の入力数値:");
if (scanf("%d", &n1) != 1) {
puts("スキャン失敗");
return -1;
}
printf("2つ目の入力数値:");
if (scanf("%d", &n2) != 1) {
puts("スキャン失敗");
return -1;
}
if (n2 == 0) {
/* ゼロ除算防止のチェック。 */
puts("2つ目の数値は非ゼロを入力してください");
return -1;
}
if (n1 == INT_MIN && n2 == -1) {
/* 除算と剰余のオーバーフローエラー防止のチェック。 */
puts("オーバーフローしない数値の組み合わせを入力してください");
return -1;
}
nadd = n1 + n2;
nsub = n1 - n2;
nmul = n1 * n2;
ndiv = n1 / n2;
nmod = n1 % n2;
printf("%d + %d = %d\n", n1, n2, nadd);
printf("%d - %d = %d\n", n1, n2, nsub);
printf("%d * %d = %d\n", n1, n2, nmul);
printf("%d / %d = %d + %d / %d\n", n1, n2, ndiv, nmod, n2);
return 0;
}
出力結果の例:
1つ目の入力数値:60
2つ目の入力数値:21
60 + 21 = 81
60 - 21 = 39
60 * 21 = 1260
60 / 21 = 2 + 18 / 21
関連する関数
[編集]fscanf
[編集]int fscanf(FILE *fp, const char *format, ...);
fscanf は第1引数にFILEポインタを指定することで、標準入力からではなくファイルストリームから読み込む。Cでは標準入力は stdin
というFILEポインタで表すので、fscanf(stdin, ...)
は scanf と完全に同義である。
sscanf
[編集]int sscanf(const char *str, const char *format, ...);
sscanf は第1引数に文字列へのポインタを指定することで、標準入力からではなく文字列ストリームから読み込む。後述するように scanf でエラー処理を実装しようとすると、エラーにならずに再度入力待ちになってしまうパターンがある。このため標準入力から数値を入力する場合には、直接 scanf を使わずにいったん fgets 関数等で文字列として読み込んでから sscanf で処理することのほうが多い。
scanfの問題点と回避方法
[編集]この節には独自研究が含まれているおそれがあります。 |
scanf は非常に簡潔に入力の制御が行えるため、Cの入門書では必ずと言っていいほど登場する。しかしこの関数にはいくつかの問題点が指摘されている。ここではよく指摘される scanf の問題点とその回避方法について述べる。
改行文字の取り扱い
[編集]入力が文字(char)の場合、主にキーボードで入力の際に押されるリターンキーが無視できないという問題点がある。例えば
char a, b, c;
scanf("%c", &a);
scanf("%c", &b);
scanf("%c", &c);
とした場合、通常なら3回入力待ちが行われることが期待されていると思われるが、実際には2回しか行われない(予期しない入力はここでは考慮しない)。これは最初の a の入力には入力された文字が代入されるが、このときストリーム上に改行コードが残されてしまい、次の b には a を入力する際に押下されたリターンキーの改行コードが代入されるためである。通常の %d や %s の場合改行コードは無視して入力を読み込むので問題にはならないが、 %c の場合は無条件にストリーム上の次のバイトを返すためこのような現象が発生する。これを防ぐには、
scanf("%c", &a);
scanf(" %c", &b);
scanf(" %c", &c);
のように最初に空白を入れることで回避される。これは入力前に空白(改行コードも含む)を読み飛ばすことを意味している。ただしこの場合 a, b, c に半角スペースを代入することはできない。そのような場合としては
scanf("%c%*c", &a);
scanf("%c%*c", &b);
scanf("%c%*c", &c);
という方法がとられる。これは入力した際の改行文字を%*cで除去することを意味する。
空白、タブの読み飛ばし
[編集]scanf は空白とタブは改行文字と同じ区切り文字として扱われる。例えば、
char a[20];
scanf("%s", a);
という方法で文字列を読み込もうとしたとする。このとき入力文字列が "This is a pen" というものだった場合、実引数に入力されるものは "This" までである。これだけでも問題であるが、さらに問題なのは、残りの " is a pen" という文字列はストリーム上に残されてしまうことである。このため次に scanf を呼んだ場合、入力にかかわらず実引数に "is" という文字列を代入しようとしてしまうことになる。これを防ぐ手段としては
scanf("%[^\n]", a);
という方法がとられる。この場合は改行文字以外の文字列を全て a に代入するため、a には空白やタブも含めて文字列が代入される。なお前述における入力時の改行コードの取り扱いも考慮した場合は、
scanf("%[^\n]%*c", a);
という記述になる。
バッファオーバーラン
[編集]他のC言語の文字列処理関数と同様に、scanf を使用する際にも使い方を誤るとバッファオーバーランが発生する可能性がある[4]。例えば
char a[20];
scanf("%s", a);
というコードがあった場合、a に入力できる文字列は終了文字を除く、19バイトまでしか入力することができず、それ以上の文字列が入力されるとバッファオーバーランが発生する。a の領域が十分であれば問題はないが、しかし入力されてくる文字列の長さを予測することは不可能であり、結局 a の長さをいくら大きくしても、さらにそれを上回る長さの入力が行われてしまえばバッファオーバーランが発生してしまう危険性がある。 この問題を防ぐ手段として一般的なのは最大フィールド幅を指定することである。上記の例の場合は
char a[20];
scanf("%19s", a);
とすることで、aには最初の19バイトが読み込まれ入力を終了する。ただしこの場合は残りの文字列はストリーム上に残ってしまうので実際には以下のようにすることで対処される。
char a[20];
scanf("%19s%*[^\n]", a);
これは19バイト読み込みさらに改行文字までの文字列を空読みすることを意味する。
さらに前述の改行コードがストリームに残る問題を考慮すると
char a[20];
scanf("%19s%*[^\n]%*c", a);
となるが、この場合はaに入る文字列が 19バイト以下の場合には、入力ストリームにやはり改行文字が残る。よって実際には、
char a[20];
scanf("%19s%*[^\n]", a);
getchar();
のように入力終了後、改行文字を空読みするなどの方法がとられる。空白文字の取り扱いも考慮する場合
char a[20];
scanf("%19[^\n]%*[^\n]", a);
getchar();
となる。
バージョン8.0 (2005) 以降のMicrosoft Visual C++コンパイラに付属するCランタイムライブラリ (CRT) には、scanf_s()
という関数が用意されていて、バッファのサイズを指定することができる[5]。scanf_s はC11規格で標準化されたが、実装は必須ではなく任意である[6]。
異常な入力が行われた時の処理
[編集]scanf関数は、予期せぬ値が入力されると、その値を読み込まず、ストリーム上に残してしまう。例えば
int i;
scanf("%d", &i);
としたコードがあった場合、入力が数字でなかった場合は scanf はストリームにその文字列を残したまま終了することになる。このため次に scanf が呼ばれたときはそのストリーム上のバッファが実引数に代入され、他の scanf にも異常な結果を返すことになる。例として 1 が入力されたら終了、それ以外はもう一度入力を求めるようなプログラムを考えてみる。もしこのプログラムを
int i;
while (1) {
scanf("%d", &i);
if (i == 1) {
break;
}
}
というようなコードで記述した場合、数字以外の文字を入力してしまうと無限ループに陥ることになる。このような現象を防ぐ手段として scanf の戻り値を利用する方法がとられる。これは scanf は代入に成功した変数の数を戻り値として返すため、指定した実引数の数と scanf の戻り値が一致しないときに入力バッファをクリアすることで回避できる。例えば上記の例の場合は以下のようになる。
int i;
while (1) {
if (scanf("%d", &i) != 1) {
scanf("%*s");
if (feof(stdin)) {
/* エラー処理 */
}
continue;
}
if (i == 1) {
break;
}
}
入力バッファのクリアはここでは文字列を空読みすることで実現している(入力バッファのクリア方法は状況によって異なる方法が考えられる)。なお主にウェブ上などで、標準入力のバッファクリアの方法に fflush(stdin) や rewind(stdin) とする方法が紹介されることがある[要出典]が、ANSI C規格ではこれらの動作は未定義であり、正しい動作を保証するものではない。
この方法でエラー処理をする場合には上記の空白文字の取り扱いの関連で問題になることがある。例えば
int i, j;
scanf("%d %d", &i, &j);
のようなコードで、異常な入力があった場合エラー処理をしたいとする。これを
int i, j;
if (scanf("%d %d", &i, &j) != 2) {
/* エラー処理 */
}
という方法で実現すると、入力が 1
と改行のみの場合、空白と改行は同一視されてしまうため、変数 i
に 1
を入力した後、変数 j
の入力待ち状態となる。もし、このような場合にもエラー処理を実行したい場合には、例えば以下のように一度文字列として読み込んで、その後 sscanf で読み込むことで実装すればよい。
char a[20];
int i, j;
scanf("%19[^\n]%*[^\n]", a);
getchar();
if (sscanf(a, "%d %d", &i, &j) != 2) {
/* エラー処理 */
}
算術オーバーフロー
[編集]文字列から数値に変換する際は、atoi/atolやatofと同じく、scanf系では算術オーバーフロー(浮動小数点数の場合は算術アンダーフローもありうる)を検出できないという問題がある。そのため、厳密な入力チェックが必要とされるアプリケーションソフトウェアではいずれも利用すべきではない。
代替として、strtolやstrtodといったオーバーフローチェックのできる変換関数を利用する[7]。
Clangの静的コード解析ツールClang-Tidyでは、scanf系を使用したコードに対して警告を発する[8]。
脚注
[編集]- ^ restrict type qualifier (since C99) - cppreference.com
- ^ Arithmetic types - cppreference.com (C)
- ^ Fundamental types - cppreference.com (C++)
- ^ “[迷信] scanfではバッファオーバーランを防げない”. C/C++迷信集. 株式会社きじねこ. 2024年11月2日閲覧。 “これは書式指定が不適切なために発生する脆弱性であって、scanf関数の問題ではありません。”
- ^ scanf_s, _scanf_s_l, wscanf_s, _wscanf_s_l | Microsoft Learn
- ^ scanf, fscanf, sscanf, scanf_s, fscanf_s, sscanf_s - cppreference.com
- ^ INT06-C. 文字列トークンを整数に変換するには strtol() 系の関数を使う
- ^ clang-tidy - cert-err34-c — Extra Clang Tools git documentation