30 de septiembre de 2016

Abstracción de matrices.

   En la entrada correspondiente a Asignación Dinámica de Memoria se presentó, en el Ejemplo 7.7, una biblioteca de funciones para la creación y manipulación de matrices utilizando asignación dinámica de memoria. En esta entrada se retomarán las ideas presentadas en las secciones Asignación Dinámica de Memoria y Abstracción en Acción para subir el nivel de abstracción en la representación de matrices, y con ello poder crear y manipular matrices de números racionales.

   El Ejemplo 8.5 es esencialmente igual al Ejemplo 7.7; tómese el tiempo de compararlos línea a línea y función por función ya aquí sólo se remarcarán los aspectos más sobresalientes, dado que la creación de matrices no es el tema de la entrada.

   La clave de la abstracción ocurre en las líneas 7-11 del Ejemplo 8.5 con la definición de la estructura RACIONAL (líneas 7-9), y la re definición del tipo doble apuntador a RACIONAL por MATRIZ_RACIONAL (línea 11).

   La re definición del tipo doble apuntador a RACIONAL por el nuevo tipo de dato MATRIZ_RACIONAL hace que ahora las funciones, en lugar de recibir un doble apuntador a int (como en el Ejemplo 7.7), reciban una variable de tipo MATRIZ_RACIONAL lo cual hace, desde su lectura e interpretación, un enfoque mucho más conveniente para la programación reforzando, no sólo la naturaleza de los tipos de datos involucrados, sino también la auto documentación del código fuente.

   Observe cómo todos los tipos de datos han sido ajustados en correspondencia al nuevo enfoque de abstracción, tal y como se muestra en las líneas 13, 21, 22, 25, 28, 38, 51, 72 y 73.

   Por otro lado, los datos almacenados en la matriz son ahora del tipo RACIONAL, por lo que cada uno de ellos tiene dos elementos miembro: p y q, los cuales son considerados tanto en la lectura (línea 45), como en la salida de datos (línea 58).

   Por último, la función suma de las líneas 72-81 se apoya (línea 79) de la función sumaRacional de las líneas 63-70 para realizar la suma de dos números racionales, ya que la suma de racionales no es la suma tradicional de elemento a elemento, sino la suma representada por la expresión:

r1 + r2 = (p1 / q1) + (p2 / q2) = [ (p1 * q2) + (p2 * q1) ] / (q1 * q2)

donde r1 y r2 son dos números racionales (Q) definidos como:

r1 = p1 / q1 con p1, q1 números enteros (Z) y  q1 != 0
r2 = p2 / q2 con p2, q2 números enteros (Z) y  q2 != 0

   El Ejemplo 8.6 es análogo al Ejemplo 7.8; tómese también el tiempo para compararlos y comprender sus diferencias. Note cómo la abstracción definida en la biblioteca de funciones ejemplo8_5.h es utilizada en la línea 9 del Ejemplo 8.6, mostrando con ello que el uso apropiado de bibliotecas de funciones también refuerza la abstracción a través de la modularidad o programación modular.

   Finalmente, una posible salida para el Ejemplo 8.6 se muestra en la siguiente figura:

Una posible salida para el Ejemplo 8.6.


Arreglos de estructuras.

   Las estructuras, al igual que los tipos de datos básicos de C, también pueden ser agrupadas en arreglos y el Ejemplo 8.4 es muestra de ello, el cual modela, por medio de un arreglo (línea 16) y una estructura (líneas 10 - 13), un directorio de contactos con los datos elementales.

   El ciclo for de las líneas 19-25 recorre el arreglo contactos para su lectura. Note que cada elemento del arreglo contactos es una entidad de tipo CONTACTO, y que por lo tanto, se leen de la entrada estándar sus dos elementos miembro: nombre (línea 21), y telefono (línea 23), a través de la función gets.

   Por otro lado, el ciclo for de las líneas 27-31 realiza un recorrido del arreglo contactos para su impresión en la salida estándar de manera análoga a lo descrito en el párrafo anterior. La siguiente figura muestra una posible salida del Ejemplo 8.4:

Una posible salida del Ejemplo 8.4.
 
    El nivel de detalle para el directorio de contactos puede ser tan elaborado como se requiera, el Ejemplo 8.4 es sólo una muestra del uso de un arreglo de estructuras, mismo que puede ser generalizado y extendido hacia otros tipos de datos.

   Los arreglos de estructuras siguen las misma reglas que se analizaron y presentaron para los arreglos en las entradas correspondientes del blog.

   Finalmente, vale la pena mencionar que en la entrada referente a la gestión de archivos se retomará el Ejemplo 8.4, y se relacionará su uso con el almacenamiento y recuperación de los datos procesados en, y de un archivo de texto, respectivamente.

