TEMA 0: Nociones de Algoritmos 0.1 Visión Histórica. Un algoritmo es el conjunto de operaciones y 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. 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 a la comprensión humana y lenguaje de bajo nivel a aquellos que son más comprensibles 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 en módulos. (Programación 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 coeficientes de mayor a menor grado." | Leer a,b,c | | +-Si sqr(b)>= 4*a*c entonces | | x=(-b+sqrt(b^2-4*a*c))/2a | +-Sino | | Escribir "No existen raíces reales." | +-Finsi
| +-Final 0.4 Organigramas. Un organigrama o diagrama de flujos es una 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 un programa obteniendo para cada 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 íntimamente con las otras, de tal forma que veces la no existencia de una provocaría una confusión en 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 "Introduzca el número a 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) 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 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 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 Circunferencia | | Constante real PI=3.1416 | Variable real r,c | | Escribir "Introduzca el radio de la circunferencia" | 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_Escalar | | Vector entero a[1..3], b[1..3] | Variable entera c | | Escribir "Introduzca el vector A (x,y,z)" | Leer a[1],a[2],a[3] | Escribir "Introduzca el vector B (x,y,z)" | Leer b[1],b[2],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] 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, 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 las entradas a los procedimientos y funciones principales (aunque estos pueden llamar a otros procedimientos 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 "Introduzca el radio de la circunferencia" Bloque | | Leer r del | | c=2*Pi*r Programa+---->Escribir "Su longitud es: ",c | +-Final 2.2 Procedimientos y Funciones. Tanto los procedimientos 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 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 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 (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 "Introduzca el número a factorizar:" | Leer num | | Factor(num,valor) <- Llama al Procedimiento 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 que pueda sufrir dicho parámetro dentro del código Hijo. Ejemplo: +-Algoritmo Factorial Constante | | Variable real num,valor | | Escribir "Introduzca el número a factorizar:" | Leer num | | valor=0 | Factor(num,3) <- Llama al Procedimiento 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 entre parámetros formales y actuales, ahora vamos a realizar una distinción entre variables globales y locales. Podemos 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 cualquiera de los subalgoritmos (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 desde cualquier parte del algoritmo mientras que las locales únicamente serán referenciadas dentro del subalgoritmo al que pertenece: +--------------------+------------------------+ | Variables Locales | Subalgoritmo propio | | Variables Globales | Cualquier subalgoritmo | +--------------------+------------------------+ TEMA 3: Sentencias Las sentencias o instrucciones 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 Procedimientos y Funciones, dentro de estas últimas englobamos todas las funciones y procedimientos que conforman la librería general de sentencias que veremos posteriormente. Las Sentencias Compuestas: - Son aquellas Funciones.
que
están
limitas
dentro
de
Procedimientos
o
Las Sentencias de Control de Flujo: - Sentencias Reiterativas: 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étricas | | chr/toascii | Obtenemos un carácter a partir de un número | | ord | Obtenemos el número correspondiente 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 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 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
+-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, 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 o condición para la entrada/salida del mismo antes de realizar la ejecución de la sección del código a repetir; esto último nos posibilita el caso de no efectuar ninguna iteración (iteración=0). +-Algoritmo Multiplicar (mult1,mult2,resul) | Parámetros reales mult1,mult2,resul | | resul=0 | +-Mientras mult2>0 hacer | | resul=resul+mult1 | | mult2=mult2-1 | +-Finmientras +-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,resul | | resul=0 | +-Repetir | | resul=resul+mult1 | | 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 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 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, 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,resul | Variable Local loop | resul=0 | +-Para loop=1 hasta mult2 de incremento 1 hacer | | resul=resul+mult1 | | 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 principales desde el punto de vista de la organización de los datos que contiene. Para comenzar podemos decir que un ordenador que no tiene la posibilidad de almacenar sus programas y datos en un dispositivo de almacenamiento, (ya sean discos, cintas, etc.) no es mas que una calculadora. En la actualidad, cualquier ordenador posee dispositivos de almacenamiento, ya sean internos (discos duros) o externos (disquetes, cartuchos, cintas, etc.). La finalidad 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 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 (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 tendremos que realizar. Los archivos o ficheros se almacenan en los dispositivos de almacenamiento, para como dijimos anteriormente puedan ser recuperados sus datos en sesiones futuras. De esta forma, podemos definir un fichero como un objeto concebido para el almacenamiento permanente 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 de almacenamiento permanente, a los cuales nos hemos referido como disquetes, discos duros, cintas, CD-ROM, etc. Estos medios de almacenamiento se pueden diferenciar en el modo en el que la información puede ser accesible. Así definimos dispositivos de almacenamiento o soportes secuenciales a aquellos donde la información es accesible 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 a aquellos donde la superficie de almacenamiento es independiente e individualmente direccionable, 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 determinada antes debemos pasar todas las que le preceden. En los dispositivos de almacenamientos hay que diferenciar las direcciones en las que se almacenan los datos. De este modo tenemos dos tipos de 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. 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 nosotros, una vez que abrimos el 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 sobre un dispositivo de almacenamiento. De tal forma que para acceder a un registro determinado 'd' debemos pasar obligatoriamente por todos los registros que le preceden. Suponiendo una cinta 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. Pasemos a ver un secuenciales, 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/Escribir/Reescribir (variable1,variable2) Esta sentencia permite la datos que contiene la variable2 determinado en la variable1.
lectura o re/escritura de los en un fichero de nombre, el
- Iniciar operaciones de lectura y/o escritura en (variable) Esta sentencia debe incluirse después de abrir un fichero y determina prácticamente 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, la función devuelve un cero cuando no se ha producido ningún error. Ejemplo: En este ejemplo se intenta dar aun de ficheros secuenciales.
visión general del uso
+-Algoritmo Copiar_fichero | | Fichero de enteros Origen,Destino | 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) | +-Finmientras | | 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 Estructurados. 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 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 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 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; y donde sus registros poseen un campo que denominaremos campo clave y que identifica inequívocamente 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 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_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,x) | | contador=contador+1 | +-Finmientras | Cerrar(F) | | Escribir ("El fichero:";F;"posee:";contador;"reg.") | +-Final 4.5 Ficheros de Acceso Indexado. Un Fichero de Acceso Indexado es aquel que se encuentra almacenado en un dispositivo direccionable; y donde sus registros poseen un campo que denominaremos campo clave principal y que identifica inequívocamente a cada registro. La clave principal debe ser aquel campo del registro estructurado que tenga siempre un valor diferente a los ya introducidos, 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 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 directa entre los valores del campo clave y el propio registro al que pertenece. Los registros se almacenan ordenados alfabéticamente 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 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, en este caso leería el registro 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 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 (relacionando así inequívocamente al registro 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 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.| +--------------------------+
+------------+---------+ | (Nombre) | (Direc) | +------------+---------+ | CARLOS | 2 | | INMA | 4 | | JOAQUIN | 5 | | JOSE | 6 | | LUIS | 1 | | TERESA | 3 | +------------+---------+ +---------------------------+ | Tabla de Ac. Clave Secund.| +---------------------------+
Obsérvese como ambas tablas aparecen ordenadas alfabéticamente (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 veamos las
como utilizar ficheros sentencias básicas que
- Abrir indexado (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/Escribir/Reescribir (variable1,KEY=,variable2) Esta sentencia permite la lectura o re/escritura 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 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, la 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_Persona. | | +-Registro estructurado datos_personales | | Variable cadena dni | | Variable cadena nombre | | Variable cadena tlf | | Variable cadena provincia | +-Finregistro | | Fichero de datos_personales F | Variable cadena documento | Variable de datos_personales dato | | Escribir "Indique el DNI de la persona a buscar" | Leer documento | | Abrir indexado (F,KEY=dni,nombre) | Iniciar lectura (F) | | Leer (F,KEY=documento,dato) | +-Si error(F)<>0 entonces | | Escribir "Ese registro no existe." | +-Sino | | Escribir " DNI: ";dato.dni | | Escribir " Nombre: ";dato.nombre | | Escribir " Tlf: ";dato.tlf | | Escribir "Provincia: ";dato.provincia | +-Finsi | | Cerrar (F) +-Final 4.6 Función de Hashing. Muchas veces surge el caso del que el fichero es tan grande que la tabla no puede mantener una ordenación eficaz debido a que cuando 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 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
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 que existe en cada Bucket que determina el salto a otro Bucket. Así, cuando se llena el Bucket número 35, existe un salto de éste al número 104 (otro Bucket) que posee datos del mismo tipo, que también puede rebosarse y apuntar a otro Bucket secundario y así sucesivamente. Ahora es cuando surgen los problemas. Cuando un dato se borra de un Bucket hay que reorganizar la información para no dejar espacios en blanco dentro de la tabla. Esto se realiza por medio de un empaquetamiento Packed. Sin embargo cuando se va a realizar muchas modificaciones 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. - 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.
campo clave a través del que han sido aleatorio
a
* Fichero de Acceso Indexado: - Almacenamiento en dispositivo 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. - 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 introducidos 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_selección | | Constante entera n=... | Vector entero a(1..n) | Variable entera x,i,j,h,mayor | | Escribir "Introduzca 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_secuencial | | Constante entera n=... | Vector entero a(1..n) | Variable entera x,i,j,h,mayor | | 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_dicotómica | | Constante entera n= ... | Vector de enteros a(1..n) | Variable entera x,i,j,m,h | | Escribir "Introduzca 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 rel="nofollow">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 | | ... | +-Finregistro | | 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 | +-Finmientras | | +-Mientras (NO fin(A)) hacer | | Escribir (C,a) | | Leer (A,a) | +-Finmientras | | +-Mientras (NO fin(B)) hacer | | Escribir (C,b) | | Leer (B,b) | +-Finmientras | | 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 con el sistema operativo UNIX versión 5. Pero pronto empezaron a surgir muchas implementaciones del C a raíz de la
popularidad creciente de los microordenadores. Por este motivo, se hizo necesario definir un C estándar que está representado 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 aplicaciones tan dispares como: hojas de cálculos, gestores de bases de datos, 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.
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 otros lenguajes como Pascal o Basic y que requiere 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) 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 se puede ejecutar 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 de C son muy sofisticados 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ó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 podrá comunicar con el usuario que lo utilice. Mostraremos el programa y a continuación describiremos cada una de las instrucciones que lo forman. /* Programa : HOLA MUNDO */ #include <stdio.h> 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, todos los compiladores de C disponen de un preprocesador, un programa que examina el código antes de compilarlo y que permite modificarlo de cara al compilador en distintos sentidos. En temas posteriores trataremos en profundidad estas directivas del preprocesador, pero para el desarrollo de los temas anteriores a este debemos conocer al menos la directiva #include. Las directivas se caracterizan 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 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) 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 incorporar todos los 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 el programa. En nuestro caso tenemos un 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 <stdio.h> 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 los datos) a una nueva línea 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 puesto que para C, las comillas representan una 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 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 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) y de las constantes, esto no tiene por que hacerse así pero digamos que se trata de una tradición de la 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 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 es necesario trabajar con grandes 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 que le dicen lo que tiene que hacer con esos datos. Los lenguajes de programación disponen de una serie de tipos de datos básicos, y proporcionan herramientas para crear 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 de cada ecuación con lo que utilizaríamos 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 los tipos básicos que proporciona el lenguaje C y dejaremos para temas posteriores la 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 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 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 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, 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 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 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 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; char letra; Si necesitamos declarar varias variables de un mismo tipo se
pueden incluir en la misma separándolos por una coma: int float
línea todos
los nombres
que deseemos
numero1,numero2,numero3; 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 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 acceder a ellas, como sólo tenemos una 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 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 almacenar un valor float (un número real) en una variable int (entera), para ello tendríamos que convertir ese número real en entero 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 <stdio.h> 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, es decir, en la cadena de caracteres entre comillas dentro del paréntesis nos indica la forma en la que se visualizarán los datos. Ya hablamos de los caracteres especiales como \n, ahora nos ocuparemos de la 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+-xx ó [-]m.ddddE+-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); Visualiza un número real en el formato normal, parte entera y parte decimal separadas por un punto. printf ("%5.2f",numero); 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 que debe visualizar los datos, pero no le hemos dicho que datos debe visualizar. Lo que se escribirá en el lugar indicado por el % está especificado 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. De esta forma un programa como este: #include <stdio.h> 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 en el ejemplo si la precisión especificada en la cadena de formateo es menor que la real del número, la función printf aproxima a la precisión especificada. 1.3 Entrada de datos por teclado El programa anterior para el cálculo de el espacio funciona correctamente, 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 variable en la que deseamos que se lea antecedida por el carácter &. Nuestro programa del móvil se convierte ahora en:
#include <stdio.h> 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 los valores de la velocidad y el tiempo y nos proporcionará el espacio que 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ón En tema anterior aprendimos a trabajar con variables, leer sus valores por teclado, visualizarlas en pantalla y realizar operaciones elementales con ellas. Los programas que escribimos hasta ahora se ejecutaban secuencialmente, 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 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 línea de terminada de nuestro código se realice si se cumple una cierta condición. Por esta razón es necesario explicar, antes de comenzar con las instrucciones 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 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, y
análogamente 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 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) instrucción;else instrucción; NOTA: La sintaxis real del bloque else bloque.
IF es la siguiente: if (condición)
Un programa ejemplo nos indicará su funcionamiento con claridad. 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 <stdio.h> main() { float
dividendo,divisor;
printf ("\nDime el dividendo:"); scanf ("%f",÷ndo); printf ("\nDime el divisor:"); scanf ("%f",&divisor); 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, 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. 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 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 que nos permiten controlar el flujo de programa, en las cuales tendremos que utilizar expresiones condicionales continuamente, por lo cual no insistiremos más en este tema. 2.2 Control del flujo de programa 2.2.0 Introducció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 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 (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 se ejecute repetidamente 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 ya se ha mencionado lo que es un bucle. Una secuencia de instrucciones que se repite un número determinado de veces o hasta que se cumplan unas determinadas condiciones. Los bucles son extremadamente algunos ejemplos son:
útiles en nuestros programas,
* 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 activa y los segundo 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 describiremos las opciones 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, y permite repetir una instrucción o conjunto de instrucciones un número determinado de veces. Su sintaxis es como sigue: for (exp1;exp2;exp3) 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. Un contador es una variable que lleva la cuenta 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 se tratará de una expresión condicional. Su interpretación sería algo como; repite la instrucción (o instrucciones) 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 que se pulse una tecla", pero para estos tenemos instrucciones 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 <stdio.h> 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 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 <stdio.h> 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++) { printf ("\n %2dx%2d=%3d",tabla1,i,tabla1*i); printf (" %2dx%2d=%3d",tabla2,i,tabla2*i); } } El ejemplo es análogo al anterior, pero en este caso visualizamos valores desde uno a diez, en lugar de visualizarlos de 0 a 9. En este ejemplo, el bucle actúa sobre un conjunto de instrucciones, no sobre una sola, por tanto debemos introducirlas entre las llaves para indicar al compilador que la instrucción for actúa sobre las dos instrucciones. 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, simplemente lo que se hace es incluir un bucle dentro de otro. Supongamos que deseamos introducir los datos
de nuestro jugadores preferidos por teclado para almacenarlos en el ordenador, y que de cada jugador queremos almacenar por ejemplo, su nacionalidad, 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. Si tenemos veinte jugadores preferidos, incluiríamos una unas instrucciones como estas: for (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 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. También es evidente que exp2 debe ser una expresión condicional. Como vemos este tipo de bucles no está orientado a contadores, es mucho más genérico, sin embargo se puede 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 y 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 <stdio.h> 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 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 (las que están entre llaves). Así 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 <stdio.h> 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 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 <stdio.h> main() { int do
numero; { 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 ocasiones no nos interese que si se da alguna condición sólo se ejecute un conjunto de todas las instrucciones 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 o en while. Esto es posible mediante dos 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 que se ejecutará será el for o el while. Veamos un ejemplo de aplicación para comprender mejor como funciona este comando. #include <stdio.h> main() { int int
numero; contador;
contador =0; do { printf ("\nIntroduce el número %2d:",contador); scanf ("%d",&numero); 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 <stdio.h> main() { int
i;
for (i=0;i<20;i++) { if (i==5) break; printf ("\n%d",i); } printf ("\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ó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 deseamos realizar sobre estos datos 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" ó "Borra de la base de datos el libro tal". Existen en la actualidad herramientas de este tipo, pero aún 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 por tanto el uso de los programas por parte de los usuarios. 2.2.2.2 Sentencia switch-case-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 y sencilla, permitiendo por tanto la creación de menús de programa de una forma sencilla. En lenguaje C esta instrucción es switch-case-default. Veamos un ejemplo de como funciona mediante un pequeño programita. #include <stdio.h> 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");
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 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-variable: código asociado; case valor2-variable: código asociado; . . case valorN-variable: código asociado; default: código asociado; } Dentro del código asociado a cada opción se deberán incluir las instrucciones break adecuadas. Ya se explicó el funcionamiento de break y continue cuando se habló de bucles. Su funcionamiento es análogo para los comandos for, while, do-while, y switch. Un fragmento de completo sería:
código para
la imprementació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 del comando switch van 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 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ón. En este tema se describirán las herramientas que proporciona el lenguaje C para trabajar con tipos y estructuras de datos, flexibilizando 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 y conceptualmente son identicas a sus homónimas matemáticas. Por tanto una matriz es un 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 identificador[tamaño1][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 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] 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 el valor 67 en especificada */ Las variables de declaraciones, se pueden
tipo matriz inicializar
la
entrada
de
como el resto en el momento
la matriz de las de su
declaración, ayudándose de inicializaciones múltiples.
las llaves
int matriz[2][3] = {
({}) para
la inclusión
de
{ 1,2,3 }, { 4,5,6 }
};
Estas líneas nos declararían una matriz llamada "matriz" de 2x3 elementos inicializada con los valores indicados. Las matrices son extremadamente útiles para trabajar con multitud de problemas matemáticos que se formulan de esta forma o para mantener tablas de datos a los que se accede con frecuencia y por tanto su referencia tiene que ser muy rápida. Supongamos que estamos desarrollando un programa para dibujar objetos en tres dimensiones y más que la exactitud de la representación (aunque esto es muy relativo), nos interesa la velocidad. En la representación de objetos tridimensionales se hace continua referencia a las funciones trigonométricas seno y coseno. El cálculo de un seno y un coseno puede llevar bastante tiempo así que antes de comenzar la representación calculamos todos los senos y cosenos que necesitemos (por ejemplo con una resolución de 1 grado -360 valores-) cada vez que necesitemos uno de estos valores accedemos a la 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 <stdio.h> #include <math.h> main() { float float int
senos[360]; /* Almacenamos senos */ cosenos[360]; i;
/* Inicializamos las matrices */ for (i=0;i<360;i++) { seno[i] = sin (3.14159*i/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]); } 3.2 Tipos compuestos 3.2.0 Introducció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 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 para cada campo (cada una de las informaciones que componen nuestra ficha) pero esto resultaría un tanto engorroso a la hora de su programación.
El lenguaje C dispone de mecanismos 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. Veamos como se definen y posteriormente comentaremos todos los aspectos relevantes de ellas. struct [Nombre_de_la_estructura] { tipo1 campo1; tipo2 campo2; . . tipoN campoN; } [variable]; La palabra clave struct define una 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), que podremos utilizar posteriormente. Si es el nombre de la estructura lo que se omite, tendremos que especificar obligatoriamente una variable que tendrá esa 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. 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. Si en la definición de punto_de_fuga no se hubiese incluído un identificador 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). En este caso lo que definiríamos sería una estructura llamada punto que pasaría a ser un nuevo tipo 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];
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 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 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 { struct vector p1; struct vector p2; struct vector p3; struct vecto p4; }; struct cubo { struct poligono_cuadrado 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, 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 = 3; mi_cubo.posicion.y = 2; mi_cubo.posicion.z = 6; mi_cubo.cara[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 = 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 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 = '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 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. 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+400+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. No debería supuesto... Los números
en binario 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 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 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 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
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 de bits. Como ya se habrá podido comprobar para compremder mínimamente como funciona esto es necesario bajar mucho al nivel de la máquina con la 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ón En este tema estudiaremos el tipo de dato más importante dentro del lenguaje C. Los punteros. Absolutamente 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 el concepto de función asociado estrechamente a la llamada programación modular que nos permite crear un programa mucho más claro y 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 informes. Este fichero está dividido en compartimientos, 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 se almacenan en la memoria). Sin embargo otros compartimientos 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, por tanto, tres compartimientos en los indicamos en que compartimiento se encuentran esos tres informes. Estos tres compartimientos serían nuestros punteros y como ocupan un compartimiento 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 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 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 de tres compartimientos en los que indicaríamos 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 para trabajar y cada vez que cambiemos de trabajo llevar los informes viejos a su compartimiento anterior y traer los nuevos informes a estos compartimientos. Esto es lo que en 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 de numerosas 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 una estructura direccionable, es decir, que se encuentre en la memoria del ordenador. Estas estructuras son fundamentalmente las variables y las funciones, de las que hablaremos posteriormente. Decimos que sólo se puede aplicar sobre estructuras direccionables porque su función es devolver la posición de memoria en la que se encuentra dicha 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 punteros. Además el carácter * se utiliza para declarar punteros los cuales como ya dijimos tienen que ser declarados (tienen su propio compartimiento en el archivo). Por supuesto el operador * debe ser aplicado sobre un puntero, mientras que el operador & sobre una estructura direccionable (variable o función). Veamos 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 compartimiento 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 ejemplo es exactamente 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 conjunto de un número de terminado de variables de un mismo tipo que se 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 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 los que cada uno de ellos es un 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 de memoria, independientemente de lo que contenga con lo cual no sería necesario declarar ningún tipo, pero esta información es necesaria para implementar la aritmética de punteros que ejemplificaremos a 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]; 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 a dicho 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). 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 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 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 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 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. Podemos tratar a las cadenas como punteros a caracteres (char *) pero tenemos que recordar siempre que un puntero no contiene 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. Utilizaremos 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
<stdio.h> cadena1[10]; cadena2[10]; *cadena;
gets(cadena1); /* Leemos un texto por teclado y lo almacenamos en cadena 1 */ gets(cadena2); /* 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); /* Cuando llenos sobre cadena ahora estamos leyendo sobre cadena2, debido al efecto de la instrucción anterior */ puts(cadena2); /* 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ó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 (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, 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 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. En primer lugar el 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 esa función. Si no se especifica ningún parámetro el compilador supondrá que nuestra función devuelve un valor entero (int). A continuación nos encontramos con el identificador de la función, es decir, el nombre con el que la vamos a referenciar en nuestro programas, seguido de una lista de parámetros 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 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 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 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 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. Veamos un ejemplo de definición de una función. int { int
busca_elemento (int *vector,int 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 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 se 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 de los valores de los 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, 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 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 es dónde almacenamos los valores de nuestros parámetros que serán respectivamente 5 y 7. Después de la ejecución de swap en esta zona de memoria los valores están 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 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 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 = t; }
/*Metemos en t el contenido de p1 */ /* Contenido de p1 = 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 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 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.