2010-07-21 28 views
29

He estado haciendo un programa bastante fácil de convertir una cadena de caracteres (suponiendo que se ingresen los números) en un entero.C - scanf() vs gets() vs fgets()

Después de que se hizo, me di cuenta de algunos "errores" muy peculiares que no puedo responder, sobre todo debido a mi limitado conocimiento de cómo funcionan las funciones scanf(), gets() y fgets(). (Yo ya había leído mucha literatura sin embargo.)

Así que sin escribir demasiado texto, aquí está el código del programa:

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

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

En segundo lugar, cuando se utiliza fgets(), el resultado es ligeramente incorrecto porque aparentemente la función 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 usa la función scanf(), el resultado es completamente incorrecto porque el primer carácter aparentemente tiene un valor ASCII -52. Para esto, no tengo ninguna explicación.

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

+0

Es posible WAN para reemplazar su función 'CharToInt()' con una llamada a 'atoi()' (que hacen la misma cosa). Además, el tipo de datos 'char' está implícitamente' signed', lo que puede explicar el "-52 valor ASCII" que estaba viendo. http://www.cplusplus.com/reference/clibrary/cstdlib/atoi/ – sigint

+0

Sí, podría usar atoi(), pero el objetivo de este programa era utilizar operadores bitwise. Además, muchas gracias por recordarme el valor firmado de char. El uso de caracteres sin signo resolvió el problema, aunque todavía no estoy seguro de cómo y por qué. – Marko

+3

@sigint: en C, char se puede firmar char o unsigned char a discreción del compilador. – ninjalj

Respuesta

4

Tienes la razón que nunca debes usar gets. Si desea usar fgets, 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 lo hace la asignación y reasignación para usted, que se encarga de posibles valores NULL incrustados, y se devuelve el recuento por lo que no tiene 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 del problema que está teniendo con scanf.

+0

Si la línea de entrada era realmente tan larga como 'sizeof (str) - 1' o más, no habrá línea nueva, por lo que este código descartaría un carácter válido. –

+0

@MattMcNabb, gracias. Creo que lo arreglé (no probado). –

+1

Tenga en cuenta que no debe usar 'strlen (str)' hasta que haya verificado que 'fgets()' haya devuelto un valor no nulo. –

18

Sí, desea evitar gets. fgets siempre leerá la nueva línea si el búfer fue lo suficientemente grande como para mantenerlo (lo que le permite saber cuándo el búfer era demasiado pequeño y hay más de la línea esperando a ser leída). Si quiere algo como fgets que no lea 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 la 'N' es reemplazada por el búfer tamaño - 1.

una forma fácil (si extraña) para eliminar la nueva línea de salida de una memoria intermedia después de leer con fgets es: strtok(buffer, "\n"); Esto no es como 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).

1

Trate de usar fgets() con esta versión modificada de su 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 y hace caso omiso de cualquier otra cosa. Esto es muy crudo, así que modifíquelo y sal al gusto.

+0

Para limpiar cosas, considere reemplazar 'strchr()' con 'isdigit()'. Mejor aún, reemplace toda la función 'CharToInt()' con una llamada a 'atoi()'. http: //www.cplusplus.com/reference/clibrary/cctype/isdigit/ http://www.cplusplus.com/reference/clibrary/cstdlib/atoi/ – sigint

+0

isdigit() es una gran sugerencia ... hecho. –

3

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

+0

Gracias a todos por sus respuestas. Fueron de mucha ayuda. Pero también me gustaría saber por qué scanf() no funciona en este programa. Gracias. – Marko