29 de septiembre de 2016

Estructuras compuestas.

   En muchos casos, el modelado de un problema requiere de elementos compuestos:
  • El nombre completo de una persona por ejemplo, está compuesto por:
    • Nombre(s).
    • Primer apellido.
    • Segundo apellido.
   El nombre completo de una persona podría ser un elemento miembro de otra estructura, y si el modelado del problema requiere de dicho nivel de especificación, convendría considerarlo como una entidad aparte.
  • Un automóvil está compuesto de varias partes, que a su vez están compuestas por otras tantas:
    • Motor (combustión interna de gasolina):
      • Sistema de enfriamiento.
      • Sistema eléctrico.
      • Bujías.
      • Cilindros.
      • Pistones.
      • Válvulas.
      • Levas.
      • Inyectores.
      • etcétera (bastante amplio por cierto).
    • Puertas:
      • Sistema de apertura y cierre.
      • Sistema eléctrico.
      • Cristales.
      • Seguros.
      • Barras de protección.
      • etcétera.
    • etcétera.
  • Los elementos geométricos son un excelente ejemplo de composición:
    • Un vértice en el espacio bidimensional está compuesto por un par ordenado de coordenadas o puntos (x, y).
    • Un triángulo en el espacio bidimensional está definido por un conjunto de tres vértices.
    • Un cuadrado está compuesto por cuatro vértices equidistantes entre sí.
   En el lenguaje de programación C no es posible definir una estructura dentro de otra estructura, lo cual quiere decir que no se pueden definir estructuras anidas; sin embargo, sí es posible definir estructuras cuyos elementos miembro sean otras estructuras, es decir, elementos miembro cuyo tipo de dato es algún otro tipo de estructura distinta de ella misma, siempre y cuando esta última haya sido definida antes que la primera. Cabe mencionar que existe una excepción a esta regla: se puede definir una variable que haga referencia a una estructura del mismo tipo, es decir, una variable de tipo apuntador a la estructura que la define, lo cual es útil para la construcción de estructuras de datos como listas, pilas, colas, árboles, etcétera, pero estos conceptos quedan fuera del alcance de este blog y no se tratarán aquí.

   El Ejemplo 8.3 muestra la definición y el uso de estructuras compuestas para un par de elementos geométricos definidos en el espacio bidimensional: el vértice y el triángulo.

   La definición de las estructuras VERTICE y TRIANGULO ocurren en las líneas 7-10 y 12-16 respectivamente. Note cómo los elementos miembro de la estructura TRIANGULO son del tipo VERTICE, por lo que la estructura VERTICE ha tenido que definirse antes que la estructura TRIANGULO.

   La función principal main declara en la línea 23, una variable t de tipo TRIANGULO, lee dicho triángulo (línea 25), y presenta el triángulo que acaba de ser leído en la salida estándar (línea 26).

   Por otro lado, la función leeVertice (líneas 40 - 43) recibe una referencia a una variable de tipo VERTICE, la cual es necesaria debido a que la función es para la lectura de datos. Observe una vez más el uso del operador flecha (línea 42) para la lectura de datos de los elementos miembro referidos por v; y que la cadena de especificación de formato de entrada ("%d, %d") contiene una coma (,), la cual no tiene otro fin más allá del estético en base al formato de entrada sugerido, tal y como se muestra en la siguiente figura:

Una posible salida para el Ejemplo 8.3.
   La función leeTriangulo (líneas 31 - 38) por otra parte, se basa o apoya en la función leeVertice descrita con anterioridad para leer los tres vértices que conforman un triángulo. Note que la función también recibe una referencia almacenada en t, y que dicha referencia es utilizada en las líneas 33, 35 y 37 para pasar a su vez la referencia respectiva al vértice correspondiente a la función leeVertice. Asegúrese de comprender ésto antes de continuar.

   Por último, la función imprimeTriangulo (líneas 45 - 49) imprime en la salida estándar los vértices del triángulo utilizando el operador punto para acceder, de izquierda a derecha, a los elementos miembro de la estructura compuesta:
  • Línea 46: para el triángulo t, de su vértice v1, el valor correspondiente a x (t.v1.x); y para el triángulo t, de su vértice v1, el valor correspondiente a y (t.v1.y).
  • Línea 47: para el triángulo t, de su vértice v2, el valor correspondiente a x (t.v2.x); y para el triángulo t, de su vértice v2, el valor correspondiente a y (t.v2.y).
  • Línea 48: para el triángulo t, de su vértice v3, el valor correspondiente a x (t.v3.x); y para el triángulo t, de su vértice v3, el valor correspondiente a y (t.v3.y).

