我一直在做一个相当简单的程序,将一串字符(假设输入数字)转换为整数。
在我完成之后,我注意到一些我无法回答的非常特殊的“错误”,主要是因为我对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()
函数的处理是什么?
答案 0 :(得分:25)
从不使用gets
。它没有提供针对缓冲区溢出漏洞的保护(也就是说,您无法告诉它传递给它的缓冲区有多大,因此它无法阻止用户输入大于缓冲区和破坏内存的行)。
避免使用scanf
。如果不小心使用,它可能会遇到与gets
相同的缓冲区溢出问题。甚至忽略了it has other problems that make it hard to use correctly。
一般情况下你应该使用fgets
,虽然它有时不方便(你必须剥离换行符,你必须提前确定缓冲区大小,然后你必须弄清楚如何处理行这太长了 - 你保留你阅读的部分和discard the excess,丢弃整个事物,动态增长缓冲区并再试一次,等等。有一些非标准函数可以为您进行动态分配(例如POSIX系统上的getline
,Chuck Falconer's public domain ggets
函数)。请注意,ggets
具有gets
- 就像语义一样,它会为您删除一个尾随换行符。
答案 1 :(得分:19)
是的,您要避免gets
。 fgets
将始终读取新行,如果缓冲区大到足以容纳它(这会让你知道缓冲区何时太小而且有更多的行等待读取)。如果您希望fgets
之类的内容无法读取新行(丢失了太小缓冲区的指示),则可以使用fscanf
进行扫描集转换,例如:{{1} },其中'N'被缓冲区大小-1替换。
使用"%N[^\n]"
阅读后,从缓冲区中删除尾随换行符的一种简单(如果很奇怪)方法是:fgets
这不是strtok(buffer, "\n");
的使用方式,但我用这种方式比预期的方式更常用(我通常会避免)。
答案 2 :(得分:10)
此代码存在众多问题。我们将修复命名错误的变量和函数并调查问题:
首先,CharToInt()
应该重命名为正确的StringToInt()
,因为它在字符串上运行而不是单个字符。
函数CharToInt()
[sic。]不安全。它不会检查用户是否意外地传入了NULL指针。
它不会验证输入,或者更准确地说,它会跳过无效输入。如果用户输入非数字,则结果将包含虚假值。即如果您输入N
,则代码*(s+i) & 15
将产生14!
接下来,temp
[原文如此]中的不起眼的CharToInt()
应该被称为digit
,因为它实际上就是这样。
此外,kludge return result / 10;
只是 - 一个糟糕的 hack 来解决有缺陷的实施。
同样MAX
名称错误,因为它似乎与标准用法冲突。即#define MAX(X,y) ((x)>(y))?(x):(y)
详细*(s+i)
不像简单*s
那样可读。没有必要使用另一个临时索引i
来使用和混乱代码。
这很糟糕,因为它可能会溢出输入字符串缓冲区。例如,如果缓冲区大小为2,并且您输入16个字符,则会溢出str
。
这同样很糟糕,因为它可能会溢出输入字符串缓冲区。
你提到&#34; 使用scanf()函数时,结果完全错误,因为第一个字符显然有-52 ASCII值。&#34;
这是由于scanf()的使用不正确。我无法复制这个错误。
这是安全的,因为您可以保证永远不会通过传入缓冲区大小(包括NULL空间)来溢出输入字符串缓冲区。
有些人建议将C POSIX standard getline()
作为替代品。不幸的是,这不是一个实用的便携式解决方案,因为微软没有实现C版本;只有标准的C ++ string template function才会回答这个问题#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
如果x * 8溢出,那么逻辑上也是x * 10。对于32位int溢出将在x * 8 = 0x100000000时发生,因此我们需要做的就是在x> = 0x20000000时进行检测。由于我们不想假设int
有多少位,我们只需要测试是否设置了前3个msb(最高有效位)。
此外,还需要进行第二次溢出测试。如果在数字连接之后设置msb(符号位),那么我们也知道数字溢出。
这是一个固定的安全版本以及可以用来检测不安全版本中的溢出的代码。我还通过signed
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;
}
答案 3 :(得分:4)
你应该永远不要使用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
时遇到了什么问题。
答案 4 :(得分:3)
永远不会使用gets(),它会导致无法解决的溢出问题。如果您的字符串数组大小为1000并且我输入1001个字符,我可以缓冲溢出您的程序。
答案 5 :(得分: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;
}
它基本上验证输入数字并忽略其他任何内容。这是非常粗糙的,所以修改它和盐味道。
答案 6 :(得分:-2)
所以我不是一个程序员,但让我试着回答你关于scanf();
的问题。我认为scanf非常好,并且在没有任何问题的情况下将其用于大多数情况。但是你采取了一种不完全正确的结构。它应该是:
char str[MAX];
printf("Enter some text: ");
scanf("%s", &str);
fflush(stdin);
“&amp;”在变量前面很重要。它告诉程序保存扫描值的位置(在哪个变量中)。
fflush(stdin);
从标准输入(键盘)清除缓冲区,因此您不太可能获得缓冲区溢出。
获取/ scanf和fgets之间的区别在于gets();
和scanf();
只扫描到第一个空格' '
,而fgets();
扫描整个输入。 (但一定要事后清理缓冲区,以免后来出现溢出)