Menu

scanf en C: leer entrada, la regla del & y por qué deja de funcionar

Cómo leer entrada con scanf en C: por qué el & es obligatorio, cómo se manejan los espacios en blanco, el desbordamiento de búfer de %s y su arreglo con un ancho, comprobar el valor de retorno, el error del salto de línea sobrante y fgets como alternativa robusta.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

scanf es el espejo de printf: la misma cabecera, cadenas de formato de aspecto parecido, dirección opuesta. Lee texto de la entrada estándar, lo convierte según el formato y guarda los resultados a través de los punteros que le das.

También es la función que produce más principiantes confundidos que ninguna otra en C, porque tres de sus comportamientos sorprenden y uno de ellos es un agujero de seguridad. Esta página cubre los cuatro.

La forma básica, y por qué el &

Escribe un número y pulsa Enter para ejecutar esto. El &age no es decoración. C pasa todos los argumentos por valor, así que si escribieras scanf("%d", age) la función recibiría una copia del valor actual (basura) de age y no tendría forma de alcanzar la variable en sí. &age pasa la dirección de la variable, en la que scanf escribe luego. Mira punteros para el panorama completo.

Omitir el & es el error más común con scanf. El compilador con -Wall lo detecta (format '%d' expects argument of type 'int *'); sin los avisos activados compila y corrompe memoria en alguna dirección al azar.

Leer varios valores

Una sola llamada puede leer varios valores. Los espacios en blanco de la entrada —espacios, tabuladores, saltos de línea— los separan, y los especificadores numéricos se saltan cualquier cantidad de ellos.

Los espacios literales de "%d %d %d" son en realidad redundantes —%d ya se salta los espacios iniciales— pero hacen legible el formato. Lo que no es redundante es comprobar el valor de retorno, que se cubre más abajo.

Los caracteres que no son espacios en blanco dentro del formato deben coincidir exactamente con la entrada, que es como se lee entrada estructurada:

Los : del formato exigen unos : en la entrada. Escribe 9:30 y se analiza; escribe 9 30 y falla tras leer el 9.

Comprueba siempre el valor de retorno

scanf devuelve la cantidad de elementos asignados con éxito, no la cantidad de caracteres ni un indicador de éxito. Devuelve EOF si la entrada terminó antes de convertir nada.

Escribe hola en el aviso. scanf devuelve 0, n queda intacta —todavía con la basura con la que empezó— y el texto ofensivo sigue en el flujo de entrada. Esa última parte es lo que convierte una sola entrada mala en un bucle infinito:

/* BUG: gira para siempre con entrada no numerica */
while (scanf("%d", &n) != 1) {
    printf("Intenta de nuevo: ");
}

scanf nunca consume el texto que no pudo convertir, así que la siguiente llamada falla con los mismos caracteres, para siempre. Para recuperarte debes descartar tú mismo el resto de la línea:

La comprobación de EOF importa: cuando el flujo de entrada termina (el usuario pulsa Ctrl+D, o un archivo canalizado se acaba), scanf devuelve EOF para siempre, así que un bucle que solo comprueba != 1 reintenta eternamente. Trata "entrada inválida" y "no hay más entrada" como los dos casos distintos que son. Con eso resuelto, vale la pena tener a mano ese auxiliar discard_line en cualquier programa que use scanf sobre entrada humana.

El desbordamiento de búfer de %s

%s lee caracteres hacia un array de char. Fíjate en que no lleva &: el nombre de un array ya decae a un puntero a su primer elemento, así que &name sería del tipo equivocado.

El problema es que un %s a secas no tiene idea de qué tan grande es tu array:

char name[10];
scanf("%s", name);      /* PELIGROSO: teclea 40 caracteres y escribe 41 bytes */

Nada detiene la escritura al final del array. Sigue de largo hacia lo que haya después en memoria —otras variables, la dirección de retorno— que es el clásico desbordamiento de búfer, e históricamente la clase de error más explotada en C. La solución es un ancho entre el % y la s, uno menos que el tamaño del array para dejar sitio al '\0' terminador:

El ancho tiene que escribirse como un literal dentro de la cadena de formato, lo que hace molesto mantenerlo sincronizado con el tamaño del array: una razón más por la que la mayoría del C de producción usa fgets.

La segunda sorpresa de %s: se detiene en el primer espacio en blanco. Escribe Ada Lovelace y name contiene Ada; Lovelace espera en el flujo para la siguiente lectura. %s lee una palabra, no una línea.

El error del salto de línea sobrante

Este es el "scanf no funciona" con el que todos se topan. Los especificadores numéricos se saltan los espacios iniciales; %c no.