28 de septiembre de 2016

Abstracción en acción.

   El segundo mecanismo de abstracción aunque simple, resulta bastante útil tanto para la abstracción como para la auto documentación de programas: typedef.

   La sentencia typedef de C se utiliza para renombrar tipos de datos existentes, y en armonía con struct, constituyen los aliados perfectos para elevar el nivel de abstracción en los programas.

Números racionales.
   Un número racional (p / q) se define como el cociente de dos números enteros, donde p, q son elementos del conjunto de los números enteros (Z).

   El lenguaje de programación C no tiene un tipo de datos racional que permita manipular variables de la forma p / q; sin embargo, es posible abstraer su representación con los tipos de datos existentes

   Observe las líneas 6-9 del Ejemplo 8.1 en las cuales se ha definido una estructura de nombre r con dos elementos miembro de tipo int p y q. Note que lo que se ha hecho es representar la definición de un número racional utilizando los elementos del lenguaje C.

   La plantilla o molde que se ha definido para generar variables que contengan dos números enteros es struct r. Observe que las variables p (línea 7) y q (línea 8) se han colocado por separado aunque su tipo de dato (int) es el mismo; lo cual es más una cuestión de estilo que una característica inherente a la definición de la estructura.

   Por otro lado, note que en la línea 14 se declara la variable r1 de tipo struct r. Es hasta ese momento cuando se genera el espacio en memoria necesario para almacenar y gestionar una variable u objeto r1 de tipo struct r.

   Es ahora cuando la utilidad de typedef toma relevancia. La línea 11 le indica al compilador que de ahí en adelante la expresión RACIONAL será exactamente equivalente a la expresión struct r, y por esta razón, la variable r2 (línea 15) y res (línea 16) son idénticas en estructura, forma, y tipo a la variable r1 de la línea 14. El typedef renombra la estructura con un nuevo identificador y aunque no es una restricción del lenguaje, se recomienda como estándar de programación, que los identificadores que representan "nuevos" tipos de datos se escriban con letras mayúsculas.

   Las variables del tipo struct r o RACIONAL lucen como se muestra en la siguiente figura:

Representación de la estructura (forma) de las variables de tipo struct r o RACIONAL del Ejemplo 8.1.
 
    Ahora bien, el acceso a los elementos miembro de una estructura se realiza a través del operador punto (.). El acceso a los elementos miembro de una estructura no sigue ningún orden específico ni está relacionado con el orden en que aparecen dichos elementos en su declaración; el acceso a los elementos miembro está únicamente condicionado a las necesidades específicas del problema en cuestión, y la sintaxis general es:

estructura.elemento_miembro;

en donde:
  • estructura es el identificador de una variable de tipo estructura (struct).
  • elemento_miembro es alguno de los elementos miembro de la estructura estructura.
   La línea 21 muestra la forma de acceder a los elementos miembro de una estructura. Note que el acceso a los elementos miembro es para leer los números racionales r1 y r2 desde la entrada estándar, que para cada uno se lee su numerador (p) y denominador (q) respectivamente, y que el acceso a los elementos miembro va precedido por el operador &.

   La cadena con los especificadores de formato de la función scanf de la línea 21 ("%d %*c %d %c %d %*c %d"), requiere de una explicación para el especificador %*c, el cual, le indica a la función scanf que ignore cualquier símbolo introducido en esa posición. Note que de hecho no es almacenado ya que resulta útil y conveniente en la representación de los números racionales, pero no para el almacenamiento, tal y como lo muestra las siguientes figuras donde se ilustra la forma de introducir números racionales para una posible ejecución del Ejemplo 8.1.

Una posible salida para los Ejemplos 8.1 y 8.2 con un operador no válido.
Una posible salida para los Ejemplos 8.1 y 8.2 con un operador válido.
 
    El ciclo do-while de las líneas 19-24 realiza una validación básica: el denominador no puede ser cero para ningún número racional.

   La sentencia switch de las líneas 26-42 determina la operación a realizar. Note que las operaciones de suma y resta no han sido especificadas y se dejan como ejercicio para el lector.

   Finalmente, observe que las líneas 31-34 representan la siguiente expresión:

