2011-02-02 15 views
7

tengo este fragmento de código:de cadenas de entrada y de salida en C

char* receiveInput(){ 
    char *s; 
    scanf("%s",s); 

    return s; 
} 

int main() 
{ 
    char *str = receiveInput(); 
    int length = strlen(str); 

    printf("Your string is %s, length is %d\n", str, length); 

    return 0; 
} 

recibo esta salida:

Your string is hellàÿ", length is 11 

mi entrada fue:

helloworld! 

puede explicar a alguien por qué, y por qué este estilo de codificación es malo, gracias de antemano

Respuesta

10

scanf no asigna memoria para usted.

Debe asignar memoria para la variable pasada a scanf.

Se podría hacer como esto:

char* receiveInput(){ 
    char *s = (char*) malloc(100); 
    scanf("%s",s); 
    return s; 
} 

Pero la advertencia:

  1. la función que llama a receiveInput tendrá la propiedad de la memoria devuelta: Vas a tener que free(str) Después de imprimir en main. (Dar la propiedad de esta manera generalmente no se considera una buena práctica).

    Una solución fácil es obtener la memoria asignada como parámetro.

  2. si la cadena de entrada es más larga que 99 (en mi caso) su programa sufrirá de desbordamiento de búfer (que es lo que ya está sucediendo).

    Una solución fácil es pasar a scanf la longitud de su memoria intermedia:

    scanf("%99s",s); 
    

un código fijo podría ser así:

// s must be of at least 100 chars!!! 
char* receiveInput(char *s){ 
    scanf("%99s",s); 
    return s; 
} 
int main() 
{ 
    char str[100]; 
    receiveInput(str); 
    int length = strlen(str); 

    printf("Your string is %s, length is %d\n", str, length); 

    return 0; 
} 
+1

también necesitan stdlib para malloc! –

+2

@fehergeri: sí, y stdio para usar 'scanf'. Supongo que los encabezados han sido omitidos por propósito. – peoro

+0

Preferiría hacer 'receiveInput (char * s, size_t len)' pero luego necesitaría pasar por algunos aros construyendo la cadena de formato. –

2

Usted tiene que asignar primero la memoria de su objeto s en su método receiveInput(). Tales como:

s = (char *)calloc(50, sizeof(char)); 
+0

¿Por qué 'calloc' cuando vas a escribir de todos modos? –

+0

Creo que a veces una "cadena" en c es mejor asignarla como una matriz que una medida "segura". – Joze

+0

la elección de 'calloc' vs' malloc' solo afecta si el contenido se sobrescribe con 0 ... nada que ver con que uno sea una matriz. Supongo que es un poco como asignar explícitamente a las variables un valor inicial, incluso si sabes que les asignarás otro valor un par de líneas más adelante sin leer en el medio: un poco paranoico pero vagamente tranquilizador si es engañoso, pero en el caso de calloc puedes asegúrese de que el optimizador no elimine la inicialización innecesaria en tiempo de ejecución. –

20

Varias preguntas se han ocupado de lo que has hecho mal y cómo solucionarlo, pero también se ha dicho (el énfasis es mío):

alguien puede explicar por qué, y por qué este estilo de la codificación es mala

Creo scanf es una terrible manera de leer la entrada. Es inconsistente con printf, hace que sea fácil olvidarse de buscar errores, hace que sea difícil recuperarse de errores, y es incompatible con operaciones de lectura ordinarias (y más fáciles de hacer correctamente) (como fgets y compañía).

Primero, tenga en cuenta que el formato "%s" solo se leerá hasta que vea espacios en blanco. ¿Por qué espacios en blanco? ¿Por qué "%s" imprime una cadena completa, pero lee en cadenas con una capacidad limitada?

Si desea leer en una línea completa, como a menudo lo hará, scanf proporciona ... con "%[^\n]". ¿Qué? ¿Que es eso? ¿Cuándo se convirtió esto en Perl?

Pero el problema real es que ninguna de ellas es segura. Ambos se desbordan sin límites y sin límites. ¿Desea verificar los límites? De acuerdo, lo tienes: "%10s" (y "%10[^\n]" está empezando a verse peor). Eso solo leerá 9 caracteres, y agregará un nul-character de terminación automáticamente. Así que eso es bueno ... cuando nuestra matriz de tamaño nunca necesite cambiar.

¿Qué sucede si queremos pasar el tamaño de nuestra matriz como argumento al scanf? printf puede hacer esto:

char string[] = "Hello, world!"; 
printf("%.*s\n", sizeof string, string); // prints whole message; 
printf("%.*s\n", 6, string); // prints just "Hello," 

quieren hacer lo mismo con scanf? Así es como:

static char tmp[/*bit twiddling to get the log10 of SIZE_MAX plus a few*/]; 
// if we did the math right we shouldn't need to use snprintf 
snprintf(tmp, sizeof tmp, "%%%us", bufsize); 
scanf(tmp, buffer); 

Así es - scanf no es compatible con la precisión "%.*s" variable de printf hace, por lo que hacer con los límites dinámicos de cheques scanf tenemos que construir nuestra propia cadena de formato en un buffer temporal. Esto es todo tipo de problemas, y aunque en realidad es seguro aquí, parecerá una muy mala idea para cualquiera que esté de visita.

Mientras tanto, echemos un vistazo a otro mundo. Veamos el mundo de fgets. Así es como se lee en una línea de datos con fgets:

fgets(buffer, bufsize, stdin); 

infinitamente menos dolor de cabeza, sin tiempo de procesador desperdiciado convertir una precisión de número entero en una cadena que sólo será reparsed por la biblioteca de nuevo en un entero, y todo el los elementos relevantes están sentados allí en una línea para que podamos ver cómo funcionan juntos.

