web-dev-qa-db-ja.com

C-scanf()vs gets()vs fgets()

私は、文字列(数字が入力されていると仮定)を整数に変換するかなり簡単なプログラムを実行しています。

完了した後、主にscanf()gets()fgets()の機能に関する知識が限られているため、答えられない非常に奇妙な「バグ」に気付きました。作業。 (しかし、私は多くの文献を読みました。)

したがって、あまり多くのテキストを書かずに、プログラムのコードを次に示します。

#include <stdio.h>

#define MAX 100

int CharToInt(const char *);

int main()
{
    char str[MAX];

    printf(" Enter some numbers (no spaces): ");
    gets(str);
//  fgets(str, sizeof(str), stdin);
//  scanf("%s", str);

    printf(" Entered number is: %d\n", CharToInt(str));

    return 0;
}

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '\0')
    {
        temp = *(s+i) & 15;
        result = (temp + result) * 10;
        i++;
    }

    return result / 10;
}

だからここに私が抱えている問題があります。まず、gets()関数を使用すると、プログラムは完全に機能します。

第二に、fgets()を使用すると、明らかにfgets()関数が改行(ASCII値10)文字を読み取って結果を台無しにするため、結果はわずかに間違っています。

第三に、scanf()関数を使用する場合、最初の文字は明らかに-52 ASCII値。これについては説明がありません。

gets()の使用は推奨されていないので、改行文字を読み取らない(または無視する)ためにfgets()をここで使用できるかどうかを知りたいと思います。また、このプログラムのscanf()関数の処理は何ですか?

35
Marko
  • Nevergetsを使用します。バッファーオーバーフローの脆弱性に対する保護は提供されません(つまり、渡すバッファーの大きさを伝えることができないため、ユーザーがバッファーより大きな行を入力してメモリを破壊することを防ぐことはできません)。

  • scanfの使用は避けてください。慎重に使用しないと、getsと同じバッファオーバーフローの問題が発生する可能性があります。それを無視しても 他の問題があり、正しく使用するのが難しくなります

  • 一般に、代わりにfgetsを使用する必要がありますが、それは時々不便です(改行を削除する必要があります。事前にバッファサイズを決定する必要があります。あなたが読んだ部分を保持し、 余分な部分を捨てる 、全部を捨てる、動的にバッファを増やして再試行するなど)。この動的な割り当てを行う非標準の関数がいくつかあります(例:POSIXシステムではgetlineチャックファルコナーのパブリックドメインggets 関数)。 ggetsには、末尾の改行を削除するという意味でgetsのようなセマンティクスがあることに注意してください。

27
jamesdlin

はい、getsを避けたいです。 fgetsは、バッファーがそれを保持するのに十分な大きさである場合、常に改行を読み取ります(これにより、バッファーが小さすぎて読み取りを待機している行がまだあることがわかります)。改行を読み取らないfgetsのようなものが必要な場合(小さすぎるバッファの表示を失う)、"%N[^\n]"のようなスキャンセット変換でfscanfを使用できます。 「N」はバッファサイズ-1に置き換えられます。

fgetsで読み込んだ後、バッファから末尾の改行を削除する簡単な(奇妙な場合)方法は次のとおりです。strtok(buffer, "\n");これはstrtokの意図ではありません使用しましたが、意図した方法よりも頻繁にこの方法で使用しました(通常は避けます)。

19
Jerry Coffin

このコードには多数の問題があります。間違った名前の変数と関数を修正し、問題を調査します。

  • まず、CharToInt()は単一の文字ではなくstringで動作するため、適切なStringToInt()に名前を変更する必要があります。

  • 関数CharToInt() [sic。]は安全ではありません。ユーザーが誤ってNULLポインターを渡したかどうかはチェックしません。

  • 入力を検証しません。より正確には、無効な入力をスキップします。ユーザーが数字以外を入力すると、結果には偽の値が含まれます。つまり、Nを入力すると、コード*(s+i) & 15は14を生成します!?

  • 次に、CharToInt() [sic。]の非記述子tempは、digitと呼ばれるべきです。

  • また、汚い_return result / 10;_はまさにそれです-バグのある実装を回避するのに悪いhackです。

  • 同様に、MAXは標準の使用法と矛盾するように見えるため、不適切な名前です。すなわち#define MAX(X,y) ((x)>(y))?(x):(y)

  • 詳細な*(s+i)は、単に_*s_ほど読みやすくありません。コードを使用して、さらに別の一時インデックスiを使用する必要はありません。

取得()

入力文字列バッファをオーバーフローさせる可能性があるため、これは悪いことです。たとえば、バッファサイズが2で、16文字を入力すると、strがオーバーフローします。

scanf()

これは、入力文字列バッファをオーバーフローさせる可能性があるため、同様に悪いです。

scanf()関数を使用する場合、最初の文字には明らかに-52 ASCII value。)があるため、結果は完全に間違っています」と述べています。

これは、scanf()の誤った使用方法によるものです。このバグを複製することはできませんでした。

fgets()

これは、バッファサイズ(NULLの余地を含む)を渡すことにより、入力文字列バッファがオーバーフローしないことを保証できるため、安全です。

getline()