r1 * r2 = (p1 / q1) * (p2 / q2) = (p1 * p2) / (q1 * q2)

donde r1 y r2 son dos números racionales (Q) definidos como:

r1 = p1 / q1 con p1, q1 números enteros (Z) y  q1 != 0
r2 = p2 / q2 con p2, q2 números enteros (Z) y  q2 != 0

y que las líneas 35-38 representan la expresión:

r1 / r2 = (p1 / q1) / (p2 / q2) = (p1 * q2) / (q1 * p2)

mientras que las líneas 44-45 presentan el resultado de las operaciones en un formato de salida intuitivo y conveniente para el usuario.

   Las restantes operaciones aritméticas con números racionales se definen a continuación:
  • Suma: r1 + r2 = (p1 / q1) + (p2 / q2) = [ (p1 * q2) + (p2 * q1) ] / (q1 * q2)
  • Resta: r1 - r2 = (p1 / q1) - (p2 / q2) = [ (p1 * q2) - (p2 * q1) ] / (q1 * q2)

Evolución.
El Ejemplo 8.2 es una evolución del Ejemplo 8.1; son esencialmente iguales por lo que no se ahondará más de lo necesario en su descripción y sólo se destacarán las siguientes diferencias:
  1. La estructura que modela o representa a los números racionales (líneas 9-11), ha sido simplificada a un renglón y fusionada con el typedef. Note que también se ha omitido el identificador del struct debido a que la estructura es renombrada desde su definición.
  2. Se incluye la función leeExpresion (líneas 13 y 55-66), la cual recibe dos argumentos de tipo apuntador a RACIONAL. Note que la función encapsula las sentencias para la lectura de datos (líneas 17-24) del Ejemplo 8.1. La función muestra el uso del operador flecha (->), el cual sirve para acceder a los elementos miembro de una estructura cuando son referidos por medio de una variable de tipo apuntador. Compare cuidadosamente la línea 21 del Ejemplo 8.1 con la línea 60 del Ejemplo 8.2.
  3. La función hazOperacion (líneas 14 y 31-53), la cual recibe dos argumentos de tipo RACIONAL y uno de tipo char. La función regresa un valor de tipo RACIONAL mismo que representa el resultado de la operación que se realiza. Observe que la función encapsula las sentencias que determinan y llevan a cabo la operación aritmética (líneas 26-42) del Ejemplo 8.1. Note que la línea 49 del Ejemplo 8.2 hace uso de la función exit, la cual hace que el programa termine como si llegara a su fin y es equivalente al return 0 de la línea 41 del Ejemplo 8.1. La función exit se encuentra definida en la biblioteca estándar stdlib.h (línea 7 del Ejemplo 8.2).
   Compruebe que la salida del Ejemplo 8.2 es idéntica a la mostrada en las figuras anteriores si se procesan los mismos datos.

   Finalmente, las siguientes figuras muestran lo que sucede desde el punto de vista del paso de parámetros para las funciones involucradas, ya que la función leeExpresion usa parámetros por referencia (apuntadores):

Paso de parámetros por referencia para la función leeExpresion().

mientras que la función hazOperacion utiliza parámetros por valor (copia de datos):

Paso de parámetros por valor para la función hazOperacion().

23 de septiembre de 2016

Argumentos en la invocación de programas.

   Muchos programas y comandos en línea reciben y procesan argumentos que modifican o alteran su funcionamiento. El compilador GNU de C (gcc) por ejemplo, cuando se ejecuta en la línea de comandos, recibe argumentos como el nombre del programa a compilar, un nombre de archivo si se va a generar un archivo de salida ejecutable específico, etc.

   La función principal de cada programa escrito en C (main) es capaz de recibir argumentos que pueden ser manipulados y procesados. El Ejemplo 7.9 muestra un sencillo programa de prueba de estas capacidades.

   Observe primero la línea 7, la cual presenta una la lista de dos argumentos para main:
  1. argc (argument counter) es el número de argumentos proporcionados en la invocación del programa, incluyendo el nombre del programa.
  2. argv (arguments vector) es un arreglo de apuntadores a char de longitud argc. Cada elemento del arreglo, hará referencia a una cadena correspondiente al argumento en cuestión.
   El ciclo for de la línea 10 se repite argc veces e imprime en la salida estándar (línea 11) los argc argumentos recibidos y referidos por cada uno de los apuntadores de argv.

   Suponga que el nombre del archivo del Ejemplo 7.9 es mainArgs.c y que se compila de la siguiente manera:

