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

Mar*_*rko 36 c gets scanf 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;
}
Run Code Online (Sandbox Code Playgroud)

所以这就是我一直遇到的问题.首先,使用gets()功能时,程序运行正常.

其次,使用时fgets(),结果有点错误,因为显然fgets()函数会读取最后一行的新行(ASCII值10)字符,这会搞砸结果.

第三,使用scanf()函数时,结果完全错误,因为第一个字符显然具有-52 ASCII值.为此,我没有解释.

现在我知道gets()不鼓励使用,所以我想知道我是否可以fgets()在这里使用它所以它不会读取(或忽略)换行符.另外,scanf()这个程序中的功能是什么?

jam*_*lin 27

  • 千万不要使用gets.它没有提供针对缓冲区溢出漏洞的保护(也就是说,您无法告诉它传递给它的缓冲区有多大,因此它无法阻止用户输入大于缓冲区和破坏内存的行).

  • 避免使用scanf.如果不小心使用,它可能会遇到与缓冲区溢出问题相同的问题gets.即使忽略这一点,它也有其他问题,使其难以正确使用.

  • 通常你应该使用fgets,虽然它有时不方便(你必须剥离换行符,你必须提前确定缓冲区大小,然后你必须弄清楚如何处理太长的行 - 你保持你的部分你读取并丢弃多余的内容,丢弃整个内容,动态增长缓冲区并再试一次,等等.有一些非标准功能可以为您进行动态分配(例如,getline在POSIX系统上,Chuck Falconer的公共域ggets功能).请注意,它ggets具有gets类似语义,因为它为您删除了一个尾随换行符.

  • @Matthew Flaschen:哪个标准?当我说"非标准"时,我指的是"非标准C",而不是非POSIX. (6认同)

Jer*_*fin 19

是的,你想避免gets.fgets总是会读取新行如果缓冲区是大到足以容纳它(它可以让你知道什么时候该缓冲区太小,有更多的等待读行).如果你想要的东西一样fgets,不会读取新线(失去过小的缓冲器的该指示),可以使用fscanf与像扫描设置的转换:"%N[^\n]",其中"N"是由缓冲区大小更换- 1 .

一个简单的(如果怪)的方式与阅读后删除从一个缓冲后的新线fgets是:strtok(buffer, "\n");这不是如何strtok打算使用,但我这种方式更经常使用它比预期的方式(这我一般避免).


Mic*_*007 10

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

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

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

  • 它不验证输入,或更正确地,跳过无效输入.如果用户输入非数字,则结果将包含虚假值.即如果输入N代码*(s+i) & 15将产生14!

  • 接下来,应该调用[sic.]中的不成文temp,因为它实际上就是这样.CharToInt()digit

  • 此外,kludge return result / 10;只是 - 一个糟糕的黑客来解决一个错误的实施.

  • 同样名称MAX也很糟糕,因为它似乎与标准用法冲突.即#define MAX(X,y) ((x)>(y))?(x):(y)

  • 冗长*(s+i)并不像简单的那样可读*s.没有必要使用另一个临时索引来使用和混乱代码i.

得到()

这很糟糕,因为它可以溢出输入字符串缓冲区.例如,如果缓冲区大小为2,并且输入16个字符,则会溢出str.

scanf()函数

这同样很糟糕,因为它可以溢出输入字符串缓冲区.

你提到" 当使用scanf()函数时,结果是完全错误的,因为第一个字符显然有-52 ASCII值. "

这是由于scanf()的使用不正确.我无法复制这个错误.

与fgets()

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

函数getline()

一些人建议将C POSIX标准 getline()作为替代品.不幸的是,这不是一个实用的便携式解决方案,因为微软没有实现C版本; 只有标准的C++ 字符串模板函数,因此SO #27755191问题的答案.微软的C++ getline()至少可以追溯到Visual Studio 6,但由于OP严格要求C而不是C++,因此这不是一个选择.

杂项.

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

这对于修复来说是微不足道的unsigned int.如果前一个部分数小于当前部分数,那么我们已经溢出,我们返回前一个部分数.

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

n = x*10 = x*8 + x*2
Run Code Online (Sandbox Code Playgroud)

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

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

这是一个固定的安全版本以及您可以使用的代码来检测不安全版本中的溢出.我还包括了a signedunsigned版本的via#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;
}
Run Code Online (Sandbox Code Playgroud)


Mat*_*hen 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
}
Run Code Online (Sandbox Code Playgroud)

这确实假设没有嵌入的 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
}
Run Code Online (Sandbox Code Playgroud)

优点getline是它为您进行分配和重新分配,它处理可能的嵌入 NULL,并返回计数,因此您不必浪费时间使用strlen. 请注意,您不能将数组与getline. 指针必须是NULL或可自由的。

我不确定你遇到了什么问题scanf