Ingeniero Técnico en Informática de Gestión
Introducción al lenguaje C. Otros tipos de datos
8
Otros tipos de datos
Introducción
El Lenguaje C permite al usuario crear nuevos tipos de datos mediante 5 herramientas:
-
La sentencia typedef, que permite dar nuevos nombres a tipos de datos que ya existen.
-
Las estructuras, agrupaciones de variables de distinto tipo bajo un nombre común.
-
Los campos de bits, que son una variante de las estructuras, y que permiten el acceso individual a los bits de una palabra.
-
Las uniones, que permiten asignar la misma zona de memoria para variables diferentes.
-
Las enumeraciones, que son listas de símbolos.
Estudiaremos cada una de ellas a continuación.
Tipos definidos por el usuario
Mediante la palabra reservada typedef podemos dar nuevos nombres a tipos de datos que ya existen. La sintaxis es la siguiente:
typedef tipo nombre;
donde tipo es un tipo de dato y nombre es el nuevo nombre para el tipo de dato anterior. Por ejemplo:
typedef register int CONTADOR;
hace que C reconozca CONTADOR como un tipo de dato idéntico a register int. Así, después de la definición anterior podemos declarar variables del nuevo tipo mediante sentencias como
CONTADOR i, j;
que declara dos variables i, j del tipo CONTADOR, es decir, register int. Además, este tipo de definiciones no anulan el tipo anterior, por lo que podemos seguir declarando variables del tipo register int que convivan con declaraciones del tipo CONTADOR.
Mediante esta sentencia podemos usar nombres más cómodos para los tipos de datos, y hacer que el código fuente sea más claro.
Estructuras
Una estructura es un conjunto de variables de igual o distinto tipo, agrupadas bajo un nombre común. A esas variables se les denomina campos de la estructura. Las estructuras se declaran mediante la palabra reservada struct. Hay varias formas de declarar una estructura, pero la más general es la siguiente:
struct nombre_estructura {
tipo variable1;
tipo variable2;
...
...
tipo variableN;
} ;
Veamos un ejemplo. La siguiente declaración
struct FICHA {
char nombre[40];
int edad;
float altura;
};
define una estructura llamada FICHA con tres campos de distinto tipo: nombre, edad y altura.
Hemos de tener en cuenta que la declaración anterior sólo define la forma de la estructura, pero no declara ninguna variable con dicha forma. Por así, decirlo, hemos definido una plantilla de 3 campos, pero no hemos aplicado dicha plantilla a ninguna variable. Para hacerlo hay que escribir sentencias como
struct FICHA registro;
que declara una variable llamada registro con la estructura FICHA, es decir, registro es una variable compuestas por 3 campos: nombre, edad y altura. Para acceder a cada uno de los campos se utiliza el operador punto (.) de la siguiente forma:
strcpy (registro.nombre, "José García");
registro.edad = 38;
registro.altura = 1.82;
Por supuesto, pueden declararse varias variables con la misma estructura:
struct FICHA var1, var2;
que son dos variables con la estructura FICHA. Los campos de cada una de ellas, como por ejemplo var1.edad y var2.edad son dos variables distintas que ocupan posiciones de memoria diferentes.
También pueden declararse las variables a la vez que se define la estructura:
struct FICHA {
char nombre[40];
int edad;
float altura;
} var1, var2;
que declara dos variables var1 y var2 con la estructura FICHA. Este tipo de declaraciones no impide que puedan declararse más variables con esa estructura:
#include <...
struct FICHA {
char nombre[40];
int edad;
float altura;
} var1, var2;
void Funcion (void);
void main (void)
{
struct FICHA var3;
...
...
}
void Funcion (void)
{
struct FICHA var4;
...
...
}
En las sentencias anteriores se declaran 4 variables con la estructura FICHA: var1 y var2, globales; var3, local a main(); y var4, local a Funcion().
Pueden hacerse declaraciones de una estructura sin darle nombre. Por ejemplo:
struct {
char nombre[40];
int edad;
float altura;
} var1, var2;
declara las variables var1 y var2 con la estructura indicada. El problema que plantea este tipo de declaraciones es que, al no tener nombre la estructura, no es posible declarar otras variables con esa estructura.
También es habitual definir las estructuras con un nuevo tipo. Por ejemplo:
typedef struct {
char nombre[40];
int edad;
float altura;
} MIESTR;
Ahora podemos hacer uso del nuevo tipo para declarar variables mediante sentencias como:
MIESTR var1, var2;
Fijémonos que, debido a la sentencia typedef, MIESTR no es una variable, sino un tipo de dato.
Inicialización de estructuras
Es posible inicializar variables con estructura en el momento en que son declaradas. Por ejemplo:
#include <...
struct FICHA {
char nombre[40];
int edad;
float altura;
};
void main (void)
{
struct FICHA registro = { "José García", 38, 1.82 };
...
que produce el mismo efecto que las sentencias
strcpy (registro.nombre, "José García");
registro.edad = 38;
registro.altura = 1.82;
Anidamiento de estructuras
Es posible el anidamiento de estructuras, en el sentido de que un campo de una estructura puede ser, a su vez, una estructura. Veámoslo con un ejemplo:
struct FECHA {
int dia;
int mes;
int anyo;
};
struct CUMPLE {
char nombre[40];
struct FECHA nacim;
};
struct CUMPLE aniversario;
La variable aniversario así definida tiene los siguientes campos:
aniversario.nombre
aniversario.nacim.dia
aniversario.nacim.mes
aniversario.nacim.anyo
El anidamiento de estructuras puede hacerse a más profundidad, en cuyo caso el acceso a los campos de la variable se hace utilizando sucesivamente operadores punto.
Matrices de estructuras
Uno de los usos más habituales de las estructuras son las matrices. Para declarar una matriz de una estructura se hace como para cualquier otra variable. Por ejemplo,
struct FICHA {
char nombre[40];
int edad;
float altura;
};
...
...
struct FICHA vector[100];
declara un vector de 100 elementos llamado vector. Cada uno de los elementos vector[i], está formado por los 3 campos definidos en la estructura. Los campos del elemento i de vector son:
vector[i].nombre
vector[i].edad
vector[i].altura
Si alguno de los campos de la estructura es una matriz (como es el caso del campo nombre) puede accederse a cada elemento de la forma siguiente:
vector[10].nombre[3]
que se refiere al carácter 3 del campo nombre del elemento vector[10].
Paso de estructuras a funciones
Un campo de una estructura puede pasarse como argumento de una función del mismo modo que cualquier variable simple. El siguiente pograma muestra en pantalla, por medio de una función, un campo de una estructura.
#include <stdio.h>
#include <conio.h>
struct FICHA {
char nombre[40];
int edad;
float altura;
};
void Funcion (int);
void main (void)
{
struct FICHA registro = { "José García", 37, 1.82 };
clrscr ();
Funcion (registro.edad);
}
void Funcion (int n)
{
printf ("\nEdad: %d", n);
}
También se puede pasar la dirección de un campo de una estructura. Un programa similar al anterior, pero manejando la dirección del campo, es el siguiente:
#include <stdio.h>
#include <conio.h>
struct FICHA {
char nombre[40];
int edad;
float altura;
};
void Funcion (int *);
void main (void)
{
struct FICHA registro = { "José García", 37, 1.82 };
clrscr ();
Funcion (®istro.edad);
}
void Funcion (int *n)
{
printf ("\nEdad: %d", *n);
}
Por último, también podemos pasar la estructura completa como argumento de una función. El siguiente ejemplo muestra cómo hacerlo:
#include <stdio.h>
#include <conio.h>
struct FICHA {
char nombre[40];
int edad;
float altura;
};
void Funcion (struct FICHA);
void main (void)
{
struct FICHA registro = { "José García", 37, 1.82 };
clrscr ();
Funcion (registro);
}
void Funcion (struct FICHA n)
{
printf ("\nFuncion3");
printf ("\nNombre: %s", n.nombre);
printf ("\nEdad: %d", n.edad);
printf ("\nAltura: %f", n.altura);
}
Punteros a estructuras
Cuando las estructuras son complejas el paso de la estructura completa a una función puede ralentizar los programas debido a la necesidad de introducir y sacar cada vez todos los elementos de la estructura en la pila. Es por ello por lo que es conveniente recurrir a pasar sólo la dirección de la estructura, utilizando punteros.
Un puntero a una estructura se declara del mismo modo que un puntero a cualquier otro tipo de variable, mediante el operador *. Por ejemplo, la sentencia
struct MIESTR *p, var;
declara un puntero p a una estructura MIESTR y una variable var con esa estructura. Después de una declaración como la anterior, puede hacerse una asignación como la siguiente:
p = &var;
que asigna a p la dirección de la variable var.
Para acceder a los campos de la estructura mediante el puntero se hace
(*p).campo
Sin embargo esta sintaxis es antigua y está fuera de uso. En su lugar se utiliza el operador flecha (->), formado por el guión (-) y el símbolo mayor que (>).
p -> campo
El siguiente programa muestra en pantalla los campos de una estructura a través de un puntero que contiene su dirección.
#include <stdio.h>
#include <conio.h>
struct FICHA {
char nombre[40];
int edad;
float altura;
};
void main (void)
{
struct FICHA *p, registro = { "José García", 37, 1.82 };
clrscr ();
p = ®istro;
printf ("\nNombre: %s", p -> nombre);
printf ("\nEdad: %d", p -> edad);
printf ("\nAltura: %f", p -> altura);
}
Campos de bits
Los campos de bits son un caso particular de estructuras que permiten acceder a los bits individuales de una palabra. La sintaxis que define un campo de bits es la siguiente:
struct nombre_estructura {
tipo variable1: ancho1;
tipo variable2: ancho2;
...
...
tipo variableN:anchoN;
};
donde tipo es uno de los tipos char, unsigned char, int o unsigned int, y anchoN es un valor de 0 a 16, que representa el ancho en bits del campo de bits variableN. Si no se pone nombre al campo de bits, los bits especificados en ancho se reservan, pero no son accesibles.
El siguiente ejemplo muestra una declaración de campo de bits:
struct MIESTR {
int i: 2;
unsigned j: 5;
int : 4;
int k: 1;
unsigned m: 4;
};
struct MIESTR var;
Esta declaración se corresponde con el siguiente diseño de la palabra:
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
m k no usados j i
Los campos de bits son útiles para almacenar variables lógicas en un byte, para codificar los bits de un dispositivo, etc. El acceso a los campos de bits se realiza del mismo modo que a los campos de cualquier estructura. Tan sólo hay que tener en cuenta un par de restricciones con los campos de bits: no se permiten matrices y no se puede trabajar con direcciones.
Los campos de bits pueden presentar algún problema respecto de la portabilidad de los programas de una máquina a otra. Aquí se ha supuesto que los campos funcionan de izquierda a derecha, pero eso puede cambiar en otras máquinas, con lo que la interprestación de los diferente campos cambia.
Uniones
Una unión es una agrupación de variables que ocupan la misma posición de memoria. Se declaran de modo similar a las estructuras:
union nombre_unión {
tipo variable_1;
tipo variable_2;
...
...
tipo variable_N;
};
Cuando se declara una unión, las variables variable_1, variable_2, ..., variable_N ocupan la misma posición de memoria. El compilador reserva el tamaño suficiente para almacenar la variable más grande.
Es importante advertir una diferencia notable entre las estructuras (struct) y las uniones (union). En las primeras cada campo ocupa una posición de memoria propia, y cuando se almacena un valor en uno de los campos, no influyen en el resto. Por el contrario, en las uniones todas las variables de la unión tienen asignada la misma posición de memoria, lo cual implica que cuando se almacena un dato en una de ellas, influye en todas las demás. Aclaremos esto con un ejemplo sencillo. Sea la estructura
struct MIESTR {
char a;
int b;
float c;
};
struct MIESTR mivar;
Esta declaración asigna 7 bytes de memoria para la variable mivar: uno para el campo a, dos más para el campo b, y otros cuatro para el campo c.
mivar
6 | 5 | 4 | 3 | 2 | 1 | 0 |
c b a
Si hacemos una asignación como
mivar.b = 120;
no afecta a mivar.a ni a mivar.c, pues cada uno de ellos tiene asignadas direcciones distintas de memoria.
Sin embargo, sea la union
union MIUNION {
char a;
int b;
float c;
};
union MIUNION mivar;
Esta declaración asigna 4 bytes de memoria para mivar (el campo más grande de la unión es c, de 4 bytes).
mivar
3 | 2 | 1 | 0 |
a
b
c
Si ahora hacemos una asignación como
mivar.b = 120;
afecta tanto a mivar.a como a mivar.c.
Veamos un ejemplo. Las siguientes declaraciones utilizan las estructuras y uniones para almacenar datos de 100 alumnos y profesores de un Centro de Enseñanza. Se almacena el tipo (A: alumno, P: profesor), nombre, edad, dirección y teléfono. Si es alumno se almacena, además, el grupo al que pertenece, el número de asignaturas que cursa y si es o no repetidor. Si es profesor se almacena el número de registro y el cargo que desempeña.
struct ALUMNO {
char grupo[15];
int asignat;
char repite;
};
struct PROFESOR {
char nrp[16];
char cargo[21];
};
union AL_PR {
struct ALUMNO al;
struct PROFESOR pr;
};
struct DATOS {
char tipo;
char nombre[40];
int edad;
char direccion[40];
char telefono[8];
union AL_PR ambos;
} personal[100];
El siguiente segmento de progama muestra los datos de la matriz personal.
for (i = 0; i < 100; i++) {
printf ("\nNombre: %s", personal[i].nombre);
printf ("\nEdad: %d", personal[i].edad);
printf ("\nDirección: %s", personal[i].direccion);
printf ("\nTeléfono: %s", personal[i].telefono);
if (personal[i].tipo == 'A') {
printf ("\nALUMNO");
printf ("\nGrupo: %s", personal[i].ambos.al.grupo);
printf ("\nNº de Asignaturas: %d", personal[i].ambos.al.asignat);
printf ("\nRepite: %d", personal[i].ambos.al.repite); }
else {
printf ("\nPROFESOR");
printf ("\nN.R.P.: %s", personal[i].ambos.pr.nrp);
printf ("\nCargo: %s", personal[i].ambos.pr.cargo);
}
}
Combinando estructuras, uniones y campos de bits podemos definir variables de 16 bits con acceso por bit, byte o palabra.
struct BITS {
unsigned bit0: 1;
unsigned bit1: 1;
unsigned bit2: 1;
unsigned bit3: 1;
unsigned bit4: 1;
unsigned bit5: 1;
unsigned bit6: 1;
unsigned bit7: 1;
unsigned bit8: 1;
unsigned bit9: 1;
unsigned bit10: 1;
unsigned bit11: 1;
unsigned bit12: 1;
unsigned bit13: 1;
unsigned bit14: 1;
unsigned bit15: 1;
};
struct BYTES {
unsigned byte0: 8;
unsigned byte1: 8;
};
union PALABRA {
int x;
struct BYTES byte;
struct BITS bit;
} dato;
Ahora, mediante dato podemos acceder a la palabra completa (dato.x), a uno de los dos bytes (dato.byte.byteN), o a bits individuales (dato.bit.bitN).
Vamos a utilizar la función de la biblioteca estándar biosequip() en un programa quehace uso de esta capacidad. La funcón biosequip() devuelve una palabra con la siguiente información:
bits 14-15 Número de impresoras paralelo
bit 13 Impresoria serie instalada
bit 12 Joystick instalado
bits 9-11 Número de puertos COM
bit 8 Chip DMA instalado (0: SÍ, 1: NO)
bits 6-7 Nº de disqueteras (si bit0 = 1)
00: 1 disquetera
01: 2 disqueteras
10: 3 disqueteras
11: 4 disqueteras
bits 4-5 Modo inicial de vídeo
00: No usado
01: 40x25 BN, adaptador color
10: 80x25 BN, adaptador color
11: 80x25 BN, adaptador monográfico
bits 2-3 RAM en placa base
00: 16k
01: 32k
10: 48k
11: 64k
bit 1 Coprocesador matemático 80x87 instalado
bit 0 Arranque desde disquete
El siguiente programa presenta en pantalla la información proporcionada por biosequip().
#include <stdio.h>
#include <conio.h>
#include <bios.h>
struct BIOSEQUIP {
unsigned arranque: 1;
unsigned cop80x87: 1;
unsigned RAM_base: 2;
unsigned video: 2;
unsigned discos: 2;
unsigned DMA: 1;
unsigned puertos: 3;
unsigned juegos: 1;
unsigned serie: 1;
unsigned paralelo: 2;
};
union EQUIPO {
int palabra;
struct BIOSEQUIP bits;
};
void Presentar (void);
void main (void)
{
union EQUIPO bios;
bios.palabra = biosequip ();
clrscr ();
Presentar ();
gotoxy (31, 1);
if (bios.bits.cop80x87) puts ("SI");
else puts ("NO");
gotoxy (31, 2);
printf ("%d Kb", (bios.bits.RAM_base + 1) * 16);
gotoxy (31, 3);
switch (bios.bits.video) {
case 1: puts ("40x25 Blanco y Negro, Adaptador Color");
break;
case 2: puts ("80x25 Blanco y Negro, Adaptador Color");
break;
case 3: puts ("80x25 Blanco y Negro, Adaptador Monocromo");
}
gotoxy (31, 4);
if (bios.bits.arranque) printf ("%d", bios.bits.discos + 1);
else puts ("0");
gotoxy (31, 5);
if (bios.bits.DMA) puts ("NO");
else puts ("SI");
gotoxy (31, 6);
printf ("%d", bios.bits.puertos);
gotoxy (31, 7);
if (bios.bits.juegos) puts ("SI");
else puts ("NO");
gotoxy (31, 8);
printf ("%d", bios.bits.serie);
gotoxy (31, 9);
printf ("%d\n", bios.bits.paralelo);
}
void Presentar (void)
{
puts ("Coprocesador Matemático ..... ");
puts ("RAM en placa base ........... ");
puts ("Modo inicial de vídeo ....... ");
puts ("Nº unidades de disquete ..... ");
puts ("Chip DMA .................... ");
puts ("Nº de puertos COM ........... ");
puts ("Joystick .................... ");
puts ("Impresora serie ............. ");
puts ("Impresoras paralelo ......... ");
}
Enumeraciones
Son grupos de constantes agrupadas de modo similar a las estructuras y que permiten controlar el rango de una variable. La sintaxis que permite definir una enumeración es:
enum nombre_enumeración {
constante1;
constante2;
...
...
constanteN;
} variable;
siendo nombre_enumeración el identificador que da nombre a la enumeración, y variable la variable que puede tomar los valores definidos para constante1, constante2, ..., constanteN. Estas constantes asumen los valores 0, 1, 2, ..., N. Por ejemplo:
enum COLORES {
NEGRO;
AZUL;
VERDE;
CYAN;
ROJO;
MORADO;
AMARILLO;
BLANCO;
} color;
Para una enumeración como la anterior, la sentencia
printf ("%d %d", AZUL, MORADO);
mostraría en pantalla los valores 1 y 5. También son posibles sentencias del tipo
switch (color) {
case NEGRO: ...
break;
case AZUL: ...
...
...
default: ...
}
Es importante no olvidar que los valores definidos con los identificadores NEGRO, AZUL, etcétera, no son variables sino constantes.
Es posible modificar el rango de valores de una enumeración, inicializando las constantes en el momento de la declaración. Así, la enumeración
enum VALORES {
CTE1;
CTE2;
CTE3 = 25;
CTE4;
CTE5 = 31;
};
define las constantes de la enumeración con los siguientes valores
CTE1: 0
CTE2: 1
CTE3: 25
CTE4: 26
CTE5: 31
Ejercicios
1. Deseamos hacer un listado de precios de los artículos de la empresa QUIEBRA,S.A. Los datos a procesar para cada artículo son los siguientes:
-
Código del artículo: Cadena de 3 caracteres.
-
Descripción: Cadena de 40 caracteres.
-
Componentes: Matriz de 5 elementos. Cada elemento de esta matriz contendrá los siguientes campos:
Código del componente: Cadena de 8 caracteres.
Cantidad: Entero entre 1 y 100.
Precio unitario: Entero entre 500 y 5000.
Construye un programa que realice las siguientes tareas:
Capturar por teclado los datos de los artículos, almacenándolos en una matriz (máximo, 25 artículos).
Imprimir los datos de la matriz con el formato siguiente:
QUIEBRA, S.A. Listado de precios | Página:XXX | ||||||
COMPONENTES | |||||||
Cod | Descripción | Código | Cant | Precio | Importe | ||
xxx | xxxxxxxxxxxxxxxxxxxxxxxxx | xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
Total: | xxxxxxx | ||||||
xxx | xxxxxxxxxxxxxxxxxxxxxxxxx | xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
xxxxxxxx | xxxxx | xxxxxx | xxxxxxx | ||||
Total: | xxxxxxx | ||||||
... | ... | ... | ... | ... | ... | ||
... | ... | ... | ... | ... | ... | ||
... | ... | ... | ... | ... | ... | ||
... | ... | ... | ... | ... | ... |
Imprime 5 artículos en cada página.
2. La función #2 de la INT 17h devuelve el estado de un puerto paralelo especificado con el registro DX. Los requerimientos previos de la función son:
AH = 2
DX = Nº del puerto paralelo
0 - LPT1:
1 - LPT2:
...
...
La función devuelve en AH el estado del puerto. Los bits de AH deben interpretarse como sigue:
bit 0 La impresora no respondió en el tiempo previsto
bits 1-2 No usados
bit3 Error de E/S
bit4 Impresora seleccionada
bit5 Sin papel
bit6 Reconocimiento de impresora (Acuse de recibo ACK)
bit7 Impresora libre
Construye un programa que muestre esta información para el primer puerto paralelo (LPT1:).
Descargar
Enviado por: | Juan |
Idioma: | castellano |
País: | España |