Esto es lo que pasa sin ese espacio. Escribes 30 y pulsas Enter. %d consume el 3 y el 0 y se detiene en el salto de línea, dejando '\n' en el búfer. El siguiente scanf("%c", &initial) lee ese salto de línea como el carácter y retorna al instante: el aviso aparece y se salta en el mismo aliento.

Un espacio en la cadena de formato de scanf significa "salta aquí cualquier cantidad de espacios en blanco", así que " %c" pasa por encima del salto de línea suelto y espera un carácter de verdad. El espacio solo hace falta antes de %c y de %[; todos los demás especificadores saltan espacios por su cuenta.

Leer decimales y caracteres

Los especificadores se comparten en su mayoría con printf, con una diferencia importante: scanf necesita saber el tamaño exacto del destino, porque escribe a través de un puntero.

Prueba con 1.5 2.25 7 x.

%f lee un float, %lf lee un double. En printf ambos imprimen igual porque los floats se promocionan a double antes de la llamada, pero en scanf no hay promoción, y usar %f con un double * escribe cuatro bytes en una variable de ocho, dejándola corrupta. El %c de aquí no necesita espacio inicial porque el %d anterior ya se detuvo en un espacio, no en un salto de línea. (Tabla completa en especificadores de formato.)

fgets: la alternativa robusta

Para cualquier cosa que teclee una persona, lee la línea entera y analízala después. fgets toma el tamaño del búfer como argumento, así que físicamente no puede desbordarse, y cuando la línea cabe consume también el salto de línea, de modo que no queda nada atrás que confunda la siguiente lectura. (Una línea más larga que el búfer deja su cola sin leer: la falta de '\n' en lo que recibiste es cómo lo detectas.)

Tres puntos sobre ese patrón:

  • sizeof line pasa el tamaño real, así que cambiar el tamaño del array no requiere ninguna otra edición, a diferencia del literal %19s.
  • fgets conserva el salto de línea si la línea cupo, y por eso está ahí el recorte con strcspn. Así también detectas una línea demasiado larga para el búfer: no hay '\n' en lo que recibiste.
  • Analiza después. strtol para enteros (también puede decirte dónde se detuvo el análisis), strtod para punto flotante, o sscanf(line, "%d %d", &a, &b) para usar un formato de scanf sobre una cadena que ya tienes de forma segura.

sscanf merece una mención propia: es scanf leyendo de una cadena en lugar del flujo de entrada, y combina a la perfección con fgets. Obtienes el cómodo análisis de scanf sin ninguno de sus problemas de flujo.

Errores comunes

  • Olvidar el & en un argumento que no sea un array. Compila con -Wall.
  • %s sin ancho. Un desbordamiento de búfer esperando una entrada larga.
  • Ignorar el valor de retorno. Y luego leer una variable que scanf nunca asignó.
  • Repetir un scanf fallido sin descartar la entrada. Un bucle infinito.
  • %c sin espacio inicial después de leer un número. El error del aviso que se salta.
  • Usar %f para un double. Corrupción silenciosa; scanf necesita %lf.
  • Esperar que %s lea una línea. Lee una palabra delimitada por espacios.

Preguntas frecuentes

¿Por qué scanf necesita un & en C?

Porque C pasa los argumentos por valor. scanf debe cambiar tu variable, así que necesita la dirección de la variable y no una copia de su valor: scanf("%d", &age). La única excepción es un array de caracteres, cuyo nombre ya es una dirección: scanf("%19s", name) no lleva &.

¿Por qué scanf se salta mi entrada para un char o una cadena?

Un scanf("%d", ...) anterior dejó el salto de línea de la tecla Enter en el búfer de entrada. %d y %f se saltan los espacios iniciales, pero %c no, así que lee ese salto de línea en vez de esperar. La solución es un espacio en el formato —scanf(" %c", &ch)— que le dice a scanf que salte primero los espacios en blanco.

¿Cómo leo una cadena con scanf de forma segura en C?

Dale a %s un ancho máximo uno menor que tu búfer: char name[20]; scanf("%19s", name);. Sin el ancho, %s escribe tantos caracteres como teclee el usuario y se pasa alegremente del final del array. Recuerda además que %s se detiene en el primer espacio en blanco, así que no puede leer un nombre completo con un espacio dentro.

¿Debería usar scanf o fgets en C?

Usa fgets para cualquier cosa que teclee una persona. Lee una línea entera en un búfer con tamaño, no puede desbordarse y, cuando la línea cabe, consume también el salto de línea, así que los clásicos errores de entrada mezclada desaparecen. Analiza la línea después con sscanf o strtol. scanf está bien para ejercicios pequeños y entrada de máquina bien formada.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR