19/08/2019, 01:14:42 am *
Bienvenido(a), Visitante. Por favor, ingresa o regístrate.
¿Perdiste tu email de activación?

Ingresar con nombre de usuario, contraseña y duración de la sesión
Noticias: ¡Atención! Hay que poner la matemática con LaTeX, y se hace así (clic aquí):
 
 
Páginas: [1]   Ir Abajo
  Imprimir  
Autor Tema: (C) Máximo Común Divisor - Algoritmo de Euclides  (Leído 12558 veces)
0 Usuarios y 1 Visitante están viendo este tema.
argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« : 16/08/2013, 07:44:46 pm »

Por el mero gusto de programar, voy aquí a escribir un programa en C del Algoritmo de Euclides, que permite calcular el máximo cómun divisor de dos números dados.

Dados dos enteros [texx]m, n[/texx], el máximo común divisor de [texx]m[/texx] y [texx]n[/texx] es el máximo número entero [texx]g[/texx] tal que [texx]g[/texx] es divisor tanto de [texx]m[/texx] como de [texx]n[/texx].
Con esa definición, el máximo común divisor siempre resulta no negativo: [texx]g\geq 0[/texx].

¿Cuánto es [texx]g[/texx] si alguno de los números es 0?
Todo número entero es divisor de 0, así que si [texx]m = 0[/texx] y [texx]n > 0[/texx], entonces [texx]g = n[/texx], porque [texx]n[/texx] divide a [texx]0[/texx] y [texx]n[/texx] simultáneamente, y es el máximo entero que puede hacer eso.

Finalmente, si [texx]m = n = 0[/texx], ocurre que todo entero [texx]g[/texx] divide a ambos valores... por lo tanto no hay un divisor común que sea el "máximo", y de ahí que podamos escribir algo como [texx]g = \infty[/texx].
Sin embargo, conviene decir que en este caso el valor de [texx]g[/texx] no está definido.

Cuando existe, el máximo común divisor será denotado como [texx]gcd(m, n)[/texx].



Euclides observó que si [texx]m > n[/texx], entonces el máximo común divisor de [texx]m[/texx] y [texx]n[/texx] coincide con el máximo común divisor de [texx]n[/texx] y el [texx]m \% n[/texx] (esto denota al resto de la división [texx]m / n[/texx]):

[texx]gcd(m, n) = gcd(n, m \% n).[/texx]

Spoiler: Demostración. (click para mostrar u ocultar)

Por definición, cuando [texx]n[/texx] es positivo, el resto de la división [texx]m / n[/texx], es el menor número [texx]r[/texx] que satisface [texx]0 \leq r < n[/texx], [texx]m = nq + r[/texx], para algún entero [texx]q[/texx] (que es el cociente).

Lo que nos interesa de esto es que [texx]r < n[/texx].

Si repetimos el procedimiento, vemos que:

[texx]gcd(m, n) = gcd(n, m \% n) = gcd(n , r) = gcd(n, n  \% r).[/texx]

O sea que podemos calcular el máximo común divisor calculando sucesivos restos de divisiones, que van decreciendo estrictamente, hasta llegar a algún paso en el que el resto obtenido sea 0. Ahí el máximo común divisor se obtiene trivialmente, aprovechando que:

[texx]gcd(x, 0) = x.[/texx]

Sabiendo que esto funciona así de bien, podemos ponerlo en un programa.



Supongamos que tenemos dos números enteros positivos tales que [texx]m  > n>0[/texx].
El algoritmo que usaríamos sería esto:

* Calcular el resto [texx]r = m\% n[/texx].
* Reemplazar el viejo valor de [texx]m[/texx] por [texx]n[/texx], y el viejo valor de [texx]n[/texx] por [texx]r[/texx].
* Tras hacer eso nos queda que [texx]m>n[/texx], y podemos volver desde el principio, porque el máximo común divisor entre estos dos nuevos valores coincide con el máximo común divisor de los valores [texx]m, n[/texx] originales.
* En cada paso se obtiene siempre que [texx]0< r  < n[/texx], y entonces los valores de [texx]m, n[/texx], van decreciendo. Como se trata de números enteros positivos, hay una cantidad finita de pasos tras la cual esto termina. Esto quiere decir que habrá un momento en el que [texx]r[/texx] no podrá seguir siendo positivo, o sea, se obtendrá [texx]r = 0[/texx].
* Cuando esto ocurre, no conviene dividir por 0 porque genera un error en el programa...
* Sin embargo observamos que la búsqueda ha terminado, porque cuando [texx]m > n = 0[/texx], lo que significa es que [texx]gcd(m, n) = gcd(m, 0) = m[/texx]. Esto quiere decir que nos hemos "topado" con el máximo común divisor que buscábamos, y podemos informar que ese valor es el resultado buscado.



Podemos ahora definir una función gcd() en C, que resuelva este problema, así:


int gcd(int m, int n) {
    /* Suponemos m > n > 0 */
    do {
        int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) >= 0 */
    } while(n > 0);

    /* n == 0, m == gcd(original m, original n) */

    return m;   
}


La función gcd() toma dos parámetros enteros [texx]m, n[/texx], y retorna como resultado otro número entero, el máximo común divisor de [texx]m[/texx] y [texx]n[/texx].
Por ahora suponemos que [texx]m > n > 0[/texx], pero después tendremos que considerar todos los casos posibles.

A continuación iniciamos un bucle del tipo do {} while();
El primer paso calcula el resto de dividir m y n, y luego definiremos nuevos valores para m, n, así:

* El más pequeño de los "viejos" valores [texx]m, n[/texx], será el "nuevo" valor asignado a [texx]m[/texx].
* El valor del resto de la división de los "viejos" valores [texx]m, n[/texx] será el "nuevo" valor de [texx]n[/texx].
* Reordenar los valores, si fuera necesario, de manera que [texx]m > n[/texx].

Cada vez que hay que reordenar dos valores, hace falta usar una variable auxiliar, que llamaremos [texx]temp[/texx].
Observando que ya habíamos tomado de entrada que m > n, el más pequeño de los dos será n.
Y como el resto [texx]r = m \% n[/texx] satisface siempre que [texx]n > r[/texx], no hay nada que reordenar:
Guardamos en [texx]temp[/texx] el "viejo" valor de [texx]n[/texx], porque lo vamos a necesitar en la siguiente iteración, el cual será el "nuevo" [texx]m[/texx].
Calculamos el resto [texx]m\% n[/texx], y lo guardamos en el "nuevo" [texx]n[/texx], sabiendo ya de antemano que este valor será más que pequeño que el "nuevo" [texx]m[/texx].

Ahora bien, como estamos usando las mismas variables [texx]m, n[/texx], para denotar los valores "viejos" y "nuevos", hay que tener cuidado en el orden en que efectuamos las asignaciones, para no perder información.
Lo que conviene es guardar en [texx]temp[/texx] el "viejo" [texx]n[/texx], luego calcular el "nuevo" [texx]n[/texx], y por último definir el "nuevo" [texx]m[/texx] igual al valor previamente guardado en [texx]temp[/texx].



Para garantizar que el ciclo no continúa eternamente, tenemos que aprovechar la propiedad que hemos mencionado de que, al calcular los sucesivos restos de las divisiones, los valores de [texx]m, n[/texx], van decreciendo en cada iteración.
Esto lo hemos escrito en el programa mismo como un comentario debajo de la línea que efectúa los cálculos.
Esa es la condición que implica que el ciclo terminará en una cantidad finita de pasos.

En particular, siempre tenemos la condición [texx]m > n[/texx] en cada iteración (un invariante del algoritmo), que en cierto modo es una de las claves que permite que el algoritmo avance sin problemas.

Además, como ya hemos mencionado, el algoritmo termina cuando el resto es 0.
Así que, para que el ciclo se repita, hemos puesto la condición [texx](n > 0)[/texx] en el do {} while();

Más aún, tras terminar el ciclo podemos estar seguros que [texx]n == 0[/texx] y que [texx]m > 0[/texx].
En ese caso, el máximo común divisor es exactamente [texx]m[/texx], sin lugar a dudas, y el proceso ha terminado.
Así, este valor es el que debe devolver la función gcd() como resultado.



En los posts que siguen voy a agregar los detalles que faltan.
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #1 : 16/08/2013, 10:02:39 pm »

Lo que tenemos que hacer ahora es arreglar la función gcd() para que funcione en todos los casos posibles.
La función tiene que ser capaz de lidiar con todo tipo de valores [texx]m, n[/texx], y retornar un valor correcto, o con sentido.

Una de las propiedades del máximo común divisor es que no depende del signo:

[texx]gcd(m,n)= gcd(|m|,|n|).[/texx]

Así, cuando alguno de los parámetros [texx]m,n[/texx], es negativo, esto no tiene que importar.

Lo que haremos será sustituir los parámetros originales [texx]m, n[/texx], por sus valores absolutos,
mediante la sentencia siguiente:


m = abs(m);
n = abs(n);


Aquí es necesario invocar la función abs(), que retorna el valor absoluto de un número.
Podemos usar las matemáticas predefinidas de C, o bien realizar nuestra propia versión.

Suelo preferir no sobrecargar los programas invocando librerías.
Así que voy a poner mi propia versión de "función valor absoluto".
He aquí el prototipo:


int abs(int);


La función abs() tomará como parámetro un número entero (tipo int) y retornará como resultado su valor absoluto, como un dato de tipo int.



Es conveniente documentar correctamente el programa indicando estas dos propiedades matemáticas:

* La propiedad lógico-matemática que le da sentido a una determinada sentencia del programa.
* La condición lógica que siempre se cumple tras ejecutar dicha sentencia.



