web-dev-qa-db-ja.com

Cで入力を読み取る/解析する方法は? FAQ

入力を読み取ったり解析したりしようとすると、Cプログラムに問題が発生します。

助けて?


これはFAQエントリです。

StackOverflowには、Cでの入力の読み取りに関連するmany質問があり、回答は通常、実際に全体像を描くことなく、その特定のユーザーの特定の問題に焦点を当てています。

これは多くの一般的な間違いを包括的にカバーする試みなので、この特定の一連の質問は、この問題の複製としてマークするだけで答えることができます。

  • 最終行が2回印刷されるのはなぜですか?
  • scanf("%d", ...)/scanf("%c", ...)が失敗するのはなぜですか?
  • gets()がクラッシュするのはなぜですか?
  • ...

答えはコミュニティウィキとしてマークされています。自由に改善し、(慎重に)拡張してください。

21
DevSolar

初心者向けC入力入門

  • テキストモードとバイナリモード
  • チェック fopen() 失敗
  • 落とし穴
    • 成功するために呼び出す関数を確認します
    • EOF、または「なぜ最後の行が2回印刷されるのか」
    • gets() は使用しないでください
    • stdinまたは読み取り用に開かれている他のストリームでは、決して fflush() を使用しないでください
    • 不正な可能性のある入力には * scanf() を使用しないでください
    • * scanf() が期待どおりに機能しない場合
  • 読み取り、、次に解析
    • fgets() を使用して入力行を(一部)読み取ります
    • メモリ内の行を解析します
  • 掃除

テキストモードとバイナリモード

「バイナリモード」ストリームは、書き込まれたとおりに読み込まれます。ただし、ストリームの末尾に、実装で定義された数のnull文字( '\0')が追加される場合とされない場合があります。

「テキストモード」のストリームは、次のようないくつかの変換を実行できます(ただしこれらに限定されません)。

  • 行末の直前のスペースの削除。
  • 出力では改行('\n')を別の何かに変更し(たとえば、Windowsでは"\r\n")、入力では'\n'に戻します。
  • 印刷文字ではない文字(isprint(c)はtrue)、水平タブ、または改行の追加、変更、または削除。

テキストモードとバイナリモードが混在しないことは明らかです。テキストファイルをテキストモードで開き、バイナリファイルをバイナリモードで開きます。

チェック fopen() 失敗

ファイルを開こうとする試みは、さまざまな理由で失敗する可能性があります-アクセス許可の欠如、またはファイルが見つからないことが最も一般的なものです。この場合、 fopen()NULLポインターを返します。 常にfopenNULLポインタを返したかどうかを確認してから、ファイルの読み取りまたは書き込みを試みます。

