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()函数时,程序运行良好。

其次,使用 时fgets(),结果略有错误,因为显然fgets()函数最后读取换行符(ASCII 值 10)字符,这搞砸了结果。

第三,使用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

是的,你想避免gets. fgets如果缓冲区足够大以容纳它,将始终读取换行符(这让您知道缓冲区何时太小并且还有更多行等待读取)。如果您想要类似的东西fgets不会读取换行符(丢失缓冲区太小的指示),您可以使用fscanf扫描集转换,例如:"%N[^\n]",其中“N”被缓冲区大小替换 - 1 .

读取后从缓冲区中删除尾随换行符的一种简单(如果奇怪)方法fgets是:strtok(buffer, "\n");这不是strtok预期的使用方式,但我以这种方式使用它的频率高于预期的方式(其中我一般避免)。

于 2010-07-21T18:01:17.640 回答
12

这段代码有很多问题。我们将修复命名错误的变量和函数并调查问题:

  • 首先,CharToInt()应该重命名为正确的StringToInt(),因为它对字符串而不是单个字符进行操作。

  • 函数CharToInt()[sic.] 是不安全的。它不检查用户是否不小心传入了 NULL 指针。

  • 它不验证输入,或者更准确地说,跳过无效输入。如果用户输入非数字,结果将包含虚假值。ie如果你输入N代码*(s+i) & 15会产生14!?

  • 接下来,应该调用 [sic.]temp中的非描述性,因为它确实是这样。CharToInt()digit

  • 此外,kludgereturn result / 10;就是这样——解决有缺陷的实现的一个糟糕的hack 。

  • 同样MAX的名字很糟糕,因为它可能看起来与标准用法冲突。IE#define MAX(X,y) ((x)>(y))?(x):(y)

  • 详细*(s+i)不如简单易读*s。没有必要使用另一个临时索引来混淆代码i

获取()

这很糟糕,因为它可能会溢出输入字符串缓冲区。例如,如果缓冲区大小为 2,而您输入 16 个字符,则会溢出str

扫描函数()

这同样糟糕,因为它会溢出输入字符串缓冲区。

您提到“使用 scanf() 函数时,结果完全错误,因为第一个字符显然具有 -52 ASCII 值。

这是由于 scanf() 的错误使用造成的。我无法复制此错误。

fgets()

这是安全的,因为您可以通过传入缓冲区大小(包括 NULL 的空间)来保证永远不会溢出输入字符串缓冲区。

获取线()

一些人建议使用 C POSIX 标准 getline()作为替代。不幸的是,这不是一个实用的可移植解决方案,因为微软没有实现 C 版本;只有标准 C++字符串模板函数作为这个 SO #27755191问题的答案。Microsoft 的 C++getline()至少早在Visual Studio 6中就可以使用,但由于 OP 严格询问 C 而不是 C++,因此这不是一个选项。

杂项。

最后,这个实现是错误的,因为它没有检测到整数溢出。如果用户输入的数字太大,数字可能会变成负数!即9876543210会变成-18815698?!让我们也解决这个问题。

对于unsigned int. 如果前一个部分数小于当前部分数,那么我们已经溢出,我们返回前一个部分数。

对于 asigned int这是一个多一点的工作。在汇编中我们可以检查进位标志,但在 C 语言中没有标准的内置方法来检测带符号 int 数学的溢出。幸运的是,由于我们乘以一个常数 ,* 10如果我们使用等价方程,我们可以很容易地检测到这一点:

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

如果 x*8 溢出,那么逻辑上 x*10 也会溢出。当 x*8 = 0x100000000 时会发生 32 位 int 溢出,因此我们需要做的就是检测 x >= 0x20000000 的时间。由于我们不想假设有多少位,int我们只需要测试是否设置了前 3 个 msb(最高有效位)。

此外,还需要进行第二次溢出测试。如果在数字连接之后设置了 msb(符号位),那么我们也知道数字溢出。

代码

这是一个固定的安全版本以及可以用来检测不安全版本中的溢出的代码。我signedunsigned通过#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。另一种选择是 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

于 2010-07-21T17:59:50.423 回答
3

永远不要使用gets(),它会导致不可预测的溢出。如果您的字符串数组大小为 1000 并且我输入 1001 个字符,我可以缓冲区溢出您的程序。

于 2010-07-21T18:05:41.833 回答
1

尝试将 fgets() 与您的 CharToInt() 的此修改版本一起使用:

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 回答
-3

So I am not much of a programmer but let me try to answer your question about the scanf();. I think the scanf is pretty fine and use it for mostly everything without having any issues. But you have taken a not completely correct structure. It should be:

char str[MAX];
printf("Enter some text: ");
scanf("%s", &str);
fflush(stdin);

The "&" in front of the variable is important. It tells the program where (in which variable) to save the scanned value. the fflush(stdin); clears the buffer from the standard input (keyboard) so you're less likely to get a buffer overflow.

And the difference between gets/scanf and fgets is that gets(); and scanf(); only scan until the first space ' ' while fgets(); scans the whole input. (but be sure to clean the buffer afterwards so you wont get an overflow later on)

于 2014-12-02T08:25:15.690 回答