Introducción a la programación Ultima modificación: 1 de marzo de 2004 Autor: Lola Cárdenas Luque
1. ¿Cómo hablo con la máquina? Pongamos que, un buen día, ante tus ojos, tienes una maravillosa máquina que la gente llama ordenador. La miras con cautela (¿morderá?), y lo primero que tus ojos observan, es que consta de varios trozos. Uno de ellos, de verdad, es clavado a la tele de la salita (¿podré ver la peli de la 2 desde aquí?). Otro de esos trozos recuerda a las máquinas de escribir de nuestra infancia (¿le podré mandar una carta a mi abuela?; ¿y por dónde meto el papel?). Sin embargo, en un ordenador "medio", suele haber algo más, que lo primero que recuerda es a una caja grande de zapatos, con la diferencia de que no tiene zapatos dentro. Se le suele llamar "torre" (pues yo no veo ningún castillo) o, simplemente, caja. Si la abrimos, entre todo el maremágnum de cables existente, podemos ver unas plaquitas muy finas. Un consejo: no las rompas. Tienes ante ti, al cerebro pensante (es un decir) de la máquina, y a todos sus súbditos. Entonces, como eres una persona emprendedora, te preguntas, "¿y cómo demonios le convenzo para que se ponga la peli de la 2?", "¿sabrá hacer ganchillo?", "¿podremos hacernos amigos?". Esto último depende de ti, y sólo de ti. Porque, lo primero que tienes que tener pero que muy claro es que la máquina hará única y exclusivamente lo que tú le digas. Lo segundo, es saber que tu máquina tiene limitaciones (lo siento mucho, pero sin brazos dudo mucho que pueda hacerte la comida) y que está diseñada para unas tareas concretas ("¡pero bueno, ¿puedo o no puedo ver la peli de la 2?!"). Lo tercero: tienes que aprender el lenguaje de la máquina. Porque, claro, si le recitas algún poema, nadie te salvará de su gesto de indiferencia: no te entiende. La pregunta lógica, pues, ¿y cuál es el idioma de la máquina? ("¿que no sabrá ruso y mira cómo se lo calla?"). Si eres despierto, habrás observado con especial atención cuando he dicho que hay un maremágnum de cables en alguna parte. No importa tanto lo de la parte como lo de los cables. Bien, veamos: cables. ¿Y yo para qué quiero un cable? Bueno, sí, puedo querer estrangular al perro de la vecina, vale, pero eso no me ayuda a saber cómo hablarle a mi máquina (ni siquiera al perro de la vecina). ¿Por dónde íbamos? Ah, sí... CABLES. Bueno, ahora que lo pienso, por los cables pasa la corriente. Uuummm... CORRIENTE, sí, vamos bien. ¿Y qué le pasa a la corriente? ("ah, yo no sé, pregúntale a ver"). Pues que, como el dinero, *está* o *no está*. Vale, vamos progresando. Si te fijas más aún, en esas placas que te he sugerido que no rompas, hay dibujitos como los de las "pelis" futuristas con un montón de líneas, y hay como una cajita negra grandota con un montón de patitas ("¡Agh, por Diox, una cucaracha!, ¡Traedme el insecticida!") ¡Vaya, pero si de esas patitas salen más de esas líneas! No te engañes: esas líneas son cables, y la cucaracha grande es la que manda. Esa cucarachita está preparada para entender un idioma (no tan rico como el castellano) que le permite hacer sus cuentas, encargarle a alguien que las haga por ella ("¿tiene una calculadora?"), y, lo más importante, por medio de esos cables, dar órdenes a diestro y siniestro a sus súbditos y recibir las respuestas de los mismos. Tiene muchas responsabilidades encima, para ser una cosa tan chica. Y suele responder al nombre de "procesador" (aunque no se gira cuando le llamas).
Sus colaboradores se encargan (si están todos lo cables enchufados como toca, cosa que presuponemos) de hacer cuentas, de enviar señales de vídeo a esa cosa "clavada a la tele de la salita" para que puedas ver desde unas tristes letras blancas en fondo negro (aunque tienen su encanto) a la última novedad en 3D con muchos colorines, de permitirnos que al pulsar una tecla aparezca por esa pantalla, en fin... ¡todo un mundo! Y el procesador, para que le hablen, simplemente necesita que le digas: "0" o "1". ("¡Pero bueno, ¿y para esto tanto rollo con que si lenguajes de programación que si historias?"). ¿Qué significa esto? ¿Por qué, de entre todas las posibles formas de comunicación, ha ido a elegir la más estúpida? Bueno, en realidad no lo es tanto. Lo que quiere decir es, "0": ausencia de corriente. "1": presencia de corriente. Es la manera más fácil. Pero esto no acaba aquí. Como podrás imaginar, los 0 y 1 pueden aparecer de muchas maneras. Ah, ahí has dado con la clave de todo: 0 y 1 son "el alfabeto", pero todas las formas en que pueden aparecer son sus "palabras" (aunque no te recomendaría que asociaras palabra con esto). Ya está claro: tengo que saber hablar en 0 y 1 si quiero tener algún tipo de relación con mi ordenador. Pero no es nada "humano", digamos intuitivo. Lo ideal sería que pudiera hablarle de forma que se parezca más a mi manera de hablar, puesto que no estamos ahora para ir aprendiendo idiomas tan "exóticos". Y entonces llega el ser humano con su ingenio, y decide inventarse un lenguaje más sencillo con el que hablarle a la máquina: y nació el ensamblador. Pero, para una persona cuyo objetivo más ambicioso (por ahora) es escribir un "Hola Mundo", resulta demasiado complicado. ¿Por qué? Pues porque requiere conocer a la máquina como a uno mismo, y si uno mismo no llega a conocerse bien, qué me direis de su máquina. Así, en un alarde de ingenio, el ser humano repite jugada, y se inventa otros lenguajes que, ahora sí, son más parecidos a nuestra forma de pensar. Estos lenguajes, como todo, hay que aprendérselos, pero tienen una ventaja, y es que hay varios puntos en común. Todos constan de unos determinados tipos de variables. Una variable es como una caja, donde ahora puedo meter un zapato y mañana un tomate. Ahora, yo puedo haber etiquetado mis cajas y decir "aquí sólo fruta": será una variable de tipo FRUTA. O, "aquí sólo legumbres": será una variable de tipo LEGUMBRE. Esto es interesante, porque en el momento en que yo vea FRUTA, ya sé que dentro no hay cerveza (y tendré que buscar en otro sitio, jo, qué sed...). Pero no sólo eso: yo sé que tengo muchas cajas para frutas, pero puedo querer bautizarlas ("tú serás la caja de frutas Juana"), y así llamarlas por su nombre. Podré cambiar (o no) su contenido (hoy Juana tiene un melón pero mañana le pondré ciruelas), pero NO lo que puede contener: FRUTA. Esto también lo tiene en común los lenguajes de programación. Sólo que sus variables son de otro TIPO. Pueden ser enteros (sí, como los números que nos enseñaron en la infancia), números con decimales, cadenas de caracteres (no es más que un caracter detrás de otro) y otros que ahora no comentamos (quedan para más adelante). Lo normal es declarar las variables al principio de un programa. Declarar no es más que decir "mira, yo quiero tres variables enteras, y quiero que una se llame Pepi, otra Juani y otra Yoli". A partir de este momento, podrás meter un -4 en Pepi, un 2 en Yoli, y hacer Pepi+Yoli. El resultado será -2. Vaya, esto se pone interesante. ¿Qué más cosas me permite hacer un lenguaje de programación? Pues me permite hacer operaciones conocidas por todos como sumar, restar, multiplicar y dividir. Los lenguajes de programación, cuentan todos en su haber con un juego de "instrucciones". Una instrucción no es más que una orden que nosotros le damos a la máquina.
Y es que, al fin y al cabo, un PROGRAMA no es más que una secuencia de instrucciones (escritas en algún lenguaje de programación) pensado para RESOLVER algún tipo de PROBLEMA. Si no sabemos resolver este problema, no podremos escribir el programa. A ti se te puede ocurrir una manera de resolverlo. A tu vecino, otra. Este METODO con el que resolveis el problema, es lo que se llama ALGORITMO, y es lo que vamos a ver en este cursito cómo se hacen. Un algoritmo no es más que una secuencia de pasos que, seguidos uno a uno, me permiten resolver un problema. Por ejemplo, cuando quiero ver una película de vídeo, podría hacer:
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20.
Elijo una película de las de mi colección. Compruebo SI TV y vídeo están conectados a la red (y procedo). SI la TV está apagada, la enciendo, SI NO, pues no. Y lo mismo con el vídeo. Abro el estuche de la película. Saco la película de su estuche. Introduzco la película en el vídeo. Dejo el estuche sobre el vídeo. SI la TV no está en el canal adecuado, la cambio, SI NO, pues no. Cojo los mandos a distancia (el del TV y el del vídeo). Me pongo cómodo. Pulso PLAY en el mando del vídeo.
¿A qué no se os había ocurrido? Fijaos bien en unos detalles que son fundamentales y que aparecen en este algoritmo: 1. 2.
La descripción de cada paso no me lleva a ambigüedades: los pasos son absolutamente explícitos y no inducen a error. El número de pasos es finito.
Y es que no puedes tener eternamente pensando a la máquina si pretendes que te dé algún resultado en algún momento. Podría poner una definición rigurosa de lo que es un algoritmo, pero me parece que con esto se coge la idea. Notar también que he escrito en mayúsculas las palabras SI y SI NO. Como podeis imaginar, formar parte de la manera en que se escriben algoritmos. Por ahora, os invito a que describais algorítmicamente situaciones cotidianas de vuestra vida. En la próxima entrega haré un resumencito escribiendo las cosas de forma algo más rigurosa
(al principio lo que importa es que se entiendan las ideas), completaré lo dicho sobre las variables, y seguiremos por esos pecaminosos senderos del mundo de la programación: las instrucciones de control de un programa.
2. Las variables, usos y costumbres La vez anterior nos quedamos hablando de variables, aquellas cajitas donde podemos ir almacenando cosas como lechugas, tomates y también... ENTEROS: Como su nombre indica, aquí podremos meter números enteros, tanto positivos como negativos. Cuando decimos que podemos tener tanto enteros positivos como negativos, lo que se suele decir es "enteros con signo". Si sólo vamos a tener enteros positivos (incluyendo el cero), decimos entonces "enteros sin signo". Esta nomenclatura de "con signo" y "sin signo" es aplicable a otros tipos, como el siguiente. REALES: Trex cuartos de lo mismo, aquí podemos poner números con decimales, tanto positivos como negativos. Hay reales con signo y reales sin signo, pero, si no decimos lo contrario, se sobreentiende que son con signo. Obviamente, dado que la memoria de la máquina es finita (vamos, que por muchos megas, incluso Gigas de RAM, algún día ésta se nos acabará), el tamaño de los datos que almacenemos también será finito. Esto quiere decir que, por ejemplo, el número Pi jamás cabrá en la memoria de un ordenador. Por tanto, para cada tipo de dato, según el lenguaje, se especifica cuántas cifras tiene, aunque suele ser unánime. CARACTERES: Si tenemos una variable de tipo caracter, lo que podemos almacenar será un caracter como 'p', 's', '3', '#' y otros. Estos son los tres tipos de datos básicos, y son los que yo usaré. Como ya comenté, las variables se suelen declarar al principio del programa, y esta será la norma que yo voy a usar; toda variable utilizada, debe haber sido previamente declarada. Para ello, escribiré algo como esto:
Declaracion de variables:
ENTERO: i,j,k REAL: x,y,z CARACTER: a,b,c
Fin de la declaracion de variables
Esto querrá decir que las variables i, j, k serán enteras (con signo), que las variables x, y, z serán reales (con signo) y que las variables a, b, c serán caracteres. Al escribir la declaración de variables de esta forma, si alguna se nos ha olvidado, es muy fácil incluirla. Una variable, en el momento que la declaramos, está vacía, hueca, sin vida, sentido ni VALOR. Y hasta que dicha variable no tenga valor, lo mejor será no hacer cosas con ella. Entonces, aquí viene la pregunta: ¿y cómo meto en mi variable un 7? ¿Puedo meter un 23? Para poder meter cosas en las variables, lo que hacemos es asignarles un valor, es decir, "Pepe es una variable de tipo entero. Si a Pepe le asigno 14, a partir de ahora, poner Pepe es tanto como poner 14".
Para ello, nosotros vamos a usar un símbolo, una flechita. Cuando queramos hacer asignaciones, haremos esto:
Pepe <- 14
(asignamos un entero)
Carolina <- -9.65
(asignamos un real)
Juan <- 'a'
(asignamos un caracter)
Y podemos hacer cuantas asignaciones queramos a nuestra variable. Por ejemplo, si hacemos:
Pepe <- 14 Pepe <- 4 Pepe <- -3 Pepe <- 42
al final, en Pepe tenemos almacenado el valor 42. Lo que no debemos hacer es:
Pepe <- 14 Pepe <- 2.5
pues estamos metiendo un valor REAL en una variable que sólo quiere ENTEROS. ¿Qué pasará aquí? Pues depende del compilador. Quizá nos trunque los decimales, quizá los redondee, quizá, simplemente, nos dé un error y no lo permita. En cualquier caso, si queríamos un 2.5 con toda nuestra alma, una variable entera no es lo más recomendable para guardarlo. "Muy bien, ya sé cómo meter cosas en las variables, ahora... ¿qué puedo hacer con mis variables?" Como los caracteres son un tanto especiales, serán tratados aparte, para que no haya confusión. Así que, por ahora, nos vamos a entender con las variables numéricas. Como obviamente parece, podemos sumar, restar, multiplicar y dividir variables. Si dividimos por una variable que en ese momento valga cero, tendremos un error. Pero, por lo demás, podemos hacer todas estas cosas. Por el mismo motivo que he comentado arriba, es mejor no mezclar enteros con reales al hacer operaciones, a no ser que de verdad uno sepa que quiere mezclarlos y por qué quiere mezclarlos. Para estas operaciones, usaremos la notación usual que nos enseñaron ya desde pequeñitos. Entonces, podremos hacer todas estas cosas:
Pepe <- 2
Mari <- 3 Juan <- Pepe*Mari
Tendremos que en Juan está almacenado el valor 2*3=6 :)
Pepe <- 2 Mari <- 3 Juan <- Pepe+Mari
Ahora, en Juan está almacenado el valor 2+3=5 :)
Pepe <- 2 Mari <- 3 Juan <- Pepe-Mari
Y ahora, en Juan está almacenado el valor 2-3=-1 :) Como estos, os podeis poner cuantos ejemplos querais. Además, los compiladores suelen traer librerías matemáticas con muchas funciones más avanzadas (aunque *todas* esas funciones se consiguen a partir de las cuatro operaciones básicas), funciones con las que podemos hacer más operaciones con nuestras variables. Por ejemplo, podemos escribir:
Mari <- Pi Pepe <- sin(Mari)
Y tendremos en Pepe almacenado el valor sin(Pi)=0 Más adelante, os pondré como ejercicio implementar alguna función matemática como el seno, la exponencial (u otras), el algoritmo será muy sencillo, y vereis como todos los cálculos no son más que combinaciones de las cuatro operaciones básicas que nos enseñaron en la escuela. De momento, eso es todo en cuanto a variables numéricas. Las variables de tipo carácter son un poco más especiales, es mejor esperar a tener un poco más de rodaje en otras cuestiones básicas.
3. Estructuras de control; secuenciales y selectivas Bueno, bueno: ya hemos hablado un poco de variables; qué son, y cosas que podemos hacer con ellas. También hablamos un poco por encima de lo que es un algoritmo, pero aún no sabemos cosas sobre ellos. Una posible "definición rigurosa" sería la siguiente (no hay que tomarla como un dogma de fe):
Un algoritmo es una sucesion finita de pasos no ambiguos que se pueden llevar a cabo en un tiempo finito.
Sucesión finita es lo contrario de infinita: esto quiere decir que se acaba en algún momento ;) Pasos no ambiguos son pasos tan claros que hasta una máquina los puede entender, de ahí la necesidad de la no ambigüedad ;) Lo del tiempo finito es una cuestión práctica: no creo que tengamos tanta paciencia (ni que lleguemos vivos, sobre todo) como para ver la salida de un programa que calcule Pi :) No vamos a entrar en filosofías de qué puede ser considerado como un algoritmo, qué no lo es, etc... , no porque no sea interesante en sí, sino porque a nosotros lo que nos preocupa en estos momentos es aprender a resolver problemas, y a eso es a lo que vamos. No hay que confundirse: un algoritmo no es lo mismo que un programa, para hacer un programa necesitamos algo más: unas estructuras de datos. Hay distintas formas de escribir un algoritmo, bien usando un lenguaje específico de descripción de algoritmos, bien mediante representaciones gráficas. Yo he elegido la primera: el pseudocódigo. Que sea un lenguaje específico no significa que haya que aprender un idioma nuevo, lo único que quiere decir es que hay unas cuantas palabras que son clave, y que son las palabras que, de acuerdo a unas reglas muy sencillitas, nos ayudan a describir el algoritmo. La estructura del pseudocódigo es bastante parecida a la de algunos lenguajes de programación (por ejemplo, el Pascal), por ello me ha parecido la más recomendable. El pseudocódigo tiene algunas ventajas:
• •
Es fácil hacer cambios si nos equivocamos en la lógica del programa Es independiente del lenguaje de programación que vaya a usarse; un algoritmo que esté escrito en pseudocódigo es fácilmente traducible a muchos lenguajes de programación.
Y ya, sin más preámbulos, vamos a lo que nos interesa: nosotros queremos resolver problemas con el ordenador, ¿no es así? Pues veamos con qué herramientas nos podemos defender para escribir nuestros propios algoritmos.
Elementos básicos para la descripción de algoritmos El principio y el fin Para delimitar el comienzo y el final de un algoritmo, o de un trozo de algoritmo (por ejemplo, en los bucles, y otros, como vamos a ver), haremos lo siguiente: inicio
... Aqui va el algoritmo en cuestion
fin
También, en vez de inicio y fin se puede usar "empezar" y "fin", o lo que querais, pero siempre quedando clara la intención. No hay que ser estrictamente riguroso con la aplicación del "inicio-fin", muchas veces, una buena indentación hace su papel.
Asignaciones Sobre las asignaciones ya hablamos la vez pasada, al tratar el tema de las variables, vamos a recordarlo brevemente: Cuando hayamos declarado una variable, llegará un momento en el que querremos que la variable tome algún valor. Por ejemplo, tenemos una variable de tipo entero que se llama I y queremos asignarle el valor 3, entonces, escribiremos: I <- 3
Comentarios Poner comentarios de lo que vamos haciendo es muy útil, sobre todo cuando llega la hora de revisar el algoritmo, si no, más de una vez nos encontraremos diciendo "uumm... ¿qué demonios hacía esto?" No cuesta nada documentar el código y nos ahorrará dolores de cabeza :) La convención que seguiremos será poner los comentario entre llaves. Así, {esto será un comentario}
ESTRUCTURAS DE CONTROL Las estructuras de control tienen una finalidad bastante definida: su objetivo es ir señalando el orden en que tienen que sucederse los pasos de un algoritmo. Veamos un ejemplo: supongamos que acabamos de mostrar un mensaje en la pantalla que pregunte al usuario "¿desea seguir adelante?". Obviamente, de la respuesta del usuario va a depender la siguiente acción del programa. Por ejemplo, si este mensaje se nos presenta tras haber pulsado un botón de cerrar la aplicación, si nosotros elegimos "Sí", la aplicación se cerrará, y si elegimos "No", la aplicación seguirá adelante. El programador tiene que haber escrito código para las dos posibilidades, aunque cuando el programa esté funcionando, sólo se elegirá una. Las estructuras de control son de tres tipos: 1. 2. 3.
Secuenciales Selectivas Repetitivas
Estructuras SECUENCIALES. Una estructura de control secuencial, en realidad, no es más que escribir un paso del algoritmo detrás de otro, el que primero se haya escrito será el que primero se ejecute (al implementarlo, por ejemplo). Veamos un ejemplo: queremos leer el radio de un círculo, calcular su área y mostrar por pantalla al usuario el resultado.
Declaracion de variables REAL: radio, area fin declaracion de variables
inicio mostrar por pantalla 'dame el radio del circulo' leer del teclado la variable radio area <- 3.14159*radio mostrar por pantalla 'el area del circulo es:' mostrar por pantalla el contenido de la variable area fin
Notar una cosa: cuando queremos hacer cosas relativas a entrada o salida por algún dispositivo, como el teclado, la pantalla o la impresora, yo pongo cosas como "mostrar por pantalla", o "leer del teclado". Dado que cada lenguaje de programación tiene sus funciones de entrada/salida, no uso la forma particular de ninguno, simplemente, lo que le diríamos al ordenador en caso de que fuera una persona ("oye, por favor, sácame esto por impresora"). Por otro lado, cuando queramos decir que nos muestre el contenido de una variable, yo he puesto "mostrar por pantalla el contenido de la variable area", pero podía haber puesto igualmente "mostrar por pantalla area", o cosas similares. Vosotros elegís la forma que os resulte más clara o más cómoda, lo importante es que las intenciones sean claras, o, lo que es lo mismo, "no ambiguas" ;) Además, si os fijais, para que quede claro que una cosa es mostrar por pantalla una cadena, y otra cosa es mostrar por pantalla el valor de una variable, el texto que aparece entre ' ' es una cadena, mientras que el que no aparece entre ' ' no es una cadena, sino que se refiere a una variable. Ahora vamos a pasar a las estructuras selectivas y a las repetitivas. Para ello, necesitamos primero hablar un poco de... CONDICIONES. La palabra condición nos sugiere frases como "lo haré a condición de que me dejes tu boli". Analicemos esta frase con un poco de detenimiento. Decimos "lo haré", pero, ¿"lo haré" siempre, pase lo que pase? o, por el contrario, ¿"lo haré" si antes hay algo que debe cumplirse? En la frase está claro que no se ha dicho "lo haré" sin más, si no que hay como una pequeña cláusula "SI me dejas tu boli". Ya nos ha vuelto a aparecer el SI; esto parece querer decir que debe ser algo importante. Lo que significa "SI me dejas tu boli" es lo siguiente, en caso de que la acción "dejarme tu boli" sea cierta (o verdad, usaré ambas palabras), yo "lo haré". Pero si la acción "dejarme tu boli" es falsa (o mentira), yo NO "lo haré". En esto reside el "quid" de las estructuras que vamos a ver a continuación: se EVALUA una condición, y se actúa en consecuencia, según que la condición sea VERDADERA o FALSA.
Estructuras SELECTIVAS Estas estructuras se utilizan para TOMAR DECISIONES (por eso también se llaman estructuras de decisión o alternativas). Lo que se hace es EVALUAR una condición, y, a continuación, en función del resultado, se lleva a cabo una opción u otra.
Alternativas SIMPLES Son los conocidos "si... entonces". Se usan de la siguiente manera (una vez más, la escritura es más o menos personal, siempre que las intenciones queden claras para todos): yo quiero evaluar una condición, y si se cumple (es decir, si es cierta), entonces realizaré una serie de pasos. Esto lo podemos escribir así: SI se cumple la condicion, ENTONCES: Hago el paso 1 .... Hago el paso N fin del SI
Muy estrictamente, sería:
SI se cumple la condicion, ENTONCES: inicio Hago el paso 1 .... Hago el paso N fin fin del SI
pero con una buena sangría nos podemos evitar escribir de más ;) Es importante cerrar el SI, ya que, si no se cumple la condición, el programa seguirá a continuación de donde termina el SI. Por ejemplo, queremos calcular la raíz cuadrada de un número; sin embargo todos sabemos (¡supongo! ;)) que la raíz cuadrada de un número negativo NO es un número real. Esto quiere decir que, tras leer el número por teclado, tendremos que ver si es positivo, ya que sólo en este caso podremos calcular su raíz cuadrada. Veamos como queda:
Declaracion de variables REAL: numero, raiz fin declaracion de variables
inicio mostrar por pantalla 'introduce un numero' leer del teclado la variable numero SI numero >= 0 ENTONCES: raiz <- raiz_cuadrada(numero)
mostrar por pantalla 'la raiz cuadrada es:' mostrar por pantalla raiz fin del SI fin
Como extraer una raíz cuadrada es una operación que, en principio, no es elemental, yo he puesto raiz_cuadrada(numero), eso significa que, en alguna parte, tengo definida una función que me calcula la raíz cuadrada de un número. Ya hablaremos de funciones y subrutinas más adelante, pero hago esta aclaración para que conste que raiz_cuadrada no forma parte de ningún tipo de especificación de pseudocódigo ;-) Sin embargo, hubiera sido bonito haber podido avisar al usuario de que no podíamos calcular la raíz cuadrada en caso de que el número fuera negativo, pero no os preocupeis, para ello tenemos las...
Alternativas DOBLES O el famoso trío "si ... entonces ... sino" ;-D Se usan de esta forma: queremos evaluar una condición, si es verdad, realizaré una serie de pasos, y SI NO es verdad (es decir, si es falsa, si es una puerca mentira cochina... ;) ), entonces realizaré otra serie de pasos. Esto lo podemos escribir así:
SI se cumple la condicion, ENTONCES: Hago el paso A1 .... Hago el paso AN y SI NO se cumple la condicion, ENTONCES: Hago el paso B1 .... Hago el paso BM fin del SI
Con esto, nuestro algoritmo para la raíz cuadrada quedaría:
Declaracion de variables REAL: numero, raiz fin declaracion de variables
inicio mostrar por pantalla 'introduce un numero' leer del teclado la variable numero SI numero >= 0 ENTONCES:
raiz <- raiz_cuadrada(numero) mostrar por pantalla 'la raiz cuadrada es:' mostrar por pantalla raiz SI NO es numero >=0 ENTONCES:
{es decir, si numero es negativo}
mostrar por pantalla 'lo siento, no puedo calcular la raiz cuadrada de un numero negativo' fin del SI fin
Alternativas MULTIPLES Es muy probable que tengamos la necesidad de incluir en nuestros programas alternativas con muchas opciones posibles. Esto lo podríamos hacer a base de anidar "si ... entonces ... si no", pero si es un número elevado de alternativas, se hace demasiado farragoso como para resultar inteligibles. Por ejemplo, si queremos hacer un menú que dé a elegir cuatro opciones, un posible algoritmo sería: {Algoritmo MENU a base de 'si ... entonces ... sino'} Declaracion de variables ENTEROS: opcion fin declaracion de variables
inicio mostrar por pantalla 'menu de opciones:' mostrar por pantalla '1. Diccionario de sinonimos' mostrar por pantalla '2. Diccionario de antonimos' mostrar por pantalla '3. Buscar palabra' mostrar por pantalla '4. Salir' leer del teclado la variable opcion
SI opcion = 1 ENTONCES {lo que toque a esta opcion} SI NO, ENTONCES SI opcion = 2 ENTONCES {lo que toque a esta opcion} SI NO, ENTONCES SI opcion = 3 ENTONCES {lo que toque a esta opcion} SI NO, ENTONCES SI opcion = 4 ENTONCES {lo que toque a esta opcion}
SI NO, ENTONCES mostrar por pantalla 'opcion incorrecta' fin del SI fin del SI fin del SI fin del SI fin
Farragoso, ¿verdad? Para evitar esto (¿os imaginais que en vez de cuatro fueran 20 las alternativas?), está la estructura "según sea". La idea es esta; por ejemplo, como en el algoritmo que hemos puesto del menú, pedimos al usuario que entre una opción, y hay cuatro posibilidades, cuatro posibles valores para los que haremos algo. Entonces, SEGUN lo que valga la variable opción, elegiremos una alternativa de entre todas las posibles. La forma de escribir esto en pseudocódigo es:
SEGUN SEA la variable o la expresion HACER VALOR1: {las acciones que toque} ... VALORN: {las acciones que toque} por defecto: {las acciones que toque} fin SEGUN
¿Qué pinta ahí ese 'por defecto'? No es más que una alternativa por si acaso se elige alguna opción no contemplada entre las que se ofrece. Como ejemplo, vamos a ver nuestro menú utilizando la alternativa múltiple SEGUN SEA:
{Algoritmo MENU a base de 'segun sea'} Declaracion de variables ENTEROS: opcion fin declaracion de variables
inicio mostrar por pantalla 'menu de opciones' mostrar por pantalla '1. Diccionario de sinonimos'
mostrar por pantalla '2. Diccionario de antonimos' mostrar por pantalla '3. Buscar palabra' mostrar por pantalla '4. Salir' leer del teclado la variable opcion
SEGUN SEA opcion HACER: opcion = 1 : {lo que toque a esta opcion} opcion = 2 : {lo que toque a esta opcion} opcion = 3 : {lo que toque a esta opcion} opcion = 4 : {lo que toque a esta opcion} por defecto: {mostrar un posible mensaje de error} fin SEGUN fin
Así queda mucho más claro, no sólo para nosotros, si no para cualquier persona que tuviera que leerlo; además, es una forma más sencilla de manejar una alternativa múltiple :) Podemos abreviar un poquito el texto; es igualmente válido poner, dentro del SEGUN, lo siguiente:
SEGUN SEA opcion HACER: 1 : {lo que toque a esta opcion} 2 : {lo que toque a esta opcion} 3 : {lo que toque a esta opcion} 4 : {lo que toque a esta opcion} por defecto: {mostrar un posible mensaje de error} fin SEGUN Me quedan las estructuras repetitivas, que las veremos en la próxima entrega, si no esto puede ser mucho trago de golpe O:) En cualquier caso, ya teneis cosas para ir empezando a pensar en la escritura de algoritmos. Yo sugiero los siguientes...
EJERCICIOS Sobre estructuras secuenciales: 1. 2. 3. 4. 5.
Escribid Escribid Escribid Escribid IVA. Escribid
un un un un
algoritmo algoritmo algoritmo algoritmo
que que que que
calcule calcule calcule calcule
el el el el
área de un triángulo. área de un rectángulo. área de un trapecio. precio de un artículo tras aplicarle un 16% de
un algoritmo que intercambie el contenido de dos variables.
Sobre estructuras selectivas: 1. 2. 3. 4.
Escribid un algoritmo que resuelva una ecuación de segundo grado, teniendo en cuenta todas las posibles alternativas. Diseñad un esquema de menú de opciones, por ejemplo, un menú para seleccionar un libro a leer de entre siete disponibles. Escribid un algoritmo que lea tres números e imprima por pantalla el mayor de ellos. Escribid un algoritmo que lea tres números e imprima por pantalla el menor de ellos.
De momento, eso es todo, cuando tengamos las estructuras repetitivas ya podremos hacer muchas más cosas :) Estoy *especialmente* interesada en el ejercicio 5 de las estructuras secuenciales y en el ejercicio 4 de las selectivas, lo digo por si no podeis hacer toda la faena... ;)
4. Estructuras de control; repetitivas Vamos a ver un poco de teoría primero, porque si no, no sé dónde meterla ni cómo contarla sin que parezca un insulto. Perdonadme que esto sea un pelín rollero las primeras 40 líneas (o así), y ya el resto lo explico con ejemplos. No la contaría si no fuera necesaria O:)
Estructuras REPETITIVAS Este tipo de estructuras marcan como orden de ejecución la reiteración de una serie de acciones basándose en un bucle. Un BUCLE (loop, en inglés) es un trozo de algoritmo cuyas instrucciones son repetidas un cierto número de veces, mientras se cumple una cierta condición que ha de ser claramente especificada. La condición podrá ser verdadera o falsa, y se comprobará en cada paso o iteración del bucle. Básicamente, existen tres tipos de estructuras repetitivas; los bucles "mientras..." (o "while"), los bucles "repetir... mientras que" (o "do... while") y los bucles "desde" (o "bucles for"). Vamos a verlas todas dentro de un ejemplo para clarificar los dos párrafos iniciales, que quedan tan bonitos como oscuros para quien nunca ha visto un bucle (ni es verde ni tiene antenas, lo siento %-D ). Sólo un poco de teoría más antes de pasar a los ejemplos para cada una de las estructuras repetitivas; hay que hacer notar que todo bucle consta de tres partes básicas, a saber:
• • •
Decisión: donde se evalúa la condición y, en caso de ser cierta, se ejecuta el... Cuerpo del bucle: son las instrucciones que queremos ejecutar repetidamente un cierto número de veces. Salida del bucle: es la condición que dice cuándo saldremos de hacer repeticiones ("mientras protestes, seguirás fregando platos", en cuanto dejas de protestar, se acabó fregar más platos).
Una forma de controlar un bucle es mediante una variable llamada CONTADOR, cuyo valor se incrementa o decrementa en una cantidad constante en cada repetición que se produzca. También, en los bucles suele haber otro tipo de variables llamadas ACUMULADOR, cuya misión es almacenar una cantidad variable resultante de operaciones sucesivas y repetidas. Es como un contador, con la diferencia que el incremento/decremento es variable. Sí, sí, sí... ya sé que no me habeis entendido ni jota... ya vamos a por ejemplos, y ahí vereis a qué me refiero.
Una situación REAL como aplicación Vamos a suponer que estamos pensando en un programa que deba REPETIR algunas veces una acción. Por ejemplo, el ordenador se ha portado mal (el güindoze se ha vuelto a colgar, no sé, por decir algo };-D ), y, como castigo, le vamos a hacer imprimir por pantalla 300 MILLONES de veces la frase "Prometo ser bueno O:-)" ¿Cómo lo hacemos? ¿Escribimos 300.000.000 de veces la instrucción pertinente? ¡Vaya martirio! ¡Ni con "Copy'n'Paste"! ¡Se supone que el castigo es para la máquina, no para uno mismo! Pues bien, las ESTRUCTURAS REPETITIVAS vienen a rescatarnos de la tediosa tarea de repetir cientos de líneas que en unas pocas quedan apañadas.
Estructura MIENTRAS(condición) En este tipo de estructura, el cuerpo del bucle (ya sabeis, las acciones que deben ejecutarse repetidas veces) se repite MIENTRAS se cumple una determinada condición, que especificamos entre paréntesis. Su estructura, genéricamente, es esta:
mientras(condición) hacer acción 1 ........ acción N fin mientras
Aplicado a nuestra situación real, sería:
Declaración de variables ENTEROS: Contador fin Declaración variables
inicio
Contador <- 1 mientras(Contador<=300.000.000) hacer mostrar por pantalla 'Prometo ser bueno O:-)' Contador <- Contador+1 fin mientras
fin
Con toda la idea, a la variable que "lleva" (por decirlo de alguna manera) la cuenta de las veces que el bucle se ha ejecutado, la he llamado contador.
Si os fijais, ANTES de entrar en el bucle le asigno el valor 1. Recordad que no es recomendable usar variables no inicializadas, no sabemos qué tienen dentro. En cuanto vemos la palabra "mientras", ya sabemos que hemos entrado en el bucle. Estamos en la decisión. ¿Es Contador<=300.000.000? Yo creo que sí, al menos si es cierto que 1<=300.000.000 Vale, es cierto, así que deben ejecutarse todas las instrucciones del cuerpo del bucle. En nuestro caso, se mostrará por pantalla la frase 'Prometo ser bueno O:-)', y, ATENCION, sumo 1 a la variable Contador. Como Contador valía 1, si ahora le sumo 1, creo que todos estamos de acuerdo en que ahora Contador vale 2. Llegamos al fin del mientras. Eso significa que se han terminado las instrucciones del cuerpo del bucle: debemos volver a evaluar la condición que tenía el "mientras" entre paréntesis para ver qué hacemos ahora. Tenemos que ver si Contador<=300.000.000. Ahora, Contador valía 2, y se cumple que 2<=300.000.000, con lo que vuelve a mostrarse por pantalla la expresión 'Prometo ser bueno O:-)' y de nuevo se suma 1 a Contador, con lo que ahora, Contador pasa a valer 3. De nuevo llegamos al fin del mientras. [... un buen rato después...] Ahora Contador vale 300.000.000. Tenemos que ver si Contador<=300.000.000. Como es cierto que 300.000.000<=300.000.000, se muestra por pantalla el mensaje 'Prometo ser bueno O:-)', y sumamos 1 a Contador. Ahora, Contador vale 300.000.001. Llegamos (una vez más) al fin del mientras, por lo que tenemos que volver a evaluar la condición entre paréntesis que acompaña al "mientras". Hay que ver si Contador<=300.000.000. Pero no es cierto que 300.000.001<=300.000.000, es más bien al revés, o sea, la condición entre paréntesis ES FALSA, ES UNA MENTIRA. ¿Eso qué quiere decir? Pues quiere decir que se acabó, que ya no se ejecuta el cuerpo del bucle, sino que hemos llegado al final, a la salida del bucle, con lo cual, el bucle ha terminado. ¿Me podría decir alguien qué hubiera pasado si no incluimos la línea "Contador <Contador+1" dentro del cuerpo del bucle? Sólo adelanto que se llegaría a una situación que debe evitarse por completo; si tenemos suerte, habrá un error de desbordamiento, en el peor de los casos, el bucle... completad la frase ;)
Una nota importante: Fijaos en una cosa, si, por ejemplo, al empezar nuestro algoritmo, en vez de hacer Contador <- 1 hubiéramos hecho Contador <- 1.000.000.000.000.000.000 aparte de que tenemos posibilidades de salirnos de rango (vamos a suponer que no fuera así), al entrar en el mientras, la condición es "Contador<=300.000.000", y Contador es, claramente, mayor estrictamente que 300.000.000 ¿Qué pasaría aquí? Pues es sencillo: la condición es falsa, luego el cuerpo del bucle se ejecuta CERO veces. ¿Y qué utilidad tiene poner un bucle para que luego no se ejecute? En este caso concreto, ninguna, pero puede haber situaciones en las que sí nos sea útil. De hecho, sucede a menudo :)
Estructura REPETIR... MIENTRAS QUE(condición) Aquí, lo que se desea es que un bucle se ejecute AL MENOS UNA VEZ antes de comprobar la condición de repetición. La estructura del REPETIR ... MIENTRAS, genéricamente, es esta:
repetir acción 1 ........ acción N mientras que(condición)
Notar que no hace falta poner "fin del repetir", puesto que está claro que se acaba donde pone "mientras que(condición)". Vamos a ver cómo resolver nuestra situación REAL con este tipo de estructura:
Declaración de variables ENTEROS: Contador fin Declaración variables
inicio
Contador <- 1 Repetir mostrar por pantalla 'Prometo ser bueno O:-)' Contador <- Contador+1 mientras que(Contador<=300.000.000)
fin
Le seguimos la pista igual que se la hemos seguido al anterior caso: empezamos con la asignación del valor 1 a la variable Contador. Llegamos a repetir, quien NO NOS EXIGE, para su entrada, verificar condición alguna. Mostramos por pantalla la frase 'Prometo ser bueno O:-)', y sumamos 1 a la variable Contador, con lo que ahora pasa a valer 2. Y AHORA es cuando llegamos a la condición: ¿es Contador<=300.000.000? Yo diría que sí, así que ale, de vuelta a repetir: mostramos por pantalla nuestra frase, sumamos 1 a contador... [... varios minutos después...] Y ya, por fin, Contador alcanza el valor 300.000.001, con lo que la comparación dice "¡MENTIRA!", y se acabó repetir esa tarea. Nuevamente, os invito a que me digais qué hubiera pasado si no hiciéramos Contador <Contador+1 ...
Otra nota importante: Vamos a remarcar una diferencia IMPORTANTE entre esta estructura y la anterior "mientras". Si al empezar nuestro algoritmo, en vez de hacer Contador <- 1 hubiésemos hecho Contador <- 1.000.000.000.000.000.000 de nuevo tenemos posibilidades de salirnos de rango, pero va, vamos a poner que esto que no fuera así; la palabra REPETIR no nos está pidiendo nada, así que ejecutamos una vez el cuerpo del bucle. Se nos muestra la frase, se incrementa el contador, y llegamos al mientras. Como la condición es falsa, no volvemos a ejecutar el bucle. ¿Cuál es la diferencia? Pues que con la estructura "mientras", el bucle se ejecutaba CERO veces, sin embargo, con la estructura "repetir...mientras que" el bucle se ejecuta UNA vez. Esta sutil diferencia es la que hace que unas veces se elija al primero y otras al segundo.
Estructura DESDE Esta estructura hubiera sido la más adecuada para resolver nuestra situación real, como veremos a continuación. La estructura "desde", tiene una pequeña peculiaridad, y es que ella solita incrementa (o decrementa) DE UNO EN UNO a la variable que utilicemos como contador. Su estructura es:
desde Contador<-Inicio hasta Contador=Fin [, decrementar,] hacer accion 1 ........ accion N fin desde
La palabra "decrementar" entre corchetes significa que es opcional, es decir, que se puede poner o no. Si NO se pone, por defecto se asume que al terminar las acciones del bucle, se hará Contador <- Contador+1 Si ponemos "decrementar", al terminar las acciones del bucle se hará Contador <Contador-1. Cuidado con esto, pues si no especificamos "decrementar", es ILEGAL escribir desde Contador<-500 hasta Contador=200 hacer así como, si escribimos "decrementar", es igualmente ILEGAL poner desde Contador<-1 hasta Contador=1257 , decrementar, hacer No se admite otro tipo de incrementos/decrementos (de 2 en 2, de 0.5 en 0.5 o de -10 en -10), para ello ya tenemos las estructuras "mientras" y "repetir...mientras", en las que nosotros elegíamos el incremento/decremento.
En el lenguaje de programación Pascal esto también sucede así en los bucles FOR. En otros lenguajes, como FORTRAN, BASIC o C, el FOR es mucho más potente, siendo el caso del lenguaje C el que riza el rizo, pudiendo llegar a hacer verdaderas obras de arte con un "simple" bucle FOR. Bueno, al grano O:) Supongo que la estructura está suficientemente clara, sin embargo, para terminar de clarificarla, vamos a aplicarla a nuestra BIEN conocida situación REAL:
Declaración de variables ENTEROS: Contador fin Declaración variables
inicio
desde Contador<-1 hasta Contador=300.000.000 hacer mostrar por pantalla 'Prometo ser bueno O:-)' fin desde
fin
Umm... lo primero que se observa es que no hace falta asignar el valor 1 a Contador fuera del bucle, puesto que en la parte Contador<-Inicio (Contador<-1, en este caso) ya se asigna automáticamente ese valor. Lo siguiente es, como ya he mencionado, que no hace falta que incrementemos el valor de Contador, puesto que es una acción que se realiza sola en un bucle de este tipo. Por último, como no hemos usado la palabra decremento, se asume entonces que incrementamos de 1 en 1 el valor de la variable Contador. ¿Y cómo se hubiera hecho esto usando decrementos? Pues, por ejemplo, así:
Declaración de variables ENTEROS: Contador fin Declaración variables
inicio
desde Contador<-300.000.000 hasta Contador=1 , decrementar, hacer mostrar por pantalla 'Prometo ser bueno O:-)' fin desde
fin
Aunque yo lo he hecho aquí por estar manejando variables de tipo entero, debo decir que es PELIGROSO usar una comparación de IGUALDAD entre dos números cualesquiera, y que, para evitar que por truncamientos, redondeos o algún otro motivo que lleve a pérdidas de decimales en los números, en vez de poner = es mejor poner <= o >=. Así, en nuestros dos ejemplos de bucle mientras, la línea "decisoria" en el bucle (por llamarla de alguna manera) es más recomendable escribirla así: desde Contador<-1 mientras Contador<=300.000.000 hacer o así, en el caso del decremento: desde Contador<-300.000.000 mientras Contador>=1 , decrementar, hacer Y con esto queda terminado el capítulo de las estructuras básicas para escribir nuestros algoritmos. Notar que aquí he cambiado la palabra hasta por la palabra mientras. Lo que quiere decir es que, mientras que Contador sea menor o igual que 300.000.000, seguiremos dentro del bucle.
"Ah, no, con la paliza que nos has dado, ahora la que no se escapa eres tú. A ver, ¿y qué hay de esas variables llamadas 'acumuladores' que has mencionado antes y que no has usado para nada?" Bueno, este... ¡me alegro de que me haga esa pregunta! :-D Voy a poner un ejemplo de un acumulador para que se vea claro para qué sirven. Suponed, por ejemplo, que quiero sumar los primeros 10 números naturales. Una posible solución sería esta:
Declaración de variables ENTEROS: i,acum fin declaración variables
inicio
acum<-0 desde i<-1 hasta i<=10 hacer acum<-acum+i fin desde
mostrar por pantalla acum fin
Probad a seguirle la pista al bucle, y decidme qué sale. ¿Lo sabríais escribir usando las otras dos estructuras repetitivas vistas hoy? Si es así, hacedlo }:-) En el primer paso del bucle, acum vale 0, y le sumamos lo que vale i, que es 1. Vale, ahora acum vale 1. Se incrementa i y volvemos al cuerpo del bucle. Sumamos a acum lo que vale i. Ahora acum vale 1, e i vale 2; tras la suma, acum vale 3. A eso es a lo que me refería antes, unas cuantas (pero cuántas) líneas atrás, cuando decía eso de "ACUMULADOR, cuya misión es almacenar una cantidad variable resultante de operaciones sucesivas y repetidas. Es como un contador, con la diferencia que el incremento/decremento es variable". A diferencia de nuestro ejemplo REAL, en el que el incremento era siempre 1, aquí el incremento primero es 1, luego 2, luego 3... espero que con este ejemplo quede más claro, y si no, me pedís que ponga otro, y otro... hasta que lo entendais. Otra nota (no sé cuántas llevo): podemos anidar los bucles. Esto es: dentro de un bucle, podemos ejecutar otro bucle, con la condición de que ese otro bucle esté COMPLETAMENTE CONTENIDO dentro del primero, si no, el algoritmo no es válido. Por ejemplo: El caso más típico de bucle anidado es querer asignar valores a una matriz. Quien no sepa qué es una matriz, que espere al capítulo 7 del curso. Supongamos que nuestra matriz tiene por dimensiones 10 y 12, y queremos asignar valores a cada una de sus componentes. Esto lo haremos así:
Declaración de variables A: Matriz(1..10,1..12) de ENTEROS ENTEROS: i,j {variables para recorrer la matriz} Fin declaración de variables
Inicio
desde i<-1 hasta i<=10 hacer desde j<-1 hasta j<=12 hacer A(i,j)<-i+j fin desde {j} fin desde {i}
Fin
Como veis, el bucle que gobierna la variable j está totalmente contenido dentro del bucle gobernado por la variable i, y, por cada iteración del bucle que dirige i, se ejecutan las 12 repeticiones del bucle dirigido por j. ¿Quién me dice cómo quedará nuestra matriz tras la asignación de valores que hemos hecho? Para no marearos, tomar las filas con la i, y las columnas con la j, como es usual en la notación de matrices. Vamos ahora a por lo que a mí me gusta... los ejercicios };-D 1.
Suponed que teneis un vector de 20 componentes (ver el capítulo 7 del curso). Escribid un algoritmo que lo invierta, es decir, la componente 1 pasa a ser la 20, la 2 pasa a ser la 19, la 3 será la 18... y así con todas.
2. 3. 4.
Escribid un algoritmo que muestre por pantalla las tablas de multiplicar del 1 al 10. PISTA: Usad bucles anidados. Escribid un algoritmo que muestre por pantalla la suma de los cuadrados de los N primeros números. Escribid un algoritmo que calcule la media aritmética de una serie de N números.
Eso es todo (ahora sí), nos vemos en la próxima entrega, en la que hablaremos de... FUNCIONES Y PROCEDIMIENTOS.
5. Funciones y Procedimientos FUNCIONES ¿Qué es una función? Matemáticamente, una función no es más que una aplicación, esto es, una REGLA o CRITERIO para obtener un cierto resultado a partir de unos valores ya existentes. Este concepto se ha trasladado así al campo de la informática, aunque conviene matizar un poco más la idea. La idea de función es la de una "caja negra" en la que nosotros metemos datos, dentro de esa "caja" pasa *algo*, y entonces, de la "caja", sale un resultado, un "producto". Qué pasa dentro de esa "caja negra" depende; si somos nosotros quienes hemos de programarla, lo sabremos, pero si no, no tenemos por qué. Para poder usar la función sólo necesitaremos saber qué datos de entrada admite, y de qué tipo será el resultado. Hay que remarcar un detalle importante: las funciones devuelven un UNICO VALOR. Por ejemplo; dentro de un programa, podemos querer calcular la media aritmética de una serie de datos. En principio, nosotros lo escribimos cuando tenemos que hacer los cálculos. Pero ahora, resulta que más adelante tenemos que volver a calcular la media aritmética de otros datos. Y más adelante, otra vez. ¿Vamos a escribir el código tantas veces? ¿No sería más lógico definirnos una función que se encargara de esa parte, y llamarla cuando la necesitemos? Muy bien... ¿y cómo definimos una función? Pues de la siguiente manera:
funcion NOMBRE (arg1,...,argN) : TIPO variables ......... {se declaran}
accion1 ....... accionN Resultado <- Valor fin funcion
TIPO es el tipo de dato que devolverá la función al terminar de hacer su trabajo. NOMBRE es el nombre que le vamos a dar a la función; por ejemplo, nuestra función se puede llamar Pepe o se puede llamar Media_Aritmetica. arg1 ... argN es la lista de parámetros que vamos a pasar a la función. La sección "variables" es una sección donde se declararán las variables a usar por la función. Más adelante hablaré del ámbito de las variables, para que se entienda el por qué de esto.
accion1 ... accionN es toda la faena que debe hacer la funcion. Al final de esa faena, la función devuelve un Resultado, que es el que hemos especificado como "Valor". ¿Y a dónde va a parar ese "Valor"? Bueno, es que para poder usar una función, tenemos que *invocarla*, llamarla de alguna manera. Si las funciones son cajas que devuelven valores, tendremos que disponer algún sitio para meter ese valor que nos devuelva la función. ¿Cómo la llamamos? Para poder llamar a una función, tendremos que tener definida en nuestra declaración de variables una variable del MISMO tipo que devuelva la función. Entonces, lo que hacemos es asignar a esa variable lo que nos devuelva la función, haciendo lo siguiente: Variable <- Nombre_Funcion(arg1, ..., argN) Esta línea hace lo siguiente: llama a la función Nombre_Funcion, pasándole los parámetros arg1, ..., argN; entonces, se ejecuta el código de la función, hasta que llega al final, momento en que devuelve un valor, y este valor devuelto es asignado a la variable Variable. Vamos a ver un ejemplo. Supongamos que queremos hacer un programa que calcule, en varios puntos, la suma de los N primeros números naturales, pero este N varía conforme el programa lo necesita. Queremos hacer una función que nos simplifique el trabajo. ¿Cómo lo hacemos? Bueno, lo primero que hay que plantearse siempre es qué parámetros necesita la función para trabajar, qué tipo de valor va a devolver y, por último, cómo va a hacer lo que tenga que hacer la función. En nuestro caso, la función sólo necesita saber quién es N, que será de tipo entero; como la suma de naturales es natural, el resultado a devolver también tendrá que ser una variable de tipo entero. Falta ver cómo implementamos esa función. Por ejemplo, lo podemos hacer así (no voy a entrar ahora en mejoras):
funcion Suma_N_Naturales(ENTERO N) : ENTERO variables ENTERO: Suma,i
Suma <- 0 desde i<-1 hasta i=N hacer Suma <- Suma+i
Resultado <- Suma fin funcion
y ahora, vamos a usarla. En nuestro programa podemos poner:
Declaracion variables ENTEROS: N, Suma fin declaracion variables
inicio
desde N=1 hasta N=200 hacer Suma <- Suma_N_Naturales(N); mostrar por pantalla ('La suma de los ',N,' primeros naturales es ',Suma) fin desde
fin
Con esto, hacemos 200 veces, incrementando en 1 cada vez N, la asignación a la variable Suma del resultado obtenido por la función Suma_N_Naturales, y mostrando por pantalla el resultado. Cada vez que se llegue a la línea de la asignación, se llamará a la función Suma_N_Naturales, se ejecutará el código de esa función, y al devolver el resultado, el programa principal recupera el control de la ejecución. Sin embargo, dentro de la función tenemos declarada una variable que se llama Suma, y en el programa principal hay otra variable que se llama Suma... ¿cómo sabemos cuál es la buena? ¿no se mezclan ni nada parecido los valores? Bueno, creo que este es un buen momento para hablar de...
AMBITO DE LAS VARIABLES Como ya hemos visto, un programa no tiene por qué estar formado por un único módulo (vamos a llamarle así) principal, si no que puede estar formado por muchas funciones, y por muchos procedimientos (de los que hablaremos más adelante). Cada función, o cada procedimiento, puede tener, dentro de su sección de declaración de variables, sus propias variables, aunque se llamen igual que las de la función de más arriba, puesto que, al declarar una función o un procedimiento, las variables que usan son LOCALES a ellos, es decir, sólo ellos saben que existen y, por tanto, pueden usarlas. Como contraposición a las variables locales, tenemos las GLOBALES, que se declaran en una sección VARIABLES GLOBALES; estas variables son reconocidas por cualquier función o procedimiento que exista en nuestro programa, cualquiera puede modificar su valor en cualquier momento. Ahora, ¿y si hay una variable global que tiene el mismo nombre que una variable local en una función que estoy usando? En ese caso, se usa la variable que es local a la función. En nuestro ejemplo no se da este conflicto, al no haber una sección de variables globales (eso implica que no las hay), ya que cada variable Suma pertenece a una función distinta. Algunas notas respecto al tema de funciones: Es una buena costumbre escribir, justo antes de la definición de la función, un comentario sobre qué hace la función, para qué nos van a servir los parámetros que vamos a pasarle, y qué es lo que devuelve. Hay que distinguir entre lo que se llama parámetros FORMALES y parámetros ACTUALES. Cuando definimos una función, en su CABECERA (la línea donde pone su nombre, los argumentos que recibe y el tipo de valor que devuelve) aparecen nombrados los argumentos. El nombre que ponemos en ese momento es lo que se llama parámetros formales. Pero, cuando la llamamos, por ejemplo, Suma_N_Naturales(27), le estamos pasando el parámetro concreto 27: a estos parámetros se les llama parámetros actuales.
Como ejercicio, escribid una función que devuelva el resultado de:
siendo x un número real y n un número natural.
PROCEDIMIENTOS Se llama así a un subprograma que ejecuta unas ciertas acciones sin que valor alguno de retorno esté asociado a su nombre. En otras palabras: NO devuelven valores (en cierto sentido). Los procedimientos son normalmente llamados desde el algoritmo principal mediante su nombre y una lista de parámetros actuales (como las funciones) a través de una instrucción específica: LLAMAR (CALL, en inglés) Se diferencian de las funciones en que los parámetros de llamada pueden ser modificados si así se especifica dentro del procedimiento; en ese sentido se puede interpretar como que devuelven valores. La forma de declararlos es la siguiente:
PROCEDIMIENTO Nombre (Lista de parámetros formales) variables
acción1 ....... acciónN Fin Procedimiento
y la forma de usarlos: LLAMAR_A Nombre(Lista de parámetros actuales) Vamos a ver un ejemplo de todo esto: queremos tener una forma de calcular la suma, la resta, el producto y el cociente de dos números cualesquiera. Obviamente, vamos a necesitar 6 variables; 2 de ellas serán los factores, y las otras 4, el resultado de las correspondientes operaciones. Podríamos pensar en 4 funciones que devolvieran cada una de ellas un número (entero, real, ...), pero podemos hacer esto de forma más compacta con un procedimiento. Veamos cómo lo declararíamos:
PROCEDIMIENTO Cuentas(ENTERO a, ENTERO b, ENTERO sum, ENTERO dif, ENTERO mul, ENTERO dif)
sum <- a+b dif <- a-b mul <- a*b div <- a/b
Fin Procedimiento
y luego lo podríamos llamar así: LLAMAR_A Cuentas(5, 3, SUMA, RESTA, MULT, DIV) con lo que a las variables SUMA, RESTA, MULT, DIV les serían asignados sus correspondientes valores; estas variables se supone que ya están declaradas previamente. Y llegamos al último punto de esta entrega:
RECURSIVIDAD Una buena definición de este concepto es la siguiente. Recursivo: ver recursivo ... :-D ... O:-) Aunque, si lo quereis de otra forma: propiedad de una función o de un subprograma de llamarse a sí mismo. Ilustremos esto con un ejemplo, nuestra función para sumar los N primeros números naturales. Vamos a escribirla de esta otra forma:
funcion Suma_N_Naturales(ENTERO N) : ENTERO variables ENTERO: Suma
si (N=1) entonces Suma <- 1 si no Suma <- N+Suma_N_Naturales(N-1) fin si
Resultado <- Suma
fin funcion
Y vamos a llamarla con Suma_N_Naturales(4), detallando los pasos: si (4=1) {falso, no se ejecuta} si no {cierto, se ejecuta} Suma <- 4+Suma_N_Naturales(4-1) {4-1=3} Entramos en Suma_N_Naturales(3): si (3=1) {falso, no se ejecuta} si no {cierto, se ejecuta} Suma <- 3+Suma_N_Naturales(3-1) {3-1=2} Entramos en Suma_N_Naturales(2): si (2=1) {falso, no se ejecuta} si no {cierto, se ejecuta} Suma <- 2+Suma_N_Naturales(2-1) {2-1=1} Entramos en Suma_N_Naturales(1): si (1=1) entonces Suma <- 1 y se devuelve el control al punto donde se llamó a Suma_N_Naturales(2), donde tenemos Suma <- 2+1, con lo que se devuelve el control al punto donde se llamó a Suma_N_Naturales(3), teniendo Suma<-3+(2+1), momento en el que se devuelve el control al punto donde se llamó a Suma_N_Naturales(4), donde tenemos Suma <- 4+(3+(2+1)), justo el resultado esperado.
6. Punteros; esos desconocidos Antes de explicar qué son, para qué sirven y cómo hacer uso de ellos, vamos primero a exponer unas cuantas consideraciones: Los punteros no están disponibles en todos los lenguajes de programación, ello significa que si, por ejemplo, programais en C, podeis utilizarlos, pero si programais en Basic no. Su uso produce unas ciertas ventajas sobre los programas, y es que aumentan la flexibilidad a la hora de, por ejemplo, declarar vectores o matrices cuyo tamaño no sea predefinido. No acaban ahí las ventajas, con estructuras de datos más complejas son el pan nuestro de cada día, pero de todo eso ya hablaremos. Sin embargo, no todo es maravilloso, y tienen serios inconvenientes que provocan más de un quebradero de cabeza. Es muy fácil equivocarse usándolos y con ello escribir en alguna zona de memoria no aconsejable (como las reservadas por el sistema operativo), lo que provoca el consiguiente cuelgue en sistemas poco robustos que permitan escribir en cualquier parte. No hay nada más peligroso que un puntero incontrolado o sin inicializar. Vistas las pegas (ya las descubrireis cuando el programa se os cuelgue sin motivo aparente), vamos a ver qué es eso de los punteros, pues parece que tenemos que tenerles un cierto respeto :-D
Qué es un puntero Intuitivamente, un puntero es una flecha que apunta a alguna parte. ¿A qué parte? Obviamente, si estamos hablando de ordenadores, apuntará a una cierta dirección de memoria. Es decir, un puntero es una representación simbólica de una dirección de memoria. (Nota importante: voy a usar la notación de C para los punteros) Veamos un ejemplo; supongamos que tenemos declarada la variable Variable_Misteriosa en nuestro programa. Si queremos saber cuál es la dirección de dicha variable, pondremos lo siguiente:
&Variable_Misteriosa { el símbolo & precediendo a una variable es el operador dirección de memoria }
y si, por ejemplo, escribimos en nuestro programa: Mostrar_por_Pantalla(&Variable_Misteriosa) saldrá en pantalla (por decir algo): 56743 que no es más que la celdilla de la memoria en la que se almacena el valor que nosotros identificamos con Variable_Misteriosa. Es decir, si hemos hecho previamente (suponiendo que fuera una variable entera): Variable_Misteriosa <- 9 lo que significa es que en la dirección de memoria 56743 está almacenado el valor 9. ¿Está todo claro hasta aquí? Bien, pues sigamos :) En el caso que he puesto como ejemplo, lo que teníamos era una *constante* puntero. Pero también podemos declarar como variables, variables de tipo puntero. Estas variables contendrán, como hemos dicho, una dirección de memoria. Por ejemplo, si tenemos una variable puntero que se llame Mi_Puntero, y una variable normal que se llame Mi_Variable, podemos hacer cosas como esta: Mi_Puntero <- &Mi_Variable con lo que en Mi_Puntero tenemos almacenada la dirección de memoria de Mi_Variable (y decimos que Mi_Puntero APUNTA a Mi_Variable). Y aquí surge el primer problema. Si hacemos: Mi_Puntero <- Mi_Variable en Mi_Puntero está almacenada, como dirección de memoria, el valor de la variable Mi_Variable. Si luego hemos de escribir algo en la zona de memoria a la que apunta Mi_Puntero, ya la hemos liado, puesto que no sabemos qué puede haber ahí. Resumiendo: no es lo mismo la dirección de memoria de una variable que el contenido de la variable (o, lo que es lo mismo, el contenido de esa dirección de memoria).
Seguimos. Como hemos dicho que Mi_Puntero es una variable de tipo puntero, podemos hacer, más adelante en el curso del programa, que apunte a otra variable de la misma forma. Ya sabemos que Mi_Puntero apunta a Mi_Variable. En este caso, podemos utilizar el operador de indirección * (también es de C; no confundirlo con el operador de multiplicación) para encontrar el valor almacenado en Mi_Variable. ¿Cómo? Escribiendo (por ejemplo):
Mi_Puntero <- &Mi_Variable Mostrar_por_Pantalla(*Mi_Puntero)
es exactamente lo mismo que hacer: Mostrar_por_Pantalla(Mi_Variable) Lo que hemos hecho ha sido apuntar a Mi_Variable con Mi_Puntero (es decir, Mi_Puntero contiene la dirección de memoria de Mi_Variable) y después, con el operador *, mostrar el CONTENIDO de lo que hay en la dirección de memoria que guarda Mi_Puntero. Es decir, lo que hace el operador de indirección es, seguido de un puntero, dar el valor almacenado en la dirección de memoria a la que apunta el puntero. Sé que todo esto es un trabalenguas de cuidado, así que releed con cuidado la primera parte del texto y seguidle bien la pista. Haced algún dibujo si eso os ayuda. Mientras, yo prosigo con el punto siguiente:
Declaración de punteros. He dicho antes que podemos tener variables de tipo puntero, así que, lo lógico es querer saber cómo declararlas, y a eso es a lo que vamos. Cuando declárabamos (hace ya mucho tiempo) variables de tipo entero, poníamos: a,b,c : ENTEROS ¿No podríamos poner para los punteros algo como:? a,b,c: PUNTEROS Bien, pues la respuesta es NO. ¿Y por qué no? Pues porque para declarar un puntero necesitamos saber, aparte de que va a apuntar a alguien, a qué TIPO de alguien va a apuntar, es decir, a qué tipo de variable va a apuntar. No será lo mismo apuntar a un entero que a un caracter o que a un real, pues estos tipos ocupan distintos tamaños en memoria, y eso es algo fundamental para otra cosa que veremos más adelante, la aritmética de punteros. Para declarar una variable de tipo puntero a un tipo de dato, lo haremos como sigue:
*Puntero1
: CARACTER
*Puntero2
: ENTERO
*Puntero3, *Puntero4 : REAL
(esto en C lo haríamos así: char *Puntero1; int *Puntero2; float *Puntero3, *Puntero4 ;) y tenemos que Puntero1 será una variable puntero que apunte a una variable de tipo caracter, Puntero2 apuntará a una variable de tipo entero y Puntero3 y Puntero4 apuntarán a variables de tipo real. Ahora que los tenemos declarados, suponiendo que tengamos las variables Var1 de tipo caracter, Var2 de tipo entero y Var3 de tipo real, podemos inicializarlos haciendo lo que ya hemos visto:
Puntero1 <- &Var1 Puntero2 <- &Var2 Puntero3 <- &Var3
y acceder a sus contenidos escribiendo:
*Puntero1 *Puntero2 *Puntero3
sin embargo, si se nos ocurre usar en el programa: *Puntero4 puede pasar de todo. ¿Por qué? Pues porque este puntero no está inicializado (lo he dejado a drede). Eso significa que, en principio, puede contener cualquier cosa. Al ser un puntero, esa ''cualquier cosa'' será interpretada como una dirección de memoria, y el contenido de ''cualquier dirección de memoria'' puede ser de lo más insólito, lo que no es muy recomendado si, por ejemplo, estamos haciendo cálculos. Una cosa que no hay que perder de vista es que los punteros, al ser variables, tienen una posición de memoria en la que se guarda su contenido. Es decir, si hacemos: &Puntero1 estamos accediendo a la dirección de memoria del puntero Puntero1. Y el contenido de esa dirección de memoria es la dirección de memoria de la variable a la que apunta.
Aritmética de Punteros. Trabajando con punteros, ¿qué sentido tiene hacer:? Puntero <- Puntero + 1 Pues eso depende de a qué tipo de variable esté apuntando la variable Puntero. Si Puntero apunta a un carácter, como un carácter ocupa 1 byte en memoria, al hacer la operación anterior, Puntero está apuntando al byte siguiente al que apuntaba antes. Si la operación que hacemos es: Puntero <- Puntero - 1
lo que hace es apuntar al byte anterior. Sin embargo, si Puntero apunta a un real, como un real ocupa en memoria 10 bytes, en este caso apunta a los 10 bytes anteriores al que estaba apuntando. En otras palabras, cuando sumamos 1 a un puntero, no estamos diciéndole que apunte a la dirección siguiente, sino que pase a apuntar a la siguiente celdilla de memoria de acuerdo con el tipo base al que apunta. Y si en vez de sumar (o restar) 1, sumamos (o restamos) N, avanzamos N veces lo que ocupe el tipo de variable al que estamos apuntando. Además, también podemos sumar y restar punteros. Veamos unos ejemplos para aclararlo. Supongamos que Puntero1 y Puntero2 son punteros a enteros, y hacemos: Puntero2 <- Puntero1 + 4 entonces, Puntero2 apunta a la posición de memoria 8 bytes posterior a la que apunta Puntero1. Y si hacemos: Puntero2 <- Puntero1 - 1 Puntero2 apunta a la posición de memoria 2 bytes anterior a la que apunta Puntero1. También podemos hacer: i <- Puntero2 - Puntero1 Esto normalmente se hace dentro de un mismo array, para saber cuántos elementos los separan. Notar que el resultado que da no es en bytes, sino en las mismas unidades que el tamaño del tipo del array. Este resultado debe ser asignado a una variable de tipo entero (la variable i que aparece en el ejemplo se presupone previamente declarada).
¿Y todo esto para qué sirve? Hablar de punteros por hablar puede ser muy entretenido si no se tiene nada mejor que hacer, pero resulta que yo he dicho que son muy útiles y no lo he dicho gratuitamente. Por ejemplo, ahora que sabemos lo que son los punteros, vamos a ver que están relacionados con el paso de parámetros en las funciones, con lo que hablaremos de paso de variables por valor y por referencia. En su día hablamos de funciones y de procedimientos, y dijimos que, en el caso de las funciones, pasábamos una serie de variables que no eran modificadas, y se nos devolvía un único valor. Por contra, en los procedimientos pasábamos una serie de variables que podían ser modificadas pero, a cambio, no se nos devolvía ningún valor. Esto es así en lenguajes como el Pascal, pero en C esto no sucede. Quiero decir, que en C no tenemos funciones y procedimientos, sino que sólo tenemos funciones. Las funciones siguen teniendo la característica de que devuelven un único valor, y que las variables que le pasamos como argumentos no pueden ser modificadas. ¿Y si necesito que se me devuelvan dos valores? ¿Cómo lo hago, si el C sólo me permite funciones? Pues lo hago con lo que se llama ''paso de parámetros por referencia''. En primer lugar, he de decir que en C, cuando pasamos variables a las funciones, lo que hacemos es un ''paso de parámetros por valor''. Es decir, la función recibe los valores de las variables, pero no sabe nada más de ellas. Cuando hacemos un paso por referencia, lo que estamos pasando a la función no es el valor de la variable, sino la dirección de memoria. Y la función, al tener la dirección de memoria de
la variable, ya sí puede modificarla, pudiendo, en cierto sentido, ''devolvernos'' varios valores, solucionando el problema que teníamos al no disponer de procedimientos. Voy a poner un ejemplo de esto comentado para que se entienda la estructura y el por qué. Y para ello, voy a utilizar un ejercicio que ya propuse, el de escribir un procedimiento que intercambie el valor de dos variables. El algoritmo de este procedimiento es algo tan sencillo como esto:
PROCEDIMIENTO Intercambia(X,Y: ENTEROS) variables auxiliar: ENTERO
inicio
auxiliar <- X X <- Y Y <- auxiliar
fin
y lo podemos llamar desde cualquier punto del programa principal sin más que poner LLAMAR_A Intercambia(Un_Valor,Y_Otro_Valor) Sin embargo, como ya he comentado, en C no tenemos procedimientos, ¿cómo salvamos este escollo?. Pues lo salvamos con una función como la siguiente:
void Intercambia(int *x,int *y)
{
int aux; aux=*x; *x=*y; *y=aux; }
y llamamos a esta función desde el programa principal como sigue:
int main(void) { int a=2,b=3;
printf("\nPrimero, a=%d y b=%d\n",a,b); Intercambia(&a,&b); printf("\npero ahora, a=%d y b=%d\n",a,b); return 0; }
Veamos cosas: para empezar, al llamar a la función Intercambia, no le hemos pasado los valores de las variables (paso por valor), sino que le hemos pasado las direcciones de las variables (paso por referencia). Así pues, los parámetros formales de la función Intercambia están recibiendo direcciones de memoria, por lo que deben declararse como punteros, cosa que hemos hecho en la cabecera de la función, indicando que recibe dos punteros. Ahora veamos la función: en primer lugar, creo la variable auxiliar necesaria para hacer el cambio. Sin embargo, en vez de hacer: aux <- x hago: aux <- *x es decir, asigno a aux el contenido de la dirección de memoria a la que apunta x. Como, al llamar la función, hemos pasado unas direcciones de memoria, *x da como resultado el contenido de la dirección de memoria donde se guarda la variable a. Si hubiera hecho: aux <- x en aux tendré almacenada la posición de memoria de la variable a y no su contenido, que es lo que yo quiero. A continuación hacemos: *x <- *y es decir, al contenido de la posición de memoria a la que apunta x se le asigna el contenido de la posición de memoria a la que apunta y. Y ya termina el intercambio con: *y <- aux con lo que al contenido de la posición de memoria a la que apunta y se le asigna el valor de la variable aux, que era el contenido de la posición de memoria a la que apuntaba x originalmente, en otras palabras, el valor de la variable a. Si tras leer con calma este punto un par de veces os perdeis, me dejais una nota con las líneas que os parezcan más oscuras, porque esto es un buen trabalenguas mental O:) La otra gran utilidad es la reserva dinámica de memoria. Si quereis, puedo dar aquí unos pequeños esbozos, pero eso ya quedaría para otro capítulo. Creo que con 400 líneas sobre punteros, para empezar, ya teneis bastante O:)
7.I. Vectores La idea de vector, en informática, es la siguiente: imaginad que en vuestro programa necesitais 10.000 variables de tipo entero que representen cada una de ellas la temperatura de unos pueblos. Imagino que a nadie se le habrá ocurrido ponerse, en la declaración de variables, a declarar una por una las 10.000 variables que se necesitan. Dado que son variables que están relacionadas entre sí por su significado (y eso es algo que lo decide siempre el programador), parece lógico querer ponerles un nombre común, y que sea el índice (en qué componente de vector se encuentran) lo que las distinga. Vemos pues que los
vectores nos dan unas ciertas ventajas: declarar muchas variables relacionadas entre sí de un mismo tipo de una vez, y acceder a cualquiera de ellas usando tan sólo un índice. Declararemos algo como esto (lo precisamos un poco más adelante): Temperaturas: es un vector de reales con 10.000 componentes Para referirnos a los elementos de un vector, tenemos que especificar el índice del elemento que estamos tratando: v[i]. Así, asignaremos un valor haciendo v[i] <- 3 y leeremos un valor haciendo m <- v[i]. Asignaremos valores a las componentes de nuestro vector de temperaturas, bien a mano: Temperaturas[1] Temperaturas[2] Temperaturas[3] Temperaturas[4] ...
<<<<-
23.5 22.7 25.2 20.4
o bien en un bucle: desde i=1 hasta i=10.000 hacer Temperaturas[i] <- Medida_Temperatura fin desde y, en la memoria del ordenador, lo que tendremos será algo como:
|--------| |
23.5
|
Este es Temperaturas[1]
|--------| |
22.7
|
Este es Temperaturas[2]
|--------| |
25.2
|
Este es Temperaturas[3]
|--------| |
20.4
|
Este es Temperaturas[4]
|--------| | ...... | |--------| |
19.2
|
Este es Temperaturas[10000]
| -------|
Para acceder a cualquier componente del vector, sólo tendremos que poner Temperaturas[posicion], donde posicion será el número de orden. Vamos a ser un poco más precisos con la nomenclatura: para declarar un vector de N elementos (numerados del 1 al N) de tipo X, lo haremos de la siguiente manera: Variable: Vector[1..N] de Tipo;
Por ejemplo, para declarar un vector de 20 enteros, haremos: v: Vector[1..N] de Entero; ¿Qué cosas podemos hacer con un vector? Esto depende de quiénes lo integren; me explico: si nuestro vector está formado por números, podremos sumar, restar y multiplicar vectores. Sin embargo, estas funciones ya NO forman parte del lenguaje de programación, si no que tendremos que hacérnoslas nosotros. En el caso de vectores numéricos, veremos cómo se puede escribir el pseudocódigo para algunas de ellas. Ya he mencionado que los vectores no sólo pueden ser de números, sino también de caracteres. Por ejemplo, podemos tener el siguiente vector de caracteres: |-----| | A | |-----| | G | |-----| | J | |-----| | H | |-----| | R | |-----| Quizá sin mucho sentido, pero sí con caracteres :) Ahora, no hace falta un alarde mucho mayor de imaginación para poner el siguiente vector de caracteres: |-----| | H | |-----| | O | |-----| | L | |-----| | A | |-----| Vaya sorpresa: una palabra ;) Sí, los vectores de caracteres se usan para formar lo que se conoce bajo el nombre de "cadenas de caracteres". Las forma de trabajar con las cadenas de caracteres depende del lenguaje de programación usado. Nosotros emplearemos la siguiente convención: para declarar una cadena de caracteres, usaremos el tipo "cadena", diciendo cuántos caracteres tiene la cadena. Por ejemplo: str: Cadena[10]; Esto será una cadena de 10 caracteres. Además, escribiendo str estamos haciendo referencia a la cadena completa, mientras que si escribimos str[i] nos estamos refiriendo al carácter i-ésimo de la cadena. Como decía más arriba, la suma de dos vectores tiene sentido si hablamos de vectores numéricos, pero, ¿qué sentido tiene "sumar" dos cadenas de caracteres? Pues el que nosotros le queramos dar, y este suele ser el de CONCATENAR, es decir, obtener una nueva cadena de caracteres, de mayor tamaño, y que tenga, primero, los caracteres de la primera
cadena para, a continuación, pasar a tener los caracteres de la segunda cadena (mucho ojo, que esto no es tan trivial como suena). Por ejemplo, supongamos que tenemos las cadenas siguientes: cadena1 <- ['H','o','l','a'] cadena2 <- ['M','u','n','d','o'] y hacemos: cadena <- cadena1+cadena2 tendremos, en cadena: cadena <- ['H','o','l','a','M','u','n','d','o'] y no cadena <- ['H','o','l','a',' ','M','u','n','d','o'] puesto que al concatenar, no se añaden espacios. Como ya hemos dicho, el mundo de las cadenas de caracteres depende del lenguaje de programación, por lo que nosotros las usaremos sin entrar en consideraciones especiales. Será cuando aprendamos un lenguaje de programación concreto cuando tendremos que atenernos a sus normas. Antes, cuando he hablado de "operaciones" con vectores, he dicho que había que definirlas. Veamos algunos ejemplos: Queremos sumar dos vectores. Lo primero de todo, es que el número de componentes ha de ser el mismo (si nos vamos al símil de algún espacio R n). Llamamos U y V a nuestros dos vectores, y W al vector suma. Las componentes de W, como sabemos, las obtendremos así: desde i=1 hasta i=tamanyo hacer W[i] <- U[i] + V[i] fin hacer La resta es igualmente sencilla (y queda como ejercicio al lector ;) ). Ahora, multiplicar dos vectores... ¿cómo? Si nuestro vector representara algo en R 3, tenemos dos productos (conocidos) para estos vectores: el vectorial y el escalar, en otro Rn, tenemos el producto escalar. Pongamos que queremos calcular el producto escalar. Sabemos que el resultado de esa multiplicación NO es un vector, sino que el resultado es un número. En el caso del producto vectorial, el resultado es un vector. O podemos inventarnos nosotros el producto. Podemos querer que sea como la suma: que la componente i-ésima del vector sea el producto de las componentes i-ésimas de los vectores factores. Todo depende del sentido que le queramos dar a esa operación. Por ejemplo, en el caso en que queramos que cada componente sea el producto de las otras dos, el algoritmo es un mero calco del de la suma: desde i=1 hasta i=tamanyo hacer W[i] <- U[i]*V[i] fin hacer Y, en el caso, del producto escalar, tampoco tiene mucha complicación.
producto=0 desde i=1 hasta i=tamanyo hacer producto <- producto + ( U[i]*V[i] ) fin hacer
Resumiendo: no hay definidas operaciones para vectores, sólo las que nosotros queramos usar, y para ello tendremos que programarlas. También podemos querer hacer otras cosas con vectores, como, por ejemplo, ORDENARLO, cuestión que resolveremos más adelante. Unos pequeños ejercicios: 1.- Escribid funciones que implementen las operaciones conocidas con vectores (y las que se os ocurran): suma y resta de vectores, producto de un número por un vector... 2.- Dados dos vectores de caracteres, escribid una función que devuelva un vector de caracteres que sea la concatenación de los dos dados. Eso es todo por ahora :)
7.II. Matrices Tenemos otra estructura, fuertemente basada en la idea matemática de matriz, y que tiene el mismo nombre: matriz (o "array", para quien le guste más el inglés). Una matriz representa la idea de "tabla": una disposición de la información en forma de filas y columnas. Por ejemplo: [ [
3 4.1
-4.5 2
0.1 -1
] ]
En este ejemplo tenemos una matriz formada por dos filas y tres columnas. El elemento de la posición (2,3) (fila 2, columna 3) es el -1. Podemos entender las matrices como vectores cuyos elementos son vectores. De hecho, en casi todos los lenguajes de programación, esto es así. Según este punto de vista, la matriz del primer ejemplo es un vector de dos elementos: el primero, el vector [3, -4.5, 0.1], y el segundo, el vector [4.1, 2, -1]. Para declarar una variable de tipo matriz escribiremos: m: Array[1..N,1..M] de Tipo; siendo N el número de filas que tendrá la matriz, y M el número de columnas. Para referirnos a la posición (i,j), usaremos la nomeclatura m[i,j]. Por ejemplo: m: Array[1..3,1..5] de Entero; m[2,3] <- 4; m[1,5] <- -2;
No perder de vista la declaración de una matriz: puede contener elementos de cualquier tipo. Enteros, reales, cadenas de caracteres, o tipos definidos por el usuario (estos los estudiamos en el capítulo siguiente). Recorrer los elementos de una matriz requiere de un bucle dentro de otro bucle. ¿Por qué? Pues porque, para cada fila que recorramos, tendremos que recorrer cada elemento de las columnas. Así, empezaremos entrando en la fila 1, y dentro de la fila 1 recorreremos los elementos 1 a M de que constan las columnas. Hecho esto, pasamos a la fila 2, y dentro de la fila 2 volvemos a recorrer los elementos 1 a M de las columnas. Siguiendo este proceso, acabaremos recorriendo todos los elementos de la matriz. Por ejemplo, para sumar dos matrices (en el sentido matemático, teniendo en cuenta que entonces serán de tipo numérico), tendremos que ir elemento a elemento sumando los valores que se encuentren en las posiciones (i,j). Es obvio que la suma de dos matrices sólo tiene sentido cuando las matrices a sumar tienen el mismo número de filas y de columnas. Una algoritmo sencillo para sumar dos matrices, almacenando su suma en una tercera, sería: desde i <- 1 hasta i = N hacer desde j <- 1 hasta j = M hacer C[i,j] <- A[i,j] + B[i,j]; fin desde fin desde Análogamente realizaríamos la resta. Tenemos una clara aplicación de las matrices en el mundo de la programación gráfica. Las transformaciones de los puntos pueden representarse en términos de unas ciertas matrices, y obtener la transformación de un punto (una rotación, una traslación o un cambio de escala) consiste sencillamente en multiplicar las coordenadas del punto por la matriz correspondiente.
8. Tipos definidos por el usuario (el tipo registro) ¿Qué es un tipo definido por usuario? Vamos a responder a esta pregunta poniendo un ejemplo. Imaginemos que queremos realizar un programa que trabaje con fichas de alumnos. Debemos empezar planteándonos qué datos son los que queremos almacenar del alumno. Por ejemplo, podemos querer su nombre, apellidos, edad, curso y nota media. Perfectamente podemos, simplemente, hacer la siguiente declaración de variables: Nombre, Apellidos: Cadena[255]; Edad, Curso: Entero; NotaMedia: Real; e ir usándolas en el programa. Esta declaración nos sirve para una única ficha de alumnos. Si queremos trabajar con dos alumnos, entonces tendríamos que hacer:
Nombre1, Nombre2, Apellidos1, Apellidos2: Cadena[255]; Edad1, Edad2, Curso1, Curso2: Entero; NotaMedia1, NotaMedia2: Real;
Si queremos trabajar con tres, la cosa empieza a enrevesarse. Tenemos cada vez más variables. ¿Podría definir un tipo "Alumno" que contuviera la información que quiero y usarlo como si fuera una única variable? La respuesta es sí. Este tipo "Alumno" sería lo que se conoce como "tipo definido por el usuario". Ahora bien, ¿cómo lo definiríamos? Usaremos la notación: Tipo TAlumno Nombre, Apellidos: Cadena[255]; Edad, Curso: Entero; NotaMedia: Real; Fin Tipo; es decir: Tipo NombreTipo Variable1: Tipo1; ... VariableN: TipoN; Fin Tipo; Ahora, para declarar variables de tipo TAlumno, simplemente escribiremos: Alumno: TAlumno; O, si queremos más de una variable de tipo TAlumno: Alumno1, Alumno2, Alumno3: TAlumno; Para acceder a las variables contenidas en el tipo, usaremos la notación punto (.). Por ejemplo, para acceder a los apellidos del segundo alumno declarado, escribiremos: Alumno2.Apellidos Podemos usar esta notación tanto para recuperar el valor almacenado como para establecer uno nuevo: NombreAlumno := Alumno3.Nombre; Alumno2.Edad := 17; La definición de tipos es muy poderosa: dentro de la definición de un tipo, podemos usar como tipo para una variable otro tipo definido por nosotros previamente declarado. Por ejemplo, una vez definido el tipo TAlumno, podemos definir un tipo TClase que contenga, a su vez, tipos TAlumno: Tipo TClase Alumnos: Vector[1..30] de TAlumno; NumeroClase: Entero; LetraClase: Carácter; ProfesorTutor: Cadena[255]; Fin Tipo; Hemos visto, además, que tipos definidos por el usuario y vectores se pueden combinar de igual manera: dentro de un tipo definido por el usuario, puedo declarar una variable de tipo vector. Si declaramos la variable:
Clase: TClase; acceder a la edad del alumno número 23 de dicha clase será tan sencillo como hacer: Clase.Alumnos[23].Edad Ahora podemos empezar a pensar en resolver problemas complejos. Por ejemplo, tengo una clase de 30 alumnos y quiero ordenar ese vector de alumnos por orden alfabético.
Ejemplos 1.
2.
Definir una estructura que represente una ficha de un empleado con los datos siguientes: Nombre, apellidos, año de entrada en la empresa, sueldo mensual bruto, sueldo mensual neto. Declarar dos variables de este tipo. Tipo TEmpleado
3.
Nombre, Apellidos: Cadena[100];
4.
AnyoEntrada, SueldoBruto, SueldoNeto: Entero;
5.
Fin Tipo;
6.
Empleado1, Empleado2: TEmpleado;
También puede definirse: Empleados: Vector[1..2] de TEmpleado; 7.
8.
Definir una estructura que represente una ficha de un paciente con los datos siguientes: Nombre, apellidos, fecha de entrada en el hospital, clase de dolencia. Declarar cincuenta variables de este tipo. Tipo TPaciente
9.
Nombre, Apellidos: Cadena[100];
10.
DiaEntrada, MesEntrada, AnyoEntrada: Entero:
11.
Dolencia: Cadena[255];
12. Fin Tipo; 13.
Pacientes: Vector[1..50] de TPaciente;
Ejercicios 1.
Definir una estructura que represente una ficha de un empleado con los datos siguientes: Nombre, apellidos, direccion, número de teléfono, fecha de entrada en la
2. 3.
empresa, sueldo bruto al mes, sueldo neto al mes, cargo, departamento al que pertenece. Declarar 20 variables de este tipo. Definir una estructura que represente una ficha de alumno con los datos siguientes: Nombre, apellidos, direccion, número de teléfono, fecha de nacimiento, curso, notas de las cinco asignaturas, nota media. Declarar 30 variables de este tipo. Definir una estructura que represente una ficha de un libro con los datos siguientes: Nombre del libro, autor, fecha de la edición, ISBN. Declarar 50 variables de este tipo.
9.I. Algoritmos de ordenación Los algoritmos de ordenación son aquellos que se preocupan por ordenar los elementos de un vector. Para ordenar los elementos de un vector hay que decidir dos cosas: por qué campo vamos a ordenar, y que querrá decir que un vector está ordenado. Por ejemplo, podemos tener un vector cuyas componentes sean registros que contengan datos sobre personas (nombre, apellidos, teléfono, etc). Tenemos varias posibilidades: decidir ordenar por nombre, por apellidos, por teléfono... Además, si ordenamos por nombre, será de la A a la Z o de la Z a la A; igual si ordenamos por apellidos, mientras que si ordenamos por teléfono podemos querer hacerlo de forma ascendente o descendente. Si nuestro vector es, por contra, numérico, tenemos menos cosas que decidir, ya que al no tratarse de una estructura compleja hay un único campo por el que ordenar. Aún así, tendremos que decidir si consideraremos que el vector está ordenado de forma ascendente o descendente. Vamos a estudiar los dos algoritmos más sencillos de ordenación. No son los más eficientes, pero sirven para ofrecer una primera idea de cómo realizar la tarea de ordenación. Para aprender sobre algoritmos más eficientes podemos buscar información adicional en la web (Google es siempre un buen aliado).
Algoritmo de selección El método de selección se basa en la siguiente idea: tenemos un vector inicialmente desordenado. Buscamos el elemento más pequeño dentro del vector y lo comparamos con el elemento de la primera posición. Si el elemento de la primera posición es mayor que el mínimo del vector, entonces intercambiamos los elementos. Ahora ya tenemos en la primera posición el elemento más pequeño. Nos olvidamos de él, y buscamos, dentro del subvector [2..N] (N el número de elementos del vector) el elemento más pequeño. Comparamos este mínimo con el elemento de la primera posición del subvector. Si el elemento de la primera posición del subvector es mayor que el mínimo del subvector [2..N], intercambiamos los elementos. Procedemos de forma análoga hasta terminar de recorrer el vector. Vamos a ver un ejemplo que aclarará cómo funciona este método. Supongamos que tenemos el vector formado por los elementos [5, 4, 3, 2, 1] y queremos ordenarlo de forma ascendente. Empezamos buscando el valor mínimo del vector: el 1. El elemento de la primera posición es el 5. Como 5>1, intercambiamos los elementos 1 y 5, quedando el vector como sigue: [1, 4, 3, 2, 5] Ya tenemos el 1 bien colocado. Ahora examinamos el subvector [2..5] (los elementos de las posiciones 2 a 5, es decir, [4, 3, 2, 5]). El mínimo dentro de este subvector es el 2, mientras que el primer elemento del subvector es el 4. Como 4>2, intercambiamos los elementos, quedando el vector como sigue: [1, 2, 3, 4, 5]
Ya tenemos el 1 y el 2 bien colocados. Ahora examinamos el subvector [3..5] (los elementos de las posiciones 3 a 5, es decir, [3, 4, 5]). El mínimo dentro de este subvector es el 3, y el primer elemento del subvector es el 3. Como coinciden, no realizamos ningún intercambio, quedando el vector como sigue: [1, 2, 3, 4, 5] Ya tenemos el 1, el 2 y el 3 bien colocados. Examinamos el subvector [4..5] (los elementos de las posiciones 4 a 5, es decir, [4, 5]). El mínimo dentro de este subvector es el 4, y el primer elemento del subvector es el 4. Como coinciden, no realizamos ningún intercambio, quedando el vector como sigue: [1, 2, 3, 4, 5] Y ya no es necesario seguir, porque el subvector que nos quedaría por comprobar sólo tiene un elemento, que seguro que es el último. Suponiendo que tenemos un vector de enteros, el algoritmo quedaría como sigue:
VARIABLES v: Vector[1..N] de Entero; i, j, min, posMin, aux: Entero; FIN VARIABLES
desde i = 1 hasta N-1 hacer min = v[i]; posMin = i; desde j = i hasta N hacer si v[j] < min entonces min = v[j]; posMin = j; fin si fin desde
aux = v[i]; v[i] = v[posMin]; v[posMin] = aux; fin desde
Algoritmo de la burbuja La idea de este algoritmo consiste en ir comparando elementos adyacentes e intercambiarlos si el orden no es correcto. Vamos a ver un ejemplo que nos mostrará cómo exactamente. Tenemos el vector [3, 1, 5, 4, 2]. Empezamos comparando entre sí la primera pareja: 3 y 1. Como 3>1, los intercambiamos, quedando el vector:
[1, 3, 5, 4, 2] Ahora comparamos la siguiente pareja: 3 y 5. Como 3<5, no es necesario intercambiarlos, quedando el vector: [1, 3, 5, 4, 2] Comparamos la siguiente pareja: 5 y 4. Como 5>4, los intercambiamos, quedando el vector: [1, 3, 4, 5, 2] Comparamos la última pareja: 5 y 2. Como 5>2, los intercambiamos, quedando el vector: [1, 3, 4, 2, 5] ¿Está ya el vector ordenado? No, como puede comprobarse. ¿Qué hemos hecho con este proceso, entonces? Pues hemos conseguido "empujar" el elemento más grande del vector a la última posición, de manera que este elemento ya sabemos que está ordenado, y ahora hemos de repetir el proceso, pero no ya con todo el vector, sino sólo con el subvector [1..4]. Vamos a ir viendo cómo se mueven los elementos en esta segunda vuelta: Inicialmente: [1, 3, 4, 2, 5] [1, 3, 4, 2, 5] (no se mueven porque 1<3) [1, 3, 4, 2, 5] (no se mueven porque 3<4) [1, 3, 2, 4, 5] (los intercambiamos porque 4>2) (no comparamos 4 con 5 porque ya sabemos que 5 está bien colocado) Ya hemos conseguido "empujar" el segundo elemento más grande del vector a la penúltima posición. Ahora repetimos el mismo proceso, pero para el subvector [1..3]: Inicialmente: [1, 3, 2, 4, 5] [1, 3, 2, 4, 5] (no se mueven porque 1<3) [1, 2, 3, 4, 5] (los intercambiamos porque 3>2) (no comparamos 3 con 4 porque ya sabemos que 4 está bien colocado) Ya hemos "empujado" el tercer elemento más grande del vector a su posición correcta. Repetimos el proceso, pero para el subvector[1..2]: Inicialmente: [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] (no se mueven porque 1<3) (no comparamos 2 con 3 porque ya sabemos que 3 está bien colocado) Y ya hemos terminado, puesto que el subvector que queda es de un único elemento que, por fuerza, tiene que estar bien colocado, ya que hemos "empujado" a los mayores que el a la posición correcta. El algoritmo, suponiendo que estamos tratando con un vector numérico (de enteros, por ejemplo), sería:
VARIABLES v: Vector[1..N] de Entero; i, j, aux: Entero; FIN VARIABLES
desde i = 1 hasta N hacer desde j = 1 hasta N - i hacer si v[j] > v[j+1] entonces aux = v[j]; v[j] = v[j+1]; v[j+1] = aux; fin desde fin desde
9.II. Algoritmos de búsqueda Los algoritmos de búsqueda son aquellos que se centran en buscar un cierto elemento dentro de un vector y, quizá, devolver la posición en la que se encuentra dicho elemento. Vamos a estudiar dos algoritmos: el algoritmo básico de búsqueda (búsqueda secuencial), y el algoritmo de búsqueda dicotómica. Este último es más rápido que el primero, pero como contrapartida tiene que sólo funcionará cuando el vector en el que se busca está ordenado. Si el vector no está ordenado, no tiene ningún sentido hacer una búsqueda dicotómica.
Algoritmo de búsqueda secuencial Es muy sencillo: se trata de recorrer el vector, elemento por elemento, preguntando si el elemento que visitamos es igual al que estamos buscando, y terminando de buscar cuando lo encontremos. El algoritmo quedaría como sigue (nuevamente, suponemos que tratamos con un vector de enteros):
VARIABLES encontrado: Logico; v: Vector[1..N] de Entero; i, elemBuscado: Entero: FIN VARIABLES
Leer_del_teclado(elemBuscado);
encontrado = Falso; i = 1;
mientras NO(encontrado) Y (i < N) hacer si v[i] = elemBuscado entonces encontrado = Verdadero; fin si
i = i + 1; fin mientras
Es evidente que si al terminar de recorrer el vector, tenemos que encontrado=Falso, quiere decir que no se ha encontrado el elemento dentro del vector.
Algoritmo de búsqueda dicotómica Como ya hemos dicho, este algoritmo asume que estamos trabajando con un vector ordenado. Esta asunción no es a la ligera, y no debemos olvidarla, como comprobaremos a continuación. La idea es empezar a buscar el elemento objetivo por la mitad del vector, es decir, miraremos si el elemento que está en el centro del vector coincide con el elemento buscado. Si coincide, ya hemos terminado. Si no coincide, entonces miramos qué sucede: si el elemento buscado es menor que el elemento que está en el centro del vector, entonces sólo buscaremos en el subvector izquierdo; si el elemento buscado es mayor que el que está en el centro del vector, entonces sólo buscaremos en el subvector derecho. Se entiende por subvector izquierdo el vector formado por los elementos [1..centro - 1] y por subvector derecho el vector formado por los elementos [centro + 1..N]. Dentro del subvector adecuado, repetimos el proceso: comparamos con el elemento central. Si es igual, hemos terminado, si es menor, buscamos en el subvector izquierdo, si es mayor, buscamos en el subvector derecho. La manera de colocarnos en el subvector adecuado es por medio de tres variables auxiliares, que llamaremos izq, der y medio. En la variable izq almacenaremos la posición del vector que se corresponderá con el extremo izquierdo del subvector, en la variable der almacenaremos la posición del vector que se corresponderá con el extremos derecho del subvector, y en la variable medio almacenaremos la posición central del subvector. Por ejemplo, si tenemos el vector [1, 2, 3, 4, 5], empieza siendo izq = 1, der = 5 y medio = 3. Ahora bien, el subvector izquierdo correspondería a izq = 1, der = 2 (= medio - 1), mientras que el subvector derecho correspondería a izq = 4 (= medio + 1), der = 5. Es decir, si nos toca movernos al subvector izquierdo, ajustaremos el valor de der como der = medio - 1 dejando izq como está, mientras que si nos toca movernos al subvector derecho, ajustaremos el valor de izq como izq = medio + 1, dejando der como está. En ambos casos hay que recalcular medio = (izq + der)/2, ya que al movernos a cualquiera de los dos subvectores, la posición central cambia. Vamos a estudiarlo con detenimiento en el siguiente ejemplo. Buscamos si el valor 12 está en el siguiente vector: v = [1, 2, 3, 4, 12, 13, 14] El vector tiene N = 7 elementos, izq = 1, der = 7, medio = (izq + der) / 2 = (1+7)/2 = 4. Así que empezamos viendo si v[medio] = 12. Pero v[4] = 4, que no es 12. Ahora bien, ¿12 < 4? No, entonces no buscamos en el subvector izquierdo (que sería [1, 2, 3]). Si 12 <> 4 y no es 12 < 4, tiene que ser 12 > 4, así que buscamos en el subvector derecho: [12, 13, 14]. Esto requiere que ajustemos los valores de izq, der y medio. Como nos hemos movido al subvector derecho, izq = medio + 1 = 5, der = 7, y ahora medio = (izq + der)/2 = (5+7)/2 = 6 (observamos que la posición 6 del vector se corresponde con el centro del subvector [12, 13, 14]). Así que vemos si v[6] = 12. Pero v[6] = 13, que no es 12. ¿12 < 13? Sí, pues entonces buscamos en el subvector izquierdo, que es [12].
Tenemos que ajustar de nuevo los valores de izq, der y medio. Como nos hemos movido al subvector izquierdo, izq se queda como está (izq = 5), der = medio - 1 = 6-1 = 5, medio = (izq + der)/2 = (5+5)/2 = 5. Así que vemos si v[5] = 12. Efectivamente, v[5] = 12, que es el valor buscado. Gráficamente (para intentar aclarar un poco más):
Ahora bien, ¿cómo sabemos si un elemento no está dentro del vector utilizando este algoritmo? Lo sabremos en el momento en que izq > der. Cuando se crucen los valores de izq y der es porque no se ha encontrado el elemento. Vamos a ver cómo sería el algoritmo:
VARIABLES v: Vector[1..N] de Entero; encontrado: Logico; izq, der, medio, elemBuscado: Entero; FIN VARIABLES
Leer_del_teclado(elemBuscado);
encontrado = Falso; izq = 1; der = n;
mientras (izq < der) Y NO(encontrado) hacer medio = (izq + der) / 2;
si v[medio] = elemBuscado entonces encontrado = Verdadero; sino si v[medio] < elemBuscado entonces izq = medio + 1; sino (* v[medio] > elemBuscado *) der = medio - 1; fin si fin si fin mientras
10. Introducción a la POO (programación orientada a objetos) La programación orientada a objetos (POO u OOP en inglés) es una filosofía de programación que se basa en considerar que los programas están compuestos de unas unidades llamadas objetos, y las acciones que se ejecutan por tanto están relacionadas con ellos. Un objeto es un tipo de datos especial que reúne una estructura de datos (lo que hemos llamado "tipo definido por el usuario"), así como a los procedimientos que trabajan con estos datos. A partir de un objeto se pueden derivar otros objetos más especializados (lo que se conoce como herencia), situación que permite modelar ciertas jerarquías que se dan en la vida real. Pongamos, por ejemplo, que queremos realizar un programa que trate sobre seres vivos. Crearemos un objeto "ser vivo" que tendrá una serie de datos como "tiempo medio de vida" y unos métodos (se llama método a la función o procedimiento que trabaja con los datos de un objeto) que permitirán manipular estos datos. Ahora resulta que dentro de los seres vivos tenemos a las plantas y a los animales. Estas dos entidades son una especialización del objeto primitivo "ser vivo". Cada uno de ellos es un ser vivo, pero tienen detalles que les hacen diferentes entre sí y por eso tienen que ser tratados de forma distinta. Dentro de los animales, a su vez, tenemos vertebrados e invertebrados. De nuevo, una especialización, que tiene todas las características de los antecesores y algunas propias. Dentro de los vertebrados, tenemos peces, anfibios, reptiles, aves y mamíferos: una nueva especialización. Vamos a poner ahora un ejemplo más cercano al mundo de la programación. Supongamos que queremos hacer un programa de dibujo. Probablemente querremos poner elementos como ventanas, botones, menús, barras de desplazamiento... y otros. Pensando un poco, todo esto se puede llevar a cabo con lo que uno sabe de programación procedural, sin embargo, ¿no sería más fácil (por ejemplo) tener un objeto VENTANA? Este objeto podría tener las siguientes variables: coordenadas del rectángulo en el que se va a dibujar, estilo de dibujo... y podría tener unas funciones que, leyendo estos datos, dibujaran la ventana, cambiaran su tamaño, la cerraran... La ventaja que tendría esto es que ya podemos crear cuantas ventanas queramos en nuestro programa, puesto que las funciones asociadas tendrían en cuenta las variables de este objeto y no deberíamos pasárselas como argumentos.
Igual que con la ventana, se podría crear el objeto BOTON, a partir del objeto ventana (pues a fin de cuentas se trata de un rectángulo que debemos de pintar), que podría tener como variables las coordenadas (heredadas de la ventana), si está pulsado o no, si está el ratón sobre él o no... y unas funciones que lo dibujaran, lo dibujaran pulsado, detectaran si se ha pulsado el botón, hicieran algo si ese botón se ha pulsado... Las funciones de un objeto nos proporcionan un interfaz para manejarlo: no necesitamos saber cómo está hecho un objeto para poder utilizarlo. Cuando creamos un objeto, en realidad estamos creando un 'molde', que recibe el nombre de clase, en el que especificamos todo lo que se puede hacer con los objetos (ahora sí) que utilicen ese molde. Es decir, en realidad lo que uno hace es crear una clase. Cuando va a utilizarla, crea instancias de la clase, y a estas instancias es a lo que se le llama objeto. No vamos a dar una notación rigurosa para la definición de clases puesto que esto es sólo una introducción, y sobre POO se puede hablar bastante. Lo que vamos a dar es una pequeña orientación sobre cómo emplear la notación de acceso a las partes de un objeto, pues el lector puede así a partir de aquí empezar a programar en un entorno como sería Delphi, sin saber mucho sobre POO, pero sabiendo lo suficiente como para emplear los objetos que Delphi pone a su disposición. Por ejemplo, en el caso de la ventana, podríamos definir una clase de la manera siguiente: CLASE Ventana VARIABLES x0, y0, x1, y1: ENTEROS FUNCIONES Inicializar_Coordenadas() Dibujar_Ventana() Mover_Ventana() FIN CLASE Ventana Y cuando queramos tener una (o varias) ventanas, las declararíamos (instanciaríamos) como cualquier otro tipo de variable: Ventana Ventana1, Ventana2; A la hora de acceder a las variables que tiene cada una de estas ventanas (cuyos valores son distintos para cada ventana) o de utilizar las funciones de cada una de estas ventanas, habría que poner: Ventana1.x0 = 3; Ventana2.Dibujar_Ventana(); es decir: Objeto.Variable Objeto.Funcion(argumentos) En la definición de una clase se distinguen las partes privadas de las partes públicas. Un elemento privado de una clase es aquel que únicamente puede modificarse/emplearse desde la propia clase. Si, en el ejemplo de la clase Ventana, declaramos las variables x0, y0, x1, y1 como privadas, sólo desde el código de las funciones de la clase Ventana se podría tener acceso/modificar estos valores, mientras que desde cualquier otro punto (función principal del programa, otras funciones del programa) obtendríamos un error al intentar acceder a estas variables. La motivación de esto es la siguiente: cuando diseñamos un objeto, existen partes del mismo que no deben ser modificadas más que por el propio objeto, pues cualquier acceso desde otra parte del programa podría causar comportamientos extraños. Así, declarando ciertas partes del objeto como privadas, se previenen los accesos indebidos que podrían provocar problemas.
Todo lo contrario sucede con las partes públicas de un objeto: son variables o funciones que pueden ser accedidas desde cualquier punto del programa. Esto será así porque su uso/modificación en cualquier parte del programa no desencadenará comportamientos extraños en el objeto. Existen, además, dos funciones especiales llamadas constructor y destructor. La misión del constructor es inicializar aquellas variables que sea necesario tener inicializadas (muy probablemente pasando dichos valores al constructor) así como poner a punto algunos comportamientos necesarios, quizá llamando a alguna de las funciones de la clase, o bien reservar memoria en el caso de que trate con variables dinámicas. La misión del destructor será, principalmente, la de liberar todos los recursos que hayan podido reservarse por el objeto.
A1. Acciones básicas A lo largo de la primera parte del curso, hemos estado hablando de algoritmos, funciones, procedimientos, estructuras repetitivas, y otras cosas no menos importantes para escribir un programa. Sin embargo, una acción muy común cuando uno se enfrenta con una máquina es coger el teclado y ponerse a escribir. Paralelamente, uno se espera que lo que escribe vaya apareciendo por la pantalla. Cuando alguien realiza un programa, lo más normal es que vaya a interactuar con el usuario de alguna manera, bien pidiendo datos, bien mostrando paso a paso los resultados de un cierto proceso, o bien leyendo la posición del puntero del ratón para ir dibujando puntos... Lógicamente, querremos que los resultados de una entrada del usuario vayan a parar a algunas variables que nosotros habremos dispuesto para luego operar con ellas. Luego, podremos querer mostrar por pantalla el resultado de una acción, que tendremos almacenado en alguna variable, para que el usuario pueda comprobar valores, usarlos posteriormente, etc... Y en todo lo que hemos visto anteriormente, sólo muy de pasada, hemos visto cómo podíamos llevar a cabo estas operaciones básicas de entrada/salida. El nombre, formato, etc., depende del lenguaje escogido; sin embargo, todas tienen algo en común, y eso es lo que vamos a describir ahora. Tenemos tres acciones básicas: asignación, lectura (o entrada) y escritura (o salida). De la asignación ya hablamos en el tema de las variables.
Acciones de LECTURA Las acciones de entrada nos permiten obtener determinados valores a partir de un periférico (teclado, ratón, un fichero...) y ASIGNARLOS a unas determinadas variables. Cuando escribimos un algoritmo, la acción de lectura se escribe: leer de (periferico) (lista de variables de entrada) Por ejemplo: leer de teclado (x,y,z) Si ahora el usuario introduce los números 20, 10, 12, automáticamente se habrán realizado las acciones de asignación:
x <- 20 y <- 10 z <- 12
Acciones de ESCRITURA Las acciones de salida permiten transferir a un periférico (pantalla, un fichero, impresora, ...) resultados obtenidos por la máquina. En un algoritmo, esta acción la pondremos de la siguiente forma: mostrar por (periférico) (lista de variables de salida) Por ejemplo, tras hacer: A <- 100 B <- 101 C <- 99 si en nuestro algoritmo pone: mostrar por impresora (A,B,C) nuestra impresora se pondrá en marcha y nos sacará los valores 100, 101, 99
Una pequeña PostData PD: Existe una estructura de salto incondicional, se llama GOTO, y tiene la forma GOTO etiqueta donde etiqueta es un nombre que nosotros elijamos como referencia. Usualmente van seguidas de : para saber que se trata de una etiqueta y no una variable (por ejemplo) NO declarada. El funcionamiento es bien sencillo: en cuanto llegamos al GOTO, el flujo del programa automáticamente da un salto a la zona especificada a partir de la etiqueta. Su uso no es nada recomendado por varios motivos: dificulta la lectura del programa y pueden descontrolarse muy fácilmente si no se les sigue bien la pista. Lo que se puede hacer un GOTO se puede hacer con las otras estructuras que hemos visto. Por ejemplo:
i <- 0 suma <- 0 saltar: suma <- suma + i si (i<10) entonces hacer i <- i+1 GOTO saltar
fin si
puede hacerse también así:
suma <- 0 desde <-0 mientras i<10 hacer suma <- suma+i fin desde
Mucho más compacto y menos engorroso. Sin embargo, podemos estar en un bucle demasiado anidado y quizá en algún momento el programa tenga un parón crítico y haya que salir de ese bucle como sea: quizá ese momento sea el único en que el GOTO nos pueda salvar, pero salvo en algo muy crítico, lo mejor es evitarlo. Me refiero en lenguajes de alto nivel, claro O:-D ....
A2. Introducción a la lógica Estamos realizando un cálculo complejo siguiendo (por ejemplo) algún método iterativo en el cual, como condición de parada, necesitamos que sucedan varias cosas: • No exceder de una cierta tolerancia o cota del error • No exceder de un número máximo de iteraciones • Tener un número mínimo de cifras exactas en el resultado y estas condiciones deben comprobarse A LA VEZ. ¿Cómo lo hacemos? Bueno, espero que quede claro tras la siguiente (necesaria y breve) introducción a la lógica. En primer lugar, tenemos las proposiciones. Una proposición es una afirmación de la que se puede decir sin ambigüedad y de forma excluyente que es cierta o falsa. El valor lógico de la verdad es 1, y el valor lógico de la mentira es 0. Por ejemplo: ''María es una chica'' es una proposición, puesto que afirmamos algo sin ambigüedad alguna y, además, o es verdad, o no lo es. O es chica, o no lo es. Sin embargo: ''si no tienes dinero, eres pobre o gastas mucho'' no es una proposición. No es una afirmación de la que se pueda decir sin ambigüedad y de forma excluyente que sea verdad o mentira. No tener dinero puede ser consecuencia de un robo, o de muchas otras cosas. Sin embargo, esto lo podemos dividir en proposiciones más sencillas: ''no tienes dinero'' (evidentemente, o es verdad o no lo es), ''eres pobre'' (cierto o no) y ''gastas mucho'' (verdad o mentira). Estas proposiciones están unidas mediante lo que se llaman CONECTORES LOGICOS. Estos son los siguientes: o
||
y
&&
no
!
o exclusivo
XOR
condicional
-> "si... entonces..."
doble condicional
<-> "... si y sólo si..."
He usado la notación de C para los tres primeros, por no tener disponibles los símbolos lógicos O:) Si tenemos dos proposiciones p, q unidas por alguno de estos conectores, el valor de verdad de la proposición compuesta, según el valor de verdad de p y de q, viene dado en la siguiente tabla (llamada Tabla de Verdad): p
q
!p
p && q
p || q
p -> q
p <-> q
p XOR q
1
1
0
1
1
1
1
0
1
0
0
0
1
0
0
1
0
1
1
0
1
1
0
1
0
0
1
0
0
1
1
0
Claramente, p && q será cierta cuando sean ciertas p y q. No podemos decir "verdad" y "mentira" a la vez y pretender que esto sea cierto. Sin embargo, p || q es cierta cuando es cierta una de las dos proposiciones. Si tengo "verdad" o "mentira", está claro que una o la otra (si no las dos) es cierto. Para aclarar el XOR, pondré un ejemplo: O estás vivo o estás muerto, pero ni puedes estar las dos cosas a la vez ni puedes estar ninguna, por eso sólo es cierto cuando únicamente una de las dos proposiciones es cierta. Una forma proposicional que es SIEMPRE VERDAD se llama TAUTOLOGIA. Si es SIEMPRE FALSA se llama CONTRADICCION. Siempre tendremos que p || (!p) es tautología, mientras que p && (!p) es contradicción. Cuando -> es tautología, se llama IMPLICACION. Cuando <-> es tautología, se llama DOBLE IMPLICACION y la simbolizaré con <|=|> Voy a poneros las propiedades de &&, || (podeis convenceros haciendo las tablas de verdad), que muchas veces os serán útiles: Leyes asociativas p || (q || r) <|=|> (p || q) || r p && (q && r) <|=|> (p && q) && r Leyes conmutativas p || q <|=|> q || p p && q <|=|> q && p Leyes distributivas p && (q || r) <|=|> (p && q) || (p && r) p || (q && r) <|=|> (p || q) && (p || r) p && 1 <|=|> p (1 es tautología) p || 0 <|=|> p p && p <|=|> p p || p <|=|> p
(0 es contradicción)
p || (!p) <|=|> 1 p && (!p) <|=|> 0 p <|=|> !(!p) Como consecuencia, se tiene (podeis comprobarlo): !1 <|=|> 0 !0 <|=|> 1 !(p && q) <|=|> !p || !q
!(p || q) <|=|> !p && !q Estas dos últimas se conocen como "Leyes de De Morgan", y son muy importantes. Además, pueden generalizarse a n proposiciones. Espero que con esto tengais un pequeño instrumento que os ayude a pensar en términos lógicos a la hora de evaluar una condición. Al fin y al cabo, vosotros quereis que la máquina haga algo si el resultado lógico es 0 o 1, así que esto es precisamente lo que necesitais.
A3. Consejos varios a la hora de desarrollar un algoritmo, un programa o, si me apuras, hasta una receta de cocina En este "breve" texto voy a exponer el procedimiento "ideal" de desarrollo de un programa. No son más que unas pocas consideraciones que, en proyectos cortos, quizá no hagan mucha falta, pero que a la hora de afrontar algo "grande" ya sí hay que prestarle más atención a qué se quiere hacer y cómo se quiere hacer. Muy esquemáticamente, lo que tendríamos que hacer es lo siguiente:
Analizar el problema: Para ello, tendremos que definir el problema con precisión, especificar los datos de partida (los datos que tendremos que entrar) y especificar la información que ha de darse al resolverse el problema (datos de salida, un mensaje comunicando algo...) Por ejemplo, si queremos escribir un algoritmo para calcular el área de un rectángulo, tendremos que pensar en las siguientes cosas: "Me tienen que dar dos datos, uno de ellos será la base del rectángulo, y el otro la altura. Estos datos serán reales. Por otro lado, la salida es el área, que también ha de ser un real."
Diseñar el algoritmo No es más que pensar cómo resolver el problema, y escribirlo de acuerdo a unas reglas. Sin embargo, no todos los problemas son tan sencillos como querer calcular el área de un rectángulo. Muchos de ellos son mucho más complejos (como, por ejemplo, buscar si existe un tal "Pepe BuenaVida" dentro de las fichas de un videoclub). Por ello, hay una estrategia conocida con el nombre de "divide y vencerás". Se basa en dividir un problema gordo y complejo en otros más sencillos. Tiene algunas ventajas:
• •
El algoritmo es más fácil de entender, al tenerlo dividido en trocitos. Es más fácil modificar cada trocito que si lo hubiéramos hecho a la brava (además, el hacer las cosas a la brava tiene la pega de que buscar el lugar donde hay que hacer modificaciones es más complicado).
Ademas, otra recomendación es describir primero los algoritmos en un número reducido de pasos y, en una segunda vuelta, describir con más detalle estos pasos. Por ejemplo, si pensamos en "hacer pollo frito", lo primero que pensamos es: 1. 2.
Coger los ingredientes Coger los utensilios
3.
Hacer el pollo frito
Pero luego, a su vez, estos tres pasos se pueden detallar mucho más. ¿Me dais sugerencias? (y así, de paso, me preparo un pollo frito a vuestra salud ;) )
Usar el ordenador para programar ese algoritmo y ver cómo funciona Obviamente, a nosotros puede parecernos que nuestro algoritmo está impecable, pero siempre es bueno probarlo, traduciéndolo al lenguaje que más nos guste (o, simplemente, al que vayamos a usar), porque, si no obtenemos la salida deseada, podemos pararnos a revisar nuestro algoritmo en busca de fallos. A la fase de conversión de un algoritmo en las instrucciones de un lenguaje de programación la llamamos codificación, y al algoritmo, una vez que lo tenemos escrito en un lenguaje de programación específico, lo llamamos código. Estos pequeños consejos, aunque obvios, son ignorados muchas veces, y hacen padecer, desde el principio, a un programa de una mala estructura. Pararse a pensar un poco en lo que se quiere hacer y recapacitar en la mejor manera posible puede ahorrarnos que, más adelante, cuando queramos hacer alguna modificación al programa, tengamos que reescribirlo por completo. Fin Ultima modificación: 1 de marzo de 2004 Autor: Lola Cárdenas Luque