concedido, esto puede no leer una línea entera. Solo leerá una línea completa si la línea es más corta que bufsize - 1 caracteres. Así es como podemos leer una línea completa:

char *readline(FILE *file) 
{ 
    size_t size = 80; // start off small 
    size_t curr = 0; 
    char *buffer = malloc(size); 
    while(fgets(buffer + curr, size - curr, file)) 
     { 
     if(strchr(buffer + curr, '\n')) return buffer; // success 
     curr = size - 1; 
     size *= 2; 
     char *tmp = realloc(buffer, size); 
     if(tmp == NULL) /* handle error */; 
     buffer = tmp; 
     } 
    /* handle error */; 
} 

La variable curr es una optimización que nos impida volver a comprobar los datos que ya hemos leído, y no es necesario (aunque útiles como leemos más datos). Incluso podríamos usar el valor de retorno de strchr para quitar el carácter "\n" final si lo prefiere.

Observe también que size_t size = 80; como punto de partida es completamente arbitrario. Podríamos usar 81, o 79, o 100, o agregarlo como un argumento proporcionado por el usuario a la función. Incluso podríamos agregar un argumento int (*inc)(int), y cambiar size *= 2; a size = inc(size);, lo que permite al usuario controlar qué tan rápido crece la matriz. Estos pueden ser útiles para la eficiencia, cuando las reasignaciones son costosas y es necesario leer y procesar gran cantidad de líneas de datos.

Podríamos escribir lo mismo con scanf, pero piense en cuántas veces tendríamos que volver a escribir la cadena de formato.Podríamos limitarlo a un incremento constante, en lugar del doble (fácil) implementado anteriormente, y nunca tener que ajustar la cadena de formato; Podríamos ceder y almacenar el número, hacer los cálculos con los datos anteriores y usar snprintf para convertirlo a un formato de cadena cada vez que reasignamos para que scanf pueda convertirlo de nuevo al mismo número; podríamos limitar nuestro crecimiento y posición inicial de tal forma que podamos ajustar manualmente la cadena de formato (digamos, simplemente incrementar los dígitos), pero esto podría volverse peludo después de un tiempo y puede requerir recurrencia (!) para funcionar limpiamente.

Además, es difícil mezclar la lectura con scanf con la lectura con otras funciones. ¿Por qué? Supongamos que quiere leer un número entero de una línea, luego lea una cadena de la siguiente línea. Intenta esto:

int i; 
char buf[BUSIZE]; 
scanf("%i", &i); 
fgets(buf, BUFSIZE, stdin); 

que leerá el "2", pero luego fgets leerá una línea vacía porque scanf no leyó el salto de línea! Está bien, tomar dos:

... 
scanf("%i\n", &i); 
... 

Se piensa que esto se come el salto de línea, y lo hace - pero también se come conduce un espacio en blanco en la línea siguiente, porque scanf no puede decir la diferencia entre los saltos de línea y otras formas de espacio en blanco. (. Además, resulta que usted está escribiendo un analizador de Python, y llevando los espacios en blanco en las líneas es importante) Para que esto funcione, usted tiene que llamar getchar o algo para leer en la nueva línea y tirar a la basura que:

... 
scanf("%i", &i); 
getchar(); 
... 

¿No es tonto? ¿Qué sucede si usa scanf en una función, pero no llama al getchar porque no sabe si la próxima lectura va a ser scanf o algo más correcto (o si el próximo carácter será incluso una nueva línea) ? De repente, la mejor manera de manejar la situación parece ser escoger uno u otro: ¿usamos scanf exclusivamente y nunca tenemos acceso a fgets-estilo de entrada de control total, o usamos fgets exclusivamente y hacemos más difícil realizar un análisis complejo ?

En realidad, la respuesta es no lo hacemos. Usamos fgets (o funciones que no son scanf) exclusivamente, y cuando necesitamos scanf, como funcionalidad, , ¡simplemente llamamos al sscanf en las cadenas! No necesitamos tener scanf ¡vaciando nuestros archivos innecesariamente! Podemos tener todo el control preciso sobre nuestra entrada que queremos y todavía obtener toda la funcionalidad del formato scanf. Y aunque no pudiéramos, muchas opciones de formato scanf tienen funciones casi directas correspondientes en la biblioteca estándar, como las funciones strtol y strtod infinitamente más flexibles (y amigos). Además, i = strtoumax(str, NULL) para tipos enteros de tamaño C99 es mucho más limpia que scanf("%" SCNuMAX, &i);, y mucho más segura (podemos usar esa línea strtoumax sin cambios para tipos más pequeños y dejar que la conversión implícita maneje los bits adicionales, pero con scanf tenemos que hacer una uintmax_t para leer en).

La moraleja de esta historia: evitar scanf. Si necesita el formato que proporciona, y no desea (o no puede) hacerlo (más eficientemente) usted mismo, use fgets/sscanf.

+2

+1 Bravo !!! ¡Esta es una muy buena explicación! ¡Gracias! Espero que el OP también lo lea :-) – Joze

+3

Releí mi monstruosa respuesta de blog equivalente a un blog y me di cuenta de que había cometido una omisión, aunque no lo sabía en ese momento. 'scanf ("% u ", & i)' _realmente arroja un comportamiento indefinido_ en el caso del desbordamiento de enteros, a diferencia de 'strtoul' que maneja el error como cualquier persona en su sano juicio podría esperar. Otra razón más para evitar 'scanf' cuando sea posible. –

+0

No lo sabía ... ¡¡Gracias !! – Joze

Cuestiones relacionadas