Autor Tema: (C) Funciones confortables de Entrada/Salida

0 Usuarios y 1 Visitante están viendo este tema.

19 Agosto, 2013, 07:26 pm
Leído 2336 veces

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Librería NBIO.H

Estos días estuve jugando con un programa acá en el foro,
que calculaba el m.c.d. de dos números,
pero que derivó en un análisis de cómo mejorar la interacción con el usuario.

Programando en C,
muchas veces surgen dudas cuando usamos scanf() o gets(), y funciones similares.

Voy a colgar acá una librería que resuelve en forma básica varios problemas que surgen en la práctica sobre este tema, en forma recurrente.



Problema 1: ¿Cómo detectar si una string está compuesta sólo por caracteres "blancos"?

Solución 1
Entendemos por caracteres blancos no sólo a los espacios, sino también a los caracteres de control reconocidos por C: \t, \n, \a, \f, etc.

Para esto podemos confiar en la función isspace() de la librería estándar ctype.h,
que reconoce en forma correcta cuándo un caracter dado es un "blanco" según la definición entendida por el estándar C.

Ahí les va:


int blank(char *s) {
    for ( ; *s; ++s)
       if (!isspace(*s))
          /* Se ha encontrado un caracter que no es blanco. */
          return false;
    
    /* s es una string que sólo contiene blancos (al "estilo" isspace()) */
    return true;
}


Nota: Si bien la función devuelve sólo 1 (true) ó 0 (false), he puesto un tipo de retorno int, por misteriosas razones... (Es para aplicarla a problemas que analizamos luego).

[cerrar]



Problema 2: ¿Cómo detectar si un caracter pertenece a un conjunto dado de caracteres?

Solución 2

Lo que hacemos es definir el conjunto como una string que simplemente lista los caracteres admisibles.

A continuación, basta escribir una sencilla función de búsqueda:


/* Buscar un caracter en una cadena */
bool char_in_str(char c, char* s) {
   for ( ; *s; s++)
      if (c == *s)
         return true; /* c "pertenece" a s */
   /* c "no pertenece" a s */  
   return false;    
}

[cerrar]



Problema 3: ¿Cómo asignarle un valor a una variable string?

Solución 3
Lo que se hace es copiar una string en otra, caracter a caracter.


void nb_set_string(char* restrict dest, const char* restrict src) {
    /* Copia src en dest */
    for( ; (*dest = *src) != '\0'; src++, dest++)
      ;
}
/* ---> OUT: dest == src, strlen(dest) == strlen(src) */


La palabra clave restrict intenta prevenir superposiciones en memoria de los bytes de cada string.
[cerrar]



Problema 4: ¿Cómo frenar el programa antes de que termine?

Solución 4
Muchas veces, al hacer un programa sencillo,
este termina abruptamente y se cierra la ventana de ejecución,
no permitiendo que veamos los últimos resultados mostrados en la pantalla.

Las soluciones típicas que se suelen usar consisten en poner antes del fin de main() una de estas sentencias:


getchar();           /* Requiere stdio.h */
system("PAUSE"); /* Requiere stdlib.h */


Lo malo de system("PAUSE"); es que sólo funciona si en el sistema subyacente hay un comando llamado PAUSE que sirva para "frenar" la consola y esperar hasta que el usuario presiona ENTER.

Lo malo de getchar(); es que,
en mi experiencia (usando el compilador GCC),
se obtienen efectos indeseados al combinarlo con llamadas a scanf().
Parece ser que no son del todo compatibles.
De algún modo, al usar scanf() queda algún caracter en la cola de espera de la entrada estándar,
y esto hace que getchar(); "siga de largo", no logrando el efecto de "frenado".

Al parecer, los efectos de getchar() y fgets( ... , stdin) (o gets()) son compatibles entre sí,
y estos errores pueden evitarse.

Aquí la solución es compleja, porque requiere que todo el tiempo tengamos una disciplina en el diseño:

* Evitar el uso directo de scanf().

En las operaciones de lectura haríamos algo como esto:

gets(buffer);
sscanf(buffer, "cadena de formato", &variables...);

/* Y cuando queramos frenar la consola: */

getchar();


Podemos colocar todo en una función, que llamaremos enter_to_continue(),
y de paso explicamos cómo usarla correctamente.
Es más, hasta podemos agregarle, si queremos, un mensaje que informe alguna cosa al usuario:


/* Mostrar un mensaje al usuario y esperar a que presione ENTER para seguir. */
/* Se usa una combinación de fgets(..., stdin) y getchar() para              */
/* leer de forma correcta caracteres "basura" hasta alcanzar el fin de línea */
/* (Con (MINGW) GCC <= 4.8 no es compatible con previas llamadas directas scanf())   */
void enter_to_continue(char* msg) {
      puts(msg);
      
      char minibuff[1] = "";      /* == { '\0' } */
      fgets(minibuff, 1, stdin);
      /* buffer[0] == '\0'; */
      /* Se han leído 0 caracteres            */
      /*     ---> No se ha podido leer el fin de línea */
      while(getchar()!='\n') ;
}


[cerrar]



Problema 5: ¿Cómo leer desde la entrada estándar una string en forma segura?

Solución 5
Quienes hayan usado scanf() para leer datos habrán tropezado más de una vez con extrañas e indeseables vicisitudes.
Puede que sea un problema de la distribución (la mía es (MINGW) GCC 4.7).
Como sea, eso me ha motivado a "postergar" la lectura de los datos ingresados por el usuario en la entrada estándar (teclado), que en C es un archivo que se llama stdio (en la librería estándar stdio.h).

Ya he comentado antes la técnica básica para esto:

gets(buffer);
sscanf(buffer, "%formatos...", &variables...);


La función gets() lee una línea completa del teclado hasta alcanzar el fin de línea '\n'.
El caracter '\n' es descartado y reeplazado por el caracter nulo '\0'.
El resultado se guarda en buffer, que es una variable de tipo char[].

Pero aquí surge un nuevo problema:

* ¡La función gets() no es segura!  :o :o :o


La razón de esto es simple: si del teclado se han leído más caracteres que los que caben en el array de caracteres buffer[], entonces no hay espacio suficiente, y se produce una sobreescritura de posiciones prohibidas de la memoria, causando estragos en el programa, y en el sistema.

Usar gets() es un pecado en la "religión" de la informática.

(Hay una conspiración global contra el uso de goto, pero yo digo que el goto no es grave, si se lo sabe usar bien. De hecho, esto le pasa a cualquier aspecto de un lenguaje de programación. En cambio el uso de gets() sí tiene que estar prohibido, porque el programador no puede controlar sus efectos).

Para evitar esto, hay que usar algún mecanismo análogo a gets(), pero que impida que se lean más caracteres que los que puede albergar el buffer.

Por suerte en stdio.h hay una alternativa que es la función fgets(): Lee líneas de archivos de texto, hasta una cantidad especificada como máximo de caracteres.
El archivo de texto que nos interesa leer es stdin: la entrada estándar (o teclado).

Si buffer es un array de N caracteres,
entonces tendríamos que leer así:

fgets(buffer, N, stdin);

Esta sentencia lee N - 1  :o :o caracteres de stdin (o sea el teclado), agrega un caracter nulo '\0',
y el resultado lo pone en buffer.
Uno de los caracteres leídos y guardados en buffer será el fin de línea '\n' (cosa que no ocurría con gets()).

Listo, ya no hay problemas de sobreescritura de memoria...  ;D

Pero se arruina todo de nuevo  >:( porque si el usuario teclea más que N - 1 caracteres,
estos quedan en la "cola de espera" de stdin,
y serán tomados como caracteres válidas en una próxima lectura de stdin,
ya sea que se haga con fgets(..., stdin), gets(), getchar() o scanf().

Esto es un nuevo problema a resolver.
Lo que necesitamos es detectar si se ha presionado o no la tecla ENTER, y en caso negativo, seguir leyendo caracteres de la "cola de espera" de stdin hasta encontrar un fin de línea '\n'.
¿Cómo se hace esto?   ??? ??? ???

Para resolver la cuestión, no nos salgamos de contexto, porque la solución puede no ser aplicable en todos los casos.
Hemos leído datos con fgets(buffer, N, stdin), y queremos determinar si se ha alcanzado el fin de línea o no.

Si buffer contiene algún '\n', quiere decir que fgets() lo ha leído y guardado allí, y que en la posición siguiente hay un '\0'.
Pero este caso no nos interesa...
Lo que nos interesa es saber cuándo NO HAY '\n' en buffer, o sea, cuándos fgets() no alcanzó a leer hasta el fin de línea.
Si esto ha ocurrido, entonces el buffer está lleno hasta la posición \( N-2 \) de caracteres distintos de '\n' y '\0'. Y hay un '\0' en la posición \( N-1 \).
Recíprocamente, si estas condiciones ocurren, quiere decir también que el fin de línea no se ha alcanzado, porque en ese caso el buffer se ha llenado hasta la posición \( N-2 \) con datos distintos al fin de línea.

Por lo tanto, es sencillo saber la condición lógica que debe cumplirse en el programa para saber si el fin de línea se ha alcanzado o no:

fgets(buffer, N, stdin) pudo alcanzar el fin de línea si y sólo si (buffer[N-2] != '\0') && (buffer[N-2] != '\n')

Si esta condición se cumple, leemos con getchar() sucesivos caracteres de stdin hasta encontrar el próximo fin de línea '\n':


while(getchar() != '\n')
    ;  /* sentencia vacía */


Todo esto funciona si el número de caracteres del buffer satisface \( N \geq 2 \).
¿Qué pasa si \( N == 1 \)?
En este caso sólo hay espacio en el buffer para 1 caracter: el nulo '\0'.
O sea que la llamada fgets(buffer, 1, stdin) nunca alcanza el fin de línea.
Aquí siempre habrá que "limpiar" la "cola de espera" buscando el próximo fin de línea.

Hay otro problema.  :-\  :'( :'(

La variable buffer[] puede que sea utilizada en el programa para diversas cosas que al programador se le ocurran, y entonces fgets() no tiene el control de lo que ocurre con los caracteres del array.
Por lo tanto, puede que a veces en la posición buffer[N-2] haya un caracter distinto del fin de línea y del nulo, aún en el caso en que fgets() haya leído el fin de línea.

Esto sólo puede ocurrir si previamente había información "basura" en buffer[].
Lo ideal sería limpiar toooooodo el array buffer[], rellenando con '\0' todos las posiciones.
Sin embargo esto es ineficiente, y además es innecesario.

Dado que la única posición que nos preocupa es buffer[N-2], basta conque limpiemos esa sola posición de memoria, ya que la "basura" de las otras posiciones no nos afecta en modo alguno.
Limpiar 1 solo caracter es más eficiente, y no importa cuán grande sea N, se tarda siempre lo mismo en esta tarea de "limpieza" tan sencilla:

buffer[N-2] = '\0';

Por fin hemos terminado de analizar todos los casos.
Ahora podemos poner todo junto en una función que haga el trabajo.
Con comentarios y todo, queda así:


/* Se usa la función "segura" fgets() en vez de gets()                     */
/* No permite al usuario ingresar cadenas de longitud mayor que el buffer. */
/* buffer debe tener al menos N caracteres disponibles.                    */
/* Si buffer no es un array con tamaño suficiente, nb_gets() no es válida. */
char* nb_gets(char *buffer, int N) {
    if (N <= 0)
        /* No hay espacio para leer */
        return NULL;
    /* N >= 1 */
    if (N == 1) {
        /* Sólo hay espacio para el caracter nulo        */
        fgets(buffer, N, stdin);
        /* buffer[0] == '\0'; */
        /* Se han leído N - 1 == 0 caracteres            */
        /*     ---> No se ha podido leer el fin de línea */
        while(getchar()!='\n')
          ;
                  
        return buffer;
    };
    /* N >= 2 */    
      
    buffer[N-2] = '\0'; /* Limpieza rápida del buffer (ver abajo) */
    fgets(buffer, N, stdin);
    /* Se cumple la siguiente propiedad:                                */
    /* (no_se_alcanzó_fin_de_línea)                                     */
    /*          <---->                                                  */
    /* (buffer[N-2] != '\0') && (buffer[N-2] != '\n')                 */
    /* Por lo tanto es suficiente limpiar el caracter .buffer[N-2]      */
    
    if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
       /* No se ha alcanzado el fin de línea */
       while ( getchar() != '\n' ) /* flush_stdin... */
            /* ---> produce una descarga de stdin */
            /*      descartando caracteres hasta llegar al fin de línea */
           /* */ ;
    /* else ; */
      /* Sí se ha alcanzado el fin de línea: no se hace nada. */
      
    return buffer;
}


Si queremos verla limpia de tandos comentarios y aserciones, queda esto:


char* nb_gets(char *buffer, int N) {
    if (N <= 0)
        /* No hay espacio para leer */
        return NULL;

    if (N == 1) {
        fgets(buffer, N, stdin);
        while(getchar() != '\n')
          ;
                  
        return buffer;
    };

    buffer[N-2] = '\0'; /* Limpieza rápida del buffer (ver abajo) */
    fgets(buffer, N, stdin);
    if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
       while (getchar() != '\n')
           ;
      
    return buffer;
}

[cerrar]



Problema 6: ¿Cómo se hace para saber si una lectura del tipo scanf(), fscanf(), sscanf() ha producido o tenido un error?

Spoiler
Lo que muchas veces olvidamos es que esas funciones retornan un valor entero que informa del éxito que se ha tenido leyendo datos del archivo de entrada.
Si el resultado de la operación es EOF, quiere decir que hubo un error en la lectura de los datos.
En cambio, si es un entero no negativo, indica cuántos argumentos se han podido reemplezar correctamente, acorde al tipo de datos especificado.

Cuando se encuentra un error de cualquier tipo, la función scanf() (por ejemplo) interrumpe su trabajo y retorna de inmediato al programa principal.
Entonces la cantidad de argumentos correctamente reemplazados puede ser menos que el total de parámetros que se le pasaron como argumento.

Para saber si hubo algún error, hay que comparar la cantidad de directivas % de la cadena de formato pasada a scanf() con el valor de retorno de scanf() que informa del número de reemplazos efectivamente realizados con éxito.
Además, no hay que contar los casos de %% ni de %*, que no reemplazan argumentos de verdad.

El único modo de lograr esto es analizar la cadena de formato, y contar cuántas directivas hay.
El problema consiste, entonces, en hacer un recuento exacto del número de directivas "efectivas" de la cadena de formato.

Según el estándar C99, se puede deducir que si al caracter % le siguen ciertos caracteres, entonces estamos ante una directiva válida. En caso negativo, preferiremos informar que hay un error del programador (un error fatal).
Una vez asegurados de que la cadena de formato no contiene directivas desconocidas,
observamos que es posible intercalar dígitos entre el caracter % y la directiva propiamente dicha.
Para no ser engañados por una directiva errónea a la que le han intercalado un dígito, vamos a ignorar todos los dígitos de la cadena de formato.
No es este el enfoque más exacto, pero aún así nos resuelve la cuestión, salvo por un muy pequeño matiz, que no tiene importancia práctica, que luego veremos.

Lo que haremos será un algoritmo que detecte la presencia de un '%' en una cadena de formato, y espere a analizar el caracter siguiente, a ver si conviene contarlo o no.
Se deja guardada la información en una variable tipo "flag" (un banderín), que es true cuando se ha encontrado un '%' y false en otro caso. Además, hay que apañarse para quitar los casos indeseables de %% y %*.

Nuestro algoritmo funciona correctamente con cadenas de formato correctas.
Con cadenas de formato inválidas, o tal que el estándar no aclara cómo se supone que han de funcionar, seguirán siendo inválidas para nosotros, pero la interpretación de la razón que la invalida es diferente.
Esto es un detalle técnico sutil, pero que en la práctica no afecta, porque un programa correcto sólo ha de tener cadenas de formato válidas.

Ahí les va:


/* El entero retornado por nb_scanfdirs() es: */
/* n >= 0: Se hallaron n directivas válidas (C99) para argumentos reales.   */
/* n < 0:  Error: hay directivas no reconocidas.                            */
/* Nota adicional: Las directivas que contienen dígitos antepuesto ("%20f") */
/* son reconocidas correctamente. */
/* Nota ténica: */
/* Los casos "%digitos%" son erróneos en C99 (ver sección 7.19.6.2.12).     */
/* pero al mismo tiempo son reconocidos inexactamente por __bui_N_ARGS().   */
/* El resultado es un programa erróneo en cualquier caso, aunque            */
/* la "semántica" para este caso de sscanf() y NB_SCANF() no coincidirá.    */
int nb_scanfdirs(char *s) {
    int n;
    bool before = false;
    
    for (n = 0; *s; s++)
      /* n == 0 ---> before == false */
      /* n > 0  --->                                              */
      /*     [ (último caracter leído no-dígito de s) == '%'      */
      /*               <----> before == true ]                    */
      if (*s == '%')
        /* Recordar que se ha encontrado un signo '%' en esta posición, */
        /* pero no tomar ninguna acción.                                */
        /* Si antes ya había un '%', es que estamos ante el caso %%,    */
        /* y entonces se debe "cancelar" el estado del flag "before".   */
        /* Si no, registramos la aparición de '%' como true,            */
        /* para ser analizado en la siguiente iteración.                */
        before = ( (before)? false: true );
      else {
        /* El caracter actual no es '%' */    
        if (before)
          /* *(s-1) == '%' (el caracter previo era un '%')          */
          /* Directiva % detectada.                                 */
          if (char_in_str(*s, SCANF_SAFE_DIGITS))
             /* Si se encuentra un dígito, mejor ignorarlo,                 */
             /* y seguir buscando el próximo caracter que no sea un dígito. */
/* CONTINUE */
             continue;
             /* before == true */
             /*     ---> Directivas con dígitos se reconocen correctamente. */
            
          else if (char_in_str(*s, SCANF_SAFE_ARG_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* que además corresponde a un argumento real.             */
             /* ---> entonces incrementar el contador.                  */
             n++;
          else if (char_in_str(*s, SCANF_SAFE_SPE_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* pero que no corresponde a un argumento real.            */
             ; /* Nada */
          else
             /* Se encontró una directiva desconocida. */
             /* Se termina abruptamente la función y retorna error. */
/* RETURN */
             return NB_FMT_NOT_VALID; /* < 0 */
        else /* if(before) */
          ;
                    
        before = false;
        /* Se registra para la iteración siguiente */
        /* que el caracter actual no es un '%'     */
      }
    /* n == número de directivas % halladas en s.                        */
    /* Sólo se cuentan casos que efectivamente se asignan a argumentos,  */
    /* de los tipos especificados por el estándar C99.                   */
    /* No se cuentan los casos de %%, %*.                                */
          
    return n;        
}


Ahora una comparación del tipo:

scanf(fmt, ...) != nb_scanfdirs(fmt)

nos informará de que hubo un error en la lectura de datos con scanf().

[cerrar]



Problema 7: ¿Cómo pasar parámetros de un tipo concreto a una macro?

Solución 7
Sabemos que las macros tipo función como:

#define MOSTRAR_CUADRADO(X) printf("%d", ((X) * (X)));

toman argumentos arbitrarios, y no hay modo de indicar en la lista de parámetros si han de ser de un tipo de datos específico u otro.
Ni siquiera podemos asegurar que sea algo con sentido lo que "cae" en X.

Es que X es sólo una abreviatura de una "porción de texto" que se usará para ser reemplazada dentro de la macro.

Pero podemos aprovechar las capacidades del compilador para forzar la macro a que los parámetros sean de un tipo específico.
Por ejemplo, si queremos que nuestra macro anterior sólo acepte X como un dato de tipo int,
lo que haremos es declarar una variable temporal de tipo int, y asignarle el valor X.
Si la asignación es sintácticamente correcta, el programa compilará, y si no, entonces no lo hará.

Para hacer las cosas con prolijidad, y evitar efectos indeseados, encerraremos el código de la macro entre llaves { }.
Además, el nombre de la variable temporal tiene que ser tal que no entre en conflicto con otros nombres que pudiera usar el programador en otros lugares.
Lo típico es usar para estos casos nombres que empiecen con __.

Así, tenemos ahora:

#define MOSTRAR_CUADRADO(X) {    \
       int __temp_x = (X);       \
       printf("%d\n", __temp_x); \
  }


Cuando escribamos:

MOSTRAR_CUADRADO(15.18741);

el compilador nos dará una advertencia o en otros casos dará un error.

Por otra parte, la variable temporal __temp_x queda "encapsulada": ya no podrá ser referenciada una vez terminada la macro.
Esto ahorra potenciales problemas. (¿Qué pasa si al programador se le ocurre hacer algo con una variable llamada "también" __temp_x?: sólo desastres podrían ocurrir).

Una ventaja adicional de este método es que evita algunos efectos indeseables de las macros.
Por ejemplo, en la versión original:

int x = 5;
MOSTRAR_CUADRADO(++x);

hubiéramos obtenido como resultado 49, que es incorrecto, porque el resultado deseado es \( (5+1)^2=36 \).

Eso es un ejemplo típico que enseña los "peligros"  :o :o :o (mira cómo tiemblo...) del uso de las macros.

Con la nueva versión, que usa la variable temporal, se obtiene el resultado correcto 36.



Sin embargo, hemos puesto un bloque entre llaves { } para que la variable temporal funcione.
Aún si quitáramos las llaves, la macro no deja de ser una colección de sentencias,
las cuales han de "terminar", sin dejar "flotando" un valor como resultado.
¿Qué diablos estoy diciendo?

La expresión: x = 5, x = (3 + x)
arroja el valor 8 como resultado, y puede usarse aún dentro de otras expresiones,
para proseguir con cálculos más complejos.
En cambio:

int x = 5;

es una sentencia que no puede insertarse en medio de otros cálculos: debe terminar ahí.

Si ahora quisiéramos una macro como:

#define CUADRADO(X) ((X) * (X))

¿cómo conseguiríamos el mismo efecto que antes, de "obligar" a que X sea de tipo int, y que además se eviten otros efectos indeseables típicos de las macros tipo función?

Esa macro quiere servir para hacer algo como esto:

int x = 5;
int ans;

ans = 70 - CUADRADO(x);
printf("%d\n", ans);


El resultado sería 45...

Si en la macro ponemos llaves { },
o tan sólo hacemos el intento de declarar una variable temporal int __temp_x, como antes,
ya no podremos invocar la macro como parte de una expresión.
La línea siguiente dará un error del compilador:

ans = 70 - CUADRADO(x);

Una solución parcial sería la de tener previamente definida una variable auxiliar de tipo int, e invocarla dentro de la macro:

int __temp_x;
#define CUADRADO(X) ((__temp_x = (X)), __temp_x * __temp_x)


El operador coma (,) va descartando todos los cálculos que aparecen,
y el resultado de toda la expresión es el que queda a la derecha de la última coma.
Por eso podemos hacer casi cualquier antes de calcular el dichoso cuadrado.
Vemos que esta técnica impide que la macro tenga efectos indeseables:

Se asegura que el dato X asignado a __temp_x es de tipo int,
y "lee" una sola vez el parámetro X, para luego operar con ese valor tranquilamente dentro del resto de la macro,
tal como hace una función con un parámetro pasado "por valor".

Lo único malo acá es que la variable "temporal" tuvo que definirse externamente a la macro.
Por ahora intentemos evitar eso lo más posible.

La "solución" que voy a emplear es la "paciencia".
En efecto: voy a esperar a que los compiladores adopten las nuevas características del estándar C 2011, en que se puede especificar el tipo de datos del parámetro de una macro.

(Más adelante voy a ensayar una alternativa, pero será complicada...)

[cerrar]



(Nuevo)

Problema 8: ¿Cómo crear una macro tipo "bloque" y que sea "segura"?

Solución 8
Una macro tipo bloque es algo como esto:

#define MACRO { \
    sentencias...; \
    sentencias...; \
    \
  }


Es decir, el cuerpo de la macro es una lista de sentencias que se encierran entre llaves { }.
A un grupo de instrucciones encerradas entre llaves se le suele denominar "bloque".

La utilidad de las llaves al definir una macro extensa y complicada,
es que aporta "seguridad".
Es que las macros por sí solas son fuente de desmanes sintácticos difíciles de detectar.
Si no se encierran bien las instrucciones entre llaves, pueden "pegarse" de formas extrañas e inarmónicas con el resto del programa, dando errores o comportamientos antiintuitivos.

Que el código de una macro sea "seguro" quiere decir que "se inserta bien y armoniosamente con el resto del programa".
Una macro sin parámetros tiene que verse intuitivamente y encajar en el código como si fuese, digamos, cualquier otro identificador (constante o variable).
Una macro con parámetros tiene que verse intuitivamente y encajar en el código como si fuese una función.

El uso de llaves parece ayudarnos en estos objetivos, pero hay una sutileza:

* Los bloques a veces se interpretan de modo diferente según las sentencias circundantes.

Por ejemplo:

if (1)
  calc();
else
   ! calc();


Si calc() es una función, el código compila bien.
Si no, si calc() es una macro puesta en bloque, la compilación falla antes de la línea "else".

La única solución que hay para esto (en el C estándar)
es usar otra construcción: en vez del bloque "pelado",
ponerlo como parte de un do {} while() que ejecuta una sola vez, así:

#define MACRO  do { \
    sentencias...; \
    sentencias...; \
    \
  } while(0)


Al final se pone while(0) para que el cuerpo de la macro se ejecute 1 sola vez.
Además, es importante "no poner" un punto y coma (;) al final, para que encaje armoniosamente en otras porciones de código. Ahora, si calc() es una macro tipo función escrita como un bloque encerrado con do {} while(0), encajará en el programa de la misma forma que una función, y casi no se notará la diferencia...

Hay sin embargo una diferencia esencial entre las funciones y las macros de bloque: éstas últimas no permiten que se retorne un valor, o sea, se computa como una sentencia y no como una expresión.
Son como funciones sin retorno (similar a los procedimientos de Pascal).

Para que la construcción no quede tan fea, y tenga un sentido más inteligible para el programador,
podemos definir macros apropiadas que se sustituyan por do y while(0), así:

#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)


Es crucial colocar las llaves ahí mismo en la definición de esas macros,
porque si no, algún desubicado podría querer usar __EndSafeBlock como el "inicio" de un while() {...}, en vez del final de un do {...} while(), que es exactamente lo que queremos.

Lo ideal sería poner esas definiciones en una librería aparte, donde se documente exactamente para qué diablos se han definido. Esta explicación del propósito de las macros hará que se eviten errores accidentales: todos los errores por el uso de dichas macros será mera intención del programador.
Ejemplo:

#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)

#define MOSTRAR_FRASE(S) \
__BegSafeBlock \
       printf("El programador le envía el siguiente mensaje:\n"); \
       printf("%s", S); \
       getchar(); \
__EndSafeBlock

int main(void) {
  MOSTRAR_FRASE("To C or not to C\n");
}  

[cerrar]

:)

19 Agosto, 2013, 07:27 pm
Respuesta #1

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Problema 9: ¿Cómo manejar la entrada de datos por teclado de una manera "confortable"?

Solución 9
Vaya uno a saber lo que quiere decir "confortable".
Básicamente, quiere decir evitar los problemas típicos: que la entrada no sea excesiva y sobreescriba memoria prohibida, que se detecten apropiadamente errores en la lectura de datos, que podamos tener un control adecuado de los saltos de línea tras leer datos de la entrada estándar stdin (teclado), etc.

Algunas de estas cuestiones las hemos resuelto en los párrafos de arriba.
Ahora queremos sustituir scanf() por un mecanismo que resuma las soluciones que hemos venido encontrando.

Vamos a definir una estructura que servirá para albergar el buffer,
así como los estados de error tras la lectura de datos del teclado.
No es la solución más elegante, pero es un comienzo...

La estructura será anónima, y tiene esta pinta:


#define __INPUT_STR_LEN  130  /* Longitud string datos entrados por teclado. */
#define NB_NOT_SPECIAL     0  /* Debe ser siempre 0 */

struct {
    char buffer[__INPUT_STR_LEN];
    bool fatalerr;
    int special;
    int  errno;
    bool userinputerr;
} nb =
   { .buffer  = "",
     .fatalerr = false,
     .special  = NB_NOT_SPECIAL,
     .errno    = 0,
     .userinputerr = false
   };


Definiremos una macro NB_scanf() que hace la misma tarea que scanf(),
pero está más controlada, y rellena adecuadamente la estructura anónima nb para dejar información sobre distintos tipos de error.

También aprovecharemos para pasarle a NB_scanf() como parámetro el nombre de una función del tipo:

int (* función)(char *)

Esa función retorna un número asociado a un cierto "comando" que el usuario ha ingresado en la cadena buffer, desde el teclado.
No se hace nada con ese "comando", sólo se detecta su número correspondiente,
y se lo guarda en el campo .special de la estructura nb.
El valor 0 se reserva para el caso en que el usuario no introduce ningún "comando especial",
sino que sólo ha entrado datos "normales".

La macro es ésta:


#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)

#define NB_scanf(SPFUNC, FMT, ...) \
   __BegSafeBlock \
       int (*__spfunc)(char*) = SPFUNC;          \
       char *__fmt = FMT;                         \
       \
       int __n_args = nb_scanfdirs(__fmt);                           \
       /* nb_scanfdirs(__fmt) == número de directivas "efectivas" en __fmt */ \
       nb.fatalerr = (__n_args < 0);                          \
       if (nb.fatalerr)                                       \
           printf("Fatal error: format string is wrong.\n");         \
       else { \
           nb_gets(nb.buffer, __INPUT_STR_LEN);                     \
           nb.special = __spfunc(nb.buffer);                  \
           if (nb.special == NB_NOT_SPECIAL) {                      \
               /* Caso "normal" de ingreso de datos. */                     \
               /* [#] __VA_ARGS__  == lista de punteros a variables   */    \
               nb.errno = sscanf(nb.buffer, __fmt, __VA_ARGS__);\
               /* nb_input.errno: entero de significado análogo al      */ \
               /*                  valor de retorno de sscanf()          */ \
               nb.userinputerr = (nb.errno < __n_args);       \
          } /* if(nb.special == 0) */ \
          else \
             ; /* No se hace nada si nb.special != 0 */ \
       } /* else */ \
    __EndSafeBlock


Su modo de uso está dado en una serie de comentarios:



/* (macro:) NB_scanf(SPFUNC, FMT, ...)  
/*
/* Macro para la entrada de datos, al estilo de scanf() o sscanf().
/* (Es decir, acepta un número variable de parámetros).
/*
/* SPFUNC:
/*    Es el nombre de una función del tipo: int (* ) (char*) (o compatible...).
/*    Es decir, una función que acepta un char* como único parámetro,
/*    y que retorna un int.
/*    Si se pasan otros tipos de objetos o de funciones incompatibles,
/*    el compilador lo detectará.
/*    Se utiliza para detectar comandos especiales ingresados por el usuario.
/*    Cada comando tendrá asociado un número entero distinto de 0,
/*    y el número asignado dependerá de la función SPFUNC,
/*    que es proveída por el programador.
/* FMT:
/*    Es una cadena de formato que funciona igual que las de scanf().
/*    (Sólo el caso de dígitos interpuestos entre un par de % %
/^     fuciona de modo diferente. Sin embargo, este caso es erróneo tanto
/*     para sscanf como para NB_SCANF() ).
/* ...:
/*    Número variable parámetros, en donde se han de consignar
/*    las direcciones de memoria de las variables a ser asignadas durante
/*    el proceso de lectura de datos
/*    (según las directivas de tipos de datos consignados en FMT).
/*
/* Para usar la macro NB_scanf() se deben consignar todos los parámetros
/* arriba explicados, y además al menos 1 parámetro más en la lista variable.
/* Al principio la macro define variables de los tipos deseados,
/* e intenta asignarles directamente los parámetros.
/* Si esto no fuera posible, el programa no compilará.
/* En el resto de la macro se usan esas variables, pues tienen tipos concretos.
/*
/* Se analiza la cadena de formato FMT, y si contiene un error se informa
/* en el campo nb.fatalerr, terminando la macro inmediatamente.
/*
/* Si no, se lee una línea de la entrada estándar (hasta el 1er fin de línea).
/* Estos caracteres se guardan en nb.buffer.
/*
/* Con la función SPFUNC() (pasada como parámetro por el programador),
/* se analiza la línea de entrada, y se intenta detectar un comando "especial".
/* En caso afirmativo (un valor no nulo), esto se registra en la variable
/* nb.special, y se termina la macro inmediatamente.
/*
/* Luego se "releen" los caracteres del buffer, y se intenta convertirlos
/* en valores apropiados, según indique la cadena de formato FMT,
/* pasando estos valores a la lista variable de parámetros.
/* El método en que se realiza esta asignación es idéntico al de sscanf().
/* El resultado de esta operación es un entero (int),
/* igual al valor que generaría una llamada a sscanf().
/* Este valor queda guardado en:
/*         nb.errno
/* Se calcula la cantidad de asignaciones esperadas
/* (esto se logra calculando la cantidad de caracteres '%' de FMT
/* que realmente se usarán para asignar valores a variables).
/* El cálculo se hace con la función:
/*        int nb_scanfdirs(char* );
/* Se compara este valor con el almacenado en nb_input.errno,
/* y el resultado se guarda como un valor bool en
/*        nb.userinputerr
/* Su valor es "true" cuando la comparación de los dos valores falla.
/* Esto es indicativo de "error" de lectura: significa que el usuario
/* ha ingresado datos erróneos, y esto quedar consignado en
/* la variable nb.userinputerr.
/*
/* La rutina que llamó a la macro NB_scanf() tiene que verificar
/* los campos de nb_input, y actuar en consecuencia.
/* El modo de hacer esto tiene que ser el siguiente:
/*
/* if(nb.fatalerr)
/*   /* Finalizar el programa */
/* else if (nb.special)
/*       switch(nb.special) {
/*           case 1: /* ejecutar rutina 1 */
/*           break;
/*           case 2: /* ejecutar rutina 2 */
/*           break;
/*           /* ... */
/*           case N: /* ejecutar rutina N */
/*           break;
/*       }
/* else if (nb.userinputerr)
/*     /* Realizar acción asociada a ingreso de datos erróneos del usuario */
/* else
/*    /* Entrada de datos "normal", compatible con la cadena de formato FMT */
/*    /* Realizar acciones con los datos ingresados por el usuario */
/*
/* Ejemplo de uso:
/*
/* #include nbio0100.h
/*
/* int main(void) {
/*     float x, y, z;
/*
/*     NB_scanf(blank, "%f %f %f", &x, &y, &z);
/*
/*     if(nb.fatalerr)
/*     else if (nb.special)
/*          switch(nb.special) {
/*              case 1: return 0; /* FIN DEL PROGRAMA */
/*              break;
/*          }
/*     else if (nb.userinputerr)
/*         printf("No puedo calcular nada con esa actitud tuya!! :( \n\n");
/*     else
/*         printf("Datos correctos!\nSu promedio es: %f\n\n", (x+y+z)/3);
/*
/*     return 0;    
/* }
/*
/* Precauciones:
/* =============
/*      La cadena de formato FMT sólo reconoce directivas de C99.
/*      Opciones adicionales o de C11 y ss. no son reconocidas.
/* */


[cerrar]

¿Cómo ponemos todo esto en una librería?
Así:

Librería NBIO.H, versión 1.00

nbio0100.h:


/* ========================================================================= */
/* NBIO.H 1.00 (Nice Basic Input Output)
/*
/* Algunas rutinas confortables para E/S
/*
/* 2013/ago/18
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
/*
/* Esta librería provee una interface sencilla y sólida para funciones de E/S.
/*
/* Hay una macro principal NB_scanf(), que reemplaza a la función scanf(),
/* y se encarga de leer la entrada estándar (teclado) de forma segura,
/* y gestionar situaciones de error de forma genérica.
/*
/* La estructura nb_input{} contiene campos
/* que controlan la entrada por teclado, y contienen señales de error.
/*
/* void nb_set_string(char *, char *):
/*    Copia la cadena "derecha" en la cadena "izquierda".
/*
/* bool blank(char* ):
/*    Indica si todos los caracteres de una cadena son blancos
/*    (acorde a isspace() de ctype.h, C99).
/*
/* bool char_in_str(char, char*):
/*    Indica si un caracter dado está en una cadena.
/*
/* char* nb_gets(char* buffer, int N):
/*     Lee la entrada estándar stdin hasta un máximo de N - 1 caracteres,
/*     y guarda los caracteres leídos en buffer.
/*     Tiene una semántica análoga a la de fgets().
/*     Si los caracteres leídos son N - 2 o menos,
/*     se incluye el caracter '\n' al final.
/*     En cualquier caso, se agrega un caracter nulo '\0' al final.
/*     Si quedan caracteres sin leer antes de alcanzar el fin de línea,
/*     se descartan todos los caracteres sobrantes y también el fin de línea.
/*
/*     Importante:
/*       Si N <= 0 retorna NULL no hace nada: retorna NULL.
/*       Si N >= 1 funciona de manera "normal".
/*
/* int nb_scanfdirs(char *):
/*     Analiza una cadena de formato para contar las directivas válidas
/*     asignables a argumentos en una sentencia de tipo scanf().
/*
/* void enter_to_continue(char *):
/*     Muestra un mensaje y
/*     espera a que el usuario presoina ENTER para continuar.
/* */

/*
/* ========================================================================= */

#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>

/* */
/* struct { ... } nb_input;
/* =========================
/*
/* Es una estructura anónima que sirve para gestionar la entrada estándar.
/* Sus campos son:
/*
/*    char buffer[]:
/*          Un array de caracteres para albertar los datos de entrada.
/*    bool fatalerr:
/*          Vale true si en la última operación de entrada con NB_SCANF()
/*          se ha utilizado una cadena de formato errónea.
/*          No todas las cadenas de formato erróneas producen "true".
/*          En general es un error del programador, y no del usuario.
/*    int special:
/*          Contiene un entero que significa algún tipo de comando "especial"
/*          que el usuario ha introducido con el teclado.
/*          Si el valor es NB_NOT_SPECIAL ( == 0), quiere decir que
/*          el usuario ha intentado ingresar datos "normales".
/*    int errno:
/*          Contiene el resultado de la operación de entrada,
/*          el cual coincide con el que generaría sscanf(),
/*          siempre que su valor sea modificado por las operaciones "internas"
/*          de éste archivo de librería.
/*    bool userinputerr:
/*          Es un "flag" (banderín) que informa si la última operación
/*          de entrada ha tenido errores (true) o ha sido exitosa (false).
/*          Para ser consecuentes con este significado,
/*          se inicializa su valor a "false" al principio del programa.
/*
/* La longitud de la cadena "buffer" es __INPUT_STR_LEN.
/* Tiene un valor de 130, suficiente para la consola típica de 128 caracteres,
/* más 1 caracter nulo que agrega al final la función gets().
/* Si se va a correr el programa en un entorno donde la consola admita entradas
/* más extensas, este valor no sería suficiente.
/* (Mas, sería preferible recortar la entrada, antes que agrandar el buffer.)
/*
/* */

#define __INPUT_STR_LEN  130  /* Longitud string datos entrados por teclado. */
#define NB_NOT_SPECIAL     0  /* Debe ser siempre 0 */

struct {
    char buffer[__INPUT_STR_LEN];
    bool fatalerr;
    int special;
    int  errno;
    bool userinputerr;
} nb =
   { .buffer  = "",
     .fatalerr = false,
     .special  = NB_NOT_SPECIAL,
     .errno    = 0,
     .userinputerr = false
   };

void nb_set_string(char* restrict, const char* restrict);
bool char_in_str(char, char*);
int blank(char* );
char* nb_gets(char *, int);
int nb_scanfdirs(char *);
void enter_to_continue(char* msg);

/* (macro:) NB_scanf(SPFUNC, FMT, ...) 
/*
/* Macro para la entrada de datos, al estilo de scanf() o sscanf().
/* (Es decir, acepta un número variable de parámetros).
/*
/* SPFUNC:
/*    Es el nombre de una función del tipo: int (* ) (char*) (o compatible...).
/*    Es decir, una función que acepta un char* como único parámetro,
/*    y que retorna un int.
/*    Si se pasan otros tipos de objetos o de funciones incompatibles,
/*    el compilador lo detectará.
/*    Se utiliza para detectar comandos especiales ingresados por el usuario.
/*    Cada comando tendrá asociado un número entero distinto de 0,
/*    y el número asignado dependerá de la función SPFUNC,
/*    que es proveída por el programador.
/* FMT:
/*    Es una cadena de formato que funciona igual que las de scanf().
/*    (Sólo el caso de dígitos interpuestos entre un par de % %
/^     fuciona de modo diferente. Sin embargo, este caso es erróneo tanto
/*     para sscanf como para NB_SCANF() ).
/* ...:
/*    Número variable parámetros, en donde se han de consignar
/*    las direcciones de memoria de las variables a ser asignadas durante
/*    el proceso de lectura de datos
/*    (según las directivas de tipos de datos consignados en FMT).
/*
/* Para usar la macro NB_scanf() se deben consignar todos los parámetros
/* arriba explicados, y además al menos 1 parámetro más en la lista variable.
/* Al principio la macro define variables de los tipos deseados,
/* e intenta asignarles directamente los parámetros.
/* Si esto no fuera posible, el programa no compilará.
/* En el resto de la macro se usan esas variables, pues tienen tipos concretos.
/*
/* Se analiza la cadena de formato FMT, y si contiene un error se informa
/* en el campo nb.fatalerr, terminando la macro inmediatamente.
/*
/* Si no, se lee una línea de la entrada estándar (hasta el 1er fin de línea).
/* Estos caracteres se guardan en nb.buffer.
/*
/* Con la función SPFUNC() (pasada como parámetro por el programador),
/* se analiza la línea de entrada, y se intenta detectar un comando "especial".
/* En caso afirmativo (un valor no nulo), esto se registra en la variable
/* nb.special, y se termina la macro inmediatamente.
/*
/* Luego se "releen" los caracteres del buffer, y se intenta convertirlos
/* en valores apropiados, según indique la cadena de formato FMT,
/* pasando estos valores a la lista variable de parámetros.
/* El método en que se realiza esta asignación es idéntico al de sscanf().
/* El resultado de esta operación es un entero (int),
/* igual al valor que generaría una llamada a sscanf().
/* Este valor queda guardado en:
/*         nb.errno
/* Se calcula la cantidad de asignaciones esperadas
/* (esto se logra calculando la cantidad de caracteres '%' de FMT
/* que realmente se usarán para asignar valores a variables).
/* El cálculo se hace con la función:
/*        int nb_scanfdirs(char* );
/* Se compara este valor con el almacenado en nb_input.errno,
/* y el resultado se guarda como un valor bool en
/*        nb.userinputerr
/* Su valor es "true" cuando la comparación de los dos valores falla.
/* Esto es indicativo de "error" de lectura: significa que el usuario
/* ha ingresado datos erróneos, y esto quedar consignado en
/* la variable nb.userinputerr.
/*
/* La rutina que llamó a la macro NB_scanf() tiene que verificar
/* los campos de nb_input, y actuar en consecuencia.
/* El modo de hacer esto tiene que ser el siguiente:
/*
/* if(nb.fatalerr)
/*   /* Finalizar el programa */
/* else if (nb.special)
/*       switch(nb.special) {
/*           case 1: /* ejecutar rutina 1 */
/*           break;
/*           case 2: /* ejecutar rutina 2 */
/*           break;
/*           /* ... ^/
/*           case N: /* ejecutar rutina N */
/*           break;
/*       }
/* else if (nb.userinputerr)
/*     /* Realizar acción asociada a ingreso de datos erróneos del usuario */
/* else
/*    /* Entrada de datos "normal", compatible con la cadena de formato FMT */
/*    /* Realizar acciones con los datos ingresados por el usuario */
/*
/* Ejemplo de uso:
/*
/* #include nbio0100.h
/*
/* int main(void) {
/*     float x, y, z;
/*
/*     NB_scanf(blank, "%f %f %f", &x, &y, &z);
/*
/*     if(nb.fatalerr)
/*     else if (nb.special)
/*          switch(nb.special) {
/*              case 1: return 0; /* FIN DEL PROGRAMA */
/*              break;
/*          }
/*     else if (nb.userinputerr)
/*         printf("No puedo calcular nada con esa actitud tuya!! :( \n\n");
/*     else
/*         printf("Datos correctos!\nSu promedio es: %f\n\n", (x+y+z)/3);
/*
/*     return 0;   
/* }
/*
/* Precauciones:
/* =============
/*      La cadena de formato FMT sólo reconoce directivas de C99.
/*      Opciones adicionales o de C11 y ss. no son reconocidas.
/* */

/* ========================================================================= */
/* Implementación:
/* */


void nb_set_string(char* restrict dest, const char* restrict src) {
    /* Copia src en dest */
    for( ; (*dest = *src) != '\0'; src++, dest++)
      ;
}
/* ---> OUT: dest == src, strlen(dest) == strlen(src) */


/* Mostrar un mensaje al usuario y esperar a que presione ENTER para seguir. */
/* Se usa una combinación de fgets(..., stdin) y getchar() para              */
/* leer de forma correcta caracteres "basura" hasta alcanzar el fin de línea */
/* (Con GCC <= 4.8 no es compatible con previas llamadas directas scanf())   */
void enter_to_continue(char* msg) {
      puts(msg);
     
      char minibuff[1] = "";
      fgets(minibuff, 1, stdin);
      /* buffer[0] == '\0'; */
      /* Se han leído 0 caracteres            */
      /*     ---> No se ha podido leer el fin de línea */
      while(getchar()!='\n') ;
}

/* Se usa la función "segura" fgets() en vez de gets()                     */
/* No permite al usuario ingresar cadenas de longitud mayor que el buffer. */
/* buffer debe tener al menos N caracteres disponibles.                    */
/* Si buffer no es un array con tamaño suficiente, nb_gets() no es válida. */
char* nb_gets(char *buffer, int N) {
    if (N <= 0)
        /* No hay espacio para leer */
        return NULL;
    /* N >= 1 */
    if (N == 1) {
        /* Sólo hay espacio para el caracter nulo        */
        fgets(buffer, N, stdin);
        /* buffer[0] == '\0'; */
        /* Se han leído N - 1 == 0 caracteres            */
        /*     ---> No se ha podido leer el fin de línea */
        while(getchar()!='\n')
          ;
                 
        return buffer;
    };
    /* N >= 2 */   
     
    buffer[N-2] = '\0'; /* Limpieza rápida del buffer (ver abajo) */
    fgets(buffer, N, stdin);
    /* Se cumple la siguiente propiedad:                                */
    /* (no_se_alcanzó_fin_de_línea)                                     */
    /*          <---->                                                  */
    /* (buffer[N-2] != '\0') && (buffer[N-2] != '\n')                 */
    /* Por lo tanto es suficiente limpiar el caracter .buffer[N-2]      */
   
    if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
       /* No se ha alcanzado el fin de línea */
       while ( getchar() != '\n' ) /* flush_stdin... */
            /* ---> produce una descarga de stdin */
            /*      descartando caracteres hasta llegar al fin de línea */
           /* */ ;
    /* else ; */
      /* Sí se ha alcanzado el fin de línea: no se hace nada. */
     
    return buffer;
}

/* Buscar un caracter en una cadena */
bool char_in_str(char c, char* s) {
   for ( ; *s; s++)
      if (c == *s)
         return true; /* c "pertenece" a s */
   /* c "no pertenece" a s */   
   return false;     
}

/* Conjunto de directivas de reemplazo efectivo por argumentos válidos      */
/* en funciones del estilo de scanf(), en la librería stdio.h, de C99 o ss. */

#define SCANF_SAFE_ARG_SET_C99 "hljztLdiouxaefgc[pAEFGXn0123456789"
#define SCANF_SAFE_SPE_SET_C99 "*"
#define SCANF_SAFE_DIGITS      "0123456789"

/* Indicador de que se halló una directiva no válida en la cadena de formato */
#define NB_FMT_NOT_VALID -1 /* Debe ser < 0 */

/* El entero retornado por nb_scanfdirs() es: */
/* n >= 0: Se hallaron n directivas válidas (C99) para argumentos reales.   */
/* n < 0:  Error: hay directivas no reconocidas.                            */
/* Nota adicional: Las directivas que contienen dígitos antepuesto ("%20f") */
/* son reconocidas correctamente. */
/* Nota ténica: */
/* Los casos "%digitos%" son erróneos en C99 (ver sección 7.19.6.2.12).     */
/* pero al mismo tiempo son reconocidos inexactamente por __nb_N_ARGS().   */
/* El resultado es un programa erróneo en cualquier caso, aunque            */
/* la "semántica" para este caso de sscanf() y NB_SCANF() no coincidirá.    */
int nb_scanfdirs(char *s) {
    int n;
    bool before = false;
   
    for (n = 0; *s; s++)
      /* n == 0 ---> before == false */
      /* n > 0  --->                                              */
      /*     [ (último caracter leído no-dígito de s) == '%'      */
      /*               <----> before == true ]                    */
      if (*s == '%')
        /* Recordar que se ha encontrado un signo '%' en esta posición, */
        /* pero no tomar ninguna acción.                                */
        /* Si antes ya había un '%', es que estamos ante el caso %%,    */
        /* y entonces se debe "cancelar" el estado del flag "before".   */
        /* Si no, registramos la aparición de '%' como true,            */
        /* para ser analizado en la siguiente iteración.                */
        before = ( (before)? false: true );
      else {
        /* El caracter actual no es '%' */   
        if (before)
          /* *(s-1) == '%' (el caracter previo era un '%')          */
          /* Directiva % detectada.                                 */
          if (char_in_str(*s, SCANF_SAFE_DIGITS))
             /* Si se encuentra un dígito, mejor ignorarlo,                 */
             /* y seguir buscando el próximo caracter que no sea un dígito. */
/* CONTINUE */
             continue;
             /* before == true */
             /*     ---> Directivas con dígitos se reconocen correctamente. */
             
          else if (char_in_str(*s, SCANF_SAFE_ARG_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* que además corresponde a un argumento real.             */
             /* ---> entonces incrementar el contador.                  */
             n++;
          else if (char_in_str(*s, SCANF_SAFE_SPE_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* pero que no corresponde a un argumento real.            */
             ; /* Nada */
          else
             /* Se encontró una directiva desconocida. */
             /* Se termina abruptamente la función y retorna error. */
/* RETURN */
             return NB_FMT_NOT_VALID; /* < 0 */
        else /* if(before) */
          ;
                   
        before = false;
        /* Se registra para la iteración siguiente */
        /* que el caracter actual no es un '%'     */
      }
    /* n == número de directivas % halladas en s.                        */
    /* Sólo se cuentan casos que efectivamente se asignan a argumentos,  */
    /* de los tipos especificados por el estándar C99.                   */
    /* No se cuentan los casos de %%, %*.                                */
           
    return n;         
}

int blank(char *s) {
    for ( ; *s; ++s)
       if (!isspace(*s))
          /* Se ha encontrado un caracter que no es blanco. */
          return false;
     
    /* s es una string que sólo contiene blancos (al "estilo" isspace()) */
    return true;
}



#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)

#define NB_scanf(SPFUNC, FMT, ...) \
   __BegSafeBlock     \
       int (*__spfunc)(char*) = SPFUNC;          \
       char *__fmt = FMT;                         \
       \
       int __n_args = nb_scanfdirs(__fmt);                           \
       /* nb_scanfdirs(__fmt) == número de directivas "efectivas" en __fmt */ \
       nb.fatalerr = (__n_args < 0);                          \
       if (nb.fatalerr)                                       \
           printf("Fatal error: format string is wrong.\n");         \
       else { \
           nb_gets(nb.buffer, __INPUT_STR_LEN);                     \
           nb.special = __spfunc(nb.buffer);                  \
           if (nb.special == NB_NOT_SPECIAL) {                      \
               /* Caso "normal" de ingreso de datos. */                     \
               /* [#] __VA_ARGS__  == lista de punteros a variables   */    \
               nb.errno = sscanf(nb.buffer, __fmt, __VA_ARGS__);\
               /* nb_input.errno: entero de significado análogo al      */ \
               /*                      valor de retorno de sscanf()          */ \
               nb.userinputerr = (nb.errno < __n_args);       \
          } /* if(nb.special == 0) */ \
          else \
             ; /* No se hace nada si nb.special != 0 */ \
       } /* else */ \
  __EndSafeBlock

/* Quitamos las definiciones de los símbolos definidos en la librería */
/* para que queden inaccesibles desde archivos externos. */

#undef SCANF_SAFE_ARG_SET_C99
#undef SCANF_SAFE_SPE_SET_C99
#undef SCANF_SAFE_DIGITS
#undef NB_FMT_NOT_VALID

[cerrar]

(Modificación: He cambiado las llaves del cuerpo de la macro por la técnica explicada en el Problema 8)

19 Agosto, 2013, 09:59 pm
Respuesta #2

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Algunas ideas:

* El buffer podría declararse como variable temporal dentro del bloque de sentencias de la macro NB_scanf(), y luego descartarlo al final del bloque.

* La estructura anónima que colecta los errores podría dejar de ser anónima y pasarse como parámetro a la macro "por referencia", para luego ser modificada internamente en forma más "seguro", evitando efectos colaterales.

Ahora que hemos encontrado el modo de usar macros como funciones, podemos tratarlas casi de la misma manera.

Una diferencia esencial es que nuestra implementación de las macros en formato de bloques encerrados por llaves {  } impide que se retorne un valor, como lo haría una función.


19 Agosto, 2013, 11:00 pm
Respuesta #3

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Para evitar efectos "colaterales", y para que la macro NB_scanf() se comporte del mismo modo que lo haría una función, se han hecho los siguientes cambios:

* Ahora hay una struct nb_s que define un tipo para colectar errores de NB_scanf() en una llamada a dicha macro. (El programador tiene que declarar una variable de ese tipo para poder colectar dichos errores, y más aún, para que NB_scanf() siquiera funcione).

* El tipo struct nb_s viene a reemplazar la variable global nb_input que había antes, la cual ha sido eliminada. Más aún, se le ha quitado el campo buffer.

* Hay un nuevo parámetro por "referencia" en la macro, de nombre ERR. Le "exigimos" que sea de tipo (struct nb_s *), con las técnicas que aprendimos arriba (ver problema 7). Esto permite tratarlo internamente como un parámetro "por referencia".

* Ahora el buffer para leer datos está declarado explícitamente dentro de la macro, como un array  de duración temporal, sólo durante el tiempo que dure el bloque de la macro. Se llama __buffer[], y tiene el mismo tamaño de siempre: __INPUT_STR_LEN (que fijamos en 130).

* Hemos cambiado, consecuentemente, los comentarios asociados a la estructura nb_s y a la macro NB_scanf().

Los cambios son estos:

nb_s


/* */
/* struct nb_s { ... } ;
/* =========================
/*
/* Es una estructura que sirve para gestionar errores de la entrada estándar.
/* Sus campos son:
/*
/*    bool fatalerr:
/*          Vale true si en la última operación de entrada con NB_SCANF()
/*          se ha utilizado una cadena de formato errónea.
/*          No todas las cadenas de formato erróneas producen "true".
/*          En general es un error del programador, y no del usuario.
/*    int special:
/*          Contiene un entero que significa algún tipo de comando "especial"
/*          que el usuario ha introducido con el teclado.
/*          Si el valor es NB_NOT_SPECIAL ( == 0), quiere decir que
/*          el usuario ha intentado ingresar datos "normales".
/*    int errno:
/*          Contiene el resultado de la operación de entrada,
/*          el cual coincide con el que generaría sscanf(),
/*          siempre que su valor sea modificado por las operaciones "internas"
/*          de éste archivo de librería.
/*    bool userinputerr:
/*          Es un "flag" (banderín) que informa si la última operación
/*          de entrada ha tenido errores (true) o ha sido exitosa (false).
/*          Para ser consecuentes con este significado,
/*          se inicializa su valor a "false" al principio del programa.
/*
/* */

#define __INPUT_STR_LEN  130  /* Longitud string datos entrados por teclado. */
#define NB_NOT_SPECIAL     0  /* Debe ser siempre 0 */

struct nb_s {
    bool fatalerr;
    int special;
    int  errno;
    bool userinputerr;
};



[cerrar]

Comentarios previos de NB_scanf()

/* (macro:) NB_scanf(ERR, SPFUNC, FMT, ...) 
/*
/* Macro para la entrada de datos, al estilo de scanf() o sscanf().
/* (Es decir, acepta un número variable de parámetros).
/*
/* ERR:
/*    Es un puntero a una estructura de tipo struct nb_s, o sea:
/*    (struct nb_s)*
/*    Se usará como parámetro por "referencia" para albergar estados de error.
/* SPFUNC:
/*    Es el nombre de una función del tipo: int (* ) (char*) (o compatible...).
/*    Es decir, una función que acepta un char* como único parámetro,
/*    y que retorna un int.
/*    Si se pasan otros tipos de objetos o de funciones incompatibles,
/*    el compilador lo detectará.
/*    Se utiliza para detectar comandos especiales ingresados por el usuario.
/*    Cada comando tendrá asociado un número entero distinto de 0,
/*    y el número asignado dependerá de la función SPFUNC,
/*    que es proveída por el programador.
/* FMT:
/*    Es una cadena de formato que funciona igual que las de scanf().
/*    (Sólo el caso de dígitos interpuestos entre un par de % %
/^     fuciona de modo diferente. Sin embargo, este caso es erróneo tanto
/*     para sscanf como para NB_SCANF() ).
/* ...:
/*    Número variable parámetros, en donde se han de consignar
/*    las direcciones de memoria de las variables a ser asignadas durante
/*    el proceso de lectura de datos
/*    (según las directivas de tipos de datos consignados en FMT).
/*
/* Para usar la macro NB_scanf() se deben consignar todos los parámetros
/* arriba explicados, y además al menos 1 parámetro más en la lista variable.
/* Al principio la macro define variables de los tipos deseados,
/* e intenta asignarles directamente los parámetros.
/* Si esto no fuera posible, el programa no compilará.
/* En el resto de la macro se usan esas variables, pues tienen tipos concretos.
/*
/* Se analiza la cadena de formato FMT, y si contiene un error se informa
/* en el campo ERR->fatalerr, terminando la macro inmediatamente.
/*
/* Si no, se lee una línea de la entrada estándar (hasta el 1er fin de línea).
/* Estos caracteres se guardan en nb.buffer.
/*
/* Con la función SPFUNC() (pasada como parámetro por el programador),
/* se analiza la línea de entrada, y se intenta detectar un comando "especial".
/* En caso afirmativo (un valor no nulo), esto se registra en la variable
/* ERR->special, y se termina la macro inmediatamente.
/*
/* Luego se "releen" los caracteres del buffer, y se intenta convertirlos
/* en valores apropiados, según indique la cadena de formato FMT,
/* pasando estos valores a la lista variable de parámetros.
/* El método en que se realiza esta asignación es idéntico al de sscanf().
/* El resultado de esta operación es un entero (int),
/* igual al valor que generaría una llamada a sscanf().
/* Este valor queda guardado en:
/*         ERR->errno
/* Se calcula la cantidad de asignaciones esperadas
/* (esto se logra calculando la cantidad de caracteres '%' de FMT
/* que realmente se usarán para asignar valores a variables).
/* El cálculo se hace con la función:
/*        int nb_scanfdirs(char* );
/* Se compara este valor con el almacenado en nb_input.errno,
/* y el resultado se guarda como un valor bool en
/*        ERR->userinputerr
/* Su valor es "true" cuando la comparación de los dos valores falla.
/* Esto es indicativo de "error" de lectura: significa que el usuario
/* ha ingresado datos erróneos, y esto quedar consignado en
/* la variable ERR->userinputerr.
/*
/* La rutina que llamó a la macro NB_scanf() tiene que verificar
/* los campos de nb = *ERR, y actuar en consecuencia.
/* El modo de hacer esto tiene que ser el siguiente:
/*
/* struct nb_s nb;
/* if(nb.fatalerr)
/*   /* Finalizar el programa */
/* else if (nb.special)
/*       switch(nb.special) {
/*           case 1: /* ejecutar rutina 1 */
/*           break;
/*           case 2: /* ejecutar rutina 2 */
/*           break;
/*           /* ... ^/
/*           case N: /* ejecutar rutina N */
/*           break;
/*       }
/* else if (nb.userinputerr)
/*     /* Realizar acción asociada a ingreso de datos erróneos del usuario */
/* else
/*    /* Entrada de datos "normal", compatible con la cadena de formato FMT */
/*    /* Realizar acciones con los datos ingresados por el usuario */
/*
/* Ejemplo de uso:
/*
/* #include nbio0100.h
/*
/* int main(void) {
/*     float x, y, z;
/*
/*     struct nb_s nb;
/*     NB_scanf(&nb, blank, "%f %f %f", &x, &y, &z);
/*
/*     if(nb.fatalerr)
/*     else if (nb.special)
/*          switch(nb.special) {
/*              case 1: return 0; /* FIN DEL PROGRAMA */
/*              break;
/*          }
/*     else if (nb.userinputerr)
/*         printf("No puedo calcular nada con esa actitud tuya!! :( \n\n");
/*     else
/*         printf("Datos correctos!\nSu promedio es: %f\n\n", (x+y+z)/3);
/*
/*     return 0;   
/* }
/*
/* Precauciones:
/* =============
/*      La cadena de formato FMT sólo reconoce directivas de C99.
/*      Opciones adicionales o de C11 y ss. no son reconocidas.
/* */

[cerrar]

La macro NB_scanf()

#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)

#define NB_scanf(ERR, SPFUNC, FMT, ...)  \
   __BegSafeBlock  \
       struct nb_s *__nb = (ERR);                 \
       int (*__spfunc)(char*) = SPFUNC;    \
       char *__fmt = (FMT);                \
       \
       __nb->fatalerr = false;             \
       __nb->special  = NB_NOT_SPECIAL;    \
       __nb->errno    = 0;                 \
       __nb->userinputerr = false;         \
       \
       int __n_args = nb_scanfdirs(__fmt); \
       /* nb_scanfdirs(__fmt) == número de directivas "efectivas" en __fmt */ \
       __nb->fatalerr = (__n_args < 0);                          \
       if (__nb->fatalerr)                                       \
           printf("Fatal error: format string is wrong.\n");     \
       else { \
           char __buffer[__INPUT_STR_LEN];                       \
           nb_gets(__buffer, __INPUT_STR_LEN);                   \
           __nb->special = __spfunc(__buffer);                   \
           if (__nb->special == NB_NOT_SPECIAL) {                \
               /* Caso "normal" de ingreso de datos. */          \
               /* [#] __VA_ARGS__  == lista de punteros a variables   */    \
               __nb->errno = sscanf(__buffer, __fmt, __VA_ARGS__);\
               /* __nb->errno: entero de significado análogo al      */ \
               /*                  valor de retorno de sscanf()          */ \
               __nb->userinputerr = (__nb->errno < __n_args);    \
          } /* if(nb.special == 0) */ \
          else \
             ; /* No se hace nada si nb.special != 0 */ \
       } /* else */ \
  __EndSafeBlock

[cerrar]

Para quien quiera usar la librería, aquí la copio completa:

NBIO0101.H

/* ========================================================================= */
/* NBIO.H 1.01 (Nice Basic Input Output)
/*
/* Algunas rutinas confortables para E/S
/*
/* 2013/ago/18
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
/*
/* Esta librería provee una interface sencilla y sólida para funciones de E/S.
/*
/* Hay una macro principal NB_scanf(), que reemplaza a la función scanf(),
/* y se encarga de leer la entrada estándar (teclado) de forma segura,
/* y gestionar situaciones de error de forma genérica.
/*
/* La estructura struct nb_s { ... } contiene campos con señales de error
/* que controlan la entrada por teclado.
/*
/* void nb_set_string(char *, char *):
/*    Copia la cadena "derecha" en la cadena "izquierda".
/*
/* bool blank(char* ):
/*    Indica si todos los caracteres de una cadena son blancos
/*    (acorde a isspace() de ctype.h, C99).
/*
/* bool char_in_str(char, char*):
/*    Indica si un caracter dado está en una cadena.
/*
/* char* nb_gets(char* buffer, int N):
/*     Lee la entrada estándar stdin hasta un máximo de N - 1 caracteres,
/*     y guarda los caracteres leídos en buffer.
/*     Tiene una semántica análoga a la de fgets().
/*     Si los caracteres leídos son N - 2 o menos,
/*     se incluye el caracter '\n' al final.
/*     En cualquier caso, se agrega un caracter nulo '\0' al final.
/*     Si quedan caracteres sin leer antes de alcanzar el fin de línea,
/*     se descartan todos los caracteres sobrantes y también el fin de línea.
/*
/*     Importante:
/*       Si N <= 0 retorna NULL no hace nada: retorna NULL.
/*       Si N >= 1 funciona de manera "normal".
/*
/* int nb_scanfdirs(char *):
/*     Analiza una cadena de formato para contar las directivas válidas
/*     asignables a argumentos en una sentencia de tipo scanf().
/*
/* void enter_to_continue(char *):
/*     Muestra un mensaje y
/*     espera a que el usuario presoina ENTER para continuar.
/* */

/*
/* ========================================================================= */

#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>

/* */
/* struct nb_s { ... } ;
/* =========================
/*
/* Es una estructura que sirve para gestionar errores de la entrada estándar.
/* Sus campos son:
/*
/*    bool fatalerr:
/*          Vale true si en la última operación de entrada con NB_SCANF()
/*          se ha utilizado una cadena de formato errónea.
/*          No todas las cadenas de formato erróneas producen "true".
/*          En general es un error del programador, y no del usuario.
/*    int special:
/*          Contiene un entero que significa algún tipo de comando "especial"
/*          que el usuario ha introducido con el teclado.
/*          Si el valor es NB_NOT_SPECIAL ( == 0), quiere decir que
/*          el usuario ha intentado ingresar datos "normales".
/*    int errno:
/*          Contiene el resultado de la operación de entrada,
/*          el cual coincide con el que generaría sscanf(),
/*          siempre que su valor sea modificado por las operaciones "internas"
/*          de éste archivo de librería.
/*    bool userinputerr:
/*          Es un "flag" (banderín) que informa si la última operación
/*          de entrada ha tenido errores (true) o ha sido exitosa (false).
/*          Para ser consecuentes con este significado,
/*          se inicializa su valor a "false" al principio del programa.
/*
/* */

#define __INPUT_STR_LEN  130  /* Longitud string datos entrados por teclado. */
#define NB_NOT_SPECIAL     0  /* Debe ser siempre 0 */

struct nb_s {
    bool fatalerr;
    int special;
    int  errno;
    bool userinputerr;
};

void nb_set_string(char* restrict, const char* restrict);
bool char_in_str(char, char*);
int blank(char* );
char* nb_gets(char *, int);
int nb_scanfdirs(char *);
void enter_to_continue(char* msg);

/* (macro:) NB_scanf(ERR, SPFUNC, FMT, ...) 
/*
/* Macro para la entrada de datos, al estilo de scanf() o sscanf().
/* (Es decir, acepta un número variable de parámetros).
/*
/* ERR:
/*    Es un puntero a una estructura de tipo struct nb_s, o sea:
/*    (struct nb_s)*
/*    Se usará como parámetro por "referencia" para albergar estados de error.
/* SPFUNC:
/*    Es el nombre de una función del tipo: int (* ) (char*) (o compatible...).
/*    Es decir, una función que acepta un char* como único parámetro,
/*    y que retorna un int.
/*    Si se pasan otros tipos de objetos o de funciones incompatibles,
/*    el compilador lo detectará.
/*    Se utiliza para detectar comandos especiales ingresados por el usuario.
/*    Cada comando tendrá asociado un número entero distinto de 0,
/*    y el número asignado dependerá de la función SPFUNC,
/*    que es proveída por el programador.
/* FMT:
/*    Es una cadena de formato que funciona igual que las de scanf().
/*    (Sólo el caso de dígitos interpuestos entre un par de % %
/^     fuciona de modo diferente. Sin embargo, este caso es erróneo tanto
/*     para sscanf como para NB_SCANF() ).
/* ...:
/*    Número variable parámetros, en donde se han de consignar
/*    las direcciones de memoria de las variables a ser asignadas durante
/*    el proceso de lectura de datos
/*    (según las directivas de tipos de datos consignados en FMT).
/*
/* Para usar la macro NB_scanf() se deben consignar todos los parámetros
/* arriba explicados, y además al menos 1 parámetro más en la lista variable.
/* Al principio la macro define variables de los tipos deseados,
/* e intenta asignarles directamente los parámetros.
/* Si esto no fuera posible, el programa no compilará.
/* En el resto de la macro se usan esas variables, pues tienen tipos concretos.
/*
/* Se analiza la cadena de formato FMT, y si contiene un error se informa
/* en el campo ERR->fatalerr, terminando la macro inmediatamente.
/*
/* Si no, se lee una línea de la entrada estándar (hasta el 1er fin de línea).
/* Estos caracteres se guardan en nb.buffer.
/*
/* Con la función SPFUNC() (pasada como parámetro por el programador),
/* se analiza la línea de entrada, y se intenta detectar un comando "especial".
/* En caso afirmativo (un valor no nulo), esto se registra en la variable
/* ERR->special, y se termina la macro inmediatamente.
/*
/* Luego se "releen" los caracteres del buffer, y se intenta convertirlos
/* en valores apropiados, según indique la cadena de formato FMT,
/* pasando estos valores a la lista variable de parámetros.
/* El método en que se realiza esta asignación es idéntico al de sscanf().
/* El resultado de esta operación es un entero (int),
/* igual al valor que generaría una llamada a sscanf().
/* Este valor queda guardado en:
/*         ERR->errno
/* Se calcula la cantidad de asignaciones esperadas
/* (esto se logra calculando la cantidad de caracteres '%' de FMT
/* que realmente se usarán para asignar valores a variables).
/* El cálculo se hace con la función:
/*        int nb_scanfdirs(char* );
/* Se compara este valor con el almacenado en nb_input.errno,
/* y el resultado se guarda como un valor bool en
/*        ERR->userinputerr
/* Su valor es "true" cuando la comparación de los dos valores falla.
/* Esto es indicativo de "error" de lectura: significa que el usuario
/* ha ingresado datos erróneos, y esto quedar consignado en
/* la variable ERR->userinputerr.
/*
/* La rutina que llamó a la macro NB_scanf() tiene que verificar
/* los campos de nb = *ERR, y actuar en consecuencia.
/* El modo de hacer esto tiene que ser el siguiente:
/*
/* struct nb_s nb;
/* if(nb.fatalerr)
/*   /* Finalizar el programa */
/* else if (nb.special)
/*       switch(nb.special) {
/*           case 1: /* ejecutar rutina 1 */
/*           break;
/*           case 2: /* ejecutar rutina 2 */
/*           break;
/*           /* ... ^/
/*           case N: /* ejecutar rutina N */
/*           break;
/*       }
/* else if (nb.userinputerr)
/*     /* Realizar acción asociada a ingreso de datos erróneos del usuario */
/* else
/*    /* Entrada de datos "normal", compatible con la cadena de formato FMT */
/*    /* Realizar acciones con los datos ingresados por el usuario */
/*
/* Ejemplo de uso:
/*
/* #include nbio0100.h
/*
/* int main(void) {
/*     float x, y, z;
/*
/*     struct nb_s nb;
/*     NB_scanf(&nb, blank, "%f %f %f", &x, &y, &z);
/*
/*     if(nb.fatalerr)
/*     else if (nb.special)
/*          switch(nb.special) {
/*              case 1: return 0; /* FIN DEL PROGRAMA */
/*              break;
/*          }
/*     else if (nb.userinputerr)
/*         printf("No puedo calcular nada con esa actitud tuya!! :( \n\n");
/*     else
/*         printf("Datos correctos!\nSu promedio es: %f\n\n", (x+y+z)/3);
/*
/*     return 0;   
/* }
/*
/* Precauciones:
/* =============
/*      La cadena de formato FMT sólo reconoce directivas de C99.
/*      Opciones adicionales o de C11 y ss. no son reconocidas.
/* */

/* ========================================================================= */
/* Implementación:
/* */


void nb_set_string(char* restrict dest, const char* restrict src) {
    /* Copia src en dest */
    for( ; (*dest = *src) != '\0'; src++, dest++)
      ;
}
/* ---> OUT: dest == src, strlen(dest) == strlen(src) */


/* Mostrar un mensaje al usuario y esperar a que presione ENTER para seguir. */
/* Se usa una combinación de fgets(..., stdin) y getchar() para              */
/* leer de forma correcta caracteres "basura" hasta alcanzar el fin de línea */
/* (Con GCC <= 4.8 no es compatible con previas llamadas directas scanf())   */
void enter_to_continue(char* msg) {
      puts(msg);
     
      char minibuff[1] = "";
      fgets(minibuff, 1, stdin);
      /* buffer[0] == '\0'; */
      /* Se han leído 0 caracteres            */
      /*     ---> No se ha podido leer el fin de línea */
      while(getchar()!='\n') ;
}

/* Se usa la función "segura" fgets() en vez de gets()                     */
/* No permite al usuario ingresar cadenas de longitud mayor que el buffer. */
/* buffer debe tener al menos N caracteres disponibles.                    */
/* Si buffer no es un array con tamaño suficiente, nb_gets() no es válida. */
char* nb_gets(char *buffer, int N) {
    if (N <= 0)
        /* No hay espacio para leer */
        return NULL;
    /* N >= 1 */
    if (N == 1) {
        /* Sólo hay espacio para el caracter nulo        */
        fgets(buffer, N, stdin);
        /* buffer[0] == '\0'; */
        /* Se han leído N - 1 == 0 caracteres            */
        /*     ---> No se ha podido leer el fin de línea */
        while(getchar()!='\n')
          ;
                 
        return buffer;
    };
    /* N >= 2 */   
     
    buffer[N-2] = '\0'; /* Limpieza rápida del buffer (ver abajo) */
    fgets(buffer, N, stdin);
    /* Se cumple la siguiente propiedad:                                */
    /* (no_se_alcanzó_fin_de_línea)                                     */
    /*          <---->                                                  */
    /* (buffer[N-2] != '\0') && (buffer[N-2] != '\n')                 */
    /* Por lo tanto es suficiente limpiar el caracter .buffer[N-2]      */
   
    if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
       /* No se ha alcanzado el fin de línea */
       while ( getchar() != '\n' ) /* flush_stdin... */
            /* ---> produce una descarga de stdin */
            /*      descartando caracteres hasta llegar al fin de línea */
           /* */ ;
    /* else ; */
      /* Sí se ha alcanzado el fin de línea: no se hace nada. */
     
    return buffer;
}

/* Buscar un caracter en una cadena */
bool char_in_str(char c, char* s) {
   for ( ; *s; s++)
      if (c == *s)
         return true; /* c "pertenece" a s */
   /* c "no pertenece" a s */   
   return false;     
}

/* Conjunto de directivas de reemplazo efectivo por argumentos válidos      */
/* en funciones del estilo de scanf(), en la librería stdio.h, de C99 o ss. */

#define SCANF_SAFE_ARG_SET_C99 "hljztLdiouxaefgc[pAEFGXn0123456789"
#define SCANF_SAFE_SPE_SET_C99 "*"
#define SCANF_SAFE_DIGITS      "0123456789"

/* Indicador de que se halló una directiva no válida en la cadena de formato */
#define NB_FMT_NOT_VALID -1 /* Debe ser < 0 */

/* El entero retornado por nb_scanfdirs() es: */
/* n >= 0: Se hallaron n directivas válidas (C99) para argumentos reales.   */
/* n < 0:  Error: hay directivas no reconocidas.                            */
/* Nota adicional: Las directivas que contienen dígitos antepuesto ("%20f") */
/* son reconocidas correctamente. */
/* Nota ténica: */
/* Los casos "%digitos%" son erróneos en C99 (ver sección 7.19.6.2.12).     */
/* pero al mismo tiempo son reconocidos inexactamente por __nb_N_ARGS().   */
/* El resultado es un programa erróneo en cualquier caso, aunque            */
/* la "semántica" para este caso de sscanf() y NB_SCANF() no coincidirá.    */
int nb_scanfdirs(char *s) {
    int n;
    bool before = false;
   
    for (n = 0; *s; s++)
      /* n == 0 ---> before == false */
      /* n > 0  --->                                              */
      /*     [ (último caracter leído no-dígito de s) == '%'      */
      /*               <----> before == true ]                    */
      if (*s == '%')
        /* Recordar que se ha encontrado un signo '%' en esta posición, */
        /* pero no tomar ninguna acción.                                */
        /* Si antes ya había un '%', es que estamos ante el caso %%,    */
        /* y entonces se debe "cancelar" el estado del flag "before".   */
        /* Si no, registramos la aparición de '%' como true,            */
        /* para ser analizado en la siguiente iteración.                */
        before = ( (before)? false: true );
      else {
        /* El caracter actual no es '%' */   
        if (before)
          /* *(s-1) == '%' (el caracter previo era un '%')          */
          /* Directiva % detectada.                                 */
          if (char_in_str(*s, SCANF_SAFE_DIGITS))
             /* Si se encuentra un dígito, mejor ignorarlo,                 */
             /* y seguir buscando el próximo caracter que no sea un dígito. */
/* CONTINUE */
             continue;
             /* before == true */
             /*     ---> Directivas con dígitos se reconocen correctamente. */
             
          else if (char_in_str(*s, SCANF_SAFE_ARG_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* que además corresponde a un argumento real.             */
             /* ---> entonces incrementar el contador.                  */
             n++;
          else if (char_in_str(*s, SCANF_SAFE_SPE_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* pero que no corresponde a un argumento real.            */
             ; /* Nada */
          else
             /* Se encontró una directiva desconocida. */
             /* Se termina abruptamente la función y retorna error. */
/* RETURN */
             return NB_FMT_NOT_VALID; /* < 0 */
        else /* if(before) */
          ;
                   
        before = false;
        /* Se registra para la iteración siguiente */
        /* que el caracter actual no es un '%'     */
      }
    /* n == número de directivas % halladas en s.                        */
    /* Sólo se cuentan casos que efectivamente se asignan a argumentos,  */
    /* de los tipos especificados por el estándar C99.                   */
    /* No se cuentan los casos de %%, %*.                                */
           
    return n;         
}

int blank(char *s) {
    for ( ; *s; ++s)
       if (!isspace(*s))
          /* Se ha encontrado un caracter que no es blanco. */
          return false;
     
    /* s es una string que sólo contiene blancos (al "estilo" isspace()) */
    return true;
}


#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)

#define NB_scanf(ERR, SPFUNC, FMT, ...)  \
   __BegSafeBlock  \
       struct nb_s *__nb = (ERR);                 \
       int (*__spfunc)(char*) = SPFUNC;    \
       char *__fmt = (FMT);                \
       \
       __nb->fatalerr = false;             \
       __nb->special  = NB_NOT_SPECIAL;    \
       __nb->errno    = 0;                 \
       __nb->userinputerr = false;         \
       \
       int __n_args = nb_scanfdirs(__fmt); \
       /* nb_scanfdirs(__fmt) == número de directivas "efectivas" en __fmt */ \
       __nb->fatalerr = (__n_args < 0);                          \
       if (__nb->fatalerr)                                       \
           printf("Fatal error: format string is wrong.\n");     \
       else { \
           char __buffer[__INPUT_STR_LEN];                       \
           nb_gets(__buffer, __INPUT_STR_LEN);                   \
           __nb->special = __spfunc(__buffer);                   \
           if (__nb->special == NB_NOT_SPECIAL) {                \
               /* Caso "normal" de ingreso de datos. */          \
               /* [#] __VA_ARGS__  == lista de punteros a variables   */    \
               __nb->errno = sscanf(__buffer, __fmt, __VA_ARGS__);\
               /* __nb->errno: entero de significado análogo al      */ \
               /*                  valor de retorno de sscanf()          */ \
               __nb->userinputerr = (__nb->errno < __n_args);    \
          } /* if(nb.special == 0) */ \
          else \
             ; /* No se hace nada si nb.special != 0 */ \
       } /* else */ \
  __EndSafeBlock
   
/* La longitud de la cadena __buffer es __INPUT_STR_LEN.
/* Tiene un valor de 130, suficiente para la consola típica de 128 caracteres,
/* más 1 caracter nulo que agrega al final la función gets().
/* Si se va a correr el programa en un entorno donde la consola admita entradas
/* más extensas, este valor no sería suficiente.
/* (Mas, sería preferible recortar la entrada, antes que agrandar el buffer.)
   

/* Quitamos las definiciones de los símbolos definidos en la librería */
/* para que queden inaccesibles desde archivos externos. */

#undef SCANF_SAFE_ARG_SET_C99
#undef SCANF_SAFE_SPE_SET_C99
#undef SCANF_SAFE_DIGITS
#undef NB_FMT_NOT_VALID


[cerrar]

(Modificación: He cambiado las llaves del cuerpo de la macro por la técnica explicada en el Problema 8)