Descarga la aplicación para disfrutar aún más
Vista previa del material en texto
Ciencias Exactas Ingeniería y Tecnología Primer Semestre Programa de la asignatura: Fundamentos de programación Unidad 3. Funciones y estructuras de Datos Universidad Abierta y a Distancia de México Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 2 Índice Presentación de la unidad ....................................................................................................................... 3 Propósitos ............................................................................................................................................................ 4 Competencia específica ............................................................................................................................. 5 Logros………………………………………………………………………………………………………………………………………………. 5 3.1. Diseño descendente (Top-Down) ................................................................................................ 6 3.2. Definición, declaración e invocación de funciones en C .......................................... 12 3.3. Alcance de las variables ................................................................................................................... 18 3.4. Paso de parámetros .......................................................................................................................... 20 3.4.1. Llamada a una función por valor ........................................................................................... 21 3.4.2. Llamada a una función por referencia ............................................................................. 23 3.5. Estructuras de datos ......................................................................................................................... 24 3.5.1. Arreglos .................................................................................................................................................... 27 3.5.2. Cadenas ............................................................................................................................................... 453 3.5.3. Estructuras ........................................................................................................................................... 49 Cierre de la unidad ...................................................................................................................................... 60 Fuentes de consulta ................................................................................................................................... 61 Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 3 Presentación de la Unidad Hasta esta etapa del curso, has aprendido a utilizar las estructuras de control para diseñar soluciones de problemas simples. También has conocido diferentes formas de representar los datos involucrados en un problema, desde simples hasta estructurados (como arreglos, cadenas y estructuras). Sin embargo, todas las soluciones propuestas constan únicamente de un módulo (función), llamado principal (en C, main); lo cual no es conveniente cuando se trata de problemas complejos que requieran de una serie de tareas para lograr su solución, pues éste sería muy grande y, por lo tanto, difícil de corregir o modificar. Lo recomendable es crear módulos independientes que realicen cada una de las tareas específicas y que en conjunto implementen la solución del problema. Según Levine (2001), se podría definir un módulo como un conjunto de instrucciones que realizan una función específica, que están contenidos en un fragmento del código principal y que tiene la característica de no ser directamente ejecutable, sino que debe ser llamado para efectuar sus funciones. Los buenos hábitos de programación indican que el módulo principal debe ser pequeño y fácil de entender, y estar constituido casi por completo de llamadas a otros módulos de más bajo nivel que realicen efectivamente el trabajo. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 4 En esta unidad se explicarán las funciones, que son el medio por el cual se implementan los módulos en lenguaje C. Se presentará la sintaxis para crear una función y la forma en la que se utilizan. Además, se resolverá un problema utilizando el diseño modular para ejemplificar el tema. Así mismo y con el fin de poder dar solución a problemas cada vez más complejos, conoceremos las estructuras que nos permiten procesar datos relacionados entre sí y que están compuestas de conjuntos de datos básicos. Propósitos En esta unidad: • Identificarás la forma en la que puedes analizar un problema para resolver tareas simples que en su conjunto encuentren la solución total. • Diseñarás algoritmos modulares para solucionar un problema. • Construirás funciones en lenguaje C que realicen tareas específicas. • Determinarás las estructuras de datos involucradas en la solución de un problema. • Diseñarás soluciones empleando arreglos y estructuras (registros). • Utilizarás arreglos y estructuras (registros) en programas escritos en lenguaje C. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 5 Competencia específica Implementar funciones y utilizar estructuras de datos para almacenar y manipular información con el fin de resolver problemas cotidianos a través del desarrollo de programas modulares escritos en lenguaje C. Logros: • Analizar la funcionalidad del diseño descendente. • Diseñar programas modulares y declarar las funciones que se utilizarán en los módulos. • Representar los módulos en diagrama de flujo y pseudocódigo. • Codificar los algoritmos modulares. • Identificar las características de las estructuras de datos y formas en que pueden aplicarse. • Implementar un arreglo en un programa en C para dar respuesta a un caso planteado por el docente en línea. • Implementar una estructura en un programa en C para dar respuesta a un caso específico. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 6 3.1. Diseño descendente (top-down) La ventaja principal de los módulos o subrutinas es que organizan funcionalmente las acciones dentro de un sistema de programación por medio de su capacidad sintetizadora, que permite definir y asignar funciones de acuerdo con un plan maestro sin tener que resolver cada vez localmente problemas que pueden ser atendidos en un solo lugar (Levine, G. 2001 p.233). Esta característica de los módulos da lugar a una metodología de programación conocida como diseño estructurado. Dentro del diseño estructurado existen varias “escuelas”, una de las cuales parte de los módulos de más alto nivel, delegando responsabilidades a los de más abajo cuando así convenga, y sin tener que esperar a que éstos estén terminados (Levine, G. 2001 p.234). A esta metodología se le conoce como diseño descendente (top-down). La metodología del diseño descendente permite establecer una relación entre las etapas de estructuración de manera que se relacionen entre sí a través de entradas y salidas de datos. Es decir, se descompone el problema en etapas de estructuración, módulos o subrutinas jerárquicos, de forma que se pueda considerar cada estructura desde dos puntos de vista: ¿qué hace? y ¿cómo lo hace? Con lo anterior podemos decir que: un módulo se caracteriza por realizar una tarea específica, posee sus propios datos de entrada – llamados parámetros de entrada – y su resultado – llamado salida o valor de retorno –. El diseño de cada módulo debe ser independiente de los otros; si es necesario que exista comunicación entre ellos, ésta únicamente podrá realizarse a través de los parámetros de entrada y del valor de retorno. En este sentido, puede ser visto,por otros módulos, como una caja negra que hacia el exterior sólo muestra qué hace y qué necesita, pero no cómo lo hace. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 7 La creación de un módulo conlleva dos partes: la definición del módulo y la llamada o invocación (ejecución). La primera consta de tres partes: • Definir los parámetros de entrada; • Definir el valor de retorno; • Escribir todas las instrucciones que le permitirán llevar a cabo la tarea, es decir, un algoritmo. La llamada o invocación a un módulo es el proceso de ejecutar el conjunto de instrucciones definidas en el módulo dado un conjunto de entradas específico. La forma general de invocar un módulo es escribiendo su nombre y entre paréntesis los valores de cada uno de los parámetros de entrada, respetando el orden con el que se definió. No existe un método para saber en cuántos módulos se debe dividir un problema, sin embargo, es importante tener en cuenta los siguientes principios del diseño modular: • Las partes altamente relacionadas deben pertenecer a un mismo módulo. • Las partes no relacionadas deben residir en módulos diferentes. Para ejemplificar esta forma de resolver problemas y cómo implementarla, se presenta la siguiente situación: Problema 3.1. Análisis y diseño con el método descendente (Top Down): Realiza el análisis y diseño de un programa que lea las temperaturas promedio mensuales registradas en una ciudad a lo largo de un año y calcule el promedio anual. Además, debe convertir las temperaturas mensuales dadas en grados Celsius a grados Fahrenheit al igual que el promedio. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 8 Empecemos por hacer un bosquejo de los pasos que se tienen que realizar para llegar al resultado deseado. 1. Leer las doce temperaturas promedio mensuales 2. Calcular el promedio anual de las temperaturas 3. Convertir las temperaturas promedio mensuales de Celsius a Fahrenheit 4. Convertir el promedio anual de temperaturas a Fahrenheit 5. Imprimir las temperaturas mensuales en grados Fahrenheit y el promedio anual de las temperaturas, en Celsius y Fahrenheit. Para registrar las temperaturas mensuales proponemos utilizar un arreglo de tamaño 12, y de acuerdo con lo anterior, proponemos tres módulos: El primero encargado de leer las temperaturas mensuales dadas en grados Celsius, este módulo necesita el nombre del arreglo donde va a almacenar los datos. Otro módulo encargado de calcular el promedio de las temperaturas, que recibe como parámetros de entrada el arreglo con las temperaturas mensuales y devuelve el promedio en grados Celsius. El último módulo sólo convierte grados Celsius a grados Fahrenheit. Esta información se resume en el diagrama modular que se muestra a continuación. Diagrama modular del problema 3.1 Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 9 Es mediante un diagrama modular como se representan de manera gráfica los módulos que integran la solución del problema. Y una vez que se han definido, el siguiente paso es diseñar el algoritmo de cada uno de ellos. En primer lugar, se muestra el pseudocódigo de los módulos secundarios: Algoritmo 3.1: Algoritmo del módulo leerTemps(temp[]) Módulo leerTemps(temp[ ]) Inicio Desde mes ← 1 mientras mes ≤ 12, mes ← mes + 1 Imprimir “Proporciona la temperatura del mes”, mes Leer temps[mes- 1] Fin_Desde Fin Observa que el ciclo del módulo leerTemps se inicia con mes igual a 1 y termina cuando mes es 13, se propone así porque se pide la temperatura de los meses 1, 2, 3,.. 12, así que la variable mes guardará el número de mes correspondiente a cada lectura, sin embargo, para almacenar la temperatura en la posición del arreglo indicada se resta uno (temps[mes-1]), así se guarda desde la posición 0 y hasta la posición 11. Algoritmo 3.2: Algoritmo del módulo promTemps(temp[]) En contraste con el ciclo del módulo leerTemp, en este módulo el contador del ciclo se inicia en 0 y termina en 11 (aunque el valor que tiene al finalizar el ciclo Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 10 es 12) pues se utiliza para ir sumando cada uno de los elementos del arreglo, así que la variable mes indicará cada una de las posiciones del arreglo. Módulo promTemps(temp[]) Inicio suma ← 0 Desde mes ← 0 mientras mes ≤ 11, mes ← mes + 1 suma ← suma + temps[mes] Fin_Desde Regresa (suma/12) Fin Algoritmo 3.3: Algoritmos de módulos secundarios del problema 3.1 (pseudocódigo) Módulo aFahrenheit(tempC) Inicio tempF = 9 5⁄ tempC + 32 Regresa tempF Fin A partir de los módulos secundarios presentados se puede plantear el módulo principal. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 11 Algoritmo 3.4: Algoritmo del módulo principal del problema 3.1 (pseudocódigo) A manera de ejercicio, puedes realizar las representaciones en diagrama de flujo del módulo principal, considerando que el símbolo que se utiliza para llamar a módulos que no devuelven ningún valor, como es el caso se leerTemps se muestra a continuación: Módulo Principal Inicio /* Lectura de las temperaturas, invocando al módulo leerTemps */ Imprimir “Ingresa los promedios de temperaturas mensuales” leerTemps(temps[]) /* Cálculo del promedio utilizando el módulo promTemps */ promC ← promTemps(temps[]) /* Conversión del promedio a grados Fahrenheit, invocando al módulo aFahrenheit */ promF ← aFahrenheit(promC) /* Conversión de las temperaturas mensuales a grados Fahrenheit */ Desde mes ← 0 mientras mes ≤ 11, mes ← mes + 1 tempsF[mes] ← aFahrenheit(temps[mes]) Fin_Desde /* Impresión de temperaturas mensuales en Fahrenheit */ Desde mes ← 1 mientras mes ≤ 12, mes ← mes + 1 Imprimir “Temperatura en Fahrenheit del mes”, mes, “ es: ”, tempF[mes-1] Fin_Desde /* Impresión del promedio en grados Celsius y grados Fahrenheit */ Imprimir “ El promedio en grados Celsius es: “, promC Imprimir “El promedio en grados Fahrenheit es: “, promF Fin leerTemps(temp[]) Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 12 Este no es el caso del resto de los módulos secundarios que sí devuelven un valor, así que se utiliza el símbolo de proceso (rectángulo). Con el ejemplo anterior resaltan indudablemente varias ventajas que proporciona un diseño modular: 1. Es posible reutilizar código, ésta indudablemente es una de las más importantes ya que no es necesario escribir el mismo código cada vez que deseamos realizar una tarea semejante. Por ejemplo, el módulo aFahrenheit se utiliza 13 veces (12 en el ciclo y una para convertir el promedio) y sólo bastó definirlo una vez. 2. Fácil detección y corrección de errores, dado que el problema fue divido en pequeños módulos cada uno responsable de una tarea, si en algún momento existiera un error en la solución global, basta identificar cuál de las tareas es la que no se está resolviendo adecuadamente y corregir únicamente aquellos módulos involucrados. 3. Fácil modificación o extensión, si se requiere modificar una tarea del problema o agregar una nueva, no será necesario rediseñar todo el algoritmo, basta con hacer las modificaciones en los módulos pertinentes. Un problema se vuelva más sencillo de solucionar si pensamos de manera modular. Una vez que se ha ilustrado cómo realizar una solución modular, lo siguiente es explicar cómo se codifica cada uno de los módulos, tema que se abordará en las siguientes secciones. 3.2. Definición, declaración e invocación de funciones en C Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 13 En el caso particular de C,un módulo se implementa como una función, recuerda que anteriormente se explicó que un programa en C está integrado de varias funciones, donde una función se define como una porción de código (un conjunto de instrucciones agrupadas por separado) enfocado a realizar una tarea en específico, resaltando la función principal main. Al igual que la función principal, cada una de las funciones existe de manera independiente, tiene sus propias variables, instrucciones y un nombre que las distingue de las otras, además de los parámetros de entrada (también llamados argumentos) que recibe y el tipo de dato que regresa. La forma general para definir una función es: <tipo de dato retorno><identificador de la función>(<parámetros de entrada>) { <instrucciones de la función> return<expresión>; } La expresión <tipo de dato retorno> indica el tipo de dato que devuelve la función (puede ser cualquiera de los tipos básicos), para lo cual se utiliza la palabra reservada return. Cuando la función no devuelve ningún dato, se especifica mediante el tipo void y no debe incluir la palabra reservada return. La expresión <identificador de la función> es el nombre que le vamos a dar a la función y con este nombre vamos a hacer referencia a ella. Se deben seguir las mismas reglas establecidas para los identificadores de variables y se recomienda que sean nemónicos. Después del nombre se coloca, entre paréntesis, la lista de parámetros o la declaración de las variables locales que van a contener los datos de entrada para la función, se debe especificar explícitamente el tipo y nombre de la variable, separados por comas aun cuando sean del mismo tipo. Ejemplo tipo1 param1, tipo2 param2, …, tipoN Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 14 paramN. Finalmente, las instrucciones del cuerpo de la función van entre llaves. La primera línea de la definición de una función se conoce como encabezado de la función (en inglés header). Para ilustrar esto, a continuación se muestra la codificación del módulo aFahrenheit definido en la sección anterior. float aFahrenheit(float tempC) { return ((9.0/5.0)*tempC+32); } Programa 3.1: Codificación del módulo aFahrenheit La llamada o invocación de una función se realiza cuando se requiere que se ejecuten las instrucciones del cuerpo con valores de entrada determinados. Para invocar a una función se tiene que escribir el nombre seguido de los valores que deben coincidir con el orden, número y tipo de los parámetros de entrada dados en la definición de la función y deben estar encerrados entre paréntesis. La sintaxis general para invocar una función ya sea predefinida o definida por el programador, es la siguiente: <identificador de la función>( <lista de valores>); Los parámetros de entrada de una función son valores determinados y pueden ser constantes (8,’a’, 5.2, “cadena constante”), una variable (el valor almacenado pasa a la función) o una expresión. Por ejemplo, la llamada a la función que definimos se muestra en la siguiente instrucción: promF = aFahrenheit(promC); Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 15 Al igual que las variables, una función debe ser declarada antes de utilizarse, es decir, antes de invocarla. La declaración de una función se realiza escribiendo el prototipo de la función. El prototipo de una función coincide con el encabezado de la misma terminando con punto y coma (;) El prototipo de una función sólo indica al compilador que existe y cómo es, más no lo que hace, por lo tanto, la función debe definirse después. Por ejemplo, el prototipo de la función anterior es: float aFahrenheit(float tempC); Cabe señalar que en el prototipo de las funciones se puede omitir los identificadores de los parámetros, más no los tipos. Para ilustrar todo lo anterior, a continuación, se muestra la codificación del algoritmo modular diseñado en la sección anterior. /* Programa: promedioTemp.c * Descripción: Calcula el promedio de las temperaturas promedio * mensuales registrada a lo largo de un año*/ /* Biblioteca */ #include<stdio.h> #include<stdlib.h> /* Variables globales */ int meses = 12; /* Prototipos de las funciones */ /* Función que lee las temperaturas promedio mensuales registradas en un año*/ void leerTemps( float temps[]); /* Función que calcula el promedio de las temperaturas promedio mensuales registradas en un año*/ float promTemps( float temps[]); /* Función que convierte de grados Celsius a grados Fahrenheit */ float aFahrenheit(float tempC); /* Función principal */ main() Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 16 { /* Declaración de variables locales a main */ float temps[12], tempsF[12], promF, promC; int mes; /* Lectura de las temperaturas, invocando a leerTemps*/ printf(“Ingresa los promedios de temperaturas mensuales\n”); leerTemps(temps); /* Cálculo del promedio utilizando la función promTemps */ promC = promTemps(temps); /* Conversión del promedio a grados Fahrenheit, invocando al módulo aFahrenheit */ promF = aFahrenheit(promC); /* Conversión de las temperaturas promedio mensuales a grados Fahrenheit, invocando al módulo aFahrenheit */ for(mes = 0; mes<=11; mes++) tempsF[mes] = aFahrenheit(temps[mes]); /* Impresión de temperaturas promedio mensuales en grados Fahrenheti*/ for(mes = 1; mes<=12; mes++) printf(“\n La temperatura en grados Fahrenheit del mes %d es %.2f: ”, mes, tempsF[mes-1]); /* Impresión del promedio */ printf(“\n\n El promedio anual en grados Celsius es: %.2f ”, promC); printf(“\n El promedio anual en grados Fahrenheit es: %.2f ”, promF); system(“pause”); } /* fin main */ /* Definición de funciones */ void leerTemps (float temps[]) { /* Definición de variables locales a leerTemps */ int mes; Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 17 for(mes = 1; mes<=12; mes++) { printf(“\n Ingresa la temperatura promedio del mes %d: ”, mes); scanf(“%f”, &temps[mes-1]); } } /* fin leerTemps */ float promTemps (float temps[]) { /* Definición de variables locales a promTemps */ int mes; float suma=0; for(mes = 0; mes<=11; mes++) suma = suma + temps[mes]; return (suma/12); } /* fin leerTemps */ float aFahrenheit(float tempC) { return ((9.0/5.0)*tempC+32); } /* fin aFahrenheit */ Programa 3.2: promedioTemp.c Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 18 Figura 3.1: Ejecución del programa promedioTemp.c Con este ejemplo se representa mediante un ejemplo, la estructura de un programa en C incorporando funciones. Se debe poner especial atención en la declaración de funciones para evitar errores de compilación. Considera que el compilador sí distingue las letras mayúsculas de las letras minúsculas; por lo tanto, debes tener especial cuidado al seleccionar los nombres de las funciones. 3.3. Alcance de las variables Cuando nos referimos al alcance de las variables, hacemos referencia al ámbito en que se podrán utilizar dentro del código cuando son declaradas fuera del cuerpo de cualquier función se denominan variables globales y pueden ser utilizadas en cualquier punto del programa a partir del lugar donde fueron declaradas, en cambio cuando son declaradas dentro del cuerpo de alguna Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 19 función se denominan variables locales a ésta, es decir sólo dentro de esa función pueden ser utilizadas. Las variables locales que tienen el mismonombre, pero fueron declaradas en diferentes funciones, no tienen relación, son espacios de memoria totalmente independientes uno de otro. Podemos decir que, son como dos personas diferentes que tienen el mismo nombre. Por otro lado, las variables que se ponen como argumentos en la declaración de una función se consideran locales a estas. Para ejemplificar lo anterior, se muestra el siguiente programa, en el cual se distinguen con diferentes colores el alcance de las variables. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 20 /asterisco porReferencia.casterisco/ #include<stdio.h> #include<stdlib.h> int TAM = 5; void inicializaA(int A[]) { int i; for (i=0; i<TAM; i++) A [i] = 0; } main() { int i; int A[] = {1,1,1,1,1}; printf(“Arreglo antes de la llamada a inicializaA: A = [”); for (i=0; i<TAM; i++) { if(i< TAM -1) printf(“%d ,”,A[i]); else printf(“%d ]\n\n\t”,A[i]); } inicializaA(A); printf(“Arreglo después de la llamada a inicializaA: A = [”); for (i=0; i<TAM; i++) { if(i< TAM -1) printf(“%d ,”,A[i]); else printf(“%d ]\n\n\t”,A[i]); } system(“pause”); } Programa 3.3: porReferencia.c Al utilizar variables globales todas las funciones pueden manipularlas, sus valores permanecen mientras el programa está en ejecución. Sin embargo, su uso puede promover errores de tipo lógico, ya que al modificar el valor de una variable dentro de una función puede afectar el resultado de otra. Variable global Referencia a una variable global Variable local A inicializaA Variable local i Declaración de variables locales a main Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 21 Por ejemplo, supongamos que la función inicializaA() modifica el valor de la variable TAM que almacena el número de elementos del arreglo A, este cambio repercutirá en los ciclos de la función main, los cuales imprimen el arreglo A. En este caso se producirá un error en la ejecución, pues si el valor es menor a cinco no se imprimirán todos los valores y si es mayor entonces habrá elementos indefinidos. Detectar y corregir este tipo de errores puede ser una tarea nada fácil, por lo que no se recomienda el uso de variables globales, lo cual no ocurre si son constantes. Las variables locales por otra parte favorecen mucho la reutilización de código y la modularidad, ya que cada función declara y manipula sus propias variables sin depender de lo que ocurra en otras funciones, esto no significa que al utilizar solamente variables locales no sea posible compartir datos entre las diferentes funciones, esto se hace mediante sus datos de entrada y retorno, una posible desventaja es que el valor de las variables locales se pierde cada vez que la función termina. 3.4. Paso de parámetros El paso de parámetros se refiere a la forma en la que se transfiere como parámetro una variable a una función, esencialmente, si se le otorgan o no permisos para modificar los valores originales. Cuando no se le otorga permisos para que la modifique se dice que es paso de parámetros por valor, pues en este caso sólo se transfiere el valor de la variable, el cual se almacena en una variable local de la función que se está llamando. En cambio, cuando la función puede modificar el valor de la variable se dice que es un paso de parámetro por referencia, pues en este caso no se pasa sólo el valor sino la dirección de memoria que le corresponde a la variable. A continuación, se explica más a detalle los dos tipos de paso de parámetro. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 22 3.4.1. Llamada a una función por valor Cuando se realiza una llamada a una función por valor y en ésta aparece una variable como uno de los argumentos, en realidad no se está pasando la variable sino una copia del valor que ésta contiene, lo cual implica que si dentro de la función se modifica el argumento esto no se ve reflejado en el programa desde el cual se hizo la llamada, pues son localidades de memoria diferentes (recuerda que en cada llamada a una función se crean nuevas variables y se destruyen una vez finalizada la ejecución). /*porValor.c*/ #include<stdio.h> #include<stdlib.h> void inicializa(int a) { a = 0; printf("\nEl valor de la variable local \"a\" es %d\n\n",a); } main() { int a=10; /* Llamada a la función incializa */ printf("\nEl valor de \"a\" antes de la llamada es %i\n\n", a); inicializa(a); printf("\nEl valor de \"a\" después de la llamada es %i\n\n", a); system("pause"); } Programa 3.4: porValor.c Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 23 La ejecución del programa es: Figura 3.2: Ejecución del programa pasoValor.c En la ejecución puedes ver que la variable local a main no se modifica, esto es porque se pasa una copia del valor que almacena cuando se realiza la llamada a la función inicializa(a). Este valor se guarda en un espacio de memoria, también llamado a, que es una variable local a la función y que existe mientras ésta se ejecuta. Observa que el cambio sí se realiza en la variable local de la función inicializa (). 3.4.2. Llamada a una función por referencia La llamada a una función por referencia sí modifica el valor de la variable, pues lo que realmente se está pasando es la dirección de memoria asignada a la variable para que la función pueda modificar el valor. En C los arreglos siempre se pasan por referencia, ya que el nombre del arreglo en realidad almacena la dirección de memoria donde se encuentra almacenado el primer elemento del arreglo. De esta manera cuando se realiza una llamada a una función y se escribe el identificador de un arreglo como parámetro, se está pasando la dirección. Para ejemplificar lo anterior se muestra la ejecución del programa pasoReferencia.c que se presentó anteriormente. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 24 Figura 3.3: Ejecución del programa pasoRefencia.c En la ejecución del programa se observa que después de la llamada a la función cambia el estado del arreglo A. Finalmente, cabe mencionar que para realizar la llamada por referencia de una variable de tipo básico en lenguaje C es necesario pasar la dirección de la variable para lo cual se utiliza el operador & seguido del nombre de la variable (&nombre), como se hace en la función scanf, este operador regresa la dirección de memoria que le corresponde a la variable indicada. 3.5. Estructuras de datos En muchas ocasiones nos vemos en la necesidad de procesar datos que están relacionados entre sí, a este tipo de datos se les conoce como estructurados, ya que están compuestos de un conjunto de datos básicos. A continuación se muestran los tipos de esctructuras de datos. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 25 Esquema de los tipos de estructuras de datos. Por ejemplo pensemos en el nombre completo de una persona, que está compuesto por: nombre, apellido paterno y apellido materno, o bien, en una dirección, formada por nombre de la calle, número y código postal, en este último caso no sólo está formada por varios datos simples sino que además podemos considerarlos de diferentes tipos (Figura 3.). Figura 3.3: Ejemplos de datos Estructurados Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 26 Con este tipo de datos será útil poder hacer referencia a ellos bajo un mismo identificador, y así tratarlos como una unidad. Una estructura de datos es un mecanismo de agrupación de datos que facilitan el manejode datos estructurados y que se caracteriza por la forma en que se acede a sus elementos. Pensemos en otro ejemplo en el cual se tienen datos relacionados, supongamos que nos enfrentamos al siguiente problema: Problema 3.2: Se requiere un programa para llevar el registro de calificaciones de un grupo de diez estudiantes y generar los siguientes reportes: • promedio del grupo, • calificación máxima, • número de estudiantes con calificación superior al promedio del grupo. En este caso, es claro que las calificaciones de cada estudiante se pueden tratar como un dato simple e independiente de los otros, sin embargo, las operaciones que se desean realizar serán las mismas para todo el conjunto de calificaciones, de tal forma que habría que escribir una serie de instrucciones secuenciales para ingresar cada dato y procesarlo. Por ejemplo, para ingresar los datos se requiere leer una por una cada calificación, para obtener el promedio se tendría que hacer la suma de todas las calificaciones y después dividirlas entre 10, hasta aquí no se ha complicado mucho, pero imagina todas las comparaciones que debes hacer para identificar cuál es la calificación mayor. Es claro que este método resulta de lo más ineficiente, y por supuesto si consideramos la posibilidad de modificar el programa para que sea capaz de procesar 60 o más calificaciones, el programa además de extenderse, implica reestructurarlo en su totalidad y que éste sea más complejo que la versión anterior. En cambio, si consideramos a todas las calificaciones como un dato Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 27 estructurado podemos hacer uso de una estructura de dato que nos facilite su manipulación. Existen diferentes tipos de estructuras de datos, cada una caracterizada por la forma de acceso a sus elementos, y el tipo que éstos pueden tener, así tenemos arreglos, listas, colas, tablas, pilas, entre otros. No obstante, para esta unidad nos centraremos sólo en las estructuras de datos que implementa el lenguaje C de forma directa: los arreglos y las estructuras. 3.5.1. Arreglos El uso de arreglos facilita y hace más eficiente la declaración y manipulación de una colección de datos de un mismo tipo que están relacionados entre sí, como es el caso de las calificaciones en el Problema mencionado, ya que todas las calificaciones se pueden considerar como valores enteros. Definición y tipos de arreglos “Un arreglo se define como una colección finita, homogénea y ordenada de elementos. Finita ya que para todo arreglo debe especificarse el número máximo de elementos que podrá contener; la homogeneidad se refiere a que todos los elementos deben ser del mismo tipo, y ordenada porque es posible determinar cuál es el primer elemento, cual el segundo, y así hasta el enésimo elemento” (Cairo Osvaldo, Guardati Buemo Silvia, 1993). A la posición que ocupa un elemento dentro de un arreglo se le denomina formalmente índice y siempre es un número entero. El tamaño o longitud de un arreglo se define como el número de elementos que lo constituyen. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 28 La dimensión de un arreglo está relacionada con el número de índices necesarios para especificar a un elemento en particular. Podemos clasificar a los arreglos de acuerdo a su dimensión como unidimensionales o multidimensionales. Los arreglos unidimensionales (también llamados lineales) reciben su nombre debido a que cualquier elemento es referenciado por un único índice, por ejemplo retomando el caso de las calificaciones del problema mencionado, éstas pueden ser almacenadas en un arreglo unidimensional como el que se muestra en la ¡Error! No se encuentra el origen de la r eferencia.siguiente: Representación gráfica de un arreglo unidimensional En donde el nombre del arreglo es lista y los nombres de las variables donde se almacenan las calificaciones son: lista[0], lista[1], lista[2], lista[3], lista[4], …, lista[9]. En este caso el nombre en común es lista y lo único que cambia para cada elemento es el número que le corresponde a cada variable según la posición que ocupa en la lista. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 29 Observa que un solo índice es suficiente para diferenciar a un elemento de otro. Por otro lado, los arreglos multidimensionales son aquellos para los cuales un solo índice no es suficiente para poder referenciar a un elemento individual, los arreglos bidimensionales son el caso más comúnmente utilizado de arreglos multidimensionales y por tanto los únicos que presentaremos. “Un arreglo bidimensional es un conjunto de datos homogéneos, finito y ordenado, donde se hace referencia a cada elemento por medio de dos índices. El primero de los cuales generalmente se utiliza para indicar renglón y el segundo para indicar columna” (Cairo Osvaldo, Guardati Buemo Silvia, 1993) Un arreglo bidimensional también puede verse como una tabla de valores, de ahí la necesidad de dos índices, como se muestra en la figura siguiente: Representación gráfica de un arreglo bidimensional En ella se muestra un ejemplo gráfico de un arreglo bidimensional, en la cual del lado derecho podemos ver al arreglo como una tabla y del lado izquierdo representado como un arreglo de arreglos. Observa que cada renglón de la tabla es cada uno de los elementos del arreglo de arreglos. Es claro que con un Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 30 solo índice no podríamos identificar a un único elemento ya que solo podríamos ubicar toda una columna o todo un renglón, en cambio la combinación de renglón-columna sí nos identifica a un elemento en particular. Declaración e inicialización En lenguaje C los índices de los arreglos siempre empiezan en cero, es decir, al primer elemento del arreglo le corresponde la posición 0, al segundo la posición 1, al tercero la posición 2 y así sucesivamente hasta llegar al elemento TAM-1, donde TAM corresponde al tamaño del arreglo, en el ejemplo antes mencionado. La declaración de un arreglo consiste en reservar espacio de memoria suficiente para el conjunto de datos homogéneos. La declaración de una variable de tipo arreglo sigue las mismas reglas que las variables simples; con la diferencia de que ahora será necesario especificar el tamaño del arreglo, esto se hace escribiendo el tamaño del arreglo encerrado entre corchetes [ TAM ], después del identificador. La sintaxis para la declaración de un arreglo unidimensional en lenguaje C es la siguiente: <tipo><nombre>[<tamaño>]; Y para un arreglo bidimensional es: <tipo><nombre>[<tamaño1>] [<tamaño2>]; El tipo de dato para los arreglos puede ser de cualquier tipo básico, es decir entero, flotante o caracter (en C int, float, double o char ). De todos ellos, los arreglos de tipo caracter (char) tienen un tratamiento especial, ya que un arreglo de este tipo se considera una cadena. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 31 Debido a la importancia que tienen las cadenas en la programación más adelante los abordaremos de manera particular. Al igual que las variables simples, un arreglo puede inicializarse al momento de ser declarado, para ello se utiliza el operador asignación “=”, pero como un arreglo almacena a un conjunto de datos, es necesario inicializarlo con un conjunto de valores, los cuales se indican mediante llaves {, separando por comas cada uno de los elementos del conjunto de valores iniciales, la sintaxis se muestra a continuación: <tipo><nombre>[<tamaño>]={<valor0>,<valor1>,…,<valorTAM-1>}; La asignación de cada valor inicial se hace consecutivamente desde el elemento 0, por lo tanto, no es posible asignar valores a elementos discontinuos.Veamos como ejemplo la declaración del arreglo unidimensional en la imagen: planteado para las calificaciones del problema mencionado. Inicializando sus elementos en la declaración queda como: int lista[10] = {9,10,8,5,9,6,7,9,4,8}; Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 32 En el caso de los arreglos bidimensionales la sintaxis es la siguiente: <tipo><nombre>[<tamaño1>][<tamaño2>]={ {<valor00>,<valor01>,…,<valor0(TAM21)>}, {<valro10>,<valor11>,…,<valor1(TAM21-1)>},…, {<valor(TAM1-1)0>,<valor (TAM2-1)1>,…,<elem(TAM1-1)(TAM2-1)>} }; Veamos ahora cómo queda la declaración del arreglo bidimensional tabla mostrado en la imagen anterior, inicializando sus valores: int tabla[5][3]={{9,10,8},{5,9,6},{7,9,4},{8,9,6},{7,9,4}}; Aunque también es posible declararlo de la siguiente forma, para fines de comprensión de código se recomienda utilizar la estructura anterior: int tabla[5][3]={9,10,8,5,9,6,7,9,4,8,9,6,7,9,4}; Esto se debe a que como ya se mencionó antes, un arreglo bidimensional se puede ver como un arreglo de arreglos. Por otro lado, en lenguaje C siempre es necesario especificar el tamaño del arreglo al momento de declararlo, sin embargo, esto se puede hacer de forma explícita o implícita. • Explícitamente es cuando se especifica el tamaño dentro de los corchetes que siguen al identificador. int lista[10] = {9,10,8,5,9,6,7,9,4,8}; • De forma implícita se hace cuando el arreglo es inicializado con un conjunto de valores, y se omite el tamaño dentro de los corchetes, entonces el compilador asume el tamaño del arreglo igual al tamaño del conjunto de valores iniciales, de tal forma que la declaración del arreglo lista puede quedar como: int lista[] = {9,10,8,5,9,6,7,9,4,8}; Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 33 Observa que en este caso no se escribe el tamaño dentro de los corchetes, pero como hay 10 elementos en el conjunto de valores iniciales, el compilador de C asume un tamaño 10 para el arreglo. Para los arreglos bidimensionales, sólo es posible especificar una dimensión de forma implícita, el tamaño de renglones siempre debe hacerse de forma explícita. La asignación de un conjunto de valores al arreglo, en una sola operación de asignación, únicamente es posible en su declaración, si se intenta realizar en otro momento, el compilador marcará un error, ya que en cualquier otra parte del programa sólo se podrán asignar valores simples a cada uno de los elementos por separado. Es importante señalar que cuando se desea inicializar el arreglo al declararlo, es posible inicializar sólo algunos de sus elementos, pero en este caso se tendría que especificar explícitamente el tamaño, además se debe recordar que la asignación de valores iniciales es consecutiva desde el elemento 0. Los elementos para los cuales no se indique un valor inicial, automáticamente se inicializan en cero. Por ejemplo, la declaración int lista[10] = {5}; Reservará espacio en memoria para los 10 elementos del arreglo de los cuales al primer elemento se le asignará un 5 y al resto se les asignará un cero. En el caso de los arreglos bidimensionales es posible declarar sólo algunos elementos por renglón, siempre y cuando los elementos sean consecutivos, como en el caso de los unidimensionales. Por ejemplo, la siguiente declaración para el arreglo tabla: int tabla[5][3]={{9,10},{5},{7,9,4},{8,9,}}; Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 34 Daría como resultado la siguiente asignación de valores iniciales [0] [1] [2] [0] 9 10 0 [1] 5 0 0 [2] 7 9 4 [3] 8 9 0 [4] 0 0 0 En el caso de que la declaración fuera: int tabla[5][3]={9,10,5,7,9,4,8,9,}; Entonces la asignación de valores iniciales se haría de la siguiente forma [0] [1] [2] [0] 9 10 5 [1] 7 9 4 [2] 8 9 0 [3] 0 0 0 [4] 0 0 0 Como puedes observar la correcta definición de arreglos te permitirá evitar errores en el manejo de los datos que se almacenen en él. Acceso a los elementos de un arreglo Para referirse a un elemento del arreglo es necesario indicar el nombre del arreglo seguido del índice o índices correspondientes al elemento que deseamos acceder. Para ello se debe seguir la siguiente sintaxis: Elementos de un arreglo unidimensional: <nombre del arreglo>[<índice>]; Elementos de un arreglo bidimensional: <nombre del arreglo>[<índice del renglón>][<índice de columna>]; Observa que para cada índice se utilizan corchetes separados. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 35 Cada elemento del arreglo se puede tratar igual que a cualquier otra variable, es decir, podemos asignarle un valor, incluir en una expresión algebraica o lógica, imprimir en pantalla su valor, asignarle desde el teclado un valor, etc. Veamos algunos ejemplos: Instrucción Descripción tabla[0][2] = 8; Asignar el valor de 8 al tercer elemento del primer renglón del arreglo tabla. printf(“%d”,lista[4]); Imprimir en pantalla el quinto elemento del arreglo lista. scanf(“%d”,&tabla[0][0]); Leer un número entero desde el teclado y lo asigna en la primera posición del arreglo tabla. lista[1]++; Incrementar en uno el valor del segundo elemento del arreglo lista. Ciclos y arreglos Los arreglos y los ciclos están estrechamente relacionados, podríamos afirmar que en cualquier programa que se emplee un arreglo, éste será manipulado por medio de una estructura repetitiva. Anteriormente mencionamos que un arreglo se utiliza cuando queremos almacenar y manipular datos relacionados entre sí y, generalmente, cuando tenemos un arreglo debemos ejecutar el mismo conjunto de operaciones sobre cada uno de sus elementos. En lenguaje C el índice del elemento se puede especificar mediante una expresión, es decir una variable, una constante o como el resultado de una operación, lo que hace posible ir cambiando el índice de un elemento dentro de un ciclo sin tener que escribir una serie de instrucciones secuenciales para realizar la misma operación sobre cada uno de los elementos del arreglo. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 36 La forma general de procesar un arreglo unidimensional por medio de un ciclo se muestra en la siguiente figura: Procesamiento de arreglos unidimensionales mediante ciclos Observa como la variable contador pos del ciclo, también se utiliza como índice para el elemento del arreglo, de tal forma que en cada iteración se haga referencia a un elemento diferente del arreglo. No es difícil intuir que para el caso de los arreglos bidimensionales será necesario no solo un ciclo sino un par de ciclos anidados, ya que tenemos dos dimensiones, cada uno de los ciclos se encargará de manipular a un índice, la estructura general para estos ciclos se muestra en la siguiente figura: Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 37 Procesamiento de arreglos bidimensionales mediante ciclos anidados Como se puede apreciar dos ciclos están anidados cuando uno está dentro del otro, de tal forma que, por cada iteración del externo, el interno completa todo un ciclo. Considerando esto, observa que se toma como contador a la variable i para el ciclo externo, la cual también se utiliza como índice de renglón. Asimismo, se utiliza la variable j como contador para el ciclo interno, además de índice de columna, de esta manera se está recorriendo el arreglo por renglón. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 38 Si analizamos el diagrama de flujo podemos observar que para la primera iteración del ciclo externo, la variable i tiene el valor de 0 mientras que j toma los valores desde 0 hasta TAMC-1 (TAMC es el número total de columnas) de tal forma que el renglón se mantienefijo mientras nos movemos en todas las columnas, en la siguiente iteración cambia el renglón ya que i toma el valor de 1 y recorremos nuevamente todas las columnas, este proceso se repite hasta el valor final de i que es TAMF-1 (TAMF es el número de renglones). Ejemplo Veamos cómo utilizar arreglos en la solución de un problema, resolviendo el ejemplo de las calificaciones, enfocándonos únicamente en la lectura de 10 valores y el cálculo del promedio. Para almacenar las calificaciones se puede utilizar un arreglo unidimensional de tipo entero, será necesario pedir al usuario que ingrese las 10 calificaciones para poder realizar las operaciones necesarias para el cálculo del promedio, es decir, la suma de las misma y la división entre el total de calificaciones, para finalmente imprimir en pantalla el promedio, adicionalmente se imprimirá también la lista de calificaciones ingresadas. No existe ninguna restricción que sea necesaria considerar. Cada uno de los procesos que se van a realizar sobre las calificaciones, es decir leerlas, sumarlas e imprimirlas en pantalla se pueden implementar mediante ciclos, en vez de tener que escribir 10 veces la misma expresión para cada una de las 10 calificaciones. La lectura y la suma de las calificaciones se pueden implementar dentro del mismo ciclo. De esta manera podemos resumir el análisis del problema de la siguiente forma: Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 39 Datos de entada: Calificaciones de los 10 estudiantes (calif [ ]) Salida: Promedio de calificaciones (prom) Método: prom = ∑ 𝑐𝑎𝑙𝑖𝑓[𝑖]9𝑖=0 10 La solución del problema se representa mediante el pseudocódigo que se muestra en el siguiente algoritmo. Inicio suma ← 0 Desde i ← 0 mientras i<10, i ← i+1 Imprimir “Ingresa la calificación” i Leer calif[i] suma← suma+calif[i] Fin Desde prom ← prom/10 Imprimir “Las calificaciones ingresadas fueron:” Desde i ← 0 mientras i<10, i ← i+1 Imprimir “Calificación” i “:” calif[i] Fin Desde Imprimir “Calificación promedio = ” prom Fin Algoritmo Promedio de calificaciones La codificación del algoritmo anterior es la siguiente: /*Directivas de preprocesador*/ #include <stdio.h> #include <stdlib.h> /* Definimos como constante simbólica el tamaño del arreglo*/ #define TAM 10 /* Definición de función principal */ main( ) Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 40 { /*Declaración del arreglo calificaciones*/ int calif[TAM]; double prom = 0; int i; printf("*******************************************\n”); printf(“* El siguiente programa calcula el promedio de *\n"); printf(“* un grupo de diez estudiantes *\n”); printf("********************************************\n”); /*Lectura y suma de las calificaciones*/ for(i=0; i < TAM; i++) { printf("Proporciona la calificación %d: ",i+1); scanf(“%d”, &calif[i]); prom = prom + calif[i]; } /*Cálculo e impresión del promedio*/ prom = prom/TAM; /*Impresión de las calificaciones*/ printf("\nLas calificaciones ingresadas fueron: \n"); for(i=0; i < TAM; i++) printf("\nCalificacion %d: %d",i+1, calif[i]); printf("\n\n\tPromedio = %.2f\n\n", prom); system("pause"); } Codificación promCalificaciones.c Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 41 En la siguiente figura se muestra una ejecución del programa. Ejecución del programa promCalificaciones.c Observa que el tamaño del arreglo se especifica por medio de una constante simbólica, utilizando la directiva #define, esto facilita el cambiar el tamaño del arreglo sin tener que hacer cambios en todo el código. A continuación, se presenta otro ejemplo para ilustrar el uso de arreglos bidimensionales. Ejemplo Se requiere un programa que calcule el determinante de una matriz de 2x2. Considerando la siguiente información. Dada la siguiente matriz: Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 42 Su determinante se define como: ∆=(a00a11)-(a01a10) Análisis del problema: Para este problema se puede utilizar un arreglo bidimensional de 2 renglones y 2 columnas para almacenar los valores de la matriz, los cuales se pueden solicitar al usuario utilizando la estructura de ciclos anidados presentada anteriormente, nuestro dato de salida será el valor del determinante y adicionalmente también se mostrará en pantalla la matriz ingresada. Datos de entada: Elementos de la matriz (A[ ][ ]) Salida: Valor del determinante (det) Método: 𝒅𝒆𝒕 = 𝑨[𝟎][𝟎] ∗ 𝑨[𝟏][𝟏] − 𝑨[𝟎][𝟏] ∗ 𝑨[𝟏][𝟎] La solución del problema se da en el siguiente diagrama de flujo. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 43 Algoritmo Determinante de una matriz 2x2 Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 44 Puedes llevar a cabo la codificación del algoritmo como ejercicio, misma que se muestra a continuación: /* Directivas al preprocesador */ #include <stdlib.h> #include <stdio.h> /* Constantes con el tamaño de la matriz */ #define TAM 2 /* Función principal */ main() { int i, j; float det; float A[TAM][TAM]; /*declaración de la matriz*/ /* Mensaje de bienvenida */ printf("***************************************\n"); printf("* Determinante de una matriz A de 2x2 *\n"); printf("***************************************\n"); /* Lectura de la matriz A */ for(i=0; i<TAM; i++) for(j=0; j<TAM; j++){ printf("\nProporciona el elemento A[%d][%d]: ", i,j); scanf("%f", &A[i][j]); } det = A[0][0]*A[1][1] - A[0][1]*A[1][0]; printf("\nA: \n\t"); /* Impresión de la matriz A */ for(i=0; i<TAM; i++){ for(j=0; j<TAM; j++) printf("%8.2f", A[i][j]); printf("\n\t"); } printf("\n\n\t\tDeterminante = %.2f\n\n", det); system("pause"); } Programa determinantes.c Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 45 En la siguiente figura se muestra una ejecución del programa. Ejecución del programa determinante.c 3.5.2. Cadenas Una cadena es una serie de caracteres tratados como una sola unidad. Una cadena puede incluir letras, dígitos y varios caracteres especiales (Deitel H. M., Deitel P. J., 1995). En algunos lenguajes de programación existe un tipo de dato específico para definir a las cadenas, sin embargo, en lenguaje C las cadenas se implementan por medio de arreglos de tipo caracter ya que no existe un tipo específico para ellas, pero existe todo un conjunto de funciones estándar definidas en la biblioteca string.h mediante las cuales es posible realizar las operaciones más comunes, por ejemplo: copiarlas, concatenarlas, compararlas, entre otras. Además, es posible imprimirlas y leerlas de forma similar que un dato simple. En lenguaje C toda constate de tipo cadena se indica entre comillas dobles, por ejemplo: “Calle 2 #135” Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 46 Una cadena en lenguaje C termina siempre con el caracter nulo ‘\0’ (cuyo valor ascii es cero) que representa el fin de cadena. Al declarar arreglos de tipo char que sean una cadena se pueden inicializar directamente con una constante cadena de la siguiente forma: char cad[50]=”saludo”; Al inicializar una variable cadena de esta manera, se agrega automáticamente el símbolo de caracter nulo para indicar el fin de cadena, es decir, en la posición 6 del arreglo cad se encuentra almacenado el fin de cadena ‘\0’. De forma general,es importante señalar que en un arreglo de tamaño N es posible almacenar correctamente una cadena de máximo N-1 caracteres. De tal forma que en el arreglo cad se puede almacenar una cadena de máximo 49 caracteres. Las cadenas en C pueden ser asignadas a un arreglo de tipo char utilizando la función scanf mediante el especificador de formato %s, por ejemplo, la línea de código: scanf(“%s”, cad); De esta manera se lee desde el teclado una cadena y se guarda en el arreglo cad, sólo que en este caso no se incluye el operador & antes del nombre del arreglo, pues el identificador del arreglo almacena la dirección del primer elemento del mismo. La función gets() también nos permite leer del teclado una cadena y asignarla a un arreglo de tipo char, por ejemplo la instrucción: gets(cad); Lee una cadena desde el teclado y la almacena en el arreglo cad. Una diferencia importante entre usar scanf y gets es que con la primera función la lectura de la cadena se da por terminada cuando el usuario presiona Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 47 la tecla [Enter] o Espacio, mientras que la segunda termina la lectura de la cadena únicamente cuando el usuario presiona la tecla [Enter], tal como se muestra en los siguientes ejemplos: Cadena ingresada Instrucción Contenido del arreglo cad[] “Calle 2 #135” scanf(“%s”, cad); cad[]: C a l l e / 0 / 0 … / 0 “Calle 2 #135” gets(cad); cad[]: C a l l e 2 # 1 3 5 / 0 / 0 Similarmente, para imprimir en pantalla una cadena se puede utilizar la función printf con el especificador de formato %s, o bien, la función puts, nuevamente ambas funciones tienen un comportamiento similar con la única diferencia de que puts incluye siempre un salto de línea al final, esto se ilustra a continuación. Código c Ejecución Impresión de cadena con printf #include <stdio.h> #include <stdlib.h> main(){ char mensaje[30]=”Mar profundo ”; printf(“%s”,mensaje); system(“pause”); } Impresión de cadena con puts #include <stdio.h> #include <stdlib.h> main(){ char mensaje[30]=”Mar profundo ”; puts(mensaje); system(“pause”); } Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 48 Las funciones que nos permiten el manejo de cadenas se encuentran en la biblioteca estándar string.h, para ilustrar algunas se muestra el siguiente programa en leguaje C. Ejemplo 3.3: El programa siguiente verifica si una clave (password) de 8 caracteres alfanuméricos ingresado por el usuario es correcta. /*Directivas de preprocesador*/ #include <stdio.h> #include <stdlib.h> #include <string.h> /* se incluye la biblioteca de cadenas */ main( ) { /* Declaración de variables */ char pwscorrecto[9]=”jk278la0”; /* Clave correcta */ char pwsingresado[9]; /* para leer la clave que ingrese el usuario */ char nombre[10]; /* Para leer el nombre del usuario */ char mensaje[50]=”Bienvenido ”; /* Lectura de datos */ printf(“Nombre: "); gets(nombre); /* Lectura de una cadena con espacios */ printf(“pasword: "); scanf(“%s”,pwsingresado); /* Lectura de una cadena sin espacios*/ if (!strcmp(pwscorrecto,pwsingresado)){ /* comparación de claves, si la función strmp regresa 0 son iguales */ printf(“pasword correcto \n”); strcat(mensaje,nombre); /* pega al final de mensaje el nombre del usuario*/ puts(mensaje); /* impresión de la cadena con salto de línea*/ } else { strcpy(mensaje, “Acceso denegado”); /* copia la cadena acceso denegado al mensaje */ puts(mensaje); /* imprime la cadena*/ } system("pause"); } Programa password.c Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 49 En la siguiente figura se muestran dos ejecuciones del programa a)Clave inválida b)Clave válida Ejecución del programa password.c Con esto concluye la sección de arreglos. Recuerda que un arreglo es un conjunto de datos del mismo tipo. En la siguiente sección verás cómo puedes agrupar datos de diferentes tipos. 3.5.3. Estructuras Las estructuras en lenguaje C al igual que los arreglos, nos permiten tratar a un conjunto de datos bajo un mismo identificador, pero a diferencia de los arreglos, las estructuras son conjuntos de datos contiguos en memoria no homogéneos de tal forma que una estructura puede estar formada por datos de diferentes tipos. Una de las aplicaciones para las estructuras es para la manipulación de registros que se recuperan o se almacenan en una base de datos. Definición, declaración e inicialización de una estructura de datos “Una estructura es una colección de una o más variables, de tipos posiblemente diferentes, agrupadas bajo un nombre para manejo conveniente (en algunos lenguajes también se conocen como registros). Las estructuras permiten tratar a un grupo de variables relacionadas como una unidad, en vez de que se traten en forma separada (Kernighan & Ritchie, 1991, pág. 141)”. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 50 La definición de una estructura en el lenguaje C inicia con la palabra reservada struct seguida del identificador para el tipo de estructura y de un bloque de definiciones de variable que constituyen el conjunto de elementos que forman parte de ella, la sintaxis para definir la estructura es la siguiente: struct<identificadorEstructura> { <tipo1><identificadorE1>; <tipo2><identificadorE2>; <tipoN><identificadorEN>; } Observa que se utiliza la palabra reservada struct para iniciar la definición y que las declaraciones para los elementos de la estructura se encierran entre llaves. Una estructura puede contener a N elementos de diferentes tipos, de cualquiera de los tipos básicos, o incluso un arreglo. Veamos un ejemplo: struct paciente { int nss; /* número de seguro social */ char apellido[50]; char nombre[20]; int edad; float estatura; char sexo; } En este ejemplo se está definiendo la estructura paciente que tiene seis elementos: dos enteros (nss y edad), dos cadenas (apellido y nombre), un flotante (estatura) y un caracter (sexo). Sin embargo, la definición anterior no reserva espacio en memoria para la estructura, más bien define un tipo de dato, por lo tanto, para poder utilizar la estructura, es necesario declarar una Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 51 variable de este tipo, es aquí cuando se reserva espacio en memoria. La sintaxis para hacer esta declaración es la siguiente: struct<identificadorEstructura><identificador_var>; Por ejemplo, la declaración: struct paciente paciente1, paciente2; Declara a las variables paciente1 y paciente2, las cuales son del tipo paciente y por tanto para cada una de ellas se reserva espacio en memoria suficiente para cada uno de sus seis elementos. Otra forma válida de llevar a cabo la declaración es haciéndola seguida a la definición de la estructura, para el ejemplo anterior puede escribirse como sigue: struct paciente { int nss; char apellido[50]; char nombre[20]; int edad; float estatura; char sexo; } paciente1, paciente2; En este caso el identificador para el tipo de estructura puede omitirse, pero entonces la única forma de declarar variables para ese tipo de estructura es en su definición, y no en una declaración por separado, de tal forma que nuestro ejemplo puede quedar como sigue: struct { int nss; Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 52 char apellido[50]; char nombre[20]; intedad; float estatura; char sexo; } paciente1, paciente2; Por otro lado, al igual que los arreglos, también se pueden inicializar los elementos de una estructura en el momento de la declaración de una variable del tipo de la estructura en cuestión, éstos deben estar encerrados entre llaves y separados por comas. La sintaxis general es la siguiente: struct<identificadorEstructura><identificador_var> = { <valorE1>,<valor2>, ,<valorN> }; Por ejemplo: struct paciente paciente1 = {1240, “PicaPiedra”, “Pedro”, 45, 1.80, ‘M’}; Sólo en el momento de la declaración es posible asignar todos los valores de una estructura (al igual que con los arreglos), así que si después se quiere modificar tendrán que hacerse las modificaciones de forma separada en cada uno de sus elementos, como se muestra en el siguiente subtema. Acceso a los elementos de una estructura Para referenciar un elemento de la estructura se utiliza el operador punto “.” con la siguiente sintaxis: <idenficador_var>.<idenficadorEi> Este operador seguido del identificador del elemento, hace referencia al elemento indicado, de tal forma que la sentencia para asignar al paciente1 un nss de 23442145 será: Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 53 paciente1.nss = 23442145; Ahora, si se quiere asignar a la misma variable el nombre “Pablo”, la instrucción sería: paciente1.nombre = “Pablo”; Del mismo modo se realiza la impresión o lectura del elemento de la estructura, sin perder de vista su tipo. Para ilustrar esto se propone el siguiente ejemplo: En el siguiente programa se declara una estructura de tipo perro, que tiene los siguientes elementos: Elemento Tipo Raza char[] Edad int Peso float Posteriormente se declaran dos variables de este tipo, una se inicializa en la declaración y a la otra se le asignan valores desde el teclado, ambos se muestran al final en pantalla. #include <stdio.h> #include <stdlib.h> #include <conio.h> main(){ /* Declaración de la estructura perro*/ struct perro{ char raza[20]; int edad; float peso; } fido, pluto = {"labrador", 7, 20} ; /* inicialización de la variable pluto*/ printf("***********************************"); printf("\n* Comparando perros *"); printf("\n***********************************"); Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 54 printf("\n\nIngresa la raza de fido:"); scanf("%s",&fido.raza); /* lectura de un elemento */ printf("Ingresa la edad de fido en a%cos:", 164); scanf("%d",&fido.edad); /* lectura de un elemento */ printf("Ingresa el peso de fido en kilos de fido:"); scanf("%f",&fido.peso); /* lectura de un elemento */ /* impresión de los elementos de las estructuras */ printf("\nFido es de raza %s, tiene %d a%cos y pesa %.2f kilos\n",fido.raza,fido.edad,164,fido.peso); printf("\nPluto es de raza %s, tiene %d a%cos y pesa %.2f kilos\n",pluto.raza,pluto.edad,164,pluto.peso); /* comparación de los nombres que son cadenas */ if(!strcmp(pluto.raza,fido.raza)) printf("\nPluto Y Fido son de la misma raza \n"); else printf("\nFido y Pluto son de razas distintas\n"); /* comparación de elementos de tipo numérico */ if(pluto.peso > fido.peso) printf("Pluto es m%cs pesado que Fido\n",160); else if(pluto.peso < fido.peso) printf("Fido es m%cs pesado que Pluto\n",160); else printf("Fido y Pluto pesan lo mismo\n"); if(pluto.edad > fido.edad) printf("Pluto es mas viejo que Fido\n"); else if(pluto.edad < fido.edad) printf("Fido es mas pesado que Pluto\n"); else printf("Fido y Pluto tienen la misma edad \n"); getch(); } Codificación programa perros.c Nota: Observa que, en este caso, para poder imprimir la letra ñ se utilizó su código Ascii (164) para lo cual en la cadena de control del printf se escribió %c en donde se desea imprimir la ñ. Este mismo procedimiento se llevó a cabo para acentuar la letra a. En la siguiente figura se muestra una ejecución del programa anterior. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 55 Ejecución del programa perros.c Para concluir con esta sección veamos el siguiente ejemplo donde se utilizan estructuras y arreglos: Se requiere un programa que permita registrar los datos de los perros que ingresan a refugio canino, y poder desplegar los datos de alguno de los perros, a cada perro se le asigna una clave numérica consecutiva. Los datos que se registran del perro son: • la fecha de ingreso (cadena) • nombre (cadena) • raza (cadena) • color (cadena) • edad (entero) • peso (flotante) El refugio tiene capacidad máxima para 100 perros. Para la solución del problema se puede plantear un menú que permita registrar o desplegar los datos de un perro. En este caso tenemos como datos de entrada la opción del menú que elija el usuario. En el caso de que sea un registro se tiene como entrada los datos del perro. Para la opción de despliegue se tendrá que dar, como dato de entrada, la clave asignada al perro y la salida serán los datos correspondientes a la clave. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 56 Para desplegar y repetir el menú se requiere un ciclo y una estructura condicional que maneje las opciones del menú. Para almacenar los datos del perro, se puede utilizar una estructura similar al ejemplo anterior. Mediante un arreglo de estas estructuras estaremos en capacidad para manipular los datos de varios perros, el tamaño de este arreglo tendrá que ser igual a la capacidad del refugio (100 perros), de tal forma que el índice del arreglo que toca a cada perro corresponderá con su clave. Una restricción que hay que tomar en cuenta es que se debe verificar que no se sobrepase la capacidad de atención del albergue (100 perros), tanto al ingresar datos como al recuperarlos, para ello se llevará un contador (c) que actualice el número de perros ingresados. Inicio c ← 0 Hacer Imprimir “Refugio para perros -Ladrido Feliz-” Imprimir “1) Registrar un perro” Imprimir “2) Buscar un perro” Imprimir “3) Salir” Imprimir “Elige una opción:” Leer op Casos para op Caso 1: Si c≥100 entonces Imprimir “El refugio está lleno” Si no Imprimir “Ingresa los datos del perro:” Imprimir “Clave:” c Imprimir “fecha de ingreso[dd/mm/aa]:” Leer perros[c].fecha Imprimir “color:” Leer perros[c].color Imprimir “nombre:” Leer perros[c].nombre Imprimir “raza:” Leer perros[c].raza Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 57 Imprimir “edad:” Leer perros[c].edad Imprimir “peso:” Leer perros[c].peso c ← c+1 Fin si-si no Caso2: Imprimir “Clave:” Leer clave Mientras clave≥100 v clave <0 hacer Imprimir “La clave no es válida, ingresa nuevamente la clave:” Leer clave Fin mientras Imprimir “nombre:”, perros[clave].nombre Imprimir “fecha de ingreso:”, perros[clave].fecha Imprimir “color: ”, perros[clave].color Imprimir “raza: ”, perros[clave].raza Imprimir “edad: ”, perros[clave].edad Imprimir “peso: ”, perros[clave].peso Caso 3: Otro caso: Imprimir ”Opción no válida”Fin casos Mientras op≠3 Fin Hacer-mientras Fin Algoritmo Registro perros Puedes llevar a cabo la prueba de escritorio como ejercicio. La codificación del algoritmo se muestra a continuación. #include <stdio.h> #include <stdlib.h> #include <conio.h> main(){ /* Declaración del arreglo de tipo estructura perro */ struct perro{ char fecha[10]; char raza[30]; char color[50]; char nombre[30]; int edad; float peso; } perros[100]; Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 58 int c=0, op, clave; do{ /* Inicio del ciclo que imprime el menú*/ printf( "\n----------------------------------------\n"); printf( "\nRefugio para perros -Ladrido Feliz- \n" ); printf( "\n----------------------------------------\n"); printf( "1) Registrar un perro \n" ); printf( "2) Buscar un perro \n" ); printf( "3) Salir \n" ); printf( "Elige una opci%cn:",162 ); scanf("%d",&op); switch (op){ case 1: /*Opción Registrar perro */ printf( "\n------------------------------\n"); if(c>=100) /* Verifica si hay espacio */ printf("El refugio esta lleno\n"); else{ /*Si hay espacio pide los datos del perro y Y los guarda en el registro c del arreglo */ printf( "Ingresa los datos del perro:"); printf( "Clave:%.3d\n", c); printf( "fecha de ingreso[dd/mm/aa]: "); scanf( "%s", perros[c].fecha); printf( "nombre: "); fflush(stdin); gets( perros[c].nombre); printf( "color: "); gets( perros[c].color); printf( "raza: "); gets( perros[c].raza); printf( "edad: "); scanf("%d" ,&perros[c].edad); printf( "peso: "); scanf("%f" ,&perros[c].peso); c++; } break; case 2: /* Opción buscar perro */ printf( "\n-------------------------------\n"); printf( "Clave: "); Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 59 scanf("%d",&clave); /* verifica que la clave sea válida */ while(clave>=100 || clave <0){ printf("La calve no es válida, ingresa nuevamente la clave:"); scanf("%d",&clave); } /* Imprime los datos del perro correspondiente a la clave */ printf("nombre:%s\n",perros[clave].nombre); printf( "fecha de ingreso: %s\n", perros[clave].fecha); printf( "color: %s\n", perros[clave].color); printf( "raza: %s\n", perros[clave].raza); printf( "edad: %d a%cos\n", perros[clave]edad,164); printf( "peso: %.2f kilos\n", perros[clave].peso); break; case 3: /* Caso salir, no hace nada */ break; default: /* Caso opción inválida */ printf( "Opcion no válida\n"); } }while (op!=3); /* El ciclo do-while se repite mientras la opción no sea salir (3) */ } Programa registroPerros.c Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 60 Cierre de la unidad Como hemos podido observar la implementación de funciones facilita el desarrollo de módulos que pueden ser reutilizados y actualizado de forma más fácil y eficiente. Al utilizar estructuras de datos para resolver problemas, almacenar y manipular la información en el flujo de datos dentro del desarrollo de programas modulares es una característica que robustece la funcionalidad de los programas desarrollados en lenguaje C. Si requieres profundizar o resolver alguna duda sobre el tema no dudes en consultar a tu Docente en línea. Unidad 3. Funciones y estructuras de datos UNADM | DCEIT | FPR 61 Fuentes de consulta • Alonso Jordá, P., García Granada, F., Onaidía de la Rivadeherrera, E. (1998). Diseño e implementación de programas en lenguaje C. Valencia: Universidad Politécnica de Valencia, Colección Libro Docente. • Cairo Osvaldo, Guardati Buemo Silvia. (2006). Estructura de Datos. México: McGraw-Hill • Deitel H, M., & Deitel P, J. (1995). Cómo programar en C/C++. México: Prentice Hall. • Joyanes, L., & Zohanero, I. (2005). Programación en C. Metodología, algoritmos y estructuras de datos. aspaño: Mc Graw Hill. • Levine G. (2001). Introducción a la Computación y a la Programación Estructurada, México: Mc Graw Hill • Kernighan, B., & Ritchie, D. (1991). El lenguaje de programción C. México: Prentice-Hall Hispanoamericana. • López, L. (2005). Programación estructurada en lenguaje C. México: Alfaomega. • Pérez, H. (1992). Física General (Primera Edición ed.). México: Publicaciones Cultura.
Compartir