26 de octubre de 2016

Repeticiones híbridas.

   El lenguaje de programación C incorpora dos sentencias bastante prácticas y útiles en diferentes contextos: break y continue. Dichas sentencias permiten implementar un tipo de repetición híbrida, en cuanto a que es posible construir ciclos que incorporen los mecanismos de repetición controlada por contador y centinela de manera combinada.

   El programa del Ejemplo 3.20 muestra el uso de la sentencia continue dentro de un ciclo for, pero tanto break como continue pueden utilizarse también en los ciclos while y do-while.

   En principio, la estructura de repetición for está gobernada por un tipo de repetición controlada por contador, y también en principio, dicho ciclo se repetirá NUM veces. Sin embargo, la sentencia continue de la línea 13 indica un cambio en el flujo de ejecución secuencial, de tal forma que cuando el flujo de ejecución procesa la sentencia continue, el flujo de ejecución brinca a la verificación de la expresión3, para después seguir con la secuencia de ejecución usual de la estructura for, es decir, hacia la validación de la expresión condicional (expresión2).

   En resumen, el ciclo for se repite NUM veces, pero no así todas las sentencias del ciclo, ya que cuando el flujo de ejecución alcanza la sentencia continue, las sentencias posteriores a ella serán ignoradas, por lo que la salida del Ejemplo 3.20, es la impresión de los números del uno al diez, uno por renglón, excepto el CENTINELA, tal y como se muestra en la siguiente figura:

Salida del Ejemplo 3.20.
 
    Ahora bien, el Ejemplo 3.21 muestra una situación análoga. Para empezar, note que dicho ejemplo es exactamente el mismo que el Ejemplo 3.20 excepto por la línea 13, donde se ha cambiado la sentencia continue por la sentencia break.

   La sentencia break ha sido descrita en la entrada referente a la estructura de selección switch, en donde servía para romper el flujo de ejecución y brincar al final de dicha estructura de selección. Para el caso del ciclo for funciona de manera análoga, ya que cuando el flujo de ejecución la procesa, es como si se diera un salto a la expresión condicional (expresión2) del ciclo y ésta fuera evaluada como cero (falsa), lo que implicaría la terminación del ciclo.

   En términos prácticos es válido decir que la sentencia break rompe el ciclo en el que se encuentre la sentencia sin importar la expresión condicional que lo gobierna. Vale pena mencionar que, si se encuentran varios ciclos anidados, la sentencia break rompe únicamente el ciclo más interno que la contenga, los demás ciclos permanecen inalterados (compruebe lo anterior probando el Ejemplo 3.21_1).

   Observe que en principio el ciclo for del Ejemplo 3.21 está también gobernado, al menos en su expresión condicional, por un tipo de repetición controlada por contador, y que en principio se repetirá NUM veces. Sin embargo, cuando el valor del contador coincida con el del CENTINELA, la sentencia break será procesada y el ciclo terminará, por lo que el efecto del programa es ver los números del uno al CENTINELA - 1, uno por renglón, tal y como se muestra en la siguiente figura:

Salida del Ejemplo 3.21.
 
    Finalmente se presenta el Ejemplo 3.25 el cual muestra una variación del Ejemplo 3.21. Tome en cuenta que el Ejemplo 3.25 genera una salida distinta y se deja como ejercicio al lector determinar su salida antes de ejecutarlo y corroborar su respuesta con su correspondiente ejecución.

   El Ejemplo 3.25 declara una variable entera llamada bandera. Una bandera es muy común en la programación y se utiliza como indicador para señalar una determinada situación. Para el caso de nuestro ejemplo, la bandera se ha "encendido" o inicializado en 1 (línea 12) para garantizar la continuidad del ciclo en la expresión condicional (expresión2) del ciclo for, por lo que su continuidad depende en principio, estrictamente de la variable de control (i). Ahora bien, cuando la expresión condicional de la línea 13 se cumple, es decir, la variable de control tiene el mismo valor que el CENTINELA, la variable bandera se "apaga" (se le asigna 0 en la línea 14) y la sentencia de la línea 15 se ejecuta independientemente de la condición de la línea 13.

   El efecto derivado de apagar la bandera es el siguiente: al llegar al fin del bloque del ciclo for, el flujo de control brinca a la modificación de la variable de control (expresión3 del for), para posteriormente pasar el control a la expresión condicional del ciclo for (expresión2) y, al determinarse que la bandera ha sido apagada (0), toda la expresión condicional es evaluada a cero (como si fuera falso) terminando así el ciclo antes de que la variable de control terminara con sus NUM iteraciones.

   En resumen, el Ejemplo 3.25 implementa una repetición controlada por contador (i), pero si se verifica la aparición del CENTINELA, la continuidad de ciclo se interrumpe por medio de una bandera.

20 de octubre de 2016

Tipos de repetición.

   Independientemente del tipo de estructura de control que se utilice para la repetición de un grupo de sentencias, existen básicamente dos tipos de repetición:
  1. Repetición controlada por contador.
  2. Repetición controlada por centinela.
   Ambos tipos de repetición pueden ser implementados en C utilizando cualquiera de las estructuras de control while, do-while y for, por lo que no hay una relación de correspondencia única entre el tipo de repetición y la estructura de control de repetición que se utilice. Sin embargo, debido a la naturaleza y al funcionamiento de las estructuras de control de repetición, el enfoque de repetición controlada por contador se implementa de manera más natural utilizando los ciclos while y for; mientras que la repetición controlada por centinela, se implementa de forma ligeramente más natural utilizando la estructura de control de repetición do-while, pero esto es más una conveniencia que una asociación.


18 de octubre de 2016

La estructura de control for.

   La estructura de control o ciclo for tiene la siguiente estructura general:

          for ( expresión1; expresión2; expresión3 ) {
              sentencia(s);
          }

   El ciclo for procesa o ejecuta el grupo de sentencia(s) delimitadas por su bloque de manera repetida, mientras la expresión2, al ser evaluada, sea distinta de cero, es decir: expresión2 != 0.

   La llave izquierda “{” delimita el inicio de su bloque, mientras que la llave derecha “}” delimita su final.

   Si se procesan el conjunto de sentencia(s) delimitadas por el bloque del ciclo for, cuando la secuencia de ejecución alcanza el indicador de fin del bloque, la estructura de repetición for modifica el flujo de ejecución secuencial y brinca automáticamente a la evaluación de la expresión3, la cual habitualmente modifica la(s) variable(s) de control; después de esto, el flujo de control se pasa a expresión2, y si expresión2 != 0, se repite el procesamiento de las sentencia(s) delimitadas por el bloque. En caso contrario, se procesa la siguiente sentencia que se encuentre después del indicador de fin de bloque del ciclo for (es un error común de programación terminar por "inercia" una estructura de repetición for con “;”, lo cual no es ningún error respecto a la gramática del lenguaje, por lo que el compilador no identificará nada extraño).

   Cabe mencionar aquí que en un ciclo for cualquiera de las expresiones: expresión1, expresión2, o expresión3, pueden ser vacías. Si expresión2 es vacía, se genera un ciclo infinito.

   La estructura de repetición for es equivalente a la estructura de repetición while en el siguiente sentido:

                  expresión1;
                  while (expresión2) {
                      sentencia(s);
                      expresión3;
                  }

por lo que puede decirse, que cualquier ciclo escrito en una estructura de repetición while puede ser reescrito en la estructura de repetición for y viceversa, de tal forma que es posible mantener la equivalencia lógica y funcional entre una y otra estructura.

   El Ejemplo 3.13 muestra el uso de la estructura de repetición for, el cual genera la misma salida que para el Ejemplo 3.9. Asegúrese de comprender su funcionamiento.

   Considere ahora el Ejemplo 3.14 el cual utiliza una estructura for un poco más elaborada que la anterior para implementar la sumatoria de los números entre 1 y n, dada por la siguiente expresión:
   Observe que es posible inicializar más de una variable en la expresión1 del ciclo for (línea 16): la variable de control i, que sirve para ir generando la serie de números, mientras que la variable sum se utiliza como acumulador. De hecho, es posible tanto inicializar como modificar más de una variable de control en expresión1 y expresión3 respectivamente, y la forma de hacerlo es escribir dichas expresiones como una lista de variables separadas por comas.

   La expresión de la línea 17 del Ejemplo 3.14 puede interpretarse en nuestro lenguaje como: “A la variable sum increméntala en i”, debido a que en C, la expresión:

variable = variable operador expresión;
es equivalente a:

variable operador= expresión;
donde operador es cualquier operador aritmético válido en C, y se le denomina notación compacta. Es muy común escribir este tipo de expresiones usando dicha notación.

   El Ejemplo 3.15 muestra el uso de la estructura de repetición for con dos variables de control i y j, el ciclo puede ser gobernado por ambas variables si el problema a resolver así lo requiere, pero para este ejemplo, sólo se imprimen las variables de control en orden ascendente y descendente respectivamente, como se muestra en la siguiente figura:

Salida del Ejemplo 3.15.
 
    Observe que la expresión1 del ciclo for (línea 11) contiene la inicialización de las dos variables de control separadas por una coma (,). De manera análoga, la expresión3 contiene la modificación, es decir el incremento (i++) y decremento (j--) respectivamente de las variables de control separadas también por una coma.

   Por otro lado, el Ejemplo 3.16 genera una secuencia de quince caracteres (véanse las línea 8, 14 y 15) aleatorios. El programa incluye en la línea 5 la biblioteca estándar stdlib.h, la cual es necesaria para poder utilizar las funciones srand y rand de las líneas 13 y 15 respectivamente.

   La biblioteca time.h de la línea 6 se ha incluido para poder hacer uso de la función time de la línea 13, la cual permite leer la fecha y hora del sistema en donde se ejecuta el programa, y regresa un número de tipo size_t el cual es capaz de almacenar los segundos que han transcurrido desde las cero horas del primero de Enero de 1970, y que es utilizado por la función srand para inicializar la semilla de números aleatorios con un valor distinto en cada ejecución.

   Los números aleatorios son generados por alguna función matemática, y para generarse, se basan en un valor inicial (semilla), si éste valor no cambia, la secuencia de números generados tampoco. Por lo tanto, la función srand es la responsable de cambiar la secuencia de generación de números aleatorios a partir del valor semilla, que para el caso del Ejemplo 3.16, utiliza la hora del sistema cada vez que se ejecuta el programa.

   Ahora bien, la función encargada de generar el número aleatorio a partir de la inicialización que hizo la función srand es la función rand, (línea 15). La función rand genera un número aleatorio entre 0 y RAND_MAX, el cual es un valor dependiente de la implementación de la biblioteca (debido a las diferentes arquitecturas de hardware y software), pero se garantiza que es de al menos 32,767 en cualquier implementación de la biblioteca que se base en el estándar ANSI C.

   Observe que el valor de la función rand es divido por 26, y que el residuo de dicha división es sumado al carácter 'a' (línea 15). Por extraño que parezca (sumarle un número a una letra), tiene sentido al menos en C, y se explica de la siguiente manera:

   Sea n el número que representa el código del carácter 'a' (recuerde que lo que la computadora entiende, procesa y “ve” son sólo números, de hecho el valor del carácter 'a' es traducido por el compilador a su representación numérica, pero para nosotros como seres humanos, es más fácil y claro de entender el símbolo 'a', que el código numérico que lo representa; y aunque se podría poner directamente el código del carácter 'a', el precio a pagar sería la portabilidad a otros sistemas, así como la problemática de los números mágicos discutida con anterioridad), y sea d definido como:

d = rand( ) % 26
y c como:
c = n + d

entonces c estará en el conjunto de las letras:

{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}

   El valor de c representado como carácter, es lo que se imprime en el especificador de formato “%c”, el cual es el especificador de formato en C para los caracteres (char).

   En resumen, el Ejemplo 3.16 genera una secuencia de quince caracteres aleatorios. Antes de avanzar, asegúrese de comprender lo que se ha descrito hasta aquí y por qué el Ejemplo 3.16 genera una salida como la de la siguiente figura. Pruebe con varias ejecuciones, quizá alguna de ellas contenga una palabra medianamente comprensible.

Una posible salida del Ejemplo 3.16.

14 de octubre de 2016

Ejercicios selectos (estructuras de control).

Primera Parte.
  1. Escriba el Ejemplo 3.1 eliminando los delimitadores de bloque para cada una de las estructuras de selección if, y compruebe que que sigue funcionando igual. Tome en cuenta que ésto es cierto cuando la estructura de control se compone únicamente de una única sentencia.
  2. Escriba un programa que determine si un número entero positivo es par o impar. Un número par es aquel que es divisible por dos.
  3. Escriba un programa que utilice una estructura de selección if-else y los operadores relacionales para determinar el menor de tres números enteros (asuma que los números son distintos). Para este ejercicio, no debe utilizar operadores lógicos, si no sabe qué son, mucho mejor, en su momento se presentarán en el blog.
  4. Escriba un programa que utilice una estructura de selección if-else y los operadores relacionales para determinar el mayor de tres números enteros (asuma que los números son distintos). Para este ejercicio, no debe utilizar operadores lógicos, si no sabe qué son, mucho mejor, en su momento se presentarán en el blog.
  5. Escriba un programa que utilice una estructura de selección if-else y los operadores relacionales para determinar el número central (en función de su ordinalidad) de tres números enteros (asuma que los números son distintos). Para este ejercicio, no debe utilizar operadores lógicos, si no sabe qué son, mucho mejor, en su momento se presentarán en el blog.
  6. Reescriba el Ejemplo 3.6 usando la simplificación de líneas descrita para el operador ternario (vea el penúltimo párrafo de la entrada El operador ternario ? :) y compruebe su equivalencia, ¿cuál es mejor y por qué?, ¿en qué casos convendría utilizar una u otra?
  7. Reescriba el Ejemplo 3.6 utilizando una estructura de selección if-else en lugar del operador ternario.
  8. Escriba un programa que utilice el operador ternario para determinar el menor de dos números enteros. Este programa es similar al del Ejemplo 3.6.
  9. Extienda el Ejemplo 3.7 para que el rango de conversión de números a palabras sea más amplio: agregue las sentencias necesarias de tal forma que el programa considere al menos números del uno al diez, ¿qué sucede si al ejecutar el programa en lugar de un número escribe una letra?
  10. Escriba un programa que convierta lo siguientes números a su correspondiente número romano: 1, 5, 9, 14, 20, 26, 40, 50, 90, 99, 100, 500 y 1000. Para cualquier otro número se deberá reportar: "No existe traducción disponible".
  11. Convierta el Ejemplo 3.7 y el ejercicio anterior a su correspondiente representación con una estructura de selección if-else anidada. Para este ejercicio, asuma que no existe la estructura de control switch; la idea es obtener la misma funcionalidad cambiando la estructura switch por una estructura if-else.
  12. Reescriba el Ejemplos 3.8 y el Ejemplo 3.9 utilizando el operador de incremento como prefijo.
  13. Reescriba los Ejemplos 3.8 y 3.9 utilizando el operador de incremento como postfijo.
  14. Para el Ejemplo 3.10 sustituya el operador de incremento (++) por el operador de decremento (--), analice lo que sucederá, determine su respuesta y compruébela con la ejecución correspondiente.
  15. Escriba un programa que imprima en la salida estándar los números del uno al diez, uno por renglón pero en orden inverso; es decir: 10, 9, 8, 7, . . . , 1. No utilice el operador de decremento.
  16. Escriba un programa que imprima los números del uno al diez uno por renglón pero en orden inverso; es decir: 10, 9, 8, 7, . . . , 1. Utilice el operador de decremento.
  17. Reescriba el Ejemplo 3.9 utilizando la estructura de repetición do-while.
  18. Reescriba el Ejemplo 3.12 utilizando la estructura de repetición while.
  19. Repita el Ejercicio 16 utilizando ahora la estructura de repetición do-while.
  20. Escriba un programa que imprima en la salida estándar los números del 1 a n, donde n es un valor proporcionado por el usuario de su programa. Así por ejemplo, si n es 6, su programa imprimirá los números del 1 al 6.
  21. Considere el Ejemplo 3.13 y coloque al final de la línea 12 un “;”, compile el programa y ejecútelo. Lo único que verá en la salida estándar es: “i = 11” ¿Por qué?
  22. Reescriba todos los ejemplos y ejercicios que utilizan la estructura de repetición while utilizando ahora la estructura de repetición for.
  23. Reescriba el Ejemplo 3.14 cambiando la estructura de repetición for por la estructura de repetición while.
  24. Reescriba el Ejemplo 3.16 con una estructura de repetición while.
  25. Modifique el Ejemplo 3.16 para que imprima en la salida estándar varias palabras (n) de longitud (línea 8). Pruebe con varias combinaciones de n y m, e imprima en varias columnas utilizando la secuencia de escape '\t'; la idea es reconocer una palabra de las que se generan; tome en cuenta que mientras más larga sea la palabra, la probabilidad de reconocer una será menor. Sugerencias:
    1. Utilice un ciclo externo que contenga al ciclo for del Ejemplo 3.16 para que controle el número de palabras (n) a generar.
    2. Defina a n y m como variables para un mayor dinamismo e interacción y no olvide validarlas.
    3. Coloque el inicializador de semilla de números aleatorios (función srand) afuera y antes de los ciclos.


 

Segunda Parte.

  1. Escriba un programa que, siguiendo la idea del Ejemplo 3.17 y considerando un esquema de calificaciones basado en letras (A, B, C, D y F), procese un conjunto de dichas calificaciones desde la entrada estándar y determine cuántas calificaciones hay de cada una. Si se procesa una calificación no considerada, se deberá indicar dicha irregularidad (no existe la calificación E por ejemplo).
  2. Escriba un programa que lea dos números enteros:
    1. Si los números son iguales, su programa reporta dicha situación y termina.
    2. Si el primer número (num1) es menor que el segundo (num2), su programa deberá imprimir en la salida estándar la sucesión ascendente de números de num1 a num2; es decir: num1, num1+1, num1+2, . . . num2.
    3. Si el primer número (num1) es mayor que el segundo (num2), su programa deberá imprimir en la salida estándar la sucesión descendente de números de num1 a num2; es decir: num1, num1-1, num1-2, . . . num2.
  3. Escriba un programa que determine si un número entero positivo n es o no un número primo. Un número primo es aquel que sólo es divisible por sí mismo y la unidad. Sugerencia: intente con un ciclo todos los posibles divisores de n, si tiene más de dos, no es primo. Otro enfoque es descartar los divisores obvios (1 y n) y buscar algún divisor entre 2 y n/2 (para n > 2), si existe alguno, no es primo.
  4. Modifique el Ejemplo 3.21_1. Cambie la sentencia break por continue y determine su salida sin ejecutarlo en la computadora; después corrobore su respuesta con la salida real.
  5. Considere el programa del Ejemplo 3.22 y determine su salida sin ejecutarlo en la computadora; después corrobore su respuesta con la salida real.
  6. Considere el programa del Ejemplo 3.23 y determine su salida sin ejecutarlo en la computadora; después corrobore su respuesta con la salida real.
  7. Para el programa del Ejemplo 3.23 agregue una llave izquierda ({) al final de la línea 11 y una llave derecha (}) en la línea 15, de tal forma que se delimite el bloque del ciclo for más externo para que las sentencias de las líneas 12-14 pertenezcan a él ¿Cambia en algo la salida del programa? Determine su respuesta sin ejecutarlo en la computadora, después ejecútelo, y analice lo que sucede.
  8. Para el programa modificado con base en lo expuesto en el ejercicio anterior, cambie la i por j en la línea 13 y determine la salida sin ejecutarlo en la computadora; después corrobore su respuesta con la salida real.
  9. Considere el programa del Ejemplo 3.24 y determine su salida sin ejecutarlo en la computadora; después corrobore su respuesta con la salida real.
  10. Modifique el Ejemplo 3.25 par que funcione exactamente igual que el Ejemplo 3.21, es decir, ambos programas deben generar la misma salida. Para este ejercicio, asuma que no existe la sentencia break y no modifique el CENTINELA ni el valor de las variables. Sugerencia: intente dos versiones, una utilizando un continue en el if original, y otra utilizando una estructura if-else sin tener que hacer uso del continue.
  11. Pruebe con más datos el Ejemplo 3.17, proporcionados tanto del teclado como de un archivo siguiendo los mecanismos de re direccionamiento.
    12. Reescriba el Ejemplo 3.18 con una estructura de repetición do-while y asegúrese de que sea lógica y funcionalmente equivalente.
    13. Reescriba el Ejemplo 3.18 con una estructura de repetición for y asegúrese de que sea lógica y funcionalmente equivalente.
    14. Modifique el Ejemplo 3.18 para que lea el número de calificaciones a procesar. Tanto el número de calificaciones (mayor que cero), como las calificaciones (entre 0 y 10) deberán ser validados. Utilice la estructura de repetición while para la repetición controlada por contador.
    15. Modifique el Ejemplo 3.18 para que lea el número de calificaciones a procesar. Tanto el número de calificaciones (mayor que cero), como las calificaciones (entre 0 y 10) deberán ser validados. Utilice la estructura de repetición do-while para la repetición controlada por contador.
    16. Modifique el Ejemplo 3.18 para que lea el número de calificaciones a procesar. Tanto el número de calificaciones (mayor que cero), como las calificaciones (entre 0 y 10) deberán ser validados. Utilice la estructura de repetición for para la repetición controlada por contador.
    17. Reescriba el Ejemplo 3.19 con una estructura de repetición do-while y asegúrese de que sea lógica y funcionalmente equivalente.
Reescriba el Ejemplo 3.19 con una estructura de repetición for y asegúrese de que sea lógica y funcionalmente equivalente.
    18. Modifique el Ejemplo 3.19 para que las calificaciones sean validadas (que estén entre cero y diez). Utilice la estructura de repetición while para la repetición controlada por centinela.
    19. Repita el ejercicio anterior pero utilice la estructura de repetición do-while para la repetición controlada por centinela.
    20. Repita el ejercicio anterior pero utilice la estructura de repetición for para la repetición controlada por centinela.
    21. Escriba un programa que realice la suma de los números del uno al diez:
                (a) Utilice la estructura de repetición while.
                (b) Utilice la estructura de repetición do-while.
    22. Escriba una programa que realice la suma de números enteros procesados desde la entrada estándar. La suma se detiene cuando el número leído sea igual a cero.
                (a) Utilice la estructura de repetición while.
                (a) Utilice la estructura de repetición do-while.
    23. Escriba un programa que permita determinar el mayor de n números proporcionados desde el teclado, donde n >1.
    24. Escriba un programa que permita determinar la suma de los primeros n números enteros positivos proporcionados desde el teclado, donde n >1. Para este programa, si el usuario introduce números negativos se deberán ignorar, y el programa deberá considerar únicamente la suma de números positivos.
    25. Escriba un programa que permita determinar por selección la suma de los primeros n números pares (n > 1). Su programa no debe generar los números pares, debe seleccionarlos.
    26. Escriba un programa que permita determinar por selección la suma de números pares contenidos entre 1 y n, donde n > 1.  Su programa no debe generar los números pares, debe seleccionarlos.
    27. Escriba un programa que permita determinar por selección la suma de números pares contenidos entre m y n, donde m > 1 y n > m.  Su programa no debe generar los números pares, debe seleccionarlos.
    28. Escriba un programa que permita identificar los números primos entre 1 y n, donde n > 1.
    29. Escriba un programa que permita determinar los números primos contenidos entre m y n, donde m > 1 y n > m.
    30. Dada una lista de números enteros positivos proporcionados desde la entrada estándar, determine el mayor y el menor de ellos. La lista termina al proporcionar cualquier número negativo o cero.
    31. ¿Qué conclusiones puede determinar de la realización de los seis ejercicios anteriores?, ¿qué tipo de estructura de repetición es más natural, desde su perspectiva, para una repetición controlada por contador y por qué? En este mismo sentido, ¿qué tipo de estructura de repetición es más natural para una repetición controlada por centinela?
    32. Reescriba el programa del Ejemplo 3.20 utilizando una estructura de repetición while y determine lo que pasa. ¿Funciona de la misma manera?, ¿se genera algún tipo de problema?, ¿se mantiene la equivalencia lógica y funcional?
    33. Reescriba el programa del Ejemplo 3.20 utilizando una estructura de repetición do-while y determine lo que pasa. ¿Funciona de la misma manera?, ¿se genera algún tipo de problema?, ¿se mantiene la equivalencia lógica y funcional?
    34. Reescriba el programa del Ejemplo 3.21 utilizando una estructura de repetición while y determine lo que sucede. ¿Funciona de la misma manera?, ¿se genera algún tipo de problema?, ¿se mantiene la equivalencia lógica y funcional?
    35. Reescriba el programa del Ejemplo 3.21 utilizando una estructura de repetición do-while y determine lo que sucede. ¿Funciona de la misma manera?, ¿se genera algún tipo de problema?, ¿se mantiene la equivalencia lógica y funcional?
    36. Escriba un programa que lea un número que representará una cantidad de dinero. El número debe estar entre 1 y 4000. Su programa deberá determinar la menor cantidad de billetes de $500, $200, $100, $50 y $20 así como de monedas de $10, $5 y $1 que debe entregar un empleado a una persona que solicita dicha cantidad.

6 de octubre de 2016

Lectura de datos.

Lectura de datos.
   El Ejemplo 2.3 muestra un sencillo programa que lee desde la entrada estándar dos números enteros decimales (los operandos), y los suma. Este ejemplo también se describirá línea a línea.

   Las líneas 1-7 deberían resultar ahora  familiares, en base a la explicación de un primer programa en C. Por otro lado, la línea 8 presenta un nuevo elemento: la declaración de variables.

   La declaración de variables en C puede hacerse al iniciar un bloque ({ ... }) y tiene la siguiente estructura general:

          {
                tipo_de_dato lista de variables;
                             .
                             .
                             .
          }

donde tipo_de_dato es alguno de los tipos de datos de C, y la lista de variables es una lista de identificadores separada por comas. En base a lo anterior, la línea 8 está declarando tres variables de tipo entero a través de la palabra reservada int.

   Una palabra reservada es una palabra (token) que no puede ser utilizada como identificador y que es propia del lenguaje, lo cual quiere decir que ninguna variable o función (ningún identificador) por ejemplo, puede llamarse “int”.

   Por otro lado, un identificador es una secuencia de caracteres sin espacios que inicia con una letra. Los identificadores sirven para dar nombre a las variables que se declaran y que serán utilizadas dentro de un programa.

   En C existen diferentes tipos de datos: int, float y char son tres de ellos. La idea de tipo de dato se refiere a una categoría de datos pertenecientes a un dominio pero, desde el punto de vista del lenguaje de programación C ¿qué es un tipo de dato?

   Lo que una computadora almacena en su memoria principal no es otra cosa que una secuencia de bits; pues bien, esa misma secuencia de bits tiene diferentes interpretaciones, y la interpretación específica está en función del tipo de dato al que se refiera.

   Considere la siguiente secuencia de bits: 1 1 0 1 0 1 0 1. Suponga que dicho byte representa, como número, al entero decimal 213. Si esta misma secuencia de bits, se interpreta ahora como carácter (char) por ejemplo, podría representar a la hermosa letra 'R' (no es que así sea, es sólo una suposición), mientras que interpretada como un número con punto decimal (float) podría representar al 69.5.

   En resumen, un tipo de dato es la manera en que una computadora interpreta un patrón de bits.

   Continuando con el ejemplo que iniciamos, la línea 10 debería resultar familiar. A este tipo de sentencias se les conoce en el argot computacional como prompt, es decir, un mensaje de solicitud de datos.

   La línea 11 es nueva y muestra uno de los usos de la función scanf. La función scanf se compone básicamente de dos partes:

  1. El (los) especificador(es) de formato indicados entre comillas.
  2. Una lista de variables separadas por comas en donde se almacenarán los valores procesados (leídos) de la entrada estándar.

   El especificador de formato%d”, le indica a la función scanf que leerá un entero decimal, mientras que la expresión “&operando1”, especifica la variable en la que se almacenará el valor leído de la entrada estándar: operando1.

   Debe observarse que el nombre de la variable ha sido antecedido por el símbolo “&”, la justificación de ello se comprenderá mejor cuando se trate el tema de Apuntadores; por ahora, basta con que no olvide anteceder con dicho símbolo el nombre de la variable en la que se desee almacenar el valor procesado, ya que su omisión no es detectada por el compilador como un error, debido a que no es una violación a la gramática del lenguaje C.

   Las líneas 12 y 13 son análogas a las líneas 10 y 11 respectivamente. Asegúrese de comprender la similitud. Por otro lado, la línea 14 presenta la expresión:

suma = operando1 + operando2;

   Dicha expresión le indica a C que realice la operación aritmética de adición sobre los valores almacenados en operando1 y operando2, y que el resultado se asigne (almacene) en la variable suma.

   La tabla siguiente muestra los operadores aritméticos en C:

