44

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

終わった後、私は答えることができないいくつかの非常に独特な「バグ」に気づきました。これは主に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()関数を使用する場合、プログラムは完全に機能します。

次に、を使用する場合、関数が最後に改行(ASCII値10)文字を読み取りfgets()、結果を台無しにするため、結果はわずかに間違っています。fgets()

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

これは使用が推奨されていないことがわかったので、ここで改行文字を読み取らない(または無視しない)ようにgets()使用できるかどうかを知りたいと思います。fgets()また、scanf()このプログラムの機能はどうなっていますか?

4

7 に答える 7

31
  • 絶対に使用しないでくださいgets。バッファ オーバーフローの脆弱性に対する保護は提供されません (つまり、渡すバッファのサイズがわからないため、ユーザーがバッファよりも大きな行に入力してメモリを破壊するのを防ぐことはできません)。

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

  • 通常はfgets代わりに使用する必要がありますが、不便な場合もあります (改行を削除する必要があり、事前にバッファー サイズを決定する必要があり、長すぎる行をどうするかを考えなければなりません。余分なものを読み取って破棄する、全体を破棄する、バッファを動的に拡張して再試行するなど)。この動的な割り当てを行う非標準関数がいくつか利用可能です (たとえばgetline、POSIX システムでは、Chuck Falconer のパブリック ドメインggets関数)。末尾の改行を削除するという点で、に似たセマンティクスがあることに注意しggetsてください。gets

于 2010-07-21T18:37:15.267 に答える
20

はい、避けたいですgetsfgetsバッファがそれを保持するのに十分な大きさである場合、常に改行を読み取ります(これにより、バッファが小さすぎて、読み取り待ちの行がさらにあることがわかります)。fgetsそのようなものが改行を読み取らないようにしたい場合(小さすぎるバッファーの兆候を失う) 、次fscanfのようなスキャン セット変換を使用できます。 "%N[^\n]".

読み取り後にバッファーから末尾の改行を削除する簡単な (奇妙ではあるが) 方法の 1 つは、次のfgetsとおりです。私は基本的に避けます)。strtok(buffer, "\n");strtok

于 2010-07-21T18:01:17.640 に答える
12

このコードには多くの問題があります。不適切な名前の変数と関数を修正し、問題を調査します。

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

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

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

  • 次に、[sic.] の nondescript を呼び出す必要がtempあります。CharToInt()digit

  • また、クラッジはまさにそれです-バグのある実装を回避するためreturn result / 10;の悪いハックです。

  • 同様MAXに、標準的な使用法と競合するように見える可能性があるため、不適切な名前が付けられています。すなわち#define MAX(X,y) ((x)>(y))?(x):(y)

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

取得()

入力文字列バッファがオーバーフローする可能性があるため、これは良くありません。例えば、バッファサイズが 2 の場合、16 文字で入力するとオーバーフローしstrます。

scanf()

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

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

これは、scanf() の使い方が間違っているためです。このバグを再現できませんでした。

fgets()

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

getline()

C POSIX 標準 getline()を代替として提案する人もいます。残念ながら、Microsoft は C バージョンを実装していないため、これは実用的な移植可能なソリューションではありません。この SO #27755191 の質問の回答として、標準の C++文字列テンプレート関数のみ。Microsoft の C++は、少なくともVisual Studio 6までさかのぼって利用できましたが、OP は C++ ではなく C について厳密に質問しているため、これはオプションではありません。getline()

その他

最後に、この実装は整数オーバーフローを検出しないという点でバグがあります。ユーザーが入力した数値が大きすぎると、数値が負になることがあります。すなわち9876543210なる-18815698でしょう?それも直しましょう。

これを修正するのは簡単unsigned intです。前の部分数が現在の部分数より小さい場合は、オーバーフローしており、前の部分数を返します。

の場合、signed intこれはもう少し作業です。アセンブリではキャリー フラグを調べることができますが、C では、signed int 演算でオーバーフローを検出する標準の組み込み方法がありません。幸いなことに、定数 を掛けている* 10ため、同等の方程式を使用すると、これを簡単に検出できます。

n = x*10 = x*8 + x*2

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

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

コード

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

#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;
}
于 2015-07-08T20:49:35.283 に答える
5

絶対に使用しないでください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 が埋め込まれていないことを前提としています。別のオプションは POSIXgetlineです:

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

于 2010-07-21T17:59:50.423 に答える
3

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

于 2010-07-21T18:05:41.833 に答える
1

この修正版の 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;
}

基本的に、入力数字を検証し、それ以外は無視します。これは非常に粗いので、それを修正し、塩で味を調えます。

于 2010-07-21T18:03:15.643 に答える