C语言中,scanf()、gets()和fgets()有什么区别?

49

我一直在做一个相当简单的程序,将字符串(假设输入的是数字)转换为整数。

完成后,我注意到了一些非常奇怪的“错误”,因为我的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()函数有什么问题呢?


3
在C语言中,char类型可以由编译器自行决定是signed char还是unsigned char。 - ninjalj
1
我猜你可能得自己写代码。至于为什么使用unsigned char解决了你的问题:一个普通(有符号)char的值范围是-128到127,而一个unsigned char的范围是0到255。位运算可能会对负值产生奇怪的影响。 - sigint
5
顺便提一下,在C语言中,*(s+i)通常写成s[i](它们具有完全相同的语义)。 - caf
atoiCharToInt都存在一个问题,即如果要转换的数字大于INT_MAX,则会导致未定义的行为。为了解决这个问题,您可以使用strtol系列中的函数,或修改CharToInt,使其在溢出而不是溢出时退出。(实际上,CharToInt还需要进一步修改;按照当前的编写方式,它只能读取到INT_MAX / 10;如果输入非数字,则会出现奇怪的情况) - M.M
你应该测试输入函数是否成功,并且只有在函数指示成功时才使用数据。如果输入中出现异常情况,第一步是清晰地打印出你所得到的内容:printf("Input: [[%s]]\n", str);。转换函数通常会跳过前导空格并停止于第一个不能成为数字的字符。如果只有尾随空格(特别是换行符),通常不会生成错误。如果转换后的字符串后面还有其他非数字字符,则可能会生成错误,也可能不会。 - Jonathan Leffler
显示剩余3条评论
7个回答

33
  • 切勿使用 gets。它无法防止缓冲区溢出漏洞(也就是你无法告诉它你传递给它的缓冲区有多大,因此它不能阻止用户输入比缓冲区更大的行并破坏内存)。

  • 避免使用scanf。如果使用不当,它可能会像gets一样出现缓冲区溢出问题。即使忽略这一点,它还有其他问题使其难以正确使用

  • 通常情况下,应该使用fgets代替,尽管有时候会有些不方便(你必须去掉换行符,必须预先确定缓冲区大小,然后还必须想办法处理太长的行——是保留读取的部分和舍弃多余部分,舍弃整个字符串,动态增加缓冲区并重新尝试等)。有一些非标准函数可用于为您执行此动态分配(例如,在POSIX系统上的getlineChuck Falconer的公共领域ggets函数)。请注意,ggets类似gets的语义,在为您删除末尾的换行符。


正如我在我的回答中所说,getline现在是标准的。 - Matthew Flaschen
7
哪个标准?当我说“非标准”时,指的是“非标准C”,而不是非POSIX。 - jamesdlin

20

是的,你要避免使用 gets。如果缓冲区足够大,fgets总会读取换行符(这样就能知道缓冲区太小而且还有更多待读取的行)。如果你需要像 fgets 这样的函数但不想读取换行符(失去了缓冲区过小的指示),你可以使用带有扫描集转换的 fscanf,例如: "%N[^\n]",其中 'N' 被缓冲区大小-1替换。

一种简单(但有点奇怪)的方法是在使用 fgets 读取缓冲区后删除尾随的换行符: strtok(buffer, "\n"); 这并不是 strtok 的预期用法,但我经常这样用(通常避免使用它的预期用法)。


12
这段代码存在众多问题。我们将修复变量和函数的不良命名,并调查问题:
  • 首先,CharToInt() 应该被重命名为适当的 StringToInt(),因为它操作的是一个字符串而不是单个字符。

  • CharToInt() 函数不安全。它没有检查用户是否意外传递了一个空指针。

  • 它不验证输入,或者更正确地说,跳过无效的输入。如果用户输入了非数字,则结果将包含错误值。例如,如果您输入 N,则代码 *(s+i) & 15 将产生 14!?

  • 接下来,在 CharToInt() 中不明确的 temp 应该被称为 digit,因为那才是它真正的含义。

  • 此外,CharToInt() 中的巧妙解决方案 return result / 10; 只是一个糟糕的补救措施,以解决有缺陷的实现问题。

  • 同样,MAX 的命名不恰当,因为它可能与标准用法冲突。即 #define MAX(X,y) ((x)>(y))?(x):(y)

  • 冗长的 *(s+i) 不如简单的 *s 读起来清晰。没有必要使用并且用另一个临时索引 i 来混淆代码。

gets()

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

scanf()

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

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

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

fgets()

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

getline()

