C – scanf () vs gets () versus fgets ()

He estado haciendo un progtwig bastante fácil de convertir una cadena de caracteres (suponiendo que se ingresen los números) en un entero.

Después de que terminé, noté algunos “errores” muy peculiares que no puedo responder, principalmente debido a mi conocimiento limitado de cómo funcionan las funciones scanf() , gets() y fgets() . (Aunque sí leí mucha literatura).

Entonces, sin escribir demasiado texto, aquí está el código del progtwig:

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

Así que aquí está el problema que he tenido. Primero, cuando se usa la función gets() , el progtwig funciona perfectamente.

En segundo lugar, cuando se utiliza fgets() , el resultado es ligeramente incorrecto porque aparentemente la fgets() lee el carácter de nueva línea (valor ASCII 10) al final, lo que daña el resultado.

En tercer lugar, cuando se utiliza la función scanf() , el resultado es completamente incorrecto porque el primer carácter aparentemente tiene un valor ASCII de -52. Para esto, no tengo ninguna explicación.

Ahora sé que se desaconseja usar fgets() , por lo que me gustaría saber si puedo usar fgets() aquí para no leer (o ignorar) el carácter de nueva línea. Además, ¿cuál es el problema con la función scanf() en este progtwig?

  • Nunca use gets . No ofrece protección contra una vulnerabilidad de desbordamiento del búfer (es decir, no se puede decir qué tan grande es el búfer que se pasa a él, por lo que no puede evitar que el usuario ingrese una línea más grande que el búfer y la memoria).

  • Evite usar scanf . Si no se usa con cuidado, puede tener los mismos problemas de desbordamiento de buffer que gets . Incluso ignorando eso, tiene otros problemas que dificultan el uso correcto .

  • En general, debes usar los fgets cambio, aunque a veces es inconveniente (tienes que quitar la nueva línea, debes determinar el tamaño del buffer antes de tiempo, y luego debes averiguar qué hacer con las líneas que son demasiado largas; ¿conservas la parte? lee y descarta el exceso , descarta todo, crece dinámicamente el búfer y vuelve a intentarlo, etc.). Hay algunas funciones no estándar disponibles que hacen esta asignación dinámica para usted (por ejemplo, getline en sistemas POSIX, función de ggets dominio público de Chuck Falconer ). Tenga en cuenta que los ggets tienen una semántica similar a la que le quita una nueva línea final.

Sí, quieres evitar se gets . fgets siempre leerá la nueva línea si el búfer fue lo suficientemente grande como para sostenerlo (lo que le permite saber cuándo el búfer era demasiado pequeño y hay más líneas esperando a ser leídas). Si desea algo como fgets que no leerá la nueva línea (perdiendo esa indicación de un búfer demasiado pequeño) puede usar fscanf con una conversión de conjunto de exploración como: "%N[^\n]" , donde el ‘N’ se reemplaza por el tamaño del búfer – 1.

Una manera fácil (aunque extraña) de eliminar la nueva línea final de un búfer después de leer con fgets es: strtok(buffer, "\n"); Esta no es la forma en que strtok está destinado a ser utilizado, pero lo he usado de esta manera más a menudo que en la forma prevista (que generalmente evito).

Hay numerosos problemas con este código. Arreglaremos las variables y funciones mal nombradas e investigaremos los problemas:

  • Primero, CharToInt() debe renombrarse como StringToInt() apropiado, ya que opera en una cadena, no en un solo carácter.

  • La función CharToInt() [sic.] No es segura. No verifica si el usuario ingresa accidentalmente un puntero NULL.

  • No valida la entrada, o más correctamente, salta la entrada no válida. Si el usuario ingresa en un dígito que no es, el resultado contendrá un valor falso. es decir, si ingresa en N el código *(s+i) & 15 producirá 14!

  • A continuación, la temp anodina en CharToInt() [sic.] Debería llamarse digit ya que eso es lo que realmente es.

  • Además, el return result / 10; kludge return result / 10; es solo eso, un truco malo para evitar una implementación defectuosa.

  • Del mismo modo, MAX está mal nombrado, ya que puede parecer entrar en conflicto con el uso estándar. es decir, #define MAX(X,y) ((x)>(y))?(x):(y)

  • El *(s+i) verboso *(s+i) no es tan legible como simplemente *s . No hay necesidad de usar y complicar el código con otro índice temporal i .

obtiene ()

