C – scanf()vs gets()vs fgets()
我一直在做一个相当简单的程序转换string(假设数字input)到一个整数。
完成之后,我注意到一些非常奇怪的“bug”,我不能回答,主要是因为我对scanf()
, gets()
和fgets()
函数的工作知之甚less。 (尽pipe我读过很多文献。)
所以不用写太多的文字,这里是程序的代码:
#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()
函数的处理是什么?
-
切勿使用
gets
。 它不提供对缓冲区溢出漏洞的保护(也就是说,你不能告诉它传递给它的缓冲区有多大,所以它不能阻止用户input大于缓冲区和破坏内存的行)。 -
避免使用
scanf
。 如果不小心使用,它可能会得到相同的缓冲区溢出问题。 即使忽视这一点, 也有其他问题,使得难以正确使用 。 -
一般来说,你应该使用
fgets
代替,尽pipe有时不方便(你必须去掉换行符,你必须提前确定一个缓冲区大小,然后你必须弄清楚如何处理太长的行 – 你保留部分你读取和放弃多余的东西,丢弃整个东西,dynamic增长缓冲区,然后再试一次,等等)。 有一些非标准的function可以为你做这个dynamic分配(例如,POSIX系统上的getline
, Chuck Falconer的公共域ggets
函数)。 请注意,ggets
具有ggets
语义的特性,因为它为您提供了一个尾随的换行符。
是的,你想避免gets
。 如果缓冲区足够大以容纳它(它可以让你知道什么时候缓冲区太小,还有更多的线路在等待读取), fgets
将总是读取新行。 如果你想要像fgets
这样的不会读取新行的东西(失去了一个太小的缓冲区的指示),你可以使用fscanf
和扫描集转换,如: "%N[^\n]"
,其中'N'被缓冲区大小-1replace。
用fgets
读取后,从缓冲区中删除尾随的新行的一种简单的方法是: strtok(buffer, "\n");
这不是打算如何使用strtok
,但我已经比预期的方式(我通常避免)更频繁地使用它。
这个代码有很多问题。 我们将修复命名不定的variables和函数,并调查问题:
-
首先,
CharToInt()
应该重命名为正确的StringToInt()
因为它对一个string操作而不是单个字符。 -
函数
CharToInt()
[原文如此]是不安全的。 它不检查用户是否意外地传递了一个NULL指针。 -
它不validationinput,或者更正确地说,跳过无效input。 如果用户input一个非数字,结果将包含一个假的值。 即如果你input
N
代码*(s+i) & 15
将产生14! -
接下来,
CharToInt()
[原文如此]中的不伦不类的temp
应该被称为digit
因为这就是它的真正含义。 -
此外,kludge
return result / 10;
就是这样 – 绕开一个错误的实现是一个糟糕的黑客攻击 。 -
同样,由于
MAX
可能看起来与标准用法相冲突,因此被错误地命名。 即#define MAX(X,y) ((x)>(y))?(x):(y)
-
详细的
*(s+i)
不像*s
那样可读。 没有必要用另一个临时索引i
来使用和混淆代码。
得到()
这是不好的,因为它可以溢出inputstring缓冲区。 例如,如果缓冲区大小为2,并且input16个字符,则会使str
溢出。
scanf()函数
这同样不好,因为它可能溢出inputstring缓冲区。
你提到“ 当使用scanf()函数时,结果是完全错误的,因为第一个字符显然有一个-52的ASCII值。
这是由于scanf()的使用不正确。 我无法复制这个错误。
与fgets()
这是安全的,因为你可以保证你永远不会溢出inputstring缓冲区通过传入缓冲区大小(其中包括NULL空间)。
函数getline()
有几个人提出了C POSIX标准的 getline()
作为替代。 不幸的是,这不是一个实用的便携式解决scheme,因为微软没有实施C版本; 只有标准的C ++ string模板函数,因为这个SO #27755191问题的答案。 微软的C ++ getline()
至less可以在Visual Studio 6中使用,但是由于OP严格要求关于C而不是C ++,所以这不是一个选项。
杂项。
最后,这个实现是错误的,因为它不检测整数溢出。 如果用户input的号码太大,号码可能会变成负数! 即9876543210
将变成-18815698
我们来解决这个问题。
这对于解决一个unsigned int
是微不足道的。 如果前面的部分数小于当前的部分数,那么我们溢出了,我们返回前面的部分数。
对于一个有signed int
这是一个更多的工作。 在汇编中,我们可以检查进位标志,但是在C中没有标准的内置方法来检测带符号整型math的溢出。 幸运的是,由于我们乘以一个常数* 10
,所以如果我们使用一个等价的方程,我们可以很容易地检测到:
n = x*10 = x*8 + x*2
如果x * 8溢出,那么逻辑x * 10也是如此。 当x * 8 = 0x100000000时,32位int溢出将会发生,因此当x> = 0x20000000时,我们需要做的就是检测。 由于我们不想假定int
有多less位,所以我们只需要testing是否设置了前3个msb(最高有效位)。
此外,还需要进行第二次溢出testing。 如果在数字连接之后msb被设置(符号位),那么我们也知道数字溢出。
码
这里是一个固定的安全版本以及可以用来检测不安全版本溢出的代码。 我还通过#define SIGNED 1
包含了signed
和unsigned
版本
#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; }
你是正确的,你永远不应该使用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 }
这确实假定没有embedded的NULL。 另一种select是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
的好处是它为你分配和重新分配,它处理可能的embeddedNULL,并返回计数,所以你不必浪费时间strlen
。 请注意,您不能在getline
使用数组。 指针必须是NULL
或可用的。
我不确定你在使用scanf
遇到了什么问题。
从来没有使用gets(),它可能导致不可改变的溢出。 如果你的string数组的大小是1000,而且input的是1001个字符,我可以缓冲你的程序溢出。
尝试使用您的CharToInt()的修改版本使用fgets():
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; }
它本质上validationinput数字,并忽略其他任何东西。 这是非常粗糙的,所以修改它和盐的味道。
所以我不是一个程序员,但让我试着回答你的问题关于scanf();
。 我认为scanf是相当不错的,主要用于一切,没有任何问题。 但是你采取了一个不完全正确的结构。 它应该是:
char str[MAX]; printf("Enter some text: "); scanf("%s", &str); fflush(stdin);
variables前面的“&”很重要。 它告诉程序在哪里(在哪个variables中)保存扫描值。 fflush(stdin);
从标准input(键盘)清除缓冲区,所以你不太可能得到缓冲区溢出。
gets / scanf和fgets的区别在于gets();
和scanf();
只有扫描,直到第一个空格' '
而fgets();
扫描整个input。 (但是请务必在之后清理缓冲区,以便以后不会发生溢出)