scanfの代わりに何を入力変換に使用できますか?


125

他の人に使用を思いとどまらせscanfたり、より良い代替案があると言ったりする人がよくいます。ただし、結局、「使用しないscanfまたは「正しい形式の文字列はここにある」のいずれかであり、言及されている「より良い代替手段」の例はありません。

たとえば、次のコードスニペットを見てみましょう。

scanf("%c", &c);

これは、最後の変換後に入力ストリームに残った空白を読み取ります。これに対する通常の提案された解決策は、使用することです:

scanf(" %c", &c);

または使用しませんscanf

のでscanf悪い、入力フォーマット変換のためのいくつかのANSI Cオプション何scanf使用せずに(例えば、整数、浮動小数点、文字列など)を通常扱うことができるのscanf

回答:


87

入力を読み取る最も一般的な方法は次のとおりです。

  • fgets通常推奨される固定サイズでの使用、および

  • を使用しfgetcます。これは、1つだけを読んでいる場合に役立ちますchar

入力を変換するために使用できるさまざまな関数があります。

  • strtoll、文字列を整数に変換する

  • strtof/ d/ ld、文字列を浮動小数点数に変換する

  • sscanf、これは単純にを使用するほど悪くはありませんscanfが、以下に述べるほとんどの欠点があります。

  • プレーンなANSI Cで区切り文字で区切られた入力を解析する良い方法はありません。スレッドセーフではないstrtok_rPOSIXまたはから使用してstrtokください。また、特別なOSサポートを必要としないため、およびを使用して独自のスレッドセーフバリアントをロールすることもできますstrcspnstrspnstrtok_r

  • それはやり過ぎかもしれないが、あなたはレクサーとパーサを使用(することができますflexし、bison最も一般的な例です)。

  • 変換せず、単に文字列を使用する


私の質問でなぜ scanf悪いの正確に説明しなかったので、詳しく説明します。

  • 変換指定子%[...]とを使用すると%cscanf空白が消費されません。この質問の多くの重複によって証明されるように、これは明らかに広く知られていません。

  • の引数(特に文字列)&を参照するときに、単項演算子をいつ使用するかについて混乱がありscanfます。

  • からの戻り値を無視するのは非常に簡単scanfです。これは、初期化されていない変数を読み取ることにより、未定義の動作を簡単に引き起こす可能性があります。

  • でのバッファオーバーフローを防ぐことを忘れるのは非常に簡単scanfです。scanf("%s", str)と同じくらい悪いgetsです。

  • で整数を変換するときにオーバーフローを検出することはできませんscanf実際、オーバーフローはこれらの関数で未定義の動作を引き起こします。



56

なぜscanf悪いのですか?

主な問題は、scanfユーザー入力の処理を意図したものではなかったことです。「完全に」フォーマットされたデータで使用することを目的としています。「完全に」という言葉は完全に真実ではないので引用しました。ただし、ユーザー入力ほど信頼性の低いデータを解析するようには設計されていません。本来、ユーザー入力は予測できません。ユーザーは、説明を誤解している、タイプミスをしている、入力が完了する前に誤ってEnterキーを押しているなどstdin。経験豊富な* nixユーザーの場合、説明は驚くことではありませんが、Windowsユーザーを混乱させる可能性があります。* nixシステムでは、パイピングを介して機能するプログラムを構築することは非常に一般的です。stdoutstdin第二の。このようにして、出力と入力が予測可能であることを確認できます。これらの状況では、scanf実際にうまく機能します。しかし、予測できない入力を処理する場合、あらゆる種類のトラブルのリスクがあります。

それでは、なぜユーザー入力用の使いやすい標準関数がないのでしょうか。ここで推測することしかできませんが、古いハードコアCハッカーは、既存の関数は非常に扱いにくいとはいえ、十分に優れていると単純に思っていたと思います。また、一般的なターミナルアプリケーションを見ると、ユーザー入力をから読み取ることはほとんどありませんstdin。ほとんどの場合、すべてのユーザー入力をコマンドライン引数として渡します。確かに例外はありますが、ほとんどのアプリケーションでは、ユーザー入力はごくわずかです。

それで、あなたは何ができますか?

私のお気に入りはfgetsとの組み合わせsscanfです。私は一度それについて答えを書きましたが、完全なコードを再投稿します。これはまともな(ただし完全ではない)エラーチェックと解析の例です。デバッグ目的には十分です。

注意

