Publi

Limpiar el buffer de teclado en Linux con ejemplos en C y C++

reducida

A veces, mientras se está desarrollando un pequeño programa en C en el que hay entradas del usuario por teclado, hay veces que parece que se pulsan teclas solas, esto es debido a una entrada de teclado anterior que no ha llegado a volcarse entera en nuestras variables.
Un ejemplo en cuestión lo tenéis aquí:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdlib.h>
#include <stdio.h>

#define MAX_NOMBRE 128

int main(int argc, char *argv[])
{
  char nombre[MAX_NOMBRE];
  int dia, mes, year;

  printf ("Introduce fecha de finalización (dd/mm/YYYY): ");
  scanf("%d/%d/%d", &dia, &mes, &year);
  printf ("Nombre de la tarea: ");
  fgets(nombre, MAX_NOMBRE, stdin);

  printf("La tarea %s termina el: %d/%d/%d\n", nombre, dia, mes, year);
  return EXIT_SUCCESS;
}

En un primer momento, cuando introducimos la fecha, tenemos que introducir “12/12/2010” por ejemplo y luego pulsar enter para que scanf() pueda finalizar, aunque esa pulsación de enter se queda almacenada en el buffer de teclado, siendo lo único que hay en el buffer en este momento. Cuando pedimos el siguiente dato con fgets() (gets(), getchar(), etc), lo primero que leerán será esa pulsación almacenada, por lo que la ejecución de nuestro programa parece que se salta esa entrada de datos.

La solución, sería, por un lado, utilizar siempre scanf() para la entrada de datos, scanf está preparado para que esto no sea problema, si lo primero que introducimos a scanf() es un enter, éste será ignorado y scanf() no terminará. Pero usar scanf() siempre, a veces no es posible, es más, tenemos que utilizar la función que más nos convenga en cada momento; otra opción es limpiar el buffer de teclado cuando sepamos que esto puede pasar o, si queremos adoptar una forma de programar más sistemática, siempre que hagamos un scanf() o una entrada de teclado.

