TEMA 0: Nociones de Algoritmos 0.1 Visión Histórica. Un algoritmo algoritmo es el el conjunto de operaciones operaciones y procedimientos procedimientos que deben seguirse para resolver un problema. La palabra "algoritmo" deriva del nombre latinizado del gran matemático árabe Mohamed Ibn Moussa Al Kow Rizmi, el cual escribió sobre entre los años 800 y 825 su obra Quitab Al Jabr Al Mugabala, donde se recogía el sistema de numeración hindú y el concepto del cero. Fue Fibonacci, el que tradujo su obra al latín y la inició con las palabras: Algoritmi dicit. 0.2 Diferencia entre el lenguaje algorítmico y el informático. El lenguaje algorítmico es aquel por medio del cual se realiza un análisis previo del problema a resolver y encontrar un método que permita resolverlo. El conjunto de todas las operaciones a realizar, y el orden en el que deben efectuarse, se le denomina algoritmo. El lenguaje informático es aquel por medio del cual dicho algoritmo se codifica a un sistema comprensible por el ordenador o computadora. computadora . Este tipo de lenguaje es más cercano a la máquina que al ser humano y podemos distinguir distintos tipos dependiendo de la proximidad a la maquina. Se denomina lenguaje de alto nivel aquel que es más cercano cercano a la comprensión humana y lenguaje lenguaje de bajo bajo nivel a aquellos que son más comprensibles comprensible s por la máquina. En concreto, nosotros vamos a estudiar un lenguaje en la frontera de uno de bajo nivel. Es por ello que el 'C' es tan potente y rápido, pues las funciones principales representan las funciones más básicas del ordenador. 0.3 Planteamientos de Problemas. Lo que pretende un algoritmo es sintetizar de alguna forma una tarea, cálculo o mecanismo antes de ser transcrito al ordenador. Los pasos que hay que seguir son los siguientes: -
Análisis previo del problema. Primera visión del método de resolución. Descomposición Descomposic ión en módulos. (Programación (Programació n estructurada) estructurada). . Búsqueda de soluciones parciales. Ensamblaje de soluciones finales.
Ejemplo: Calcular las posibles raíces grado: ax^2+bx+c=0
para una ecuación de segundo
+-Algoritmo raíces | | Variables reales a,b,c,x,y | | Escribir "Introduzca los coeficient coeficientes es de mayor a menor grado." | Leer a,b,c | | +-Si sqr(b)>= 4*a*c entonces | | x=(-b+sqrt(b^2-4*a*c))/ x=(-b+sqrt( b^2-4*a*c))/2a 2a | +-Sino | | Escribir "No existen raíces reales." | +-Finsi
| +-Final 0.4 Organigrama Organigramas. s. Un organigrama o diagrama de flujos es una representación representaci ón semigráfica del algoritmo en cuestión. Esto nos facilita la visión descriptiva de la ejecución del programa, así como la generación de la traza del algoritmo. Se denomina traza de un algoritmo a la ejecución manual de d e un programa obteniendo para cada ca da paso un resultado. Símbolos generales: * * * *
Inicio y fin de un programa. Operaciones de I/O , aritméticas y lógico-aritméticas. Decisiones lógicas. Flujo de la ejecución.
0.5 Traza de un Algoritmo. La traza de un Algoritmo se puede definir como la ejecución manual de forma secuencial de las sentencias que lo componen. Así, la traza del siguiente algoritmo es el valor que van adoptando las variables a medida que se va ejecutando un programa. +-Algoritmo Suma | | Variable entera a,b | | Escribir "Indique el primer sumando" | Leer a | Escribir "Indique el segundo sumando" | Leer b | c=a+b | Escribir "El resultado es: ";c | +-Final +----------------------------+ | T R A Z A | +------------+---------------+ | Comentario | Valores | +------------+---------------+ | Leemos a: | a <- 4 | | Leemos b: | b <- 5 | | Calcula c: | c <- a+b <- 9 | | Escribe c: | c <- 9 | +------------+---------------+ La función principal que posee realizar la traza de un algoritmo es la de comprobar que éste funciona correctamente o para realizar la etapa de depuración en la que se intenta corregir errores, simplificar el algoritmo al máximo e incrementar su eficacia y velocidad. TEMA 1: Composición de Algoritmos Los algoritmos están compuestos
por diferentes partes, unas
relacionadas relacionada s íntimamente con las otras, de tal forma que veces la no existencia existencia de una provocaría una confusión confusión en el el Por ello es muy importante el saber las partes principales que se divide los algoritmos y saber cuales son esenciales y no.
muchas mismo. en las cuales
1.1 Cabecera (nombre del Algoritmo). Al comenzar cualquier algoritmo, este debe ser bautizado, de tal forma que tan solo leer la cabecera sepamos cual va a ser su propósito. Ejemplo: +-Algoritmo Factorial <- Cabecera | | Variable entera a,b,c | Escribir "Introduzc "Introduzca a el número a factorizar. factorizar." " | Leer a | b = 1 | +-Para c desde 2 hasta a hacer | ³ b=b*c | À-FinPara | Escribir "El factorial es: ",b +-Final 1.2 Sección de datos (declaración de variables). Esta parte es esencial para cualquier algoritmo que trabaje con variables. En esta sección se va a declarar cuales son las variables con las que vamos a trabajar y cuales son sus tipos. 1.2.1 Tipos. El tipo de una variables define el contenido de ésta, es decir, indica cual va a ser el propósito de la variable. Los tipos de datos estándar son: -
Enteras Reales Carácter Cadena Lógicas
(Su (Su (Su (Su (Su
contenido será un número entero) contenido será un número real) contenido será un carácter alfanumérico alfanumérico) ) contenido será un un conjunto de caracteres) valor indica un hecho cierto o falso)
Existen otros tipos de variables dbyte, dword, etc. Que son variables programación profesional.
tales como byte, word, más encaminadas a la
Las variables se caracterizan caracteriza n pues poseen una jerarquía que viene definida por el número de bytes que se asignan para cada una. Así un carácter posee un longitud de un byte, (donde se almacena un número al que se le ha asociado mediante la norma ASCII) sin embargo un entero posee dos byte. Sería lógico pensar que una variable entera contuviera a un carácter y de hecho esto puede ser así, sin embargo el mezclar tipos de variables es impropio de una programación programació n ordenada y elegante. Es decir, no se debe mezclar tipos de variables a no ser que se produzca a través de una función de conversión de tipos (convertir un entero a una cadena y viceversa).
En el programa anterior se observa la declaración de variables después de la cabecera, que es el orden que debe seguirse en la elaboración de un algoritmo y en un programa informático. 1.2.2 Variables y Constantes. La principal diferencia entre variables y constantes es que las primeras pueden variar a lo largo de la ejecución del programa, mientras que las segundas permanecen constantes siempre. Las constantes se declaran después de la cabecera y antes de las variables. Ejemplo: +-Algoritmo Circunferen Circunferencia cia | | Constante real PI=3.1416 | Variable real r,c | | Escribir "Introduzca el radio de la circunferen circunferencia" cia" | Leer r | c=2*Pi*r | Escribir "Su longitud es: ",c | +-Final Se define Vector como una variable cuya estructura es una sucesión de elementos del mismo tipo. Así una variable de cadena es un vector de caracteres, ya que esta formado por una sucesión de variables del tipo carácter. Así podemos crear vectores de diferentes tipos. Ejemplo: Producto
escalar
de dos vectores en una base ortonormal.
+-Algoritmo Producto_Es Producto_Escalar calar | | Vector entero a[1..3], b[1..3] | Variable entera c | | Escribir "Introduzc "Introduzca a el vector A (x,y,z)" | Leer a[1],a[2], a[1],a[2],a[3] a[3] | Escribir "Introduzc "Introduzca a el vector B (x,y,z)" | Leer b[1],b[2], b[1],b[2],b[3] b[3] | | c=a[1]*b[1]+a[2]*b[2]+a[3]*b[3] | | Escribir "El producto escalar es: ",c | +-Final De igual forma tenemos vectores que se define como:
que
una
matriz
es un vector de
¦ matriz entera Rotacional [1..3,1..3] Hay que decir que el concepto de [Columnas,Filas] [Columnas,F ilas] o [Filas, Columnas] es más bien arbitrario, ya que podemos adoptar el formato que queramos, siempre y cuando lo mantengamos a lo largo del programa.
TEMA 2: Sección del Código Es esta sección, la que se puede considerar como el corazón del algoritmo. En ella van los procedimientos, procedimient os, las funciones y el cuerpo del programa, dentro de los cuales van las sentencias que indican los pasos a realizar por el programa. 2.1 Bloque del Programa. El bloque del programa es como el centro neurálgico del programa, desde él, se controla controla las entradas entradas a los los procedimientos procedimientos y funciones principales (aunque estos pueden llamar a otros procedimientos procedimien tos y funciones secundarios). En el programa anterior se representa como la siguiente parte: +-Algoritmo Circunferencia | | Constante real PI=3.1416 | Variable real r,c | +---->Escribir +---->Escri bir "Introduzca el radio de la circunferen circunferencia" cia" Bloque | | Leer r del | | c=2*Pi*r Programa+---->Escribir Programa+--->Escribir "Su longitud es: ",c | +-Final 2.2 Procedimientos y Funciones. Tanto los procedimientos procedimient os como las funciones son los módulos en los que se puede descomponer un algoritmo. Cada modulo se encarga de realizar una operación independiente independien te de los restantes desde el punto de vista funcional pero este puede estar relacionado con otros procedimientos y funciones para el intercambio de valores de variables. Hay que decir, que cualquier algoritmo se puede transformar en un procedimiento procedimien to para ser utilizado dentro de otro algoritmo mayor. Así, uno de los programas anteriores quedaría de la siguiente forma: +-Procedimiento Factor(a,b) <- Parámetros Formales. | | Parámetro real a,b | Variable entera c | | b = 1 | +-Para c desde 2 hasta a hacer | | b=b*c | +-FinPara | +-FinFactor 2.2.1 Parámetros Formales y Actuales. Como hemos visto, entre los procedimientos procedimien tos (funciones también) y su entorno se producen una relación en base a un intercambio de valores de las variables. Estas variables reciben nombres diferentes según este en el código Padre o en el código Hijo. Vamos a definir como código Padre, aquel desde el cual se llama a una subrutina y, el código Hijo, la subrutina que estamos llamando.
Parámetros Actuales son los que utiliza el programa Padre para relacionarse con una subrutina en concreto, y parámetro Formales son los que posee el programa Hijo y que lo relaciona con el Padre. +-Algoritmo Factorial | | Variable real num,valor | | Escribir "Introduzc "Introduzca a el número a factorizar: factorizar:" " | Leer num | | Factor(num,valor) Factor(num, valor) <- Llama al Procedimie Procedimiento nto Factor | | | | +---+---> Parámetro Actuales. | | Escribir "El factorial es: ",valor | +-Final Obsérvese que los parámetros actuales y formales no tienen por que llamarse de igual forma, sin embargo es condición necesaria que sean del mismo tipo y que estén en el mismo orden. La transmisión de un parámetro como valor y no como variable, hace que el programa Padre no reciba las posibles modificaciones modificaciones que pueda sufrir dicho parámetro dentro del código Hijo. Ejemplo: +-Algoritmo Factorial Constante | | Variable real num,valor | | Escribir "Introduzc "Introduzca a el número a factorizar: factorizar:" " | Leer num | | valor=0 | Factor(num,3) Factor(num,3 ) <- Llama al Procedimient Procedimiento o Factor | | Escribir "El factorial es: ",valor | +-Final Se puede observar claramente que siempre que introduzcamos cualquier número vamos a obtener que el factorial es 0, pues al introducir la variable de forma numérica no se actualiza en el procedimiento. 2.2.2 Variables Globales y Locales. De igual forma que en el apartado anterior diferenciábamos diferenciábam os entre parámetros formales y actuales, ahora vamos a realizar una distinción entre variables v ariables globales y locales. loca les. Podemos Po demos definir variable global como aquella que puede ser utilizada (leída, modificada, etc.) a lo largo de todo el algoritmo principal y también por por cualquiera cualquiera de los subalgoritmos subalgoritmos (entiéndase (entiéndase funciones y procedimientos) que componen el algoritmo en sí. De igual forma, una variable local, es aquella que sólo puede ser referenciada dentro
del subalgoritmo en el cual ha sido declarada. Para simplificar, podemos decir que las variables globales pueden ser referenciadas referenciadas desde cualquier parte del algoritmo mientras que las locales únicamente serán referenciadas dentro del subalgoritmo al que pertenece: +--------------------+------------------------+ | Variables Locales | Subalgoritm Subalgoritmo o propio | | Variables Globales | Cualquier subalgoritmo | +--------------------+------------------------+ TEMA 3: Sentencias Las sentencias o instrucciones principalmente principalmente en tres grandes grupos:
se
pueden
dividir
- Sentencias Simples. - Sentencias Compuestas. - Sentencias de control del flujo del algoritmo. Las Sentencias Simples son del tipo de: - Asignación de Variables y Constantes. - Llamadas a Procedimient Procedimientos os y Funciones, dentro de estas últimas englobamos todas las funciones y procedimiento procedimientos s que conforman la librería general de sentencias que veremos posteriormente. Las Sentencias Compuestas: - Son aquellas Funciones.
que
están
limitas
dentro
de
Procedimie ntos Procedimientos
o
Las Sentencias de Control de Flujo: - Sentencias Reiterativ Reiterativas: as: Mientras, Repetir, Para. - Sentencias Condicionales: Si, Case... of 3.1 Aritméticas y varias funciones. Dentro de la asignaciones de variables juega un gran papel los operadores y funciones matemáticas, tales como: +-------------+-----------------------------------------------+ | + | Suma | | | Resta | | * | Producto | | / | División (devuelve un valor real) | | div | División (devuelve un valor entero) | | mod | Cálculo del módulo aritmético. | | log | Logaritmo en base 10 | | ln | Logaritmo neperiano | | exp | Exponencial de un número | | pow | Potencia de un número | | random | Obtención de un número aleatorio | | abs | Obtenemos el valor absoluto de un número | | sqr | Obtención del cuadrado de un número | | sqrt | Obtención de la raíz cuadrada | | sin,cos,tan | Funciones trigonométric trigonométricas as | | chr/toascii | Obtenemos un carácter a partir de un número | | ord | Obtenemos el número correspondien correspondiente te al código |
| | ASCII | +-------------+-----------------------------------------------+ Lógicamente, existen más adelante.
más funciones aritméticas
que veremos
3.2 Sentencias de Control. Como ya dijimos en la introducción del tema 3, las sentencias de control son aquellas que interrumpen la ejecución secuencial de las instrucciones instruccione s de un algoritmo, permitiendo la generación de reiteraciones. 3.2.1 Condicionales (Si y Case). La función SI, viene acompañada por una serie de elementos que son los operadores relacionales y operadores booleanos. Los operadores relacionales son los siguientes: +----+-----------------+ | = | Igual a | | < | menor que | | > | mayor que | | <= | menor o igual a | | >= | mayor o igual a | | <> | distinto de | +----+-----------------+ Estos operadores nos van a servir generalmente para comparar y comprobar los valores que toman las variables a lo largo del algoritmo, y dependiendo de los resultados, realizar una cosa u otra. Operadores booleanos: Los operadores booleanos compuestas. (AND, OR, XOR, NOT) Ejemplo: ¨Es cierto?
la variable b
nos permiten
mayor que
12
generar condiciones
y la variable
d igual a
Operador AND: (debe cumplirse las dos condiciones) +--------------------------+ | true AND true -> true | | true AND false -> false | | false AND true -> false | | false AND false -> false | +--------------------------+ Operador OR: (debe cumplirse una de las
dos condiciones)
+-------------------------+ | true OR true -> true | | true OR false -> true | | false OR true -> true | | false OR false -> false | +-------------------------+ Operador XOR: (se cumple cuando las dos condiciones son distintas)
+--------------------------+ | true XOR true -> false | | true XOR false -> true | | false XOR true -> true | | false XOR false -> false | +--------------------------+ Operador NOT: (niega el resultado de una condicion) +--------------------+ | NOT true -> false | | NOT false -> true | +--------------------+ - La sentencias SI puede tener las siguientes estructuras: +-Si (condición) entonces | ... +-Sino | ... +-Finsi También puede aparecer en estructuras más complejas: +-Si (condición1) entonces | +-Si (condición2) entonces | | ... | +-Finsi +-Sino | ... | +-Si (condición3) entonces | | ... | +-Sino | | ... | +-Finsi +-Finsi A este tipo de estructuras encadenamiento encadenamiento de sentencias SI".
se le
denomina "anidamiento o
- En cuanto a la sentencia EN CASO hay que decir, que se trata de una simplificación de sentencias SI anidadas. De esta forma, sea una variable A tenemos la siguiente estructura de Si encadenados. +-Si (condición1) entonces | acción1 +-Sino Si (condición2) entonces | acción2 +-Sino Si (condición3) entonces | acción3 +-Sino | acción4 +-Finsi Con una estructura del tipo resuelto de la siguiente forma:
EN CASO, tendríamos el problema problema
+-En Caso de Variable | condición1: Acción1 | condición2: Acción2
| condición3: +-En Otro Caso | Acción4 +-Fincaso
Acción3
3.2.2 Bucles Mientras, Repetir y Para. Las iteraciones son otro tipo de sentencias de control. Las que veremos son las siguientes: Mientras, Repetir y Para. Siendo este último uno de los más usados. La utilización de un bucle en un programa permite la posibilidad de realizar iteraciones de secciones de código, evitando así andar con condiciones, condiciones , etiquetas y la sentencia GOTO. Esta última sentencia GOTO, hay que evitarla en por todos los medios. Cualquier programa puede ser realizado sin tener que utilizar dicha sentencia; ya que su uso crea confusión a la hora de seguir la ejecución secuencial de las sentencias de un algoritmo. 3.2.2.1 Bucle Mientras. El bucle Mientras se caracteriza por ser utilizado cuando no conocemos el número de iteraciones con antelación. Es por ello que nos ayudamos de una comprobación comprobació n o condición para la entrada/salida entrada/sali da del mismo antes antes de realizar realizar la ejecución de la sección sección del código código a repetir; esto último nos posibilita el caso de no efectuar ninguna iteración (iteración=0). (iteración=0). +-Algoritmo Multiplicar (mult1,mult2,resul) | Parámetros reales mult1,mult2 mult1,mult2,resul ,resul | | resul=0 | +-Mientras mult2>0 hacer | | resul=resul+mult1 resul=resul +mult1 | | mult2=mult2-1 mult2=mult2 -1 | +-Finmientra +-Finmientras s +-Final 3.2.2.2 Bucle Repetir. El bucle Repetir se caracteriza porque al igual que el anterior no sabemos el número de iteraciones que debemos realizar, es por ello que se apoya en condiciones para salir del mismo. Al contrario que el anterior, la comprobación va al final del bucle, de esta forma, como mínimo siempre se produce una iteración. Veamos el algoritmo anterior utilizando el bucle repetir. +-Algoritmo Multiplicar (mult1,mult2,resul) | Parámetros reales mult1,mult2 mult1,mult2,resul ,resul | | resul=0 | +-Repetir | | resul=resul+mult1 resul=resul +mult1 | | mult2=mult2-1 mult2=mult2 -1 | +-Hasta que mult2<=0 (o también hasta que not (mult2 >0) ) +-Final Una forma muy sencilla para pasar un bucle Mientras a Repetir, es hallando la condición opuesta, o bien poniendo un not en la comprobación, negando así la condición del mientras. También se puede hallar teniendo en cuenta el significado significado de los operadores relacionales y booleanos.
+----------+----+ | NOT (<) | >= | | NOT (<=) | > | | NOT (>) | <= | ¦ | NOT (>=) | < | | NOT (=) | <> | | NOT (<>) | = | +----------+----+
* Negación de lo operadores ralacionales.
3.2.2.3 Bucle Para Lo que caracteriza al bucle Para es que ya sabemos con antelación el número de iteraciones a realizar. Es por ello que no nos hace falta una comprobación comprobació n de salida y/o entrada. También existe la posibilidad de realizar 0 iteraciones, cuando la variable secundaria es menor que la primaria. Otra característica, característi ca, es la posibilidad de realizar incrementos de n en n en el contador del bucle. Pasemos a ver el algoritmo anterior mediante un bucle Para. +-Algoritmo Multiplicar (mult1,mult2,resul) | Parámetros reales mult1,mult2 mult1,mult2,resul ,resul | Variable Local loop | resul=0 | +-Para loop=1 hasta mult2 de incremento 1 hacer | | resul=resul+mult1 resul=resul +mult1 | | mult2=mult2-1 mult2=mult2 -1 | +-Finpara +-Final En este caso, la variable primaria es "loop" y la secundaria es "mult2". Si esta última toma un valor inferior a la primaria, entonces el bucle no se realiza. Obsérvese que hemos puesto el incremento= 1, este es el valor que posee el bucle Para por defecto, es por ello que cuando el incremento es de 1 en 1, no se debe especificar.
TEMA 4: Manejo de Archivos Dentro de este apartado vamos a aprender que son y como utilizar los archivos. Veremos cual es su finalidad, que tipos son los más comunes y cuales son sus características características principales desde el punto de vista de la organización organización de los datos que contiene. Para comenzar podemos decir que un ordenador que no tiene la posibilidad de almacenar almacenar sus programas y datos en un dispositivo dispositivo de almacenamiento, almacenamie nto, (ya sean discos, cintas, etc.) no es mas que una calculadora. En la actualidad, cualquier ordenador posee dispositivos dispositivo s de almacenamiento, almacenamie nto, ya sean internos (discos duros) o externos (disquetes, cartuchos, cintas, etc.). La finalidad fin alidad es obvia, la de poder guardar los datos para su posterior recuperación y tratamientos de los mismos en otras sesiones. Un común error entre los principiantes es el de confundir la memoria de tipo RAM con la capacidad de almacenamiento del disco que acompaña al ordenador. La RAM (Memoria de Acceso Aleatorio) es denominada memoria volátil, ya que una vez que se apaga el ordenador la información que esta contenía se pierde. La RAM se mide generalmente generalment e en MegaBytes, aunque con el paso del tiempo la unidad puede cambiar (1 Mb son 1024 Ks, a su vez 1 K es 1024 bytes, y
finalmente 1 Byte son 8 bits, siendo esta última la cantidad mínima de información que puede procesar un computador). La capacidad de los dispositivos de almacenamiento almacenamie nto (entiéndase disquetes, discos duros, cintas, CD-ROM, etc.) se mide en las mismas unidades, es por ello que la gente suele confundir la memoria RAM de un ordenador con la capacidad de almacenamiento que suele tener en un disco duro. La memoria RAM es uno de los aspectos que limita la potencia de un ordenador, cuanta más RAM tengamos, mayores programas y datos podremos almacenar en ella y menos accesos a los dispositivos de almacenamiento almacenamiento tendremos que realizar. Los archivos o ficheros se almacenan en los dispositivos de almacenamiento, almacenamie nto, para como dijimos anteriormente anteriormen te puedan ser recuperados sus datos en sesiones futuras. De esta forma, podemos definir un fichero como un objeto concebido para el almacenamiento almacenamie nto permanente perm anente de la información. Información que puede ser organizada de diferente forma dependiendo del trato que le vayamos a dar. 4.1 Nociones de dispositivos de almacenamiento. En el apartado anterior hemos aprendido que la información puede ser volcada en dispositivos dispositivos de almacenamiento almacenamiento permanente, a los cuales nos hemos referido como disquetes, discos duros, cintas, CD-ROM, etc. Estos medios de almacenamiento almacenamie nto se pueden diferenciar en el modo en el que la información puede ser accesible. Así definimos dispositivos de almacenamiento alm acenamiento o soportes secuenciales secuenciale s a aquellos donde la información es accesible secuencialmente, secuencialmente, es decir, para leer o escribir un dato determinado, antes tenemos que pasar por todos los datos que le preceden. De esta forma, en una cinta magnética los datos se graban y leen uno detrás de otro. Y no podemos ir directamente a uno en concreto sin pasar antes por los demás. De igual forma se define soportes direccionables direccionabl es a aquellos donde la superficie de almacenamiento almacenamien to es independiente independient e e individualmente individualm ente direccionable, direccionabl e, es decir, podemos acceder a un dato en concreto sin tener que pasar antes por los demás. Este es el caso de los discos. Por poner un ejemplo, en un disco de vinilo, podemos elegir la canción que queremos tan sólo con poner el cabezal en el comienzo de la misma. Sin embargo, en una cinta de casete, para oír una canción ca nción determinada de terminada antes debemos pasar todas t odas las la s que le preceden. En los dispositivos de almacenamientos almacenamient os hay que diferenciar las direcciones en las que se almacenan los datos. De este modo tenemos dos tipos de direcciones: direcciones : direcciones absolutas o físicas y direcciones relativas o lógicas. Las direcciones absolutas o físicas son las direcciones con las que juega el ordenador de forma física en el dispositivo, es decir, cuando el ordenador se refiere a un dato en concreto lo esta haciendo por el conocimiento de ciertos datos que identifican la posición física del dato en el soporte de almacenamiento. almacenamien to. Así, el ordenador juega con parámetros tales como: unidad, cabeza, cara, pista o cilindro y sector.
Por el contrario, las direcciones relativas o lógicas son con las que vamos a jugar jugar nosotros, una vez que abrimos el fichero fichero para realizar operaciones de Input/output (I/O) nos referimos al dato con un índice numérico, es decir, el registro número x. 4.2 Ficheros de Acceso Secuencial. Un Fichero de Acceso Secuencial es aquel donde los registros están ubicados consecutivamente consecutiva mente sobre un dispositivo de almacenamiento. De tal forma que para acceder a un registro determinado 'd' debemos pasar obligatoriamente obligatoriam ente por todos los registros que le preceden. Suponiendo una cinta c inta magnética, debemos pasar antes por el 'a', 'b', 'c'. -----------------------------------------+- a b c d ... | -----------------------------------------+> cabeza lectora.
para
leer
registro 'd'
Para leer el registro 'd', la cabeza lectora, deberá pasar antes por los que le preceden.
Un Fichero de Acceso Secuencial puede ser almacenado tanto en un dispositivo de almacenamiento direccional como secuencial, sin embargo los Ficheros de Acceso Directo e Indexado únicamente puede ubicarse en dispositivos de almacenamiento direccional. 4.2.1 Ejemplo de I/O en Ficheros Secuenciales. Secuenciales. Pasemos a ver un secuenciales, secuenciale s, pero antes utilizaremos.
ejemplo de veamos las
como utilizar ficheros sentencias básicas que
- Abrir secuencial (variable) Esta sentencia abre un fichero para el acceso secuencial. Donde la variable contiene el nombre del fichero a abrir. - Cerrar (variable) Esta sentencia cierra un el nombre del fichero a cerrar.
fichero donde la variable contiene
- Leer/Escribi Leer/Escribir/Reescribi r/Reescribir r (variable1,v (variable1,variable2) ariable2) Esta sentencia permite la datos que contiene cont iene la variable2 determinado en la variable1.
lectura o re/escritura re/escritur a de los en un fichero de nombre, nom bre, el
- Iniciar operaciones de lectura y/o escritura en (variable) Esta sentencia debe incluirse después de abrir un fichero y determina prácticamente prácticament e el tipo de acceso que vamos a realizar, es decir, si abrimos el fichero para leer y/o escribir. - NO fin (variable) Esta función devuelve el valor lógico 'true' si no se ha encontrado el final del fichero; y devuelve el valor lógico 'false' si se ha encontrado el final del fichero.
- Error (variable) Esta función devuelve el código del error producido al realizar cualquier operación anterior. Por defecto, def ecto, la l a función devuelve un cero cuando no se ha producido ningún error. Ejemplo: En este ejemplo se intenta dar aun de ficheros secuenciales. secuenciales.
visión general del uso
+-Algoritmo Copiar_fich Copiar_fichero ero | | Fichero de enteros Origen,Desti Origen,Destino no | Variable entera x | | Escribir ("Indique el nombre del fichero Origen:"); | Leer Origen | Escribir ("Indique el nombre del fichero Destino:") | Leer Destino | | Abrir secuencial (Origen) | Iniciar lectura en (Origen) | Abrir secuencial (Destino) | Iniciar escritura en (Destino) | | +-Mientras (NO fin(Origen)) hacer | | Leer (Origen,x) | | Escribir (Destino,x) | +-Finmientra +-Finmientras s | | Escribir ("Fichero Copiado:") | | Cerrar (Origen) | Cerrar (Destino) | +-Final Nota: Obsérvese que la variable utilizada en las operaciones de lectura y/o escritura, deben ser del mismo tipo que la declaración del fichero. 4.3 Registros Estructurad Estructurados. os. Hasta ahora hemos visto como los registros eran de un tipo único, es decir, eran todos carácter, enteros, reales, etc. Sin embargo, hay situaciones en las que debemos realizar agrupaciones de tipos para formar un registro estructurado. Así, un registro estructurado esta formado por un conjunto de variables de diferentes tipos que denominaremos denominaremos campos. De este modo podemos decir que un registro estructurado es un conjunto de campos. Ejemplo: +-Registro | Cadena | Cadena | Cadena | Cadena | Cadena | Cadena
datos-alumno nombre apellido1 apellido2 NIF curso telefono
| Cadena fecha_nac | ... +-Finregistro De esta forma, podemos leer y escribir en un fichero registros estructurados que nos permitirán un almacenamiento almacenamien to más lógico y ordenado de los datos. Al generar un registro es como si estuviésemos definiendo un nuevo tipo de variable independiente independient e de las ya existente (enteras, reales, cadena, etc.). Este tipo de registros o record se utiliza para agrupar la información que se desee volcar a un fichero determinado. 4.4 Ficheros de Acceso Directo. Un Fichero de Acceso Directo (es también denominado de acceso aleatorio) es aquel que se encuentra almacenado en un dispositivo direccionable; direccionable; y donde sus registros poseen un campo que denominaremos denominarem os campo clave y que identifica inequívocamente inequívocame nte a cada registro. En los ficheros de acceso directo el campo clave es el número del registro en el fichero. Así se establece una correspondencia corresponde ncia directa entre los valores del campo clave y las direcciones lógicas en el soporte. Los registros se almacenan según el orden de entrada y no quedan ordenados. De esta forma en un Fichero de Acceso Directo nos referimos a un registro por medio de su posición en este. Así, podremos obtener el reg número 4 sin pasar antes por los demás. +-----+------------+ | Reg | D A T O | +-----+------------| | 1 | LUIS | | 2 | CARLOS | | 3 | TERESA | | 4 | JOAQUIN | | 5 | INMA | | 6 | JOSE | +-----+------------+
Fíjese que los datos están almacenados en el orden en el que han sido introducidos por el usuario. Accedemos a los datos por medio del valor de la posición del registro.
La tabla anterior se denomina tabla de acceso. Esta tabla relaciona de forma única el número del registro con el registro correspondiente, así el reg número 2 corresponde al dato Carlos. 4.4.1 Ejemplo de I/O en Ficheros de Acceso Directo. Las sentencias que manejan los ficheros de acceso son las mismas, sólo que poseen el prefijo directo. Ejemplo: +-Algoritmo Contador_de Contador_de_registros. _registros. | | Fichero de enteros F | Variable entera x,contador | | Abrir directo (F) | Iniciar lectura (F) | contador = 0
directo
| +-Mientras (NO fin(F)) hacer | | Leer directo(F, directo(F,x) x) | | contador=contador+1 contador=co ntador+1 | +-Finmientra +-Finmientras s | Cerrar(F) | | Escribir ("El fichero:";F; fichero:";F;"posee:";co "posee:";contador;"reg. ntador;"reg.") ") | +-Final 4.5 Ficheros de Acceso Indexado. Un Fichero de Acceso Indexado es aquel que se encuentra almacenado en un dispositivo direccionable; direccionable ; y donde sus registros poseen un campo que denominaremos denominaremo s campo clave principal y que identifica inequívocamente a cada registro. La clave principal debe ser aquel campo del registro estructurado estructurado que tenga siempre un valor diferente a los ya introducidos, introducido s, es decir, dentro del fichero Indexado no puede haber dos registros con los campos clave principal iguales. Además del campo clave principal pueden existir otros campos claves secundarios secundarios que realizan la misma tarea que el campo clave, sin embargo, sus valores pueden repetirse. En los Ficheros de Acceso Indexado el campo clave puede ser cualquiera de los campos de un registro estructurado. Así se establece una correspondencia corresponden cia directa entre los valores del campo clave y el propio registro al que pertenece. Los registros se almacenan ordenados alfabéticamente alfabéticame nte por el campo clave, esto nos facilita la búsqueda y listados ordenados por los distintas claves. Para cada campo clave, el fichero genera una tabla, donde dicha clave aparece ordenada alfabéticamente alfabéticamente y se relaciona con la dirección de los datos. De esta forma en un Fichero de Acceso Indexado nos referimos a un registro por medio de alguna de las claves que posea el fichero, tanto la principal como la secundaria. Es decir, leer el registro cuya clave principal sea: 46.399.554, 46.399.554, en este caso leería el registro correspondiente correspondiente a INMA. También podríamos haber dicho, leer los registro cuya clave secundaria sea la Edad=16 y primero nos leería el registro correspondiente correspondiente a los datos de Luis y en la siguiente petición de lectura los datos de Teresa. La diferencia entre clave principal y secundaria, está en que la clave principal es única úni ca (relacionando (rela cionando así inequívocamente ine quívocamente al registro reg istro al que pertenece) mientras que las claves principales puede ser iguales. +------------+-------------------------------------+ | Clave | Clave Clave Clave | | Principal | Secundaria Secundaria Secundaria | +--------------------------------------------------+ +---------+------------+------------+-------------+--------+ | (Direc) | (D.N.I.) | (Nombre) | (Provincia) | (Edad) | +---------|------------|------------|-------------|--------+ | 1 | 55.366.546 | LUIS | Las Palmas | 16 | | 2 | 42.386.225 | CARLOS | Salamanca | 17 | | 3 | 32.387.203 | TERESA | Oviedo | 16 | | 4 | 46.399.554 | INMA | Palencia | 20 | | 5 | 60.643.434 | JOAQUIN | Salamanca | 17 | | 6 | 22.543.986 | JOSE | Las Palmas | 23 | +---------+------------+------------+-------------+--------+
Como podemos indexado.
observar, esto sería un
ejemplo de un fichero
Para cada campo clave, el fichero genera una tabla, donde dicha clave aparece ordenada alfabéticamente alfabéticamente y se relaciona con la dirección de los datos. Así las tablas para la clave principal (DNI) y la clave secundaria (Nombre) serían: +------------+---------+ | (D.N.I.) | (Direc) | +------------+---------| | 22.543.986 | 6 | | 32.387.203 | 3 | | 42.386.225 | 2 | | 46.399.554 | 4 | | 55.366.546 | 1 | | 60.643.434 | 6 | +------------+---------+ +--------------------------+ | Tabla de Ac. Clave Princ.| Princ.| +--------------------------+
+------------+---------+ | (Nombre) | (Direc) | +------------+---------+ | CARLOS | 2 | | INMA | 4 | | JOAQUIN | 5 | | JOSE | 6 | | LUIS | 1 | | TERESA | 3 | +------------+---------+ +---------------------------+ | Tabla de Ac. Clave Clave Secund.| Secund.| +---------------------------+
Obsérvese como ambas tablas aparecen ordenadas alfabéticamente alfabéticam ente (o de menor a mayor en el caso del DNI). Como ya dijimos, esto nos da grandes facilidades a la hora de realizar búsquedas y/o listados ordenados. 4.5.1 Ejemplo de I/O en Ficheros de Acceso Indexado. Pasemos a indexados, pero utilizaremos.
ver un ejemplo de antes an tes veamos ve amos las l as
como utilizar ficheros sentencias sent encias básicas que
- Abrir indexado (variable,KEY=...) (variable,KEY=...) Esta sentencia abre un fichero para el acceso indexado. Donde la variable contiene el nombre del fichero a abrir y en 'KEY=' ponemos los campos claves separados por comas, comenzando por el campo clave principal. - Cerrar (variable) Esta sentencia cierra un el nombre del fichero a cerrar.
fichero donde la variable contiene
- Leer/Escribi Leer/Escribir/Reescribi r/Reescribir r (variable1,K (variable1,KEY=,variabl EY=,variable2) e2) Esta sentencia permite la lectura o re/escritura re/escritur a de los datos por medio de un campo clave (principal o secundaria) que debemos poner después de 'KEY='. El contenido será almacenado o mandado por la variable2 en un fichero de nombre, el determinado en la variable1. - Iniciar operaciones de lectura y/o escritura en (variable) Esta sentencia debe incluirse después de abrir un fichero y determina prácticamente prácticament e el tipo de acceso que vamos a realizar, es decir, si abrimos el fichero para leer y/o escribir. - NO fin (variable)
Esta función devuelve el valor lógico 'true' si no se ha encontrado el final del fichero; y devuelve el valor lógico 'false' si se ha encontrado el final del fichero. - Error (variable) Esta función devuelve el código del error producido al realizar cualquier operación anterior. Por defecto, def ecto, la l a función devuelve un cero cuando no se ha producido ningún error. Las sentencias que manejan los ficheros de acceso Indexado son las mismas que hemos utilizado en los ficheros secuenciales, sólo que poseen el prefijo Indexado y en las operaciones de lectura y/o escrituras hay que indicar la clave ('KEY='). Ejemplo: +-Algoritmo Buscar_Pers Buscar_Persona. ona. | | +-Registro estructurado datos_personales | | Variable cadena dni | | Variable cadena nombre | | Variable cadena tlf | | Variable cadena provincia | +-Finregistr +-Finregistro o | | Fichero de datos_person datos_personales ales F | Variable cadena documento | Variable de datos_perso datos_personales nales dato | | Escribir "Indique el DNI de la persona a buscar" | Leer documento | | Abrir indexado (F,KEY=dni,n (F,KEY=dni,nombre) ombre) | Iniciar lectura (F) | | Leer (F,KEY=doc (F,KEY=documento,dato) umento,dato) | +-Si error(F)<> error(F)<>0 0 entonces | | Escribir "Ese registro no existe." | +-Sino | | Escribir " DNI: ";dato.dni | | Escribir " Nombre: ";dato.nombr ";dato.nombre e | | Escribir " Tlf: ";dato.tlf | | Escribir "Provincia: ";dato.provi ";dato.provincia ncia | +-Finsi | | Cerrar (F) +-Final 4.6 Función de Hashing. Muchas veces veces surge el caso del que el fichero fichero es tan grande grande que la tabla no puede mantener una ordenación eficaz debido a que cuando introducimos introducimos un nuevo dato debe hacerse un espacio en la misma para albergar a éste. Es por ello que recurrimos al "Hashing". El "Hashing" consiste simplemente en relacionar la clave principal con la dirección por medio de una fórmula matemática. Así, antes de introducir datos se crean unos espacios para albergar las futuras modificaciones modificaciones y adiciones de datos. Este método crea una
serie de conjuntos llamados "Buckets". A cada Bucket le corresponde un número que será el que devuelva la fórmula matemática. A su vez los Buckets poseen un número que determina la cantidad máxima de claves que pueden almacenarse en él. De esta manera cuando vamos a buscar el dato "Manolo" el Hashing nos determina la posición del conjunto (Buckets). En ese conjunto habrá otra serie de datos a los cuales les corresponde el mismo valor de la función Hashing. La búsqueda ahora se hará de forma secuencial a lo largo del Bucket. Veamos un ejemplo: Bucket Clave Prin. Claves Secundarias +-----+------------+----------------------+ | | Manolo | . . . | | +------------+----------------------+ | 35 | Manuel | . . . | +-----+------------+----+-----+-----------+----------------------------------+ ----+ +--| 104 | Manuela | . . . | | +-----+-----+-----+------------+----------+----------------------------------+ ----+ | | | Natalia | . . . | | | +------------+----------+-----------+----------------------+ -----------+ | | 36 | Naranjo | . . . | | | +------------+----------+-----------+----------------------+ -----------+ | | | Noelia | . . . | | +-----+------------+----------------------+ | . | . | . | +-----+------------+----------------------+ | | | Mark | . . . | | | +------------+----------+-----------+----------------------+ -----------+ +->| 104 | Miguel | . . . | | +------------+----------------------+ | | María | . . . | +-----+------------+----------------------+
Al número 104 se le denomina puntero de desbordamiento desbordamien to
4.6.1 Gestión de las colisiones. En este método parecen una serie de conflictos cuando las claves son muy parecidas, como podemos observar para claves casi idénticas, el Hashing nos devuelve el mismo Bucket. Esto implica que el Bucket puede llenarse de datos; cuando esto ocurre la solución está en un puntero puntero que existe en cada Bucket Bucket que determina determina el salto salto a otro Bucket. Así, cuando se llena el Bucket número 35, existe un salto de éste éste al número 104 (otro Bucket) Bucket) que posee datos del mismo tipo, que también puede rebosarse y apuntar a otro Bucket secundario y así sucesivamente. sucesivamente. Ahora es cuando surgen los problemas. Cuando un dato se borra de un Bucket Bucket hay que que reorganizar la información información para no dejar espacios en blanco dentro dentro de la tabla. Esto se realiza por medio de de un empaquetamiento empaquetamie nto Packed. Sin embargo cuando se va a realizar muchas modificaciones modificacione s y/o borrados y el fichero es muy grande, es aconsejable hacer una actualización de los datos del fichero. 4.7 Esquema Básico de los tres tipos de Organización. * Fichero de Acceso Secuencial:
- Almacenamiento en dispositivo secuencial o direccionable. - No existe campos claves que relacione a algún registro. - Los datos están almacenados en el orden en el que han sido introducidos. - El acceso a los registros es únicamente secuencial. * Fichero de Acceso Directo: - Almacenamiento en dispositivo direccionable. direccionable. - Existe en los registros un campo denominado que hace referencia inequívoca a dicho registro número de registro. - Los datos están almacenados en el orden en el introducidos. - El acceso a los registros puede ser tanto través del campo clave como secuencial secuencial. .
campo clave a través del que han sido aleatorio
a
* Fichero de Acceso Indexado: - Almacenamiento en dispositivo direccionable. direccionable. - Existe en los registros un campo denominado campo clave principal y campo clave secundario, que hacen referencia inequívoca a dicho registro. - Los datos están almacenados en el orden alfabético por el campo clave. - El acceso a los registros puede ser tanto aleatorio a través del campo clave como secuencial secuencial. . - El acceso Indexado-Secuencial permite el acceso como si se tratara de un fichero secuencial, sin embargo, los datos no saldrán en el orden en el que fueron introducido introducidos s sino en orden alfabético por el campo que estamos leyendo. TEMA 5: Problemas y Algoritmos importantes En este apartado vamos a ver una serie de algoritmos de gran importancia en esta asignatura. El dominio de los siguientes algoritmos nos da pie a enfrentarnos a una serie de algoritmos más complejos y cuyo código depende de aquellos. 5.1 Ordenación por selección. +-Algoritmo Orden_selec Orden_selección ción | | Constante entera n=... | Vector entero a(1..n) | Variable entera x,i,j,h,mayo x,i,j,h,mayor r | | Escribir "Introduzc "Introduzca a el vector a ordenar" | +-Para h desde 1 hasta n hacer | | Leer a(h) | +-Finpara | | +-Para k desde 1 hasta n-1 hacer | | mayor <- k | | +-Para j desde k+1 hasta n hacer | | | +-Si a(j)
| | x <- a(j) | | a(j) <- a(i) | | a(i) <- x | +-Finpara | Escribir a +-Final 5.2 Búsqueda Secuencial o Lineal. +-Algoritmo Busqueda_se Busqueda_secuencial cuencial | | Constante entera n=... | Vector entero a(1..n) | Variable entera x,i,j,h,mayo x,i,j,h,mayor r | | Escribir "Introduzca el valor a buscar" | Leer x | | k <- 0 | +-Repetir | | k <- k+1 | | +-Si a(k)=x entonces | | | Escribir "El dato: ";a; | | | "esta en la posición: ";k | | +-Finsi | +-Hasta que (k=n) or (a(k)=x) +-Final 5.3 Búsqueda Dicotómica o Binaria. +-Algoritmo Búsqueda_di Búsqueda_dicotómica cotómica | | Constante entera n= ... | Vector de enteros a(1..n) | Variable entera x,i,j,m,h | | Escribir "Introduzc "Introduzca a el vector a ordenar" | +-Para h desde 1 hasta n hacer | | Leer a(h) | +-Finpara | | Escribir "Introduzca el valor a buscar" | Leer x | | i <- 1 | j <- n | | +-Repetir | | m <-(i+j) div 2 | | +-Si x < a(m) entonces | | | j <- m-1 | | +-Sino | | | i <- m+1 | | +-Finsi | +-Hasta que (a(m)=x or i>j) | | +-Si i>j entonces | | Escribir "El dato a buscar no se encuentra." | +-Finsi
| +-Final 5.4 Mezcla de ficheros Ordenados. +-Algoritmo mezcla | | +-Registro estructurado r | | variable cadena clave | | ... | +-Finregistr +-Finregistro o | | Ficheros de r A,B,C | Variables enteras a,b | | Abrir secuencial A,B,C | Iniciar lectura en A,B | Iniciar escritura en C | | Leer (A,a) | Leer (B,b) | | +-Mientras (NO fin(A) AND NO fin(B)) hacer | | +-Si a.clave < b.clave entonces | | | Escribir (C,a) | | | Leer (A,a) | | +ÄSino | | | Escribir (C,b) | | | Leer (B,b) | | +-Finsi | +-Finmientra +-Finmientras s | | +-Mientras (NO fin(A)) hacer | | Escribir (C,a) | | Leer (A,a) | +-Finmientra +-Finmientras s | | +-Mientras (NO fin(B)) hacer | | Escribir (C,b) | | Leer (B,b) | +-Finmientra +-Finmientras s | | Cerrar A,B,C | +-Final TEMA 0: Introducción 0.1 Orígenes del C El lenguaje C cuando trabajaba, junto operativo UNIX.
fue inventado por Dennis Ritchie en 1972 con Ken Thompson, en el diseño del sistema
El lenguaje C deriva del lenguaje B de Thompson, el cual, a su vez, deriva del lenguaje BCPL desarrollado por Martin Richards. Durante muchos años el estándar de C fue la versión proporcionada proporciona da con el sistema operativo UNIX versión 5. Pero pronto empezaron a surgir muchas implementaciones implementacio nes del C a raíz de la
popularidad creciente de los microordenadores. microordenado res. Por este motivo, se hizo necesario definir un C estándar que está representado representad o hoy por el ANSI C. 0.2 Características del lenguaje C Algunas características del lenguaje C son las siguientes: - Es un lenguaje de propósito general. Este lenguaje se ha utilizado para el desarrollo de aplicacione aplicaciones s tan dispares como: hojas de cálculos, gestores de bases de datos, compiladores compiladores, , sistemas operativos, ... - Es un lenguaje de medio nivel. Este lenguaje permite programar a alto nivel (pensando a nivel lógico y no en la máquina física) y a bajo nivel (con lo que se puede obtener la máxima eficiencia y un control absoluto de cuanto sucede en el interior del ordenador). - Es un lenguaje portátil. Los programas fácilmente transportables a otros sistemas.
escrito s escritos
en
C
son
- Es un lenguaje potente y eficiente. Usando C, un programador puede casi alcanzar la eficiencia del código ensamblador junto con la estructura del Algol o Pascal. Como desventajas habría que reseñar que es más complicado de aprender que que otros lenguajes como Pascal Pascal o Basic y que requiere requiere una una cierta experiencia para poder aprovecharlo a fondo. 0.3 Uso del C Los pasos a seguir desde el momento que se comienza a escribir el programa C hasta que se ejecuta son los siguientes: 1.2.3.4.-
Escribirlo en un editor. Compilarlo en un compilador. Enlazarlo en un enlazador. Ejecutarlo.
Paso 1: ESCRIBIRLO El programa se puede escribir en cualquier editor que genere ficheros de texto estándar, esto es, que los ficheros generados no incluyan códigos de control y caracteres no imprimibles. Estos ficheros que contienen código C se llaman ficheros fuentes. Los ficheros fuentes son aquellos que contienen código fuente, es decir, ficheros con texto que el usuario puede leer y que son utilizados como entrada al compilador de C. Los programas pequeños suelen ocupar un solo fichero fuente; pero a medida que el programa crece, se va haciendo necesario distribuirlo en más ficheos fuentes. Paso 2: COMPILARLO El
compilador produce ficheros objetos a partir de los ficheros fuentes. Los ficheros objetos son los ficheros que contienen código objeto, es decir, ficheros con código máquina (número binarios que tiene significado para el microprocesador) microprocesa dor) y que son utilizados como entrada al enlazador.
La extensión de estos ficheros es OBJ, aunque también los hay con extensión LIB. A estos últimos se les llama también ficheros de librería o biblioteca; contienen código máquina perteneciente a código compilado suministrado por el compilador. Paso 3: ENLAZARLO El enlazador ficheros objetos.
produce un fichero ejecutable
a partir de los
Los ficheros ejecutables son aquellos que contienen código máquina y se pueden ejecutar directamente por el sistema operativo. La extensión estos ficheros es EXE o COM. Al proceso de enlazado también se le suele llamar el proceso de linkado. Paso 4: EJECUTARLO El programa p rograma se puede pu ede ejecutar e jecutar simplemente tecleando su nombre desde la línea de comandos del sistema operativo. ESQUEMA Los pasos anteriores se resumen en el siguiente esquema: f1.c
---->
f1.obj
f2.c
---->
f2.obj
. . . fn.c
. . . ---->
fn.obj f1.lib f2.lib . . . fm.lib
---------+ | ---------| | | | | | ---------| |---------------> |--------------> f.exe ---------| | ---------+ | | | | | ---------+
Hoy día los compiladores com piladores de C son muy mu y sofisticados sofis ticados e incluyen entornos integrados desde los cuales editamos, compilamos, enlazamos, y podemos realizar una multitud de servicios más. En algunos de ellos se pueden realizar los pasos de compilado, enlazado y ejecutado con la pulsación de una sola tecla. En programación, la experiencia es el gran maestro. Por ello es conveniente empezar a hacer programas en C cuanto antes. TEMA 1 : Conceptos básicos 1.0 Introducció Introducción n
En este segundo tema se describirá la estructura básica de un programa en lenguaje C así como la forma de visualizar distintos tipos de datos en pantalla. Se introducirán los conceptos de tipos de datos básicos y su utilidad. 1.1 El programa HOLA MUNDO Este programa se ha convertido en un clásico dentro de los libros de programación. Simplemente muestra en pantalla el mensaje HOLA MUNDO, esto que puede parecer muy tonto es algo fundamental puesto que si no sabemos imprimir mensajes ó datos en la pantalla difícilmente nuestro programa se se podrá comunicar con el usuario usuario que que lo utilice. Mostraremos el programa y a continuación describiremos cada una de las instrucciones instrucciones que lo forman. /* Programa : HOLA MUNDO */ #include main() { printf ("\nHola mundo"); } Como podemos observar se trata de un programa muy sencillo. La primera línea es lo que se conoce como un comentario, un mensaje que el programa añade al código del programa para explicar o aclarar su funcionamiento o el de una parte de él. Los comentarios se pueden situar en cualquier parte de nuestro código y se considerará como comentarios cualquier mensaje que se encuentre entre los caracteres /* y */. Los "/*" y "*/" no son caracteres, sino símbolos o banderas. La siguiente línea es lo que se conoce como directiva del preprocesador, preprocesad or, todos los compiladores de C disponen de un preprocesador, preprocesador, un programa que examina el código antes de compilarlo y que permite pe rmite modificarlo mo dificarlo de cara car a al compilador en distintos disti ntos sentidos. En temas posteriores trataremos en profundidad estas directivas del preprocesador, preprocesado r, pero para el desarrollo de los temas anteriores a este debemos conocer al menos la directiva #include. Las directivas se caracterizan caracteriza n por comenzar con el carácter # y se deben incluir al comienzo de la línea aunque es probable que esto dependa de la implementación implementac ión del compilador con el que estemos trabajando. La directiva include permite añadir a nuestro código algún fichero de texto, de tal forma que la directiva es sustituida por el texto que contiene el fichero indicado. En general los ficheros que acompañan a esta directiva son ficheros .H (Header Cabecera), en los que se incluyen definiciones de funciones que deseamos utilizar en nuestros programas, constantes o tipos complejos de datos. La librería stdio.h (STandarD Input/Output) Input/Outpu t) con tiene las funciones estándar de entrada salida, y en ella se encuentra la función printf que utilizamos en nuestro programa. Como se observa en el código el nombre de la función a incluir debe ir entre los caracteres <...>. A medida que vayan surgiendo iremos indicando las funciones estándar que deberían deberían incorporar todos los compiladores compiladores C y cual es su fichero de definición .H.
En la siguiente línea nos encontramos con main(). Esto indica que aquí comienza nuestro programa, en realidad estamos definiendo una función (esto se indica con los paréntesis al final de la línea) pero esto lo discutiremos en temas posteriores. La función main (principal en inglés) siempre debe existir y contendrá el programa principal. Finalmente nos encontramos el programa principal, una sentencia printf entre llaves ({, }). Las llaves en C representan bloques, y encierran un conjunto de sentencias o instrucciones (lo que el computador ejecutará), considerando todas ellas como una sola, permitiendo una definición homogénea de los distintos bloques que constituyen constituyen el programa. En nuestro caso caso tenemos tenemos un sólo sólo bloque que no es ni más ni menos que el programa principal, que en nuestro caso está compuesto por una sola sentencia (la línea que contiene el printf). Como se observa en el código las sentencias en C deben terminar con un punto y coma (;), #include y main() no son sentencias, dan información sobre la estructura del programa, y por tanto no finalizan con un punto y coma. NOTA: La razón de que la línea "main()" no tenga un punto y coma ";" al final es debido a que la sentencia en sí termina al cerrar el corchete, no en que dicha línea proporcione información sobre la estructura del programa. De hecho, si el "main()" constituyese una línea de prototipo tendría un ";" al final. La función printf permite visualizar datos formateados en pantalla, es decir, permite indicar un formato como si de un impreso o formulario se tratase indicando donde se deben visualizar cada uno. En el siguiente tema cuando se introduzcan los tipos básicos de datos se comprenderá mejor ésto. Por ahora sólo nos interesa conocer que printf visualiza mensajes en pantalla. El mensaje debe ir entre comillas dobles (") y dentro de las comillas se puede mostrar cualquier secuencia de caracteres. El formato de esta función para este segundo tema será: printf ("mensaje"); En el siguiente tema, cuando expliquemos instrucción, ampliaremos esta definición.
en profundidad la
En nuestro programa observamos que el mensaje de texto que visualiza la instrucción printf comienza con los caracteres \n. Estos caracteres nos permiten algunas funciones especiales para controlar la forma de visualizar los mensajes, la más utilizada en \n que significa nueva línea, así nuestra sentencia printf ("\nHOLA MUNDO"); moverá el cursor (la posición de la pantalla donde actualmente se visualizan visualizan los datos) a una nueva línea línea situándolo situándolo a la izquierda de la pantalla y visualizará el mensaje HOLA MUNDO. Para finalizar con este punto se indicará las secuencias antecedidas por \ que se pueden incluir en una instrucción printf: +-------------------------+------+ | Nueva línea | \n | | Tabulador horizontal | \t | | Tabulador vertical | \v | | Backspace (<-) | \b | | Retorno de carro | \r |
| Avance de página | \f | | Pitido (alerta) | \a | | Caracter \ | \\ | | Caracter ? | \? | | Caracter ' | \' | | Caracter " | \" | | Número Octal (ooo) | \ooo | | Número Hexadecimal (hh) | \xhh | +-------------------------+------+ Algunos comentarios sobre estos códigos. En primer lugar el primer grupo (hasta carácter \), eran utilizados para mover el cursor en terminales. Los terminales podían ser una pantalla o una impresora, esta es la razón por la que nos encontramos cosas como avance de página o retorno de carro. Los caracteres \ ? ' " son especiales puesto que se utilizan dentro del mensaje a visualizar para indicar como se visualiza, o sea, si escribimos \ el compilador buscará el siguiente carácter y si es alguno de los anteriores los visualiza sino corresponde con ninguno simplemente lo ignora, con lo cual no podríamos visualizar el carácter \. Otro tanto sucede con las comillas comillas puesto que para C, las comillas representan una cadena cadena de caracteres, si escribimos " en nuestro mensaje se considerará que éste termina ahí con lo que lo que se encuentre después no tendrá sentido para el compilador. Por último Número octal y Número hexadecimal nos permite introducir directamente el código numérico en octal o hexadecimal del carácter que deseemos visualizar, dependiendo del que esté activo en nuestro computador. En general el código utilizado para representar internamente los caracteres por los computadores computadore s es el código ASCII (American Standard Code for Information Interchange). En este segundo tema hemos aprendido a escribir programas que visualicen en pantalla mensajes utilizando la función estándar de la librería stdio. Antes de terminar unos breves comentarios. El lenguaje C diferencia entre mayúsculas y minúsculas con lo cual main, MAIN, MaIn, serían identificadores identificadores distintos para el compilador, en general en C se suele escribir todo en minúsculas a excepción de los mensajes a visualizar (cuyo uso depende del programador) programador ) y de las constantes, esto no tiene por que hacerse así pero digamos que se trata de una tradición de la programación programació n en C. Las separaciones entre líneas también son arbitrarias y su única función es facilitar la legibilidad del código, sirviendo de separadores entre fragmentos de programa relacionados relacionados entre sí. Y esto es todo por ahora. 1.2 Tipos de datos básicos del C La mayoría de los programas realizan algo útil y generalmente para ello ello es necesario trabajar con grandes grandes cantidades cantidades de datos, si queremos realizar un programa que nos calcule un sistema de ecuaciones tenemos que indicar cuales son las ecuaciones para que el programa las resuelva. Por tanto un programa estará constituido por una serie de datos y una serie de sentencias o instrucciones instrucciones que le dicen lo que tiene que hacer con esos datos. Los lenguajes de programación programació n disponen de una serie de tipos de datos básicos, básicos, y proporcionan herramientas herramientas para crear crear estructuras estructuras a medida que faciliten el acceso a la información. Así en nuestro caso ficticio de resolver un sistema de ecuaciones podemos almacenar los coeficientes coeficiente s de cada ecuación con lo que utilizaríamos utilizaríam os como tipo de
dato los números, si planteásemos el problema desde un punto de vista matricial nos interesaría tener un tipo de datos matriz y lo ideal sería tener un tipo de datos ecuación. En este apartado describiremos describirem os los tipos básicos que proporciona el lenguaje C y dejaremos para temas posteriores la declaración declaración de tipos complejos. Estos tipos básicos son los siguientes: +--------+---------------------------------------+ | int | Representa números enteros. | | float | Representa números decimales. | | double | Representa números decimales de mayor | | | precisión. | | char | Representa caracteres. | +--------+---------------------------------------+ Aunque el tipo char represente caracteres internamente para el computador no es más que un número comprendido entre 0 y 255 que identifica un caracter dentro de el código especificado especificado para tal propósito en el sistema en el que nos encontremos trabajando. El código más utilizado para este tipo de representación es el ASCII ya mencionado anteriormente. NOTA: Según la máquina, el compilador empleado y las opciones de compilación activas, "char" puede interpretarse con signo o sin signo. Esto es, de -128 a 127 o desde 0 a 255. Si se requiere una representación representac ión que no dependa de las opciones del compilador, etc., se puede poner "signed char" o "unsigned char", según el caso. Como decíamos antes el ordenador debe de disponer de los datos necesarios para resolver el problema para el que lo queramos programar. Difícilmente se podría resolver un sistema de ecuaciones si no se dispone de éstas. Para ello podemos definir variables. Las variables almacenan valores de un tipo especificado especificado y en ellas almacenamos los datos de nuestro problema, se denominan variables por que su valor puede cambiar a lo largo del programa. Para referenciar una variable especificada es necesario que la podamos identificar para ello se utiliza un nombre o identificador que no es más que una secuencia de caracteres, caracteres, esta secuencia no puede contener caracteres españoles (acentos y eñes), ni caracteres que tengan alguna función especial en el C como por ejemplo ej emplo los caracteres que representan operaciones matemáticas +, -, etc..., tampoco pueden contener espacios por lo que se suele utilizar el carácter subrayado (_) si el identificador que deseamos asignarle a nuestra variable está formado por varias palabras, pero en general con los caracteres y números tenemos suficiente para dar nombres autoexplicativos, aunque los números no pueden comenzar el nombre de una variable. Las variables se suelen escribir con letra minúscula aunque no es necesario y es aconsejable que los nombres sean auto explicativos explicativo s para que resulte más sencillo leer el programa (es mejor llamar resultado a una variable que x). Las variables se declaran indicando el tipo que van a tener seguido de su identificador identificador y terminando la línea con un punto y coma. Algunos ejemplos: int numero; /* número no sería un nombre válido */ float mi_variable mi_variable; ; char letra; Si necesitamos declarar varias variables de un mismo tipo se
pueden incluir en la misma separándolos por una coma:
línea todos
los nombres
que deseemos
int numero1,numero2,numero3; float coordenada_x,coordenada_y; El compilador tiene que conocer las variables que va ha utilizar cada bloque para reservarles sitio, por ello las variables se suelen declarar al principio de cada bloque. Puesto que aún no sabemos como definir funciones nuestro programa sólo dispone de un bloque (el main()) con lo que nuestras variables deben de declararse al comienzo del main() dentro del bloque, es decir, inmediatamente a continuación de la llave abierta ({). Un ejemplo: NOTA: Aunque el párrafo anterior da a entender que se puede declarar una variable en cualquier momento, el estándar ANSI C obliga a realizar las declaraciones declaracione s al principio de cada bloque. En el caso se variables globales la sintaxis es más flexible, para poder utilizar el "Scope" en nuestro provecho. main() { int numero; numero =20; } Podemos también declarar variables fuera del bloque main(). Estas variables se conocen como variables globales y cualquier función puede puede acceder acceder a ellas, ellas, como sólo sólo tenemos una función función (main) en este caso nos daría igual declarar las variables dentro o fuera de main. De poco nos servirían estos datos numéricos si no pudiésemos realizar operaciones con ellos, el lenguaje C permite realizar las operaciones básicas con estas variables de tipo numérico, estas son: +---+---------------------------------------------+ | + | para indicar suma | | - | para indicar resta | | * | para indicar producto | | / | para indicar división | | % | para indicar módulo (resto división división entera) | +---+---------------------------------------------+ Podemos combinar estas operaciones en la forma que nos plazca con variables o constantes (podemos operar variables con números fijos) y utilizar los paréntesis, caracteres ( y ) para indicar precedencia de las operaciones como lo haríamos en una expresión matemática normal. En principio sólo podemos realizar operaciones con variables que sean del mismo tipo, aunque en general podemos operar los tipos float y double con tipos int o incluso char, en principio no podríamos podríamos almacenar un valor float (un número real) en una variable int (entera), (en tera), para p ara ello ell o tendríamos tendr íamos que convertir ese número núme ro real en entero ente ro de alguna forma. Podemos convertir tipos básicos utilizando la facilidad del C conocida como cast. Esta facilidad simplemente consiste en indicar antes de una variable o constante el tipo al que lo deseamos convertir entre paréntesis y el compilador se encargará del resto. Un ejemplo : NOTA: El C también define la conversión automática de tipos.
float int
a; b;
b=30; a=(float)b; Para ejemplificar todo esto vamos a realizar un programa que nos calcule el espacio recorrido por un móvil con velocidad uniforme durante un tiempo determinado. El programa sería algo así: #include main() { float
e,v,t;
v = 30; /* Velocidad del móvil en Km/h */ t = 5; /* Tiempo durante el cual se mueve */ e = v*t; printf ("\nVelocidad : %f\nTiempo : %f",v,t); printf ("\nEspacio recorrido : %f",e); } Este programa calcula el espacio recorrido por un móvil en movimiento uniforme a una velocidad indicada por la variable v, durante un tiempo indicado por la variable t. Lo más interesante de este programa es que hemos utilizado la función printf para visualizar valores de variables. Como decíamos más arriba la función printf permite visualizar mensajes formateados, formateados , es decir, en la cadena de caracteres entre comillas dentro del paréntesis nos indica la forma en la que se visualizarán visualizarán los datos. Ya hablamos de los caracteres especiales como \n, ahora nos ocuparemos de la visualización visualizaci ón de las variables. Cuando deseamos visualizar una variable debemos indicarlo en la cadena de formateo (la secuencia de caracteres entre comillas) mediante el caracter % seguido de un caracter que nos indica el tipo de dato a visualizar. Estos tipos son los siguiente: +-----+------------------------------------------------+ | d,i | Entero en notación decimal con signo. | | o | Entero notación octal sin signo. | | x,X | Entero en notación hexadecimal sin signo. | | u | Entero en notación decimal sin signo. | | c | Entero como caracter simple. | | s | Cadena de caracteres. | | f | Tipo double (ó float) de la forma [-]mmmm.ddd. | | e,E | Tipo double (ó float) en notación científica o | | | exponencial [-]m.dddde+[-]m.dddde+-xx xx ó [-]m.ddddE+[-]m.ddddE+-xx. xx. | | p | Tipo puntero. | +-----+------------------------------------------------+ Podemos así mismo indicar el número de cifras a visualizar, intercalando entre el % y la letra que identifica el tipo de dato un número. Si indicamos dos números separados por un punto, el primer número indica el número total de caracteres a visualizar y el segundo el número de cifras decimales que queremos que se muestren,
obviamente este formato Algunos ejemplos:
sólo se utilizará con tipo
float o double.
printf ("%f",numero ("%f",numero); ); Visualiza un número real en el formato formato normal, parte entera entera y parte decimal separadas por un punto. printf ("%5.2f",num ("%5.2f",numero); ero); Visualiza un número entero en el mismo formato que la anterior, pero sólo visualizando 5 cifras y siendo dos de ellas reservadas para la parte decimal. Hasta ahora hemos visto como decir a la función printf el formato en el el que debe visualizar los datos, pero no le hemos hemos dicho que datos debe visualizar. visualizar. Lo que se escribirá escribirá en el el lugar indicado por el % está especificado especificad o a continuación de la cadena de formateo entre comillas, separando cada una de ellas por comas. El primer % coincide con el primer parámetro después de las comillas, el segundo con el segundo y así sucesivamente. sucesivament e. De esta forma un programa como este: #include main() { int i; float a,b; i = 10; a = 30.456; b = 678.12; printf ("\nvar1:%d var2:%6.2f var3:%6.1f",i,a,b); } tendrá como salida: var1:10 var2:30.46 var3:678,1 Como se puede observar o bservar en el ejemplo si la precisión especificada especificad a en la cadena de formateo es menor que la real del número, la función printf aproxima a la precisión especificada. especificada. 1.3 Entrada de datos por teclado El programa anterior para el cálculo de el espacio funciona correctamente, correctamen te, pero cada vez que deseemos calcular un nuevo espacio debemos dar valores a las variables v y t y recompilar nuestro programa. Sería estupendo poder leer por teclado los datos de la velocidad y del tiempo y así permitir a nuestro programa trabajar con cualquier valor de velocidad y tiempo. Aquí es donde realmente se comprende de donde viene el nombre de variables. Para leer los datos por teclado se utiliza la función scanf cuya definición se encuentra también en el fichero stdio.h. El formato es idéntico al de printf utilizando una cadena de formateo con los caracteres % y la letra que indica el tipo, ahora del dato que vamos a leer, seguido de la la variable variable en la que que deseamos deseamos que se lea antecedida por el carácter &. Nuestro programa del móvil se convierte ahora en:
#include main() { float
v,t,e;
printf ("\nDime la velocidad de el móvil:"); scanf ("%f",&v); printf ("\Dime el tiempo :"); scanf ("%f",&t); e = v*t; printf ("\nUn móvil desplazándose a %5.2f Km/h durante %5.2f horas, recorre una distancia de %5.2f Km",v,t,e); } Ahora cada vez que el programa se ejecute nos pedirá que introduzcamos introduzcamos los valores de la velocidad y el tiempo y nos proporcionará proporciona rá el espacio que recorreríamos, recorreríamos , evitando de esta forma la necesidad de recompilar el programa cada vez que deseemos realizar un nuevo cálculo. TEMA 2: Control de Flujo de programa 2.0 Introducció Introducción n En tema anterior aprendimos a trabajar con variables, leer sus valores va lores por po r teclado, tecla do, visualizarlas vis ualizarlas en pantalla pan talla y realizar operaciones elementales con ellas. Los programas que escribimos e scribimos hasta ahora se ejecutaban secuencialmente, secuencialm ente, es decir, instrucción tras instrucción, sin posibilidad de volver a ejecutar una instrucción ya ejecutada o evitar la ejecución de un grupo de instrucciones instruccione s si se dan unas características determinadas. En este tercer tema se describirán las instrucciones que nos permiten escribir programas que no se ejecuten de una forma secuencial en el sentido explicado en el párrafo anterior. 2.1 Expresiones condicionales. En ciertas ocasiones nos puede interesar que un programa llegado a un punto de su ejecución vuelva hacia atrás y se ejecute de nuevo, pero lo que en general nos interesará será que este regreso a una una línea de terminada de nuestro código se realice realice si se se cumple una cierta condición. Por esta razón es necesario explicar, antes de comenzar comen zar con las instrucciones ins trucciones propiamente dichas de control de flujo de programa, como le indicamos al ordenador que deseamos evaluar una condición. Las expresiones que nos permiten realizar ésto reciben el nombre de expresiones condicionales condicional es o booleanas. Estas expresiones sólo pueden tomar dos valores: VERDADERO (TRUE) o FALSO (FALSE). En general un valor de 0 indica que la expresión es falsa y un valor distinto de 0 indica que la expresión es verdadera. Como hemos indicado se trata de expresiones condicionales, condicionales, y
análogamente análogament e a las expresiones aritméticas podemos comparar variables entre sí, constantes entre sí (lo cual no es muy útil puesto que si conocemos los dos valores ya sabemos la relación que existe entre ambas constantes) y por supuesto variables y constantes. Además podemos agrupar condiciones entre sí formando expresiones más complejas y ayudarnos de los paréntesis para indicar el orden de evaluación. Los operadores condicionales son: == != > < >= <=
Representa igualdad. Representa desigualdad Mayor que. Menor que. Mayor o igual que. Menor o igual que.
Podemos encadenar distintas expresiones expresiones condicionales, condicionales, las cuales deben de ir entre paréntesis (comparamos de dos en dos) utilizando los operadores: && ||
Y lógico. O lógico.
Veamos un ejemplo de expresión condicional (a==2)||((b>=0)&&(b<=20)) la expresión será cierta si la variable a es igual a dos o si la variable b tiene un valor comprendido entre 0 y 20. 2.1.1 La instrucción if... else. En inglés if significa si condicional, por ejemplo, si llueve me llevaré el paraguas, else significa sino, sino llueve me iré a la playa. Este es el significado que poseen en programación. Su sintaxis es: if (condición (condición) ) instrucción; instrucción;else else instrucción; NOTA: La sintaxis real real del bloque else bloque.
IF es la siguiente: siguiente: if (condición) (condición)
Un programa p rograma ejemplo nos indicará su funcionamiento f uncionamiento con claridad. Supongamos Supongamos que deseamos dividir dos números. El número por el que dividimos no puede ser cero, ésto nos daría un valor de infinito, provocando un error en el ordenador. Por tanto antes de dividir deberíamos de comprobar si el divisor es cero. El programa sería algo como ésto: #include main() { float dividendo,divisor; printf ("\nDime el dividendo:") dividendo:"); ; scanf ("%f",÷ndo) ("%f",÷ndo); ; printf ("\nDime el divisor:"); scanf ("%f",&divi ("%f",&divisor); sor); if (divisor==0)
printf ("\nNo podemos dividir un número por 0"); else printf ("\nEl resultado es: %f",dividendo/divisor); } Como en todas los comandos del lenguaje C una instrucción, instrucción, en general, puede ser solamente una o un conjunto de ellas incluidas entre llaves. Por último el lenguaje C dispone de un operador ternario (de tres elementos) que permite construir determinadas estructuras if-else, en concreto toma un valor u otro dependiendo de una expresión condicional. condicional. Su sintaxis es: exp1 ? exp2 : exp3 Si exp1 es cierta la expresión tomará el valor exp2, sino tomará el valor exp3. Un ejemplo de su utilización: /* La variable z toma el valor máximo entre a y b */ z = ( (a>b) ? a : b); Como se puede observar se trata de una secuencia if else pero muy concreta, probablemente probablement e el compilador generará un código mucho más eficiente para este tipo de secuencia de ahí su inclusión en el juego de operadores del C. A continuación se describirán las instrucciones instruccione s que nos permiten controlar el flujo de programa, en las cuales tendremos que utilizar expresiones condicionales condicionale s continuamente, continuament e, por lo cual no insistiremos más en este tema. 2.2 Control del flujo de programa 2.2.0 Introducció Introducción n A estas alturas el lector ya debería conocer lo que es el flujo de programa. El flujo de programa es la secuencia de instrucciones instrucciones que un programa ejecuta desde su comienzo hasta que finaliza. En principio la ejecución es secuencial, comienza con la primera instrucción y termina con la última. Sin embargo es común que nos interese que nuestro programa no termine con la última de las instrucciones instruccione s (si por ejemplo no podemos abrir un fichero y la función del programa es modificar ese fichero, el programa no debería realizar ninguna operación y terminar al detectar el error), o puede que nos interese que un grupo de instrucciones instruccion es se ejecute repetidamente repetidamen te hasta que le indiquemos que pare. Todo esto se puede conseguir con las instrucciones que se describirán a continuación. 2.2.1 Creación de bucles de ejecución. 2.2.1.0 Concepto de bucle En la introducción introducción ya se ha mencionado lo que es un bucle. Una secuencia de instrucciones instruccione s que se repite un número determinado de veces o hasta que se cumplan unas determinadas condiciones. Los bucles bucles son extremadamente extremadamente algunos ejemplos son:
útiles en nuestros nuestros programas, programas,
* Lectura/Visualización Lectura/Visualización de un número determinado como por ejemplo una matriz.
de datos,
* A veces se hace necesario introducir esperas en nuestros programas ya sea por trabajar con un periférico lento o simplemente por ralentizar su ejecución. Los primeros se llaman bucles de espera es pera activa y los lo s segundo seg undo bucles vacíos. * En aplicaciones gráficas rellenado de polígonos.
como
trazado
de
líneas
o
* Lectura de datos de un fichero... A continuación c ontinuación describiremos describirem os las la s opciones op ciones que nos proporciona el lenguaje de programación C para crear y gestionar los bucles. 2.2.1.1 Bucle for La primera opción de que disponemos es el bucle for. Este tipo de instrucción se halla presente en la mayoría de los lenguajes de programación estructurados, estructurad os, y permite repetir una instrucción o conjunto de instrucciones un número determinado de veces. Su sintaxis es como sigue: for (exp1;exp2; (exp1;exp2;exp3) exp3) instrucción instrucción; ; exp1 es una expresión que sólo se ejecuta una vez al principio del bucle. El bucle for suele utilizarse en combinación con un contador. contador. Un contador contador es una variable variable que lleva la cuenta de de las veces que se han ejecutado las instrucciones sobre las que actúa el comando for. Por tanto exp1 suele contener una expresión que nos permite inicializar ese contador generalmente a 0 aunque eso depende de para qué deseemos utilizar el bucle. exp2 es la expresión que nos indica cuando debe finalizar el bucle, por tanto tan to se tratará de una expresión condicional. Su interpretación interpretac ión sería algo como; repite la instrucción (o instrucciones) instruccion es) mientras se cumpla exp2. Esta expresión se evaluará en cada ciclo del bucle para determinar si se debe realizar una nueva iteración. NOTA: Hay que recordar que bucle, y no al final. Por tanto NINGUNA vez.
exp2 se evalúa al principio del es posible no ejecutar el bucle
exp3 es una expresión que se ejecuta en cada iteración. Puesto que como ya indicamos el bucle for se utiliza junto a un contador, exp3 en general contiene una instrucción que actualiza nuestro contador. Por tanto en un bucle diferenciadas:
con contador distinguimos tres partes
* La inicialización del contador (exp1). * La condición de fin de bucle (exp2).
* Y la actualización del contador (exp3). El bucle for esta especialmente pensado para realizar bucles basados en contadores. Se puede utilizar en bucle del tipo "repite esto hasta h asta que se pulse puls e una tecla", pero para pa ra estos esto s tenemos instrucciones instruccion es más apropiadas. Veamos unos ejemplos que nos permitan comprender más fácilmente el funcionamiento del comando for. Ejemplo 1: Contar hasta diez. #include main() { int i; /* Esta variable la utilizaremos como contador*/ for (i=0;i<10;i++) printf ("\n%d",i); } Este programa mostrará en pantalla numeros de 0 a 9 (diez en total). exp1 inicializa nuestro contador que en este caso es una variable de tipo entero, con el valor 0, exp2 nos dice que nuestra instrucción (la función printf) debe repetirse mientras el contador sea menor que diez y finalmente exp3 indica que el contador debe de incrementarse incrementarse en una unidad en cada ciclo del bucle. Nos podría interesar contar desde diez hasta 1, en este caso el comando for debería de ser: for (i=10;i>0;i--) printf ("\n%d",i); Ejemplo 2: Visualizar dos tablas de multiplicar en pantalla. #include main() { int int
i; tabla1,tabla2;
tabla1 = 2; /* Primero la tabla del dos */ tabla2 = 5; /* y a continuación la tabla del cinco*/ for (i=1;i<11;i++) (i=1;i<11;i++) { printf ("\n %2dx%2d=%3d",tabla1,i,tabla1*i); printf (" %2dx%2d=%3d",tabla2,i,ta %2dx%2d=%3d" ,tabla2,i,tabla2*i); bla2*i); } } El ejemplo ejem plo es análogo al anterior, anter ior, pero pe ro en este caso visualizamos visualizamo s valores desde uno a diez, en lugar de visualizarlos visualizarlo s de 0 a 9. En este ejemplo, el bucle actúa sobre un conjunto de instrucciones, instrucciones, no sobre una sola, por tanto debemos introducirlas introducirlas entre las llaves para indicar al compilador que la instrucción for actúa sobre las dos instrucciones. instruccione s. Estamos considerando todo lo que se encuentre entre las llaves como una sola instrucción. Para terminar con los bucles de tipo for un leve comentario sobre los bucles añadados, añadados, simplemente simplemente lo que se hace es incluir un un bucle dentro de otro. Supongamos que deseamos introducir los datos
de nuestro jugadores preferidos por teclado para almacenarlos almacenarlo s en el ordenador, y que de cada jugador queremos almacenar por ejemplo, su nacionalidad, nacionalida d, su peso y su altura. En este caso nos sería útil un bucle anidado. El bucle exterior nos contaría jugadores, mientras que para cada jugador tendríamos otro bucle que nos leyera los tres datos que necesitamos. nece sitamos. Si tenemos tene mos veinte vein te jugadores juga dores preferidos, incluiríamos una unas instrucciones como estas: for (i=0;i<20;i++) (i=0;i<20;i++) { printf ("Jugador preferido %d",i); for (j=0;j<3;j++) { leo característica j; la almaceno donde sea; } } Nada más en lo que a bucles de tipo for respecta. A continuación continuació n veremos las otras estructuras que nos proporciona el lenguaje C para la realización de bucles. 2.2.1.2 Bucles while. La sintaxis de este bucle será algo así: while (exp2) instrucción; En inglés while significa mientras, por tanto la línea anterior significaría mientras de cumpla exp2 ejecuta la instrucción. Obviamente la instrucción que ejecutemos debe de permitir que en algún caso se cumpla exp2, de lo contrarío el ordenador permanecería eternamente ejecutando instrucción. instrucción . También es evidente evidente que exp2 exp2 debe ser una expresión condicional. condicional. Como vemos este tipo de bucles no está orientado a contadores, es mucho más genérico, sin embargo embargo se puede puede utilizar utilizar de forma análoga a for. Con la nomenclatura utilizada anteriormente tendríamos algo como ésto: exp1; while (exp2) { instrucción; exp3; } Con este esquema se hace patente la utilidad de la instrucción for para bucles con contador puesto que se "centraliza" todo el proceso de gestión del contador (inicialización (inicializac ión y actualización) actualizaci ón) en una sola instrucción. Un error común al escribir un bucle con contador con una estructura while es olvidar introducir exp3, con lo cual nunca se cumple exp2 y nunca salimos del bucle. De nuevo un bucle infinito aunque a veces nos interesa tener un bucle infinito. La forma más sencilla de realizar un bucle infinito es con la expresión: while (1) instrucción; Como indicamos exp2 es una expresión condicional y para estas expresiones un valor distinto de 0 es verdadero por tanto un 1 es siempre cierto y no hay manera de salir del bucle puesto que es
una constante y ninguna modificación de variables por parte de instrucción tendría repercusiones sobre ella. Los bucle while son útiles en aplicaciones como; lee datos de este fichero mientras no llegues al final ó muestra estos datos en la pantalla mientras no pulse una tecla. Una peculiaridad de esta instrucción es que puede no ejecutarse nunca la instrucción afectada por el while. Algunos ejemplos: Ejemplo 1: Contar hasta diez. #include main() { int
i;
i = 0; while (i<10) { printf ("\n%d",i); i++; } } El primer valor que visualizaremos será el 0. Cuando i tenga el valor nueve la condición i<10 todavía se cumple por lo que entraremos en el bucle de nuevo, visualizaremos el nueve e incrementamos incrementam os i con lo que pasa a tener el valor 10 momento en el cual se vuelve a evaluar la expresión i<10 que en este caso sería falsa y no volveríamos a entrar en el bloque de instrucciones instruccion es (las que están entre llaves). Así visualizamos visualizamos nueve número de 0 a 9 como antes. Si incluyésemos una instrucción para visualizar el valor de i antes de abandonar el programa (justo antes de las últimas llaves el valor que se mostraría sería 10. Ejemplo 2: Lee números enteros hasta que se valor hasta que se introduzca el valor 0.
introduzca el
#include main() { int
numero;
numero = 10; while (numero!=0) { printf ("\nDime un número:"); scanf ("%d",&numero); } } En este ejemplo tenemos que introducir en la variable número un valor distinto de cero antes de entrar en el bucle, puesto que en principio al declarar una variable el valor de ésta no está determinado y podría valer cero, en cuyo caso nunca se ejecutaría el bucle. Esta es la misión de la instrucción numero = 10;.
2.2.1.3 Bucle do.. while Su funcionamiento funcionamien to es análogo al anterior, con la única salvedad de que la condición ahora se evalúa después de ejecutar la instrucción su sintaxis sería: do instrucción while (exp2); Si en el ejemplo anterior utilizamos esta estructura no sería necesario actualizar numero con un valor distinto de cero, puesto que antes de comprobar si es cero leeríamos el valor. #include main() { int
numero;
do { printf ("\nDime un numero :"); scanf ("%d",&numero); } while (numero !=0); La diferencia fundamental con la instrucción anterior es que esta estructura ejecuta la instrucción sobre la que actúa al menos una vez. 2.2.1.4 Modificadores del flujo de programa. Puede que en ciertas ciertas ocasiones ocasiones no nos interese interese que si se da da alguna condición sólo se ejecute un conjunto de todas las instrucciones instruccion es sobre las que actúa el bucle o simplemente salir del bucle antes de llegar a la condición que hayamos indicado en el comando for for o en while. Esto es posible mediante mediante dos dos modificadores: modificadores: continue y break. El primero de ellos, continue, permite volver a reevaluar la condición de salida, es decir, después de ejecutar continue la siguiente instrucción instrucción que se ejecutará será el for o el while. Veamos un ejemplo de aplicación para comprender mejor como funciona este comando. #include main() { int int
numero; contador;
contador =0; do { printf ("\nIntroduce el número %2d:",contador); scanf ("%d",&numer ("%d",&numero); o); if ((numero<0)||(numero>20)) continue; contador++; } while (contador<50); }
Este programa lee números en una variable hasta un máximo de 50, alcanzado este máximo el programa termina. Además si el número no está entre 0 y 20 (si es menor que 0 o mayor que 20) vuelve a pedir que lo introduzcamos. El comando continue en la instrucción if obliga al programa a saltar a la instrucción while donde se vuelve a evaluar la condición, sin pasar por la línea en la que se incrementa el contador. De esta forma se nos vuelve a pedir el mismo número y la entrada incorrecta no es tenida en cuenta. La función de break es ligeramente distinta no salta a la instrucción en la que se evalúa la condición sino que simplemente abandona el bucle y continúa la ejecución en la línea inmediatamente siguiente al bucle. #include main() { int
i;
for (i=0;i<20;i++) (i=0;i<20;i++) { if (i==5) break; printf ("\n%d",i); } printf ("\n\n%d",i) ("\n\n%d",i); ; } La salida de este programa sería algo como esto: 0 1 2 3 4 5 Y con esto terminamos todo lo relacionado con los bucles en lenguaje C. Los bucles son una estructura básica y es necesario utilizarla en la inmensa mayoría de los programas. 2.2.2 Menús de Opciones. 2.2.2.1 Introducció Introducción n La mayoría de los programas permiten realizar una serie de operaciones sobre datos. Lo ideal sería poder indicarle al ordenador que operación op eración deseamos realizar sobre estos es tos datos dat os en lenguaje natural. Por ejemplo, para una base de datos nos podría interesar decirle al ordenador: "Buscame todas la fichas de libros que traten sobre informática" informática" ó "Borra de la base de datos el libro tal". Existen en la actualidad actualidad herramientas herramientas de este tipo, pero pero aún distan distan bastante del lenguaje natural, por ello una de las formas más sencillas de indicar al ordenador la operación que deseamos realizar es utilizar un menú de opciones. La
otra
solución
comúnmente
adoptada
es
una
línea
de
comandos, es decir, escribimos una frase en un lenguaje muy reducido indicando al ordenador lo que deseamos hacer (de una forma similar a como lo hacemos en MS-DOS). Esta solución tiene la desventaja de tener que aprender complicados comandos y distintas secuencias para distintos programas. Un menú nos muestra en pantalla todas las opciones que podemos realizar con nuestro programa de forma que no es necesario que el usuario conozca ninguna serie de ordenes complejas, simplificando simplifican do por tanto el uso de los programas por parte de los usuarios. 2.2.2.2 Sentencia switch-case switch-case-default -default La mayoría de los lenguajes de alto nivel disponen de alguna instrucción que permite gestionar el valor de una variable de una forma estructurada estructurada y sencilla, sencilla, permitiendo permitiendo por tanto la creación creación de menús de programa progr ama de una forma fo rma sencilla. senc illa. En E n lenguaje lengu aje C esta instrucción es switch-case-default. Veamos un ejemplo de como funciona mediante un pequeño programita. #include main() { int printf printf printf printf printf
opcion; ("\nEjemplo de Menú de Programa"); ("\n1.-Cargar fichero de datos"); ("\n2.-Almacenar fichero de datos"); ("\n3.-Modificar datos"); ("\n4.-Salir"); ("\n4.-Salir ");
printf ("\n\nDime tu opción :");scanf ("%d",&opcion); switch (opcion) { case 1: /* Código para cargar fichero de datos*/ break; case 2: /* Código para almacenar datos */ break; case 3: /* Código para modificar datos */ break; case 4: /* Salir del programa */ exit (0); default : printf ("\nSu opción no está disponible"); printf ("\nInténtelo con otra"); } Del ejemplo se deduce fácilmente el funcionamiento funcionamie nto de esta secuencia. El comando switch (var) realizará una bifurcación o salto a la línea indicada por la variable var. Si var vale 2, el programa se seguirá ejecutando a partir de la línea marcada con case 2. Todos los separadores case están separador por un comando break, ya que de no ser así la ejecución seguiría lineal hasta encontrar la llave que
termina el comando switch. La palabra clave default indica el código que se debería ejecutar si el valor de la variable especificada en el switch no corresponde con ninguno de los indicados por los case dentro del switch. Así en nuestro ejemplo si opcion tiene un valor distinto de 1, 2, 3 ó 4, se mostrará en pantalla un mensaje indicando que la opción indicada no es correcta. La sintaxis de esta estructura sería: switch (variable) { case valor1-varia valor1-variable: ble: código asociado; case valor2-varia valor2-variable: ble: código asociado; . . case valorN-varia valorN-variable: ble: código asociado; default: código asociado; } Dentro del código asociado a cada opción se deberán incluir las instrucciones instruccione s break adecuadas. Ya se explicó el funcionamiento funcionamien to de break y continue cuando se habló de bucles. Su funcionamiento funcionamient o es análogo para los comandos for, while, do-while, y switch. Un fragmento de completo sería:
código para
la imprementación imprementac ión
de un menú
while (Opcion!=0) { /* Secuencia de printfs que muestran en pantalla el menú. En este caso la opción 0 debería ser salir */ switch (opcion) { /* Secuencia de cases */ default : /* Mostrar mensaje de error */ } } Por su puesto las aplicaciones aplicaciones del comando comando switch van mucho mucho más allá de la simple creación de menús. Puede ser utilizada en cualquier aplicación en la que se necesite realizar distintas operaciones dependiendo de un valor. Un ejemplo sencillo podría ser un un programa que imprimiese textos en impresora. Podríamos marcar en el texto mediante una secuencia especial como debería ser impreso el texto a continuación. Por ejemplo: @N @n @C,@c @S,@s etc...
Activa Negrita. Desactiva Negrita. Activa/desactiva cursiva. idem sibrayado
Leeríamos estos valores provenientes del teclado o de un fichero y con algo de procesamiento procesamiento y una instrucción switch con dicha variable, en cada case enviaríamos a la impresora la secuencia adecuada para realizar cada una de la opciones.
TEMA 3 : Estructuras de datos estáticas 3.0 Introducció Introducción. n. En este tema se describirán las herramientas que proporciona el lenguaje C para trabajar con tipos y estructuras de datos, flexibilizando flexibiliza ndo de esta forma la creación de programas por parte del programador. 3.1 Matrices Estáticas. La matriz es una estructura de datos básica dentro de los lenguajes de programación programación y conceptualmente conceptualmente son identicas a sus homónimas matemáticas. matemáticas. Por tanto tanto una matriz es es un conjunto conjunto de datos de un tamaño definido que se encuentran consecutivos en memoria y en la que es posible el acceso al elemento que deseemos simplemente con indicar su posición. La declaración de una matriz en lenguaje C es como sigue: tipo_de_dato tipo_de_dat o identificado identificador[tamaño1][ r[tamaño1][tamaño2]...; tamaño2]...; Dónde : tipo_de_dato: Es el tipo de datos que contendrá la matriz. Hasta ahora sólo conocemos los tipos básicos de datos; int, float, double, char. Posteriormente veremos como definir nuestros propios tipos de datos. identificador: Es el nombre que le damos a la variable matriz y po el cual la referenciaremos referenciaremos en nuestro programa. [tamaño] : Indica el número de elementos de tipo tipo_de_datos contendrá la matriz identificador. Si definimos dos tamaños [tamaño1][tamaño2] [tamaño1][tamaño2] nuestra matriz será bidimensional. Algunas declaraciones de matrices serían: /* Matriz de números reales de 10x10 */ float matriz[10][10]; /* Matriz tridimensional de números enteros 20x20x10 */ int Tridimensional[20][20][10]; Como ya se supondrá el acceso a cada elemento de la matriz se realiza especificando su posición, pero ésta comienza a contarse desde el valor 0, es decir, la primera matriz que hemos definido (matriz) tendrá elementos desde el [0][0] al [9][9]. Esto puede causar algunos mal entendidos cuando se trabaja con matrices estáticas. Por ejemplo: a = matriz [2][1]; /* A toma el valor del elemeto (2,1) comenzando a contar desde 0 o del (3,2) si consideramos que el primer valor de la matriz es el (1,1) */ tridimensional [5][16][1] = 67; /* Introduce Intr oduce el e l valor 67 en especificada */ Las variables de declaraciones, declaracion es, se pueden
tipo matriz inicializar
la
ent rada entrada
de
como el resto en el momento
la matriz de las de su
declaración, declaración , ayudándose de inicializaciones inicializaciones múltiples.
las llaves
({}) para
la inclusión
de
int matriz[2][3] = { { 1,2,3 }, { 4,5,6 } }; Estas líneas nos declararían una matriz llamada "matriz" de 2x3 elementos inicializada inicializad a con los valores indicados. Las matrices son extremadamente extremadamen te útiles para trabajar con multitud de problemas matemáticos que se formulan de esta forma o para mantener tablas de datos a los los que se accede accede con frecuencia y por tanto su referencia tiene que ser muy rápida. Supongamos que estamos desarrollando desarrolland o un programa para dibujar objetos en tres dimensiones y más que la exactitud de la representación representac ión (aunque esto es muy relativo), nos interesa la velocidad. En la representación de objetos tridimensionales tridimensio nales se hace continua referencia a las funciones trigonométricas trigonométr icas seno y coseno. El cálculo de un seno y un coseno puede llevar bastante tiempo así que antes de comenzar la representación representac ión calculamos todos los senos y cosenos que necesitemos (por ejemplo con una resolución de 1 grado -360 valores-) cada vez que necesitemos necesitemos uno de estos valores accedemos a la matriz matriz en lugar de llamar a la función que nos lo calcula. Veamos como sería nuestro programa (las funciones sin y cos se encuentran en la librería estandar math.h y sus paramétros están en radianes). #include #include main() { float float int
senos[360]; /* Almacenamos senos */ cosenos[360]; i;
/* Inicializamos las matrices */ for (i=0;i<360; (i=0;i<360;i++) i++) { seno[i] = sin (3.14159*i/ (3.14159*i/180); 180); coseno[i] = cos (3.14159*i/180); } printf ("\nEl coseno de 30 es : %f",coseno[30]); printf ("\nEl seno de 30 es : %f",seno[30]); %f",seno[30]); } 3.2 Tipos compuestos 3.2.0 Introducció Introducción n En muchas ocasiones nos interesaría disponer de variables compuestas de otras variables y trabajar con ellas como si se tratasen de una sola. Un ejemplo típico es una ficha de datos de una agenda. Necesitaríamos Necesitaríam os una variable que nos almacenase el nombre, otra variable que nos almacenase la dirección, otra para el teléfono y así sucesivamente para todos los datos que deseemos mantener. Podríamos disponer de una variable variable para para cada campo campo (cada una de las informaciones informacion es que componen nuestra ficha) pero esto resultaría un tanto engorroso a la hora de su programación.
El lenguaje l enguaje C dispone dis pone de mecanismos mec anismos para trabajar con variables compuestas de otras variables con suma facilidad. Existen dos tipos básicos: estructuras y uniones. 3.2.1 Estructuras de datos. Se trata de la forma más versatil de trabajar con fichas de información. información . Veamos como se definen y posteriormente posteriormen te comentaremos todos los aspectos relevantes de ellas. struct [Nombre_de_la_estructura [Nombre_de_la_estructura] ] { tipo1 campo1; tipo2 campo2; . . tipoN campoN; } [variable]; La palabra palabra clave struct define una una estructura. estructura. Por tratarse de un tipo de datos puede utilizarse directamente para definir una variable. La variable aparece entre corchetes puesto que puede ser omitida. Si se especifica una variable, estaremos definiendo una variable cuyo tipo será la estructura que la precede. Si la variable no es indicada definimos un nuevo tipo de datos (struct Nombre_de_la_estructura) Nombre_de_l a_estructura), , que podremos utilizar posteriormente. posteriorme nte. Si es el nombre de la estructura lo que se omite, tendremos que especificar obligatoriamente obligatoriamente una variable que tendrá tendrá esa estructura estructura y no podremos definir otras variables con esa estructura sin tener que volver a especificar todos los campos. Lo que se encuentra dentro de las llaves es una definición típica de variables con su tipo y su identificador. identificado r. Todo esto puede parecer un poco confuso pero lo aclararemos con unos ejemplos. struct punto { float x; float y; int color; } punto_de_fuga; Aquí estamos definiendo una variable llamada punto_de_fuga cuyo tipo es una estructura de datos formada por tres campos y a la que hemos llamado punto. Dos de ellos son de tipo float y representan las coordenadas del punto, el tercer valor es un entero que indica el color de ese punto. En este caso hemos definido una variable y una estructura. Al disponer de un identificador para esta última podemos definir nuevas variables de esta estructura. struct punto struct punto
origen1; final1;
Donde origen1 y final1 son variables de tipo struct punto que hemos definido anteriormente. anteriorment e. Si en la definición de punto_de_fuga punto_de_fu ga no se hubiese hub iese incluído in cluído un u n identificador ident ificador para la estructura (en este caso el identificador es punto), no podríamos definir nuevas variables con esa estructura ya que no estaría identificada por ningún nombre.
También podríamos haber excluído el nombre de la variable (punto_de_fuga). (punto_de_f uga). En este caso lo que definiríamos definiríamo s sería una estructura llamada punto que pasaría a ser un nuevo tipo disponible disponible por el usuario. Así los tipos de variables de que dispondríamos ahora serían: int float double char struct punto Por tanto podríamos definir cualquier tipos o incluso definir matriz de estos tipos. struct punto
variable con
estos
matriz_de_puntos[30]; matriz_de_pu ntos[30];
Así estaríamos definiendo una matriz de 30 elementos en la que cada elemento es una struct punto con sus tres campos. Lo que ahora nos interesa es saber como referenciar esos campos y acceder o modificar, por tanto la información que contienen. Esto se consigue separando el identificador identificad or del campo de la variable mediante un punto. Así: punto_de_fuga.x = 0; punto_de_fuga.y = 0; punto_de_fuga.color = 10; inicializa la cada uno de los campos de la variable punto de fuga con sus valores correspondientes. Está claro que para acceder a los campos necesitamos alguna variable cuyo tipo sea nuestra estructura. Si no tenemos variable no tenemos información (sería como hacer int = 6). En el caso de la matriz tenemos tantas variables de tipo struct punto como las indicadas, puesto que el punto separa el nombre de la variable del campo al que queremos acceder, la forma de modificar una entrada de la matriz sería: matriz_de_puntos[4].x = 6; matriz_de_puntos.x[4] = 6; /* No sería correcto */ Esta última declaración estructura de un tipo como:
se
podría
utilizar
con
una
struct otra { float x[10]; } matriz_de_puntos; Con lo cual accederíamos accederíamo s al cuarto elemento del campo x de matriz_de_puntos que es una variable de tipo struct otra constituida por una matriz de diez floats. Para terminar con la declaración struct indicar que es posible la declaración de estructuras anidadas, es decir, un campo de una estructura puede ser otra estructura. struct vector
{ float float float };
x; y; z;
struct poligono_cuadrado poligono_cuadrado { struct vector p1; struct vector p2; struct vector p3; struct vecto p4; }; struct cubo { struct poligono_cua poligono_cuadrado drado int struct vector }; struct cubo
cara[6]; color; posicion;
mi_cubo;
Hemos declarado una variable (mi_cubo) de tipo struct cubo que es una estructura conteniendo un valor entero que nos indica el color de nuestro objeto, una variable de tipo struct vector (posicion) indicando la posición del objeto en un espacio de tres dimensiones (posicion tiene tres campos x,y,z por tratarse de una struct vector) y una matriz de seis elemento en la que cada elemento es un struct poligono_cuadrado, poligono_cu adrado, el cual está formado por cuadro vectores que indican los cuatro vértices del cuadrado en 3D. Para aceder a todos los campos de esta variable necesitaríamos sentencias del tipo. mi_cubo.color = 0; mi_cubo.posicion.x mi_cubo.pos icion.x = 3; mi_cubo.posicion.y mi_cubo.pos icion.y = 2; mi_cubo.posicion.z mi_cubo.pos icion.z = 6; mi_cubo.cara[0].p1.x mi_cubo.car a[0].p1.x = 5; /* Ahora acedemos a la coordenada 0 del tercer polígono de la cara 0 de mi_cubo*/ mi_cubo.cara[0].p3.z mi_cubo.car a[0].p3.z = 6; .... 3.2.2 Estructuras solapadas. union La definición de una union es analoga a la definición de una estructura. La diferencia entre ambas es que los campos que especifiquemos especifique mos en una union ocupan todos la misma posicion de memoria. Cuando se declara una union se reserva espacio para poder almacenar el campo de mayor tamaño de los declarados y como ya se dijo todos los campos ocupan la misma posición en la memoria. Veamos un ejemplo. union ejemplo { char int } mi_var;
caracter; entero;
mi_var es una variable cuyo tipo es union ejemplo, y el acceso a cada campo de los definidos se realiza igual que en las struct mediante la utilización de un punto. Hasta aquí nada nuevo lo que sucede es que carácter y entero (los dos campos) ocupan la misma posición de memoria. Así: mi_var.entero = 0; /* Como el tipo int ocupa más que el tipo char ponemos a 0 toda la union */ mi_var.caracter mi_var.carac ter = 'A'; /* El código ASCII de A es 65, por tanto ahora mi_var.entero = 65 */ mi_var.entero = 0x00f10; Esta última instrucción introduce un valor en hexadecimal en la variable mi_var.entero. El código hexadecimal se representa en C anteponiendo al número los caracteres 0x. Para comprender lo que realiza esta instrucción veamos un poco como el ordenador representa representa los número internamente. Todos hemos oido alguna vez que el ordenador sólo entiende ceros y unos, pues bien, lo único que significa ésto es que el ordenador cuenta en base dos en lugar de hacerlo en base diez como nosotros. Cuando contamos en base diez comenzamos en 0 y al llegar a nueve añadimos una unidad a la izquierda para indicar que llegamos a las centenas y así consecutivamente. consecutivamente. Cada cifra de un número en base diez representa esa cifra multiplicada por una potencia de diez que depende de la posición del dígito. Es lo que se llama descomposición factorial de un número. 63452 = 6*10^4+3*10^3+4*10^2+5*10^1+2*10^0= = 60000+3000+4 60000+3000+400+50+2 00+50+2 Como nuestro ordenador en lugar de contar de diez en diez cuenta de dos en dos cada cifra es una potencia de dos. El sistema de numeración en base dos se denomina sistema binario. b100101 = 1*2^5+0*2^4+0*2^3+1*2^2+0*2^1+1*2^0= = 32 + 0 + 0 + 4 + 1 = 37 Así es como representa el ordenador el número 37 en su sistema binario. Cada una de las cifras de un número binario se denomina BIT (BInary digiT) y los ordenadores los suelen agrupar el grupos de 8. Así 8 bits se denomina un byte, 16bits serían 2 bytes y se denomina word o palabra y así sucesivamente. El mayor número que podríamos representar byte (8bits) sería:
en binario con 1
b11111111 = 255 Este es el tamaño que el lenguaje C asigna al tipo char, que sólo puede representar 256 valores distintos, desde 0 a 255. El tipo int short suele ocupar una palabra es decir, 16 bits. Así con 16 bits el mayor número que podemos representar es: b1111111111111111 = 65535 NOTA: El tamaño asociado a cada tipo de específico de cada compilador/ordenador. compilador/o rdenador. No debería supuesto... Los números
en binario rápidamente rápidamente se
datos es muy darse nada por
hacen muy largos por
ello se utilizan otros sistemas de numeración que permitan una escritura más compacta sin perter la información binaria en gran medida. Esto sistemas son en general sistemas con bases que son potencias de dos. Así tenemos el sistema octal (base 8) y el sistema hexadecimal (base 16). Este último es el más ampliamente usado, disponemos de 16 cifras de 0 a F(15) y la característica característ ica más importante de este sistema es que cada cifra hexadecimal, representa cuatro bits binarios, con lo cual el paso de un sistema al otro es extremadamente extremadamente fácil. Volvamos ahora a la instrucción anteriormente indicada mi_var.entero = 0x00f10; Si pasamos este número a binario obtenemos: 0 -> 0000 f -> 1111 -> 15 en decimal 1 -> 0001 -> 1 en decimal 0f10 <-> 0000111100010000 -> 3856 en decimal Como dijimos anteriormente anteriormente un char ocupa 8 bits y un int ocupa 16, como la union los solapa tendríamos un esquema en la memoria del ordenador como éste: int char
000011110001 0000 0000111100010000 00010000
-> 3856 -> 65 ('A')
Así mi_var.caracter contendrá el valor A, pero mi_var.entero contendrá el valor 3856. NOTA: Como ya se indicó en la nota anterior, el tamaño asignado a cada tipo depende del ordenador y del compilador. Además, algunos ordenadores almacenan los números en formato Bajo/Alto (los 8 bits e Intel) y otros en formato Alto/Bajo (Motorola, Sparc, etc.). Este tipo de estructura se suele utilizar en aplicaciones a bajo nivel en la que es necesario poder utilizar este tipo de solapamiento solapamient o de bits. Como C omo ya se habrá habr á podido podid o comprobar compr obar para compremder mínimamente como funciona esto es necesario bajar mucho al nivel de la máquina con la consiguiente consiguiente complicación de la explicación. 3.2.3 Tipos definidos por el usuario. Con las palabras clave struct y union, podemos definir nuevos tipos de variables pero tenemos que indicar estos tipos con todo su nombre, es decir, struct mi_struct. El lenguaje C dispone de un comando que nos permite dar el nombre que nosotros deseemos a cualquier tipo de variable. El comando es typedef y su forma de utilización es como sigue: typedef tipo nuevo_tipo Algunos ejemplos para aclarar las cosas: typedef unsigned char typedef struct cubo
BYTE; HEXAHEDRO;
Así con estas definiciones una declaración de las siguientes variables: BYTE HEXAEDRO
var1,var2; var3;
Sería equivalente a: unsigned char struct cubo
var1,var2; var3;
TEMA 4 : Punteros y funciones 4.0 Introducció Introducción n En este tema estudiaremos el tipo de dato más importante dentro del lenguaje C. Los punteros. Absolutamente Absolutament e todos los datos en C pueden ser tratados como punteros y por ello este lenguaje proporciona una serie de importantes herramientas para trabajar con ellos. Además introduciremos introducirem os el concepto de función asociado estrechamente estrechamente a la llamada programación programación modular que nos permite crear un programa programa mucho más claro y fácil fácil de corregir a la hora de encontrar errores. 4.1 Punteros 4.1.1 ¿ Qué son los punteros ? Como su nombre indica un puntero es algo que apunta, es decir, nos indica dónde se encuentra una cierta cosa. Supongamos (como otras tantas veces) que disponemos de un gran archivo en el que almacenamos a lmacenamos informes. Este fichero fi chero está dividido d ividido en compartimientos, compartimie ntos, cada uno de los cuales contiene uno de nuestros informes (esto sería equivalente a las variables con las que hemos trabajado hasta ahora -informes-, la cuales contienen información, y el archivo representa la memoria de nuestro ordenador, obviamente las variables va riables se almacenan alma cenan en e n la memoria). Sin embargo em bargo otros compartimientos compartimie ntos no contienen informes, sino que lo que contienen es una nota que nos dice dónde está ese informe. Supongamos que como máximo trabajamos con tres informes a la vez, digamos que no nos gusta leer demasiado, y reservamos, reservamos, por tanto, tres compartimientos compartimient os en los indicamos en que compartimiento compartimien to se encuentran encuentran esos tres informes. Estos tres tres compartimientos compartimientos serían serían nuestros punteros y como ocupan un compartimiento compartimient o en el archivo (nuestra memoria) son realmente variables, pero variables muy especiales. Estas variables punteros ocupan siempre un tamaño fijo, simplemente contienen el número de compartimiento compartimie nto en el que se encuentra la información. No contienen la información en sí. Si en nuestro archivo pudiésemos almacenar un máximo de 20.000 hojas, esta sería la capacidad de nuestra memoria (unos 19 Kilobytes). Estas hojas de nuestros informes las agruparíamos de distintas formas. Quizá un informe sólo ocupe 5 páginas mientras que otro puede ocupar 100. Podemos ver esto como los distintos tipos de datos del C, es lógico pensar que necesitamos más espacio para almacenar un número real que uno entero o que una matriz de 20x20
elemento. Estos son nuestro informes en nuestro archivo. Sin embargo los punteros siempre ocupan lo mismo, en nuestro ejemplo nos llegaría con una página para poder escribir el número del compartimiento compartimiento en el que se encuentra el inicio del informe. Así en nuestro supuesto de que sólo trabajemos con tres informes a la vez, dispondríamos dispondríamos de tres compartimientos compartimientos en los los que indicaríamos indicaríamo s dónde se encuentran esos informes que buscamos y de esta forma cuando terminemos con ellos y deseemos trabajar con otros sólo tendremos que cambiar el contenido de esos tres compartimientos diciendo donde se encuentran los nuevos informes. De esta forma no es necesario reservar unos compartimientos compartimie ntos para trabajar y cada vez que cambiemos de trabajo llevar los informes viejos a su compartimiento compartimiento anterior anterior y traer los nuevos nuevos informes informes a estos compartimientos. Esto es lo que en programación programación se conoce como referencia indirecta o indireción. Accedemos a la información a través de un puntero que nos dice dónde se encuentra ésta. Y a grandes rasgos ésto son los punteros, referencias indirectas a datos en la memoria del ordenador. Los punteros en C son muy importantes puesto que su utilización es básica para la realización realización de numerosas operaciones. operaciones. Entre ellas: paso de parámetros que deseamos sean modificados, tratamiento de estructuras dinámicas de datos (ésto es, variables que no se declaran en el programa y se crean durante la ejecución del programa), cadenas de caracteres ... 4.1.2 Operadores que actúan sobre punteros. El lenguaje C proporciona dos operadores relacionados directamente con los punteros. El primero de ellos es el operador &. Ya hemos visto este operador antes en las llamadas a la función scanf, posteriormente explicaremos por que la función scanf necesita ser llamada con el operador &. El operador &, es un operador unario, es decir, actúa sobre un sólo operando. Este operando tiene que ser obligatoriamente obligatoriamente una estructura direccionable, direccionable , es decir, que se encuentre en la memoria del ordenador. Estas estructuras son fundamentalmente fundamentalme nte las variables y las funciones, de las que hablaremos posteriormente. posteriormente. Decimos que sólo se puede aplicar sobre estructuras direccionables direccionabl es porque su función es devolver la posición de memoria en la que se encuentra dicha estructura. estructura. En nuestro ejemplo nos indicaría cual sería el compartimiento en el que se encuentra el informe que le indiquemos. El segundo operador es el *. También se trata de un operador unario como el anterior y su función en este caso es la de permitir el acceso al contenido de la posición indicada por un puntero. En nuestro ejemplo el operador * nos permitiría leer o escribir el informe al que apunta uno de nuestros compartimientos compartimien tos punteros. Además el carácter * se utiliza para declarar punteros los cuales como ya dijimos tienen que ser declarados (tienen su propio compartimiento compartimiento en el archivo). Por supuesto el operador * debe ser aplicado sobre un puntero, mientras que el operador & sobre una estructura direccionable direccionable (variable (variable o función). Veamos un un ejemplo de su utilización:
main () { int x,y; int *px;
/* Variables de tipo entero */ /* Puntero a una variable de tipo entero */
/* Leemos la dirección -compartimiento- de la variable -informe- x mediante & y lo almacenamos en la variable puntero px */ px = &x; /* px contiene la dirección en la que se encuentra x */ /* Utilizando el operador *, podemos acceder a su información. *px representa ahora el valor de la variable x */ *px = 10; /* Ahora x contiene el valor 10 */ y = 15; /* Si ahora hacemos que nuestro puntero apunte a la variable y utilizando de nuevo el operador & */ px = &y; /* El valor que ahora toma *px será el valor de y puesto que es el compartimie compartimiento nto al que ahora estamos apuntando */ *px = 125; /* Ahora y contiene el valor 125 */ x = *px /* Ahora x contiene también 125 */ } Como hemos visto en este es te ejemplo eje mplo es exactamente ex actamente igual acceder a una variable que utilizar un puntero que apunte a ella (hacemos que apunte a ella mediante el operador &) junto con el operador *. Pero el lenguaje C aún ofrece otra herramienta más para trabajar con punteros. Es lo que se suele llamar aritmética de punteros. Este tema lo trataremos en profundidad en el siguiente apartado. 4.1.3 Punteros y matrices Ya hemos hablado de las matrices en el tema anterior. Se trataba de un un conjunto de un número de terminado de variables de un mismo tipo que se referenciaban referenciaban con un nombre común seguido de su posición entre corchetes con relación al primer elemento. Todas las entradas de una matriz están consecutivas en memoria, por eso es muy sencillo acceder al elemento que queramos en cada momento simplemente indicando su posición. Sólo se le suma a la posición inicial ese índice que indicamos. Es un ejemplo que casa perfectamente perfectamen te con nuestro ejemplo de los informes, cada informe podría ser considerado como una matriz de tantos elementos como páginas tenga el informe y en en los los que que cada cada uno de ellos es un un tipo tipo de datos llamado página. Las matrices son realmente punteros al inicio de una zona consecutiva de los elementos indicados en su declaración, por lo cual podemos acceder a la matriz utilizando los corchetes como ya vimos o utilizando el operador *. elemento[i] <=> *(elemento +i) Como ya se ha comentado todos los punteros ocupan lo mismo en memoria, el espacio suficiente para contener una dirección, sin embargo cuando se declaran es necesario indicar cual es el tipo de datos al que van a apuntar (entero, real, alguna estructura definida
por el usuario). En nuestro ejemplo tendríamos un tipo de puntero por cada tipo de informe distinto, un puntero para informes de una página, otro puntero para informes de 2 páginas y así sucesivamente. En principio esto es irrelevante por que una dirección de memoria es una dirección dirección de memoria, independiente independientemente mente de lo que que contenga contenga con lo cual no sería necesario declarar ningún tipo, pero esta información es necesaria necesaria para implementar la aritmética aritmética de punteros que ejemplificaremos a continuación. continuación. Supongamos que hemos definido un tipo de datos en nuestro programa que fuese página, si cada página puede contener 80 caracteres de ancho por 25 de alto, podría ser algo como ésto: typedef char página[80][25]; Y supongamos también que sólo tenemos tres informes, de 1 página, de 5 páginas y de 25 páginas: typedef página typedef página typedef página Y en nuestro siguientes variables: main() { página informe1 informe2 informe3
tipos
de
informe1; informe2[5]; informe3[25]; informe3[25] ; programa
principal
hemos
declarado
las
*punt_página; i1[10],*punt1; i3[5],*punt2; i4[15],*punt3;
.... Por tanto disponemos de un puntero a páginas y tres punteros, uno para cada tipo de informe y tres matrices de distintos tipos de informes que nos permiten almacenar en nuestro archivo un máximo de 30 informes (10 de 1 página, 5 de 5 páginas y 15 de 25 páginas). Supongamos que en el programa principal se llenan esas matrices con datos (por teclado o leyendo de un fichero, por ejemplo) y realizamos las siguientes operaciones: punt_página = (página *) &i4[0]; punt3 = (informe3 *)&i4[0]; Los cast (que comentamos en el tema 1) convierten las direcciones al tipo apropiado, las direcciones que contendrán punt_página y punt3 serán exactamente iguales, apuntarán al principio del primer informe de tipo3. Sin embargo punt_página es un puntero de tipo página y punt3 es un puntero de tipo informe3, ¨qué significa ésto?. Si ejecutásemos una instrucción como ésta: punt_página = punt_página + 5; punt_página pasaría a apuntar a la quinta página del primer informe de tipo 3 (i4[0]), puesto que punt_página es un puntero de paginas. Mientras que si la operación fuese: punt3 = punt3 + 5;
punt3 pasaría a apuntar a el quinto informe de tipo 3 (i4[5]), puesto que punt3 es un puntero a informes de tipo tres. Si ahora realizásemos la operación: punt_página = (página *)punt3; Ahora punt página apuntaría a la primera página del quinto informe de tipo 3. En esto consiste la aritmética de punteros, cuando se realiza una operación aritmética sobre un puntero las unidades de ésta son el tipo que se le ha asociado asociado a dicho dicho puntero. puntero. Si el puntero es de tipo página operamos con páginas, si es de tipo informes operamos con informes. Es evidente que un informe de tipo 3 y una página tienen distintos tamaños (un informe de tipo 3 son 25 páginas por definición). definición). Como hemos visto las matrices se pueden considerar como punteros y las operaciones con esos punteros depende del tipo asociado al puntero, además es muy recomendable utilizar el cast cuando se realizan conversiones de un tipo de puntero a otro. 4.1.4 Punteros y cadenas de caracteres Como su propio nombre indica una cadena de caracteres es precisamente precisament e eso un conjunto consecutivo de caracteres. Como ya habíamos comentado los caracteres se codifican utilizando el código ASCII que asigna un número desde 0 hasta 255 a cada uno de los símbolos representables representabl es en nuestro ordenador. Las cadenas de caracteres utilizan el valor 0 ('\0') para indicar su final. A este tipo de codificación se le ha llamado alguna vez ASCIIZ (la Z es de zero). Las cadenas de caracteres se representan entre comillas dobles (") y los caracteres simples, como ya habíamos indicado con comillas simples ('). Puesto que son un conjunto consecutivo de caracteres la forma de definirlas es como una matriz de caracteres. char
identificador[tamaño_de_la_cadena];
Y por ser en esencia una matriz todo lo comentado anteriormente anteriormente para matrices y punteros puede ser aplicado a ellas. Así la siguiente definición constituye también una cadena de caracteres: char
*identificador;
La diferencia entre ambas declaraciones declaracione s es que la primera reserva una zona de memoria de tamaño_de_la_cadena para almacenar el mensaje que deseemos mientras que la segunda sólo genera un puntero. La primer por tratarse de una matriz siempre tiene un puntero asociado al inicio del bloque del tamaño especificado. especificado . Podemos tratar a las cadenas como punteros a caracteres (char *) pero tenemos que que recordar recordar siempre que un puntero puntero no contiene contiene información información sólo nos indica dónde se encuentra ésta, por tanto con la segunda definición no podríamos hacer gran cosa puesto que no tenemos memoria reservada para ninguna información. Veamos un ejemplo para comprender mejor la diferencia entra ambas declaraciones. declaracione s. Utilizaremos Utilizaremo s dos funciones especiales de stdio.h para trabajar con cadenas. Estas son puts y gets que definiríamos como un printf y un scanf exclusivo para cadenas.
#include main() { char char char
cadena1[10]; cadena2[10]; *cadena;
gets(cadena1); gets(cadena 1); /* Leemos un texto por teclado y lo almacenamos en cadena 1 */ gets(cadena2); gets(cadena 2); /* Idem cadena2 */ puts (cadena1); /* Lo mostramos en pantalla */ puts (cadena2); cadena = cadena1; /* cadena que sólo es un puntero ahora apunta a cadena1 en donde tenemos 10 caracteres reservados por la definición */ puts (cadena); /* Mostrara en pantalla el mensaje contenido en cadena1 */ cadena = cadena2; /* Ahora cadena apunta a la segunda matriz de caracteres */ gets(cadena); gets(cadena) ; /* Cuando llenos sobre cadena ahora estamos leyendo sobre cadena2, debido al efecto de la instrucción anterior */ puts(cadena2); puts(cadena 2); /* SI imprimimos ahora cadena2 la pantalla nos mostrará la cadena que acabamos de leer por teclado */ } En el programa vemos como utilizamos cadena que solamente es un puntero para apuntar a distintas zonas de memoria y utilizar cadena1 o cadena2 como destino de nuestras operaciones. Como podemos ver cuando cambiamos el valor de cadena a cadena1 o cadena2 no utilizamos el operador de dirección &, puesto que como ya hemos dicho una matriz es en sí un puntero (si sólo indicamos su nombre) y por tanto una matriz o cadena de caracteres sigue siendo un puntero, con lo cual los dos miembros de la igualdad son del mismo tipo y por tanto no hay ningún problema. 4.2 Funciones 4.2.1 Introducció Introducción n Hasta el momento hemos utilizado ya numerosas funciones, como printf o scanf, las cuales forman parte de la librería estándar de entrada/salida entrada/salid a (stdio.h). Sin embargo el lenguaje C nos permite definir nuestras propias funciones, es decir, podemos añadir al lenguaje tantos comandos como deseemos. Las funciones son básicas en el desarrollo de un programa cuyo tamaño sea considerable, considerabl e, puesto que en este tipo de programas es común que se repitan fragmentos de código, los cuales se pueden incluir en una función con el consiguiente ahorro de memoria. Por otra parte el uso de funciones divide un programa de gran tamaño en subprogramas subprograma s más pequeños (las funciones), facilitando su comprensión, así como la corrección de errores. Cuando llamamos a una principal main() o desde otra
función función
desde nuestra función lo que estamos haciendo
realmente es un salto o bifurcación al código que le hayamos asignado, en cierto modo es una forma de modificar el flujo de control del programa como lo hacíamos con los comandos while y for. 4.2.2 Definición de funciones Ya hemos visto cual es la estructura general de una función puesto que nuestro programa principal, main() no es otra cosa que una función. Veamos cual es el esquema genérico: tipo_a_devolver identificador (tipo1 parámetro1, tipo2 ...) { tipo1 Variable_Local1; tipo2 Variable_Local2; ... Código de la función return valor del tipo valor a devolver; } Lo primero con lo que nos encontramos es la cabecera de la función. Esta cabecera está formada por una serie de declaraciones. declaraciones. En primer lugar el tipo_a_devolver. tipo_a_devolver. Todas las funciones tienen la posibilidad de devolver un valor, aunque pueden no hacerlo. Si definimos una función que nos calcula el coseno de un cierto ángulo nos interesaría que nuestra función devolviese ese valor. Si por el contrario nuestra función realiza el proceso de borrar la pantalla no existiría ningún valor que nos interesase conocer sobre sobre esa función. función. Si no se especifica especifica ningún parámetro el compilador supondrá que nuestra función devuelve un valor entero (int). A continuación continuación nos encontramos encontramos con el identificador identificador de la función, es es decir, el nombre con el que la vamos a referenciar referenciar en nuestro programas, seguido de una lista de d e parámetros parám etros entre paréntesis y separados por comas sobre los que actuará el código que escribamos para esa función. En el caso de la función coseno a la que antes aludíamos, el parámetro sería el ángulo calculamos el coseno de un cierto ángulo que en cada llamada a la función probablemente probablemente sea distinto. Véase la importancia de los parámetros, si no pudiésemos definir un parámetro para nuestra función coseno, tendríamos que definir una función para cada ángulo, en la que obviamente no indicaríamos ningún parámetro. A continuación nos encontramos el cuerpo de la función. En primer lugar declaramos las variables locales de esa función. Estas variables solamente podrán ser accedidas acc edidas dentro de la función, esto es, entre las llaves ({}). Los nombres de las variables locales pueden ser los mismos en distintas funciones puesto que sólo son accesibles dentro de ellas. Así si estamos acostumbrados acostumbrados a utilizar una variable entera llamada i como contador en nuestro bucles, podemos definir en distintas funciones esta variable y utilizarla dentro de cada función sin que haya interferencias interferencias entre las distintas funciones. Con respecto al código de la función, pues simplemente se trata de un programa como todos los que hemos estado haciendo hasta ahora.
La instrucción return del final puede omitirse si la función no devuelve ningún valor, su cometido es simplemente indicar que valor tomaría esa función con los parámetros que le hemos pasado. En otros lenguajes las funciones que no devuelven valores se conocen como procedimientos. procedimientos. Veamos un ejemplo de definición de una función. int { int
busca_elemento busca_eleme nto (int *vector,in *vector,int t valor,int longitud) i;
for (i=0;i
Cuando el valor que retornan las funciones no es entero, es necesario que el compilador sepa de antemano su tipo por lo cual es necesario añadir al comienzo del programa lo que se llaman prototipos. Los prototipos simplemente son una predeclaración predeclaraci ón de la función, solo indican el tipo que devuelve, su nombre y los tipos de los parámetros, no es necesario indicar un identificador para los parámetros. Un prototipo para la función anterior sería: int busca_elemento (int *, int, int); Los fichero .h que se incluyen con la directiva del procesador #include, contienen entre otras cosas los prototipos de las funciones a las que nos dan acceso. Para finalizar con las funciones vamos a explicar como pasar parámetros que deseamos que la función modifique. Cuando pasamos parámetros a una función ésta realiza una copia de los valores de éstos en una zona de memoria propia, con lo cual la función trabaja con estas copias de los valores y no hay peligro de que se modifique la variable original con la que llamamos a la función, forzando de esta forma a utilizar el valor retornado por la función como parámetro. Sin embargo es posible que nos interese que nuestra función nos devuelva más de una valor o que uno de los parámetros con los que lo llamamos llamamos se modifique modifique en función de las operaciones realizadas por la función. En este caso tenemos que pasar los parámetros como punteros. Cuando pasamos los valores como punteros la función realiza una copia copi a de los valores va lores de los parámetros par ámetros de las funciones en su zona propia de memoria, pero en este caso el valor que pasamos no es un valor en sí, sino que es una dirección de memoria en la que se encuentra ese valor que deseamos se modifique, es decir, creamos un puntero que apunta a la posición que deseamos modificar, con lo cual tenemos acceso a esos valores. Veamos un ejemplo típico de parámetros que deben modificarse, este es la función swap(a,b) cuya misión es intercambiar los valores de los dos parámetros, parámetros, es decir, el parámetro a toma el valor del parámetro b y viceversa. La primera codificación que se nos ocurre sería ésta: swap (int a,int b) { int t; t = a; a = b; b = t; } Y nuestro programa principal podría ser algo como ésto: main () { int c,d; c = 5; d = 7; swap (c,d); }
Veamos que pasa en la memoria de nuestro ordenador. -Función main() -Espacio para la variable c (Posición de memoria x) -Espacio para la variable d (Posición de memoria y) -Inicialización -Inicializa ción de las variables -swap(c,d) -Fin de main() -Función swap -Código de la función swap -Espacio privado para almacenar los parámetros (Posición de memoria z) En este último compartimiento compartimiento es dónde almacenamos los valores de nuestros parámetros que serán respectivamente respectivame nte 5 y 7. Después de la ejecución de swap en esta zona de memoria los valores están intercambiados, intercambiados, nuestro parámetro a que se corresponde con la variable c en la llamada a swap contendrá el valor 7 y el parámetro b correspondiente correspondi ente a d en la función main contendrá el valor 5. Esto es lo que se encuentra almacenado en la zona privada de memoria de la función. Con este esquema cuando la función swap termina su ejecución y se devuelve el control al programa principal main, los valores de c y d no han cambiado, puesto que los compartimientos compartimien tos o posiciones de memoria x e y no han sido tocados por la función swap, la cual sólo ha actuado sobre el compartimiento z. Si declaramos ahora nuestra función swap como sigue: swap (int *p1,int *p2) { int t; t = *p1; *p1 = *p2; *p2; *p2 = t; }
/*Metemos en t el contenido de p1 */ /* Contenido Contenido de p1 p1 = contenido contenido de p2 */
Tendremos el mismo esquema de nuestra memoria que antes pero en lugar de almacenar en la zona privada de la función swap para los parámetros los valores 5 y 7 tenemos almacenados en ella los compartimientos compartimient os en los que se encuentran, ésto es, hemos almacenado las posiciones x e y en lugar de 5 y 7. De esta forma accedemos mediante un puntero a las variables c y d del programa principal que se encuentran en las posiciones x e y modificándolas modificándolas directamente así que al regresar al programa principal sus valores se encuentran ahora intercambiados. En resumen, cuando deseemos que una función modifique el valor de uno de los parámetros con los que es llamada debemos pasar un puntero a esa variable en lugar del valor de esa variable. Es evidente que si implementamos nuestra función de esta forma, los parámetros jamás podrán ser constantes, puesto que difícilmente podríamos modificar el valor de una constante.