Introducción al lenguaje C. Otros tipos de datos

Elementos de un programa C. Tipos básicos de datos. E/S básica. Sentencias de control. Funciones. Asignación dinámica de memoria. Ficheros. Ficheros indexados: la interfase Btrieve. Compilación y enlazado. Biblioteca de funciones de Turbo C

  • Enviado por: Juan
  • Idioma: castellano
  • País: España España
  • 19 páginas
publicidad

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 (&registro.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 = &registro;

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:).