Operador      Descripción
                   /             División (cociente)
               %            Módulo (residuo)
                             *             Multiplicación (producto)
             +             Adición (suma)
                  -              Sustracción (resta)

   Los operadores se evalúan siguiendo una precedencia. La precedencia de operadores le indica al compilador cuáles de los operadores en una expresión deben ser evaluados primero. Considere la siguiente expresión:

3 + 5 * 4

   La expresión anterior es evaluada como 23 y no como 32, debido precisamente a que la multiplicación tiene una precedencia mayor sobre la adición, sin importar que la adición sea la operación que aparece primero (más a la izquierda).

   Si lo que se desea es primero realizar la suma y luego el producto, la expresión debe escribirse como:

(3 + 5) * 4

   Los paréntesis modifican la forma en que son evaluadas las expresiones, es decir, modifican la precedencia de cualquier operador y éste es su uso más común, aunque en muchas ocasiones se utilizan sólo por claridad en la forma de representar una expresión sin que modifiquen la precedencia de los operadores involucrados.

   Los operadores aritméticos “/”, “%” y “*” tienen la misma precedencia y es más alta que la de los operadores “+” y “-”. Si en una expresión existen operadores con la misma precedencia, la expresión es evaluada de izquierda a derecha.

   Finalmente, la línea 15 (la línea 17 se explicó a detalle en un primer programa en C ) introduce una novedad respecto al primer ejemplo, ya que muestra el uso de especificadores de formato dentro de la función printf.

   Al igual que en la función scanf, el especificador de formato “%d” le indica a la función printf que en la posición del especificador de formato va a imprimir, en la salida estándar, un entero decimal cuyo valor está almacenado en la variable suma, la cual se encuentra después de la coma. En este sentido, por cada especificador de formato debe existir su correspondiente variable asociada, de tal forma que tanto el número de especificadores de formato como el de variables, debe corresponder.

   Una posible salida de nuestro ejemplo se muestra en la siguiente figura. Asegúrese de comprender la descripción realizada hasta el momento, así como de entender lo que sucede con la ejecución y la salida correspondiente antes de continuar.

Una posible salida del Ejemplo 2.3.

   Considere ahora el Ejemplo 2.4 y compárese con el anterior. Note que todas las líneas son iguales excepto por la línea 15. La línea 15 contiene tres especificadores de formato “%d”. Cada especificador de formato, le indica a la función printf que imprima en orden, el entero decimal correspondiente almacenado en cada una de las variables correspondientes: operando1, operando2 y suma.

   La lógica del programa de este nuevo ejemplo no cambia en nada con respecto de la lógica del ejemplo anterior, sólo se ha cambiado el estilo en la forma de presentar los resultados, la cual es más ad hoc con la intención del programa.

   El segundo ejemplo  muestra la forma de incluir más de un especificador de formato en la función printf, de tal forma que puede observarse que por cada especificador de formato existe su correspondiente variable asociada. Es importante mencionar también que es responsabilidad del programador el asegurar que exista una correspondencia entre el especificador de formato y el tipo de dato de la variable, C no realiza esta verificación.

   Los especificadores de formato para printf tienen la siguiente forma general:

%[-]m.nx

donde % delimita el inicio del especificador de formato y x representa el especificador de formato a utilizar. El guión o signo de menos alinea el campo a la izquierda; si se omite, el campo se alinea a la derecha. Ahora bien, dependiendo del valor de x, los número enteros representados por m y n se interpretan de manera diferente:
  • Usualmente, m es la longitud mínima y n es la longitud máxima del campo (ancho de campo o espacio) que se utilizará para imprimir x.
  • Si x representa el especificador de formato para un número con punto decimal, n es interpretado como la precisión que deberá ser utilizada (número de decimales después del punto).
   Una posible salida para el Ejemplo 2.4 se muestra en la siguiente figura. Pruebe ambos ejemplos con distintos valores y observe sus resultados; repita el procedimiento hasta que se sienta cómodo y entienda por completo el mecanismo de funcionamiento de los programas.

Una posible salida del Ejemplo 2.4.

4 de octubre de 2016

Archivos binarios.


   Aunque C no impone una estructura a los archivos, es posible definir una estructura específica para éstos. Sin embargo, la creación, administración y el acceso a dicha estructura, son responsabilidad del programador.

   Un archivo binario es un archivo con una estructura específica, la cual no puede ser visualizada ni interpretada de manera directa, sino únicamente como un conjunto de bytes relacionados entre sí, los cuales representan los tipos de datos que fueron almacenados en la estructura del archivo.

Archivos de acceso aleatorio.
   La creación de una estructura determinada sobre un archivo tiene, como casi todo en la vida, ventajas y desventajas.

   La principal ventaja es que es posible acceder a un elemento específico dentro del archivo sin la necesidad de procesar todos los elementos anteriores a él, como en el caso de los archivos de texto de acceso secuencial.

   Por otro lado, la principal desventaja es que, antes de poder acceder a los datos del archivo, se debe crear la estructura correspondiente y por lo tanto, es preciso definir desde la creación del archivo, el número de elementos que almacenará.

   Como analogía, puede decirse que los archivos de acceso aleatorio son a las unidades de almacenamiento (discos), lo que los arreglos son a la memoria principal; de hecho, note que tanto la ventaja como la desventaja mencionadas con anterioridad se tienen también presentes en los arreglos.

   En resumen, un archivo de acceso aleatorio es un archivo binario con una estructura específica determinada por el programador, al que se pueden acceder sus elementos de manera aleatoria, de manera análoga a como se acceden los elementos en un arreglo.