Así, mejor escribimos las sentencias anteriores rodeadas de comentarios:


    m = abs(m);
    n = abs(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */


La 3er línea contiene un comentario que expresa que el máximo común divisor será invariante respecto la aplicación de valor absoluto a cada parámetro. Esto es consecuente con la matemática subyacente.

En la 4ta línea se expresan las condiciones [texx]m\geq 0, n\geq 0[/texx], que son invariantes del algoritmo.
Lo que significa es que: siempre que el programa llegue a ese punto de ejecución, las condiciones [texx]m\geq 0, n\geq 0[/texx], son verdaderas.



Resuelto el problema de los signos, tenemos ahora que intentar poner las cosas en el formato cómodo que ya habíamos estudiado, a saber: [texx]m>n>0[/texx].
Hay que manejar los casos en que algún parámetro es 0, o en que ambos parámetros son iguales, etc.

Lo que conviene hacer, digo yo, es reordenar los parámetros de manera que [texx]m\geq n[/texx].
Y como ya hemos logrado que ambos sean no-negativos, tendremos más precisamente: [texx]m\geq n\geq 0[/texx].
Si ya fuera cierto que [texx]m\geq n[/texx], entonces no haríamos nada.
En cambio, si [texx]m<n[/texx], intercambiaríamos los papeles de [texx]m, n[/texx].

Esto se puede hacer sin problemas porque la operación de máximo común divisor es "conmutativa", es decir, no varía si invertimos los parámetros:

[texx]gcd(m, n) = gcd(n,m).[/texx]

Esta propiedad es la que nos autoriza a intercambiar los valores de las variables para que nos quede del modo que a nosotros nos resulte cómodo.

El intercambio de valores entre [texx]m, n[/texx], requiere, como es usual, el uso de una variable temporal.
Por ahora vamos a hacerlo de forma sencilla, así:


 /* swap m and n ... */
{
   int temp = m; m = n; n = m;
}


La sentencia completa luce así:


    if (m < n)
       /* swap m and n ... */
      {
            int temp = m; m = n; n = m;
      }
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n >= 0*/


Como se puede apreciar, tras realizar el "swap" (intercambio),
se cumplirá siempre la condición invariante deseada: [texx]m \geq n\geq 0[/texx].

Hemos agregado también una línea de comentario que explica el resultado matemático que se obtendrá como consecuencia del "swap": la función [texx]gcd(m, n)[/texx] dará el mismo resultado que [texx]gcd(n, m)[/texx].



El siguiente caso que quisiéramos descartar de inmediato es cuando [texx]m = n = 0[/texx].
Este caso es el único que no da un número entero válido como resultado.

Pero la función gcd() necesita retornar un número entero, porque está declarada como int.
¿Cómo lo arreglamos?

Lo primero que recordamos aquí es que, cuando el valor gcd(m, n) está bien definido, es un número entero [texx]\geq 0[/texx].
Entonces podemos usar valores negativos a modo de "valores señalizadores" para indicar que algo anduvo mal.

Esto deberá estar documentado en la cabecera de la función gcd(), pero lo haremos luego.

Ahora simplemente manejamos el caso [texx]m = n = 0[/texx] retornando un valor negativo sencillo, como [texx]-1[/texx], que para nosotros va a significar: "señal de valor infinito" o "señal de resultado no definido".


    if ( (m == 0) && (n == 0) )
       return -1;
    /* m > 0, m >= n >= 0 */


Una vez que ese "if" ha pasado al siguiente punto de ejecución del programa,
obtenemos que [texx]m\geq n\geq 0[/texx], como antes, pero además sabemos que [texx]m>0[/texx].
Estas dos condiciones las ponemos como comentario en ese punto de ejecución.



Una vez que hemos descartado el caso insalubre de [texx]m = n = 0[/texx],
todavía nos gustaría sacarnos de encima los casos triviales en que [texx]m = n \neq 0[/texx].
En esta situación es fácil calcular el máximo común divisor:

[texx]gcd(m, m) = m.[/texx]

Sin embargo, lo que realmente nos importa es sacarnos de encima el caso [texx]m = n[/texx],
porque nuestro algoritmo original funcionaba de maravillas sólo para [texx]m > n[/texx],
y queremos poder aprovechar eso.

Supongamos que [texx]m = n[/texx]. Como ya hemos descartado el caso [texx]m = n = 0[/texx], entonces tenemos la certeza ahora que [texx]m = n \neq 0[/texx], y por lo tanto el valor el máximo común divisor es [texx]m[/texx], y no [texx]\infty[/texx].

Aquí vemos, pues, un ejemplo de la utilidad de los invariantes: nos ahorra tener que evaluar condiciones que previamente ya hemos tomado en cuenta.

El código sería éste:


    if (m == n)
       return m;
    /* m == n > 0 ---> gcd(m, n) == m */
    /* m > n >= 0 */


En la 3er línea ponemos en un comentario la conclusión lógica de las operaciones que hemos efectuado: "en caso de que [texx]m = n > 0[/texx], la función [texx]gcd(m, n)[/texx] retornará [texx]m[/texx]".

En la 4ta línea tomamos nota del estado en que se encuentran las variables m, n, en el presente punto de ejecución. Al llegar allí podemos estar seguros de que [texx]m > n  \geq  0[/texx].



Sin embargo, vemos que todavía no nos hemos sacado de encima el posible caso en que n = 0.
Y puede que tampoco sea necesario...
Pero por ahora lo vamos a descartar sin más, y más tarde mejoraremos el algoritmo.

Si [texx]n = 0[/texx], como ahora [texx]m > n[/texx], obtenemos [texx]m > 0[/texx], y entonces [texx]gcd(m, 0) = m[/texx], con lo cual tranquilamente podemos retornar el valor [texx]m[/texx].



Habiendo ya descartado todos los casos "molestos", llegamos finalmente a la situación principal deseada, en que [texx]m > n > 0[/texx].
Aquí basta insertar lo que ya teníamos antes.

Además voy a agregar la línea de comentario que justifica el algoritmo:


        /* gcd(m, n) == gcd(n, m % n) */




La función gcd() completa quedaría así:


int gcd(int m, int n) {
   
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna -1, que significa: "valor infinito/indefinido" */
   
    m = abs(m); n = abs(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       /* swap m and n ... */
       {
                int temp = m; m = n; n = m;
       }
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n */
   
    if ((m == 0) && (n == 0))
       /* ---> m == n == 0 */
       return -1;
    /* m > 0, m >= n >= 0 */
   
    if (m == n)
       return m;
    /* m > 0 ---> gcd(m, m) = m */
    /* m > n >= 0 */
   
    if (n == 0)
      return m;
    /* gcd(m, 0) == m */
    /* m > n > 0 */
   
    do {
        /* m > n > 0 */
        /* gcd(m, n) == gcd(n, m % n) */
        int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
    } while(n > 0);
    /* n == 0, m == gcd(original m, original n) */
   
    return m;   
}




En el siguiente post haremos retoques totalmente inútiles,
pero que ilustran algunas técnicas de programación.
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #2 : 16/08/2013, 10:51:42 pm »

Hemos completado la definición de la función gcd(), que analiza todos los casos posibles, sin dejar cabos sueltos.
Vamos a hacer algunas pequeñas modificaciones a dicha función.

Notemos que la línea que evalúa el caso: (m == n) && (n == 0)
tiene algo de redundancia.
En efecto, el invariante previo indica que, en ese punto de ejecución, es cierta la condición [texx]m\geq n\geq 0[/texx].
Por lo tanto, si verificamos que [texx]m = 0[/texx], esto implica inmediatamente que también [texx]n = 0[/texx], y por lo tanto no hay que obigar al programa a que efectivamente haga la comprobación.

Luego, esa sentencia "if" puede cambiarse por esta otra:


    if (m == 0)
       /* ---> m == n == 0 */
       return -1;
    /* m > 0, m >= n >= 0 */


En vez de comprobar que "n == 0" en la instrucción "if",
mejor hemos "trasladado" esa condición a un comentario en la línea siguiente.

Estos comentarios pueden llamarse también "aserciones lógicas" del algoritmo.
En el punto de ejecución correspondiente a la línea inmediata debajo del "if" podemos estar seguros que [texx]m = n = 0[/texx], y entonces es válido retornar [texx]-1[/texx], que usábamos para indicar [texx]\infty[/texx].



El número [texx]-1[/texx] es lo que en programación se denomina, a veces, número "mágico".
Esto se refiere a números que no tienen significado alguno cuando una persona lee el programa en cuestión.
Si bien hemos documentado dentro de la función gcd() el significado de ese número,
conviene enmascararlo de algún modo, poniendo en evidencia el verdadero significado que tiene el numerito en cuestión.

Vamos a definir, pues, una constante llamada INFTY_SIGNAL (que significa: "señal de infinito"),
y cuyo valor pondremos igual a [texx]-1[/texx].


#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */


La directiva #define permite sustituir el símbolo INFTY_SIGNAL automáticamente por el valor [texx]-1[/texx].
Nosotros escribimos INFTY_SIGNAL y el compilador entiende [texx]-1[/texx].

Si bien para el compilador no hay diferencia alguna, en cambio para nosotros sí la hay:

* El idenfiticador INFTY_SIGNAL tiene un significado o sentido intuitivo concreto para nosotros, mientras que el número -1 es sólo un número.

* Si nos equivocamos poniendo un número distinto a -1, el compilador no notará que es un error. En cambio, una vez que INFTY_SIGNAL tiene asignado el valor -1, este valor nunca cambia. Se reduce así la posibilidad de asignar valores erróneos, difíciles de detectar.

* Si por alguna razón de diseño necesitamos cambiar el valor -1 por otro valor, no tenemos necesidad de rastrear todas las partes del programa en que figura el dichoso -1, sino que basta cambiar una sola vez, en la línea #define, el valor -1 por otro, digamos -3. La lógica del programa no se verá afectada.

* Si usamos el mismo valor -1 con otro significado en otras partes del programa, sería confuso que se usara para indicar varios sentidos distintos. Peor aún sería que se nos ocurra cambiar ese valor por otro para uno de los significados, y para el otro no. Pasaríamos toda una tarde cambiando pacientemente numeritos, con grandes riesgos de errores. En cambio, con #define, todo se puede arreglar con mayor claridad y rapidez.


¿Y qué es ese comentario que pusimos al costado, diciendo que el valor de INFTY_SIGNAL tiene que ser un número negativo?
Bueno, esto tiene que ver con lo que hemos dicho arriba, de que se nos puede ocurrir cambiar el valor de la constante -1 por algún otro que nos parezca mejor.
En ese caso, el comentario indica que podemos hacer esto, pero que no puede ponerse cualquier número ahí, sino que ha de ser un entero negativo, y de tipo int.



La función abs() que calcula el valor absoluto la implementamos así:



int abs(int x) {
    return (x < 0)? (-x): (x);
}


Hemos usado el operador ?: el cual permite abreviar engorrosas sentencias "if", transformándolas en sencillas operaciones aritméticas...

Como se ve, la función tiene una definición simple y breve:

* Toma como parámetro un entero [texx]x[/texx].
* Si [texx]x[/texx] es negativo, le cambia el signo.
* Si no, lo deja tal cual está.

El resultado es un valor de tipo int, que da el valor absoluto de [texx]x[/texx].



La operación de "swap" es muy común en la programación,
así que conviene tenerla definida en forma genérica,
para no tener que estar repitiéndola todo el tiempo cada vez que nos haga falta.

Debido a que el "swap" (o intercambio de valores entre dos variables)
es una operación que suele hacerse para cualquier tipo de datos,
no conviene restringirse a un tipo específico, sino declararla en un modo genérico.

De modo que no definiremos una función swap(), sino una macro llamada SWAP().

Dentro del "swap" hace falta una variable temporal en donde guardar por un rato el valor de una de las dos variables a intercambiar.
Aquí hace falta declarar dicha variable temporal, y esto quiere decir que tenemos que darle un tipo específico.

El tipo de la variable temporal tiene que ser el mismo que el de las dos variables que habrán de intercambiarse.
¿Cómo hace la macro SWAP para saber el tipo de datos a utilizar?

Bueno, como las computadoras no son adivinas, tendremos que decírselo.
Así, la macro SWAP aceptará 3 parámetros: el primero será un "tipo de datos", y los otros dos serán las variables a intercambiar. La definición es ésta:


/* La macro SWAP(TYPE, X, Y) toma como parámetros:
     * TYPE: un tipo de datos válido de C
     * X, Y: dos lvalues de tipo TYPE
   La macro intercambia entre sí los valores de X é Y
*/
 
#define SWAP(TYPE, X, Y) { TYPE __Temp = X; X = Y; Y = __Temp; }


Un ejemplo de uso sería éste:


SWAP(int, m, n)


Esa invocación equivale a hacer esto:


{ int __Temp = m; m = n; n = __Temp; }


Como las sentencias están dentro de un bloque encerrado por llaves { }
el efecto es que la variable __Temp tiene duración sólo durante el breve lapso de tiempo en que se ejecuta el bloque en cuestión.

Unas líneas más arriba hemos puesto 4 líneas de comentarios que explican el modo correcto de usar la macro SWAP, y cuál es el resultado esperado.

Esto hay que hacerlo por varias razones:

  • Es más importante lo que "se supone" que hace una macro o una función, que el "cómo lo hace".
    En efecto, si sabemos lo que una macro o una función tiene que hacer,
    y si resulta que la hemos programado mal,
    entonces será fácil corregirla.

    En cambio, si definimos la macro o función con todas sus operaciones,
    pero en ninguna parte describimos lo que se supone que tiene que hacer,
    es imposible darse cuenta si tiene o no un error de diseño, porque no hay ningún criterio que la valide o no.

  • En general sucede que las macros no son código "seguro", es decir,
    pueden dar lugar a comportamientos anómalos inesperados.

    Por ejemplo, en el caso de la macro SWAP(), es fácil "crackearla",
    ya que si no se respetan las condiciones exigidas en los comentarios,
    los resultados no serán los esperados.

    Ejemplo 1:

    float x = 1.0; char *s = "Hola";
    SWAP(void, x, s);

    Ejemplo 2:

    SWAP(int, 3, 4);

    Ejemplo 3:

    int x = 0, y = 1;
    SWAP(3.14, x, y);

    ___

    El Ejemplo 1 falla porque intercambia variables de distinto tipo, y encima el tipo indicado es todavía otro: void.
    El Ejemplo 2 falla porque los parámetros X, Y, no son lvalues.
    El Ejemplo 3 falla porque el parámetro TYPE es 3.14, que no es un tipo válido.




Con estas pequeñas modificaciones, la función gcd() queda así:


#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */

/* La macro SWAP(TYPE, X, Y) toma como parámetros:
     * TYPE: un tipo de datos válido de C
     * X, Y: dos lvalues de tipo TYPE
   La macro intercambia entre sí los valores de X é Y
*/
 
#define SWAP(TYPE, X, Y) { TYPE __Temp = X; X = Y; Y = __Temp; }

int abs(int x) {
    return (x < 0)? (-x): (x);
}

int gcd(int m, int n) {
   
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */
   
    m = abs(m); n = abs(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       SWAP(int, m, n);
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n */
   
    if (m == 0)
       /* ---> m == n == 0 */
       return INFTY_SIGNAL;
    /* m > 0, m >= n >= 0 */
   
    if (m == n)
       return m;
    /* m > 0 ---> gcd(m, m) = m */
    /* m > n >= 0 */
   
    if (n == 0)
      return m;
    /* gcd(m, 0) == m */
    /* m > n > 0 */
   
    do {
        /* m > n > 0 */
        /* gcd(m, n) == gcd(n, m % n) */
        int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
    } while(n > 0);
    /* n == 0, m == gcd(original m, original n) */
   
    return m;   
}





Nuestra función gcd() quedó maravillosa, pero todavía no tenemos un programa que haga algo.

En el siguiente post vamos a implementar un programa que efectivamente corra y muestre resultados de una vez por todas.


En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #3 : 16/08/2013, 11:24:33 pm »

Para interactuar con el usuario usaremos la librería estándar <stdio.h>.

Dado que quisiéramos divertirnos jugando con el máximo común divisor hasta cansarnos,
vamos a hacer que nuestro programa sea en realidad un bucle repetitivo tal que,
en cada iteración, solicite al usuario el ingreso de dos números enteros.
Tras el ingreso de datos, el programa llama a la función gcd() con esos dos números como parámetros, y muestra en pantalla el resultado.

También es cierto que el programa tiene que terminar alguna vez,
y para eso necesitamos alguna condición que indique finalización.
Una posibilidad sería la de preguntarle al usuario si desea continuar usando el programa.
Pero en esta primera versión no vamos a hacer eso.

Más bien vamos a poner una condición sencilla de terminación.
Vamos a hacer que el programa termine cuando el usuario ingrese a propósito el caso infinito,
es decir, el caso en que [texx]m = n = 0[/texx].

Para esto, guardaremos el resultado de la llamada a función gcd() en una variable de nombre ans (abreviatura de "answer" = "respuesta").
Cuando el resultado de gcd() es igual a INFTY_SIGNAL, esto quiere decir que el usuario ha ingresado los valores 0 0.
En ese caso daremos por terminado el programa.

La rutina sería así:


#include <stdio.h>

int main(void) {
   int m, n, ans;   
   
   do {
     scanf("%d %d", &m, &n);
     printf("gcd(%d, %d) = %d\n\n", m, n, ans = gcd(m, n));     
   } while (ans >= 0) ;

   return 0;
}


La primer línea invoca la librería <stdio.h> para funciones de entrada/salida de datos.
Allí están definidas las funciones clásicas scanf() (para entrada de datos por teclado) y printf() (para salida estándar de datos por pantalla).

Los parámetros %d indican a scanf() que se espera que el usuario ingreso números enteros como datos.
Se deben ingresar dos números separados por un espacio en blanco.

Dentro de la función main()
se declaran las variables [texx]m, n, ans[/texx].

Finalmente viene un bucle do {} while()
que se ejecuta al menos una vez,
y de ahí en más se repetirá hasta que la respuesta obtenida en ans sea un valor negativo.



Observemos que las variables [texx]m, n, ans[/texx], no necesitan inicializarse ya que sus valores son iniciados en el momento apropiado por scanf(), o por la asignación siguiente: ans = gcd(m, n).



(Finalmente viene la sentencie return 0;, que devuelve el control al sistema operativo).



Para mayor prolijidad, vamos a agregar los prototipos de las funciones utilizadas:


int abs(int);
int gcd(int, int);


Esto deja más a la vista las funciones que se usan en el programa,
sin tener que estar mirando todo el engorro de cómo están implementadas dichas funciones por dentro.
Dicha implementación se posterga para la parte inferior del archivo de programa.



También vamos a agregar información para que el usuario del programa sepa qué diablos hace el programa, qué datos tiene que ingresar, qué significa el resultado devuelto, y cómo se hace para terminar el programa.

Lo hacemos encadenando strings en un único printf():


   printf(
     "Este programa calcula el M.C.D. de dos nros. enteros\n"
     "Ignora los signos negativos.\n"
     "Si ambos valores son nulos, "
     "el resultado es infinito y el programa termina.\n"
     "\n"
     "Modo de uso:\n"
     "   Escriba un par de nros. enteros separados por un espacio en blanco.\n"
     "\n"
   );




El programa completo quedar ahora así:

Spoiler: (Programa completo GCD:) (click para mostrar u ocultar)
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #4 : 17/08/2013, 12:32:40 am »

En resumen...

Hasta ahora hemos hecho más o menos esto:

  • (1) Análisis matemático del problema.
  • (2) Reflexión sobre el algoritmo para el caso típico del problema.
  • (3) Estudio de los casos excepcionales o atípicos, y su cuidadosa implementación.
  • (4) Comentarios del tipo "aserción lógica".
  • (5) Comentarios del tipo "implicación lógica".
  • (6) Comentarios adicionales.
  • (7) Funciones y macros de uso frecuente (SWAP, abs).
  • (8) Puesta en marcha de un programa que permite mostrar resultados concretos.
  • (9) Información previa que permite al usuario ayudarle en el uso e interpretación del programa.

Comentemos los ítems (1) a (9):

  • (1) Primero enunciamos el problema matemático, y estudiamos teoremas que permiten arribar a una solución práctica, implementable en un algoritmo sistemático eficiente de computación.

    Esto se basó en el estudio de las propiedades del máximo común divisor y
    cómo se comportan los restos de las divisiones,
    o el hecho de que toda función estrictamente decreciente de enteros positivos tiene que hacerse 0 en una cantidad finita de pasos.

    Son todos ellos hechos matemáticos que luego se tienen en cuenta al diseñar el algoritmo.
  • (2) Se busca luego la manera de llevar las observaciones matemáticas hacia un procedimiento iterativo, o automático de alguna índole, para poder ser programable en una computadora.

    Ahí surgió la idea de mantener siempre la condición [texx]m > n[/texx] para facilitar el cálculo de las sucesivas iteraciones del máximo común divisor, reduciéndolo a un problema de calcular restos sucesivos de una secuencia decreciente de números enteros positivos.

    Con este método se evita estar comprobando todo el tiempo cuál de los dos números es el más grande, para después intercambiar sus valores, etc.

    Se simplifica también el diseño del algoritmo, reduciéndolo a pocas sentencias sencillas y claras.
    De lo contrario, podría producirse un engorro al considerar múltiples casos intermedios, y tomar acciones según cada caso...
  • (3) Luego de haber resuelto el problema para el caso más común o más importante, es menester tranquilizarse un poco y analizar con paciencia todos los casos posibles.

    Desde el punto de vista informático, una función que acepta ciertos parámetros, tiene que reaccionar de determinada manera, y de forma correcta, para todos los valores posibles en el rango soportado por el tipo de dichos parámetros.

    Si un caso no es tenido en cuenta, la función está mal programada, porque dejará sin resolver algunos casos, o peor todavía,
    puede dejar cabos sueltos en el diseño, que conduzcan a errores por imprevisión de situaciones límite o patológicas.

    En nuestro ejemplo, hemos tenido que analizar con cuidado los casos en que los parámetros son negativos, o cero, y dar una respuesta adecuada en cada caso.

    Los resultados atípicos deben estar bien documentados, en lo posible en el cuerpo mismo de la función, y no en el prototipo. (Un prototipo puede improvisarse en cualquier parte, y resulta "volátil" como fuente de documentación para el programador).

  • (4) Las aserciones lógicas permiten llevar un control del estado de programa, sabiendo todo el tiempo qué condiciones se cumplen en cada punto de ejecución.

    A menudo esto facilita el diseño de algunas porciones de código, porque uno puede estar tranquilo sabiendo que varias condiciones ideales se cumplen en determinado momento, y sin necesidad de estar comprobándolas con un "if".

    Esto no sólo da programas más simples, sino también da un método de hacer programas correctos.

    También, al tener certezas lógico-matemáticas, se evitan comprobaciones y cálculos innecesarios en el programa. Esto redunda en un ahorro de esfuerzo y tiempo de computación.

    No obstante, las "comparaciones" que ahora pueden quitarse de los "if" (por ser redundantes), han de aparecer escritas de todos modos, pero en forma de "comentarios".
    Los comentarios no se "ejecutan", aunque dan información valiosa al programador.

  • (5) Al tomar ciertas decisiones en un algoritmo (como el de hacer m = abs(m)),
    se obliga al programa a actuar de determinada manera.
    Esto trae "implicaciones" en el comportamiento del programa,
    y dichas implicaciones pueden documentarse.

    El resultado de esto han sido comentarios que muestran condiciones matemáticas que el programa cumple tras efectuar determinadas operaciones.

    Si nos acostumbramos a tomar conciencia de las consecuencias de las operaciones que realizamos en un algoritmo, nos será más fácil depurar nuestros programas, porque tendremos a la vista, dentro del mismo programa fuente, un resumen de las condiciones que el programa cumple en determinadas circunstancias.

    Es decir, tendremos una visión más clara del comportamiento esperado de nuestros programas.

    En nuestro ejemplo hemos indicado las "implicaciones" con una especie de "flecha" en un comentario:

    /*     ---> m == n == 0      */

  • (6) Los comentarios adicionales incluyen el nombre del programa, la versión, la fecha, el autor,
    y lo más importante de todo: para qué sirve el programa.

    También se pueden incluir comentarios que explican cómo se usa una determinada función o una macro, y qué comportamientos cabe esperar bajo determinadas circunstancias, o cuál es el uso recomendado, precauciones a tener en cuenta, y otras informaciones.
  • (7) Las operaciones de uso frecuente, como "swap" o "calcular valor absoluto", conviene definirlas como macros o como funciones.

    Más aún, existen en C las funciones inline, que tienen la interface de una función, pero funcionan casi como una macro, pues el compilador reemplaza cada invocación de la función por el cuerpo mismo de la función.

    El uso de funciones o macros da más agilidad al proceso de diseño del programa,
    permitiendo tratar por separado pequeñas partes de un problema,
    y hasta reusarlas en otros programas.

    La definición de funciones o macros de uso frecuente tiene la misma utilidad que la definición de identificadores de constantes: permite repetir en forma consistente y sólida un mismo procedimiento a lo largo de todo el programa.
    O sea, si repetimos una operación, no hace falta reescribir todo el código, sino simplemente escribir la macro o función una sola vez, y reutilizarla tantas veces como haga falta.

    Y si tuviéramos un error en dicha función o macro, bastaría arreglarla una sola vez, para que la corrección repercuta automáticamente en todo lugar en que se la invoque.
  • (8) Dentro de la función main() se ponen las instrucciones principales que controlan el flujo principal del programa.

    Es en este lugar donde conviene realizar las operaciones de entrada/salida de datos.

    Resulta más prolijo no utilizar funciones de entrada/salida en otras partes del programa que no están dentro main().

    Es aquí donde nos preocupamos por el problema de cómo interactuar con el usuario.
    Este aspecto debe ser independiente, en lo posible, de los demás trozos del programa.
    De esta manera, una misma solución a un determinado problema puede ser presentada al usuario con distintos formatos o interfaces, según convenga.

  • (9) El programa tiene que ser fácil de entender por el usuario. Tiene que tener una interface clara y concreta.
    Por sobretodas las cosas, debe ser informativo.

    No se puede hacer un programa "pelado" que sólo calcule cosas extrañas y escupa números sin razón aparente.

    Esta "documentación" beneficia a un hipotético usuario del programa ya compilado,
    y por eso difiere de la "documentación" interna del programa, puesta en forma de comentarios con /* */,
    cuya única utilidad es hacerle la vida más fácil al programador.


Hay muchísimos tips de diseño, y en realidad más que reglas para esto, lo que hay es una discusión que está "viva".
A medida que vamos puliendo nuestros programas, aparecen nuevas ideas para hacerlos mejores en uno u otro sentido, hasta que al final queda algo irreconocible.



Debemos anotar también que el uso de scanf() no es seguro.
Da lugar a que el programa falle.
Hagamos el ejercicio de compilar y correr el programa, y de introducir números que no son enteros, o bien cualquier dato basura que no sean números.
En ese caso veremos que el comportamiento del programa se vuelve impredecible.
La razón es que, al ingresar otros tipos de datos, ocupan distintos tamaños en memoria, produciendo invasiones en zonas de memoria inadecuadas, o alguna que otra patología.



Una de las mejoras que tendremos que hacerle a nuestro programa, pues,
será la de evitar el uso de funciones inseguras como scanf().

También debemos preocuparnos en repensar el algoritmo gcd() para ver si hay modos más convenientes de escribirlo.

Por último, nos está faltando un análisis de eficiencia: ¿cuántos recursos de memoria y tiempo gasta el programa? ¿Cuántos cálculos realiza?
Hay que hacer un análisis asintótica de comportamiento en el peor caso posible.

En caso de obtener un rendimiento pobre, habrá que replantear el algoritmo para ver si hay versiones más eficientes.

Finalmente, debemos poner los algoritmos en archivos separados del programa principal,
para que se vuelvan independientes y reutilizables,
y entonces serían invocados cargándolos como librerías de usuario. (Aquí entendemos "usuario" como el "programador").

En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #5 : 17/08/2013, 02:06:39 am »

Reformulemos el Algoritmo de Euclides...

La función abs() innecesariamente se restringe al tipo de datos int.
Cualquier tipo de datos numérico debiera funcionar con el mismo procedimiento.
Parece más bien que abs() es candidata a ser una macro antes que una función.

Algo como esto funcionaría:


#define ABS(X) ((X < 0)? (-(X)): X)


Luego la invocaríamos con:

m = ABS(m);

Observemos que lo más probable es que el usuario ingrese números positivos.
En ese caso, la operación de asignación (=) es innecesaria.
Estaríamos "gastando" una operación de asignación, cuando en la mayoría de los casos no hará falta.

Entonces cambiamos un poco el enfoque, evitando hacer esa asignación en el caso que el valor de la variable [texx]m[/texx] ya sea previamente positivo.
Hacemos pues una macro que haga todo junto: cambia el signo y hace la asignación a la variable, si el valor es negativo, y sólo retorna el valor que tenía la variable previamente, en otro caso:


#define TO_ABS(X) ((X < 0.0)? (X = -(X)): X)


Así, TO_ABS(X) tiene el significado de: "convertir a valor absoluto de X".
La sentencia TO_ABS(m) equivale ahora a:

((m < 0.0)? (m = -(m)): m)

En caso de que m sea un número (entero o real) negativo, se llevará a cabo la asignación m = -m.
Si no, sólo se devuelve m, ahorrando una asignación.

¿Por qué devolvemos m?
Bueno, ocurre que la sintaxis del operador ?: nos obliga a poner algo detrás del signo :
Y ya que en la 1er parte, la asignación m = -m hace que el valor de la expresión sea igual a m,
ponemos, consecuentemente, también m en la 2da parte.
Así, en cualquier caso, la expresión total tiene valor igual a m.

Si el parámetro X no fuese un lvalue, la macro no funcionará.

Comparando con 0.0 en vez de 0 otorga mayor generalidad, y no afecta al funcionamiento de la macro, y no tiene nada que ver con el tipo de número que es X.
Sólo está 0.0 como parte de una operación de comparación.

Se evitan además comparaciones con punteros.

Sin embargo, vamos a comparar mejor contra el entero 0.
Esto lo hacemos así porque el caso típico en nuestro programa será el ingreso de números enteros.
Se evitan entonces costosas operaciones de conversión innecesario de X a un tipo float.

Aunque ahora puede que X sea un puntero que se compara con 0,
el compilador típicamente ha de dar en este caso algún tipo de aviso de esto.



Una vez arreglado el signo de los parámetros,
observamos que los casos en que el usuario ingresa valores iguales son los más raros.
Así que preferimos evitar todas las comprobaciones innecesarias,
y quedarnos primero con el caso típico en que [texx]m\neq n[/texx].

Allí, como siempre, si [texx]m < n[/texx], hacemos el "swap".
Si [texx]m > n[/texx], pasamos al algortimo principal.
Y si [texx]m == n[/texx], entonces sólo restan dos casos: o bien [texx]m = n > 0[/texx] o bien [texx]m = n = 0.[/texx]
Reunimos ambos casos en una sola sentencia, mediante el operador condicional ?:
y más aún, el resultado directamente lo pasamos como valor de retorno de la función mediante return.

Hay que tener especial cuidado al cambiar el orden de todas estas sentencias, porque en tal caso cambian también las aserciones lógicas que habíamos puesto en un principio.

En particular, se pueden juntar los casos en que [texx]m > n > 0[/texx] y [texx]m > n \geq 0[/texx] en uno solo,
notando que si [texx]n == 0[/texx], el algoritmo puede darse directamente por terminado,
siendo [texx]m[/texx] el máximo común divisor.
Esto nos obliga a evaluar desde el principio la condición n > 0,
y entonces tenemos que reemplazar el bucle do {} while () con uno del tipo while(){}.



La función gcd() quedar ahora así:


#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */

/* La macro SWAP(TYPE, X, Y) toma como parámetros:
     * TYPE: un tipo de datos válido de C
     * X, Y: dos lvalues de tipo TYPE
   La macro intercambia entre sí los valores de X é Y
*/
#define SWAP(TYPE, X, Y) { TYPE __Temp = X; X = Y; Y = __Temp; }


/* Macro: TO_ABS(X)
/* ======
/* Convertir un número X a su valor absoluto.     */
/* X debe ser un lvalue.                          */
/* Funciona con todo tipo de dato numérico.       */
/* La conversión es eficiente, inline.            */
/* No hace nada si el número es no-negativo.      */
/* El resultado de la operación es |X|.           */
#define TO_ABS(X) ((X < 0)? (X = -(X)): X)

int gcd(int m, int n) {

    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */
   
    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       SWAP(int, m, n);
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n >= 0 */
   
    /* Se analiza primero el caso típico,  */
    /* para evitar todas las comparaciones que vienen después. */
    if (m > n) {
        /* m > n >= 0 */ 
        while(n > 0) {
            /* m > n > 0 */
            /* gcd(m, n) == gcd(n, m % n) */
            int temp = n; n = m % n; m = temp;
            /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
        };
        /* n == 0, m == gcd(original m, original n) */
       
        return m;   
    }
    /* m == n >= 0 */
   
    return ((m > 0)? m: INFTY_SIGNAL);
    /* m > 0 ---> gcd(m, m) == m; m == 0 ---> gcd(0, 0) == INFTY_SIGNAL */
   
}




Podemos apreciar cómo se ha abreviado el algoritmo.
Se han quitado un par de comparaciones en el camino, y luce todo más conciso.


En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #6 : 17/08/2013, 02:38:33 pm »

Otra reducción importante del algoritmo puede darse ahora que tenemos todo el panorama más despejado.

Observemos que tras la aserción lógica:

/* m >= n >= 0 */

podemos evitarnos analizar el caso en que (m == n), ya que el algoritmo funcionará de todos modos.
Quitando el condicional "if (m > n)" nos ahorramos una comparación.

En el caso que m == n, esto obliga al algoritmo a realizar una operación % y 3 asignaciones (dentro de las sentencias del while).
Pero esto es aceptable dado que m == n es un caso atípico, y en promedio no pesará demasiado sobre el total de casos posibles.

La ganancia real está en la manera simplificada en que nos queda escrito el algoritmo, así:


int gcd(int m, int n) {

    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */

    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       SWAP(int, m, n);
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n >= 0 */
   
    while(n > 0) {
       /* m >= n > 0 */
       /* gcd(m, n) == gcd(n, m % n) */
       int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
    };
    /* n == 0 */
       
    return ((m > 0)? m: INFTY_SIGNAL);
        /* m > 0  ---> m == gcd(original m, original n) */
        /* m == 0 ---> return INFTY_SIGNAL */
}




Observamos que al finalizar el while()
nos queda la aserción lógica n == 0 (¿por qué?).

Si m > 0, entonces la variable m contiene el máximo común divisor buscado.
Si m == 0, entonces estamos en el caso [texx]m = n = 0[/texx], que corresponde al caso infinito.

La pregunta de si [texx]m > 0[/texx] la hacemos directamente en la sentencia return, mediante el operador ?:
abreviando aún más el algoritmo.
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #7 : 17/08/2013, 03:02:03 pm »

¿Realmente es necesario verificar que [texx]m > n[/texx]?

Podemos quitar esta comprobación, reduciendo así aún más el algoritmo.

Supongamos que quitamos la línea del SWAP(int, m, n).

Si ocurriese que [texx]m < n[/texx], el algoritmo mismo lo corregiría, ya que m % n == m en este caso,
y para la siguiente iteración queda n igual m % n, mientras que m queda igual al valor que tenía n.
Esto no es otra cosa que un swap implícito entre [texx]m, n[/texx], que se realiza automáticamente.

Ni siquiera el caso m == 0 trae problemas, ya que tras el swap implícito dentro del bucle while(),
implica que en la siguiente iteración se obtiene m > 0, n == 0, que hace terminar el bucle.

Dado que de todos modos se realiza un swap cuando [texx]m < n[/texx], es redundante hacer la comparación [texx]m < n[/texx] antes de entrar al while().

La condición [texx]n > 0[/texx] que controla el bucle while() es suficiente para prevenir todo tipo de inconvenientes que surgirían de una hipotética división por 0.

Nuestro algoritmo queda entonces aún más breve:


int gcd(int m, int n) {
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */

    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    while(n > 0) {
       /* n > 0 */
       int temp = n; n = m % n; m = temp;
       /* gcd(m, n) == gcd(n, m % n) */
       /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
       /* m >= n >= 0 */
    };
    /* n == 0 */
       
    return ((m > 0)? m: INFTY_SIGNAL);
        /* m > 0  ---> m == gcd(original m, original n) */
        /* m == 0 ---> return INFTY_SIGNAL */
}


Hemos tenido que hacer un cambio importante a nivel lógico:

Ya no es cierta al principio del while() la siguiente aserción lógica:

       /* m >= n > 0 */

Sin embargo se vuelve cierta hacia el final del while().
(Aunque también ahora puede ser n == 0, pero esto no es cierto al iniciar la iteración del while(), en donde obligadamente es n > 0).



Tenemos ahora un algoritmo totalmente simplificado, resumido y optimizado.

El programa ahora también se reduce, porque ya no necesitamos usar la macro SWAP(), y la borraremos.

El programa completo, en spoiler:

Spoiler (click para mostrar u ocultar)


Si quitáramos todos los comentarios y aserciones, quedaría esto:


#define INFTY_SIGNAL  -1

#define TO_ABS(X) ((X < 0)? (X = -(X)): X)

int gcd(int m, int n) {
    TO_ABS(m); TO_ABS(n);

    while(n > 0) {
       int temp = n; n = m % n; m = temp;
    };

    return ((m > 0)? m: INFTY_SIGNAL);
}

En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #8 : 17/08/2013, 03:38:45 pm »

Es hora de separar la interface del usuario del algoritmo, creando una librería para el máximo común divisor.


Nuestra primera versión de la librería (que he numerado como 1.10, para respetar las mofidicaciones que hemos venido haciendo hasta ahora).

Lo que haremos será "recortar y pegar" aquellas partes del programa que son inherentes exclusivamente al cálculo de la función gcd().
Esto abarca la constante INFTY_SIGNAL, la macro TO_ABS(), y la función gcd().

Al principio del archivo escribiremos largos comentarios a modo de documentación.
Explicaremos cuál es el contenido de la librería, qué hace, cómo se usa, cómo funciona,
cuáles son los resultados esperados, qué tipo de parámetros son válidos, etc.
Se informa también la versión, fecha, autor, etc.

Lo importante aquí es que la librería quede claramente dividida en 3 partes:

(1ra parte): Información genérica y manual de uso de la librería.
(2da parte): Interface que permite a los programadores ver de un golpe de vista cuáles son todas las definiciones, macros y funciones que ofrece la librería.
(3ra parte): Implementación de las macros y funciones, y también de rutinas ocultas a los usuarios de la librería.

La 1era parte es una extensa porción de texto puesto como comentarios.
Ahora mostraremos cómo quedó, y así me ahorro explicaciones.
Se enumeran ahí todos los usos de los objetos definidos en la librería.

La 2da parte contiene los prototipos de las funciones y todas las otras declaraciones que hagan falta.
No hay prototipos para macros, pero podemos poner en esta parte comentarios que contengan el prototipo de uso de la macro, y postergar su definición real hacia la 3ra parte.

En la 3ra parte ponemos todo el trabajo duro de programación, con todos los detalles de algoritmos, sentencias, análisis, y demás cosas, que ni el mismo tipo que las programó podría entenderlas.



Ahora tenemos dos archivos: la librería gcd, y el programa gcd que utiliza la librería:

Spoiler: (Librería gcd versión 1.10) (click para mostrar u ocultar)

Spoiler: (Programa gcd versión 1.10) (click para mostrar u ocultar)

En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #9 : 17/08/2013, 09:49:39 pm »

Cambios en la interface de usuario.

La función scanf() de la librería estándar stdio.h
sirve sólo para salir del paso, y así poder probar rápidamente nuestros algoritmos,
a ver si funcionan bien, o si hacen lo que esperamos.

Según el parámetro de conversión (por ejemplo %i para enteros)
es posible interpretar correctamente el tipo de datos que el usuario está ingresando a través del teclado.

Pero recordemos que lo que realmente ingresa el usuario por teclado es una string ó cadena de caracteres.

Aunque scanf() esté lista para recibir un entero,
el usuario del programa puede ingresar cualquier otra cadena de caracteres que no sea numérica,
y hacer añicos nuestro programa, dando resultados anómalos o inclusive errores en tiempo de ejecución.



Esto ocurre porque no tenemos el control absoluto de lo que ocurre con la entrada de datos.
Hay varias opciones:

(a) Controlar manualmente los caracteres de entrada, creando rutinas que permitan decidir si los datos son correctos o no.

(b) Estudiar más profundamente la función scanf() y ver si hay algún modo seguro de utilizarla.

(c) Alguna solución intermedia, usando funciones como gets() y sscanf().



La opción (a) a menudo termina siendo la preferible.
Involucra el uso de funciones de conversión de string a entero, y viceversa.

La opción (b) es un paso que podemos dar para profundizar nuestro conocimiento de las funciones de ingreso de datos de la librería stdio.h, pero no auguro buen final si nos quedamos sólo con esto.

Nosotros vamos a avanzar con la opción (c).



La función scanf() tiene la siguiente interface:

int scanf(char * fmt, ...);

Los puntos suspensivos indican un número variable de argumentos.
Allí debemos indicar las direcciones de memoria de las variables que deseamos ir cargando con los datos ingresados por el usuario.

La cadena de formato fmt indica lo que el usuario debe tipear, y el tipo de datos que será interpretado.

El valor de retorno de la función es un entero (int) que indica cuántas variables efectivamente se han cargado con éxito.
Si este valor de retorno es menor que el número de variables que hemos apuntado, entonces quiere decir que ha habido un error en los datos ingresados: el usuario ha escrito algo que es incompatible con el tipo de datos indicado en la cadena de formato.

Las directivas que indican el tipo de datos más típicas son éstas:

%i: Se ingresa un número entero.
%f: Se ingresa un número de punto flotante.
%c: Se ingresa un caracter.
%s: Se ingresa una string sin espacios en blanco.

Por lo general, los espacios en blanco se usan para separar datos distintos en la cadena ingresada por el usuario, y eso explica por qué %s no acepta blancos.

Más aún, los espacios en blanco son ignorados.
En general, todos los caracteres de tipo "blanco" son ignorados: "\b\n\r\t\a\f ".

También son ignorados los datos "sobrantes" que el usuario pudiera ingresar, hasta alcanzar el fin de línea.

Cuando se produce un error, la función scanf() interrumpe lo que está haciendo,
y retorna al programa principal, indicando el número de variables correctamente asignadas.
Aún en el caso de que se hayan asignado correctamente todas las variables, puede que el usuario haya ingresado datos adicionales, con formato erróneo o indeseado.
En teoría estos datos extra debieran ser ignorado, pero a veces el programa tiene comportamientos extraños.
Por ejemplo, puede que quede "colgado" algún caracter fin de línea que se lee siempre, aún cuando el usuario no presiona ENTER.
Las razones por las que puede ocurrir algo así no son muy claras para mí.  :¿eh?:


NOTA importante: En general scanf() no funciona bien cuando el usuario presiona ENTER directamente, sin haber introducido dato alguno. El comportamiento puede ser errático, o simplemente puede que scanf() lo ignore, esperando a que el usuario ingrese al menos "algo" (una cadena no vacía).

Cuando scanf() no lee apropiadamente el fin de línea, produce errores de lectura en subsecuentes intentos de usar scanf(),
o incluso de otras funciones de lectura de datos, como getchar().




Para evitar desajustes en la lectura de datos del teclado,
primero leemos directamente una línea de la entrada mediante gets(),
y la guardamos en una variable string llamada str.



A continuación intentamos leer datos desde str, ahora usando sscanf().
La función sscanf() funciona igual que scanf(), sólo que en vez de tomar como datos de entrada al teclado, toma una string. La interface es esta:

int sscanf(char *str, char *fmt, ...);

Vayamos a la situación en la cual nos interesa ingresar un par de números enteros.

Dado que la entrada de datos por consola suele tener menos de 128 caracteres,
podemos asumir que 130 es un tamaño adecuado para una cadena de entrada (hay un caracter nulo adicional que gets() agrega, y descarta el caracter fin de línea).

Así, escribimos:

char str[130], sstr[130];
int m, n;
gets(str);
int err = sscanf(str, "%i %i", &m, &n);
if (err < 2)
  printf("Datos no validos\n");


La cadena str recibe una línea de texto que el usuario ingresa por teclado,
por medio de la función gets().
La función sscanf() lee los datos que están ahora en str,
y de allí intenta asignar dos valores enteros consecutivos a las variables m, n.
La cantidad de valores correctamente asignados quedan guardados en la variable err.
Si hay algún tipo de error, se obtiene un valor EOF para err (que es negativo).

Si err == 2, obviamente significa que la asignación ha sido exitosa,
ya que las 2 variables m, n, han sido correctamente asignadas, y podemos proseguir.

Si no, obtendremos err < 2.
En este caso, informaremos al usuario que ha ingresado datos erróneos,
y lo instaremos a que reintente.



Ahora vayamos al programa mismo, y supongamos que hacemos uso de nuestra función gcd(), que calcula el máximo común divisor.

Vemos que el programa contiene sólo dos líneas relativas al máximo común divisor:
la que incluye la librería gcd0110.h, y la que invoca la función gcd().
El resto del programa consiste sólo en interacción con el usuario y manejo de los datos ingresados por teclado, información e instrucciones para el usuario, etc.

Definiremos una función blank() que se encarga de determinar si una string contiene sólo caracteres blancos, o no. Retorna 1 (true, verdadero) en caso afirmativo, y 0 si no.
Para ello tendremos que hacer uso de la librería ctype.h, que contiene la función estándar isspace().
Es más sano, en general, hacer uso de las funciones de ctype.h para interpretar caracteres, antes que definir a mano funciones similares.

También usaremos el tipo de datos bool que viene con el estándar C99,
junto con los valores true (1) y false (0),
lo cual nos impele a usar la librería stdbool.h.

En todas partes agregamos comentarios que explican lo que harán las sentencias,
y detrás de cada sentencia ponemos aserciones lógicas que indican el estado del programa,
o del estado en que quedó el buffer de lectura de scanf() y sscanf().

El resultado es un programa "seguro", es decir, incrackeable por el usuario. (Que yo sepa  :rodando_los_ojos: ).
Veamos cómo queda el programa completo (en spoiler):




La moraleja sería que es más seguro usar la combinación de gets() y sscanf() antes que usar directamente scanf().

Si miramos el programa, vamos a ver que es una maraña de comentarios y aserciones lógicas,
que explican el sentido de todo lo que ocurre.
Sin embargo, las sentencias "de verdad" son muy pocas.
El programa, sin comentarios, queda así:

Spoiler (click para mostrar u ocultar)

En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #10 : 18/08/2013, 02:36:41 am »

Poniendo la interface en una librería...

Así como pusimos el algoritmo principal en una librería separada,
lo propio podemos hacer con la interface que interactúa con el usuario.

Vamos a definir una librería de nombre bui.h (Basic User Interface).
Esto quiere decir que nuestra librería no es muy ambiciosa.
Sin embargo intentará sistematizar en forma sólida algunas operaciones típicas de intercambio de datos con el usuario, a través de teclado y pantalla.



En un programa hay distintos tipos de informaciones o mensajes que se muestran al usuario.
Los pondremos todos juntos en una estructura que los coleccionará.
Estas informaciones pueden ser:

  • Nombre del programa.
  • Versión.
  • Fecha.
  • País.
  • Autor.
  • Comentarios generales.
  • Ayuda.
  • Solicitudes al usuario.
  • Mensajes.

Los primeros 5 tipos de información son en general breves,
y los almacenaremos en cadenas de longitud corta (digamos, menos de 32 caracteres).
Los siguientes pueden ser mensajes más extensos,
pero aún así acotaremos su tamaño en menos de, digamos, 1024 caracteres.

Además, puede haber distintos tipos de comentarios, distintos tipos de ayudas, distintos tipos de solicitudes, y distintos tipos de mensajes.
Consideramos estos mensajes extra colocados en un vector de strings.
Este vector es una "lista" que tiene una longitud máxima, que por ahora fijaremos en 16.

(Estamos pensando en una gran generalidad, que nuestro programa GCD no necesita).

Pondremos todo esto dentro de una struct anónima denominada bui.
¿Hace falta una struct?

Estamos pensando en una estructura anónima como un "espacio de nombres", como los que hay en C++.
Agrupamos datos en un bloque que está relacionado a la librería que los define, en este caso bui.h.
De esta manera, los nombres quedan reutilizables para otros archivos, librerías, o programas.

Estamos mostrando preocupación por posibles conflictos en los nombres de los objetos de un programa, que puede crecer de modo descontrolado, y cuyos nombres de variables y funciones pueden empezar a perderse de vista con facilidad, causando conflictos o errores por esta causa.


#define __SHORT_STR_LEN   32  /* Longitud string corta. */
#define  __LONG_STR_LEN 1024  /* Longitud string larga. */
#define __MAX_LIST        16  /* Longitud de listas de varios ítems en bui.h */

struct {
    char
      program_name[__SHORT_STR_LEN],
      program_vers[__SHORT_STR_LEN],
      program_auth[__SHORT_STR_LEN],
      program_date[__SHORT_STR_LEN],
      program_coun[__SHORT_STR_LEN],
      program_purp[__LONG_STR_LEN],
      program_comm[__LONG_STR_LEN],
      program_comm_[__MAX_LIST][__LONG_STR_LEN],
      program_help[__LONG_STR_LEN],
      program_help_[__MAX_LIST][__LONG_STR_LEN],
      program_req[__LONG_STR_LEN],
      program_req_[__MAX_LIST][__LONG_STR_LEN],
      program_mess[__LONG_STR_LEN],
      program_mess_[__MAX_LIST][__LONG_STR_LEN],
      program_err[__LONG_STR_LEN],
      program_err_[__MAX_LIST][__LONG_STR_LEN];
} bui;




También vamos a definir una estructura adicional para el manejo de los "inputs" desde el teclado.
Contendrá estos elementos:

  • Un buffer para albergar caracteres.
  • Una variable entera que albergará el valor devuelto por funciones del tipo scanf(), al intentar leer datos.
  • Una variable "flag" (banderín, señalizador) que indicará el estado de la última operación de lectura (si hay error o no).


#define __INPUT_STR_LEN  130  /* Longitud string datos entrados por teclado. */

struct {
    char buffer[__INPUT_STR_LEN];
    int errno;
    bool error_state;
} bui_input;


El tamaño de 130 para el buffer es razonable, porque las entradas por consola suelen tener un tamaño menor a 128 caracteres.
Si fuera necesario, este valor puede agrandarse, por supuesto.



Pondremos allí la función

bool blank(char* );

que servía para detectar si una string contiene sólo caracteres blancos.
Como es una función de propósito general, la dejaremos visible, o sea, no encapsulada en la librería que estamos desarrollando.


bool 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 */
    return true;
}





Definiremos una función sencilla llamada

void bui_show_program_info(void);

Su propósito es mostrar la información básica del programa,
lo cual vendría a ser el contenido de las strings cortas de la estructura bui.


void bui_show_program_info(void) {
    printf("%s %s\n%s %s\n%s\n\n",
       bui.program_name,   bui.program_vers,
       bui.program_date,    bui.program_coun,
       bui.program_auth,
       bui.program_comm
    );
}




Cuando queramos mostrar toda una lista de strings, haremos uso de una función dedicada a esta tarea:

void bui_print_list(char **list);

La función estará definida así:


void bui_print_list(char (*list)[__LONG_STR_LEN]) {
    for (int i = 0; i < __MAX_LIST; i++)
      if (list[i][0])
        printf("%s", list[i]);
}


La sentencia "if" interpuesta bajo el "for" previene contra el caso en que haya cadenas vacías en la lista. En ese caso, no se muestra nada en pantalla.



Puede parecer ridículo usar una instrucción como:

        printf("%s", list[i]);

cuando sería más fácil y directo esto otro:

        printf(list[i]);

Después de todo, list[i] es una string, y printf() puede mostrarla sin problemas.

Pero puede ocurrir que haya directivas de formato en la string que deseamos imprimir, y esto causaría terribles estragos.

¡Peor sería si la cadena a imprimir fuese una string ingresada por el usuario!
Allí tendríamos un comportamiento totalmente impredecible,
pues el usuario podría escribir libremente signos % en todas partes,
y esto causaría destrozos en nuestro programa.

El método de usar printf("%s", str); para imprimir una string str, es más seguro.   :sorprendido:

Un modo de evitar esta discusión es usar directamente una función como puts(), y asunto arreglado.
No hay que preocuparse por las directivas de formato, ni nada.
En una futura versión de la librería, lo haremos así.



Cada programa ha de seleccionar su propia lista de strings con información que le sea pertinente.
Para eso, a la variable bui que está definida de una vez y para siempre en la librería bui.h,
debemos asignarle valores a cada uno de sus componentes individuales, de alguna manera.

Esto se puede hacer fácilmente mediante un "copiado de caracteres" de una string (la nuestra) en otra (en una de las componentes de bui).

Para copiar cadenas podríamos invocar la librería estándar strings.h.
Pero como sólo nos interesa usar una sencilla función de copiado, y nada más sofisticado que eso,
preferimos definir dicha función a mano, para así no invocar nuevas librerías.


void bui_set_string(char* restrict, const char* restrict);


La palabra restrict pertenece al estándar C99, y se usa para reforzar la seguridad de las funciones que manejan strings.
La idea es que las cadenas no se superpongan entre sí, impidiendo odiosas o imprevistas sobreescrituras de una cadena sobre sí misma en memoria.

Es una prevención útil, pero no la estudiaremos en este momento.

La función de copiado de strings tiene una forma sencilla:


void bui_set_string(char* restrict dest, const char* restrict src) {
    /* Copia src en dest */
    for( ; (*dest = *src) != '\0'; src++, dest++)
      ;
}




Usaremos una función auxiliar para leer cadenas desde el teclado.
Esta función leerá caracteres desde la entrada estándar con gets(),
y el resultado lo guardará en bui_input.buffer.

Además, tomará como parámetro una string req, que será un mensaje para el usuario,
indicando el tipo de dato que se le solicita (es un "request", solicitud):


void bui_gets(char *req) {
    printf("%s", req);
    gets(bui_input.buffer);   
}




Nos interesa calcular el número de directivas en una cadena de formato del tipo que se usan en scanf() y funciones similares.
Esta cantidad se calculará mediante la función

int __bui_N_ARGS(char *);

que toma como parámetro una string, y devuelve el número de directivas % de dicha string.

Para hacer el cálculo, hay que contar todos aquellas situaciones tales que
al caracter actual, si es distinto de '%', le precede un caracter '%'.
Para eso, memorizamos en una variable de estado llamada before,
si es que "antes" (en el caracter inmediato precedente) había un '%'.
Y entonces hacemos el conteo.

El algoritmo se hace así, medio enrevesado, para evitar de forma sencillo el conteo equivocado de casos como "%%", que no deben contarse más de una vez.
(De hecho no debieran contarse ni una sola vez, pero esto lo arreglaremos en una versión futura de la librería).


int __bui_N_ARGS(char *s) {
    int n;
    bool before = false;
   
    for (n = 0; *s; s++)
      if (*s == '%')
        before = true;     
      else {
        if (before)
           n++;
        before = false;   
      }
           
    return n;         
}




En base a nuestra experiencia previa manejando entradas erróneas o no, con gets() y sscanf(),
vamos a definir una macro que servirá para el propósito de ingresar datos desde el teclado,
controlar si han habido errores, y tomar las decisiones pertinentes según los casos.

La macro se llamará BUI_SCANF(),
y sustituirá en tareas a sscanf().

¿Por qué una macro en vez de una función?
El motivo para esto está en que las funciones de la familia de scanf() tienen un número variable de argumentos.
En C99 es más sencillo crear una macro con un número variable de argumentos que ponerse a hacer una función con comportamiento análogo.

Una macro tiene siempre puntos flacos,
es código poco seguro, crackeable de un modo u otro.
De todos modos insistiremos por ahora en este enfoque.

Desarrollaremos la macro en cuestión en el próximo post.



Finalmente un detalle de estilo.
A fin de encapsular las macros tipo constante definidas en bui.h,
vamos a quitar su definición al finalizar el archivo, mediante #undef:


#undef __INPUT_STR_LEN
#undef __SHORT_STR_LEN
#undef __LONG_STR_LEN
#undef __MAX_LIST

En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #11 : 18/08/2013, 12:48:39 pm »

Interacción con el usuario, controlada...

Desarrollaremos aquí la macro que controla lo que el usuario ingresa por teclado.
Intentaremos mantenerla lo más breve y simple que sea posible,
pero también queremos que nos permita tomar decisiones importantes,
que sea versátil...

Las intenciones y objetivos detrás de una pieza de código son lo que realmente hace funcionar correctamente el algoritmo.

Si se sabe lo que una macro o función se supone que tiene que hacer, entonces podemos usarla con confianza, y delegar al programador encargado de diseñar esa macro o función la responsabilidad de programarla correctamente.



La macro BUI_SCANF() aceptará una lista de parámetros, que iremos especificando de a poco.

Lo primero que ha de hacer la macro es leer una línea de texto de la entrada estándar, tecleada por el usuario. Para ello usaremos la función gets().
Pero necesitamos una cadena de caracteres con la suficiente capacidad para albergar el texto tecleado.
Esa cadena será la componente bui_input.buffer, de la estructura previamente definida bui_input.
Ya tiene el tamaño apropiado, y no es necesario que el programador se ponga a definir nuevas variables.
Para eso la hemos puesto como parte inherente de la librería.

Sin embargo, es usual que a una sentencia de lectura de datos le anteceda un mensaje que informa al usuario el tipo de dato que se le solicita, y alguna otra información que pudiera hacer falta.
Esto será especificado en una variable de tipo array de caracteres,
que el programador tiene que especificar a través de un parámetro de la macro,
que llamaremos REQ_MSG.

Así, la sentencia de lectura de datos del teclado será simplemente:

bui_gets(REQ_MSG);

Esta función mostrará en  pantalla el mensaje contenido en REQ_MSG, y luego leerá una línea de caracteres del teclado, guardándola en bui_input.buffer.

Nota: Ya hemos discutido en mensajes previos las razones de por qué hemos evitado el uso directo de scanf(), y cómo es que terminamos usando una combinación de gets() con sscanf().

Aquí sólo estamos reescribiendo esa experiencia previa en forma de una macro, que pueda accederse de manera universal desde cualquier archivo que invoque la librería bui.h.



La siguiente sentencia es lo que ya esperamos: una relectura de los datos mediante sscanf().
El escollo aquí es que sscanf() es una función con un número variable de parámetros.

 :¿eh?: ¿Cómo hacemos para pasar a una macro una cantidad variable de parámetros?
 :¿eh?: ¿Y cómo los invocamos dentro de la macro?

Lo que se hace es poner al final de la lista de parámetros de la macro 3 puntos suspensivos: ...
Eso informa que la macro acepta una cantidad variable de parámetros.
Para poder "incrustar" todos esos parámetros en alguna parte, basta escribir __VA_ARGS__,
que es una palabra del preprocesador que sirve para sustituir por "lo que sea" que le hayamos arrojado como lista de parámetros a los puntos suspensivos.

La macro tendría esta cabecera, por ejemplo:

#define BUI_SCANF(REQ_MSG, ...)

Suponiendo que hemos puesto esos dichosos puntos suspensivos, podríamos escribir ahora confiados algo así:

sscanf(bui_input.buffer, __VA_ARGS__);

Esa instrucción toma como "entrada de datos" la cadena de caracteres bui_input.buffer,
y luego toma como lista de parámetros a toda la lista variable de parámetros que el programador indica, en reemplazo de los puntos suspensivos.
Un modo de usar la macro sería por ejemplo esta:

BUI_SCANF("Ingrese sus datos!!!\n\n", "%d %f", nn, xx);

Los últimos 3 parámetros: "%d %f", nn, xx
reemplazan a los puntos suspensivos ...
cada vez que en la macro aparece la palabra __VA_ARGS__.
Sería equivalente a haber escrito esto:

sscanf(bui_input.buffer, "%d %f", &nn, &xx);

Nota importante: En lo que sigue vamos a modificar todo este modo de uso.



Para tener un control más fino de la entrada de datos, tenemos que hacer como en posts anteriores: tener en cuenta el valor de retorno de sscanf() para detectar posibles errores.
Si el valor de retorno es 0 ó positivo, indica la cantidad de variables que se pudieron "cargar" correctamente durante el proceso de sscanf().
Si es negativo, indica EOF, que a su vez significa que hubo algún otro tipo de error en la lectura de los datos.

Este dato lo guardaremos en la componente bui_input.errno.
Así que nuestra sentencia debe cambiarse por esta otra:

bui_input.errno = sscanf(bui_input.buffer, __VA_ARGS__);

Para saber si todos las variables han sido correctamente asignadas durante el proceso de sscanf(),
tenemos que observar el valor de bui_input.errno y ver si coincide con la cantidad de parámetros que aparecen en la cadena de formato "%d %f".
Esa cantidad en este caso es 2, pero puede variar, claro está.

Lo que necesitamos es contar la cantidad de veces que aparece la directiva % en la cadena de formato.

Y para eso necesitamos acceder desde la macro a dicha cadena de formato.
No nos sirve el enfoque de "haberla puesto sin nombre junto a toda la lista de parámetros __VA_ARGS__".
Así que ese parámetros le daremos un nombre explícito en la cabecera de la macro, digamos FMT,
y sólo a partir de aquí consideraremos la lista variable de parámetros con los puntos suspensivos ...

La cabecera de la macro quedaría por ahora así:

#define BUI_SCANF(REQ_MSG, FMT, ...)

La sentencia de invocación de sscanf() quedará ahora así:

bui_input.errno = sscanf(bui_input.buffer, FMT, __VA_ARGS__);

Y ahora calculamos el número de directivas % así:

__bui_N_ARGS(FMT)

Eso fue fácil.  :sonrisa_amplia:

En realidad la función __bui_N_ARGS() ya la habíamos definido, sabiendo que la íbamos a necesitar.

Nota importante: La función __bui_N_ARGS() no funciona a la perfección todavía. Requiere cambios en versiones futuras.
Reconoce como "variables efectivas" para ser asignadas, casos que no lo son.



Lo que nos interesa es, como dijimos, comparar esa cantidad con el número de error arrojado en bui_input.errno.

Según la definición de sscanf(), sabemos que el entero que retorna en bui_input.errno nunca puede ser mayor que el número de directivas % que aparece en la cadena de formato FMT.

Ahí tenemos, pues, la siguiente aserción lógica:

/* bui_input.errno <= __bui_N_ARGS(FMT)  */

Nunca se da el caso >.

Para saber si hubo un error, tenemos que ver si el lado izquierdo es estrictamente menor que el lado derecho.
El resultado de esto vamos a necesitarlo posteriormente en el programa,
así que lo vamos a guardar en la componente booleana bui_input.error_state:

bui_input.error_state = (bui_input.errno < __bui_N_ARGS(FMT));

El valor de esta variable es true en caso de que haya algún error,
y es false en caso de que sscanf() haya asignado correctamente todos los valores a cada uno de sus parámetros.



Ahora tenemos que tomar una decisión.
Si bui_input.error_state es false, no hacemos nada, y dejamos que el programa fluya.
Si es true, hay dos opciones: o bien el programa termina, o bien muestra un mensaje de error y continúa normalmente.
En este último caso, la rutina principal que ha invocado la macro tiene la responsabilidad de verificar si ha habido o no un error (mirando el valor de bui_input.error_state), y entonces decidir qué hace en esa situación.
(Lo que nosotros venimos haciendo hasta ahora es "saltearnos" el trozo de código del bucle principal mediante un continue;, que salta al principio del bucle para recomenzar).

El esquema de decisión sería éste:


       if (bui_input.error_state)   
          if ( (evaluar_condicion_de_terminacion_del_programa ) )       
              /* Terminar el programa */                   
          else
              /* Mostrar mensaje de error */
       else;




La condición para terminar el programa es algo que debe dejarse "abierto a todas las posibilidades",
ya que esto es decisión de diseño del programador.
No puede ser la librería bui.h quien decida cuál es el mejor requisito para terminar un programa, pues es una librería de servicios.

Sin embargo, podemos restringir un poco el universo de ocurrencias del programador,
diciendo que una "condición de terminación" es algo bien específico, por ejemplo:

Una función que acepta como entrada un parámetro de tipo cadena de caracteres,
y como salida envía un valor booleano, indicando true para "terminar" y false para "no terminar".


Supongamos que, como hasta ahora, queremos que el programa termine cuando el usuario ha ingresado una cadena de caracteres con puros blancos.
En este caso, podemos usar la función blank(), que por suerte ya la tenemos definida y funcionando correctamente.

Lo que haremos ahora será pasar el nombre de la función, es decir blank,
como un parámetro a la macro BUI_SCANF().
Ese parámetro indicará, pues, el nombre de una función cuyo cometido es dar una condición verdadera o falsa, para terminar o no el programa.
Entonces la cabecera de la macro se modificaría así:

#define BUI_SCANF(REQ_MSG, TERMINATION_FUNC, FMT, ...)

Como TERMINATION_FUNC es sólo el "nombre" de una función, podemos intercalarlo alegremente dentro de la macro, usándola cuidadosamente para llamar funciones.
Sabiendo de antemano que esa función admite 1 parámetro de tipo cadena de caracteres, podemos escribir con tranquilidad lo siguiente:

TERMINATION_FUNC (bui_input.buffer)

Eso equivaldrá a reemplazar el parámetro TERMINATION_FUNC por blank, y entonces equivale a haber invocado:

blank (bui_input.buffer)

Debemos tener mucho cuidado con este tipo de manipulaciones.
Vemos, pues, que el uso de las macros es riesgoso, porque nada impide que un programador ponga cualquier cosa como parámetro. ¿Qué pasa si no pone una función, sino un float, o un puntero?
Por ahora dejaremos esas preguntas sin responder.

Concentrémonos en lograr que las cosas funcionen, y después veamos los reparos técnicos.

El esquema de decisión tiene ahora este otro aspecto:


       if (bui_input.error_state)   
          if ( TERMINATION_FUNC (bui_input.buffer) )       
              /* Terminar el programa */                   
          else
              /* Mostrar mensaje de error */
       else;


Vemos que, cualquiera sea la función que indiquemos para determinar la condición de terminación,
va a actuar siempre sobre la variable bui_input.buffer, que es una cadena de caracteres declarada en la librería bui.h.

Es decir que las "condiciones de terminación" son condiciones establecidas sobre la cadena de caracteres que el usuario ha ingresado por teclado.
Esto es un diseño lógico, ya que esperamos que sea una decisión del usuario la que termine el programa.
Lo que puede cambiar es el tipo de entrada que el programa principal elige como indicador de "quiero terminar este programa de una vez por todas".



Una observación importante es que una macro no es una función.
Ni tampoco es una pieza de código que tenemos claramente a la vista.
Una macro es un caballo de Troya, del que no se sabe qué diablos saldrá de su interior.

En nuestro versión 1.10 de GCD habíamos puesto sentencias como break, continue, o return.

Esas tres sentencias típicas de "quiebre" del flujo normal y secuencial del programa deben evitarse adentro de una macro, porque no se sabe cómo van a repercutir en la rutina principal que la invoca.

Por ejemplo, el caso de break es fuente segura de problemas.
Si la rutina que la invoca está dentro de un bloque while(), la sentencia break causará que salgamos del while().
Pero si estamos en un switch(), que a su vez está dentro del while(), no se consigue el mismo efecto, porque el break sólo servirá para salir del switch().

Poniendo break, continue ó return dentro de las sentencias de una macro no permite predecir con exactitud hacia dónde se producirá el "salto".


La solución a este dilema es otorgar un mayor control sobre los saltos.
Esto se puede lograr con etiquetas claramente definidas y usando dentro de la macro una sentencia goto, que se dirijan hacia esa etiqueta y no otra.
Ahí se acaba toda posible incertidumbre o ambigüedad.

Recordemos que las etiquetas y los saltos goto tienen sentido en C sólo dentro del bloque que abarca una función.

Lo que deberíamos hacer es pasar el nombre de la etiqueta como un parámetro más a la macro BUI_SCANF, digamos TERMINATION_LABEL, en la cabecera:

#define BUI_SCANF(REQ_MSG, TERMINATION_FUNC, TERMINATION_LABEL, FMT, ...)

Y dentro de la macro, en el lugar donde se consigue una condición de "terminación", poner la sentencia:

goto TERMINATION_LABEL;

Esto hará el trabajo.
Nuestro esquema de decisión se va completando mejor, y ahora tenemos esto:


       if (bui_input.error_state)   
          if ( TERMINATION_FUNC (bui_input.buffer) )       
              goto TERMINATION_LABEL;
          else
              /* Mostrar mensaje de error */
       else;


Ahora, si en nuestro programa ponemos una etiqueta llamada fin_programa:
antes del final, invocaríamos a BUI_SCANF() así:


int main(void) {

    BUI_SCANF("Ingrese sus datos aquí: \n\n", blank, fin_programa, "%d %f", nn, xx);
   
    /* sentencias varias... */

fin_programa:

    return 0;
}




Todos estamos acostumbrados a oír la eterna canción de

"No uses goto en la programación."

Eso se repite una y otra vez como un mantra religioso.

En realidad en la programación "hay que hacer lo que hay que hacer", según la situación que corresponda, para bien del programa que estamos desarrollando.
Me causan mucha risa  :sonrisa_amplia: :sonrisa_amplia: :sonrisa_amplia: las prevenciones morales en el terreno informático.  :malvado: :malvado: :malvado:

En realidad lo que no habría que usar son las macros:sorprendido: :sorprendido: :sorprendido:
Esas sí que son peligrosas.

Pero entonces, ya que estamos haciendo un diseño basado en una macro,
tenemos que ponernos en situación, y advertir que hay acciones peligrosas dentro de una macro.
En este caso, el uso de sentencias aparentemente seguras como break, continue, return,
conducen ahora a resultados impredecibles en el momento del diseño,
y lo más saludable es controlar los saltos con etiquetas bien especificadas,
para decirle a la macro exactamente a dónde queremos que vaya,
y esto trae como consecuencia la necesidad de hacer los saltos con goto hacia esas etiquetas.

En este caso, lo más "seguro" es usar goto.



En caso de que la condición de terminación no se dé,
la alternativa es mostrar un mensaje de error que informe al usuario lo que ha ocurrido,
y que le indique además cómo salir del error, o qué hacer a continuación.

Este mensaje de error será una cadena de caracteres que debe elegirse desde la rutina invocadora,
y por lo tanto es un parámetro adicional, digamos ERROR_MSG, que tendremos que pasar a la macro BUI_SCANF().
La cabecera quedará así:

#define BUI_SCANF(REQ_MSG, TERMINATION_FUNC, TERMINATION_LABEL, ERROR_MSG, FMT, ...)

Ahora el esquema de decisión queda completo, así:


       if (bui_input.error_state)
          if (TERMINATION_FUNC (bui_input.buffer))       
              goto TERMINATION_LABEL;                     
          else
              printf(ERROR_MSG);                         
       else;   




Habiendo explicado todos los aspectos de la macro BUI_SCANF(), estamos en condiciones de mostrarla en su forma final, que sería ésta:


#define BUI_SCANF(REQ_MSG, TERMINATION_FUNC, TERMINATION_LABEL, ERROR_MSG, FMT, ...) { \
       bui_gets(REQ_MSG);                                \
       bui_input.errno = sscanf(bui_input.buffer, FMT, __VA_ARGS__); \
       \
       bui_input.error_state = (bui_input.errno < __bui_N_ARGS(FMT)); \
       if (bui_input.error_state)    \
          if (TERMINATION_FUNC (bui_input.buffer))        \
              goto TERMINATION_LABEL;                     \
          else \
              printf(ERROR_MSG);                          \
       else;   \
    }


Todos los caracteres \ que aparecen al final de cada línea son necesarios en la definición de una macro, para indicar que en realidad se trata de una línea que "continúa debajo".
Hemos puesto el código de la macro "entre llaves", cuando no es estrictamente necesario,
ya que no es una función.
Pero el uso de llaves encierra bien las sentencias, dando lugar a código más seguro, evitando errores sintácticos indeseables o inentendibles.



La librería bui.h está lista.
Le pondemos número de versión 1.20, para seguir con la numeración que traíamos desde GCD,
aunque no tengan ahora nada que ver una cosa con la otra.
Ya hemos independizado por completo la interface de usuario con la rutina matemática GCD.
El archivo luce así (en spoiler):

Spoiler (click para mostrar u ocultar)



La versión 1.20 del programa GCD ahora quedará muy breve, pues se limita a invocar librerías, funciones y macros, y controlar el flujo del programa.

Sin embargo, a nuestro programa le queda todavía una tarea importante:

* Definir todas las cadenas de caracteres que dan información al usuario.

Esto lo haremos en una función llamada start(),
en donde definiremos las componentes que nos interesen de la estructura bui.
Veamos el programa completo directamente, ya que será fácil de interpretar:

Spoiler (click para mostrar u ocultar)

Vemos que en start() hacemos uso de las funciones bui_set_string(), que usamos para asignar cadenas de caracteres concretas a las componentes de la estructura gui.

No hace falta asignar valores a todas las componentes.

Luego invocamos la función bui_show_program_info(), que muestra los datos básicos del programa.
Esos datos al menos sí tendrían que ser bien asignados en las líneas precedentes.

Observemos cómo el programa se controla con un bucle que ahora es muy sencillo de interpretar:

* Primero lee datos del teclado con BUI_SCANF(),
* luego verifica si hubo un error (y en tal caso reinicia el bucle),
* y si toda está correcta realiza el cálculo del máximo común divisor y muestra los resultados.
* Por último, el bucle se repite indefinidamente.

A nuestro programa le faltan algunos comentarios, por ejemplo unos que informen un poco mejor sobre las condiciones bajo las cuales el programa termina. Esto lo haremos luego.

En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #12 : 18/08/2013, 04:17:05 pm »

Cambios nulos de versión...

Si hacemos cambios nulos a un programa, digamos, poniéndole comentarios, agregando líneas en blanco, retocando estéticamente las instrucciones,
pero no hacemos ningún cambio que cambie la sintaxis de fondo del programa, ni nada que afecta al comportamiento cuando el programa se ejecuta.
¿Vale la pena consignarlo como un cambio de versión?

Yo digo que sí.  :lengua_afuera:

Los cambios nulos introducen nuevas ideas relativas al programa,
mejoran la comprensión del mismo, y sirven para aclarar puntos oscuros del funcionamiento del mismo.
Estas aclaraciones sirven además como punto de apoyo para realizar futuros cambios reales en el programa, o inspirar métodos diferentes.

En este post vamos a hacer retoques de efecto nulo a la librería bui.h.
También arreglaremos efectivamente algunos pequeños detalles "de verdad".




En esta versión he cambiado printf("%s", s); por puts(s); en bui_print_list().
La función puts() agrega un caracter fin de línea al final de la cadena s.
Este comportamiento ha de quedar debidamente documentado para evitar sorpresas.

El significado de bui_input.error_state es que "en la última operación de lectura hecha con BUI_SCANF() ha surgido un error".
Para que este significado permanezca siempre "verdadero" (como un invariante del programa) en todo instante de la ejecución del programa, decidimos inicializarlo a false.

En C99 podemos inicializar componentes individuales de una struct, así:


struct {
    char buffer[__INPUT_STR_LEN];
    int errno;
    bool error_state;
} bui_input = { .error_state = false };


Puede parecer extraño inicializar sólo esa variable, y dejar las otras "al aire".
Pero en realidad lo que importa es por qué decidimos inicializarla: es para mantener el mismo significado lógico a lo largo de todo el programa.

Este significado puede verse alterado si el programador cambia manualmente el valor de la variable.
Eso está documentado, y obviamente no se recomienda.
Como sea, es una práctica poco segura, y es culpa nuestra, de los diseñadores de la librería bui.h que esto esté así de desprolijo.

En todas partes documentaremos advertencias de que la función __bui_N_ARGS() no funciona exactamente como debería: no maneja todos los casos.
Esto repercute en que la cadena de formato que le pasemos a BUI_SCANF() no puede ser cualquiera, sino que debe evitar algunas directivas especiales como: %%, %n, %*.

Agregamos a la documentación del programa explicaciones de qué son y qué hacen todos los objetos que figuran en la interface de la librería.
Se ponen recomendaciones de uso, advertencias de malos usos, e incluso información de mejoras que deben hacerse en el futuro.



Hay una presentación de la librería (1er bloque),
luego la interface principal de la librería (2do bloque),
y finalmente la implementación de las funciones y macros de la librería (3er bloque).

En el 1er bloque se explica en forma genérica qué objetos hay en la librería y para qué sirven.

En el 2do bloque se comentan las intenciones y modo de uso de los objetos definidos en la librería.
Se incluyen los prototipos de funciones y macros (éstas sólo en comentarios).
Se explica qué es cada parámetro (de ser necesario),
y se indica cuál es el resultado esperado.
No se colocan aquí las implementaciones de las funciones o macros (a menos que sean triviales).

En el 3er bloque se desarrollan las funciones y macros, y se ponen comentarios técnicos.
Por ejemplo, se insertan aserciones lógicas o implicaciones.

Al final de la función bui_set_string() pusimos "por afuera" del cuerpo de la función un comentario que explica qué le pasa a los parámetros dest y src pasados a la función.
No se puede poner esto dentro de la función, porque las direcciones de memoria apuntan a cualquier parte ahí.
Sólo cuando la función retorna el control a la rutina que la llamó es que podemos hablar de las direcciones de memoria originales de dest y src. En dichas direcciones quedan albergadas dos cadenas de caracteres, que tienen idéntica información y longitud.

Este tipo de comentarios los ponemos pegaditos al final del cuerpo de la función,
y añadiendo una flecha: --->, así:


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

(resto del archivo...)




Hay comentarios precedidos con [!].
Con esto querremos indicar una advertencia: la rutina o resultado obtenido no es perfecto.
Es una señal que dejaremos para indicar que debe mejorarse ese detalle en futuras versiones.

Otros comentarios están precedidos por [#].
Con esto querremos indicar esta advertencia:

Se espera aquí el siguiente rango o tipo de datos: (...)
Si se usa la macro o función de otra manera, los resultados son impredecibles.


Sobretodo esa advertencia aparecerá cuando usemos parámetros dentro de las macros.
Estos parámetros no tienen un tipo de datos asociado, ni siquiera tienen por qué ser un dato de algún tipo, sino que puede ser casi cualquier cosa imaginable.

Recordemos que el preprocesador nos da un lenguaje de macros que muy poco tiene que ver con el lenguaje C. Van por caminos muy distintos en la vida.


En realidad esto no tiene mucha solución dentro del estándar C99.
Pero podemos dejarlo así, especulando conque en el futuro, el estándar C11 (ISO C del año 2011) agregará unas capacidades extra a las macros, para detectar tipos de datos en los parámetros:guiño: :guiño:



Nuestro diseño es tal que ocurren algunos efectos "colaterales".
Se modifican variables de las estructuras bui y bui_input desde diversos lugares del programa,
no siempre visibles.
En algunos casos lo hemos advertido en los comentarios.



El exceso de comentarios puede ser engorroso, hacer que un programa sea difícil de leer o digerir.
Pero esto no es tan grave, ya que es sencillo realizar un programa que "filtre" los comentarios de un modo u otro, y nos devuelva el mismo programa, pero limpio de comentarios.

Por otra parte, considero que el espíritu del programa es lo que más documentación merece,
porque es lo que permite guiar las futuras correcciones, mejoras o cambios de toda índole.


También tiene que estar bien documentada la interface de la librería: qué hace, cómo lo hace, qué se espera que haga, ejemplos típicos de uso, advertencias de limitaciones o fuentes de error, etc.



He aquí el resultado final, a ver si les gusta:

Spoiler (click para mostrar u ocultar)

 :tranqui:
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #13 : 18/08/2013, 07:38:05 pm »

Mejorando la librería bui.h...

Al desplazar las instrucciones de entrada/salida desde el programa principal a una librería,
nos hemos visto atrapados en algunos problemas de diseño.
Nos hemos topado con efectos colaterales, un uso casi obligado de goto, y aunque no lo mencionamos antes, también hay una desprolijidad general al tener acceso "externo" hacia todos los datos de la librería.



Lo primero que vamos a reparar es la función __bui_N_ARGS(), a fin de que funcione correctamente.
Reconoce con exactitud todas las directivas de scanf() según el estándar de C99.
Si hay alguna directiva que no está en la lista, automáticamente se corta el análisis
y se retorna un valor negativo indicando la presencia de un error.
No cuenta los casos de %%, ni los casos ficticios de %*, que es lo que corresponde.

Más aún, reconoce y cuenta correctamente los casos en que hay directivas con dígitos, como %15f.
¿Cómo se hace esto?
Con algo de astucia la solución es simple: simplemente se ignoran los dígitos, "saltando" a la siguiente iteración.
Así, si el indicador "before" era true, sigue siendo true hasta el próximo caracter no-dígito, y en caso contrario sigue siendo false.

Para los casos aceptados por C99 (que denominaremos "seguros", porque sabemos qué consecuencias tienen en el programa), usaremos el enfoque de coleccionar en una constante string todos los caracteres válidos de directivas para reemplazar por un argumento real.
Si se detecta un caso de estos, se aumenta la cuenta en 1.
También usaremos otra constante string para las directivas que no reemplazan por argumentos reales. Si se detecta un caso de estos, no se hace nada.

Si en cambio hay un caso no contemplado, la solución que adoptaremos es drástica: informamos un error y retornamos inmediatamente, terminando la función ahí.

Para saber si un caracter está o no en una lista de caracteres, usaremos una función que llamaremos char_in_str(), que es ésta:


/* 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;     
}


El caso %% es especial y lo tratamos directamente en el algoritmo.
Si "antes" ya había un '%', y hallamos ahora otro consecutivo,
entonces quiere decir que estamos ante el caso especial %%,
y cancelamos el estado del flag "before" poniéndolo a false.
Obviamente, no aumentamos el contador.

Nuestra función quedaría así:

Spoiler: (función __bui_N_ARGS():) (click para mostrar u ocultar)

Hay un caso intrincado que puede generar dudas, y es el siguiente:

¿Qué pasa cuando ponemos la cadena de formato "%234%f"?

Nuestra función ignora los dígitos, así que considera que es análoga a "%%f", y por lo tanto estamos ante la directiva especial %%, y no ante %f.
Así que en este caso el contador no se incrementa.

¿Es correcto que esto ocurra? ¿O es un error?
Si estudiamos la definición de la función scanf() y sus dichosas cadenas de formato,
veremos que "%234%f" en realidad es una cadena de formato errónea.
Su significado no es análogo a "%%f", sino a algo "erróneo" seguido de "%f".

El estándar es claro en esto:
Cita
The complete conversion specification shall be %%.

En este caso lo único que podemos asegurar, según interpretamos del estándar C99,
es que se trata de un caso "no válido", y el resultado es indefinido: el estándar no asegura ningún comportamiento concreto a este caso.

Dado que es un caso no válido, no tendríamos obligación de remediarlo.
Sin embargo, es una nota técnica interesante el notar que nuestra función es inconsecuente con este caso, en el sentido de que no hace el reconocimiento sintáctico exacto, como "debiera ser".

Si bien tanto para sscanf() como para __bui_N_ARGS() se trata de un caso erróneo,
decimos que la semántica de sscanf() y BUI_SCANF() en ambos casos ya no coincide.
Es un detalle fino, que en alguna futura versión sería bueno corregir,
para que la semántica de ambas sea análoga.



Al final del programa agregamos nuevas "undefiniciones":


#undef SCANF_SAFE_ARG_SET_C99
#undef SCANF_SAFE_SPE_SET_C99
#undef SCANF_SAFE_DIGITS
#undef BUI_FMT_NOT_VALID


Además el párrafo de "Precauciones" en los comentarios sobre la macro BUI_SCANF()
ahora dice así:


/* Precauciones:
/* =============
/*      La cadena de formato FMT sólo reconoce directivas de C99.
/*      Opciones adicionales o de C11 y ss. no son reconocidas.
/* */




Adicionalmente, ahora podemos reconocer los casos de cadenas de formato erróneas.
Como esto es un error del programador antes que del usuario,
el mensaje de error lo provee directamente la librería, e intenta "defenderse" tanto como puede,
saltando tan afuera como se le permita (con goto TERMINATION_LABEL;) e informando en bui_input.error_state que ha habido un error.

La idea aquí es que el programador controla adecuadamente, desde la rutina principal,
los casos en que hay un error de usuario, evitando llegar al punto de "escape" de la etiqueta TERMINATION_LABEL.
Si se llega a ese punto con una señal de error, es que algo más ha ocurrido: en este caso un error en la cadena de formato.
Además aparece impreso en pantalla un mensaje, que será este:

Fatal error: format string is wrong.

Lo pongo en inglés para darle más efecto robótico.

Las líneas concretas dicen esto:


       int __n_args = __bui_N_ARGS(FMT);                             \
       if (__n_args < 0) {                                           \
          printf("Fatal error: format string is wrong.\n");          \
          bui_input.error_state = true;                              \
          goto TERMINATION_LABEL;                                    \
       }                                                             \
       bui_input.error_state = (bui_input.errno < __n_args);\


Vemos que tenemos que usar una variable temporal auxiliar __n_args,
cuya "duración" es sólo durante la existencia del bloque demarcado por las llaves.
(Ahora por fin vemos una utilidad de esas dichosas llaves).

La macro completa está aquí:

Spoiler: (macro BUI_SCANF() modificada:) (click para mostrar u ocultar)



A esta altura parece obvio que la macro BUI_SCANF() se está complicando demasiado.

Por ahora dejamos esto así, y buscaremos mejoras de estilo a partir de los siguientes posts.


En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #14 : 18/08/2013, 11:19:12 pm »

Me cansé de los gotos hacia quién sabe dónde de la macro BUI_SCANF().
Una macro dedicada a procesar datos del teclado no debiera preocuparse del destino del resto del programa.
Más bien es preferible que deje registrado todos los posibles errores en alguna parte (en la struct anónima bui_input, por ejemplo), y que el programador se encargue de tomar las decisiones pertinentes.

Vamos a cambiar dicha estructura, para considerar varios tipos de errores:


struct {
    char buffer[__INPUT_STR_LEN];
    int errno;
    bool userinputerr;
    bool fatalerr;
} bui_input = { .userinputerr = false; .fatalerr = false; };


El campo error_state lo he cambiado a userinputerr.
Es que su significado será sólo "el usuario ha ingresado datos erróneos".
En cambio, el campo fatalerr significará "hay un error fatal", que normalmente indicará un error del programador, o alguna otra situación irregular durante la ejecución del programa.



Pasar parámetros a una macro... ¡sólo del tipo especificado!

Ahora vamos a la macro BUI_SCANF(), con la intención de cambiarla por completo.

1ero que nada, vamos a asegurarnos que el tipo de datos que pasamos como parámetros a la macro realmente coincide con lo que nos gusta.
Para ello, definiremos instancias de variables locales al bloque interno de la macro BUI_SCANF(),
aprovecharemos las reglas del compilador para detectar tipos erróneos.

Lo único lamentable de este enfoque es que debemos duplicar variables ya existentes.
(En el caso de strings no se duplica toda la cadena de caracteres, sino sólo el puntero que apunta a ella).
Sin embargo, esto no es muy diferente de lo que ocurre cuando se llama a una función que realiza copias locales de los parámetros que se le han pasado.


       char *__req_msg = REQ_MSG;                 \
       /* [--->] typedef(REQ_MSG) == (char*) */   \
       char *__error_msg = ERROR_MSG;             \
       /* [--->] typedef(ERROR_MSG) == (char*) */ \
       char *__fmt = FMT;                         \
       /* [--->] typedef(FMT) == (char*) */       \
       int (*__spfunc)(char*) = SPFUNC;          \
       /* [--->] typedef(SPFUNC) == int (función) (char*) */   \


Cuando el programador intente pasar parámetros irresponsables o erróneos a la macro,
el compilador los rechazará sin problemas.

Nota: He eliminado el parámetro TERMINATION_FUNC.
En su lugar voy a poner otro que diga SPFUNC,
que tiene otro significado: "función especial".

Esa función admite una string (char*) como parámetro, y retorna un int.
Si no se pasa una función con el formato apropiado, el programa no compilará, lo cual nos da un gran alivio,
ya que estamos controlando las arbitrariedades intrínsecas del uso de macros.


La "función especial" ahora retornará información más rica que un simple true/false,
ya que se usará para tener en cuenta toda una lista de posibles entradas especiales que el usuario pueda elegir, es decir: ¡comandos!

Estos comandos habrán de estar referenciados (como constantes int) en alguna parte. ¿En dónde conviene? ¿En la librería bui.h, o en el programa principal?
Lo adecuado, creo yo, es que se definan en el programa principal.

La macro BUI_SCANF() solamente tendrá en cuenta si el resultado de esa "función especial" es 0 o no.
Con el valor 0 vamos a significar: "no ha ocurrido nada especial", es decir, el usuario ha entrado datos tal como se le pedían, sin intentar ningún "comando extraño".

Esto debe estar documentado, para que el programador pueda diseñar funciones "especiales" que trabajen armoniosamente con BUI_SCANF().

A la estructura bui_input le agregaremos el campo special,
que guardará el resultado de la llamada a la función "especial" __spfunc().

Así que ahora nuestra macro tendrá un comportamiento más lógico:

(1ro) Descarta de plano los parámetros de tipo incorrecto.
(2do) Verifica si la cadena de formato es correcta. Si es errónea, da un mensaje de error fatal, registra el error en bui_input.fatalerr, y no hace nada más.
(3ro) Si no, lee datos desde el teclado: exactamente una línea hasta que el usuario presiona Enter.
(4to) Verifica si el usuario ha introducido algún comando "especial", lo cual equivale a un valor no nulo. En ese caso registra el hecho en bui_input.special, y no hace nada más.
(5to) Si no, se sobreentiende que el usario ha intentado ingresar datos de manera normal.
     Se verifica si los datos ingresados son correctos.
(6to) Si no son datos correctos, se imprime en pantalla el mensaje de error que está en la string __msg_err.
(7mo) Si los datos ingresados por el usuario son correctos, no se hace nada, pero queda registrado que no hubo errores en bui_input.userinputerr.

En todos los casos, tras terminar el bloque de la macro BUI_SCANF()
se han actualizado correctamente todos los campos booleanos de bui_input,
para que el programa principal pueda tomar decisiones en base a ellos.

Spoiler: (macro BUI_SCANF(), nueva versión) (click para mostrar u ocultar)

Desde la rutina principal hay que realizar verificaciones en un orden específico,
que es el que explicaremos en comentarios como los que siguen:


/* if(bui_input.fatalerr)
/*   /* Finalizar el programa */
/* else if (bui_input.special)
/*       switch(bui_input.special) {
/*           case 1: /* ejecutar rutina 1 */
/*           break;
/*           case 2: /* ejecutar rutina 2 */
/*           break;
/*           /* ... ^/
/*           case N: /* ejecutar rutina N */
/*           break;
/*       }
/* else if (bui_input.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 */




La parte de comentarios que explican el uso de BUI_SCANF() ha cambiado,
y también hemos cambiado los comentarios de la estructura bui_input.
Además, ahora se me ocurrió mejor dejar inicializados todos los campos de bui_input.

La librería en su nueva versión queda ahora así:

Spoiler: (bui.h versión 1.23) (click para mostrar u ocultar)

En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #15 : 18/08/2013, 11:28:44 pm »

El programa del máximo común divisor (que ya no me acuerdo qué significaba eso)
ahora hace caso a las recomendaciones de los comentarios que figuran en bui.h,
y actúa en consecuencia, según los errores encontrados en la estructura bui_input.

El resultado tiene esta pinta:

Spoiler: (GCD 1.23) (click para mostrar u ocultar)

A pesar de todas las revoluciones internas que hemos estado haciendo,
el programa hace lo mismo que hace unos 5 o 6 versiones atrás.

Sólo estamos aprendiendo a programar con más prolijidad...  :sonrisa:
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #16 : 19/08/2013, 01:16:57 am »

La traicionera función gets()...

Hasta ahora he usado gets() para leer la entrada, en forma totalmente desvergonzada.
No es posible predecir el tamaño de los datos de entrada, causando posibles estragos no sólo en el programa, sino en todo el sistema.  :sorprendido: :sorprendido: :sorprendido: :sorprendido:

Esto se debe a que si se leen más caracteres que los que caben en el buffer utilizado,
el resultado es una sobreescritura en las posiciones aledañas de memoria, que no se sabe quién o qué las está ocupando.

gets() es una función que está totalmente "prohibida".
Usarla es un pecado informático.  :malvado: :malvado:


Necesitamos una alternativa.

La función fgets() cumple una tarea similar, salvo que lee una línea de cualquier archivo que le indiquemos.
¡Pero la entrada estándar también es un archivo! Y su nombre es stdin.

Así que, reemplazaríamos

gets(buffer);

por la sentencia:

fgets(buffer, n, stdin);

Ahí apareció un misterioso parámetro "n".
Es un número entero que indica cuántos caracteres se van a leer como máximo.
(En realidad se leen [texx]n - 1[/texx], o sea, 1 menos que el especificado).
Esto es importante porque ahora podemos poner un límite superior a la cantidad de caracteres que queremos leer.
Ajustaremos esa cantidad a la constante __INPUT_STR_LEN de nuestra librería bui.h.

Ahora tenemos un método más "seguro" para lectura de datos de la entrada estándar...

Pero hay algunos problemas nuevos que atender.
Si bien gets() lee hasta el caracter fin de línea, procede luego a descartarlo, y lo reemplaza por un caracter nulo.
En cambio fgets() no descarta el caracter fin de línea cuando lo lee.
En cualquier caso, añade luego un caracter nulo.

Este salto de línea '\n' que queda retenido en la cadena leída no es gran problema,
ya que nosotros luego hacemos una relectura con sscanf(), que hace caso omiso de los saltos de línea que cuelgan al final, ya que son datos "basura", que no influyen en la asignación de valores a los argumentos de sscanf().

No obstante, la posible presencia de un caracter fin de línea al final de la cadena
es algo que debe documentarse, para que el programador diseñe funciones del tipo que encaja en el parámetro SPFUNC de la macro BUI_SCANF(), y tiene que tener en cuenta la presencia de tales caracteres (y descartarlos elegantemente).



Pero el problema mayor ocurre cuando el buffer es demasiado pequeño, y fgets() termina de llenarlo antes de haber alcanzado el caracter fin de línea.
Esto quiere decir que los caracteres sobrantes que ha ingresado el usuario
todavía quedan aguardando en memoria hasta la próxima llamada a fgets().

El efecto de esto es que la próxima llamada a fgets() no pedirá datos al usuario, sino que los tomará directamente de la memoria, porque ahí ya había datos aguardando a ser usados.

Se presenta entonces el problema de "limpiar" el archivo de entrada estándar stdin.
El modo más claro y sencillo de hacerlo es con una función como ésta:

void flush_stdin(void) {
       while ( getchar() != '\n' )
           /* */ ;
}


Pero si invocamos esta función detrás de fgets(),
incluso en los casos en que se ha alcanzado y leído correctamente el caracter fin de línea,
la función flush_stdin() buscará otro más.

Un modo de evitar esto es analizar si el buffer contiene o no un caracter fin de línea.
Pero tenemos que analizar esto con inteligencia.
Si el buffer contiene un '\n', contiene uno solo, y puede estar ubicado en cualquier parte de la cadena.
¿Acaso nos vamos a poner a buscarlo por ahí adentro?  :enojado:
¿Qué tan complicado se vuelve esto de descartar un simple fin de línea?  :enojado:  :enojado: :enojado:

La verdad es que eso no nos interesa.
Lo que nos interesa es reconocer el caso en que un '\n' no ha sido alcanzado.
Entonces el buffer contiene todos caracteres distintos de '\n'.
Más aún, el buffer está lleno, porque si no, significaría que el fin de línea '\n' se habría alcanzado.
En este caso, el último caracter del buffer es '\0', el caracter nulo.

Supongamos que buffer tiene longitud [texx]N[/texx].
Como la primer posición es 0, el último caracter tiene índice [texx]N-1[/texx].
Así que

buffer[N-1] == '\0'

En cambio

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

Y más aún:

buffer[N-2] != '\n'

Recíprocamente,
si estas últimas dos condiciones se cumple, quiere decir que el buffer está lleno,
o sea que fgets() no ha alcanzado el fin de línea.

En resumen:
no se ha alcanzado el fin de línea si y sólo si (buffer[N-2] != '\0') && (buffer[N-2] != '\n')

Así que ahora tenemos una manera correcta de limpiar la entrada estándar:


char buffer[N];
fgets(buffer, N, stdin);
if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
   /* No se ha alcanzado el fin de línea */
   flush_stdin();
else
  /* Sí se ha alcanzado el fin de línea: no se hace nada. */
  ;


Eso debería funcionar...
pero siempre falla algo.
¿Cuál es el problema?

El problema es que el buffer puede contener caracteres e información basura
provenientes de previas acciones realizadas en el programa.
El buffer no está "limpio", y puede que en la posición [texx]N-2[/texx] haya caracteres que no han sido colocados ahí por la última llamada a fgets().

La solución más natural sería limpiar tooooodo el buffer, poniendo todos sus caracteres a 0,
antes de cada llamada a fgets():llorando: :llorando:

Pero obviamente esto no hace falta.  :tranqui: :tranqui:

Ya que toda la información que necesitamos está en buffer[N-2],
basta que "limpiemos" ese único caracter, poniéndolo a 0.  :beso: :beso:
Es extrañísimo, pero funcionará.

Esta limpieza extraña quiero hacerla así por una razón bien concreta:

* Es mucho menos costoso limpiar 1 solo caracter que toda una cadena de caracteres.

Si en el programa hacemos mucho uso de la "limpieza", estaríamos gastando muchos recursos.
El programa se vuelve lento...

Así que nos quedamos con esta versión de las cosas.
(Notar que el tamaño del buffer tiene que ser ahora [texx]N \geq 2[/texx] para que no haya problemas).


/* Elegir un valor de N que sea bastante grande, en particular, >= 2. */
char buffer[N];

buffer[N-2] = '\0'; /* Limpieza rápida del buffer */
fgets(buffer, N, stdin);
if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
   /* No se ha alcanzado el fin de línea */
   flush_stdin();
else
  /* Sí se ha alcanzado el fin de línea: no se hace nada. */
  ;




Como resultado de todos estos divagues, la versión 1.24 de bui.h contiene sólo modificaciones de la función bui_gets(), que queda ahora así:

Spoiler: (bui_gets():) (click para mostrar u ocultar)
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #17 : 19/08/2013, 02:21:54 am »

Aún con bui_gets()...

Me molestan los efectos colaterales de bui_gets().

(Post modificado: he corregido bui_gets() para el caso N == 1).

En primer lugar quitaré la sentencia printf(req); de ahí,
y la moveré directamente al cuerpo de BUI_SCANF(), así:

printf(__req_msg);

Además, cambiaremos la manera de trabajar de bui_gets().
Haremos que se parezca a fgets():

Aceptará 2 parámetros: una string que hará de buffer para albergar los datos leídos, y un entero N que indica el máximo de caracteres a leer de la entrada estándar.

Ahora bui_gets() es una función de propósito general, que no depende de los datos de la estructura bui_input, por ejemplo.

En particular, tiene que manejar bien el caso [texx]N < 2[/texx].
Si N <= 0, tiene que retornar NULL y no hacer nada, porque no hay espacio disponible para ingreso de datos.
Si N == 1, entonces sólo hay espacio para 1 caracter: el nulo.
Para ser consecuentes, se lee la entrada estándar con fgets(),
pero sabemos ahora que, con toda seguridad, no se ha leído el fin de línea,
pues se han leído N - 1 == 0 caracteres.

En cualquier caso, puede que queden caracteres de "sobra", y se realiza la "limpieza" ya explicada.

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, bui_gets() no es válida. */
char* bui_gets(char *buffer, int N) {
    if (N <= 0)
        /* No hay espacio para leer ---> error */
        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;
}
 



Esto obliga a cambiar el modo de usar bui_gets() dentro de BUI_SCANF().
Los cambios se notan sólo en el último bloque else,
y por eso sólo reproducimos ese trozo:


       else { \
           printf(__req_msg);                                               \
           bui_gets(bui_input.buffer, __INPUT_STR_LEN);                     \
           bui_input.special = __spfunc(bui_input.buffer);                  \

           /* resto del bloque else... */

       } /* else */ \




Además, todo lo que aparece dentro de la macro debe ser visible desde "afuera" de la librería bui.h.
Así que quitamos la línea que "undefine" __INPUT_STR_LEN:


// #undef __SHORT_STR_LEN /* Línea borrada */




Como ahora bui_gets() es una función de propósito general,
la mostraremos en la interface de la librería, prototipada y comentada, así:


/* bui_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".
/*
/* */
char* bui_gets(char*, int);




Ya hemos pulido bastante la librería bui.h, quitándole los efectos colaterales,
los gotos, las funciones no seguras, los problemas indeseables en la entrada que pudiera dar scanf(), etc., etc.

La coloco en spoiler para quien tenga ganas de probarla:

Spoiler: (bui.h versión 1.25): (click para mostrar u ocultar)



Hay muchas mejoras técnicas posibles,
pero las dejaremos para otro momento.
Por ahora me doy por satisfecho.

 :beso:
En línea

argentinator
Consultar la FIRMAPEDIA __________________________________________________________________________________________________________________
Administrador
Pleno*
*****

Karma: +0/-0
Desconectado Desconectado

Sexo: Masculino
Argentina Argentina

Mensajes: 7.273

Vean mis posts activos en mi página personal


Ver Perfil WWW
« Respuesta #18 : 19/08/2013, 03:58:57 am »

(En el post anterior voy a quitar el scanf("%*c");, que no es buena idea, por el bucle while() que limpia stdin...)
En línea

Páginas: [1]   Ir Arriba
  Imprimir  
 
Ir a:  

Impulsado por MySQL Impulsado por PHP Powered by SMF 1.1.4 | SMF © 2006, Simple Machines LLC XHTML 1.0 válido! CSS válido!