CAPÍTULO 8 Estructuras y uniones 8.1. Introducción Cuando estudiamos arreglos en los capítulos 5 y 6, observamos que representan un tipo de datos estructurado y permiten resolver un gran número de problemas en forma efectiva. Definimos a los arreglos como una colección finita, homogénea y ordenada de elementos. En este capítulo estudiaremos dos tipos de datos estructurados que se distinguen fundamentalmente de los arreglos porque sus elementos pueden ser heterogéneos, es decir, pueden pertenecer —aunque no necesariamente— a tipos de datos diferentes. Estas estructuras de datos reciben el nombre de estructuras y uniones. 288 Capítulo 8. Estructuras y uniones 8.2. Estructuras Las estructuras, conocidas generalmente con el nombre de registros, representan un tipo de datos estructurado. Se utilizan tanto para resolver problemas que involucran tipos de datos estructurados, heterogéneos, como para almacenar información en archivos —como veremos en el siguiente capítulo. Las estructuras tienen varios componentes, cada uno de los cuales puede constituir a su vez un tipo de datos simple o estructurado. Sin embargo, los componentes del nivel más bajo de un tipo estructurado, siempre son tipos de datos simples. Formalmente definimos a una estructura de la siguiente manera: “Una estructura es una colección de elementos finita y heterogénea.” Finita porque se puede determinar el número de componentes y heterogénea porque todos los elementos pueden ser de tipos de datos diferentes. Cada componente de la estructura se denomina campo y se identifica con un nombre único. Los campos de una estructura pueden ser de tipos de datos diferentes como ya hemos mencionado, simples o estructurados; por lo tanto, también podrían ser nuevamente una estructura. Para hacer referencia a un campo de una estructura siempre debemos utilizar tanto el nombre de la variable tipo estructura como el nombre del campo. En la figura 8.1 se muestra la representación gráfica de una estructura. Estructura Nombre de la variable tipo estructura Segundo campo Primer campo FIGURA 8.1 Representación gráfica de una estructura N-ésimo campo 8.2 Estructuras 289 EJEMPLO 8.1 Consideremos que por cada alumno de una universidad debemos almacenar la siguiente información: • Matrícula del alumno (entero). • Nombre del alumno (cadena de caracteres). • Carrera del alumno (cadena de caracteres). • Promedio del alumno (real). • Dirección del alumno (cadena de caracteres). La estructura de datos adecuada para almacenar esta información es la estructura. Cabe aclarar que no es posible utilizar un arreglo para resolver este problema, porque sus componentes deben ser del mismo tipo de datos. En la figura 8.2 se muestra la representación gráfica de este ejemplo. Alumno Matrícula Nombre Carrera Promedio Domicilio FIGURA 8.2 Representación gráfica de la estructura del ejemplo 8.1 El primer campo de la estructura es Matrícula; el segundo, Nombre; el tercero, Carrera, y así sucesivamente. Si queremos acceder entonces al primer campo de la estructura debemos escribir variable-de-tipo-estructura-Alumno.Matrícula. Si en cambio queremos hacer referencia al domicilio del alumno escribimos variable-de-tipo-estructura-Alumno.Domicilio. 8.2.1. Declaración de estructuras Observemos a continuación diferentes formas de declarar estructuras con la explicación correspondiente. EJEMPLO 8.2 En el siguiente programa podemos observar la forma en que se declara la estructura del ejemplo 8.1, así como también diferentes formas en que los campos reciben valores. 8 290 Capítulo 8. Estructuras y uniones Programa 8.1 #include <stdio.h> #include <string.h> /* Estructuras-1. El programa muestra la manera en que se declara una estructura, así como la ➥forma en que se tiene acceso a los campos de las variables de tipo estructura ➥tanto para asignación de valores como para lectura y escritura. */ struct alumno /* Declaración de la estructura. */ { int matricula; char nombre[20]; char carrera[20]; /* Campos de la estructura. */ float promedio; char direccion[20]; }; /* Observa que la declaración de una estructura termina con punto y ➥coma. */ void main(void) { /* Observa que las variables de tipo estructura se declaran como cualquier otra ➥variable. a1, a2 y a3 son variables de tipo estructura alumno. */ ➥struct alumno a1 = {120, ”María”, ”Contabilidad”, 8.9, ”Querétaro”}, a2, a3; /* Los campos de a1 reciben valores directamente. */ char nom[20], car[20], dir[20]; int mat; float pro; /* Los campos de a2 reciben valores por medio de una lectura. */ printf(”\nIngrese la matrícula del alumno 2: ”); scanf(”%d”, &a2.matricula); fflush(stdin); printf(”Ingrese el nombre del alumno 2:”); gets(a2.nombre); printf(”Ingrese la carrera del alumno 2: ”); gets(a2.carrera); printf(”Ingrese el promedio del alumno 2: ”); scanf(”%f”, &a2.promedio); fflush(stdin); printf(”Ingrese la dirección del alumno 2: ”); gets(a2.direccion); /* Los campos de a3 reciben valores por medio de asignaciones. */ printf(”\nIngrese la matrícula del alumno 3: ”); scanf(”%d”, &mat); a3.matricula = mat; fflush(stdin); printf(”Ingrese el nombre del alumno 3: ”); gets(nom); 8.2 Estructuras strcpy(a3.nombre, nom); printf(”Ingrese la carrera del alumno 3: ”); gets(car); strcpy(a3.carrera, car); printf(”Ingrese el promedio del alumno 3: ”); scanf(”%f”, &pro); a3.promedio = pro; fflush(stdin); printf(”Ingrese la dirección del alumno 3: ”); gets(dir); strcpy(a3.direccion, dir); /* Observe la forma en que se imprimen los campos de a1 y a2. */ printf(”\nDatos del alumno 1\n”); printf(”%d\n”, a1.matricula); puts(a1.nombre); puts(a1.carrera); printf(”%.2f\n”, a1.promedio); puts(a1.direccion); printf(”\nDatos del alumno 2\n”); printf(”%d\n”, a2.matricula); puts(a2.nombre); puts(a2.carrera); printf(”%.2f\n”, a2.promedio); puts(a2.direccion); /* Observa otra forma de escribir los campos de la variable de tipo estructura a3. */ printf(”\nDatos del alumno 3\n”); printf(”%d \t %s \t %s \t %.2f \t %s”, a3.matricula, a3.nombre, a3.carrera, a3.promedio, a3.direccion); } EJEMPLO 8.3 En el siguiente programa podemos observar diferentes formas en que los campos de las variables declaradas como apuntadores de una estructura reciben valores, así como también el acceso a los campos de estas variables. Programa 8.2 #include <string.h> /* Estructuras-2. El programa muestra la manera en que se declara una estructura, así como la ➥forma en que se tiene acceso a los campos de los apuntadores de tipo estructura ➥tanto para lectura como para escritura. Se utiliza además una función que ➥recibe como parámetro un apuntador de tipo estructura. */ struct alumno /* Declaración de la estructura. */ { 291 8 292 Capítulo 8. Estructuras y uniones int matricula; char nombre[20]; char carrera[20]; float promedio; char direccion[20]; /* Campos de la estructura alumno. */ }; void Lectura(struct alumno *); /* Prototipo de función. */ void main(void) { struct alumno a0 = {120, ”María”, ”Contabilidad”, 8.9, ”Querétaro”}; struct alumno *a3, *a4, *a5, a6; /* Observa que las variables *a3, *a4 y *a5 se declaran como apuntadores de ➥tipo estructura alumno. a6 es una variable de tipo estructura alumno. */ a3 = &a0; /* En este caso al apuntador de tipo estructura alumno a3 ➥se le asigna la dirección de la variable de tipo estructura alumno, a0. */ a4 = new (struct alumno); /* Nota que al apuntador a4 es necesario asignarle una dirección de memoria. ➥Para tener acceso a los campos de un apuntador de tipo estructura, utiliza uno ➥de los dos formatos siguientes: apuntador->campo o bien (*apuntador).campo En la lectura de los campos de la variable a4 se utilizan como ejemplo ambos ➥formatos. */ printf(”\nIngrese la matrícula del alumno 4: ”); scanf(”%d”, &(*a4).matricula); fflush(stdin); printf(”Ingrese el nombre del alumno 4: ”); gets(a4->nombre); printf(”Ingrese la carrera del alumno 4: ”); gets((*a4).carrera); printf(”Ingrese promedio del alumno 4: ”); scanf(”%f”, &a4->promedio); fflush(stdin); printf(”Ingrese la dirección del alumno 4: ”); gets(a4->direccion); a5 = new (struct alumno); Lectura(a5); /* En este caso se pasa el apuntador de tipo estructura alumno a5 a la función Lectura. */ Lectura(&a6); /* En este caso se pasa la variable de tipo estructura alumno a6, ➥a la función Lectura. Observa que en este caso debemos utilizar el operador de ➥dirección para preceder a la variable. */ printf(”\nDatos del alumno 3\n”); /* Observa la forma de escribir los campos de los apuntadores de tipo ➥estructura. */ 8.2 Estructuras printf(”%d\t%s\t%s\t%.2f\t%s”, a3->matricula, a3->nombre, a3->carrera, ➥a3->promedio, a3->direccion); printf(”\nDatos del alumno 4\n”); printf(”%d\t%s\t%s\t%.2f\t%s”, a4->matricula, a4->nombre, a4->carrera, ➥a4->promedio, a4->direccion); printf(”\nDatos del alumno 5\n”); printf(”%d\t%s\t%s\t%f\t%s”, a5->matricula, a5->nombre, a5->carrera, ➥a5->promedio, a5->direccion); printf(”\nDatos del alumno 6\n”); /* Observa la forma de escribir los campos de la variable tipo estructura. */ printf(”%d\t%s\t%s\t%.2f\t%s”, a6.matricula, a6.nombre, a6.carrera, ➥a6.promedio, a6.direccion); } void Lectura(struct alumno *a) /* Esta función permite leer los campos de un apuntador de tipo estructura ➥alumno. */ { printf(”\nIngrese la matrícula del alumno: ”); scanf(”%d”, &(*a).matricula); fflush(stdin); printf(”Ingrese el nombre del alumno: ”); gets(a->nombre); fflush(stdin); printf(”Ingrese la carrera del alumno: ”); gets((*a).carrera); printf(”Ingrese el promedio del alumno: ”); scanf(”%f”, &a->promedio); fflush(stdin); printf(”Ingrese la dirección del alumno: ”); gets(a->direccion); } 8.2.2. Creación de sinónimos o alias La instrucción typedef permite al usuario definir alias o sinónimos, es decir, nuevos tipos de datos equivalentes a los ya existentes. El objetivo de esta instrucción consiste en utilizar nombres más apropiados y más cortos para los tipos de datos, puesto que evitamos escribir la palabra struct en la declaración de las variables. La instrucción typedef se puede utilizar tanto con tipos de datos simples como con estructurados. Con los tipos de datos simples su uso no resulta muy práctico, más bien es redundante. Por ejemplo, si tenemos que declarar cinco variables de tipo entero, C1, C2, C3, C4 y C5, para un problema en particular, lo hacemos de esta forma: 293 8 294 Capítulo 8. Estructuras y uniones . . . int C1, C2, C3, C4, C5; . . . Si en cambio utilizáramos la instrucción typedef, tendríamos que escribir las siguientes instrucciones: . . . typedef int contador; /* Se declara un tipo de datos definido por el ➥usuario, contador en este caso, equivalente al tipo de dato ➥int. */ . . . void main(void) . . . contador C1, C2, C3, C4, C5; /* Posteriormente, ya sea en el programa principal o en una función, ➥declaramos las variables C1, C2, C3, C4 y C5 como de tipo contador. */ . . . En los tipos de datos estructurados, específicamente en las estructuras, su uso es importante ya que elimina la necesidad de escribir reiteradamente la palabra struct cada vez que hacemos referencia a una variable o apuntador de tipo estructura. Observemos a continuación la modificación que realizamos al programa 8.2. . . . typedef struct { int matricula; char nombre[20]; char carrera[20]; float promedio; char direccion[20]; } alumno; /* Declaración de la estructura utilizando typedef.*/ /* alumno es el nuevo tipo de datos creado por el ➥usuario. */ void Lectura (alumno *); /* Prototipo de función. Observa que al haber creado ➥el tipo de datos definido por el usuario alumno, se elimina la necesidad de ➥escribir la palabra struct antes de alumno en el parámetro. */ void main(void) { alumno a0 = {120, ”María”, ”Contabilidad”, 8.9, ”Querétaro”}, *a3, *a4, *a5, a6; /* En este caso se evita escribir la palabra struct en la declaración de las ➥variables tipo alumno. */ . . . 8.2 Estructuras 295 8.2.3. Estructuras anidadas 8 Las estructuras representan un tipo de datos estructurado, que tiene por lo tanto varios componentes. Cada uno de estos componentes puede a su vez ser un tipo de datos simple o estructurado. Las estructuras anidadas se presentan cuando en la declaración de una estructura, por lo menos uno de sus componentes es una estructura. Observemos el siguiente ejemplo. EJEMPLO 8.4 Consideremos que en una empresa requieren almacenar la siguiente información de cada empleado: • Nombre del empleado (cadena de caracteres). • Departamento de la empresa (cadena de caracteres). • Sueldo (real). • Domicilio • Calle (cadena de caracteres). • Número (entero). • Código Postal (entero). • Localidad (cadena de caracteres). A continuación se observa la representación gráfica de esta estructura: Empleado Domicilio Nombre Departamento Sueldo Calle Número CP Localidad FIGURA 8.3 Representación gráfica de una estructura anidada El programa muestra la manera como se declara una estructura anidada, así como la forma de acceso a los campos de cada una de las variables o apuntadores de tipo estructura, tanto para lectura como para escritura. Observa que para la lectura de los datos de algunas variables y apuntadores de tipo estructura se utiliza una función. 296 Capítulo 8. Estructuras y uniones Programa 8.3 #include <stdio.h> #include <string.h> /* Estructuras-3. El programa muestra la manera en que se declara una estructura anidada, así ➥como la forma de acceso a los campos de las variables o apuntadores de tipo ➥estructura, tanto para lectura como para escritura. Se utiliza además una ➥función que recibe como parámetro un apuntador de tipo estructura. */ typedef struct /* Declaración de la estructura domicilio utilizando ➥un typedef. */ { char calle[20]; int numero; int cp; char localidad[20]; } domicilio; struct empleado /* Declaración de la estructura anidada empleado. */ { char nombre[20]; char departamento[20]; float sueldo; domicilio direccion; /* direccion es un campo de tipo estructura ➥domicilio de la estructura empleado. */ }; void Lectura(struct empleado *a) /* Función que permite leer los campos de un apuntador de tipo estructura ➥empleado. */ { printf(”\nIngrese el nombre del empleado: ”); gets(a->nombre); fflush(stdin); printf(”Ingrese el departamento de la empresa: ”); gets(a->departamento); printf(”Ingrese el sueldo del empleado: ”); scanf(”%f”, &a->sueldo); fflush(stdin); printf(”—-Ingrese la dirección del empleado—-”); printf(”\n\tCalle: ”); gets(a->direccion.calle); printf(”\tNúmero: ”); scanf(”%d”, &a->direccion.numero); printf(”\tCódigo Postal: ”); scanf(”%d”, &a->direccion.cp); fflush(stdin); printf(”\tLocalidad: ”); 8.2 Estructuras gets(a->direccion.localidad); } void main(void) { struct empleado e0 = {”Arturo”, ”Compras”, 15500.75, ”San Jerónimo”, 120, ➥3490, ”Toluca”}; struct empleado *e1, *e2, e3, e4; /* Se declaran diferentes variables y apuntadores de la estructura empleado ➥para que el lector pueda apreciar también las diferentes formas en que los ➥campos reciben valores. */ /* En el programa principal se leen los campos de una variable, e3, y un ➥apuntador de tipo estructura, *e1. */ e1 = new (struct empleado); printf(”\nIngrese el nombre del empleado 1: ”); scanf(”%s”, &(*e1).nombre); fflush(stdin); printf(”Ingrese el departamento de la empresa: ”); gets(e1->departamento); printf(”Ingrese el sueldo del empleado: ”); scanf(”%f”, &e1->sueldo); printf(”—-Ingrese la dirección del empleado—-”); printf(”\n\tCalle: ”); fflush(stdin); gets(e1->dirección, calle); printf(”\tNúmero: ”); scanf(”%d”, &e1->direccion.numero); printf(”\tCódigo Postal: ”); scanf(”%d”, &e1->direccion.cp); printf(”\tLocalidad: ”); fflush(stdin); gets(e1->direccion.localidad); printf(”\nIngrese el nombre del empleado 3: ”); scanf(”%s”, &e3.nombre); fflush(stdin); printf(”Ingrese el departamento de la empresa: ”); gets(e3.departamento); printf(”Ingrese el sueldo del empleado: ”); scanf(”%f”, &e3.sueldo); printf(”—-Ingrese la dirección del empleado—-”); printf(”\n\tCalle: ”); fflush(stdin); gets(e3.direccion.calle); printf(”\tNúmero: ”); scanf(”%d”, &e3.direccion.numero); printf(”\tCódigo Postal: ”); scanf(”%d”, &e3.direccion.cp); printf(”\tLocalidad: ”); fflush(stdin); gets(e3.direccion.localidad); 297 8 298 Capítulo 8. Estructuras y uniones /* En la función Lectura se leen los campos de una variable, e4, y un apuntador ➥de tipo estructura, *e2. */ e2 = new (struct empleado); Lectura(e2); Lectura(&e4); printf(”\nDatos del empleado 1\n”); printf(”%s\t%s\t%.2f\t%s\t%d\t%d\t%s”, e1->nombre, e1->departamento, ➥e1>sueldo, e1->direccion.calle, e1->direccion.numero, e1->direccion.cp, ➥e1->direccion.localidad); printf(”\nDatos del empleado 4n”); printf(”%s\t%s\t%.2f\t%s\t%d\t%d\t%s”, e4.nombre, e4.departamento, e4.sueldo, ➥e4.direccion.calle, e4.direccion.numero, e4.direccion.cp, e4.direccion.localidad); } 8.2.4. Estructuras con arreglos Existen numerosos casos en la vida real en los que para resolver un problema de manera eficiente necesitamos utilizar estructuras combinadas con arreglos. Observemos el siguiente ejemplo, en el que uno de los campos de la estructura es a su vez otro arreglo. EJEMPLO 8.5 En una escuela almacenan la información de sus alumnos utilizando arreglos unidimensionales. La siguiente información de cada alumno se guarda en una estructura: • Matrícula (entero). • Nombre y apellido (cadena de caracteres). • Promedios de las materias (arreglo unidimensional de reales). Dato: ARRE[N] (donde ARRE es un arreglo unidimensional de tipo ALUMNO, 1 ≤ N ≤ 100). Escribe un programa en C que obtenga lo siguiente: a) La matrícula y el promedio de cada alumno. b) Las matrículas de los alumnos cuya calificación en la tercera materia sea mayor a 9. c) El promedio general de la materia 4. 8.2 Estructuras 299 Programa 8.4 8 #include <stdio.h> #include <string.h> /* Escuela. El programa genera información estadística de los alumnos de una escuela. */ typedef struct /* Declaración de la estructura alumno utilizando un ➥typedef. */ { int matricula; char nombre[30]; float cal[5]; /* Observa que el campo de la estructura alumno es un arreglo ➥unidimensional. */ } alumno; void void void void Lectura(alumno, int T); F1(alumno *, int TAM); F2(alumno *, int TAM); F3(alumno *, int TAM); /* Prototipos de funciones. */ void main(void) { alumno ARRE[50]; /* Se declara un arreglo unidimensional de tipo alumno. */ int TAM; do { printf(”Ingrese el tamaño del arreglo: ”); scanf(”%d”, &TAM); } while (TAM > 50 || TAM < 1); /* Se verifica que el tamaño del arreglo sea ➥correcto. */ Lectura(ARRE, TAM); F1(ARRE, TAM); F2(ARRE, TAM); F3(ARRE, TAM); } void Lectura(alumno A[], int T) /* La función Lectura se utiliza para leer un arreglo unidimensional de tipo ➥estructura alumno de T elementos. */ { int I, J; for (I=0; I<T; I++) { printf(”\nIngrese los datos del alumno %d”, I+1); printf(”\nIngrese la matrícula del alumno: ”); scanf(”%d”, &A[I].matricula); 300 Capítulo 8. Estructuras y uniones fflush(stdin); printf(”Ingrese el nombre del alumno:”); gets(A[I].nombre); for (J=0; J<5; J++) { printf(”\tIngrese la calificación %d scanf(”%f”, &A[I].cal[J]); } del alumno %d: ”, J+1, I+1); } } void F1(alumno A[], int T) /* La función F1 obtiene la matrícula y el promedio de cada alumno. */ { int I, J; float SUM, PRO; for (I=0; I<T; I++) { printf(”\nMatrícula del alumno: %d”, A[I].matricula); SUM = 0.0; for (J=0; J<5; J++) SUM = SUM + A[I].cal[J]; PRO = SUM / 5; printf(”\t\tPromedio: %.2f”, PRO); } } void F2(alumno A[], int T) /* La función F2 obtiene las matrículas de los alumnos cuya calificación en la ➥tercera materia es mayor a 9. */ { int I; printf(”\nAlumnos con calificación en la tercera materia > 9”); for (I=0; I<T; I++) if (A[I].cal[2] > 9) printf(”\nMatrícula del alumno: %d”, A[I].matricula); } void F3(alumno A[], int T) /* Esta función obtiene el promedio general del grupo de la materia 4. */ { int I; float PRO, SUM = 0.0; for (I=0; I<T; I++) SUM = SUM + A[I].cal[3]; PRO = SUM / T; printf(”\n\nPromedio de la materia 4: %.2f”, PRO); } 8.3 Uniones 8.3. Uniones Las uniones representan también un tipo de datos estructurado. Son similares a las estructuras. Sin embargo, se distinguen fundamentalmente de éstas porque sus miembros comparten el mismo espacio de almacenamiento en la memoria interna rápida de la computadora. Son muy útiles para ahorrar memoria. Sin embargo, es necesario considerar que sólo pueden utilizarse en aquellas aplicaciones en que sus componentes no reciban valores al mismo tiempo. Es decir, sólo uno de sus componentes puede recibir valor a la vez. El espacio de memoria reservado para una unión corresponde a la capacidad del campo de mayor tamaño. Formalmente definimos una unión de la siguiente manera: “Una unión es una colección de elementos finita y heterogénea, en la cual sólo uno de sus componentes puede recibir valor a la vez.” 8.3.1. Declaración de uniones La declaración de uniones es similar a la de estructuras. Observemos a continuación en el siguiente ejemplo la forma de declarar uniones. EJEMPLO 8.6 Supongamos que debemos almacenar la siguiente información de cada alumno de una universidad: • Matrícula del alumno (entero). • Nombre del alumno (cadena de caracteres). • Carrera del alumno (cadena de caracteres). • Promedio del alumno (real). • Teléfono celular (cadena de caracteres). • Correo electrónico (cadena de caracteres). El programa muestra la manera en que se declara una unión, así como la forma en que se tiene acceso a los campos de cada una de las variables de tipo unión, tanto para lectura como para escritura. Observa que en algunas variables de tipo estructura se utiliza una función para la lectura de los datos. 301 8 302 Capítulo 8. Estructuras y uniones Programa 8.5 #include <stdio.h> #include <string.h> /* Uniones. El programa muestra la manera como se declara una unión, así como la forma de ➥acceso a los campos de las variables de tipo unión tanto para asignación ➥de valores como para lectura y escritura. */ union datos { char celular[15]; char correo[20]; }; /* Declaración de una unión. */ typedef struct /* Declaración de una estructura utilizando typedef. */ { int matricula; char nombre[20]; char carrera[20]; float promedio; union datos personales; /* Observa que uno de los campos de la estructura alumno es una unión. */ } alumno; void Lectura(alumno a); /* Prototipo de función. */ void main(void) { alumno a1 = {120, ”María”, ”Contabilidad”, 8.9, ”5-158-40-50”}, a2, a3; /* Observa que sólo el primer componente de una unión puede recibir valores por ➥medio de este tipo de asignaciones. */ /* Para que puedas observar las diferentes formas en que los campos de las ➥variables de tipo estructura alumno reciben valores, ingresamos los valores ➥de los campos de tres formas diferentes. Los campos de a1 reciben valores ➥directamente, los campos de a2 se leen en el programa principal, y los campos ➥de a3 reciben valores a través de una función. */ printf(”Alumno 2\n”); printf(”Ingrese la matrícula: ”); scanf(”%d”, &a2.matricula); fflush(stdin); printf(”Ingrese el nombre: ”); gets(a2.nombre); fflush(stdin); printf(”Ingrese la carrera: ”); gets(a2.carrera); printf(”Ingrese el promedio: ”); scanf(”%f”, &a2.promedio); fflush(stdin); 8.3 Uniones printf(”Ingrese el correo electrónico: ”); gets(a2.personales.correo); /* Observa que en la variable a2 de tipo estructura alumno el segundo campo de la ➥unión recibe un valor. */ printf(”Alumno 3\n”); Lectura(&a3); /* Se llama a una función para leer los campos de la variable a3. */ /* Impresión de resultados. */ printf(”\nDatos del alumno 1\n”); printf(”%d\n”, a1.matricula); puts(a1.nombre); puts(a1.carrera); printf(”%.2f\n”, a1.promedio); puts(a1.personales.celular); /* Observa que escribe el valor del teléfono celular asignado. */ ➥puts(a1.personales.correo); } /* Observa que si tratamos de imprimir el campo correo, escribe basura. */ strcpy(a0.personales.correo, “[email protected]”); /* Se ingresa ahora un valor al segundo campo de la unión de la variable a0. */ puts(a0.personales.celular); /* Ahora escribe basura en el campo del teléfono celular. */ puts(a0.personales.correo); /* Escribe el contenido del campo ([email protected]). */ printf(”\nDatos del alumno 2\n”); printf(”%d\n”, a2.matricula); puts(a2.nombre); puts(a2.carrera); printf(”%.2f\n”, a2.promedio); puts(a2.personales.celular); puts(a2.personales.correo); /* Escribe basura. */ /* Escribe el contenido del segundo campo. */ printf(”Ingrese el teléfono celular del alumno 2: ”); fflush(stdin); gets(a2.personales.celular); puts(a2.personales.celular); puts(a2.personales.correo); printf(”\nDatos del alumno 3\n”); printf(”%d\n”, a3.matricula); puts(a3.nombre); puts(a3.carrera); printf(”%.2f\n”, a3.promedio); puts(a3.personales.celular); puts(a3.personales.correo); } /* Escribe el teléfono celular ingresado. */ /* Ahora escribe basura. */ /* Escribe basura. */ 303 8 304 Capítulo 8. Estructuras y uniones void Lectura(alumno *a) /* La función Lectura se utiliza para leer los campos de una variable de tipo ➥estructura alumno. */ { printf(”\nIngrese la matrícula: ”); scanf(”%d”, &(*a).matricula); fflush(stdin); printf(”Ingrese el nombre: ”); gets(a->nombre); fflush(stdin); printf(”Ingrese la carrera: ”); gets((*a).carrera); printf(”Ingrese el promedio: ”); scanf(”%f”, &a->promedio); printf(”Ingrese el teléfono celular: ”); fflush(stdin); gets(a->personales.celular); } Problemas resueltos Problema PR8.1 Una comercializadora farmacéutica distribuye productos a distintas farmacias de la Ciudad de México. Para ello almacena en un arreglo unidimensional, ordenado de menor a mayor en función de la clave, toda la información relativa a sus productos: • Clave del producto (entero). • Nombre del producto (cadena de caracteres). • Existencia (entero). • Precio unitario (real). Dato: (donde INV es un arreglo unidimensional de tipo PRODUCTO de N elementos, 1 ≤ N ≤ 100). INV [N] Realice un programa en C que construya los siguientes módulos: a) Ventas. El módulo registra la venta de diferentes productos a un cliente —farmacia—. Obtiene el total de la venta y actualiza el inventario correspondiente. El fin de datos para la venta de un cliente es 0. b) Reabastecimiento. Este módulo permite incorporar productos —cantidades— al inventario. El fin de datos es 0. Problemas resueltos 305 c) Nuevos Productos. El módulo permite incorporar nuevos productos al inventario. Los productos se encuentran ordenados en el arreglo por su clave. El fin de datos es 0. d) Inventario. El módulo permite imprimir el inventario completo. En la siguiente figura se muestra la representación gráfica de la estructura de datos necesaria para resolver este problema. INV: Arreglo unidimensional de tipo estructura producto INV clave nombre precio existencia clave nombre precio existencia . . . clave nombre precio existencia Producto 1 Producto 2 ... Producto 100 Estructura producto FIGURA 8.4 Representación gráfica de la estructura de datos necesaria para el problema PR8.1 Programa 8.6 #include <stdio.h> #include <string.h> /* Comercializadora farmacéutica. El programa maneja información sobre ventas, inventario, reabastecimiento y ➥nuevos productos de una comercializadora farmacéutica. */ typedef struct { int clave; char nombre[15]; float precio; int existencia; } producto; void void void void void /* Declaración de la estructura producto. */ Lectura(producto *, int); /* Ventas(producto *, int); Reabastecimiento(producto *, int); Nuevos_Productos(producto *, int *); Inventario(producto *, int); Prototipos de funciones. */ 8 306 Capítulo 8. Estructuras y uniones void main(void) { producto INV[100]; /* Se declara un arreglo unidimensional de tipo estructura producto. */ int TAM, OPE; do { printf(”Ingrese el número de productos: ”); scanf(”%d”, &TAM); } while (TAM > 100 | | TAM < 1); /* Se verifica que el número de productos ingresados sea correcto. */ Lectura(INV, TAM); printf(”\nIngrese operación a realizar. \n\t\t1 – Ventas \n\t\t 2 – ➥Reabastecimiento \n\t\t 3 - Nuevos Productos \n\t\t 4 – Inventario \n\t\t 0 - Salir: ”); scanf(”%d”, &OPE); while (OPE) { switch (OPE) { case 1: Ventas(INV, TAM); break; case 2: Reabastecimiento(INV, TAM); break; case 3: Nuevos_Productos(INV, &TAM); /* Se pasa el parámetro por referencia, porque se puede modificar el ➥número de elementos del arreglo en la función. */ break; case 4: Inventario(INV, TAM); break; }; printf(”\nIngrese operación a realizar. \n\t\t1 – Ventas \n\t\t 2 – ➥Reabastecimiento \n\t\t 3 - Nuevos Productos \n\t\t 4 – Inventario \n\t\t 0 - Salir: ”); scanf(”%d”, &OPE); } } void Lectura(producto A[], int T) /* Esta función se utiliza para leer un arreglo unidimensional de tipo ➥estructura producto de T elementos. */ { int I; for (I=0; I<T; I++) { printf(”\nIngrese información del producto %d”, I+1); printf(”\n\tClave: ”); scanf(”%d”, &A[I].clave); fflush(stdin); Problemas resueltos printf(”\tNombre:”); gets(A[I].nombre); printf(”\tPrecio:”); scanf(”%f”, &A[I].precio); printf(”\tExistencia: ”); scanf(”%d”, &A[I].existencia); } } void Ventas(producto A[], int T) /* Esta función se utiliza para manejar las venta a un cliente. Se ingresan ➥productos y cantidades, el fin de datos está dado por el cero. Además de ➥obtener el total de las ventas, se actualiza el inventario. */ { int CLA, CAN, I, RES; float TOT, PAR; printf(”\nIngrese clave del producto -0 para salir-: ”); scanf(”%d”, &CLA); TOT = 0.0; while (CLA) { printf(”\tCantidad: ”); scanf(”%d”, &CAN); I = 0; while ((I < T) && (A[I].clave < CLA)) /* Se realiza una búsqueda para localizar la clave del producto. */ I++; if ((I == T) | | (A[I].clave > CLA)) printf(”\nLa clave del producto es incorrecta”); else if (A[I].existencia >= CAN) /* Se analiza si el stock es suficiente para satisfacer el pedido. */ { A[I].existencia -= CAN; /* Se actualiza el stock del producto. */ PAR = A[I].precio * CAN; TOT += PAR; } else { printf(”\nNo existe en inventario la cantidad solicitada. Solo hay %d”, A[I].existencia); printf(” \nLos lleva 1 - Si 0 – No?: ”); scanf(”%d”, &RES); if (RES) { PAR = A[I].precio * A[I].existencia; A[I].existencia = 0; /* El stock queda en cero. */ TOT += PAR; } } 307 8 308 Capítulo 8. Estructuras y uniones printf(”\nIngrese la siguiente clave del producto -0 para salir-:); scanf(”%d”, &CLA); } printf(”\nTotal de la venta: } %f”, TOT); void Reabastecimiento(producto A[], int T) /* Esta función se utiliza para reabastecer al inventario. */ { int CLA,CAN,I; printf(”\nIngrese clave del producto -0 para salir-: ”); scanf(”%d”, &CLA); while (CLA) { I = 0; while ((I < T) && (A[I].clave < CLA)) I++; if ((I==T) || (A[I].clave > CLA)) printf(”\nLa clave del producto ingresada es incorrecta”); else { printf(”\tCantidad: ”); scanf(”%d”, &CAN); A[I].existencia += CAN; } printf(”\nIngrese otra clave del producto -0 para salir-: ”); scanf(”%d”, &CLA); } } void Nuevos_Productos(producto A[], int *T) /* Esta función se utiliza para incorporar nuevos productos al inventario. ➥Dado que los productos se encuentran ordenados por clave, puede suceder que ➥al insertar un nuevo producto haya que mover los elementos del arreglo para ➥que continúen ordenados. */ { int CLA, I, J; printf(”\nIngrese clave del producto -0 para salir-: ”); scanf(”%d”, &CLA); while ((*T < 30) && (CLA)) { I=0; while ((I < *T) && (A[I].clave < CLA)) /* Búsqueda de la posición que le corresponde a CLA en el arreglo. */ I++; if (I == *T) /* Se inserta el elemento en la última posición. */ { A[I].clave = CLA; printf(”\tNombre:”); fflush(stdin); gets(A[I].nombre); Problemas resueltos printf(”\tPrecio:”); scanf(”%f”, &A[I].precio); printf(”\tCantidad: ”); scanf(”%d”, &A[I].existencia); *T = *T + 1; } else if (A[I].clave == CLA) printf(”\nEl producto ya se encuentra en el inventario”); else { for (J=*T; J>I; J--) /* Se inserta el nuevo producto en el arreglo. Se mueven una posición ➥a la derecha los elementos del arreglo que tengan una clave de producto mayor a la ingresada. */ A[J] = A[J-1]; A[I].clave = CLA; printf(”\tNombre:”); fflush(stdin); gets(A[I].nombre); printf(”\tPrecio:”); scanf(”%f”, &A[I].precio); printf(”\tCantidad: ”); scanf(”%d”, &A[I].existencia); *T = *T + 1; } printf(”\nIngrese otra clave de producto -0 para salir-: ”); scanf(”%d”, &CLA); } if (*T == 30) printf(”\nYa no hay espacio para incorporar nuevos productos”); } void Inventario(producto A[], int T) /* Esta función se utiliza para escribir la información almacenada en —el ➥inventario— un arreglo unidimensional de tipo estructura producto de T ➥elementos. */ { int I; for (I=0; I<T; I++) { printf(”\nClave: %d”, A[I].clave); printf(”\tNombre: %s”, A[I].nombre); printf(”\tPrecio: %d”, A[I].precio); printf(”\tExistencia: %d \n”, A[I].existencia); } } 309 8 310 Capítulo 8. Estructuras y uniones Problema PR8.2 En una escuela guardan la información de sus alumnos utilizando arreglos unidimensionales. Se registra la siguiente información de cada alumno en una estructura: • Matrícula del alumno (entero). • Nombre y apellido (cadena de caracteres). • Materias y promedios (arreglo unidimensional de estructura). • Materia (cadena de caracteres). • Promedio (real). Dato: ALU[N], donde ALU es un arreglo unidimensional de tipo ALUMNO (1 ≤ N ≤ 50). Escribe un programa en C que obtenga lo siguiente: a) La matrícula y el promedio general de cada alumno. b) Las matrículas de los alumnos cuya calificación en la tercera materia sea mayor a 9. c) El promedio general de la materia 4. Nota: El problema es similar al del ejemplo 8.5. Varía en la forma de almacenar las calificaciones del alumno. Además de éstas, aparece el nombre de la materia. En la siguiente figura se muestra la representación gráfica de la estructura de datos necesaria para resolver este problema. ALU: Arreglo unidimensional de tipo estructura alumno Estructura alumno matrinom Alumno 1 cal matrinom matpro mat pro ALU cal Alumno 2 . . . matri nom ... cal Alumno 50 Estructura matpro cal: Arreglo unidimensional de tipo estructura matpro FIGURA 8.5 Representación gráfica de la estructura de datos necesaria para resolver el problema PR8.2 Problemas resueltos 311 Programa 8.7 8 #include <stdio.h> #include <string.h> /* Escuela. El programa genera información importante de los alumnos de una escuela. */ typedef struct { char mat[20]; int pro; } matpro; typedef struct { int matri; char nom[20]; matpro cal[5]; /* Declaración de la estructura matpro. */ /* Materia. */ /* Promedio. */ /* Declaración de la estructura alumno. */ /* Matrícula. */ /* Nombre del alumno. */ /* Observa que cal es un arreglo unidimensional de tipo ➥estructura matpro —la estructura definida en primer término. */ } alumno; void void void void Lectura(alumno * , int); F1(alumno *, int); F2(alumno *, int); F3(alumno *, int); /* Prototipos de funciones. */ void main(void) { alumno ALU[50]; /* ALU es un arreglo unidimensional de tipo alumno. */ int TAM; do { printf(”Ingrese el tamaño del arreglo: ”); scanf(”%d”, &TAM); } while (TAM > 50 | | TAM < 1); /* Se verifica que el tamaño del arreglo sea ➥correcto. */ Lectura(ALU, TAM); F1(ALU, TAM); F2(ALU, TAM); F3(ALU, TAM); } void Lectura(alumno A[], int T) /* Esta función se utiliza para leer la información de un arreglo unidimensional ➥de tipo estructura alumno de T elementos. */ { 312 Capítulo 8. Estructuras y uniones int I, J; for(I=0; I<T; I++) { printf(”\nIngrese los datos del alumno %d”, I+1); printf(”\nIngrese la matrícula del alumno: ”); scanf(”%d”, &A[I].matri); fflush(stdin); printf(”Ingrese el nombre del alumno:”); gets(A[I].nom); for (J=0; J<5; J++) { printf(”\tMateria %d: ”, J+1); fflush(stdin); gets(A[I].cal[J].mat); printf(”\tPromedio %d: ”, J+1); scanf(”%d”, &A[I].cal[J].pro); } } } void F1(alumno A[], int T) /* Esta función se utiliza para obtener la matrícula y el promedio general de ➥cada alumno. */ { int I, J; float SUM; for (I=0; I<T; I++) { printf(”\nMatrícula del alumno : %d”, A[I].matri); SUM = 0.0; for (J=0; J<5; J++) SUM = SUM + A[I].cal[J].pro; SUM = SUM / 5; printf(”\tPromedio: %.2f”, SUM); } } void F2(alumno A[], int T) /* Esta función se utiliza para obtener las matrículas de los alumnos cuya ➥calificación en la tercera materia es mayor a 9. */ { int I; printf(”\nAlumnos con calificación mayor a 9 en la tercera materia”); for (I=0; I<T; I++) if (A[I].cal[2].pro > 9) printf(”\nMatrícula del alumno : %d”, A[I].matri); } void F3(alumno A[], int T) /* Esta función se utiliza para obtener el promedio general de la cuarta materia. */ { Problemas resueltos int I; float SUM = 0.0; for (I=0; I<T; I++) SUM = SUM + A[I].cal[3].pro; SUM = SUM / T; printf(”\n\nPromedio de la cuarta materia: %.2f”, SUM); } Problema PR8.3 En un hospital almacenan la siguiente información de sus pacientes: • • • • • Nombre y apellido (cadena de caracteres). Edad (entero). Sexo (caracter). Condición (entero). Domicilio (estructura). • Calle (cadena de caracteres). • Número (entero). • Colonia (cadena de caracteres). • Código Postal (cadena de caracteres). • Ciudad (cadena de caracteres). • Teléfono (cadena de caracteres). Dato: HOSPITAL[N] (donde HOSPITAL es un arreglo unidimensional de tipo estructura PACIENTE, 1 ≤ N ≤ 100). Nota: Condición se refiere al estado de salud en que ingresa el paciente. Los valores que toma condición van de 1 a 5, y 5 representa el máximo grado de gravedad. Escribe un programa en C que genere lo siguiente: a) El porcentaje tanto de hombres como de mujeres registrados en el hospital. b) El número de pacientes de cada una de las categorías de condición. c) El nombre y teléfono de todos los pacientes que tuvieron una condición de ingreso de máxima gravedad (5). En la siguiente figura se muestra la representación gráfica de la estructura de datos necesaria para resolver este problema. 313 8 314 Capítulo 8. Estructuras y uniones HOSPITAL: Arreglo unidimensional de tipo estructura paciente Estructura paciente HOSPITAL nom edad sexo con dom tel nom edad sexo con dom tel . . . nom edad sexo con Paciente 2 Paciente 1 ... dom tel Paciente 100 dom cal num col cp ciu Estructura domicilio FIGURA 8.6 Representación gráfica de la estructura de datos necesaria para resolver el problema PR8.3 Programa 8.8 #include <stdio.h> #include <string.h> /* Hospital. El programa genera información acerca de los pacientes de un hospital. */ typedef struct { char cal[20]; int num; char col[20]; char cp[5]; char ciu[20]; } domicilio; typedef struct { char nom[20]; int edad; char sexo; int con; domicilio dom; char tel[10]; } paciente; void void void void /* Declaración de la estructura domicilio. */ /* /* /* /* /* Calle. */ Número. */ Colonia. */ Código Postal. */ Ciudad. */ /* Declaración de la estructura paciente. */ /* Nombre y apellido. */ /* Condición. */ /* Observa que el campo dom es de tipo estructura ➥domicilio. */ /* Teléfono. */ Lectura(paciente *, int); F1(paciente *, int); F2(paciente *, int); F3(paciente *, int); void main(void) { /* Prototipos de funciones. */ Problemas resueltos paciente HOSPITAL[100]; /* Arreglo unidimensional de tipo estructura ➥paciente. */ int TAM; do { printf(”Ingrese el número de pacientes: ”); scanf(”%d”, &TAM); } /* Se verifica que el tamaño del arreglo sea while (TAM > 50 | | TAM < 1); ➥correcto. */ Lectura(HOSPITAL, TAM); F1(HOSPITAL, TAM); F2(HOSPITAL, TAM); F3(HOSPITAL, TAM); } void Lectura(paciente A[], int T) /* Esta función se utiliza para leer un arreglo unidimensional de tipo ➥estructura paciente de T elementos. */ { int I; for (I=0; I<T; I++) { printf(”\n\t\tPaciente %d”, I+1); fflush(stdin); printf(”\nNombre: ”); gets(A[I].nom); printf(”Edad: ”); scanf(”%d”, &A[I].edad); printf(”Sexo (F-M): ”); scanf(”%c”, &A[I].sexo); printf(”Condición (1..5): ”); scanf(”%d”, &A[I].con); fflush(stdin); printf(”\tCalle: ”); gets(A[I].dom.cal); printf(”\tNúmero: ”); scanf(”%d”, &A[I].dom.num); fflush(stdin); printf(”\tColonia: ”); gets(A[I].dom.col); fflush(stdin); printf(”\tCódigo Postal: ”); gets(A[I].dom.cp); fflush(stdin); printf(”\tCiudad: ”); gets(A[I].dom.ciu); fflush(stdin); printf(”Teléfono: ”); gets(A[I].tel); } } void F1(paciente A[], int T) 315 8 316 Capítulo 8. Estructuras y uniones /* Esta función se utiliza para obtener el porcentaje tanto de hombres como de ➥mujeres registrados en el hospital. */ { int I,FEM, MAS, TOT; for (I=0; I<T; I++) switch (A[I].sexo) { case ‘F’: FEM++; break; case ‘M’: MAS++; break; } TOT = FEM + MAS; printf(”\nPorcentaje de Hombres: %.2f%”, (float)MAS / TOT * 100); printf(”\nPorcentaje de Mujeres: %.2f%”, (float)FEM / TOT * 100); } void F2(paciente A[], int T) /* Esta función se utiliza para obtener el número de pacientes que ingresaron al ➥hospital en cada una de las categorías de condición. */ { int I, C1 = 0, C2 = 0, C3 = 0, C4 = 0, C5 = 0; for (I=0; I<T; I++) switch (A[I].con) { case 1: C1++; break; case 2: C2++; break; case 3: C3++; break; case 4: C4++; break; case 5: C5++; break; } printf(”\nNúmero pacientes en condición 1: %d”, C1); printf(”\nNúmero pacientes en condición 2: %d”, C2); printf(”\nNúmero pacientes en condición 3: %d”, C3); printf(”\nNúmero pacientes en condición 4: %d”, C4); printf(”\nNúmero pacientes en condición 5: %d”, C5); } void F3(paciente A[], int T) /* La función F3 se utiliza para generar el nombre y teléfono de todos los ➥pacientes que tuvieron una condición de ingreso de máxima gravedad (5). */ { int I; printf(”\nPacientes ingresados en estado de gravedad”); for (I=0; I<T; I++) if (A[I].con == 5) printf(”\nNombre: %s\tTeléfono: %s”, A[I].nom, A[I].tel); } Problemas resueltos 317 Problema PR8.4 8 Una empresa de bienes raíces de Lima, Perú, lleva información sobre las propiedades que tiene disponibles tanto para venta como para renta. • • • • • Clave de la propiedad (cadena de caracteres). Superficie cubierta (real). Superficie terreno (real). Características (cadena de caracteres). Ubicación geográfica. • Zona (cadena de caracteres). • Calle (cadena de caracteres). • Colonia (cadena de caracteres). • Precio (real). • Disponibilidad (caracter). Dato: PROPIE[N] (donde es un arreglo unidimensional de tipo estructura 100). PROPIE PROPIEDADES, 1 ≤ N ≤ Escribe un programa en C que realice lo siguiente: a) Un listado de las propiedades disponibles para venta en la zona de Miraflores cuyo valor oscile entre 450,000 y 650,000 nuevos soles. b) Al recibir una zona geográfica y un cierto rango respecto al monto, obtenga un listado de todas las propiedades disponibles para renta. Nota: El listado debe mostrar lo siguiente: clave de la propiedad, superficie cubierta, superficie total, características, calle, colonia y precio. En la siguiente figura se muestra la representación gráfica de la estructura de datos necesaria para resolver este problema. 318 Capítulo 8. Estructuras y uniones PROPIE: Arreglo unidimensional de tipo estructura propiedades Estructura propiedades PROPIE ubi clave scu ste car precio dispo clave scu ste car ubi precio dispo . . . clave scu ste car Propiedad 2 Propiedad 1 ... ubi precio dispo Propiedad 100 ubi zona calle colo Estructura domicilio FIGURA 8.7 Representación gráfica de la estructura de datos necesaria para resolver el problema PR8.4 Programa 8.9 #include <stdio.h> #include <string.h> /* Bienes raíces. El programa maneja información sobre las propiedades que tiene una empresa ➥de bienes raíces de la ciudad de Lima, Perú, tanto para venta como para ➥renta. */ typedef struct { char zona[20]; char calle[20]; char colo[20]; } ubicacion; typedef struct { char clave[5]; float scu; float ste; char car[50]; ubicacion ubi; float precio; char dispo; } propiedades; /* Declaración de la estructura ubicación.*/ /* Colonia. */ /* Declaración de la estructura propiedades.*/ /* Superficie cubierta. */ /* Superficie terreno. */ /* Características. */ /* Observa que este campo es de tipo estructura ubicación. */ /* Disponibilidad. */ void Lectura(propiedades , int); void F1(propiedades *, int); void F2(propiedades *, int); /* Prototipos de funciones. */ Problemas resueltos void main(void) { propiedades PROPIE[100]; /* Se declara un arreglo unidimensional de tipo estructura propiedades. */ int TAM; do { printf(”Ingrese el número de propiedades: ”); scanf(”%d”, &TAM); } while (TAM > 100 | | TAM < 1); /* Se verifica que el tamaño del arreglo sea correcto. */ Lectura(PROPIE, TAM); F1(PROPIE, TAM); F2(PROPIE, TAM); } void Lectura(propiedades A[], int T) /* Esta función se utiliza para leer un arreglo unidimensional de tipo estructura ➥propiedades de T elementos. */ { int I; for (I=0; I<T; I++) { printf(”\n\tIngrese datos de la propiedad %d”, I + 1); printf(”\nClave: ”); fflush(stdin); gets(A[I].clave); printf(”Superficie cubierta: ”); scanf(”%f”, &A[I].scu); printf(”Superficie terreno: ”); scanf(”%f”, &A[I].ste); printf(”Características: ”); fflush(stdin); gets(A[I].car); printf(”\tZona: ”); fflush(stdin); gets(A[I].ubi.zona); printf(”\tCalle: ”); fflush(stdin); gets(A[I].ubi.calle); printf(”\tColonia: ”); fflush(stdin); gets(A[I].ubi.colo); printf(”Precio: ”); scanf(”%f”, &A[I].precio); printf(”Disponibilidad (Venta-V Renta-R): ”); scanf(”%c”, &A[I].dispo); } } 319 8 320 Capítulo 8. Estructuras y uniones void F1(propiedades A[], int T) /* Esta función se utiliza para generar un listado de las propiedades ➥disponibles para venta en la zona de Miraflores, cuyo valor oscila entre ➥450,000 y 650,000 nuevos soles. */ { int I; printf(”\n\t\tListado de Propiedades para Venta en Miraflores”); for (I=0; I<T; I++) if ((A[I].dispo == ‘V’) && (strcmp (A[I].ubi.zona, ”Miraflores”) == 0)) if ((A[I].precio >= 450000) && (A[I].precio <= 650000)) { printf(”\nClave de la propiedad: ”); puts(A[I].clave); printf(”\nSuperficie cubierta: %f”, A[I].scu); printf(”\nSuperficie terreno: %f”, A[I].ste); printf(”\nCaracterísticas: ”); puts(A[I].car); printf(”Calle: ”); puts(A[I].ubi.calle); printf(”Colonia: ”); puts(A[I].ubi.colo); printf(”Precio: %.2f\n”, A[I].precio); } } void F2(propiedades A[], int T) /* Al recibir como datos una zona geográfica de Lima, Perú, y un cierto rango ➥respecto al monto, esta función genera un listado de todas las propiedades ➥disponibles para renta. */ { int I; float li, ls; char zon[20]; printf(”\n\t\tListado de Propiedades para Renta”); printf(”\nIngrese zona geográfica: ”); fflush(stdin); gets(zon); printf(”Ingrese el límite inferior del precio:”); scanf(”%f”, &li); printf(”Ingrese el límite superior del precio:”); scanf(”%f”, &ls); for (I=0; I<T; I++) if ((A[I].dispo == ‘R’) && (strcmp (A[I].ubi.zona, zon) == 0)) if ((A[I].precio >= li) && (A[I].precio <= ls)) { printf(”\nClave de la propiedad: ”); puts(A[I].clave); printf(”\nSuperficie cubierta: %d”, A[I].scu); printf(”\nSuperficie terreno: %d”, A[I].ste); Problemas resueltos printf(”\nCaracterísticas: ”); puts(A[I].car); printf(”Calle: ”); puts(A[I].ubi.calle); printf(”Colonia: ”); puts(A[I].ubi.colo); printf(”Precio: %.2f”, A[I].precio); } } Problema PR8.5 En una empresa de artículos domésticos almacenan la siguiente información de cada uno de sus vendedores: • • • • • • • • • Número vendedor (entero). Nombre y apellido (cadena de caracteres). Ventas del año (arreglo unidimensional de reales). Domicilio (estructura). • Calle y número (cadena de caracteres). • Colonia (cadena de caracteres). • Código Postal (cadena de caracteres). • Ciudad (cadena de caracteres). Salario mensual (real). Clave forma de pago (entero). Forma de pago (unión). • Banco (estructura). Nombre del banco (cadena de caracteres). Número de cuenta (cadena de caracteres). • Ventanilla (caracter). Dato: (donde VENDEDORES es un arreglo unidimensional de tipo estructura VENDEDOR, 1 ≤ N ≤ 100). VENDEDORES[N] Notas: Ventas del año es un arreglo unidimensional de 12 elementos de reales, en el que se almacenan las ventas de los empleados en cada uno de los meses. Forma de pago es una unión en la que se almacena la forma de pago al empleado: cuenta de cheques, nómina o ventanilla. En los dos primeros casos se utiliza una estructura para almacenar el nombre del banco y el número de cuenta del empleado. 321 8 322 Capítulo 8. Estructuras y uniones Escribe un programa en C que realice lo siguiente: a) Obtenga las ventas totales anuales de cada uno de los empleados. b) Incremente 5% el salario a todos aquellos empleados cuyas ventas anuales superaron $1,500,000. c) Liste el número de empleado, el nombre y total de ventas, de todos aquellos vendedores que en el año vendieron menos de $300,000. d) Liste el número de empleado, el nombre del banco y el número de cuenta de todos aquellos empleados a quienes se les deposita en cuenta de cheques. En la siguiente figura se muestra la representación gráfica de la estructura de datos necesaria para resolver este problema. VENDEDORES: Arreglo unidimensional de tipo estructura vendedor Estructura vendedor VENDEDORES num nom ven domi sal cla pago num nom ven domi sal cla pago Vendedor 1 ... Vendedor 2 num nom ven domi sal cla pago ... Vendedor 100 pago venta che noba nucu Unión fpago nomi noba nucu Estructura banco Estructura banco domi cnu col cp ciu Estructura domicilio FIGURA 8.8 Representación gráfica de la estructura de datos necesaria para resolver el problema PR8.5 Programa 8.10 #include <stdio.h> #include <string.h> /* Vendedores. El programa maneja información sobre las ventas que realizan los vendedores de ➥artículos domésticos de una importante empresa de la Ciudad de México. */ typedef struct /* Declaración de la estructura banco. */ { Problemas resueltos char noba[10]; char nucu[10]; } banco; typedef union { banco che; banco nomi; char venta; } fpago; typedef struct { char cnu[20]; char col[20]; char cp[5]; char ciu[15]; } domicilio; typedef struct { int num; char nom[20]; float ven[12]; domicilio domi; float sal; fpago pago; int cla; } vendedor; void void void void void /* Nombre del banco. */ /* Número de cuenta. */ /* Declaración de la union fpago. */ /* Cheque. Campo de tipo estructura banco. */ /* Cómina. Campo de tipo estructura banco. */ /* Ventanilla. */ /* Declaración de la estructura domicilio. */ /* /* /* /* Calle y número. */ Colonia. */ Código Postal. */ Ciudad. */ /* Declaración de la estructura vendedor. */ /* /* /* /* Número de vendedor. */ Nombre del vendedor. */ Ventas del año. Arreglo unidimensional de tipo real. */ domi es de tipo estructura domicilio. */ /* Salario mensual. */ /* pago es de tipo unión fpago. */ /* Clave forma de pago. */ Lectura(vendedor *, int); F1(vendedor *, int); F2(vendedor *, int); F3(vendedor *, int); F4(vendedor *, int); /* Prototipos de funciones. */ void main(void) { vendedor VENDEDORES[100]; /* Declaración del arreglo unidimensional de tipo estructura vendedor. */ int TAM; do { printf(”Ingrese el número de vendedores: ”); scanf(”%d”, &TAM); } while (TAM > 100 | | TAM < 1); /* Se verifica que el número de elementos del arreglo sea correcto. */ Lectura (VENDEDORES, TAM); F1 (VENDEDORES, TAM); F2 (VENDEDORES, TAM); 323 8 324 Capítulo 8. Estructuras y uniones F3 (VENDEDORES, TAM); F4 (VENDEDORES, TAM); printf(”\n\tFIN DEL PROGRAMA”); } void Lectura(vendedor A[], int T) /* Esta función se utiliza para leer un arreglo unidimensional de tipo ➥estructura vendedor de T elementos. */ { int I, J; for (I=0; I<T; I++) { printf(”\n\tIngrese datos del vendedor %d”, I+1); printf(”\nNúmero de vendedor: ”); scanf(”%d”, &A[I].num); printf(”Nombre del vendedor: ”); fflush(stdin); gets(A[I].nom); printf(”Ventas del año: \n”); for (J=0; J<12; J++) { printf(”\tMes %d: ”, J+1); scanf(”%f”, &A[I].ven[J]); } printf(”Domicilio del vendedor: \n”); printf(”\tCalle y número: ”); fflush(stdin); gets(A[I].domi.cnu); printf(”\tColonia: ”); fflush(stdin); gets(A[I].domi.col); printf(”\tCódigo Postal: ”); fflush(stdin); gets(A[I].domi.cp); printf(”\tCiudad: ”); fflush(stdin); gets(A[I].domi.ciu); printf(”Salario del vendedor: ”); scanf(”%f”, &A[I].sal); printf(”Forma de Pago (Banco-1 Nómina-2 Ventanilla-3): ”); scanf(”%d”, &A[I].cla); switch (A[I].cla) { case 1:{ printf(”\tNombre del banco: ”); fflush(stdin); gets(A[I].pago.che.noba); printf(”\tNúmero de cuenta: ”); fflush(stdin); gets(A[I].pago.che.nucu); } Problemas resueltos break; case 2:{ printf(”\tNombre del banco: ”); fflush(stdin); gets(A[I].pago.nomi.noba); printf(”\tNúmero de cuenta: ”); fflush(stdin); gets(A[I].pago.nomi.nucu); } break; case 3: A[I].pago.venta = ‘S’; break; } } } void F1(vendedor A[], int T) /* Esta función se utiliza para generar las ventas totales anuales de cada uno ➥de los vendedores de la empresa. */ { int I, J; float SUM; printf(”\n\t\tVentas Totales de los Vendedores”); for (I=0; I<T; I++) { printf(”\nVendedor: %d”, A[I].num); SUM = 0.0; for (J=0; J<12; J++) SUM += A[I].ven[J]; printf(”\nVentas: %.2f\n”, SUM); } } void F2(vendedor A[], int T) /* Esta función se utiliza para incrementar 5% el salario de todos aquellos ➥vendedores cuyas ventas anuales superaron $1,500,000. */ { int I, J; float SUM; printf (”\n\t\tIncremento a los Vendedores con Ventas > 1,500,000$”); for (I=0; I<T; I++) { SUM = 0.0; for (J=0; J<12; J++) SUM += A[I].ven[J]; if (SUM > 1500000.00) { A[I].sal = A[I].sal * 1.05; printf(”\nNúmero de empleado: %d\nVentas: %.2f\nNuevo salario: %.2f”, A[I].num, SUM, A[I].sal); } 325 8 326 Capítulo 8. Estructuras y uniones } } void F3(vendedor A[], int T) /* Esta función se utiliza para generar un listado de todos aquellos ➥vendedores que en el año vendieron menos de $300,000. */ { int I, J; float SUM; printf(”\n\t\tVendedores con Ventas < 300,000”); for (I=0; I<T; I++) { SUM = 0.0; for (J=0; J<12; J++) SUM += A[I].ven[J]; if (SUM < 300000.00) printf(”\nNúmero de empleado: %d\nNombre: %s\nVentas: %.2f”, A[I].num, A[I].nom, SUM); } } void F4(vendedor A[], int T) /* Esta función se usa para imprimir el número de empleado, el nombre del ➥banco y el número de cuenta de todos aquellos empleados a quienes se les ➥deposita su sueldo en cuenta de cheques. */ { int I; float SUM; printf(”\n\t\tVendedores con Cuenta en el Banco”); for (I=0; I<T; I++) if (A[I].cla == 1) printf(”\nNúmero de vendedor: %d\n Banco: %s\nCuenta: %s”, ➥A[I].num, A[I].pago.che.noba, A[I].pago.che.nucu); } Problemas suplementarios Problema PS8.1 Un importante banco, cuya casa central se encuentra ubicada en Quito, Ecuador, lleva la información de sus clientes en un arreglo unidimensional. Éste se encuentra ordenado en función del número de cuenta. El banco almacena la siguiente información de cada cliente: Problemas suplementos • Número de cuenta (entero extendido). • Nombre del cliente (cadena de caracteres). • Domicilio (estructura). • Calle y número (cadena de caracteres). • Código Postal (cadena de caracteres). • Colonia (cadena de caracteres). • Ciudad (cadena de caracteres). • Teléfono (cadena de caracteres). • Saldo (real). Dato: (donde CLI es un arreglo unidimensional de tipo estructura CLIENTE de elementos, 1 ≤ N ≤ 100). CLI[N] N Escribe un programa en C que realice las siguientes operaciones: a) Depósitos. Al recibir el número de cuenta de un cliente y un monto determinado, debe actualizar el saldo. b) Retiros. Al recibir el número de cuenta de un cliente y un monto determinado por medio de un cheque o un retiro de un cajero, el programa debe actualizar el saldo. El cajero no puede pagar el cheque o autorizar el retiro si el saldo es insuficiente. Nota: El programa debe realizar y validar diferentes transacciones. El fin de datos se expresa al ingresar el número 0. Problema PS8.2 La Federación Mexicana de Fútbol (FEMEXFUT) almacena en un arreglo unidimensional la información de la tabla de posiciones de sus torneos apertura y clausura. Las estadísticas se ordenan lógicamente en función de los puntos. Se almacena la siguiente información de cada equipo: • • • • • • • • • Nombre del equipo (cadena de caracteres). Partidos jugados (entero). Partidos ganados (entero). Partidos empatados (entero). Partidos perdidos (entero). Goles a favor (entero). Goles en contra (entero). Diferencia de goles (entero). Puntos (entero). 327 8 328 Capítulo 8. Estructuras y uniones Dato: FUTBOL[20] (donde FUTBOL es un arreglo unidimensional de tipo estructura EQUIPO). Escribe un programa en C que actualice la información después de cada fecha. El programa recibe la información de la siguiente manera: América 0 – Puebla 2 Cruz Azul 3 – Veracruz 2 Necaxa 2 – Monterrey 3 . . . Después de actualizar la información, el programa debe escribir la nueva tabla de posiciones, ordenada en función de los puntos de cada equipo. Problema PS8.3 En una universidad de Barranquilla, en Colombia, almacenan la información de sus profesores utilizando arreglos unidimensionales. La siguiente información de cada profesor se almacena en una estructura: • • • • • • • Número de empleado (entero). Nombre y apellido (cadena de caracteres). Departamento al que pertenece (cadena de caracteres). Puesto que ocupa (cadena de caracteres). Grado académico (cadena de caracteres). Nacionalidad (cadena de caracteres). Salario (arreglo unidimensional de reales). Dato: (donde EMPLE es un arreglo unidimensional de tipo estructura PROFESOR, 1 ≤ N ≤ 200). EMPLE[N] Nota: Salario es un arreglo unidimensional de tipo real de 12 posiciones que almacena los ingresos mensuales de los profesores. Considera además que en la universidad existen cuatro departamentos: Economía, Derecho, Computación y Administración. Escribe un programa en C que obtenga lo siguiente: a) El nombre, departamento al que pertenece y nacionalidad del profesor que más ganó el año anterior. También debe escribir el ingreso total del profesor. Problemas suplementos b) El monto total pagado a los profesores extranjeros (nacionalidad diferente a Colombia) y el porcentaje respecto al monto total erogado por la universidad. c) El departamento que más egresos —pago de salarios— tuvo el año anterior. Problema PS8.4 En una empresa ubicada en Santiago de Chile almacenan la siguiente información de cada uno de sus empleados: • • • • Número de empleado (entero). Nombre y apellido (cadena de caracteres). Departamento (cadena de caracteres). Domicilio (estructura). • Calle y número (cadena de caracteres). • Colonia (cadena de caracteres). • Código Postal (cadena de caracteres). • Ciudad (cadena de caracteres). • Teléfono (cadena de caracteres). • Salario mensual (real). Dato: (donde EMPLE es un arreglo unidimensional, ordenado en función del número de empleado, de tipo estructura EMPLEADO, 1 ≤ N ≤ 100). EMPLE[N] Escribe un programa en C que contemple los siguientes módulos: a) Altas. Al recibir el número de un empleado, debe darlo de alta incorporando lógicamente todos los datos del empleado. b) Bajas. Al recibir el número de un empleado, debe darlo de baja. c) Listado. Al recibir el nombre de un departamento, debe escribir el número de cada uno de sus empleados, sus nombres y salarios correspondientes. Problema PS8.5 Una tienda especializada en artículos electrónicos vende como máximo 100 productos diferentes. La información de cada producto se almacena en una estructura: • Clave del producto (entero). • Nombre del producto (cadena de caracteres). • Existencia (entero). 329 8 330 Capítulo 8. Estructuras y uniones Dato: TIENDA[N] (donde TIENDA es un arreglo unidimensional de tipo estructura Producto de N elementos, 1 ≤ N ≤ 100). Escribe un programa que actualice la información de acuerdo con las siguientes transacciones: OPE1 CLA1 CAN1 OPE2 CLA2 CAN2 . . . ’0’ 0 0 Donde: es una variable de tipo caracter que representa el tipo de operación que se realiza: ‘C’ compras, ‘V’ ventas. CLAi es una variable de tipo entero que representa la clave del producto. CANi es una variable de tipo entero que significa la cantidad del producto. OPEi Problema PS8.6 En una escuela privada de la Ciudad de México almacenan la información de cada uno de sus alumnos en un arreglo unidimensional de tipo estructura. Se almacena la siguiente información de cada alumno: • Matrícula del alumno (entero). • Nombre y apellido (cadena de caracteres). • Domicilio (estructura). • Calle y número (cadena de caracteres). • Código Postal (entero). • Colonia (cadena de caracteres). • Ciudad (cadena de caracteres). • Teléfono (cadena de caracteres). • Nivel de Estudios (estructura). • Nivel (cadena de caracteres). • Grado (entero). • Salón (cadena de caracteres). • Calificaciones (arreglo unidimensional de estructuras). • Materia (cadena de caracteres). • Promedio (real). Dato: ESCUELA[N] Alumno, 1 (donde ESCUELA es un arreglo unidimensional de tipo estructura ≤ N ≤ 1000). Problemas suplementos 331 Nota: Cada alumno tiene siete materias en sus cursos. Escribe un programa en C que realice lo siguiente: a) Al recibir como dato la matrícula de un alumno, calcule e imprima el promedio general del mismo. b) Al recibir como datos el nivel de estudios (primaria, secundaria o preparatoria), el grado y el salón, liste la matrícula de todos los alumnos, el nombre y su promedio. c) Al recibir como datos el nivel de estudios (primaria, secundaria o preparatoria), el grado y el salón, obtenga el alumno que tiene el mayor promedio. Debe escribir la matrícula, su nombre y el promedio correspondiente. 8 9. Punteros y gestión dinámica de memoria 9.1. ¿Por qué usar estructuras dinámicas? Hasta ahora teníamos una serie de variables que declaramos al principio del programa o de cada función. Estas variables, que reciben el nombre de ESTÁTICAS, tienen un tamaño asignado desde el momento en que se crea el programa. Este tipo de variables son sencillas de usar y rápidas... si sólo vamos a manejar estructuras de datos que no cambien, pero resultan poco eficientes si tenemos estructuras cuyo tamaño no sea siempre el mismo. Es el caso de una agenda: tenemos una serie de fichas, e iremos añadiendo más. Si reservamos espacio para 10, no podremos llegar a añadir la número 11, estamos limitando el máximo. Una solución sería la de trabajar siempre en el disco: no tenemos límite en cuanto a número de fichas, pero es muchísimo más lento. Lo ideal sería aprovechar mejor la memoria que tenemos en el ordenador, para guardar en ella todas las fichas o al menos todas aquellas que quepan en memoria. Una solución “típica” (pero mala) es sobredimensionar: preparar una agenda contando con 1000 fichas, aunque supongamos que no vamos a pasar de 200. Esto tiene varios inconvenientes: se desperdicia memoria, obliga a conocer bien los datos con los que vamos a trabajar, sigue pudiendo verse sobrepasado, etc. La solución suele ser crear estructuras DINÁMICAS, que puedan ir creciendo o disminuyendo según nos interesen. Ejemplos de este tipo de estructuras son: Las pilas. Como una pila de libros: vamos apilando cosas en la cima, o cogiendo de la cima. Las colas. Como las del cine (en teoría): la gente llega por un sitio (la cola) y sale por el opuesto (la cabeza). Las listas, en las que se puede añadir elementos, consultarlos o borrarlos en cualquier posición. Y la cosa se va complicando: en los árboles cada elemento puede tener varios sucesores, etc. Todas estas estructuras tienen en común que, si se programan bien, pueden ir creciendo o decreciendo según haga falta, al contrario que un array, que tiene su tamaño prefijado. En todas ellas, lo que vamos haciendo es reservar un poco de memoria para cada nuevo elemento que nos haga falta, y enlazarlo a los que ya teníamos. Cuando queramos borrar un elemento, enlazamos el anterior a él con el posterior a él (para que no “se rompa” nuestra estructura) y liberamos la memoria que estaba ocupando. Revisión 0.90– Página 127 9.2. ¿Qué son los punteros? Un puntero no es más que una dirección de memoria. Lo que tiene de especial es que normalmente un puntero tendrá un tipo de datos asociado: por ejemplo, un “puntero a entero” será una dirección de memoria en la que habrá almacenado (o podremos almacenar) un número entero. Vamos a ver qué símbolo usamos en C para designar los punteros: int num; int *pos; /* "num" es un número entero */ /* "pos" es un "puntero a entero" (dirección de memoria en la que podremos guardar un entero) */ Es decir, pondremos un asterisco entre el tipo de datos y el nombre de la variable. Ese asterisco puede ir junto a cualquiera de ambos, también es correcto escribir int* pos; Esta nomenclatura ya la habíamos utilizado aun sin saber que era eso de los punteros. Por ejemplo, cuando queremos acceder a un fichero, hacemos FILE* fichero; Antes de entrar en más detalles, y para ver la diferencia entre trabajar con “arrays=” o con punteros, vamos a hacer dos programas que pidan varios números enteros al usuario y muestren su suma. El primero empleará un “array” (una tabla, de tamaño predefinido) y el segundo empleará memoria que reservaremos durante el funcionamiento del programa. El primero podría ser así: /*---------------------------*/ /* Ejemplo en C nº 71: */ /* c071.c */ /* */ /* Sumar varios datos */ /* Version 1: con arrays */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> main() { int datos[100]; int cuantos; int i; long suma=0; /* /* /* /* Preparamos espacio para 100 numeros */ Preguntaremos cuantos desea introducir */ Para bucles */ La suma, claro */ do { printf("Cuantos numeros desea sumar? "); scanf("%d", &cuantos); if (cuantos>100) /* Solo puede ser 100 o menos */ printf("Demasiados. Solo se puede hasta 100."); } while (cuantos>100); /* Si pide demasiado, no le dejamos */ Revisión 0.90– Página 128 /* Pedimos y almacenamos los datos */ for (i=0; i<cuantos; i++) { printf("Introduzca el dato número %d: ", i+1); scanf("%d", &datos[i]); } /* Calculamos la suma */ for (i=0; i<cuantos; i++) suma += datos[i]; printf("Su suma es: %ld\n", suma); } Los más avispados se pueden dar cuenta de que si sólo quiero calcular la suma, lo podría hacer a medida que leo cada dato, no necesitaría almacenar todos. Vamos a suponer que sí necesitamos guardarlos (en muchos casos será verdad, si los cálculos son más complicados). Entonces nos damos cuenta de que lo que hemos estado haciendo hasta ahora no es eficiente: • • • Si quiero sumar 1000 datos, o 500, o 101, no puedo. Nuestro límite previsto era de 100, así que no podemos trabajar con más datos. Si sólo quiero sumar 3 números, desperdicio el espacio de 97 datos que no uso. Y el problema sigue: si en vez de 100 números, reservamos espacio para 5000, es más difícil que nos quedemos cortos pero desperdiciamos muchísima más memoria. La solución es reservar espacio estrictamente para lo que necesitemos, y eso es algo que podríamos hacer así: /*---------------------------*/ /* Ejemplo en C nº 72: */ /* c072.c */ /* */ /* Sumar varios datos */ /* Version 2: con punteros */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #include <stdlib.h> main() { int* datos; /* Necesitaremos espacio para varios numeros */ int cuantos; /* Preguntaremos cuantos desea introducir */ int i; /* Para bucles */ long suma=0; /* La suma, claro */ do { printf("Cuantos numeros desea sumar? "); scanf("%d", &cuantos); datos = (int *) malloc (cuantos * sizeof(int)); if (datos == NULL) /* Si no hay espacio, avisamos */ printf("No caben tantos datos en memoria."); } while (datos == NULL); /* Si pide demasiado, no le dejamos */ /* Pedimos y almacenamos los datos */ for (i=0; i<cuantos; i++) { printf("Introduzca el dato número %d: ", i+1); Revisión 0.90– Página 129 scanf("%d", datos+i); } /* Calculamos la suma */ for (i=0; i<cuantos; i++) suma += *(datos+i); printf("Su suma es: %ld\n", suma); free(datos); } Este fuente es más difícil de leer, pero a cambio es mucho más eficiente: funciona perfectamente si sólo queremos sumar 5 números, pero también si necesitamos sumar 120.000 (y si caben tantos números en la memoria disponible de nuestro equipo, claro). Vamos a ver las diferencias: En primer lugar, lo que antes era int datos[100] que quiere decir “a partir de la posición de memoria que llamaré datos, querré espacio para a guardar 100 números enteros=”,se ha convertido en int* datos que quiere decir “a partir de la posición de memoria que llamaré datos voy a guardar varios números enteros (pero aún no sé cuantos)”. Luego reservamos el espacio exacto que necesitamos, haciendo datos = (int *) malloc (cuantos * sizeof(int)); • • • Esta orden suena complicada, así que vamos a verla por partes: “malloc” es la orden que usaremos para reservar memoria cuando la necesitemos (es la abreviatura de las palabra “memory” y “allocate”). Como parámetro, le indicamos cuanto espacio queremos reservar. Para 100 números enteros, sería “100*sizeof(int)”, es decir, 100 veces el tamaño de un entero. En nuestro caso, no son 100 números, sino el valor de la variable “cuantos”. Por eso hacemos “malloc (cuantos*sizeof(int))”. Para terminar, ese es el espacio que queremos reservar para nuestra variable “datos=”. Y esa variable es de tipo “int *” (un puntero a datos que serán números enteros). Para que todo vaya bien, debemos “convertir” el resultado de “malloc” al tipo de datos correcto, y lo hacemos forzando una conversión como vimos en el apartado 2.4 (operador “molde”), con lo que nuestra orden está completa: datos = (int *) malloc (cuantos * sizeof(int)); • • Si “malloc” nos devuelve NULL como resultado (un “puntero nulo”), quiere decir que no ha encontrado ninguna posición de memoria en la que nos pudiera reservar todo el espacio que le habíamos solicitado. Para usar “malloc” deberemos incluir “stdlib.h” al principio de nuestro fuente. La forma de guardar los datos que teclea el usuario también es distinta. Cuando trabajábamos con un “array”, hacíamos scanf("%d", &datos[i]) (“el dato número i”), pero con punteros usaremos scanf("%d", datos+i) (en la posición datos + i). Ahora ya no necesitamos el símbolo “ampersand” (&). Este símbolo se usa para indicarle a C en qué posición de memoria debe almacenar un dato. Por ejemplo, float x; es una variable que podremos usar para guardar un número real. Si lo hacemos con la orden “scanf”, esta orden no espera que le digamos en qué variable deber guardar el dato, sino en qué posición de memoria. Por eso hacemos scanf("%f", Revisión 0.90– Página 130 &x); En el caso que nos encontramos ahora, int* datos ya se refiere a una posición de memoria (un puntero), por lo que no necesitamos & para usar “scanf”. Finalmente, la forma de acceder a los datos también cambia. Antes leíamos el primer dato como datos[0], el segundo como datos[1], el tercero como datos[2] y así sucesivamente. Ahora usaremos el asterisco (*) para indicar que queremos saber el valor que hay almacenado en una cierta posición: el primer dato será *datos, el segundo *(datos+1), el tercero será *(datos+2) y así en adelante. Por eso, donde antes hacíamos suma += datos[i]; ahora usamos suma += *(datos+i); También aparece otra orden nueva: free. Hasta ahora, teníamos la memoria reservada estáticamente, lo que supone que la usábamos (o la desperdiciábamos) durante todo el tiempo que nuestro programa estuviera funcionando. Pero ahora, igual que reservamos memoria justo en el momento en que la necesitamos, y justo en la cantidad que necesitamos, también podemos volver a dejar disponible esa memoria cuando hayamos terminado de usarla. De eso se encarga la orden “free”, a la que le debemos indicar qué puntero es el que queremos liberar. 9.3. Repasemos con un ejemplo sencillo El ejemplo anterior era “un caso real”. Generalmente, los casos reales son más aplicables que los ejemplos puramente académicos, pero también más difíciles de seguir. Por eso, antes de seguir vamos a ver un ejemplo más sencillo que nos ayude a asentar los conceptos: Reservaremos espacio para un número real de forma estática, y para dos números reales de forma dinámica, daremos valor a dos de ellos, guardaremos su suma en el tercer número y mostraremos en pantalla los resultados. /*---------------------------*/ /* Ejemplo en C nº 73: */ /* c073.c */ /* */ /* Manejo básico de */ /* punteros */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #include <stdlib.h> main() { float n1; float *n2, *suma; /* Primer número, estático */ /* Los otros dos números */ n1 = 5.0; /* Damos un valor prefijado a n1 (real) */ n2 = (float *) malloc (sizeof(float)); /* Reservamos espacio para n2 */ *n2 = 6.7; /* Valor prefijado para n2 (puntero a real) */ suma = (float *) malloc (sizeof(float)); /* Reservamos espacio para suma */ *suma = n1 + *n2; /* Calculamos la suma */ printf("El valor prefijado para la suma era %4.2f\n", *suma); Revisión 0.90– Página 131 printf("Ahora es tu turno: Introduce el primer número "); scanf("%f",&n1); /* Leemos valor para n1 (real) */ printf("Introduce el segundo número "); scanf("%f",n2); /* Valor para n2 (puntero a real) */ *suma = n1 + *n2; /* Calculamos nuevamente la suma */ printf("Ahora la suma es %4.2f\n", *suma); free(n2); free(suma); /* Liberamos la memoria reservada */ } Las diferencias son: • • n1 es un “float”, así que le damos valor normalmente: n1 = 0; Y pedimos su valor con scanf usando & para indicar en qué dirección de memoria se encuentra: scanf("%f", &n1); n2 (y también “suma”) es un “puntero a float”, así que debemos reservarle espacio con “malloc” antes de empezar a usarlo, y liberar con “free” el epacio que ocupaba cuando terminemos de utilizarlo. Para guardar un valor en la dirección de memoria “a la que apunta”, usamos un asterisco: *n2 = 0; Y pedimos su valor con scanf, pero sin necesidad de usar &, porque el puntero ES una dirección de memoria: scanf("%f", n2); (En este ejemplo, no hemos comprobado si el resultado de “malloc” era NULL, porque sólo pedíamos espacio para dos variables, y hemos dado por sentado que sí habría memoria disponible suficiente para almacenarlas; en un caso general, deberemos asegurarnos siempre de que se nos ha concedido ese espacio que hemos pedido). 9.4. Aritmética de punteros Si declaramos una variable como int n=5 y posteriormente hacemos n++, debería resultar claro que lo que ocurre es que aumenta en una unidad el valor de la variable n, pasando a ser 6. Pero ¿qué sucede si hacemos esa misma operación sobre un puntero? int *n; n = (int *) malloc (sizeof(int)); *n = 3; n++; Después de estas líneas de programa, lo que ha ocurrido no es que el contenido de la posición n sea 4. Eso lo conseguiríamos modificando “*n”, de la misma forma que le hemos dado su valor inicial. Es decir, deberíamos usar (*n) ++; En cambio, nosotros hemos aumentado el valor de “n”. Como “n” es un puntero, estamos modificando una dirección de memoria. Por ejemplo, si “n” se refería a la posición de memoria Revisión 0.90– Página 132 número 10.000 de nuestro ordenador, ahora ya no es así, ahora es otra posición de memoria distinta, por ejemplo la 10.001. ¿Y por qué “por ejemplo”? Porque, como ya sabemos, el espacio que ocupa una variable en C depende del sistema operativo. Así, en un sistema operativo de 32 bits, un “int” ocuparía 4 bytes, de modo que la operación n++; haría que pasáramos de mirar la posición 10.000 a la 10.004. Generalmente no es esto lo que querremos, sino modificar el valor que había almacenado en esa posición de memoria. Olvidar ese * que indica que queremos cambiar el dato y no la posición de memoria puede dar lugar a fallos muy difíciles de descubrir (o incluso a que el programa se interrumpa con un aviso de “Violación de segmento” porque estemos accediendo a zonas de memoria que no hemos reservado). 9.5. Punteros y funciones: parámetros por referencia Hasta ahora no sabíamos cómo modificar los parámetros que pasábamos a una función. Recordemos el ejemplo 64: /*---------------------------*/ /* Ejemplo en C nº 64: */ /* c064.c */ /* */ /* Dos variables locales */ /* con el mismo nombre */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> void duplica(int n) { n = n * 2; } main() { int n = 5; printf("n vale %d\n", n); duplica(n); printf("Ahora n vale %d\n", n); } Cuando poníamos este programa en marcha, el valor de n que se mostraba era un 5, porque los cambios que hiciéramos dentro de la función se perdían al salir de ella. Esta forma de trabajar (la única que conocíamos hasta ahora) es lo que se llama “pasar parámetros por valor”. Pero existe una alternativa. Es lo que llamaremos “pasar parámetros por referencia”. Consiste en que el parámetro que nosotros pasamos a la función no es realmente la variable, sino la dirección de memoria en que se encuentra dicha variable (usando &). Dentro de la Revisión 0.90– Página 133 función, modificaremos la información que se encuentra dentro de esa dirección de memoria (usando *), así: /*---------------------------*/ /* Ejemplo en C nº 74: */ /* c074.c */ /* */ /* Modificar el valor de */ /* un parámetro */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> void duplica(int *x) { *x = *x * 2; } main() { int n = 5; printf("n vale %d\n", n); duplica(&n); printf("Ahora n vale %d\n", n); } Esto permite que podamos obtener más de un valor a partir de una función. Por ejemplo, podemos crear una función que intercambie los valores de dos variables enteras así: /*---------------------------*/ /* Ejemplo en C nº 75: */ /* c075.c */ /* */ /* Intercambiar el valor de */ /* dos parámetros */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> void intercambia(int *x, int *y) { int auxiliar; auxiliar = *x; *x = *y; *y = auxiliar ; } main() { int a = 5; int b = 12; intercambia(&a, &b); printf("Ahora a es %d y b es %d\n", a, b); } Este programa escribirá en pantalla que a vale 12 y que b vale 5. Dentro de la función “intercambia”, nos ayudamos de una variable auxiliar para memorizar el valor de x antes de cambiarlo por el valor de y. Revisión 0.90– Página 134 Ejercicio propuesto: Crear una función que calcule las dos soluciones de una ecuación de segundo grado (Ax2 + Bx + C = 0) y devuelva las dos soluciones como parámetros. 9.6. Punteros y arrays En C hay muy poca diferencia “interna” entre un puntero y un array. En muchas ocasiones, podremos declarar un dato como array (una tabla con varios elementos iguales, de tamaño predefinido) y recorrerlo usando punteros. Vamos a ver un ejemplo: /*---------------------------*/ /* Ejemplo en C nº 76: */ /* c076.c */ /* */ /* Arrays y punteros (1) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> main() { int datos[10]; int i; /* Damos valores normalmente */ for (i=0; i<10; i++) datos[i] = i*2; /* Pero los recorremos usando punteros */ for (i=0; i<10; i++) printf ("%d ", *(datos+i)); } Pero también podremos hacer lo contrario: declarar de forma dinámica una variable usando “malloc” y recorrerla como si fuera un array: /*---------------------------*/ /* Ejemplo en C nº 77: */ /* c077.c */ /* */ /* Arrays y punteros (2) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #include <stdlib.h> main() { int *datos; int i; /* Reservamos espacio */ datos = (int *) malloc (20*sizeof(int)); /* Damos valores como puntero */ Revisión 0.90– Página 135 printf("Uso como puntero... "); for (i=0; i<20; i++) *(datos+i) = i*2; /* Y los mostramos */ for (i=0; i<10; i++) printf ("%d ", *(datos+i)); /* Ahora damos valores como array */ printf("\nUso como array... "); for (i=0; i<20; i++) datos[i] = i*3; /* Y los mostramos */ for (i=0; i<10; i++) printf ("%d ", datos[i]); /* Liberamos el espacio */ free(datos); } 9.7. Arrays de punteros Igual que creamos “arrays” para guardar varios datos que sean números enteros o reales, podemos hacerlo con punteros: podemos reservar espacio para “20 punteros a enteros=” haciendo int *datos[20]; Tampoco es algo especialmente frecuente en un caso general, porque si fijamos la cantidad de datos, estamos perdiendo parte de la versatilidad que podríamos tener al usar memoria dinámica. Pero sí es habitual cuando se declaran varias cadenas: char *mensajesError[3]={"Fichero no encontrado", "No se puede escribir", "Fichero sin datos"}; Un ejemplo de su uso sería este: /*---------------------------*/ /* Ejemplo en C nº 78: */ /* c078.c */ /* */ /* Arrays de punteros */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> main() { char *mensajesError[3]={"Fichero no encontrado", "No se puede escribir", "Fichero sin datos"}; printf("El primer mensaje de error es: %s\n", mensajesError[0]); printf("El segundo mensaje de error es: %s\n", mensajesError[1]); printf("El tercer mensaje de error es: %s\n", mensajesError[2]); } Revisión 0.90– Página 136 9.8. Punteros y estructuras Igual que creamos punteros a cualquier tipo de datos básico, le reservamos memoria con “malloc” cuando necesitamos usarlo y lo liberamos con “free” cuando terminamos de utilizarlo, lo mismo podemos hacer si se trata de un tipo de datos no tan sencillo, como un “struct”. Eso sí, la forma de acceder a los datos en un struct cambiará ligeramente. Para un dato que sea un número entero, ya sabemos que lo declararíamos con int *n y cambiaríamos su valor haciendo algo como *n=2, de modo que para un struct podríamos esperar que se hiciera algo como *persona.edad = 20. Pero esa no es la sintaxis correcta: deberemos utilizar el nombre de la variable y el del campo, con una flecha (->) entre medias, así: persona->edad = 20. Vamos a verlo con un ejemplo: /*---------------------------*/ /* Ejemplo en C nº 79: */ /* c079.c */ /* */ /* Punteros y structs */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> main() { /* Primero definimos nuestro tipo de datos */ struct datosPersona { char nombre[30]; char email[25]; int edad; }; /* La primera persona será estática */ struct datosPersona persona1; /* La segunda será dinámica */ struct datosPersona *persona2; /* Damos valores a la persona estática */ strcpy(persona1.nombre, "Juan"); strcpy(persona1.email, "[email protected]"); persona1.edad = 20; /* Ahora a la dinámica */ persona2 = (struct datosPersona*) malloc (sizeof(struct datosPersona)); strcpy(persona2->nombre, "Pedro"); strcpy(persona2->email, "[email protected]"); persona2->edad = 21; /* Mostramos los datos y liberamos la memoria */ printf("Primera persona: %s, %s, con edad %d\n", persona1.nombre, persona1.email, persona1.edad); printf("Segunda persona: %s, %s, con edad %d\n", persona2->nombre, persona2->email, persona2->edad); free(persona2); } Revisión 0.90– Página 137 Ejercicio propuesto: Mejorar la versión de la agenda que leía todos los datos al principio de la ejecución y guardaba todos los datos cuando terminábamos su uso (apartado 6.4). Esta nueva versión deberá estar preparada para manejar hasta 1000 fichas, pero sólo reservará espacio para las que realmente sean necesarias. 9.9. Opciones de la línea de comandos: parámetros de “main” Es muy frecuente que un programa que usamos desde la “línea de comandos” tenga ciertas opciones que le indicamos como argumentos. Por ejemplo, bajo Linux o cualquier otro sistema operativo de la familia Unix, podemos ver la lista detallada de ficheros que terminan en .c haciendo ls –l *.c En este caso, la orden sería “ls=”, y las dos opciones (a rgumentos o parámetros) que le indicamos son “-l” y “*.c”. Pues bien, estas opciones que se le pasan al programa se pueden leer desde C. La forma de hacerlo es con dos parámetros. El primero (que por convenio se suele llamar “argc”) será un número entero que indica cuantos argumentos se han tecleado. El segundo (que se suele llamar “argv”) es una tabla de cadenas de texto, que contiene cada uno de esos argumentos. Por ejemplo, si bajo Windows o MsDos tecleamos la orden “DIR *.EXE P”, tendríamos que: • • • • argc es la cantidad de parámetros, incluyendo el nombre del propio programa (3, en este ejemplo). argv[0] es el nombre del programa (DIR, en este caso). argv[1] es el primer argumento (*.EXE). argv[2] es el segundo argumento (/P). Un fuente en C de ejemplo, que mostrara todos los parámetros que se han tecleado sería: /*---------------------------*/ /* Ejemplo en C nº 80: */ /* c080.c */ /* */ /* Argumentos de "main" */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> int main (int argc, char *argv[]) { int i; printf ("Nombre del programa:\"%s\".\n",argv[0]); if (argc > 0) for (i = 1; i<argc; i++) Revisión 0.90– Página 138 printf("Parámetro %d = %s\n", i, argv[i]); else printf("No se han indicado parámetros.\n"); return 0; } Ejercicios propuestos: • Crear un programa llamado “suma”, que calcule (y muestre) la suma de dos números que se le indiquen como parámetro. Por ejemplo, si se teclea “suma 2 3” deberá responder “5”, y si se teclea “suma 2” deberá responder “no hay suficientes datos. • Crear una calculadora básica, llamada “calcula”, que deberá sumar, restar, multiplicar o dividir los dos números que se le indiquen como parámetros. Ejemplos de su uso sería “calcula 2 + 3” o “calcula 5 * 60”. 9.10. Estructuras dinámicas habituales 1: las listas enlazadas Ahora vamos a ver dos tipos de estructuras totalmente dinámicas (que puedan aumentar o disminuir realmente de tamaño durante la ejecución del programa). Primero veremos las listas, y más adelante los árboles binarios. Hay otras muchas estructuras, pero no son difíciles de desarrollar si se entienden bien estas dos. Ahora “el truco” consistirá en que dentro de cada dato almacenaremos todo lo que nos interesa, pero también una referencia que nos dirá dónde tenemos que ir a buscar el siguiente. Sería algo como: (Posición: 1023). Nombre : 'Nacho Cabanes' Web : 'www.nachocabanes.com' SiguienteDato : 1430 Este dato está almacenado en la posición de memoria número 1023. En esa posición guardamos el nombre y la dirección (o lo que nos interese) de esta persona, pero también una información extra: la siguiente ficha se encuentra en la posición 1430. Así, es muy cómodo recorrer la lista de forma secuencial, porque en todo momento sabemos dónde está almacenado el siguiente dato. Cuando lleguemos a uno para el que no esté definido cual es el siguiente dato, quiere decir que se ha acabado la lista. Por tanto, en cada dato tenemos un enlace con el dato siguiente. Por eso este tipo de estructuras recibe el nombre de “listas simplemente enlazadas=” olistas simples. Si tuvieramos enlaces hacia el dato siguiente y el posterior, se trataría de una “lista doblemente enlazada” o lista doble, que pretende hacer más sencillo el recorrido hacia delante o hacia atrás. Con este tipo de estructuras de información, hemos perdido la ventaja del acceso directo: ya no podemos saltar directamente a la ficha número 500. Pero, por contra, podemos tener tantas Revisión 0.90– Página 139 fichas como la memoria nos permita, y eliminar una (o varias) de ellas cuando queramos, recuperando inmediatamente el espacio que ocupaba. Para añadir una ficha, no tendríamos más que reservar la memoria para ella, y el compilador de C nos diría “le he encontrado sitio en la posición 4079”. Entonces nosotros iríamos a la última ficha y le diríamos “tu siguiente dato va a estar en la posición 4079”. Esa es la “idea intuitiva”. Ahora vamos a concretar cosas en forma de programa en C. Primero veamos cómo sería ahora cada una de nuestras fichas: struct f { /* Estos son los datos que guardamos: */ char nombre[30]; /* Nombre, hasta 30 letras */ char direccion[50]; /* Direccion, hasta 50 */ int edad; /* Edad, un numero < 255 */ struct f* siguiente; /* Y dirección de la siguiente */ }; La diferencia con un “struct” normal está en el campo “siguiente” de nuestro registro, que es el que indica donde se encuentra la ficha que va después de la actual, y por tanto será otro puntero a un registro del mismo tipo, un “struct f *”. Un puntero que “no apunta a ningún sitio” tiene el valor NULL (realmente este identificador es una constante de valor 0), que nos servirá después para comprobar si se trata del final de la lista: todas las fichas “apuntarán” a la siguiente, menos la última, que “no tiene siguiente”, y apuntará a NULL. Entonces la primera ficha definiríamos con struct f *dato1; /* Va a ser un puntero a ficha */ y la comenzaríamos a usar con dato1 = (struct f*) malloc (sizeof(struct f)); /* Reservamos memoria */ strcpy(dato1->nombre, "Pepe"); /* Guardamos el nombre, */ strcpy(dato1->direccion, "Su casa"); /* la dirección */ dato1->edad = 40; /* la edad */ dato1->siguiente = NULL; /* y no hay ninguna más */ (No debería haber anada nuevo: ya sabemos cómo reservar memoria usando “malloc” y como acceder a los campos de una estructura dinámica usando ->). Ahora que ya tenemos una ficha, podríamos añadir otra ficha detrás de ella. guardamos espacio para la nueva ficha, como antes: Primero struct f *dato2; dato2 = (struct f*) malloc (sizeof(struct f)); /* Reservamos memoria */ strcpy(dato2->nombre, "Juan"); /* Guardamos el nombre, */ strcpy(dato2->direccion, "No lo sé"); /* la dirección */ dato2->edad = 35; /* la edad */ dato2->siguiente = NULL; /* y no hay ninguna más */ Revisión 0.90– Página 140 y ahora enlazamos la anterior con ella: dato1->siguiente = dato2; Si quisieramos introducir los datos ordenados alfabéticamente, basta con ir comparando cada nuevo dato con los de la lista, e insertarlo donde corresponda. Por ejemplo, para insertar un nuevo dato entre los dos anteriores, haríamos: struct f *dato3; dato3 = (struct f*) malloc (sizeof(struct f)); /* La tercera */ strcpy(dato3->nombre, "Carlos"); strcpy(dato3->direccion, "Por ahí"); dato3->edad = 14; dato3->siguiente = dato2; /* enlazamos con la siguiente */ dato1->siguiente = dato3; /* y la anterior con ella */ printf("La lista inicialmente es:\n"); La estructura que hemos obtenido es la siguiente Dato1 - Dato3 - Dato2 - NULL Gráficamente: Es decir: cada ficha está enlazada con la siguiente, salvo la última, que no está enlazada con ninguna (apunta a NULL). Si ahora quisiéramos borrar Dato3, tendríamos que seguir dos pasos: 1.- Enlazar Dato1 con Dato2, para no perder información. 2.- Liberar la memoria ocupada por Dato3. Esto, escrito en "C" sería: dato1->siguiente = dato2; free(dato3); /* Borrar dato3: Enlaza Dato1 y Dato2 */ /* Libera lo que ocupó Dato3 */ Hemos empleado tres variables para guardar tres datos. Si tenemos 20 datos, ¿necesitaremos 20 variables? ¿Y 3000 variables para 3000 datos? Sería tremendamente ineficiente, y no tendría mucho sentido. Es de suponer que no sea así. En la práctica, basta con dos variables, que nos indicarán el principio de la lista y la posición actual, o incluso sólo una para el principio de la lista. Por ejemplo, una rutina que muestre en pantalla toda la lista se podría hacer de forma recursiva así: void MuestraLista ( struct f *inicial ) { Revisión 0.90– Página 141 if (inicial!=NULL) { /* Si realmente hay lista */ printf("Nombre: %s\n", inicial->nombre); printf("Dirección: %s\n", inicial->direccion); printf("Edad: %d\n\n", inicial->edad); MuestraLista ( inicial->siguiente ); /* Y mira el siguiente */ } } Lo llamaríamos con "MuestraLista(Dato1)", y a partir de ahí el propio procedimiento se encarga de ir mirando y mostrando los siguientes elementos hasta llegar a NULL, que indica el final. Antes de seguir, vamos a juntar todo esto en un programa, para comprobar que realmente funciona: añadimos los 3 datos y decimos que los muestre desde el primero; luego borramos el del medio y los volvemos a mostrar: /*---------------------------*/ /* Ejemplo en C nº 81: */ /* c081.c */ /* */ /* Primer ejemplo de lista */ /* enlazada simple */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #include <stdlib.h> struct f { /* Estos son los datos que guardamos: */ char nombre[30]; /* Nombre, hasta 30 letras */ char direccion[50]; /* Direccion, hasta 50 */ int edad; /* Edad, un numero < 255 */ struct f* siguiente; /* Y dirección de la siguiente */ }; struct f *dato1; struct f *dato2; struct f *dato3; /* Va a ser un puntero a ficha */ /* Otro puntero a ficha */ /* Y otro más */ void MuestraLista ( struct f *inicial ) { if (inicial!=NULL) { /* Si realmente hay lista */ printf("Nombre: %s\n", inicial->nombre); printf("Dirección: %s\n", inicial->direccion); printf("Edad: %d\n\n", inicial->edad); MuestraLista ( inicial->siguiente ); /* Y mira el siguiente */ } } int main() { dato1 = (struct f*) malloc (sizeof(struct f)); /* Reservamos memoria */ strcpy(dato1->nombre, "Pepe"); /* Guardamos el nombre, */ strcpy(dato1->direccion, "Su casa"); /* la dirección */ dato1->edad = 40; /* la edad */ dato1->siguiente = NULL; /* y no hay ninguna más */ dato2 = (struct f*) malloc (sizeof(struct f)); /* Reservamos memoria */ strcpy(dato2->nombre, "Juan"); /* Guardamos el nombre, */ strcpy(dato2->direccion, "No lo sé"); /* la dirección */ dato2->edad = 35; /* la edad */ dato2->siguiente = NULL; /* y no hay ninguna más */ dato1->siguiente = dato2; /* Enlazamos anterior con ella */ Revisión 0.90– Página 142 dato3 = (struct f*) malloc (sizeof(struct f)); /* La tercera */ strcpy(dato3->nombre, "Carlos"); strcpy(dato3->direccion, "Por ahí"); dato3->edad = 14; dato3->siguiente = dato2; /* enlazamos con la siguiente */ dato1->siguiente = dato3; /* y la anterior con ella */ printf("La lista inicialmente es:\n"); MuestraLista (dato1); dato1->siguiente = dato2; /* Borrar dato3: Enlaza Dato1 y Dato2 */ free(dato3); /* Libera lo que ocupó Dato3 */ printf("Y tras borrar dato3:\n\n"); MuestraLista (dato1); return 0; } Vamos a ver otro ejemplo, que cree una lista de números, y vaya insertando en ella varios valores ordenados. Ahora tendremos una función para mostrar datos, otra para crear la lista insertando el primer dato, y otra que inserte un dato ordenado /*---------------------------*/ /* Ejemplo en C nº 82: */ /* c082.c */ /* */ /* Segundo ejemplo de lista */ /* enlazada (ordenada) */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #include <stdlib.h> struct lista { int numero; struct lista* sig; }; /* Nuestra lista */ /* Solo guarda un numero */ /* Y el puntero al siguiente dato */ struct lista* CrearLista(int valor) { /* Crea la lista, claro */ struct lista* r; /* Variable auxiliar */ r = (struct lista*) malloc (sizeof(struct lista)); /* Reserva memoria */ r->numero = valor; /* Guarda el valor */ r->sig = NULL; /* No hay siguiente */ return r; /* Crea el struct lista* */ }; void MuestraLista ( struct lista *lista if (lista) { printf("%d\n", lista->numero); MuestraLista (lista->sig ); }; }; ) { /* Si realmente hay lista */ /* Escribe el valor */ /* Y mira el siguiente */ void InsertaLista( struct lista **lista, int valor) { struct lista* r; /* Variable auxiliar, para reservar */ struct lista* actual; /* Otra auxiliar, para recorrer */ actual = *lista; if (actual) if (actual->numero < valor) /* Si hay lista */ /* y todavía no es su sitio */ Revisión 0.90– Página 143 InsertaLista(&actual->sig,valor); /* mira la siguiente posición */ else { /* Si hay lista pero ya es su sitio */ r = CrearLista(valor); /* guarda el dato */ r->sig = actual; /* pone la lista a continuac. */ *lista = r; /* Y hace que comience en el nuevo dato */ } else { /* Si no hay lista, hay que crearla */ r = CrearLista(valor); *lista = r; /* y hay que indicar donde debe comenzar */ } } int main() { struct lista* l; l = CrearLista(5); InsertaLista(&l, 3); InsertaLista(&l, 2); InsertaLista(&l, 6); MuestraLista(l); return 0; } /* /* /* /* /* /* /* La lista que crearemos */ Crea una lista e introduce un 5 */ Inserta un 3 */ Inserta un 2 */ Inserta un 6 */ Muestra la lista resultante */ Se acabó */ No es un fuente fácil de leer (en general, no lo serán los que manejen punteros), pero aun así los cambios no son grandes: • Hemos automatizado la inserción de un nuevo dato con la función CrearLista. • Cuando después de ese dato debemos enlazar datos existentes, lo hacemos en dos pasos: r = CrearLista(valor); r->sig = actual; • Si además el dato que modificamos es el primero de toda la lista, deberemos modificar la dirección de comienzo para que lo refleje: r = CrearLista(valor); *lista = r; • Como ya vimos, cuando queremos modificar un dato de tipo “int”, pasamos un parámetro que es de tipo “int *”. En este caso, la función InsertaLista puede tener que modificar un dato que es de tipo “struct lista *”, por lo que el parámetro a modificar deberá ser de tipo “struct lista **”. Al final del tema comentaremos algo más sobre este tipo de expresiones. • Por otra parte, también hemos abreviado un poco alguna expresión: if (lista) es lo mismo que if (lista!=NULL) (recordemos que NULL es una constante que vale 0). Finalmente, hay varios casos particulares que resultan más sencillos que una lista “normal”. Vamos a comentar los más habituales: • Una pila es un caso particular de lista, en la que los elementos siempre se introducen y se sacan por el mismo extremo (se apilan o se desapilan). Es como una pila de libros, en la que para coger el tercero deberemos apartar los dos primeros (excluyendo malabaristas, que los hay). Este tipo de estructura se llama LIFO (Last In, First Out: el último en entrar es el primero en salir). Revisión 0.90– Página 144 • Una cola es otro caso particular, en el que los elementos se introducen por un extremo y se sacan por el otro. Es como se supone que debería ser la cola del cine: los que llegan, se ponen al final, y se atiende primero a los que están al principio. Esta es una estructura FIFO (First In, First Out). Ejercicio propuesto: Las listas simples, tal y como las hemos tratado, tienen la ventaja de que no hay limitaciones tan rígidas en cuanto a tamaño como en las variables estáticas, ni hay por qué saber el número de elementos desde el principio. Pero siempre hay que recorrerlas desde DELANTE hacia ATRAS, lo que puede resultar lento. Una mejora relativamente evidente es lo que se llama una lista doble o lista doblemente enlazada: si guardamos punteros al dato anterior y al siguiente, en vez de sólo al siguiente, podremos avanzar y retroceder con comodidad. Implementa una lista doble enlazada que almacene números enteros. 9.11. Estructuras dinámicas habituales 2: los árboles binarios En las listas, después de cada elemento había otro, el “siguiente” (o ninguno, si habíamos llegado al final). Pero también nos puede interesar tener varias posibilidades después de cada elemento, varios “hijos=”, por ejemplo 3. De cada uno de estos 3 “hijos=” saldrían otros 3, y así sucesivamente. Obtendríamos algo que recuerda a un árbol: un tronco del que nacen 3 ramas, que a su veces se subdividen en otras 3 de menor tamaño, y así sucesivamente hasta llegar a las hojas. Pues eso mismo será un árbol: una estructura dinámica en la que cada nodo (elemento) puede tener más de un “siguiente”. Nos centraremos en los árboles binarios, en los que cada nodo puede tener un hijo izquierdo, un hijo derecho, ambos o ninguno (dos hijos como máximo). Para puntualizar aun más, trataremos los árboles binarios de búsqueda, en los que tenemos prefijado un cierto orden, que nos ayudará a encontrar un cierto dato dentro de un árbol con mucha rapidez. Este "orden prefijado" será el siguiente: para cada nodo tendremos que • la rama de la izquierda contendrá elementos menores que él. • la rama de la derecha contendrá elementos mayores que él. Para que se entienda mejor, vamos a introducir en un árbol binario de búsqueda los datos 5,3,7,2,4,8,9 Primer número: 5 (directo) 5 Segundo número: 3 (menor que 5) Revisión 0.90– Página 145 5 / 3 Tercer número: 7 (mayor que 5) 5 / \ 3 7 Cuarto: 2 (menor que 5, menor que 3) 5 / \ 3 7 / 2 Quinto: 4 (menor que 5, mayor que 3) 5 / \ 3 7 / \ 2 4 Sexto: 8 (mayor que 5, mayor que 7) 5 / \ 3 7 / \ \ 2 4 8 Séptimo: 9 (mayor que 5, mayor que 7, mayor que 8) 5 / \ 3 7 / \ \ 2 4 8 \ 9 ¿Qué ventajas tiene esto? La rapidez: tenemos 7 elementos, lo que en una lista supone que si buscamos un dato que casualmente está al final, haremos 7 comparaciones; en este árbol, tenemos 4 alturas => 4 comparaciones como máximo. Y si además hubiéramos “equilibrado” el árbol (irlo recolocando, de modo que siempre tenga la menor altura posible), serían 3 alturas. Esto es lo que se hace en la práctica cuando en el árbol se va a hacer muchas más lecturas que escrituras: se reordena internamente después de añadir cada nuevo dato, de modo que la altura sea mínima en cada caso. De este modo, el número máximo de comparaciones que tendríamos que hacer sería log2(n), lo que supone que si tenemos 1000 datos, en una lista podríamos llegar a tener que hacer 1000 comparaciones, y en un árbol binario, log2(1000) => 10 comparaciones como máximo. La ganancia en velocidad de búsqueda es clara. No vamos a ver cómo se hace eso de los “equilibrados=”, ue q sería propio de un curso de programación más avanzado, pero sí vamos a empezar a ver rutinas para manejar estos árboles binarios de búsqueda. Revisión 0.90– Página 146 Recordemos que la idea importante es que todo dato menor estará a la izquierda del nodo que miramos, y los datos mayores estarán a su derecha. Ahora la estructura de cada nodo (dato) será: struct arbol { /* El tipo base en sí: */ int dato; /* - un dato (entero) */ struct arbol* hijoIzq; /* - puntero a su hijo izquierdo */ struct arbol* hijoDer; /* - puntero a su hijo derecho */ }; Y las rutinas de inserción, búsqueda, escritura, borrado, etc., podrán ser recursivas. Como primer ejemplo, la de escritura de todo el árbol en orden sería: void Escribir(struct arbol *punt) { if (punt) /* { Escribir(punt->hijoIzq); /* printf("%d ",punt->dato); /* Escribir(punt->hijoDer); /* }; }; Si no hemos llegado a una hoja */ Mira la izqda recursivamente */ Escribe el dato del nodo */ Y luego mira por la derecha */ Quien no se crea que funciona, debería coger lápiz y papel comprobarlo con el árbol que hemos visto antes como ejemplo. Es muy importante que esta función quede clara antes de seguir leyendo, porque los demás serán muy parecidos. La rutina de inserción sería parecida, aunque algo más "pesada" porque tenemos que pasar el puntero por referencia, para que se pueda modificar el puntero: void Insertar(struct arbol **punt, int valor) { struct arbol * actual= *punt; if (actual == NULL) /* Si hemos llegado a una hoja */ { *punt = (struct arbol *) malloc (sizeof(struct arbol)); /* Reservamos memoria */ actual= *punt; actual->dato = valor; /* Guardamos el dato */ actual->hijoIzq = NULL; /* No tiene hijo izquierdo */ actual->hijoDer = NULL; /* Ni derecho */ } else /* Si no es hoja */ if (actual->dato > valor) /* Y encuentra un dato mayor */ Insertar(&actual->hijoIzq, valor); /* Mira por la izquierda */ else /* En caso contrario (menor) */ Insertar(&actual->hijoDer, valor); /* Mira por la derecha */ }; Y finalmente, la de borrado de todo el árbol, casi igual que la de escritura, sólo que en vez de borrar la izquierda, luego el nodo y luego la derecha, borraremos primero las dos ramas y en último lugar el nodo, para evitar incongruencias (intentar borrar el hijo de algo que ya no existe): Revisión 0.90– Página 147 void Borrar(struct arbol *punt) { if (punt) /* { Borrar(punt->hijoIzq); /* Borrar(punt->hijoDer); /* free (punt); /* }; }; Si no hemos llegado a una hoja */ Va a la izqda recursivamente */ Y luego a la derecha */ Finalmente, libera lo que ocupa el nodo */ Vamos a juntar todo esto en un ejemplo "que funcione": /*---------------------------*/ /* Ejemplo en C nº 83: */ /* c083.c */ /* */ /* Arbol binario de */ /* búsqueda */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> #include <stdlib.h> struct arbol { /* El tipo base en sí: */ int dato; /* - un dato (entero) */ struct arbol* hijoIzq; /* - puntero a su hijo izquierdo */ struct arbol* hijoDer; /* - puntero a su hijo derecho */ }; void Escribir(struct arbol *punt) { if (punt) /* { Escribir(punt->hijoIzq); /* printf("%d ",punt->dato); /* Escribir(punt->hijoDer); /* }; }; Si no hemos llegado a una hoja */ Mira la izqda recursivamente */ Escribe el dato del nodo */ Y luego mira por la derecha */ void Insertar(struct arbol **punt, int valor) { struct arbol * actual= *punt; if (actual == NULL) /* Si hemos llegado a una hoja */ { *punt = (struct arbol *) malloc (sizeof(struct arbol)); /* Reservamos memoria */ actual= *punt; actual->dato = valor; /* Guardamos el dato */ actual->hijoIzq = NULL; /* No tiene hijo izquierdo */ actual->hijoDer = NULL; /* Ni derecho */ } else /* Si no es hoja */ if (actual->dato > valor) /* Y encuentra un dato mayor */ Insertar(&actual->hijoIzq, valor); /* Mira por la izquierda */ else /* En caso contrario (menor) */ Insertar(&actual->hijoDer, valor); /* Mira por la derecha */ }; /* Cuerpo del programa */ Revisión 0.90– Página 148 int main() { struct arbol *arbol = NULL; Insertar(&arbol, 5); Insertar(&arbol, 3); Insertar(&arbol, 7); Insertar(&arbol, 2); Insertar(&arbol, 4); Insertar(&arbol, 8); Insertar(&arbol, 9); Escribir(arbol); return 0; } 9.12. Indirección múltiple Lo que estamos haciendo mediante los punteros es algo que técnicamente se conoce como “direccionamiento indirecto”: cuando hacemos int *n, generalmente no nos va a interesar el valor de n, sino que n es una dirección de memoria a la que debemos ir a mirar el valor que buscamos. Pues bien, podemos hacer que ese direccionamiento sea todavía menos directo que en el caso normal: algo como int **n se referiría a que n es una dirección de memoria, en la que a su vez se encuentra como dato otra dirección de memoria, y dentro de esta segunda dirección de memoria es donde se encuentra el dato. Es decir, n sería un “puntero a puntero a entero”. Esta no es una situación habitual, así que no profundizaremos más en ella. Aun así, lo que sí se debe recordar es que char **datos es algo muy parecido a char datos[][], por lo que alguna vez lo veremos indicado como parámetros de una función de una forma o de la otra. 9.13. Un ejemplo: copiador de ficheros en una pasada Como ejemplo de un fuente en el que se apliquen algunas de las ideas más importantes que hemos visto, vamos a crear un copidor de ficheros, que intente copiar todo el fichero de origen en una única pasada: calculará su tamaño, intentará reservar la memoria suficiente para almacenar todo el fichero a la vez, y si esa memoria está disponible, leerá el fichero completo y lo guardará con un nuevo nombre. /*---------------------------*/ /* Ejemplo en C nº 84: */ /* c084.c */ /* */ /* Copiador de ficheros en */ /* una pasada */ /* */ /* Curso de C, */ /* Nacho Cabanes */ /*---------------------------*/ #include <stdio.h> Revisión 0.90– Página 149 FILE *fichOrg, *fichDest; char *buffer; char nombreOrg[80], nombreDest[80]; long longitud; long cantidad; /* Los dos ficheros */ /* El buffer para guardar lo que leo */ /* Los nombres de los ficheros */ /* Tamaño del fichero */ /* El número de bytes leídos */ main() { /* Accedo al fichero de origen */ printf("Introduzca el nombre del fichero Origen: "); scanf("%s",nombreOrg); if ((fichOrg = fopen(nombreOrg, "rb")) == NULL) { printf("No existe el fichero origen!\n"); exit(1); } /* Y ahora al de destino */ printf("Introduzca el nombre del fichero Destino: "); scanf("%s",nombreDest); if ((fichDest = fopen(nombreDest, "wb")) == NULL) { printf("No se ha podido crear el fichero destino!\n"); exit(2); } /* Miro la longitud del fichero de origen */ fseek(fichOrg, 0, SEEK_END); longitud = ftell(fichOrg); fseek(fichOrg, 0, SEEK_SET); /* Reservo espacio para leer todo */ buffer = (char *) malloc (longitud); if (buffer == NULL) { printf("No se ha podido reservar tanto espacio!\n"); exit(3); } /* Leo todos los datos a la vez */ cantidad = fread( buffer, 1, longitud, fichOrg); /* Escribo tantos como haya leído */ fwrite(buffer, 1, cantidad, fichDest); if (cantidad != longitud) printf("Cuidado: no se han leido (ni copiado) todos los datos\n"); /* Cierro los ficheros */ fclose(fichOrg); fclose(fichDest); } Revisión 0.90– Página 150