$gcc -Wall -o cambiaBase mainArgs.c

   El ejecutable de mainArgs.c se genera entonces en el archivo cambiaBase, por lo que si ahora se ejecuta el programa de la forma siguiente:

$./cambiaBase 101011101 2 9

   Se obtendrá la salida de la siguiente figura (a) y su correspondiente representación mostrada en (b).

(a) Una posible salida del Ejemplo 7.9.
(b) Representación de argv para la ejecución mostrada en la figura (a).
   Observe que el valor de argc es de cuatro, y que cada argumento es referido por los cuatro apuntadores correspondientes de argv para el caso específico de la ejecución mostrada en la figura anterior (a).

   Finalmente, note también que el primer argumento es el nombre del programa. Esta característica resulta sumamente conveniente y dota de mucha versatilidad a los programas.

22 de septiembre de 2016

Apuntadores y arreglos.

   Los apuntadores son variables especiales pero variables finalmente, ¿será posible entonces crear un arreglo de apuntadores? La respuesta es sí, de hecho, los arreglos de apuntadores son de lo más común en los programas en C.

   El Ejemplo 7.5 muestra en la línea 9 un arreglo de apuntadores a char de nombre codigoMorse. Observe que dicho arreglo es inicializado en tiempo de compilación y que cada elemento del arreglo hace referencia a una cadena o arreglo de caracteres.