少数の人々は、C POSIX標準getline()を代替として提案しています。残念ながら、MicrosoftはCバージョンを実装していないため、これは実用的なポータブルソリューションではありません。標準のC++のみ string template function as this SO #27755191 質問の答え。MicrosoftのC++ getline()Visual Studio 6 であるが、OPはC++ではなくCについて厳密に要求しているため、これはオプションではありません。

その他。

最後に、この実装は整数オーバーフローを検出しないという点でバグがあります。ユーザーが大きすぎる数値を入力すると、数値が負になる可能性があります!つまり、_9876543210_は_-18815698_ ?!になります!それも修正しましょう。

これは_unsigned int_の修正は簡単です。前の部分番号が現在の部分番号よりも小さい場合、オーバーフローし、前の部分番号を返します。

_signed int_の場合、これはもう少し作業です。 Assemblyでは、キャリーフラグを検査できますが、Cでは、signed int mathでオーバーフローを検出する標準の組み込み方法はありません。幸いなことに、定数_* 10_を乗算しているので、同等の方程式を使用すると、これを簡単に検出できます。

_n = x*10 = x*8 + x*2
_

X * 8がオーバーフローすると、論理的にx * 10もオーバーフローします。 32ビット整数の場合、x * 8 = 0x100000000のときにオーバーフローが発生するため、x> = 0x20000000のときに検出するだけです。 intにいくつのビットがあるかを想定したくないので、上位3 msb(Most Significant Bits)が設定されているかどうかをテストするだけです。

さらに、2番目のオーバーフローテストが必要です。桁の連結の後にmsbが設定されている場合(符号ビット)、数字がオーバーフローしたこともわかります。

コード

修正された安全なバージョンと、安全でないバージョンのオーバーフローを検出するために使用できるコードを次に示します。 _#define SIGNED 1_経由でsignedunsignedの両方のバージョンも含めました

_#include <stdio.h>
#include <ctype.h> // isdigit()

// 1 fgets
// 2 gets
// 3 scanf
#define INPUT 1

#define SIGNED 1

// re-implementation of atoi()
// Test Case: 2147483647 -- valid    32-bit
// Test Case: 2147483648 -- overflow 32-bit
int StringToInt( const char * s )
{
    int result = 0, prev, msb = (sizeof(int)*8)-1, overflow;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if ((*s >= '0') && (*s <= '9'))
        {
            prev     = result;
            overflow = result >> (msb-2); // test if top 3 MSBs will overflow on x*8
            result  *= 10;
            result  += *s++ & 0xF;// OPTIMIZATION: *s - '0'

            if( (result < prev) || overflow ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

// Test case: 4294967295 -- valid    32-bit
// Test case: 4294967296 -- overflow 32-bit
unsigned int StringToUnsignedInt( const char * s )
{
    unsigned int result = 0, prev;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if (*s >= '0' && *s <= '9')
        {
            prev    = result;
            result *= 10;
            result += *s++ & 0xF; // OPTIMIZATION: += (*s - '0')

            if( result < prev ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

int main()
{
    int  detect_buffer_overrun = 0;

    #define   BUFFER_SIZE 2    // set to small size to easily test overflow
    char str[ BUFFER_SIZE+1 ]; // C idiom is to reserve space for the NULL terminator

    printf(" Enter some numbers (no spaces): ");

#if   INPUT == 1
    fgets(str, sizeof(str), stdin);
#Elif INPUT == 2
    gets(str); // can overflows
#Elif INPUT == 3
    scanf("%s", str); // can also overflow
#endif

#if SIGNED
    printf(" Entered number is: %d\n", StringToInt(str));
#else
    printf(" Entered number is: %u\n", StringToUnsignedInt(str) );
#endif
    if( detect_buffer_overrun )
        printf( "Input buffer overflow!\n" );

    return 0;
}
_
10
Michaelangel007

getsを使用しないでください。 fgetsを使用する場合は、単に改行を上書きできます。

char *result = fgets(str, sizeof(str), stdin);
char len = strlen(str);
if(result != NULL && str[len - 1] == '\n')
{
  str[len - 1] = '\0';
}
else
{
  // handle error
}

これは、埋め込まれたNULLがないことを前提としています。別のオプションはPOSIX getline

char *line = NULL;
size_t len = 0;
ssize_t count = getline(&line, &len, stdin);
if(count >= 1 && line[count - 1] == '\n')
{
  line[count - 1] = '\0';
}
else
{
  // Handle error
}

getlineの利点は、割り当てと再割り当てを行い、埋め込みNULLを処理し、カウントを返すため、strlenで時間を無駄にする必要がないことです。 getlineでは配列を使用できないことに注意してください。ポインターはNULLまたはフリーである必要があります。

scanfでどのような問題が発生しているかわかりません。

4

gets()を使用しないでください。予測できないオーバーフローが発生する可能性があります。文字列配列のサイズが1000で、1001文字を入力すると、プログラムをバッファオーバーフローできます。

3
Peter Miehle

CharToInt()のこの修正バージョンでfgets()を使用してみてください。

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '\0')
    {
        if (isdigit(*(s+i)))
        {
            temp = *(s+i) & 15;
            result = (temp + result) * 10;
        }
        i++;
    }

    return result / 10;
}

基本的に、入力数字を検証し、それ以外は無視します。これは非常に粗雑なので、味と塩を変更します。

1
Amardeep AC9MF