一些人建议使用C的POSIX标准getline()来替代。不幸的是,这并不是一个实用的可移植解决方案,因为Microsoft没有实现C版本,只有标准C++ string模板函数,正如SO #27755191问题所回答的那样。尽管Microsoft的C++ getline()至少早在Visual Studio 6就已经存在,但由于OP严格要求使用C而不是C ++,因此这不是一个选项。

其他

最后,这个实现有一个漏洞,它无法检测整数溢出。如果用户输入的数字太大,数字可能会变成负数!例如9876543210将变成-18815698?让我们也修复这个问题。

对于无符号整数unsigned int,这个问题很容易解决。如果之前的部分数字小于当前部分数字,那么我们就发生了溢出,然后返回之前的部分数字。

对于有符号整数signed int,需要做更多的工作。在汇编语言中,我们可以检查进位标志,但在C语言中,没有标准内置的方法来检测有符号整数计算的溢出。幸运的是,由于我们乘以一个常数* 10,如果使用等效方程,我们就可以轻松地检测到这一点:

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

如果x*8溢出,那么逻辑上x*10也会溢出。对于32位int类型,当x*8=0x100000000时会发生溢出,因此我们只需要检测x是否大于等于0x20000000。由于我们不想假设int类型有多少位,所以我们只需要测试前三个最高有效位是否设置。
此外,需要进行第二次溢出测试。如果数字连接后msb(符号位)被设置,则我们也知道该数字已经溢出。
代码
这里是一个修复后的安全版本,以及您可以使用的代码,以检测不安全版本中的溢出。我还通过#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;
}

strlen()函数不会检查您是否传递了空指针。标准C库规范明确说明(§7.1.4使用库函数):如果函数的参数具有无效值(例如函数域之外的值,程序地址空间之外的指针,空指针或指向非可修改存储器的指针,而相应的参数未被const限定),或者类型(在提升后)与具有可变数量参数的函数所期望的类型不同,则行为是未定义的。 要求非空指针是合理的。 - Jonathan Leffler
最好添加一行安全检查并捕获粗心的错误,而不是假设调用方不会犯错,但感谢规范的详细说明! - Michaelangel007

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时遇到了什么问题。

如果输入行的长度实际上等于sizeof(str) - 1或更长,则不会有换行符,因此该代码将舍弃一个有效的字符。 - M.M
@MattMcNabb,谢谢。我想我修好了(未测试)。 - Matthew Flaschen
1
请注意,在检查fgets()返回非空值之前,不要使用strlen(str) - Jonathan Leffler

3

永远不要使用gets()函数,它可能导致不可预测的溢出。如果您的字符串数组大小为1000,而我输入了1001个字符,那么就会发生缓冲区溢出。


感谢你们的答案,它们非常有帮助。但我也想知道为什么这个程序中 scanf() 不起作用?谢谢。 - Marko

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;
}

它基本上验证输入的数字并忽略其他内容。这很简单,所以可以根据需要进行修改和调整。


为了清理代码,考虑使用isdigit()替换strchr()。更好的做法是用atoi()调用来替换整个CharToInt()函数。 http://www.cplusplus.com/reference/clibrary/cctype/isdigit/ http://www.cplusplus.com/reference/clibrary/cstdlib/atoi/ - sigint

-3

我不是很擅长编程,但让我试着回答你关于scanf();的问题。我认为scanf非常好用,几乎可以用它来处理所有事情而不会出现任何问题。但你使用了一个不完全正确的结构。应该是:

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

变量前面的"&"很重要。它告诉程序在哪个变量中保存扫描到的值。 fflush(stdin);清除标准输入(键盘)缓冲区,这样你就不太可能遇到缓冲区溢出。

gets();scanf();只会扫描到第一个空格' ',而fgets();会扫描整个输入。(但一定要在之后清除缓冲区,以免后续出现溢出)


在C语言中,数组是通过指针传递的,因此在str前面省略&是完全可以的。也就是说,scanf("%s", str);scanf("%s", &str[0]);是完全等价的。 - Michaelangel007
这个答案在多个方面都是错误的,而且很危险。 - siride
准确来说:(1) str 前面的 & 不是必需的,有些编译器会产生警告;(2) 应该测试 scanf() 返回的值以确保你得到了期望的数据;(3) 使用 fflush(stdin) 不受标准 C 支持——它只在某些平台上有效,尤其是 Microsoft 平台;(4) gets() 读取到行末(没有任何防止溢出的保护);(5) fgets() 不会扫描整个输入——它会读取到行末或者直到缓冲区没有剩余空间;(6) scanf() 可能会导致缓冲区溢出——如果 MAX==100,请使用 scanf("%99s", str) - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接