El primer tipo de dato definible por el usuario que veremos es la estructura, mediante la cual podemos agrupar varias variables bajo un mismo nombre, proporcionando una manera conveniente de mantener junta información que está relacionada. Generalmente, todos los elementos de una estructura están relacionados lógicamente el uno con el otro. Por ejemplo, el nombre completo de una persona, su domicilio y su número telefónico pueden ser relacionados mediante una estructura. El siguiente fragmento de código es una declaración de estructura que define al título de un libro, a su autor y el precio de venta del libro:
struct libro {
char titulo[MAXTIT];
char autor[MAXAUT];
float valor;
};
Esta es la plantilla de una estructura, el plan maestro que nos describe la manera en la cual la estructura está conformada. En primer lugar aparece la palabra clave struct. Le sigue una etiqueta con la cual se identifica a la estructura, la etiqueta libro. Lo que sigue puesto entre corchetes es la estructura en sí. La estructura que tenemos arriba consiste de dos arrays de caracteres y una variable de tipo float; estos son los miembros de la estructura encerrados entre corchetes. Cada miembro de la estructura está descrito por su propia declaración, terminada en un semicolon. Por ejemplo, la porción titulo es un array de chars conteniendo MAXTIT elementos. Los miembros de una estructura pueden ser cualquiera de los tipos de datos que ya hemos visto previamente, e inclusive esto puede incluír otras estructuras.
Finalmente, ponemos un semicolon al final de la estructura, a la derecha del corchete de cierre. Esto en virtud de que una estructura es un enunciado C.
Una vez que ha sido declarada una estructura, podemos definir una variable de estructura. Para declarar una variable de estructura, podemos escribir algo como lo siguiente:
struct libro fisica;
Este enunciado declara una variable de estructura de tipo libro llamada fisica.
Cuando declaramos una estructura, en esencia estamos definiendo un tipo complejo de variable formada por los elementos de estructura. Sin embargo, no es sino hasta que declaramos una variable de tal tipo que una estructura así entra en existencia. C asignará automáticamente suficiente espacio en la memoria para darle cabida a todas las variables que comprenden una variable de estructura.
Podemos declarar una o más variables de estructura al mismo tiempo que declaramos una estructura, por ejemplo:
struct libro {
char titulo[MAXTIT];
char autor[MAXAUT];
float valor;
} fisica1, fisica2, fisica3;
Lo anterior define un tipo de estructura llamada libro y al mismo tiempo declara tres variables fisica1, fisica2 y fisica3 de dicho tipo.
Si en un programa solo se requiere de una variable de estructura, entonces la etiqueta es opcional y no es requerida. Esto significa que:
struct {
char titulo[MAXTIT];
char autor[MAXAUT];
float valor;
} quimica;
declara una variable llamada quimica definida mediante la estructura que le precede.
Podemos ver que la forma general de una estructura es la siguiente:
struct nombre del tipo de la estructura {
tipo elemento1;
tipo elemento2;
.
.
,
tipo elementoN;
} variables de estructura ;
Posiblemente lo más instructivo será dar un ejemplo poniendo en práctica lo que hemos visto arriba, como en el siguiente programa:
#include <stdio.h>
#define MAXTIT 48
#define MAXAUT 35
struct libro {
char titulo[MAXTIT];
char autor[MAXAUT];
float valor;
};
void main(void) {
struct libro coleccion;
printf("Dame el titulo del libro.\n");
gets(coleccion.titulo);
printf("Dame el nombre del autor.\n");
gets(coleccion.autor);
printf("Dame el precio del libro.\n");
scanf("%f", &coleccion.valor);
printf("%s por %s $%.2f\n",
coleccion.titulo, coleccion.autor, coleccion.valor );
printf("%s: \"%s\" \($%.2f\)\n",
coleccion.autor, coleccion.titulo, coleccion.valor );
};
El resultado de este programa interactivo será algo como esto (lo que aparece en amarillo lo escribe el usuario del programa):
dame dos hileras: armando
armando longitudes: 7 7 las hileras son iguales armandoarmando C:> |
La estructura de libro, tal y como aparece en la memoria, es la siguiente:
titulo | 48 bytes |
autor | 35 bytes |
valor | 8 bytes |
Así pues, cada variabe de estructura del tipo libro consume por lo tanto 48+35+8 ó 91 bytes de memoria RAM.
Una vez creada una variable de estructura, ¿cómo podemos referenciar miembros individuales de dicha estructura? El programa que se dió arriba nos muestra cómo: mediante un punto. O mejor dicho, mediante el operador punto. El siguiente fragmento asignará la cantidad 364.00 al campo valor de la variable de estructura fisica:
fisica.valor = 364.00;
El nombre de una variable de estructura seguido por un operador punto y el nombre del elemento individual hará una referencia a ese elemento de estructura individual. Todos los elementos de una estructura son accesados de la misma manera. La forma general es:
variable . elemento
Por lo tanto, para imprimir en la pantalla el título del ejemplo anterior, podemos escribir:
printf("%s", coleccion.titulo);
Del mismo modo, el array de caracteres coleccion.autor puede ser tomado con una invocación a la función de entrada gets() como se muestra aquí:
gets(coleccion.autor);
Esto le pasará a la función gets() un puntero de tipo char al inicio en la memoria del elemento autor.
Quienes hayan leído previamente la serie de entradas tituladas “El entorno Visual Basic” seguramente encontrarán aquí algo que les empieza a resultar harto familiar, porque esto se parece mucho al operador punto usado para accesar las propiedades de los objetos (controles, tales como cajas de texto, cajas de imagen, botones de opción, etcétera) definidos en la Caja de Herramientas de Visual Basic. ¿Significa entonces que podemos empezar a considerar una estructura struct de C como una especie de objeto? No. Por principio de cuentas, todos los datos (elementos) que pertenecen a una estructura pueden ser accesados libremente desde cualquier lugar de un programa dentro de los corchetes en los que se define una estructura, y se puede modificar el contenido de cualquier variable de tipo estructura. Puesto en terminología más elegante, los datos de una estructura no son privados a la estructura, son públicos. Esto va en sentido contrario a la filosofía de encapsulación de un objeto en lo que se entiende por una programación orientada a objetos, en donde la única manera en la que se pueden accesar y modificar las propiedades (datos) de un objeto es mediante los métodos (funciones) que son propios a un objeto. Y como hemos visto arriba, una variable de tipo estructura carece por completo de funciones, lo único que contiene son datos, todos los cuales son públicos. Sin embargo, si tomamos la definición de lo que es una estructura en C, y la ampliamos un poco para permitir que pueda poseer funciones (métodos), restringiendo el acceso a los datos de la estructura a través de los métodos, entonces podemos tener algo equiparable al objeto del que tanto se habla en la programación orientada a objetos. Pero al hacer tal cosa, debemos prescindir de la palabra struct para introducir otra palabra con la cual se pueda designar a la armazón básica a partir de la cual se puedan crear objetos individuales, al igual que con la palabra reservada struct en C podemos definir variables de estructura. Este ascenso en nivel de sofisticación es precisamente lo que siguió a la creación del lenguaje C, pero para poder dar este brinco es necesario haber entendido bien primero los fundamentos sobre los cuales descansa el lenguaje C que fue usado como punto de partida.
Hemos visto en otras entradas cómo podemos inicializar variable y arrays:
int conteo = 0;
float temperaturas[] = {17.8, 21.5, 24.3, 19.6};
¿Podemos inicializar también una variable de estructura? Si, siempre y cuando la variable de estructura sea declarada como externa o bien como static. El hecho de que una variable de estructura sea externa depende del lugar en el cual la variable en sí sea definida, no del lugar en donde la plantilla de la estructura es definida. En el ejemplo que hemos visto arriba, la plantilla de libro es externa (está definida antes y fuera de la función main()), pero la variable de estructura coleccion no lo es, porque está definida dentro de la función y, por convención predeterminada (default), es puesta en la clase de almacenamiento automatic. Supóngase, sin embargo, que hubiéramos hecho la siguiente declaración:
static struct libro coleccion;
Entonces la clase de almacenamiento sería static, y podríamos inicializar la estructura del siguiente modo:
static struct libro coleccion = {
"Fundamentos de Logica Digital",
"Armando Martinez",
328.00
};
Para hacer las inicializaciones más obvias, se le ha dado a cada miembro de la estructura su propia línea de inicializacion, aunque lo único que necesita el compilador son las comas para separar la inicialización de un miembro del siguiente.
Las variables de estructura, siendo variables, invitan a la reflexión sobre si es posible asignar una estructura a otra al igual que podemos asignar una variable B a otra variable A con el enunciado A.=.B. En relación a esto, la respuesta es afirmativa. Si dos estructuras son del mismo tipo, y solo si son del mismo tipo, podemos asignar una estructura a otra. En este caso, todos los elementos de la estructura en el lado izquierdo de la asignación recibirán los valores de los elementos correspondientes de la estructura en el lado derecho. El siguiente programa asigna uno a dos, y muestra el resultado como dos números puestos en la misma línea, 25 3.141592:
#include <stdio.h>
main(void)
{
struct ejemplo {
int i;
double d;
} uno, dos;
uno.i = 25;
uno.d = 3.141592;
dos = uno; /* se asigna una estructura a otra */
printf("%d %lf", dos.i, dos.d);
return 0;
}
Una vez que se ha definido una estructura, la cual es un nuevo tipo de dato que podemos usar al igual que todos los demás datos definidos en C, podemos ir más lejos implementando uno de los usos más comunes de las estructuras: los arrays de estructuras.
Para declarar un array de estructuras, primero tenemos que definir una estructura, y una vez hecho declaramos una variable de array de dicho tipo. A modo de ejemplo:
struct libro coleccion[MAXLIBROS];
Esto declara a coleccion como un array de estructuras con el elemento MAXLIBROS. Cada elemento del array es una estructura del tipo libro. De este modo, coleccion[0] es una estructura libro, coleccion[1] es una segunda estructura libro, y así sucesivamente. El nombre coleccion en sí no es un nombre de estructura, es el nombre de un array que contiene estructuras.
Con un array de estructuras, podemos ampliar el programa dado arriba al principio para dar entrada a varios libros en lugar de uno solo, lo cual nos permite crear un inventario de libros como una pequeña base de datos con capacidad para recibir un maximo de 50 libros:
#include <stdio.h>
#include <string.h> /* archivo cabecera para strcmp() */
#define MAXTIT 48
#define MAXAUT 35
#define MAXLIB 50
#define ALTO "" /* hilera nula, para concluir programa */
struct libro {
char titulo[MAXTIT];
char autor[MAXAUT];
float valor;
};
void main(void) {
struct libro coleccion[MAXLIB];
/* este es un array de estructuras de libros */
int conteo = 0;
int indice;
printf("Dame el titulo del libro.\n");
printf("Oprime [Enter] al comienzo de una linea para parar.\n");
while (conteo < MAXLIB &&
strcmp(gets(coleccion[conteo].titulo),ALTO) !=0)
{
printf("Dame el nombre del autor.\n");
gets(coleccion[conteo].autor);
printf("Dame el precio del libro.\n");
scanf("%f", &coleccion[conteo++].valor);
while (getchar() != '\n'); /* limpiar linea de entrada */
if (conteo < MAXLIB)
printf("Dame el siguiente titulo.\n");
}
printf("Esta es la lista de libros en el inventario:\n");
for (indice = 0; indice < conteo; indice++)
printf("%s por %s: $%.2f\n",
coleccion[indice].titulo,
coleccion[indice].autor,
coleccion[indice].valor);
}
La ejecución interactiva del programa anterior permite ir creando un inventario de libros dándose al final del programa un resumen de los libros introducidos al array de estructuras junto con el autor y el precio. En la pantalla se irá desarrollando algo como lo siguiente (como se ha hecho anteriormente, el texto que aparece en blanco es puesto por el programa mientras que el texto que aparece en amarillo es información introducida desde el teclado por el usuario del programa):
Dame el titulo del libro.
Oprime [Enter] al comienzo de una linea para parar. La Odisea Dame el nombre del autor. Homero Dame el precio del libro. 89.00 Dame el siguiente titulo. Quimica Organica Dame el nombre del autor. Hector Murillo Dame el precio del libro. 175.00 |
Siguen otras entradas de libros al inventario
Esta es la lista de libros en el inventario:
La Odisea de Homero: 89.00 Quimica Organica de Hector Murillo: 175.00 La Mecanica Cuantica de Armando Martinez: 196.00 Los Miserables de Victor Hugo: 104.00 La Amada Inmovil de Amado Nervo: 73.00 Logica Digital de Armando Martinez: 142.00 Viaje al centro de la Tierra de Jules Verne: 78.00 |
Podemos conceptualizar el array de estructuras que se ha creado arriba con la siguiente ilustración:
Nuestro programa interactivo para crear un inventario de libros deja mucho que desear en el sentido de que no proporciona forma alguna de poder corregir una entrada equivocada, el programa tendría que ser ampliado para proporcionar acceso individual a cada miembro de cada estructura permitiendo editar la información que tenga que ser modificada. Más aún, es deseable poder borrar entradas del inventario o agregar entradas adicionales en tiempos posteriores, lo cual requiere la capacidad de poder almacenar esta pequeña base de datos en alguna memoria permanente como un dispositivo flash USB o como un disco duro, lo cual requiere capacidades adicionales para el manejo de archivos que tienen que ser incorporadas a C con nuevas funciones como fopen() y fclose().
Hasta este punto, se ha trabajado sobre el hecho que todas las estructuras y los arrays de estructuras usadas en los ejemplos previos son globales o están definidas dentro de las funciones que las usan. Daremos ahora consideración sobre cómo podemos pasar estructuras completas y sus elementos a funciones que sean capaces de manipularlas.
Cuando le pasamos un elemento de una variable de estructura a una función, lo que se está haciendo realmente es pasarle el valor del elemento a la función. Por lo tanto, lo que se está haciendo es pasar una variable sencilla (a menos, claro está, de que el elemento sea algo más complejo, como un array de caracteres). Considérese por ejemplo esta estructura:
struct {
char a;
int b;
float c;
char h[100];
} ejemplo;
A continuación tenemos ejemplos de cada elemento pasado a una función diferente con esta estructura:
funcion1(ejemplo.a); /* se pasa el valor caracter de a */
funcion2(ejemplo.b); /* se pasa el valor entero de b */
funcion3(ejemplo.c); /* se pasa el valor flotante de c */
funcion4(ejemplo.h); /* se pasa el domicilio de la hilera h */
funcion5(ejemplo.h[10]); /* se pasa el valor caracter de h[10] */
Sin embargo, si lo que queremos hacer es pasar el domicilio de un elemento individual de estructura para lograr una transferencia de invocación por referencia, en tal caso anteponemos el operador & al nombre de la variable. De este modo, para pasar los domicilios de los elementos en la variable de estructura ejemplo, escribimos lo siguiente:
funcion1(&ejemplo.a); /* se pasa el domicilio del caracter a */
funcion2(&ejemplo.b); /* se pasa el domicilio del entero b */
funcion3(&ejemplo.c); /* se pasa el domicilio del flotante c */
funcion4(ejemplo.h) /* se pasa el domicilio de la hilera h */
funcion5(ejemplo.h[10]); /* se pasa el domicilio del caracter h[10] */
Obsérvese que el operador & precede al nombre de la variable de estructura, no al nombre del elemento individual. Obsérvese también en el caso de funcion4() que el elemento de hilera h ya representa un domicilio, con lo cual no se requiere el &.
Además de poderle pasar a una función elementos individuales de una estructura, podemos pensar en grande y pasar estructuras completas a funciones.
Cuando una estructura completa es pasada como un argumento a una función, la estructura completa es pasada usando el método de llamada-por-valor. Esto significa que cualquier cambio hecho a los contenidos de una estructura dentro de la función a la cual es pasada la estructura no alteran los contenidos de la estructura que es usada como argumento.
La consideración más importante es que al usar una estructura completa como un parámetro el tipo de argumento debe estar aparejado exactamente con el tipo del parámetro. El siguiente programa dá un ejemplo de esto, declarando al argumento arg y al parámetro param como el mismo tipo de estructura:
#include <stdio.h>
/* definir un tipo de estructura */
struct ejemplo {
int a, b;
char ch;
} ;
/* prototipo de funcion */
void funcion(struct ejemplo param);
main(void)
{
struct ejemplo arg; /* declarar arg */
arg.a = 1857;
funcion(arg);
return 0;
}
/* definicion de funcion */
void funcion(struct ejemplo param)
{
printf("%d", param.a);
}
Si se compila y se ejecuta el programa anterior, se imprimirá el número 1857. Como lo muestra el programa, es mejor definir globalmente un tipo de estructura y entonces usar su nombre para declarar variables de estructura y parámetros conforme sean requeridos, lo cual asegura que los argumentos y los parámetros estén aparejados, además de que garantiza el hecho de que cualquier otro programador que lea el programa tome nota de que param y arg son del mismo tipo.
Ahora bien, si es posible definir punteros hacia variables de tipos distintos, debe ser posible definir también punteros a estructuras de la misma manera. Sin embargo, hay algunos detalles en lo que tiene que ver con punteros a estructuras de los cuales tenemos que estar al tanto.
Los punteros a estructuras son declarados poniendo el operador de indirección (*) adelante del nombre de una variable de estructura. Tomando el ejemplo que vimos al principio, lo siguiente declara a libro como un puntero hacia datos de ese tipo:
struct libro *puntero_libro;
Hay varias aplicaciones para los punteros de estructura. Una de tales aplicaciones es poder lograr una invocación por llamada a una función. La otra tiene que ver con la creación de listas enlazadas y otras estructuras dinámicas de datos.
¿Qué otra razón nos podría motivar a recurrir a punteros de estructura? Hay un inconveniente en pasar todas excepto las más sencillas estructuras a funciones, y este es la carga adicional de trabajo en la máquina requerido para empujar (y botar) todos los elementos de la estructura hacia (y de) una pila (stack). Tratándose de estructuras sencillas con unos cuantos elementos esta carga de trabajo no es muy importante, pero si se usan varios elementos de la estructura o si algunos de los elementos son arrays, entonces el desempeño de la máquina en tiempo de ejecución se puede degradar a niveles inaceptables. La solución a este tipo de problema es pasar únicamente un puntero a la estructura. Cuando se pasa únicamente el puntero de una estructura a una función, únicamente el domicilio es empujado (y botado) de la pila. Esto significa que se puede ejecutar una invocación sumamente rápida a una función. Además, en virtud de que la función está referenciando la estructura actual y no una copia de la misma, puede modificar los elementos de la estructura usados en la invocación. Este tipo de detalles son obvios para aquellos que programan en lenguaje ensamblador acercándose en forma simbólica lo más posible al lenguaje de máquina del procesador C que se está utilizando, y las optimizaciones logradas de este modo reflejan lo cerca que puede trabajar C al nivel de la máquina manteniendo al mismo tiempo su portabilidad hacia máquinas con otras arquitecturas.
Para encontrar el domicilio de una variable de estructura, el operador & se coloca antes del nombre de la variable de estructura. A modo de ejemplo, dado el siguiente fragmento:
struct lista {
char nombre[85];
long numero_usuario;
float saldo;
} cuentahabiente;
struct lista *p /* se declara un puntero de estructura */
Hecho lo anterior, entonces la instrucción:
p = &cuentahabiente;
coloca el domicilio de cuentahabiente en el puntero p. Para poder accesar el elemento saldo podemos escribir:
(*p).saldo
Sin embargo, el programador rara vez verá, si acaso lo llega a ver, referencias a un miembro de estructura usando explícitamente el operador * como se acaba de mostrar. En virtud de que el acceso al miembro de una estructura a través de un puntero es algo muy común, se ha definido en ANSI C un operador especial para llevar a cabo esta tarea. Se llama el operador flecha, formado con un signo de menos (guión medio) seguido por el signo de mayor-que:
- >
El operador flecha es usado en lugar del operador punto cuando se está accesando un elemento de estructura usando un puntero a la variable de estructura.
De este modo, el enunciado que vimos arriba es usualmente escrito del siguiente modo:
p - > cuentahabiente
El siguiente programa, que implementa un temporizador capaz de imprimir las horas, minutos y segundos transcurridos, hace uso de un puntero de estructura:
#include <stdio.h>
#include <conio.h>
struct estructura_tiempo {
int horas;
int minutos;
int segundos;
} ;
/* prototipos de funciones */
void actualizar(struct estructura_tiempo *t);
void mostrar(struct estructura_tiempo *t);
void retardo(void);
main(void)
{
struct estructura_tiempo tiempo;
tiempo.horas=0;
tiempo.minutos=0;
tiempo.segundos=0;
for( ; !kbhit(); ) {
actualizar(&tiempo);
mostrar(&tiempo);
}
return 0;
}
void actualizar(struct estructura_tiempo *t)
{
t->segundos++;
if(t->segundos==60) {
t->segundos = 0;
t->minutos++;
}
if(t->minutos==60) {
t->minutos = 0;
t->horas++;
}
if(t->horas==24) t->horas=0;
retardo();
}
void mostrar(struct estructura_tiempo *t)
{
printf("%d:", t->horas);
printf("%d:", t->minutos);
printf("%d\n", t->segundos);
}
void retardo(void)
{
long int t;
for(t=1; t<128000; ++t) ;
}
El temporizador es implementado no mediante el hardware de la máquina (midiendo el tiempo transcurrido de acuerdo al reloj electrónico del sistema) sino mediante una rutina de bucle repetitivo que trata de medir el tiempo a través de cada conteo repetido efectuado por el bucle, o sea mediante software. La precisión del tiempo transcurrido se puede refinar ajustando el conteo del bucle en la función retardo(). Como puede verse, hay una estructura global (declarada fuera de todas las funciones del programa) llamada estructura_tiempo, pero no se declara variable global alguna. Adentro de la función principal main() se declara una estructura llamada tiempo que solo es conocida directamente por la función main(), y esta estructura es inicializada a 00:00:00 (o mejor dicho, los tres valores de la estructura son inicializados a cero). Hay dos funciones, actualizar(), la cual se encarga de ir cambiando el tiempo conforme va transcurriendo, y mostrar(), a las cuales les es pasado como argumento el domicilio de tiempo con &tiempo. En ambas funciones, el tipo del argumento es declarado ser structura_tiempo, lo cual es requerido para que el compilador sepa cómo referenciar los elementos de la estructura. El referenciamiento de cada elemento de estructura es a través de un puntero. Si se quiere fijar las horas de regreso a un valor cero al llegar a la hora 24:00:00, en tal caso se escribe lo siguiente:
if( t -> horas == 24 ) t -> horas = 0;
Esto le dice al compilador que tome el domicilio de t, que corresponde a tiempo en la función principal main(), y que se le asigne un valor de cero al elemento llamado horas.
Es importante mantener clara la diferencia que hay entre el uso del operador punto y el operador flecha. Cuando queremos accesar y modificar elementos de la estructura al operar sobre la misma estructura usamos el operador punto, mientras que cuando lo que se tiene es un puntero a una estructura lo que se utiliza es el operador flecha. Téngase presente también que cuando se tiene que pasar el domicilio de una estructura a una función se usa el operador &.
La implementación de un cronómetro recurriendo a un temporizador basado por completo en software tal vez no resulte satisfactorio para algunos programadores que prefieran usar el cronómetro interno de hardware que poseen muchas computadoras de escritorio, laptops y tabletas. Para ello, la mayoría de los compiladores y entornos C tienen disponible un archivo de cabecera con un nombre como TIME.H, el cual contiene los prototipos de las funciones para poder trabajar con la fecha del sistema y el tiempo del sistema. En este archivo de cabecera suelen incluírse dos tipos, uno de los cuales puede representar la fecha y el tiempo del sistema como un entero de tipo long (con un nombre como time_t), en lo que es conocido como “tiempo de calendario”. El otro tipo es una estructura que mantiene la fecha y el tiempo separados en sus elementos esenciales, una estructura con un nombre como tm definida de un modo como el que se muestra a continuación:
struct tm {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wkday;
int tm_yday;
int tm_isdst;
}
La siguiente tabla resume lo que se mide con cada entero:
elemento de la estructura |
lo que mide |
tm_sec | segundos, 0-59 |
tm_min | minutos, 0-59 |
tm_hour | horas, 0-23 |
tm_mday | dia del mes, 1-31 |
tm_mon | meses desde Enero, 0-11 |
tm_year | años desde 1900 |
tm_wday | dias desde el Domingo, 0-6 |
tm_yday | dias desde Enero 1, 0-365 |
tm_isdst | is Daylight Savings Time |
El último elemento es un indicador que surte efectos en Norteamérica en donde el horario de invierno tiene una diferencia de una hora con respecto al horario de verano para fines de ahorro de energía, en lo que se conoce como Daylight Savings Time (Tiempo de Ahorro Diurno). El elemento tm_isdst toma un valor positivo cuando el horario de ahorro de energía está en vigor, tomará un valor de cero si no está vigente, y tomará un valor negativo si no hay información disponible.
El prototipo para la función time() que en ANSI C fija las convenciones para la medición de tiempo y fecha de acuerdo al reloj del sistema es el siguiente (obsérvese que la función recibe como argumento un puntero del tipo time_t y regresa un argumento del tipo time_t):
time_t time(time_t *time)
La función time() regresa el número de segundos que han transcurrido desde el 1 de Enero de 1970, y puede ser invocada ya sea con un puntero nulo o con un puntero a una variable del tipo time_t; cuando se usa la segunda opción entonces al argumento se le asignará también el tiempo de calendario. Para convertir este tiempo de calendario hacia el tiempo definido mediante la estructura dada arriba, usamos otra función frecuentemente llamada en muchas bibliotecas localtime() con un prototipo como el siguiente:
struct tm *localtime(time_t *time)
La función localtime() regresa un puntero a la forma del tiempo proporcionada en forma de estructura; el tiempo es representado en el tiempo local mantenido por la máquina. El valor del tiempo es generalmente obtenido mediante una invocación a la función time().
Para mayor claridad, se tiene el programa que se dá a continuación que imprime en la pantalla el la fecha y el tiempo, tomados del reloj del sistema (si el reloj de la computadora no está sincronizado correctamente a la fecha y la hora del día, la información regresada será incorrecta, aunque ello ya no es culpa del programa):
#include <stdio.h>
#include <time.h>
main(void)
{
struct tm *puntero;
time_t tiempo_local;
tiempo_local = time('\0');
puntero = localtime(&tiempo_local);
printf(asctime(puntero));
return 0;
}
La estructura utilizada por la función localtime() para darle cabida a los elementos de la estructura de tiempo es cambiada cada vez que la función es invocada, y si el programador desea conservar los contenidos de la estructura (por ejemplo para fines de archivaje histórico) se tiene que copiar hacia otro lado. En el programa dado arriba puede verse que se utilizó la función asctime() en lo que viene siendo la manera más fácil de generar una hilera de fecha y tiempo, ciertamente más fácil que usar la forma estructurada de la fecha y el tiempo. El prototipo para esta función es el siguiente:
char *asctime(struct tm *puntero);
Como puede verse, la función asctime() regresa un puntero a una hilera de caracteres, tras haber llevado a cabo la conversión de la información que está almacenada en la estructura hacia la cual apunta el puntero. La hilera de caracteres regresada tiene el siguiente aspecto:
día mes fecha horas:minutos:segundos años\n\0
El puntero a la estructura que se le pasa a la función asctime() es lo que se obtiene cuando se utiliza previamente la función localtime(), como en la línea:
puntero = localtime(&tiempo_local);
Al igual que como ocurre con la función localtime(), la memoria buffer que es utilizada por la función asctime() para poner la hilera de caracteres indicada arriba es adjudicada estáticamente y es modificada cada vez que la función es invocada, y por lo tanto si el programador desea preservar la información (la hilera de caracteres) antes de que sea borrada y modificada se tiene que copiar hacia otro lado.
Tal vez el lector se esté preguntando: ¿Realmente es necesario entrar en estos detalles a este nivel de profundidad cuando un programa C está interactuando directamente con el hardware de la máquina? En verdad, no hay modo de evadir el asunto, y para este tipo de cosas hay que acostumbrarse a tener a la mano toda la documentación que viene con cada paquete de compilación C o de entorno IDE C, y hay que irse acostumbrando a leer código elaborado por programadores profesionales. El consuelo es que los principios fundamentales del lenguaje C son los mismos y son universales, lo que se requiere es desarrollar la fluidez en el uso del lenguaje al igual que cuando se está aprendiendo un idioma nuevo; no se puede aspirar a lograr una conversación inteligente con un ciudadano del otro lado del mundo si no se ha tenido práctica en el manejo de su idioma.
Como vimos arriba, además de tener estructuras podemos tener arrays de estructuras. Pero esto es apenas el principio. Los miembros de una estructura no solo pueden ser cualquier tipo de dato válido en C; a su vez los elementos de una estructura también pueden ser estructuras (léase bien esto: ¡estructuras contenidas dentro de estructuras!). Cuando una estructura es un elemento de otra estructura, tenemos lo que se conoce como estructuras anidadas. El el siguiente ejemplo, el elemento variable de estructura familiares esta anidado dentro de linaje:
struct linaje {
struct fam familiares;
int numero;
} cubano;
En este ejemplo, fam es una estructura que suponemos que se ha definido previamente (y se subraya que tiene que aparecer definida previamente para poder ser usada como elemento miembro de otra estructura) y la estructura linaje ha sido definida como una estructura que consta de dos elementos. El primer elemento es la estructura de tipo fam que contendrá el nombre y apellidos de la cabeza de la familia, mientras que el segundo elemento es numero que contendrá el número de miembros de que consta la familia.
¿Y cómo podemos referenciar un elemento de una estructura que a su vez es elemento miembro de otra estructura? La siguiente porción de código asignará el telefono 3444982 al campo telefono de la estructura familiares de cubano:
cubano.familiares.telefono = 3444982;
Como puede apreciarse, los elementos de cada estructura son referenciados siguiendo un orden de izquierda a derecha desde el más exterior hasta el más interior. Obsérvese el uso repetido del operador punto (esto se vuelve importante en el estudio de la programación orientada a objetos cuando manejamos objetos que a su vez contienen otros objetos).
Otra cosa que desde un principio distinguió a C de otros lenguajes de alto nivel es la capacidad de poder accesar uno o más bits individuales dentro de un byte o de una palabra binaria formada por varios bits. Se trata de algo casi indispensable en un lenguaje mediante el cual aspiramos a poder tomar control directo de funciones del hardware sin tener que recurrir a las penalidades de elaborar los programas en un lenguaje ensamblador que ni siquiera podrá ser portátil hacia otras máquinas distintas. Independientemente de que el acceso a bits individuales es algo indispensable para el programador de sistemas, esta capacidad resulta útil cuando queremos almacenar en un mismo sitio varias variables de tipo Boleano (falso/verdadero), sobre todo cuando muchas interfaces de dispositivos con los cuales la computadora se conecta al exterior transmiten y/o reciben en un solo byte información codificada dentro del byte. El ejemplo más claro es el de las banderas; cada bandera (bit) dentro de un byte puede representar algo como la paridad de una palabra binaria, la disponibilidad de un dato subsecuente para ser enviado, la disponibilidad del dispositivo para recibir datos, etcétera. Además varias rutinas de encriptación de datos requieren el poder accesar bits individuales dentro de un byte.
Una manera de poder accesar bits individuales en C utiliza una estructura conocida como el campo de bit, el cual es simplemente un tipo especial de miembro de estructura que define qué tan grande será en bits cada elemento de la estructura. La sintaxis de la forma general de la declaración de campos de bit tiene el siguiente aspecto:
struct nombre {
tipo nombre1: extensión;
tipo nombre2: extensión;
tipo nombre3: extensión;
.
.
.
tipo nombreN: extensión;
}
Por ser esencialmente Boleano, capaz de poder tomar únicamente uno de dos valores, falso (0) y verdadero (1), un campo de bit solo puede ser declarado como int, unsigned o signed. Los campos de bit cuya extensión es de un dígito tienen que ser declarados como unsigned en virtud de que un bit individual carece de signo.
Conforme han ido evolucionando las máquinas aumentando en capacidades y funciones disponibles en el hardware, las estructuras usadas para definir campos de bits han ido cambiando en forma acelerada, y no hay nada que pueda ser considerado aquí como universal e invariable. Y en muchos casos no hay estructuras definidas, cada programador tiene que ir definiendo “sobre la marcha” sus propias estructuras. Un caso así ocurrió cuando aparecieron las primeras computadoras caseras tipo IBM que incorporaban en la tarjeta madre (motherboard) de la electrónica un circuito integrado conocido como el BIOS (Basic Input/Output System), el cual más que un procesador digital de hardware en realidad es un programa en lenguaje de máquina almacenado en un chip que se puede comunicar directamente con los dispositivos instalados en la máquina (lector de CDs, lector de DVDs, disco duro, monitor, teclado, tarjeta de sonido, etcétera). Para estas primeras máquinas, a través del chip BIOS se puedew obtener una lista del equipo instalado en la computadora usando una función definida en C en los primeros compiladores C que salieron al mercado para las computadoras compatibles-IBM, cuyo prototipo es el siguiente:
int biosequip(void);
Esta función, la cual ha sirvió como punto de partida para modelar los programas C usados para máquinas posteriores más potentes, regresa como un entero de dos bytes (16 bits en total) una lista del equipo instalado en una computadora de acuerdo a la siguiente tabla válida para las computadoras IBM PC XT y AT:
Bit | Equipo instalado |
0 | El boot se debe llevar a cabo desde el drive de diskettes |
1 | Coprocesador de matemáticas 80x78 instalado |
2, 3 | RAM instalado en la máquina 0 0: 16 kilobytes 0 1: 32 kilobytes 1 0: 48 kilobytes 1 1: 64 kilobytes |
4, 5 | Modo inicial de video 0 0: No utilizado 0 1: 40x25 ByN, adaptador color 1 0: 80x25 ByN, adaptador color 1 1: 80x25, adaptador monocromo |
6, 7 | Número de drives floppy 0 0: un drive 0 1: dos drives 1 0: tres drives 1 1: cuatro drives |
8 | Chip DMA instalado |
9, 10, 11 | Número de puertos seriales 0 0 0: ningún puerto disponible 0 0 1: un puerto 0 1 0: dos puertos 0 1 1: tres puertos 1 0 0: cuatro puertos 1 0 1: cinco puertos 1 1 0: seis puertos 1 1 1: siete puertos |
12 | Adaptador de juegos instalado |
13 | Impresora serial (solo PC junior) |
14, 15 | Número de impresoras 0 0: ninguna impresora 0 1: una impresora 1 0: dos impresoras 1 1: tres impresoras |
A modo de ejemplo, si el entero que nos regresa una máquina (de esa época) tiene un valor de 21626, entonces, puesto que ese número entero equivale a la siguiente palabra binaria conformada por dos bytes:
0 1 0 1 0 1 0 0 0 1 1 1 1 0 1 0
podemos deducir que la máquina (el primer bit en ser leído es el que está más hacia la derecha, continuándose así hasta llegar al bit que está más hacia la izquierda):
* Tiene que arrancar con un diskette DOS puesto en el drive de floppies
* Tiene instalado un coprocesador de matemáticas
* Tiene un banco de memoria RAM de 48 kilobytes
* Tiene un monitor monocromático que puede reproducir 25 líneas de 80 columnas
* Tiene instalados dos drives floppy
* No tiene instalado un chip DMA de acceso directo a la memoria RAM
* Tiene dos puertos seriales disponibles
* Tiene instalado un adaptador de juegos
* No tiene impresora serial tipo IBM PCjr
* Tiene instalada una impresora serial genérica
Así pues, puede verse que la información no es proporcionada por biosequip() en forma de estructura, sino como una palabra binaria. Sin embargo, la información puede ser representada como campos de bits definiendo la siguiente estructura usando la sintaxis que fue especificada arriba (obsérvese, por ejemplo, que mientras que se requiere de un solo byte para definir si la computadora tiene instalado un adaptador de juegos, se requiere de dos bytes para definir la cantidad de memoria RAM instalada en la máquina, y se requiere de tres bytes para definir la cantidad de puertos seriales disponibles en la máquina; obsérvese también que el tipo de dato de todos los elementos es el de un entero sin signo, o sea unsigned):
struct equipoPC { unsigned boot_floppy: 1; unsigned tiene80x87: 1; unsigned RAM_instalado: 2; unsigned modo_video: 2; unsigned floppies: 2; unsigned dma: 1; unsigned puertos: 3; unsigned adaptadorjuegos: 1; unsigned no_utilizado: 1; unsigned num_impresoras: 2; } PC;
A manera de ejemplo, el siguiente programa C con la ayuda de la función biosequip() es capaz de poder sacar fuera la información acerca del número de drives floppies así como el número de puertos disponibles instalados en una máquina IBM PC-compatible (obsérvese que el prototipo de la función se encuentra en el archivo de cabecera BIOS.H):
#include "stdio.h" #include "bios.h" main(void) { struct equipoPC { unsigned boot_floppy: 1; unsigned tiene80x87: 1; unsigned RAM_instalado: 2; unsigned modo_video: 2; unsigned floppies: 2; unsigned dma: 1; unsigned puertos: 3; unsigned adaptadorjuegos: 1; unsigned no_utilizado: 1; unsigned num_impresoras: 2; } PC; int *i; i = (int *) &PC; *i = biosequip(); printf("%d floppies\n", PC.floppies+1); printf("%d puertos\n", PC.puertos+1); return 0; }
Si se ejecuta el programa, se puede obtener un resultado como el siguiente (esto en una máquina típica con el sistema operativo Windows 98 instalado en ella):
2 floppies
5 puertos
En el programa, al puntero de entero i se le asigna el domicilio de PC con la línea:
i = (int *) &PC;
mientras que el valor de retorno de la función biosequip() (o sea la palabra binaria de dos bytes que contiene codificada la información acerca del equipo) es asignado a PC a través de este puntero:
*i = biosequip();
Estos dos pasos son necesarios porque C no permite que se le haga una operación de cast a un entero hacia una estructura.
En el programa proporcionado, cada campo de bit es accesado usando el operador punto:
PC . floppies
PC . puertos
Sin embargo, cuando la estructura sea referenciada a través de un puntero, se tiene que usar el operador flecha.
No es necesario nombrar cada campo de bit; esto hace más fácil llegar al bit que se desea, pasando por encima de los que no son utilizados. A modo de ejemplo, los campos tiene80x87, dma y no_utilizado se pueden dejar vacíos, sin nombre:
struct equipoPC { unsigned boot_floppy: 1; unsigned : 1; unsigned RAM_instalado: 2; unsigned modo_video: 2; unsigned floppies: 2; unsigned : 1; unsigned puertos: 3; unsigned adaptadorjuegos: 1; unsigned : 1; unsigned num_impresoras: 2; } PC;
Aunque podemos hacer cosas tales como lo anterior, existen ciertas restricciones sobre cosas que no se pueden hacer con este tipo de estructuras de campos de bits. No podemos tomar el domicilio de una variable de campo de bit; tampoco se pueden hacer arrays de variables de campos de bits; ni se pueden traslapar fronteras delimitadoras de enteros. Al trabajar con este tipo de cosas, hay que tener en cuenta también que cualquier programa que utilice campos de bits puede tener algunas dependencias de máquina. Sin embargo, es válido mezclar miembros normales de estructura con elementos de campos de bits como se hace en el siguiente ejemplo:
struct paciente { struct identificacion nombres; int edad; unsigned sexo: 1; /* hombre o mujer */ unsigned mexicano: 1; /* ciudadano mexicano o no */ unsigned profesionista: 1; /* profesionista o no */ };
Esta estructura define al registro de un paciente que requiere de tan solo de un byte para almacenar tres datos: el sexo del paciente, la ciudadanía del paciente, y si el paciente es o no un profesionsta. De no haberse usado campos de bits, el almacenamiento de la misma información habría requerido no de tres bits (los cuales caben en un byte) sino de tres bytes. En una base de datos en donde haya cientos de miles de pacientes, el ahorro en memoria de almacenamiento puede ser considerable.
Habrá ocasiones en las cuales un programa manejará distintos tipos de datos usando la misma variable. En una situación así, podemos crear una estructura struct que contenga todos los tipos diferentes que queremos almacenar (otra forma de lograrlo es creando lo que se conoce como una clase, pero esto es algo que requiere una versión más refinada y avanzada de C habilitada para llevar a cabo lo que se conoce como la programación orientada a objetos). Sin embargo, para esta situación hay otra alternativa que consiste en usar una unión. Una unión pone todos los datos en un mismo espacio, y el compilador se encarga de determinar cuál es el espacio en la memoria requerido para almacenar el mayor dato de todos (por ejemplo, 8 bytes), y adjudica a la unión este espacio que ciertamente podrá dar cabida a todos los tipos. Así pues, una unión es simplemente una localidad de la memoria (o mejor dicho, varias localidades contiguas de memoria medidas en bytes) que es usada por distintas variables de tipos distintos. Las uniones son utilizadas cuando queremos conservar espacio de memoria.
Siempre que ponemos un valor en una unión, el valor empieza siempre en el mismo lugar al principio de la unión, pero únicamente se utiliza la cantidad de espacio que sea necesaria. De este modo, podemos crear una “supervariable” que puede almacenar cualquiera de las variables union. Todos los domicilios de las variables <b>union son los mismos (entanto que en una estructura son diferentes).
La sintaxis para declarar una unión, usando para ello la palabra clave reservada union, es semejante a la sintaxis usada para declarar una estructura, como podemos verlo en el siguiente ejemplo:
union muestra {
int A;
char ch;
} ;
Al igual que como ocurre con las estructuras, la declaración de una unión tampoco declara variables de unión. Si queremos declarar variables de unión, podemos hacerlo poniendo el nombre de la variable al final de la declaración, como se hace a continuación en donde se declara la variable compuesto del tipo muestra:
union muestra {
int A;
char ch;
} compuesto;
Podemos también usar un enunciado separado de declaración, y de este modo para declarar la variable compuesto2 del tipo muestra que se acaba de dar, podemos hacerlo con la siguiente línea:
union muestra compuesto2;
En ambos casos, tanto el entero A como el caracter ch comparten la misma localidad de la memoria. Esto puede causar alguna confusión al principio, porque el entero A requiere en muchos sistemas por lo menos dos bytes, mientras que un caracter solo requiere de un byte. La unión no se trata de adjudicar tres bytes, sino de adjudicar únicamente dos bytes a un dato en el que tanto A como ch comparten no dos localidades distintas de la memoria sino la misma localidad de la memoria.
Un uso típico de una unión sería para crear una tabla capaz de contener una mezcla de varios tipos en un orden que no es regular ni es conocido de antemano. La unión permite crear un array de unidades de igual tamaño, cada una de las cuales puede contener una variedad de tipos de distinto tamaño. Las uniones son usadas frecuentemente cuando se requiere de conversiones de tipo porque le permiten al programador mirar en más de una manera una región de la memoria. Un ejemplo de ello es la función de biblioteca putw() (put word) usada para escribir a un archivo en el disco duro la representación binaria de un entero. Aunque hay varias maneras de construír esta función, usaremos una en la que se recurre a una unión. Primero que nada, creamos una unión que consta de un entero y de un array de caracteres de dos bytes:
union ponerpalabra {
int i;
char car[2];
};
Hecho esto, podemos definir a putw() usando la union anterior (nota: el segundo argumento con el cual declaramos un puntero de archivo *fp es requerido para poder accesar los archivos del disco duro desde C):
void putw(union ponerpalabra palabra, FILE *fp)
{
putc(palabra->car[0], fp); /* escribir la primera mitad */
putc(palabra->car[1], fp); /* escribir la segunda mitad */
}
De este modo, recurriendo ahora a la función de biblioteca putc() (put character), aunque dicha función sea invocada con un entero la función putw() puede usar a la función putc() para escribir un entero a un archivo de disco.
Como otro ejemplo del uso de campos de bits, en el siguiente programa combinamos uniones con campos de bits para ir mostrando el código ASCII de cada tecla que vayamos oprimiendo en el teclado al poner en marcha el programa. La unión le permite a la función getche() asignarle el valor de la tecla a una variable de caracter, mientras que usamos los campos de bits para poner en la pantalla los bits individuales de los que consta el caracter ASCII respectivo.
#include <stdio.h>
#include <conio.h>
struct byte {
int b1 : 1;
int b2 : 1;
int b3 : 1;
int b4 : 1;
int b5 : 1;
int b6 : 1;
int b7 : 1;
int b8 : 1;
} ;
union bits {
char ch;
struct byte bit;
} ASCII ;
void decodificar(union bits b);
main(void)
{
do {
ASCII.ch = getche();
printf(". ");
decodificar(ASCII);
} while(ASCII.ch!='x'); /* salir al oprimir la tecla x */
return 0;
}
void decodificar(union bits b)
{
if(b.bit.b8) printf(" 1");
else printf(" 0");
if(b.bit.b7) printf(" 1");
else printf(" 0");
if(b.bit.b6) printf(" 1");
else printf(" 0");
if(b.bit.b5) printf(" 1");
else printf(" 0");
if(b.bit.b4) printf(" 1");
else printf(" 0");
if(b.bit.b3) printf(" 1");
else printf(" 0");
if(b.bit.b2) printf(" 1");
else printf(" 0");
if(b.bit.b1) printf(" 1");
else printf(" 0");
printf("\n");
}
Al ejecutarse el programa, conforme el usuario va oprimiendo teclas desde el teclado (teclas de letras o números) a la derecha de cada tecla se imprime en formato binario el equivalente en código ASCII del caracter que corresponde a la tecla, y se pasa a la siguiente línea inferior para esperar a que el usuario oprima otra tecla de la cual se quiere obtener el código ASCII. Y si se quiere salir del programa dándolo por terminado, se oprime la tecla “x”. La siguiente captura de imagen nos muestra una secuencia típica proporcionada por el programa:
Veremos ahora la manera en la cual con una unión podemos cargar un entero en un campo de bits. Como ya se mencionó previamente, C no permite que un entero pueda ser asignado directamente a una estructura de campos de bits. Sin embargo, el programa que se muestra a continuación que es una modificación del programa dado arriba para sacar hacia afuera a través del chip BIOS de la máquina la información acerca del número de drives floppies así como el número de puertos disponibles instalados en una máquina IBM PC-compatible, crea una unión que contiene un entero y un campo de bits. Cuando la función de biblioteca biosequip() regresa la información del hardware de la máquina, codificada como un entero, dicha información es asignada al entero. Pero el programa está en plena libertad de usar el campo de bits al reportar los resultados obtenidos:
#include "stdio.h" #include "bios.h" main(void) { struct equipoPC { unsigned boot_floppy: 1; unsigned tiene80x87: 1; unsigned RAM_instalado: 2; unsigned modo_video: 2; unsigned floppies: 2; unsigned dma: 1; unsigned puertos: 3; unsigned adaptadorjuegos: 1; unsigned no_utilizado: 1; unsigned num_impresoras: 2; } ; union { struct equipoPC PC; unsigned i; } PC_union; PC_union.i = biosequip(); printf("%d floppies\n", PC_union.PC.floppies+1); printf("%d puertos\n", PC_union.PC.puertos+1); return 0; }
Otro tipo de dato definido por el usuario se apoya en la palabra clave reservada enum con la cual podemos crear un nuevo “tipo” y especificar los valores que puede tener. En rigor de verdad, enum es de tipo entero int, así que lo que estamos haciendo es simplemente crear un nuevo nombre para un tipo ya existente. El propósito de los tipos enumerados es mejorar la legibilidad de un programa. Al igual que como ocurre con una union, la sintaxis es parecida a la que usamos para la declaración de las estructuras. Podemos hacer declaraciones como las siguientes:
enum gama {rojo, naranja, amarillo, verde, azul, violeta};
enum gama color;
La primera declaración establece a gama como un nombre de tipo, y la segunda declaración hace a color una variable del tipo gama. Los identificadores dentro de los corchetes (rojo, naranja, etc.) enumeran los valores posibles que una variable gama puede tener. De este modo, los valores posibles para color son rojo, naranja, amarillo, y así sucesivamente. Esto nos permite elaborar un programa usando enunciados como los siguientes:
color = azul;
if (color == amarillo)
...;
Tal vez el lector se esté preguntando: ¿Y qué exactamente son rojo y naranja? Técnicamente, son constantes int. Podemos saber los valores enteros que les son asignados por el compilador con una instrucción como la siguiente:
printf("rojo = %d, naranja = %d\n", rojo, naranja);
La salida obtenida será algo como esto:
rojo = 0, naranja = 1
Podemos ver que rojo se ha convertido en una constante nombrada que representa al entero 0. De modo semejante, los otros identificadores son contantes nombradas que representan los enteros 1 al 5. El proceso es parecido al de las constantes definidas, excepto que estas definiciones son montadas por el compilador y no por el preprocesador C.
En forma predeterminada, las constantes en una lista enumerada son asignadas los valores enteros 0, 1, 2, etc. Así pues, la declaración:
enum chicos {tommy, turquito, chiquis, nena, chilindrina};
hace que se le asigne a chiquis el valor 2.
Podemos cambiar los valores enteros que queremos para las constantes. Todo lo que tenemos que hacer es incluír en la declaración los valores deseados:
enum niveles {bajo = 100, medio = 300, alto = 500};
Si asignamos un valor a una constante pero no a la siguiente constante, la siguiente constante será numerada secuencialmente, como en la siguiente declaración:
enum felino {gato, lince = 10, puma, tigre};
en la cual en forma predeterminada gato recibe el valor 0, y lince, puma y tigre reciben los valores 10, 11 y 12.
Como ya se mencionó, el propósito de los tipos enumerados es mejorar la legibilidad y el propósito de un programa. Si estamos manejando colores, usando rojo y azul es algo mucho más obvio que usar los números 0 y 4. Obsérvese que los tipos enumerados son para uso interno. Si se quiere meter a la variable color un valor de naranja, debemos poner un 1 y no la palabra naranja. O bien podemos poner la hilera “naranja” y modificar el programa para que lleve a cabo la conversión de la hilera al entero apropiado.
Dado el hecho de que los valores de enumeración tienen que ser convertidos manualmente a sus valores de hilera equivalentes en castellano (o en el idioma en que se elabore el programa) al llevar a cabo operaciones de entrada/salida a través de una terminal o consola, encuentran su mayor uso en rutinas que no hacen tales conversiones. Es común ver que una enumeración sea usada para definir una tabla de símbolos en un compilador, la cual jamás requiere de una interacción con los usuarios.
C permite definir explícitamente nombres nuevos para tipos de datos usando la palabra clave typedef. Es importante aclarar que no se está creando un nuevo tipo de dato; más bien se está definiendo un nombre nuevo para un tipo ya existente. Esto puede ayudar a la portabilidad de programas que son dependientes del tipo de máquina en donde se están ejecutando, en cuyo caso únicamente los enunciados typedef tienen que ser cambiados al pasar de una máquina a otra. También puede ayudar en la documentación del código permitiendo nombres descriptivos para los tipos de datos convencionales. La sintaxis de la forma general de un enunciado typedef es la siguiente:
typedef tipo nombre;
en donde tipo es cualquier tipo de dato permitido y nombre es el nombre nuevo para el tipo. El nombre nuevo que se define es una adición y no un reemplazo para el nombre de un tipo existente.
Podemos crear, por ejemplo, un nuevo nombre para el tipo float usando:
typedef float num_con_puntodecimal;
Este enunciado le dice al compilador que reconozca num_con_puntodecimal como otro nombre para float. Tras esto, podemos crear una variable de tipo float usando el nuevo tipo:
num_con_puntodecimal temperatura;
Aquí temperatura es una variable de punto flotante del tipo num_con_puntodecimal que es otra palabra para float.
Podemos usar typedef también para crear nombres para tipos más complejos, como en el siguiente ejemplo:
typedef struct paciente {
float peso;
int edad;
char nombre[50];
} clinica123;
clinica123 listado[IMSS];
En este ejemplo, clinica123 no es una variable de tipo paciente, sino otro nombre para struct paciente.
El uso de typedef puede ayudar a que un programa largo sea más fácil de leer y comprender, y que sea más fácil de transportar a otro tipo de máquina, aunque hay que recordar que no se están creando nuevos tipos de datos, lo cual es algo a tomar en cuenta cuando hay limitaciones en la memoria o en la rapidez del sistema.