ユーザーに1つの行に2つの異なるものを入力するように求めるのは特に好きではありません。彼らが自然にお互いに属しているときだけ、私はそれをします。たとえばprintf("Enter the price in the format <dollars>.<cent>: ")、のように使用しますsscanf(buffer "%d.%d", &dollar, &cent)。のようなことは決してしませんprintf("Enter height and base of the triangle: ")fgets以下を使用する主な目的は、入力をカプセル化して、1つの入力が次の入力に影響を与えないようにすることです。

#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);

これらの多くを行う場合は、常にフラッシュするラッパーを作成することをお勧めします。

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

このようにすることで、ネストされた入力を混乱させる可能性がある後続の改行という一般的な問題が解消されます。しかし、これには別の問題がありbsizeます。それは、ラインがより長い場合です。で確認できif(buffer[strlen(buffer)-1] != '\n')ます。改行を削除したい場合は、を使用して削除できますbuffer[strcspn(buffer, "\n")] = 0

一般に、ユーザーが別の変数に解析する必要がある奇妙な形式で入力を入力することを期待しないことをお勧めします。変数heightとを割り当てる場合はwidth、両方を同時に要求しないでください。ユーザーがそれらの間でEnterキーを押すことを許可します。また、このアプローチは、ある意味で非常に自然です。stdinEnterキーを押すまで入力を取得することはないので、常に行全体を読み取らないようにしてください。もちろん、ラインがバッファよりも長い場合、問題が発生する可能性があります。Cでのユーザー入力は不格好であることを覚えていますか?:)

バッファーよりも長い行の問題を回避するには、適切なサイズのバッファーを自動的に割り当てる関数を使用できますgetline()。欠点は、free後で結果が必要になることです。

ゲームのステップアップ

Cでユーザー入力を使用してプログラムを作成することに真剣に取り組んでいる場合は、などのライブラリを確認することをお勧めしますncurses。そのため、いくつかの端末グラフィックスを使用してアプリケーションを作成することもできます。残念ながら、そのようにすると一部の移植性が失われますが、ユーザー入力をはるかに制御しやすくなります。たとえば、ユーザーがEnterキーを押すのを待つのではなく、キー入力を瞬時に読み取ることができます。


