sábado, 17 de mayo de 2014

Evitar desbordamiento de buffer con Scanf y limitar cantidad de caracteres

Evitar desbordamiento de búfer  y basura en el buffer del teclado al usar scanf.

Opción 1

Siempre cuando empezamos a programar en C se nos presentan unos problemas al leer cadenas de caracteres usando scanf:
1) Por lo general scanf solo lee palabras, pero es posible hacer que lea una frase usando "%[^\n]" en lugar de "%s", lo que esto hace exactamente es leer sucesivamente los caracteres del dispositivo de entrada estándar mientras que cada carácter de entrada no coincida con alguno de los incluidos en los corchetes y luego de '^', en este caso concreto nos toma todo lo que ingresamos hasta toparse con el caracter "nueva linea" (tecla enter).

      Ejemplo:
      . . .
      scanf("%[^\n]", frase) ;
      //lee una cadena de caracteres ingresada por teclado y la guarda en el array "frase"
      . . .
     
2) Otro problema que tenemos, es que cuando leemos una cadena de caracteres y presionamos enter queda "basura"en el buffer utilizado por la entrada estándar (el teclado). ¿Esto que ocasiona? ocurre que cuando intentemos leer otra cadena no funcionara, y se salteara a las siguiente instruccion como si hubiéramos ingresado algo (lo cual no hicimos), por lo general se trata de la tecla enter que quedo guarda en el buffer. Una solución a este problema es usar "fflush(stdin)" para limpiar el buffer utilizado por la entrada estándar siempre antes de hacer un scanf, pero esto no se recomienda hacer ya que la funcion fflush() solo esta definida para dispositivos de salida, por lo tanto no debe usarse en dispositivos de entrada (pero aun asi funciona en windows, en linux no funciona). Mi solución es agregar un espacio delante del  "%[^\n]" para que no cargue como parte de la cadena ese enter que quedo guardado anteriormente:

       . . .
      scanf(" %[^\n]", frase) ;
      /*notese el espacio en blanco antes del %, esto sirve para que no tome algun caracter que haya
      quedado en el buffet */
      . . .

3) El problema mas grave al usar scanf es que no podemos controlar que la longitud de la cadena sea menor que la longitud del array donde vamos a guardarlo, lo cual en caso de excedernos provoca un desbordamiento del buffer si la longitud de la cadena es mayor que la que el vector puede guardar, la solucion es colocar el tamañano de array - 1 despues del "%", y asi dejar espacio para que en la ultima posicion se guarde el valor nulo '\0' (es decir, si el array es por ejemplo de longitud 10 colocamos 9 luego del parentesis):

ejemplo

      . . .
      char frase[20];
      . . .
      scanf(" %19[^\n]", frase) ;
      /* toma un maximo de 19 caracteres ingresados y lo guarda en la variable "frase" */
      . . .

Esto tiene una consecuencia, y es que en caso de ingresar mas de 19 caracteres los caracteres sobrantes quedan en el buffer del teclado junto con el enter, esto ocaciona muchos problemas cuando querramos leer otra cadena, LA SOLUCION DEFINITIVA ES colocar %*[^\n] para que los valores sobrantes sea leidos pero no guardados en ninguna variable :

ejemplo

      . . .
      char frase[20];
      . . .
      scanf(" %19[^\n]%*[^\n]", frase) ;
      /* el asterisco antes del segundo [^\n] es un caracter de supresión de asignación el cual nos indica
      que el tipo de dato que se leerá debe ser descartado y no se guardara en ninguna variable, en este
     caso concreto nos dice que los caracteres excedentes a los 19 máximo deben leerse y descartarse*/
      . . .

Aun asi sigue quedando el "enter" en el buffer, si por la fuerza queremos que no quede basura podemos usar:

 scanf(" %N[^\n]%*[^\n]%*c", array)

donde  N es tamaño menos uno del array. Es importante colocar siempre el espacio después de la primera doble comilla"


Opción 2

Como habran visto la solucion anterior no es muy comoda de usar, como alternativa podemos emular fflush(stdin) usando una funcion, y asi solamente necesitaremos usar scanf(" %[^\n]", array) para cargar una cadena y luego usar la funcion para limpiar la basura restante:

   #include <stdio.h>
 
   //Funcion alternativa al fflush(stdin)
    void flush_in()
   {
           int ch;

           while( (ch = fgetc(stdin)) != EOF && ch != '\n' ){}
    }

    main()
   {
    char nombre[20];

     scanf("%19[^\n]",nombre);
      flush_in();  // aqui llamamos la funcion para limpiar el buffer de entrada
    }

1 comentario: