关于C#:C-scanf()vs gets()vs fgets()

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

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

完成后,我注意到一些我无法回答的非常奇怪的"错误",主要是因为我对scanf()gets()fgets()函数的工作原理了解有限。 (不过,我确实读过很多文献。)

因此,无需编写太多文本,这是程序的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#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
"
, 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()函数有何处理?


  • 切勿使用gets。它没有针对缓冲区溢出漏洞的保护措施(也就是说,您无法告诉它传递给它的缓冲区有多大,因此它不能阻止用户输入比缓冲区大的行并破坏内存)。

  • 避免使用scanf。如果使用不当,它可能会出现与gets相同的缓冲区溢出问题。即使忽略这一点,它也存在其他问题,导致难以正确使用。

  • 通常,您应该使用fgets代替,尽管有时不方便(您必须去除换行符,必须提前确定缓冲区大小,然后必须弄清楚如何处理太长的行,请保留该部分您会读取并丢弃多余的内容,丢弃整个内容,动态增加缓冲区并重试,等等)。有一些非标准功能可以为您执行此动态分配(例如,POSIX系统上的getline,Chuck Falconer的公共域ggets功能)。请注意,ggets具有类似gets的语义,因为它为您去除了结尾的换行符。


是的,您要避免使用gets。如果缓冲区足够大,fgets将始终读取换行符(这将使您知道何时缓冲区太小,并且还有更多行等待读取)。如果您想要像fgets这样的东西不会读取换行符(丢失指示缓冲区太小的指示),则可以将fscanf与扫描集转换一起使用,例如:"%N[^
]"
,其中'N '替换为缓冲区大小-1。

fgets读取后,从缓冲区中删除尾随换行符的一种简单(如果很奇怪)的方法是:strtok(buffer,"
");
这不是打算使用strtok的方式,但是我以这种方式使用了它比预期的方式(我通常会避免)更多。


此代码存在许多问题。我们将修复名称错误的变量和函数,并研究问题:

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

  • 功能CharToInt() [sic。]不安全。它不会检查用户是否意外传入了NULL指针。

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

  • 接下来,应该将CharToInt() [sic。]中的非描述temp称为digit,因为这实际上就是它。

  • 同样,kludge 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 ++字符串模板才有此功能#27755191问题答案。微软的C ++ getline()至少可以追溯到Visual Studio 6,但是由于OP严格询问C而不是C ++,因此这不是一个选择。

杂项

最后,此实现存在错误,因为它无法检测到整数溢出。如果用户输入的数字太大,该数字可能变为负数!即9876543210将变为-18815698 ?!让我们也修复它。

这对于修复unsigned int而言是微不足道的。如果先前的部分号小于当前的部分号,则我们溢出并返回先前的部分号。

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

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

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

此外,还需要第二次溢出测试。如果在数字级联之后将MSB置1(符号位),则我们也知道数字溢出。

这是一个固定的安全版本,以及可用于检测不安全版本中溢出的代码。我还通过#define SIGNED 1包括了signedunsigned版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#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
"
, StringToInt(str));
#else
    printf(" Entered number is: %u
"
, StringToUnsignedInt(str) );
#endif
    if( detect_buffer_overrun )
        printf("Input buffer overflow!
"
);

    return 0;
}


您是正确的,永远不要使用gets。如果要使用fgets,则可以简单地覆盖换行符。

1
2
3
4
5
6
7
8
9
10
11
char *result = fgets(str, sizeof(str), stdin);
char len = strlen(str);
if(result != NULL && str[len - 1] == '
'
)
{
  str[len - 1] = '\0';
}
else
{
  // handle error
}

这确实假定没有嵌入式NULL。另一个选项是POSIX getline

1
2
3
4
5
6
7
8
9
10
11
12
char *line = NULL;
size_t len = 0;
ssize_t count = getline(&line, &len, stdin);
if(count >= 1 && line[count - 1] == '
'
)
{
  line[count - 1] = '\0';
}
else
{
  // Handle error
}

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

我不确定您在使用scanf遇到什么问题。


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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

它实际上是验证输入数字,并忽略其他任何内容。这是非常粗糙的,所以将其修改并加盐调味。


因此,我不是一名程序员,但是让我尝试回答您有关scanf();的问题。我认为scanf很好,几乎可以将其用于所有内容,而不会出现任何问题。但是您采用的结构并不完全正确。它应该是:

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

变量前面的"&"很重要。它告诉程序将扫描值保存在哪个位置(在哪个变量中)。
fflush(stdin);从标准输入(键盘)清除缓冲区,因此您不太可能出现缓冲区溢出。

gets / scanf和fgets之间的区别是gets();scanf();仅扫描到第一个空格' ',而fgets();扫描整个输入。 (但请确保稍后清洗缓冲区,以免以后再溢出)