私は、他の人がscanf
を使うことを思いとどまらせ、より良い代替案があると言っているのをよく見ます。ただし、最終的に表示されるのは、 "scanf
を使用しない"または "のいずれかです。正しいフォーマット文字列です「、およびの例は決してありません」「より良い代替案」が言及されています。
たとえば、次のコードスニペットを見てみましょう。
scanf("%c", &c);
これは、最後の変換後に入力ストリームに残った空白を読み取ります。これに対する通常の推奨される解決策は以下を使用することです。
scanf(" %c", &c);
またはscanf
を使用しない場合。
scanf
は悪いので、scanf
が通常使用できる入力形式(整数、浮動小数点数、文字列など)をscanf
を使用せずに変換するためのANSI Cオプションは何ですか?
入力を読み取る最も一般的な方法は次のとおりです。
通常推奨される固定サイズのfgets
を使用します。
fgetc
を使用します。これは、単一のchar
のみを読み取る場合に役立ちます。
入力を変換するには、使用できるさまざまな関数があります。
strtoll
、文字列を整数に変換します
strtof
/d
/ld
、文字列を浮動小数点数に変換します
sscanf
。これは、単純にscanf
を使用することほど悪いasではありませんが、以下で説明するほとんどの欠点があります
区切り文字で区切られた入力をプレーンなANSI Cで解析する良い方法はありません。POSIXのstrtok_r
またはスレッドセーフではないstrtok
を使用してください。また、strtok_r
には特別なOSサポートが含まれないため、strcspn
およびstrspn
を使用して 独自にロール スレッドセーフバリアントを使用することもできます。
やり過ぎかもしれませんが、レクサーとパーサーを使用できます(flex
とbison
が最も一般的な例です)。
変換なし、単に文字列を使用する
あなたは正確にwhyscanf
を入力しなかったので、質問で悪いので、詳しく説明します:
変換指定子%[...]
および%c
を使用すると、scanf
は空白を使い果たしません。 この質問 の多くの重複によって証明されるように、これは明らかに広く知られていません。
scanf
の引数(具体的には文字列)を参照するときに、単項&
演算子をいつ使用するかについて混乱があります。
scanf
からの戻り値を無視するのは非常に簡単です。これにより、初期化されていない変数の読み取りによる未定義の動作が簡単に発生する可能性があります。
scanf
のバッファオーバーフローを防ぐことは非常に簡単です。 scanf("%s", str)
は、gets
と同じくらい悪いです。
scanf
を使用して整数を変換すると、オーバーフローを検出できません。 実際、オーバーフローはこれらの関数で 未定義の動作 を引き起こします。
scanf
が悪いのはなぜですか?主な問題は、scanf
がユーザー入力を処理することを意図していないことです。 「完全に」フォーマットされたデータで使用することを意図しています。言葉は完全に真実ではないので、「完全に」引用しました。ただし、ユーザー入力ほど信頼性の低いデータを解析するようには設計されていません。本来、ユーザー入力は予測できません。ユーザーは指示を誤解したり、タイプミスをしたり、実行前に誤ってEnterキーを押したりします。ユーザー入力に使用すべきでない関数がstdin
から読み取られる理由を合理的に尋ねる場合があります。あなたが経験豊富な* nixユーザーであれば、説明は驚くことではありませんが、Windowsユーザーを混乱させるかもしれません。 * nixシステムでは、パイピング経由で動作するプログラムをビルドすることは非常に一般的です。つまり、最初のプログラムのstdout
をstdin
にパイピングすることにより、あるプログラムの出力を別のプログラムに送信します。第二の。これにより、出力と入力が予測可能であることを確認できます。これらの状況では、scanf
は実際にうまく機能します。しかし、予測不能な入力で作業する場合、あらゆる種類のトラブルのリスクがあります。
では、なぜユーザー入力に使いやすい標準機能がないのでしょうか?ここでしか推測することはできませんが、古いハードコアCハッカーは、非常に不格好であるにもかかわらず、既存の機能は十分であると単純に考えていたと思います。また、典型的なターミナルアプリケーションを見ると、stdin
からユーザー入力を読み取ることはほとんどありません。ほとんどの場合、すべてのユーザー入力をコマンドライン引数として渡します。確かに例外はありますが、ほとんどのアプリケーションでは、ユーザー入力は非常に小さなものです。
私のお気に入りは、fgets
と組み合わせたsscanf
です。私はかつてそれについて答えを書きましたが、完全なコードを再投稿します。以下に、適切な(ただし完全ではない)エラーチェックと解析の例を示します。デバッグの目的には十分です。
注意
1行に2つの異なるものを入力するようユーザーに求めるのは特に好きではありません。それらが自然な形で互いに属している場合にのみ、それを行います。たとえば、
printf("Enter the price in the format <dollars>.<cent>: ")
のように、sscanf(buffer "%d.%d", &dollar, ¢)
を使用します。printf("Enter height and base of the triangle: ")
のようなことは決してしません。以下のfgets
を使用する主なポイントは、入力をカプセル化して、ある入力が次の入力に影響を与えないようにすることです。
_#define bsize 100
void error_function(const char *buffer, int no_conversions) {
fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
fprintf(stderr, "%d successful conversions", no_conversions);
exit(EXIT_FAILURE);
}
char c, buffer[bsize];
int x,y;
float f, g;
int r;
printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);
printf("You entered %d %d %f %c\n", x, y, f, c);
_
このようにすることで、ネストした入力を台無しにする可能性のある末尾の改行である一般的な問題を排除できます。ただし、行がbsize
より長い場合は別の問題があります。 if(buffer[strlen(buffer)-1] != '\n')
で確認できます。改行を削除する場合は、buffer[strcspn(buffer, "\n")] = 0
を使用して削除できます。
一般に、異なる変数に解析する必要がある奇妙な形式でユーザーが入力することを期待しないことをお勧めします。変数height
とwidth
を割り当てたい場合、同時に両方を要求しないでください。ユーザーがそれらの間でEnterキーを押すことを許可します。また、このアプローチはある意味で非常に自然です。 Enterキーを押すまでstdin
から入力を取得することはないので、行全体を常に読み取らないのはなぜですか。もちろん、ラインがバッファより長い場合、これは依然として問題につながる可能性があります。 Cではユーザー入力が不格好であることを思い出したのですか? :)
バッファーよりも長い行の問題を回避するには、適切なサイズのバッファーを自動的に割り当てる関数を使用できます。getline()
を使用できます。欠点は、後で結果をfree
する必要があることです。
ユーザー入力を使用してCでプログラムを作成することに真剣に取り組んでいる場合は、ncurses
などのライブラリを参照することをお勧めします。そのため、端末のグラフィックスを使用してアプリケーションを作成することも考えられます。残念ながら、それを行うと移植性が失われますが、ユーザー入力の制御がはるかに良くなります。たとえば、ユーザーがEnterキーを押すのを待つのではなく、キーが押されたことを即座に読み取ることができます。
scanf
は、knowを入力した場合に素晴らしいです。入力は常に適切に構造化され、適切に動作します。さもないと...
IMO、scanf
の最大の問題は次のとおりです。
バッファオーバーフローのリスク-%s
および%[
変換指定子にフィールド幅を指定しない場合、バッファオーバーフローのリスクがあります(バッファより多くの入力を読み取ろうとする保持するサイズです)。残念ながら、それを(printf
のように)引数として指定する良い方法はありません-変換指定子の一部としてそれをハードコードするか、いくつかのマクロシェナンガンを行う必要があります。
should拒否される入力を受け入れます-%d
変換指定子で入力を読み込んでいて、 12w4
のような何かを入力すると、expectscanf
がその入力を拒否しますが、そうではありません-正常に変換します12
を割り当て、入力ストリームにw4
を残して、次の読み取りをファウルします。
だから、代わりに何を使うべきですか?
通常、all対話型入力をfgets
を使用してテキストとして読み込むことをお勧めします-一度に読み込む最大文字数を指定できます、バッファオーバーフローを簡単に防ぐことができます。
char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
// error reading from input stream, handle as appropriate
}
else
{
// process input buffer
}
fgets
の奇妙な点は、余白がある場合にバッファーに末尾の改行を格納することです。そのため、予想よりも多くの入力を誰かが入力したかどうかを簡単に確認できます。
char *newline = strchr( input, '\n' );
if ( !newline )
{
// input longer than we expected
}
どのように対処するかはあなた次第です。入力全体を手で拒否するか、getchar
で残りの入力を丸lurみすることができます。
while ( getchar() != '\n' )
; // empty loop
または、これまでに取得した入力を処理して、もう一度読むことができます。それはあなたが解決しようとしている問題に依存します。
入力をtokenize(1つ以上の区切り文字に基づいて分割)するには、strtok
を使用できますが、注意してください-strtok
は入力を変更し(区切り文字を文字列ターミネーターで上書きします)、その状態を保持できません(つまり、1つの文字列を部分的にトークン化してから別の文字列のトークン化を開始し、中断した場所から再開します)元の文字列)。トークナイザーの状態を保持するバリアントstrtok_s
がありますが、その実装はオプションです(使用可能かどうかを確認するには、__STDC_LIB_EXT1__
が定義されていることを確認する必要があります)。
入力をトークン化した後、文字列を数字に変換する必要がある場合(つまり、"1234"
=> 1234
)、オプションがあります。 strtol
とstrtod
は、整数と実数の文字列表現をそれぞれの型に変換します。また、上記の12w4
の問題をキャッチすることもできます-それらの引数の1つは、最初の文字notへのポインターですストリング:
char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
// input is not a valid integer string, reject the entire input
else
val = tmp;
この回答では、テキストの行を読んで解釈していると仮定します。おそらく、何かを入力してRETURNを押しているユーザーにプロンプトを出しているのでしょう。または、何らかの種類のデータファイルから構造化されたテキストの行を読んでいる可能性があります。
テキストの行を読んでいるので、テキストの行を読み取るライブラリ関数を中心にコードを整理することは理にかなっています。標準関数はfgets()
ですが、他にもあります( getline
を含む)。そして次のステップは、そのテキスト行を何らかの形で解釈することです。
fgets
を呼び出してテキスト行を読み取るための基本的なレシピを次に示します。
_char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);
_
これは、単に1行のテキストを読み込み、それを出力し直します。書かれているように、これにはいくつかの制限がありますが、これについては後で説明します。また、非常に優れた機能があります。fgets
の2番目の引数として渡した512という数値は、line
の読み取りを要求している配列fgets
のサイズです。このことは、fgets
に読み取りを許可する量を伝えることができるということは、fgets
が読み取りすぎて配列をオーバーフローさせないことを確認できることを意味します。
テキストの行の読み方はわかりましたが、整数、浮動小数点数、単一の文字、または単一の単語を本当に読みたい場合はどうでしょうか? (つまり、改善しようとしているscanf
呼び出しが_%d
_、_%f
_、_%c
_、または_%s
_のようなフォーマット指定子を使用していた場合はどうなりますか?)
これらのいずれかのように、テキストの行、つまり文字列を再解釈するのは簡単です。文字列を整数に変換するための最も簡単な(不完全な)方法は、atoi()
を呼び出すことです。浮動小数点数に変換するには、 `atof()があります。 (また、すぐにわかるように、より良い方法もあります。)これは非常に簡単な例です:
_printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);
_
ユーザーに単一の文字(おそらくy
またはn
をyes/no応答として)を入力させたい場合は、次のように、行の最初の文字をそのまま取得できます。
_printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);
_
(これはもちろん、ユーザーが複数文字の応答を入力した可能性を無視します;入力された余分な文字を静かに無視します。)
最後に、入力行を処理する場合、ユーザーに空白を含む文字列notを確実に入力させたい場合
_hello world!
_
文字列_"hello"
_に続いて何か他のもの(これはscanf
形式_%s
_が行うことです)として、まあ、その場合、私は少し手を振ったので、結局のところ、質問のその部分に対する答えは少し待たなければなりません。
しかし、最初にスキップした3つの点に戻りたいと思います。
(1)私たちは電話してきました
_fgets(line, 512, stdin);
_
配列line
に読み込むには、512は配列line
のサイズであるため、fgets
はオーバーフローしないことがわかります。しかし、512が正しい数であることを確認するには(特に、誰かがサイズを変更するためにプログラムを微調整したかどうかを確認するために)、line
が宣言された場所に戻って読む必要があります。これは迷惑ですので、サイズを同期させるには、はるかに良い方法が2つあります。 (a)プリプロセッサを使用して、サイズの名前を作成します。
_#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);
_
または、(b)Cのsizeof
演算子を使用します。
_fgets(line, sizeof(line), stdin);
_
(2)2番目の問題は、エラーをチェックしていないことです。入力を読んでいるときは、alwaysエラーの可能性をチェックする必要があります。何らかの理由でfgets
が要求したテキスト行を読み取れない場合、nullポインターを返すことでこれを示します。だから私たちは次のようなことをしていたはずです
_printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
printf("Well, never mind, then.\n");
exit(1);
}
_
最後に、テキストの行を読み取るために、fgets
が文字を読み取り、行を終了する_\n
_文字が見つかるまで、それを配列に書き込むという問題があります_\n
_文字も配列に埋めます。前の例を少し変更すると、これがわかります。
_printf("you typed: \"%s\"\n", line);
_
これを実行し、プロンプトが表示されたときに「Steve」と入力すると、印刷されます
_you typed: "Steve
"
_
2行目の_"
_は、読み取って出力される文字列が実際には_"Steve\n"
_であったためです。
余分な改行は問題にならない場合があります(atoi
またはatof
を呼び出したときのように、両方とも数値の後の余分な非数値入力を無視するため)。そのため、頻繁にその改行を削除したいと思うでしょう。それにはいくつかの方法がありますが、これについては後で説明します。 (私は多くのことを言ってきたことを知っています。しかし、私はそれらすべてに戻ると約束します。)
この時点で、「scanf
はダメだと思ったし、この他の方法ははるかに良いと思った。しかしfgets
は迷惑に見え始めている。scanf
の呼び出しはso easy!使い続けられませんか?」
もちろん、必要に応じてscanf
を使用し続けることができます。 (そしてreally単純なこと、ある意味ではより単純です。)しかし、それが原因であなたが失敗したとき、私に泣かないでくださいその17の癖と脆弱性の1つ、または予期しない入力のために無限ループに入った場合、またはそれを使用してより複雑な操作を行う方法がわからない場合。そして、fgets
の実際の迷惑を見てみましょう。
常に配列サイズを指定する必要があります。もちろん、それはまったく迷惑ではありません。これは、バッファオーバーフローは本当に悪いことなので、機能です。
戻り値を確認する必要があります。 scanf
を正しく使用するには、その戻り値も確認する必要があるため、実際には、それはウォッシュです。
_\n
_を削除する必要があります。これは本当の迷惑です。この小さな問題がないことを指摘できる標準機能があればいいのにと思います。 (誰もgets
を呼び出さないでください。)しかし、_scanf's
_ 17種類の迷惑と比較して、私はこの1つの迷惑なfgets
をいつでも取ります。
それでは、その改行を削除する方法do 3つの方法:
(a)明白な方法:
_char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
_
(b)扱いにくいコンパクトな方法:
_strtok(line, "\n");
_
残念ながら、これは常に機能するとは限りません。
(c)別のコンパクトでややあいまいな方法:
_line[strcspn(line, "\n")] = '\0';
_
そして、これで邪魔にならないので、atoi()
とatof()
の不完全さをスキップした別のことに戻ることができます。これらの問題は、成功または失敗の成功を示す有用な指標を提供しないことです。非数値入力を静かに無視し、数値入力がない場合は静かに0を返します。他の利点もある好ましい代替案は、strtol
およびstrtod
です。 strtol
では、10以外のベースを使用することもできます。つまり、scanf
を使用して、(特に)_%o
_または_%x
_の効果を得ることができます。しかし、これらの機能を正しく使用する方法を示すこと自体が物語であり、すでにかなり断片化された物語に変わっているものからあまりにも気を散らすことになるので、私はそれらについてこれ以上話すことはありません。
残りの主要な物語は、単一の数字や文字よりも複雑な解析しようとしている入力に関するものです。 2つの数字、空白で区切られた複数の単語、または特定のフレーミング句読点を含む行を読みたい場合はどうしますか?それは、物事が面白くなり、scanf
を使用して物事を行おうとした場合に物事がおそらく複雑になり、fgets
を使用して1行のテキストをきれいに読んだ今、非常に多くのオプションがありますこれらのオプションはおそらく本を埋めることができるので、ここでは表面をスクラッチすることしかできません。
私のお気に入りのテクニックは、行を空白で区切られた「単語」に分割し、各「単語」でさらに何かをすることです。これを行うための主要な標準関数の1つはstrtok
です(これにも問題があり、個別の議論を評価します)。私自身の好みは、それぞれの分割された「Word」へのポインターの配列を作成するための専用関数です。これは これらのコースノート で説明しています。とにかく、「単語」を取得したら、おそらく同じatoi
/atof
/strtol
/strtod
関数を使用して、それぞれをさらに処理できます。
逆説的に、ここではかなりの時間と労力を費やしてscanf
から離れる方法を考え出していますが、fgets
で読み取ったテキスト行を処理する別の優れた方法は、sscanf
に渡すことです。このようにして、scanf
のほとんどの利点が得られますが、ほとんどの欠点はありません。
入力構文が特に複雑な場合は、「regexp」ライブラリを使用して解析することが適切な場合があります。
最後に、任意のad hoc解析ソリューションを使用できます。 _char *
_ポインターを使用して、期待する文字をチェックしながら、一度に1文字ずつ行を移動できます。または、strchr
、strrchr
、strspn
、strcspn
、strpbrk
などの関数を使用して特定の文字を検索できます。または、前にスキップしたstrtol
またはstrtod
関数を使用して、数字文字のグループを解析/変換およびスキップできます。
言うことができることは明らかにもっとたくさんありますが、この入門書があなたを始めることを願っています。
Scanfの代わりに入力の解析に使用できるものは何ですか?
scanf(some_format, ...)
の代わりに、fgets()
とともにsscanf(buffer, some_format_and %n, ...)
を検討してください
" %n"
を使用することで、コードはall形式が正常にスキャンされたかどうか、および余分な非空白ジャンクが最後になかったことを簡単に検出できます。
// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2]; // Suggest 2x, no real need to be stingy.
if (fgets(buffer, sizeof buffer, stdin)) {
int n = 0;
// add -------------> " %n"
sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
// Did scan complete, and to the end?
if (n > 0 && buffer[n] == '\0') {
// success, use `some_int, some_float`
} else {
; // Report bad input and handle desired.
}
解析の要件を次のように述べましょう。
有効な入力を受け入れる必要があります(他の形式に変換する)
無効な入力は拒否する必要があります
入力が拒否された場合、拒否された理由(「プログラマーではない普通の人でも簡単に理解できる」言語で)を説明する説明メッセージをユーザーに提供する必要があります問題の修正方法)
物事を非常にシンプルに保つために、単一の単純な10進整数(ユーザーが入力したもの)だけを解析することを考えてみましょう。ユーザーの入力が拒否される可能性のある理由は次のとおりです。
「許容できない文字を含む入力」も適切に定義しましょう。そして言う:
これから、次のエラーメッセージが必要であると判断できます。
この点から、文字列を整数に変換する適切な関数は、非常に異なる種類のエラーを区別する必要があることがわかります。そして、「scanf()
」または「atoi()
」または「strtoll()
」のようなものは、何が間違っていたかを示すことができないため、完全にまったく価値がないこと入力を使用して(および「有効な入力」であるかどうかの完全に無関係で不適切な定義を使用します)。
代わりに、役に立たないものを書き始めましょう。
_char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
return "Code not implemented yet!";
}
int main(int argc, char *argv[]) {
char *errorString;
int value;
if(argc < 2) {
printf("ERROR: No command line argument.\n");
return EXIT_FAILURE;
}
errorString = convertStringToInteger(&value, argv[1], -10, 2000);
if(errorString != NULL) {
printf("ERROR: %s\n", errorString);
return EXIT_FAILURE;
}
printf("SUCCESS: Your number is %d\n", value);
return EXIT_SUCCESS;
}
_
規定の要件を満たすために、このconvertStringToInteger()
関数は、数百行のコードだけで終わる可能性があります。
これは、「単一の単純な10進整数を解析する」だけです。複雑なものを解析したい場合を想像してください。 「名前、住所、電話番号、メールアドレス」構造のリストのように。またはプログラミング言語のようなものかもしれません。このような場合、数千行のコードを記述して、不自由な冗談ではない解析を作成する必要があります。
言い換えると...
Scanfの代わりに入力の解析に使用できるものは何ですか?
要件に合わせて、自分で(潜在的に数千行)コードを記述します。
以下は、flex
を使用して単純な入力をスキャンする例です。この場合、ASCIIいずれかのUS(n,nnn.dd
)または欧州(n.nnn,dd
)形式。これは、はるかに大きなプログラムからコピーされただけなので、未解決の参照がいくつかある可能性があります。
/* This scanner reads a file of numbers, expecting one number per line. It */
/* allows for the use of European-style comma as decimal point. */
%{
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifdef WINDOWS
#include <io.h>
#endif
#include "Point.h"
#define YY_NO_UNPUT
#define YY_DECL int f_Lex (double *val)
double atofEuro (char *);
%}
%option prefix="f_"
%option nounput
%option noinput
EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS [ \t\x0d]
%%
[!@#%&*/].*\n
^{WS}*{EURONUM}{WS}* { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}* { *val = atof (yytext); return (1); }
[\n]
.
%%
/*------------------------------------------------------------------------*/
int scan_f (FILE *in, double *vals, int max)
{
double *val;
int npts, rc;
f_in = in;
val = vals;
npts = 0;
while (npts < max)
{
rc = f_Lex (val);
if (rc == 0)
break;
npts++;
val++;
}
return (npts);
}
/*------------------------------------------------------------------------*/
int f_wrap ()
{
return (1);
}