25
  • Nunca uso 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 el uso de scanf. Si no se usa con cuidado, puede tener los mismos problemas de desbordamiento del búfer que gets. Incluso ignorando eso, it has other problems that make it hard to use correctly.

  • En general, usted debe utilizar fgets lugar, aunque a veces es un inconveniente (hay que quitar el salto de línea, debe determinar un tamaño de búfer antes de tiempo, y luego debe averiguar qué hacer con las líneas que son demasiado largos – ¿conserva la parte que leyó y discard the excess, descartar todo, crecer dinámicamente el búfer y volver 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 Chuck Falconer's public domain ggets). Tenga en cuenta que ggets tiene una semántica similar a gets, ya que quita una nueva línea final para usted.

+0

Como dije en mi respuesta, 'getline' ahora es estándar. –

+6

@Matthew Flaschen: ¿Qué estándar? Cuando digo "no estándar", me refiero a "C no estándar", no a POSIX. – jamesdlin

-2

Así que no soy mucho de un programador, pero voy a tratar de responder a su 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 "&" delante de la variable es importante. Le dice al programa dónde (en qué variable) guardar el valor escaneado. 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 obtiene/scanf y fgets es que gets(); y scanf(); analizar sólo hasta el primer espacio ' ' mientras fgets(); escanea toda la entrada. (pero asegúrese de limpiar el búfer después para que no obtenga un desbordamiento más adelante)

+0

Omitir el & delante de str está perfectamente bien ya que en C las matrices se pasan por puntero. Es decir, 'scanf ("% s ", str);' es exactamente equivalente a 'scanf ("% s ", & str [0]);' – Michaelangel007

+0

Esta respuesta es incorrecta en varios aspectos y peligrosa. – siride

+0

Para ser precisos: (1) el '&' delante de 'str' no es necesario y puede generar advertencias de un compilador educado; (2) debe probar qué '' scanf() 'regresa para asegurarse de obtener los datos que esperaba; (3) el uso de 'fflush (stdin)' no es compatible con el estándar C; solo funciona en algunas plataformas, especialmente en Microsoft; (4) 'gets()' se lee al final de la línea (sin protección contra desbordamientos); (5) 'fgets()' no escanea toda la entrada, se lee hasta el final de la línea o hasta que no queda espacio en el búfer; (6) 'scanf()' puede desbordar el buffer - use 'scanf ("% 99s ", str)' si 'MAX == 100'. –

8

Hay numerosos problemas con este código. Vamos a arreglar las variables y funciones mal llamada e investigar los problemas:

  • En primer lugar, CharToInt() nombre debería cambiarse por el buen StringToInt() ya que opera en una cadena de ni 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, omite 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, el anodino temp en CharToInt() [sic.] Debería llamarse digit ya que es lo que realmente es.

  • Además, este truco return result/10; es sólo eso - una mala hackear a trabajar en torno a una aplicación con errores.

  • Igualmente MAX tiene un nombre incorrecto, ya que puede entrar en conflicto con el uso estándar. es decir, #define MAX(X,y) ((x)>(y))?(x):(y)

  • La detallada *(s+i) no es tan fácil de leer como *s. No hay necesidad de utilizar y saturar el código con otro índice temporal i.

gets()

Esto es malo, ya que puede desbordar el búfer de cadena de entrada. Por ejemplo, si el tamaño del búfer es 2 e ingresa 16 caracteres, se desbordará str.

scanf()

Esto es igual de malo, ya que puede desbordar el búfer de cadena de entrada.

usted menciona "cuando se utiliza la función scanf(), el resultado es totalmente erróneo, porque en primer carácter aparentemente tiene un valor de -52 ASCII."

Eso es debido a un uso incorrecto de scanf(). No pude duplicar este error.

fgets()

Esto es seguro porque se puede garantizar que nunca desbordan el búfer cadena de entrada por los que pasa en el tamaño del búfer (que incluye espacio para el NULL.)

getline()

Algunas personas han sugerido el C POSIX standardgetline() como reemplazo. Lamentablemente, esta no es una solución portátil práctica ya que Microsoft no implementa una versión C; solo el estándar C++ string template function como este SO #27755191 pregunta respuestas. El C++ getline() de Microsoft estaba disponible al menos desde Visual Studio 6, pero dado que el OP pregunta estrictamente sobre C y no sobre C++, 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, 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, ya que estamos multiplicando por una constante, * 10, podemos detectar fácilmente este si utilizamos una ecuación equivalente:

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

Si x * 8 desbordamientos 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 es int, solo tenemos que comprobar si se han configurado los 3 mejores msb (bits más significativos).

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í es una versión segura fijo junto con el código que se puede jugar con el desbordamiento de detectar en las versiones inseguras. También he incluido tanto un signed y unsigned versiones a través de #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; 
} 
+0

La función 'strlen()' no verifica si pasó un puntero nulo. La especificación estándar de la biblioteca C explícitamente dice (§7.1.4 Uso de funciones de biblioteca): _Si un argumento a una función tiene un valor inválido (como un valor fuera del dominio de la función, o un puntero fuera del espacio de direcciones del programa , o un puntero nulo, o un puntero al almacenamiento no modificable cuando el parámetro correspondiente no es const-calificado) o un tipo (después de la promoción) no esperado por una función con un número variable de argumentos, el comportamiento no está definido. Es razonable para requerir un puntero no nulo. –

+0

Es mejor agregar una verificación de seguridad de una línea y detectar errores descuidados luego asumir que la persona que llama no los hará, ¡pero gracias por el versículo del capítulo de la especificación! – Michaelangel007