Representación del arreglo de apuntadores codigoMorse del Ejemplo 7.5.
 
    La figura anterior muestra la representación del arreglo de apuntadores definido en la línea 9 del Ejemplo 7.5. Note cómo cada elemento de codigoMorse hace referencia a cadenas de diferente longitud correspondientes al código Morse de cada una de las letras del alfabeto inglés (se ha hecho así para no tener que lidiar con aspectos irrelevantes a los arreglos de apuntadores, como el código de la letra “ñ" por ejemplo). Observe también que en las primeras dos representaciones de la figura anterior (correspondientes a los índices 0 y 1 respectivamente del arreglo de apuntadores codigoMorse), se ha hecho explícito el almacenamiento de arreglo de caracteres para los apuntadores correspondientes, mientras que para las siguientes la representación de cadenas se ha puesto entre comillas dobles.

   El ciclo for de la línea 15 resulta peculiar pero es un ejemplo clave de la versatilidad del lenguaje de programación C. Note cómo la variable de control letra es de tipo char y cómo letra es inicializada con el símbolo 'a' en la expresión de inicialización del ciclo; letra se utilizará para recorrer el alfabeto (letra++) mientras no se llegue al símbolo 'z' (letra <= 'z') como lo indica la expresión condicional.

   Lo último que resta por explicar es la expresión de la línea 16:

codigoMorse[letra - 'a']

aquí letra es inicialmente 'a', así que:
'a' - 'a' =  0
'b' - 'a' =  1
    .
    .
    .
'z' - 'a' = 25

   Si comprendió la explicación quizá esté sorprendido y la respuesta a su posible interrogante es sí: C puede restar letras. En realidad está restando los códigos de los símbolos respectivos, pero es mejor dejarle los detalles de los códigos subyacentes al compilador que tener que lidiar con los códigos de cada símbolo (lo cual además de ser un mejor enfoque de abstracción que utilizar números mágicos, hace que el código sea más portable al no depender de códigos de tipo ASCII (American Standard Code for Information Interchange), UTF (Unicode Transformation Format), etc.).

   La salida del Ejemplo 7.5 se muestra en la siguiente figura :

Salida del Ejemplo 7.5.

21 de septiembre de 2016

Aritmética de apuntadores.

   Las variables de tipo apuntador son variables que almacenan direcciones en memoria de otras variables pero finalmente son variables, por lo que es posible realizar algunas operaciones aritméticas sobre los valores que almacenan.

   Las operaciones válidas de apuntadores son [Kernighan]:
  1. Asignación de apuntadores del mismo tipo.
  2. Adición de un apuntador y un entero.
  3. Sustracción de un apuntador y un entero.
  4. Resta de dos apuntadores a miembros del mismo arreglo.
  5. Comparación de dos apuntadores a miembros del mismo arreglo.
  6. Asignación del valor cero.
  7. Comparación con cero.
   Cualquier otra aritmética de apuntadores es ilegal; por lo tanto, no es legal sumar dos apuntadores, multiplicarlos o dividirlos, sumarles un valor float, o realizar asignaciones entre diferentes tipos de apuntadores sin una conversión de tipo forzada (cast).

   Cuando se suma o resta un entero a un apuntador, dicho entero representa una escala que está en función del tamaño en bytes del tipo de datos a los que apunta el apuntador.

   Para visualizar mejor lo anterior, suponga que ptr es un apuntador a int que hace referencia al primer elemento de un arreglo de int, y que n es también un int, con 0 < n < TAM (donde TAM representa el tamaño del arreglo), entonces la expresión:

ptr += n;
ó
ptr = ptr + n;

indica al compilador que ptr se mueva o avance al n-ésimo elemento respecto de su posición actual.

   En resumen, no se está realizando una suma convencional de una dirección de memoria con un entero, sino una suma escalada en función del tipo de datos. Así, si cada int se almacena en cuatro bytes por ejemplo, a la dirección que almacena el apuntador se le asigna ptr + (4 * n), y en general:

ptr = ptr + (sizeof(tipo_de_dato) * n)

donde tipo_de_dato es el tipo de dato del elemento al que hace referencia el apuntador y sizeof es un operador de C que determina el número de bytes que utiliza tipo_de_dato.

   En la entrada siguiente referente a "Apuntadores y Arreglos" se hará uso de éste tipo de notación; por ahora, el Ejemplo 7.3 muestra el uso del modificador const con apuntadores y el uso de aritmética de apuntadores para realizar recorridos de cadenas.

   La línea 8 establece que se definirá una función de nombre imprimeSinEspacios que no regresará nada y que recibirá un apuntador constante a char, lo cual significa que dentro de la función, no se podrá modificar, a través del apuntador cPtr (línea 22), el elemento al que se haga referencia a través de él. La línea 9 se describe de manera análoga a la línea 8, pero para la función imprimeAlReves.

   Por otro lado, la función main se encarga de definir un palíndromo y almacenarlo en una cadena (línea 12), imprimir dicha cadena (líneas 14 y 17), y enviársela a las funciones imprimeAlReves e imprimeSinEspacios (líneas 15 y 16 respectivamente).

   La función imprimeSinEspacios (línea 22), realiza un recorrido de la cadena a través del apuntador cPtr y el ciclo while. Observe cómo el apuntador se va moviendo por los distintos elementos de la cadena (línea 26) haciendo referencia a cada uno de ellos uno a la vez (líneas 24 y 25). La función hace lo que su nombre indica: imprime sin espacios (línea 24) la cadena a la que hace referencia cPtr sin modificar la cadena.

   Finalmente, la función imprimeAlReves (línea 31) realiza un recorrido de la cadena (línea 35) para llegar al final de ella a través del apuntador cPtr y el ciclo while. Una vez que se llega al final de la cadena, se utiliza otro ciclo while (línea 36) para regresar, a través de cPtr, a la posición de inicio que se respaldó en inicioPtr (línea 32). La función hace lo que su nombre indica: imprime al revés la cadena a la que hace referencia cPtr sin modificar la cadena.

   El Ejemplo 7.3 hace uso de la aritmética de apuntadores más usual. Note que se está incrementando (líneas 26 y 35) y decrementando (línea 36) el apuntador cPtr, las cuales son operaciones clave de recorridos de arreglos con apuntadores. La salida de dicho ejemplo se muestra en la siguiente figura:

Salida del Ejemplo 7.3.
   La biblioteca estándar de funciones ctype.h incluye un conjunto de funciones para clasificar y transformar caracteres individuales. Se recomienda la revisión de esta biblioteca en algún manual de referencia o guía del lenguaje C, aquí sólo se presenta un muy breve resumen de algunas de las funciones que incorpora:
  • isalnum: regresa 1 si su argumento es alfanumérico y 0 si no lo es.
  • isalpha: regresa 1 si su argumento pertenece al alfabeto (inglés) y 0 si no pertenece.
  • isdigit: regresa 1 si su argumento es un dígito decimal y 0 si no lo es.
  • isxdigit: regresa 1 si su argumento es un dígito hexadecimal y 0 si no lo es.
  • islower: regresa 1 si su argumento es una letra minúscula y 0 si no lo es.
  • isupper: regresa 1 si su argumento es una letra mayúscula y 0 si no lo es.
  • ispunct: regresa 1 si su argumento es un carácter de puntuación y 0 si no lo es.
  • tolower: regresa su argumento convertido a una letra minúscula.
  • toupper: regresa su argumento convertido a una letra mayúscula.
   El Ejemplo 7.4 muestra el uso de las funciones islower y toupper, el resto de las funciones trabajan de manera análoga.

   Respecto al Ejemplo 7.4, note que el símbolo de fin de cadena se ha colocado ahora como una constante simbólica (línea 8), misma que es utilizada por el ciclo for de la línea 24 como parte de su expresión condicional. En este sentido, note también que ahora el recorrido de la cadena referida por cPtr se realiza con un ciclo for en lugar de un ciclo while, y que como el recorrido se realiza a través del apuntador cPtr, no es necesaria la expresión de inicialización de variables de control para el ciclo for por lo que ésta aparece vacía.

   La función convierteAMayusculas (líneas 23-27) verifica si el carácter al que hace referencia cPtr es una letra minúscula (línea 25) y si lo es, lo convierte a mayúscula y lo almacena en la misma posición en donde estaba dicho carácter (línea 26).

   Una posible salida del Ejemplo 7.4 se muestra en la siguiente figura. Note cómo solamente son afectados los caracteres en minúscula, todos los demás caracteres, incluyendo los espacios, son ignorados, es decir, se dejan sin modificación en la cadena que los contiene.

Una posible salida del Ejemplo 7.4.

19 de septiembre de 2016

Apuntadores.

   Los apuntadores son uno de los mitos del lenguaje de programación C (otro es la recursividad aunque ésta no está asociada con ningún lenguaje sino con la programación).

   Esta entrada introduce a los conceptos y manipulación de apuntadores; la principal intención es que los apuntadores dejen de ser un mito y pasen a ser parte del repertorio de herramientas fundamentales del programador, ya que los apuntadores son, en más de un sentido, la piedra angular del lenguaje de programación C.

Definición, estructura y representación.
   La sencillez de los apuntadores se basa en su definición la cual es bastante fácil de recordar y es, en esencia, casi todo lo que se tiene que saber respecto a los apuntadores y de ahí la importancia de su comprensión.

   Un apuntador almacena direcciones de memoria. Básicamente hay dos tipos de apuntadores:
  1. Apuntadores a variables.
  2. Apuntadores a funciones.
   Si el apuntador es a una variable, entonces el apuntador almacena la dirección en memoria de dicha variable. Si el apuntador es a una función, entonces el apuntador almacena la dirección en memoria del inicio del código de dicha función. Por ser en principio más simples, se presentarán primero los apuntadores a variables.

   Las siguientes figuras muestran la representación de un apuntador, y su explicación se verá reforzada cuando se analice el Ejemplo 7.1. Mientras tanto, tome en cuenta que un apuntador sólo puede hacer referencia o apuntar a objetos de su mismo tipo de datos, esto es: un apuntador a int, sólo puede hacer referencia a variables de tipo int, un apuntador a char, sólo puede hacer referencia a variables de tipo char, etc.
(a) Representación de un apuntador (abstracción).
(b) Representación física (en memoria) de un apuntador.
 
    La declaración de una variable de tipo apuntador tiene la siguiente estructura general en el lenguaje de programación C:

tipo_de_dato * nombre_del_apuntador;

en donde:
  • tipo_de_dato es cualquier tipo de dato válido en C.
  • nombre_del_apuntador es un identificador válido en C.
  • * es el operador que denota que la variable nombre_del_apuntador es una variable de tipo apuntador a tipo_de_dato.
   Una variable de tipo apuntador debe ser congruente, es decir, sólo puede hacer referencia (apuntar) a variables de su mismo tipo de datos; así por ejemplo, una variable de tipo apuntador a int puede hacer referencia a una variable de tipo int, mientras que una variable de tipo apuntador a float puede hacer referencia a una variable de tipo float y así sucesivamente para los demás tipos de datos.

Piedra angular de la piedra angular.
   El Ejemplo 7.1 es fundamental para entender a los apuntadores; por lo que resulta indispensable su total y absoluta comprensión para poder continuar y construir, en base a él, los conceptos subsecuentes. Recuerde apoyarse de las figuras anteriores para la mejor comprensión del ejemplo.

   La línea 9 muestra la declaración de dos variables de tipo apuntador a entero: aPtr y bPtr (note que como parte del identificador para cada una de las variables, se ha utilizado Ptr (Pointer) a forma de sufijo, esto no es obligatorio ni por sí mismo vuelve a la variable de tipo apuntador, pero es una buena práctica de programación el hacerlo así, debido a que refuerza, por auto documentación, que la variable es de tipo apuntador, ya que después de la declaración, no hay nada particular en la variable que en sí misma denote que es o no de tipo apuntador). Observe que el operador "*" no se distribuye en las variables como lo hace el tipo de dato, por lo que por cada variable de tipo apuntador que se necesite, se deberá escribir dicho operador para especificar que la variable asociada es de tipo apuntador.

   Las líneas 11, 12 y 13 inicializan a la variable a con 40178, y a aPtr y bPtr con la dirección en memoria de a. El operador "&" obtiene la dirección en memoria del operando (variable) asociado(a), y se conoce como el operador de dirección.

   Una variable de tipo apuntador, al ser una variable cuyo contenido es la dirección en memoria de otra variable, almacena direcciones de memoria obtenidas por el operador de dirección (el único valor que puede asignarse directamente a una variable de tipo apuntador es el 0, denotando así que el apuntador no hace referencia a nada, es decir, que no apunta a ninguna dirección válida de memoria en cuyo caso se dice que es un apuntador nulo), y aunque en principio es posible asignarle a un apuntador un valor específico, esto generará, con toda seguridad una violación de memoria y el programa será terminado por la mayoría de sistemas operativos (ningún sistema operativo debería permitir la intromisión en áreas de memoria que no sean las propias del programa, ya que ésto incurriría en graves errores de seguridad). Las figuras anteriores (a) y (b) ilustran lo que ocurre en las líneas 11-13.

   Las funciones printf de las líneas 15 y 16 contienen el especificador de formato "%p" (pointer), el cual permite visualizar en la salida estándar direcciones de memoria. Note cómo la dirección de memoria que se imprime en estas líneas es la misma, tal y como lo muestra la siguiente figura:

Salida del Ejemplo 7.1.
 
    La línea 19 muestra el uso del operador de desreferencia "*", el cual se utiliza para acceder al valor de la variable a la que se apunta o se hace referencia a través del apuntador.

   Para variables de tipo apuntador, el operador "*" tiene una dualidad:
  1. En la declaración de variables: se utiliza o sirve para denotar que la variable asociada es de tipo apuntador.
  2. En la desreferencia de variables: se utiliza o sirve para acceder al valor al que hace referencia o apunta el apuntador.
   Finalmente, la línea 21 modifica el valor de la variable a a través del apuntador bptr (recuerde que la variable a está siendo referida (apuntada) por las variables aPtr y bPtr (líneas 12 y 13 respectivamente)), y del operador de desreferencia, por lo que el valor a imprimir en la salida estándar para la variable a, será 2018, tal y como lo muestra la figura anterior.



   Antes de continuar, asegúrese de entender en su totalidad el Ejemplo 7.1 y lo que se ha descrito de él, ya que se insiste en que su comprensión es fundamental para entender los conceptos descritos en las entradas siguientes.

6 de septiembre de 2016

Suma de matrices.

   La suma de matrices es una operación clave del álgebra de matrices, y es muy sencilla debido a que la suma se realiza elemento a elemento entre las matrices que conforman los operandos.

   Sean A y B dos matrices definidas de la siguiente manera:


   La suma de las matrices anteriores denotada por:  C, se define como:


   Que en notación del lenguaje C sería (por filas):

Primera fila
c[0][0] = a[0][0] + b [0][0]
c[0][1] = a[0][1] + b [0][1]
.  .  .
c[0][n-1] = a[0][n-1] + b [0][n-1]

Segunda fila
c[1][0] = a[1][0] + b [1][0]
c[1][1] = a[1][1] + b [1][1]
.  .  .
c[1][n-1] = a[1][n-1] + b [1][n-1]

.  .  .

Última fila
c[m-1][0] = a[m-1][0] + b [m-1][0]
c[m-1][1] = a[m-1][1] + b [m-1][1]
.  .  .
c[m-1][n-1] = a[m-1][n-1] + b [m-1][n-1]

   El Ejemplo 6.8 muestra la función suma que realiza la suma de las matrices a y b, y almacena el resultado en la matriz c. Observe que las matrices a y b tienen la forma descrita por las matrices A y B anteriores, y que la matriz c que almacena la suma en el ejemplo, se calcula siguiendo los lineamientos expuestos en las expresiones anteriores por medio de ciclos anidados. Asegúrese de comprender ésto antes de continuar.

   Quizá el Ejemplo 6.8 no luzca todavía como una biblioteca de funciones, de hecho la intención es que progresivamente se vaya conformando y robusteciendo dicha biblioteca con más funciones. Por otro lado, se propone como ejercicio para el lector el escribir un programa que pruebe la función suma del Ejemplo 6.8. Asegúrese de declarar tres matrices, colocar datos en dos de ellas (los operandos), y enviárselos a la función suma.