(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2は、末尾の非数値テキストを不良として検出しないことに注意してください。
chux-モニカを

1
@chux修正済み%f%f。最初の意味は?
klutt

fgets()ofを使用すると"1 2 junk"if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {「ジャンク」がある場合でも、入力に関する問題は報告されません。
chux-モニカを

@chuxああ、そうか。まあそれは意図的だった。
klutt

1
scanf完全にフォーマットされたデータで使用することを目的としていますが、それは事実ではありません。@chuxで言及されている「ジャンク」の問題に加えて、などの形式"%d %d %d"は、1行、2行、または3行(または間に空白行がある場合はそれ以上)からの入力を喜んで読み取るという事実もありません。力(言う)のようなものを実行して、2行の入力への道"%d\n%d %d"などは、 scanfフォーマット済みのために適切であるかもしれないストリーム入力が、それは何のラインベースのためにすべての良いではありません。
スティーブサミット

18

scanf入力が常に適切に構造化され、適切に動作していることがわかっている場合は、すばらしいです。さもないと...

IMO、ここに最大の問題がありscanfます:

  • バッファオーバーフローのリスク - %sおよび%[変換指定子にフィールド幅を指定しない場合、バッファオーバーフローのリスクがあります(バッファが保持できるサイズよりも多くの入力を読み取ろうとする)。残念ながら、(のようにprintf)引数としてそれを指定する良い方法はありません-変換指定子の一部としてハードコードするか、いくつかのマクロを実行する必要があります。

  • 入力を受け入れなければならない拒否される -あなたが入力を読んでいる場合は%d、変換指定子と同様に、あなたが何かを入力し12w4、あなたがなり、期待する scanfその入力を拒否するように、それはしていません-それは成功した変換および譲受人12残して、w4入力ストリームに次の読みを汚す。

では、代わりに何を使うべきですか?

通常、すべてのインタラクティブ入力をテキストとして読み取ることをお勧めfgetsします。一度に読み取る最大文字数を指定できるため、バッファオーバーフローを簡単に防ぐことができます。

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

奇妙な点の1つfgetsは、スペースがある場合は、後続の改行をバッファーに格納するので、簡単なチェックを実行して、予想以上の入力をしたかどうかを確認できます。

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

それをどのように扱うかはあなた次第です-あなたは手に負えない入力全体を拒否し、残りの入力をgetchar次のように丸めることができます:

while ( getchar() != '\n' ) 
  ; // empty loop

または、これまでに得た入力を処理して、もう一度読むこともできます。解決しようとしている問題によって異なります。

入力をトークン化する(1つ以上の区切り文字に基づいて分割する)には、を使用できますstrtokが、注意してください- strtok入力を変更し(区切り文字を文字列ターミネーターで上書きします)、その状態を保持できません(つまり、 t 1つの文字列を部分的にトークン化してから、別の文字列をトークン化し、元の文字列で中断したところから始めます)。strtok_sトークナイザーの状態を保持するバリアントがありますが、その実装はオプションです(__STDC_LIB_EXT1__使用可能かどうかを確認するには、その定義を確認する必要があります)。

入力をトークン化したら、文字列を数値に変換する必要がある場合("1234"=>など1234)、オプションがあります。 strtolそしてstrtod、整数と実数の文字列表現をそれぞれの型に変換します。また、12w4前述の問題をキャッチすることもできます。引数の1つは、文字列で変換されない最初の文字へのポインターです。

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;

フィールド幅を指定しない場合%*[%\n] ...- または変換抑制(たとえば、これは、回答の中で長すぎる行を処理するのに役立ちます)。
Toby Speight

フィールド幅のランタイム仕様を取得する方法はありますが、それは良くありません。コード内でフォーマット文字列を作成する必要があります(おそらくを使用しますsnprintf())。
Toby Speight

5
あなたはisspace()そこで最も一般的な間違いを犯しました- として表される署名されていない文字を受け入れるintので、署名さunsigned charれているプラ​​ットフォームでUBを回避するためにキャストする必要がありますchar
Toby Speight

9

この回答では、あなたがテキストの行を読んで解釈していると仮定します。おそらく、何かを入力してRETURNを押しているユーザーにプロンプ​​トを表示しているのでしょう。あるいは、何らかのデータファイルから構造化テキストの行を読み取っているのかもしれません。

テキストの行を読み取っているので、テキストの行を読み取るライブラリ関数を中心にコードを編成することは理にかなっています。標準関数はですがfgets()、他にもあります(などgetline)。次に、そのテキスト行をどうにかして解釈します。

fgetsテキストの行を読み取るために呼び出すための基本的なレシピは次のとおりです。

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

これは単に1行のテキストを読み取り、それを印刷します。書かれているように、これにはいくつかの制限があります。これについては、すぐに説明します。また、これには非常に優れた機能があります。2番目の引数として渡した512という数値fgetsは、読み込む配列のサイズ 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または nyes / noの応答として)、次のように文字通り行の最初の文字を取得できます。

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(もちろん、これはユーザーが複数文字の応答を入力した可能性を無視します。入力された余分な文字を静かに無視します。)

最後に、ユーザーに空白を絶対に含まない文字列を入力させたい場合、入力行を処理したい場合

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番目の問題は、エラーをチェックしていないことです。入力を読み取るときは、常にエラーの可能性を確認する必要あります。何らかの理由で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
"

それ"それが戻っ読み出して印刷された文字列が実際にあったので、二行目にあります"Steve\n"

余分な改行は問題にならない場合があります(atoiまたはを呼び出したときのように atof、どちらも数値の後の余分な非数値入力を無視するため)。そのため、多くの場合、その改行を削除します。その方法はいくつかありますが、これについては1分後に説明します。(私は多くのことを言ってきたことを知っています。しかし、私はそれらすべてのものに戻ると約束します。)

この時点で、あなたは考えているかもしれません:「あなたはscanf 悪いことだと思っていたので、これfgetsはもっと良い方法だと思いました。しかし、迷惑のように見え始めています。通話scanfとても簡単だったのです!使い続けることはできませんか? 」

もちろん、必要scanfに応じてを使い続けることができます。(そして、本当に 単純なことについては、いくつかの点でより単純です。)しかし、17の癖とfoiblesの1つが原因で失敗した場合、または入力のために無限ループに陥った場合、私に泣いてはいけません期待していなかった、またはそれを使用してより複雑な何かを行う方法を理解できない場合。そして、のfgets実際の迷惑を見てみましょう:

  1. 常に配列サイズを指定する必要があります。もちろん、それはまったく厄介なことではありません。これは機能です。バッファオーバーフローは本当に悪いことです。

  2. 戻り値を確認する必要があります。実際、これはウォッシュscanfです。正しく使用するには、戻り値もチェックする必要があるためです。

  3. あなたは\n背中をはぎ取らなければならない。これは本当の迷惑です。この小さな問題がなかったことを指摘できる標準関数があればいいのにと思います。(誰も育てないでくださいgets。)しかし、scanf's17種類の迷惑と比較して、私はこの1つの迷惑をfgetsいつの日か受けます。

では、どのようにてその改行を取り除くのですか?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以外のベースを使用することができます%oか、%xscanf。しかし、これらの関数を正しく使用する方法を示すこと自体がストーリーであり、すでにかなり断片化された物語になっているものから気を散らすので、ここではこれ以上何も述べません。

残りの主なナラティブは、解析しようとしている可能性のある入力に関するもので、1つの数字や文字だけではなく、より複雑です。2つの数字、空白で区切られた複数の単語、または特定のフレーミング句読点を含む行を読みたい場合はどうでしょうか。ここで物事が面白くなり、を使用してやろうとすると物事がおそらく複雑になり、を使用してscanf1行のテキストをきれいに読んだので、非常に多くのオプションがありfgetsます。おそらく本を埋めることができるので、ここでは表面のみをスクラッチすることができます。

  1. 私のお気に入りのテクニックは、行を空白で区切られた「単語」に分割し、各「単語」でさらに何かを行うことです。これを行うための1つの主要な標準機能は strtok(これにも問題があり、全体の個別の議論を評価する)です。私自身の好みは、分割された各「単語」へのポインタの配列を構築するための専用関数であり、これらのコースノートで説明し ます。いずれにせよ、「単語」を取得したら、おそらく すでに見てきた同じatoi/ atof/ strtol/ strtod関数を使用して、それぞれをさらに処理できます。

  2. 逆説的に言えば、ここから離れる方法を見つけるためにかなりの時間と労力を費やしてきましたが、scanf今読んだテキストの行を処理するもう1つの優れた方法 fgetsは、に渡すことsscanfです。このようにして、のほとんどの利点が得られますscanfが、ほとんどの欠点はありません。

  3. 入力構文が特に複雑な場合は、「regexp」ライブラリを使用して解析するのが適切な場合があります。

  4. 最後に、必要に応じてアドホック解析ソリューションを使用できます。char *期待する文字をチェックするポインタを使用して、一度に1行ずつ行を移動でき ます。それとも、好きな機能を使用して、特定の文字を検索することができるstrchrstrrchr、またはstrspnあるいはstrcspn、またはstrpbrk。または、前にスキップしたstrtolor strtod関数を使用して、数字文字のグループを解析/変換してスキップできます。

言うことができることはもっとたくさんありますが、うまくいけば、この紹介があなたを始めさせるでしょう。


sizeof (line)単純に書くのではなく、書く理由はありsizeof lineますか?前者はline型名に見える!
Toby Speight

@TobySpeight良い理由は?いいえ、私はそれを疑います。括弧は私の習慣です。なぜなら、それが必要なオブジェクトなのかタイプ名なのかを思い出すのは面倒なことですが、多くのプログラマーは、可能な場合は省略します。(私にとってそれは個人的な好みとスタイルの問題であり、それはかなりマイナーなものです。)
スティーブサミット

+1 sscanfは変換エンジンとして使用しますが、別のツールを使用して入力を収集します(場合によってはマッサージします)。しかし、多分getlineコンテキストで言及する価値があります。
dmckee ---元モデレーターの子猫

fscanf「の実際の迷惑行為」について話すとき、fgets?そして、迷惑その3は本当に私をいらいらさせます。特に、scanf入力された文字数を返すのではなく、バッファへの役に立たないポインタを返すためです(これにより、改行がきれいに取り除かれます)。
スーパーキャット

1
あなたのsizeofスタイルの説明ありがとうございます。私にとって、かっこがいつ必要かを思い出すのは簡単です。私(type)は値のないキャストのようなものだと思います(型だけに関心があるためです)。もう1つstrtok(line, "\n")、それが常に機能するとは限らないが、いつ機能しないかは明らかではありません。行がバッファよりも長い場合を考えていると思いますので、改行はなく、strtok()null を返しますか?fgets()改行がそこにあるかどうかを知ることができるので、残念ながらこれ以上有用な値は返されません。
Toby Speight

7

scanfの代わりに何を使用して入力を解析できますか?

代わりにscanf(some_format, ...)、考えるfgets()sscanf(buffer, some_format_and %n, ...)

を使用して " %n"、コードはすべてのフォーマットが正常にスキャンされ、余白以外の余分なジャンクが最後にないことを簡単に検出できます。

// 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.
  }

6

解析の要件を次のように述べましょう:

  • 有効な入力を受け入れる(および他の形式に変換する)必要があります

  • 無効な入力を拒否する必要があります

  • 入力が拒否された場合、拒否された理由を(明確に「プログラマーではない通常の人でも簡単に理解できる」言語で)説明する説明メッセージをユーザーに提供する必要があります(そのため、人々は修正方法を理解できます問題)

非常に単純にするために、単一の単純な10進整数(ユーザーが入力したもの)のみを解析することを検討してみましょう。ユーザーの入力が拒否される理由は次のとおりです。

  • 入力に使用できない文字が含まれています
  • 入力は、受け入れられる最小値よりも小さい数値を表します
  • 入力は、受け入れられた最大値より大きい数値を表します
  • 入力は、0以外の小数部分を持つ数値を表します

「許容できない文字を含む入力」も適切に定義しましょう。そしてそれを言う:

  • 先頭の空白と末尾の空白は無視されます(例: "
    5」は「5」として扱われます)
  • ゼロまたは1つの小数点を使用できます(たとえば、「1234」と「1234.000」はどちらも「1234」と同じように扱われます)
  • 少なくとも1桁の数字が必要です(たとえば、「。」は拒否されます)
  • 小数点は1つしか許可されません(例:「1.2.3」は拒否されます)
  • 数字の間にないカンマは拒否されます(たとえば、 "、1234"は拒否されます)
  • 小数点より後のコンマは拒否されます(例: "1234.000,000"は拒否されます)
  • 別のコンマの後のコンマは拒否されます(たとえば、 "1、、234"は拒否されます)
  • 他のすべてのコンマは無視されます(たとえば、 "1,234"は "1234"として扱われます)
  • 最初の非空白文字ではないマイナス記号は拒否されます
  • 最初の非空白文字ではない正符号は拒否されます

これから、次のエラーメッセージが必要であると判断できます。

  • 「入力開始時の不明な文字」
  • 「入力の終わりの不明な文字」
  • 「入力途中の不明な文字」
  • 「数が少なすぎます(最小は....)」
  • 「数が多すぎます(最大は....)」
  • 「数値は整数ではありません」
  • 「小数点が多すぎます」
  • 「小数桁なし」
  • 「番号の最初のコンマが間違っています」
  • 「番号の末尾のコンマが間違っています」
  • 「数字の真ん中に悪いコンマ」
  • 「小数点の後の不正なコンマ」

この時点から、文字列を整数に変換する適切な関数は、非常に異なるタイプのエラーを区別する必要があることがわかります。そして、「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の代わりに何を使用して入力を解析できますか?

要件に合わせて、自分で(場合によっては数千行)のコードを記述します。


5

次に、を使用flexして単純な入力をスキャンする例を示します。この場合、US(n,nnn.dd)またはヨーロッパ(n.nnn,dd)形式のASCII浮動小数点数のファイルです。これははるかに大きなプログラムからコピーされただけなので、未解決の参照がいくつかある可能性があります。

/* 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);
}

-5

他の答えは正しい低レベルの詳細を与えるので、私は高レベルに限定します:まず、各入力行がどのように見えると期待するかを分析します。入力を正式な構文で記述してみてください。運が良ければ、通常の文法、または少なくとも文脈自由文法を使用して入力を記述できることがわかります。通常の文法で十分な場合は、有限状態機械をこれは、各コマンドラインを一度に1文字ずつ認識して解釈します。その後、コードは(他の応答で説明されているように)行を読み取り、ステートマシンを介してバッファー内の文字をスキャンします。特定の状態では、これまでにスキャンされた部分文字列を停止して数値などに変換します。これが簡単なものであれば、おそらく「自分でロール」することができます。完全なコンテキストフリーの文法が必要であることがわかった場合は、既存の解析ツール(re:lexおよびyacc/またはそのバリアント)の使用方法を理解することをお勧めします。


有限状態機械はやり過ぎかもしれません。変換のオーバーフローを検出する簡単な方法(errno == EOVERFLOW使用後のチェックなどstrtoll)が可能です。
SSアン

1
flexでそれらを簡単に記述できるのに、なぜ独自の有限状態マシンをコーディングするのでしょうか?
jamesqf
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.