C - scanf() vs gets() vs 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 。它没有针对缓冲区溢出漏洞的保护措施(也就是说,您无法告诉它传递给它的缓冲区有多大,因此它不能阻止用户输入比缓冲区大的行并破坏内存)。 -
避免使用
scanf 。如果使用不当,它可能会出现与gets 相同的缓冲区溢出问题。即使忽略这一点,它也存在其他问题,导致难以正确使用。 -
通常,您应该使用
fgets 代替,尽管有时不方便(您必须去除换行符,必须提前确定缓冲区大小,然后必须弄清楚如何处理太长的行,请保留该部分您会读取并丢弃多余的内容,丢弃整个内容,动态增加缓冲区并重试,等等)。有一些非标准功能可以为您执行此动态分配(例如,POSIX系统上的getline ,Chuck Falconer的公共域ggets 功能)。请注意,ggets 具有类似gets 的语义,因为它为您去除了结尾的换行符。
是的,您要避免使用
]"
用
");
此代码存在许多问题。我们将修复名称错误的变量和函数,并研究问题:
-
首先,应将
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个字符,则会溢出
scanf()
这同样很糟糕,因为它可能会使输入字符串缓冲区溢出。
您提到"使用scanf()函数时,结果完全错误,因为第一个字符显然具有-52 ASCII值。"
那是由于对scanf()的错误使用。我无法复制此错误。
fgets()
这是安全的,因为您可以通过传递缓冲区大小(其中包括NULL的空间)来确保您永远不会溢出输入字符串缓冲区。
getline()
少数人建议使用C POSIX标准
杂项
最后,此实现存在错误,因为它无法检测到整数溢出。如果用户输入的数字太大,该数字可能变为负数!即
这对于修复
对于
1 | n = x*10 = x*8 + x*2 |
如果x * 8溢出,则逻辑上x * 10也将溢出。对于32位int,当x * 8 = 0x100000000时将发生溢出,因此我们要做的就是检测x> = 0x20000000时的情况。由于我们不想假设
此外,还需要第二次溢出测试。如果在数字级联之后将MSB置1(符号位),则我们也知道数字溢出。
码
这是一个固定的安全版本,以及可用于检测不安全版本中溢出的代码。我还通过
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; } |
您是正确的,永远不要使用
1 2 3 4 5 6 7 8 9 10 11 |
这确实假定没有嵌入式NULL。另一个选项是POSIX
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 } |
我不确定您在使用
永远不要使用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; } |
它实际上是验证输入数字,并忽略其他任何内容。这是非常粗糙的,所以将其修改并加盐调味。
因此,我不是一名程序员,但是让我尝试回答您有关
变量前面的"&"很重要。它告诉程序将扫描值保存在哪个位置(在哪个变量中)。
gets / scanf和fgets之间的区别是