En principio tenemos fflush(), es una función para el volcado de buffer de escritura, escritura de ficheros o escritura en la pantalla o dispositivos; pero bueno, funciona cuando hacemos fflush(stdin) y compilamos en Windows, pero no en Linux… según la especificación del lenguaje C, y por tanto, el comportamiento de fflush(stdin) no está definido, y Microsoft en su manual de C sí que lo documenta como una función que vuelca los streams de salida y vacía los streams de entrada.
Aunque no está todo perdido, si programáis bajo GNU/Linux, Mac, o queréis que vuestro código sea portable a esas plataformas, tenemos algunos métodos que pueden resultar de ayuda (aunque seguro que se nos ocurren muchas más):

  1. __fpurge() – Disponible sólo para Linux, aunque no es una función estandar, y en alguna ocasión me ha dado algún problema (no ha hecho su trabajo como debía). Si no estáis en Linux, podéis probar fpurge(). Para utilizar __fpurge() debemos incluir stdio_ext.h:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdlib.h>
    #include <stdio.h>
    #include <stdio_ext.h>

    #define MAX_NOMBRE 128

    int main(int argc, char *argv[])
    {
      char nombre[MAX_NOMBRE];
      int dia, mes, year;

      printf ("Introduce fecha de finalización (dd/mm/YYYY): ");
      scanf("%d/%d/%d", &dia, &mes, &year);
      __fpurge(stdin);
      printf ("Nombre de la tarea: ");
      fgets(nombre, MAX_NOMBRE, stdin);

      printf("La tarea %s termina el: %d/%d/%d\n", nombre, dia, mes, year);
      return EXIT_SUCCESS;
    }
  2. Otra solución, parece un poco más rudimentaria, pero no me ha dejado tirado:
    1
    2
    int ch;
    while ((ch = getchar()) != '\n' && ch != EOF);

    Un gran problema aquí es que si el buffer está completamente vacío y la entrada es bloqueante (como casi siempre, en terminal, es decir, cuando estamos pidiendo datos, el programa se para por completo hasta que terminemos de introducir información por teclado). Si lo preferimos, podemos crear una función del tipo:

    1
    2
    3
    4
    5
    void vacia_buffer()
    {
      int ch;
      while ((ch = getchar()) != '\n' && ch != EOF);
    }

    y podremos llamarla sin problemas cuando la necesitemos. Si queremos, podemos simplificarla un poco así:

    1
      while (getchar() != '\n');

    sólo comprobaremos el retorno de carro, y en la mayoría de los casos puede ser suficiente.

  3. setbuf(stdin, NULL); : con esta línea (sin usar nada más que stdio.h, conseguimos que no se utilice un buffer de teclado para funciones como fgets(), getchar() y demás. Aunque tendremos que llamarla siempre justo antes de utilizar una de estas funciones.
  4. ¡Ah! ¿Que te quieres complicar la vida? En unix, siempre puedes cambiar el tipo de entrada de teclado como no bloqueante, es decir, no necesitamos bloquear el proceso mientras el usuario introduce teclas, o pulsar intro al final, pero sólo durante un momento, es decir, cuando sabemos que en el buffer hay algo, así podemos leer todo lo que haya hasta encontrar un EOF (end of file), o fin de fichero, en este caso, fin de la entrada por teclado, y cuando esté todo limpio, volvemos a poner el modo de teclado como estaba:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #include <stdlib.h>
    #include <stdio.h>
    #include <stdio_ext.h>
    #include <unistd.h>
    #include <fcntl.h>

    #define MAX_NOMBRE 128

    int main(int argc, char *argv[])
    {
      char nombre[MAX_NOMBRE];
      int dia, mes, year;
      int fdflags;


      printf ("Introduce fecha de finalización (dd/mm/YYYY): ");
      scanf("%d/%d/%d", &dia, &mes, &year);

      fdflags = fcntl(STDIN_FILENO, F_GETFL, 0);
      fcntl(STDIN_FILENO, F_SETFL, fdflags | O_NONBLOCK);
      while (getchar()!=EOF);
      fcntl(STDIN_FILENO, F_SETFL, fdflags);

      printf ("Nombre de la tarea: ");
      fgets(nombre, MAX_NOMBRE, stdin);

      printf("La tarea %s termina el: %d/%d/%d\n", nombre, dia, mes, year);
      return EXIT_SUCCESS;
    }

    Aunque todo queda mejor con una función:

    1
    2
    3
    4
    5
    6
    7
    8
    int vacia_buffer()
    {
      int fdflags;
      fdflags = fcntl(STDIN_FILENO, F_GETFL, 0);
      fcntl(STDIN_FILENO, F_SETFL, fdflags | O_NONBLOCK);
      while (getchar()!=EOF);
      fcntl(STDIN_FILENO, F_SETFL, fdflags);
    }

¿ Y qué pasa en C++ ?

En C++, dentro de istream (por ejemplo, dentro de cin), tenemos ignore(), por lo que podemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>

using namespace std;

int main(int argc, char *argv[])
{
  string nombre;
  int dia, mes, year;
  char ch;

  cout << "Introduce fecha de finalización (dd/mm/YYYY): ";
  cin >> dia >> ch >> mes >> ch >> year;

  cin.clear();
  cin.ignore();         /* Aquí borramos el buffer! */

  cout << "Nombre de la tarea: ";
  getline(cin, nombre);
  cout << "La tarea "<<nombre<<" termina el: "<<dia<<"/"<<mes<<"/"<<year<<endl;

  return 0;
}

El problema aquí es que si el buffer de entrada está vacío, el programa permanecerá en espera hasta que haya algo en el buffer que ignorar. Además, sólo ignoraremos un carácter del buffer, aunque podemos hacer lo siguiente:

1
cin.ignore(1000, '\n');

De esta manera ignoraremos hasta 1000 caracteres, pero, terminaremos en el caso que suceda antes, que hayamos contado 1000 caracteres en el buffer, o que encontremos un enter (\n). Pero claro, tenemos que estar pendientes de que el número que ponemos es lo suficientemente grande, y que nunca el buffer será mayor, aunque, si incluimos limits (#include al principio) podemos hacer:

1
  cin.ignore(numeric_limits<streamsize>::max(), '\n');

Es decir, esperaremos que se limpie el buffer completo o que veamos un enter.

Por cierto, cin.clear() vale para eliminar el flag de error en la entrada de datos, si por casualidad se ha detectado alguno que, con teclado no suele suceder, si queremos, podemos eliminar esta línea, aunque en teoría está bien dejarla por si las moscas.

Lamentáblemente, con el teclado no podemos jugar con el descriptor de fichero, por ejemplo fseek(stdin, 0L, SEEK_END) en C, o cin.tellg(0, cin.end) en C++, porque su comportamiento con este buffer no está especificado y, en unos lugares (compiladores, plataformas y sistemas operativos determinados) pueden funcionar y en otros sitios no.

¿Conoces alguna otra forma de limpiar el buffer de teclado en C o C++?

Actualización 17/10/2015: Una explicación algo más extensa, foto de portada y algunos ejemplos más.
Actualización 18/10/2015: Versión C++.

Foto: Dustin Lee (Unsplash)

También podría interesarte...

There are 4 comments left Ir a comentario

  1. Angelverde /
    Usando Mozilla Firefox Mozilla Firefox 3.6.3 en Ubuntu Linux Ubuntu Linux

    No sabía que no funcionaba en Linux.

    Seguro mi profe tampoco.

    Gracias.

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 41.0 en Ubuntu Linux Ubuntu Linux

      ¡¡ Acabo de ver tu comentario !! Después de tantos años más de cinco años. Lo siento…

      Pues mira, yo no sé por qué en la implementación de Windows se saltaron la especificación y por qué en POSIX no hay nada estándar en C para eso. Eso sí, gracias a tu comentario, voy a actualizar un poco el post, para que tenga un poco más de información 🙂

  2. Leo /
    Usando Google Chrome Google Chrome 53.0.2785.101 en Windows Windows NT

    Gracias por la explicacion. 😀 ahora solo me falta ponerlo en practica

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 48.0 en Ubuntu Linux Ubuntu Linux

      Ya estás tardando !! Espero que nos cuentes qué tal tus andanzas !

Leave a Reply