fopenが失敗すると、通常、グローバルな errno 変数を設定して、失敗した理由を示します。 (これは厳密にはC言語の要件ではありませんが、POSIXとWindowsの両方で保証されています。)errnoは、errno.hの定数と比較できるコード番号ですが、単純なプログラムで使用できます、通常、必要なのは perror() または strerror() を使用してエラーメッセージに変換して出力することだけです。エラーメッセージには、fopenに渡したファイル名も含める必要があります。これを行わないと、ファイル名が思ったとおりではないという問題があるときに、非常に混乱します。

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr, "usage: %s file\n", argv[0]);
        return 1;
    }

    FILE *fp = fopen(argv[1], "rb");
    if (!fp) {
        // alternatively, just `perror(argv[1])`
        fprintf(stderr, "cannot open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    // read from fp here

    fclose(fp);
    return 0;
}

落とし穴

成功した​​かどうかを呼び出す関数をチェックします

これは明らかです。ただし、do呼び出した関数のドキュメントで戻り値とエラー処理を確認し、checkで条件を確認します。

これらは、早い段階で状態をキャッチすると簡単に発生するエラーですが、そうしないと、多くの頭を引っかくことになります。

EOF、または「なぜ最後の行が2回印刷されるのか」

関数 feof() は、EOFに達した場合にtrueを返します。「到達する」ことについての誤解EOF =実際には、多くの初心者が次のようなものを書くことを意味します:

// BROKEN CODE
while (!feof(fp)) {
    fgets(buffer, BUFFER_SIZE, fp);
    printf("%s", buffer);
}

これにより、入力印刷の最後の行がtwiceになります。これは、最後の行が読み取られると(最後の改行まで、入力ストリームの最後の文字)、EOFは設定されていません設定されています

EOFは、最後の文字pastを読み取ろうとしたときにのみ設定されます。

したがって、上記のコードはもう一度ループします fgets() は別の行の読み取りに失敗し、EOFを設定してbuffer untouched。その後、再び出力されます。

代わりに、fgetsが直接失敗したかどうかを確認します。

// GOOD CODE
while (fgets(buffer, BUFFER_SIZE, fp)) {
    printf("%s", buffer);
}

gets() 、everは使用しないでください

この関数を安全に使用する方法はありません。 このため、C11の登場により言語から削除されました

fflush() を使用しないでくださいstdinまたはその他の読み取り用に開いているストリーム、これまでにない

多くの人々は、まだ読まれていないユーザー入力をfflush(stdin)が破棄することを期待しています。 それは行いません。プレーンISO Cでは、入力ストリームで fflush() を呼び出すと ndefined behaviour になります。 POSIXとMSVCでは明確に定義された動作がありますが、どちらもまだ読み取られていないユーザー入力を破棄しません。

通常、保留中の入力をクリアする正しい方法は、改行までの文字を読み取って破棄することです。

int c;
do c = getchar(); while (c != EOF && c != '\n');

不正な可能性のある入力には * scanf() を使用しないでください

多くのチュートリアルでは、あらゆる用途の入力を読み取るために * scanf() を使用することを教えています。

しかし、 * scanf() の目的は、事前に定義された形式であるときにある程度信頼できるバルクデータを読み取ることです。 (他のプログラムで書かれているなど)

それでも * scanf() は非監視者をつまずかせることができます:

  • 何らかの形でユーザーの影響を受ける可能性のあるフォーマット文字列を使用することは、大きなセキュリティホールです。
  • 入力が予期される形式と一致しない場合、 * scanf() はすぐに解析を停止し、残りの引数を初期化しないままにします。
  • 成功した割り当ての数が表示されます-これが理由で戻りコードをチェックする必要があります(上記を参照)-ただし、正確に入力の解析を停止した場所ではなく、正常なエラー回復が困難です。
  • 省略しない場合([c、およびn変換)を除いて、入力内の先頭の空白はすべてスキップします。 (次の段落を参照してください。)
  • いくつかのまれなケースでは、それはやや独特の振る舞いをします。

* scanf() が期待どおりに機能しない場合

* scanf() でよくある問題は、ユーザーが考慮しなかった入力ストリームに未読の空白(' ''\n'、...)がある場合です。

数値("%d"など)または文字列("%s")の読み取りは、空白文字で停止します。そして、ほとんどの*scanf()変換指定子skip入力の空白、[cおよびnは、ない。したがって、改行はまだ最初の保留中の入力文字であり、%c%[のいずれかが一致しなくなります。

明示的にそれを読むことで、入力の改行をスキップできます。 fgetc() を使用するか、空白を * scanf() フォーマット文字列に追加します。 (フォーマット文字列内の単一の空白は、入力内のany数の空白と一致します。)

読み取り、、次に解析

* scanf() を使用しないことをお勧めします。ただし、実際に、積極的に、何をしているかを知っている場合は除きます。それで、代わりに何を使うのですか?

* scanf() が試行するように、入力を一度に読み取って解析する代わりに、ステップを分けます。

fgets()を介して入力行を(一部)読み取ります

fgets() には、バッファのオーバーフローを回避するために、入力を最大でそのバイト数に制限するためのパラメータがあります。入力行がバッファーに完全に収まった場合、バッファーの最後の文字が改行になります('\n')。すべてが収まらない場合は、部分的に読み取られた行を調べています。

メモリ内の行を解析します

strtol() および strtod() 関数ファミリは、メモリ内の解析に特に役立ちます。これらは、 * scanf() と同様の機能を提供します変換指定子diuoxaef、およびg

しかし、それらはまた、解析を停止した場所に正確にを通知し、対象の型には大きすぎる数値の意味のある処理を行います。

それらを超えて、Cは 幅広い文字列処理関数 を提供します。入力はメモリにあり、いつまでにそれを解析したかを常に正確に知っているので、入力を理解するために何回でも戻ることができます。

そして、他のすべてが失敗した場合、ユーザーに役立つエラーメッセージを出力するために利用できる行全体があります。

掃除

(正常に)開いたストリームを明示的に閉じてください。これにより、まだ書き込まれていないバッファがフラッシュされ、リソースリークが回避されます。

fclose(fp);
29
DevSolar