La función scanf y la lectura de la entrada de teclado

El objetivo es explicar por qué nunca hay que usar la función scanf para leer la entrada del teclado.

Cuando se lee la entrada generada por el teclado se deben tener en cuenta algunos aspectos:

  1. lo leído es generado por un ser humano que como tal se equivoca;
  2. el programa no accede al teclado mismo sino que es el sistema operativo el que le pasa los datos ingresados de alguna manera.

El primer aspecto ya alcanza para descartar scanf como herramienta válida para leer la entrada del teclado. La función está hecha para leer con un formato determinado y es imposible estar seguros de que un humano va a respetar ese formato sin ningún error. A pesar de esto, miles y miles de ejemplos de código que lee de teclado en C usan scanf aportando confusión y haciendo caer en el error a quienes recién se inician.

El otro aspecto es más técnico y requiere más explicación. Cuando se dice que programa lee del teclado en realidad lo que está haciendo es leer de un área de memoria llamada “buffer del teclado”. Por la naturaleza de los teclados su buffer es un “buffer de línea”, eso significa que los datos que vienen del teclado se incorporan de a líneas enteras y no carácter por carácter. Es por eso que cuando leemos de ahí, por más que ingresemos muchos caracteres, hasta que no ingresamos el fin de línea (enter) el programa no lee nada y se queda trabado esperando que haya algo en el buffer.

La forma en la que se llena el buffer del teclado nos permite estar seguros de 2 cosas:

  1. si el buffer no está vacío, sí o sí hay al menos un fin de línea;
  2. el buffer siempre tiene un carácter de fin de línea al final.

El primer problema que presenta scanf es que no siempre lee el carácter de fin de línea. Eso depende del formato que se usa. Cuando se usan “%d” o “%s” el fin de línea queda en el buffer y será leído en la próxima invocación a una función que lea del teclado. Esto genera incertidumbre en cuanto a qué hay en el buffer al momento de ir a leer. La primera vez está vacío, pero en las veces subsiguientes puede ser que tenga algunos caracteres que quedaron sin leer.

El segundo problema es que scanf no lee hasta el fin de línea (aunque lo deje sin leerlo) sino que lee hasta que se acaba el formato especificado. Si se le pide que lea un entero (“%d”) y se ingresan 2 enteros separados por un espacio o por una letra lee el primero y deja el resto en el buffer. Nuevamente esto genera incertidumbre en las lecturas subsiguientes no sabemos si quedaron caracteres ingresados anteriormente.

El tercer problema es más grave porque puede ocasionar que se cuelgue el programa. Scanf no controla que la cantidad de caracteres leídos sea menor o igual al tamaño del arreglo o string que se le pasa por parámetro. Si se esperan leer 10 caracteres es típico leerlos en un arreglo de 11 (uno para el fin del string), pero si el usuario ingresa 11 caracteres, scanf va a escribir más allá del fin de arreglo con el consiguiente error de memoria. Si usamos un formato que especifique la longitud máxima entonces el problema no será pasarse del tamaño del arreglo, sino que nuevamente dejaremos sin leer caracteres en el buffer.

El último problema es que scanf no permite detectar la mayoría de los errores de ingreso de datos del usuario. Cuando mandamos a leer un entero (“%d”) y el usuario ingresa letras, scanf nos devuelve un entero fruto de una conversión de las letras a número. ¿Cómo determinar si el usuario ingresó letras o realmente ese número? Imposible.

Para leer en forma correcta del teclado, es necesario asegurarse de 3 cosas.

  1. Dejar el buffer vacío luego de la lectura (leer siempre hasta el fin de línea). Es la única manera de evitar que las lecturas posteriores lean caracteres ingresados anteriormente y que no esperan.
  2. Leer carácter por carácter almacenándolos en un arreglo (o donde se quiera) teniendo en cuenta el tamaño disponible. Los caracteres que no entren igual deben ser leídos (para cumplir el punto anterior). No hay forma de predecir cuantos caracteres se van a leer.
  3. Ver de qué manera se puede informar que lo leído no es lo esperado de manera de que no se confundan un valor válido con uno erróneo. Por ejemplo, si se están leyendo números y se ingresan letras, devolver -1 puede interpretarse como un valor que señala el error o como un valor leído correctamente. Hay que elegir un valor que no pueda ser confundido con uno válido.

A modo de ejemplo del punto 3, la función getchar a pesar de que que lee caracteres el tipo de retorno no es char sino int. De esa manera cuando devuelve un valor -1 no puede confundirse con un carácter ya que estos son todos positivos.

Alguno se preguntará para qué existe scanf que es una función que lee de teclado, si no hay que usarla para ello. La respuesta radica en un detalle: scanf y algunas otras funciones como getchar no leen de teclado, sino que leen de la entrada estándar. Si no se indica lo contrario, la entrada estándar proviene del teclado, pero es muy simple hacer que provenga de un archivo cualquiera. Hay muchos programas en C que jamás se invocan sin establecer que la entrada estándar provenga de algún archivo. En esos casos, scanf puede ser útil, sobre todo si el archivo fue generado con printf.

Actualizado: Hace un tiempo publiqué cómo leer un entero de teclado. Ahora le corregí un bug y le mejoré el uso de memoria.

Anuncios

Leer un entero de teclado en C

Sin usar scanf, porque no sirve para leer de teclado.

#include<stdlib.h>
#include<stdio.h>

#define MAX_DIGITS 9
#define BASE 10

static int* BUFFER = NULL;

int* readInt()
{
int i = 0,
j = 0,
base = 1,
factor = 1,
*result,
ch = getchar();
if(BUFFER == NULL){
BUFFER = malloc(MAX_DIGITS * sizeof(int));
}
if(ch == 45){
factor = -1;
ch = getchar();
}
while(i < MAX_DIGITS && ch != 10 && ch != -1 && 48 <= ch && ch <= 57){
BUFFER[i++] = ch;
ch = getchar();
}

if(i == 0 || (ch != 10 && ch != -1)){
while(ch != 10 && ch != -1){
/* consumo lo que haya en el buffer para vaciarlo.*/
ch = getchar();
}
return NULL;
}
/* transformo el string en un entero según la base.*/
result = (int*) malloc(sizeof(int));
*result = 0;
/* De atrás para adelante, multiplicando por la base y sumando.*/
for(j = i – 1; j >= 0; j = j-1){
*result += ( BUFFER[j] – 48 ) * base;
base *= BASE;
}
/* aplico el signo*/
*result *= factor;
return result;
}