Esto es malo porque puede desbordar el búfer de cadena de entrada. Por ejemplo, si el tamaño del búfer es 2 y usted ingresa 16 caracteres, desbordará str .

scanf ()

Esto es igualmente malo porque puede desbordar el búfer de cadena de entrada.

Menciona ” cuando se usa la función scanf (), el resultado es completamente incorrecto porque el primer carácter aparentemente tiene un valor ASCII -52.

Eso se debe a un uso incorrecto de scanf (). No pude duplicar este error.

Fgets ()

Esto es seguro porque puede garantizar que nunca desborde el búfer de cadena de entrada al pasar el tamaño del búfer (que incluye espacio para el NULL).

obtener línea()

Algunas personas han sugerido el getline() estándar de C POSIX getline() como reemplazo. Lamentablemente, esta no es una solución portátil práctica ya que Microsoft no implementa una versión C; solo la función estándar de plantilla de cadena C ++ como esta pregunta SO # 27755191 responde. La línea de getline() de C ++ getline() Microsoft estaba disponible al menos desde hace mucho tiempo como Visual Studio 6, pero dado que el OP está preguntando estrictamente sobre C y no sobre C ++, esta no es una opción.

Misc.

Por último, esta implementación tiene errores ya que no detecta el desbordamiento de enteros. ¡Si el usuario ingresa un número demasiado grande, el número puede volverse negativo! es decir, 9876543210 se convertirá en -18815698 ?! Vamos a arreglar eso también.

Esto es trivial de arreglar para un unsigned int . Si el número parcial anterior es menor que el número parcial actual, entonces nos hemos desbordado y devolvemos el número parcial anterior.

Para un signed int esto es un poco más de trabajo. En el ensamblaje, pudimos inspeccionar el indicador de acarreo, pero en C no hay una forma incorporada estándar para detectar el desbordamiento con las operaciones matemáticas firmadas. Afortunadamente, dado que estamos multiplicando por una constante, * 10 , podemos detectar esto fácilmente si usamos una ecuación equivalente:

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

Si x * 8 se desborda entonces lógicamente x * 10 también lo hará. Para un desbordamiento int de 32 bits ocurrirá cuando x * 8 = 0x100000000 por lo tanto, todo lo que tenemos que hacer es detectar cuando x> = 0x20000000. Como no queremos suponer cuántos bits tiene un int , solo necesitamos probar si los 3 mejores msb (bits más significativos) están configurados.

Además, se necesita una segunda prueba de desbordamiento. Si el msb está configurado (bit de signo) después de la concatenación de dígitos, entonces también sabemos el número desbordado.

Código

Aquí hay una versión segura y segura junto con un código con el que puede jugar para detectar el desbordamiento en las versiones inseguras. También unsigned versiones unsigned y unsigned través de #define SIGNED 1

 #include  #include  // 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; } 

Tienes razón de que nunca deberías usar gets . Si desea utilizar los datos, simplemente puede sobrescribir la nueva línea.

 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 } 

Esto supone que no hay NULL incrustados. Otra opción es 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 } 

La ventaja de getline es que te asigna y reasigna, maneja posibles NULL incorporados, y devuelve el conteo para que no tengas que perder el tiempo con strlen . Tenga en cuenta que no puede usar una matriz con getline . El puntero debe ser NULL o free-able.

No estoy seguro de qué problema tienes con scanf .

nunca use gets (), puede conducir a desbordamientos no probables. Si su matriz de cadenas tiene un tamaño de 1000 e ingreso 1001 caracteres, puedo desbordar el progtwig.

Intenta usar fgets () con esta versión modificada de tu 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; } 

En esencia, valida los dígitos de entrada e ignora cualquier otra cosa. Esto es muy crudo, así que modifíquelo y sal al gusto.

Así que no soy muy progtwigdor, pero déjame intentar responder a tu pregunta sobre el scanf(); . Creo que el escaneo es bastante bueno y lo uso para casi todo sin tener ningún problema. Pero has tomado una estructura no completamente correcta. Debería ser:

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

El “y” al frente de la variable es importante. Le dice al progtwig dónde (en qué variable) guardar el valor escaneado. el fflush(stdin); borra el búfer de la entrada estándar (teclado) por lo que es menos probable que obtenga un desbordamiento de búfer.

Y la diferencia entre gets / scanf y fgets es que gets(); y scanf(); solo escanea hasta el primer espacio ' ' mientras fgets(); escanea toda la entrada. (pero asegúrese de limpiar el búfer después para que no tenga un desbordamiento más adelante)