Creación de la estructura del archivo.
   El primer paso para la manipulación de archivos de acceso aleatorio es la creación de la estructura del archivo.

   La creación de la estructura consiste básicamente en definir cuáles y de qué tipo de dato serán los elementos almacenados en el archivo. Lo anterior se realiza, habitualmente, encapsulando dentro de una estructura los elementos a almacenar en el archivo; sin embargo, es posible almacenar elementos de un solo tipo de datos en el archivo sin necesidad de representarlos dentro de una estructura.

   El Ejemplo 9.5 muestra la creación de la estructura de un archivo que contendrá un directorio de contactos. Esta idea ha sido ya utilizada en la entrada referente a Archivos de Texto, aquí se retoma para mostrar ahora la representación del directorio de contactos en un archivo de acceso aleatorio.

   Note que la estructura (struct) de las líneas 9-13 ha sido ligeramente modificada respecto de las anteriores, ya que se le ha agregado el elemento miembro num (línea 10), el cual servirá como índice para localizar a una estructura específica dentro del archivo.

   La línea 16 define la variable contacto de tipo CONTACTO misma que se inicializa con el valor cero, y con las cadenas vacías para num, nombre y telefono respectivamente.

   Como elemento clave del Ejemplo 9.5, observe que el modo de apertura seleccionado para el archivo "contactos.dat" en la línea 20 es "wb", el cual especifica que se debe crear un archivo binario en modo de escritura.

   Ahora bien, el ciclo for (línea 23) escribe N veces en el archivo referido por archivoPtr, sizeof(CONTACTO) bytes almacenados en la estructura contacto a través de la función fwrite (línea 24). El número 1 (tercer argumento) de la función fwrite le indica a la función cuántos elementos del tamaño especificado como segundo argumento (el tamaño se especifica en número de bytes) obtendrá de la dirección especificada como primer argumento, para escribirlos en el flujo proporcionado como cuarto argumento. Éste es el uso más común de la función fwrite.

   Por otro lado, si el tercer argumento n proporcionado a la función fwrite es mayor que uno, entonces el primer argumento debe corresponder al nombre de un arreglo del cual se obtendrán los n elementos. El número t de bytes de cada uno de los elementos del arreglo se proporciona como segundo argumento, y el flujo donde se desea que se escriban los t bytes se proporciona como cuarto argumento.

   Resumiendo: la ejecución del programa del Ejemplo 9.5 crea el archivo contactos.dat, cuya estructura está conformada por N elementos (línea 5) de tipo CONTACTO, es decir, genera una especie de arreglo de N elementos de tipo CONTACTO en el archivo contactos.dat.

Acceso aleatorio a la estructura del archivo.
   El programa del Ejemplo 9.6 muestra los pormenores respecto al manejo de archivos binarios de acceso aleatorio. Tome en cuenta que este ejemplo se basa en la estructura de archivo creada en el Ejemplo 9.5 descrito en la sección anterior.

   Las funciones menu, leeContacto e imprimeContacto se dejan como material de análisis y comprensión para el lector. Todos los detalles de dichas funciones deberían ser claramente comprendidos; asegúrese de que así sea antes de continuar.

   Respecto a la función main sólo se harán dos observaciones, todo lo demás debe resultar familiar:

  1. La línea 25 muestra el modo de apertura rb+, lo cual le indica a la función fopen que abra el archivo binario contactos.dat para actualización.
  2. A diferencia de todos los ejemplos anteriores, el archivo contactos.dat es abierto al iniciar el programa, y permanece así hasta su terminación (línea 40). Observe cómo la variable archivoPtr es enviada como argumento a las funciones que trabajan con el archivo (líneas 28 y 31).
   Teniendo en cuenta lo anterior, la atención del Ejemplo 9.6 se centrará entonces en dos funciones:
  1. leeDirectorio: es la encargada de leer los datos del archivo por medio de la función fread (línea 74). Mientras fread pueda leer datos del archivo, regresará el número total de elementos exitosamente procesados, de tal forma que cuando se llegue al fin de archivo y la función no pueda leer más datos, regresará cero. La función fread trabaja de manera semejante pero en el sentido contrario a la función fwrite explicada en el Ejemplo 9.5. Si el elemento miembro num de algún contacto es distinto de cero (línea 75), se imprimen sus datos y se contabiliza. Aquí el valor cero indica que es un contacto en blanco, dado que así se inicializó la estructura del archivo (Ejemplo 9.5), cualquier otro caso hace referencia a un número de contacto con datos ya almacenados previamente en el archivo.
  2. agregaContacto: Esta función se desglosará en varias partes por orden de secuencia:
    1. Solicita y valida un número de contacto (líneas 49 - 52). Recuerde que en el Ejemplo 9.5 se generó una estructura en el archivo para almacenar 100 contactos.
    2. La función getchar (línea 53) absorbe el '\n' introducido en la entrada estándar después del número leído en la línea 51; el número proporcionado se almacena en num, pero si el '\n' no es procesado, será el elemento del que disponga la siguiente lectura de datos y si ésta es de una cadena dará la impresión de leer una cadena vacía.
    3. La función fseek (líneas 55 y 63) establece el indicador de posición del archivo archivoPtr en una determinada posición, la cual es proporcionada como segundo argumento en la forma de un desplazamiento en bytes. El tercer argumento le indica a la función la posición utilizada como referencia para el desplazamiento:
      1. SEEK_SET: del inicio del archivo.
      2. SEEK_CUR: de la posición actual del indicador de posición del archivo. Note que la expresión de desplazamiento siguiente, realiza el cálculo del elemento específico dentro del archivo, lo cual, respecto a la analogía planteada con anterioridad, es equivalente al índice de un arreglo: 
      (num - 1) * sizeof(CONTACTO)
    4. La combinación de las funciones fseek y fread (líneas 55 y 56) y de fseek y fwrite (líneas 63 y 64) realizan la parte medular respecto a la inserción de un nuevo contacto en el archivo.
    5. Note la necesidad de volver a calcular la posición del elemento a insertar en el archivo (línea 63), debido a que la lectura del contacto (línea 56) deja el indicador de posición del archivo, en el elemento siguiente al de interés.
   La salida del Ejemplo 9.6 puede ser bastante extensa; la invitación es hacia compilar el programa y probarlo, así como a entender la relación del funcionamiento de cada una de la sentencias que componen el programa hasta obtener una total comprensión de él.

Archivos con cualquier contenido.
   El Ejemplo 9.7 abre un archivo binario independientemente de su contenido, formato o estructura, lee su contenido y presenta, en la salida estándar, los bytes que lo conforman. La mayoría de los elementos del programa han sido ya analizados o mencionados con anterioridad, por lo que su comprensión debería ser sencilla; sin embargo, se enfatizarán algunos aspectos relevantes:
  1. El programa procesa datos de la línea de comandos, es decir, recibe el archivo a procesar como argumento en la invocación del programa (líneas 8 y 11 - 16).
  2. Note que la función rewind (línea 24) establece el indicador de posición nuevamente al inicio del archivo referido por archivoPtr, debido a que en la línea 21 se lee el archivo byte por byte para poder determinar su tamaño a través de la función ftell (línea 23). La función ftell regresa el indicador de posición actual del flujo asociado al archivo archivoPtr. Si el archivo al que se hace referencia es binario, dicho indicador corresponde al número de bytes respecto del inicio del archivo.
  3. Observe cómo la función fread lee el archivo completo en la línea 33; y que el número de bytes leídos es almacenado en tamanio2 para ser posteriormente comparado (línea 34) con el número de bytes que se le especificó a la función fread que leyera.
  4. Finalmente, note en la línea 42 el uso del especificador de formato de salida "%x", el cual le indica a la función printf que imprima el dato almacenado en buffer[i] en formato hexadecimal (minúsculas); mientras que el especificador de formato de salida "%u" (línea 43) se utiliza para imprimir un entero sin signo.
   Pruebe el programa del Ejemplo 9.7 con distintos archivos binarios; puede probarlo con archivos ejecutables, de música, de video, de imágenes, e incluso de texto.

   Por último, tome en cuenta que el Ejemplo 9.7 fue diseñado para contener en memoria todos los bytes del archivo procesado, por lo que si el archivo es muy grande, la solicitud de una gran cantidad de memoria podría ser rechazada. Se deja como ejercicio para el lector la oportunidad de corregir dicha deficiencia leyendo el archivo por bloques.

2 de octubre de 2016

Dos aplicaciones de archivos de texto.

   Esta entrada presenta dos sencillas pero prácticas aplicaciones de los archivos de texto; recuerdo al lector que las posibilidades están limitadas únicamente a la imaginación del programador.

Implementación de cat.
   El comando cat (por concatenar) es un programa de Unix y GNU/Linux utilizado para concatenar y mostrar archivos en la salida estándar.

   El programa del Ejemplo 9.3 realiza una implementación básica del comando cat respecto a la presentación de archivos en la salida estándar, no en cuanto a la concatenación.

   La mayor parte de los elementos y sentencias utilizadas en el Ejemplo 9.3 ya han sido estudiados y presentados con anterioridad, por lo que debería comprenderse sin ningún problema. En este sentido, sólo se enfatizarán los siguientes aspectos:
  1. En caso de que no se proporcionen o se exceda el número de argumentos requeridos para su ejecución (líneas 11 y 12), se presenta una sentencia de uso del comando al estilo de los comandos de Unix y GNU/Linux.
  2. Se utiliza directamente la cadena almacenada en argv[1] para acceder al archivo indicado en la línea de comandos (línea 13).
  3. Se hace uso de la función fputs (línea 15) la cual imprime lo que se le envía como primer argumento, en el flujo especificado como su segundo argumento es decir: imprime en la salida estándar (stdout) la cadena "cadena", misma que fue leída por la función fgets en la línea 14.
   Pruebe el funcionamiento del programa del Ejemplo 9.3 y asegúrese de proporcionarle archivos de texto como argumento; puede probar con el código fuente del mismo programa por ejemplo.

Cambia caracteres.
   Suponga que se tiene la necesidad de cambiar cada ocurrencia (aparición) de un carácter dentro de un archivo por otro carácter distinto. Un escenario de solución para esto es realizar manualmente el cambio, otro posible escenario, es escribir un programa en C que lleve a cabo dicha tarea por nosotros; el Ejemplo 9.4 muestra un programa que satisface dicha necesidad.

   Al igual que antes, las sentencias del Ejemplo 9.4 deben resultar familiares al lector, por lo que sólo se resaltarán los siguientes puntos:
  1. La función main procesa los argumentos introducidos en la línea de comandos, y los utiliza para enviárselos a la función que realizará el cambio sobre el archivo: cambiaOcurrenciaArchivo.
  2. La función cambiaOcurrenciaArchivo recibe cuatro parámetros:
    1. El carácter a reemplazar v.
    2. El carácter n que substituirá a v.
    3. La cadena a1 que contiene la ruta y el nombre del archivo sobre el que se buscará a v.
    4. La cadena a2 que contiene la ruta y el nombre del archivo sobre el que se realizará el cambio de v por n.
  3. La constate simbólica TAM: en este tipo de uso, su ámbito se define de la línea 31 a la 49.
  4. La función cambiaOcurrenciaCadena, que se encarga de substituir el carácter v por el carácter n en la cadena cad utilizando notación de apuntadores.
  5. El uso de la función cambiaOcurrenciaCadena de la línea 44 para cambiar la cadena "cadena" leída del archivo archivoPtr1 (línea 43), en la cadena cadena que se almacenará en el archivo archivoPtr2 (línea 45) por medio de la función fputs (la cual trabaja de manera análoga a la función puts pero recibe como segundo parámetro, un apuntador al flujo sobre el que se desea escribir la cadena referida por su primer parámetro).

     Pruebe con distintos archivos de texto el programa del Ejemplo 9.4. La salida en pantalla es sólo informativa respecto a la terminación del programa, lo interesante ocurre en los archivos procesados, los cuales son proporcionados en la invocación del programa.