UNIVERSIDAD SALESIANA DE BOLIVIA INGENIERIA DE SISTEMAS DOSSIER PROGRAMACION III DOCENTE LIC. PATRICIA PALACIOS ZULETA PARALELO A2 I I- 2011 1 INDICE Pag. UNIDAD I CONCEPTOS FUNDAMENTALES DE PROGRAMCION ORIENTADA A OBJETOS. Introducción……………………………………………………………………………………….. Programación Orientada a Objetos………………………………………………...…………… Breve historia de la P.O.O.................................................................................................... Conceptos básicos............................................................................................................... Definición de objeto.............................................................................................................. Herencia................................................................................................................................ Abstracción............................................................................................................................ Encapsulación....................................................................................................................... Polimorfismo.......................................................................................................................... Función Amiga....................................................................................................................... 1 3 5 7 10 13 16 16 17 17 UNIDAD II PRINCIPIOS DE PROGRAMACION ORIENTADA A OBJETOS CON C++ Y JAVA Introducción.......................................................................................................................... Historia y características del C++.......................................................................................... Características del C++......................................................................................................... Abstracción de datos............................................................................................................ Clases y objetos en C++........................................................................................................ Envió de mensajes............................................................................................................... Encapsulamiento.................................................................................................................. Constructores y destructores................................................................................................ Historia del Java.................................................................................................................... Ejercicios................................................................................................................................ 18 18 19 20 22 22 24 27 28 32 UNIDAD III SOBRECARGA Sobrecarga............................................................................................................................ Sobrecarga de funciones....................................................................................................... Sobrecarga de operadores.................................................................................................... Ejercicios................................................................................................................................ 33 33 36 39 UNIDAD IV HERENCIA Introducción........................................................................................................................... Herencia................................................................................................................................ Herencia Simple..................................................................................................................... Control de acceso a la Clase base......................................................................................... Constructores en Herencia..................................................................................................... Herencia Múltiple.................................................................................................................... 2 42 42 42 45 50 52 UNIDAD V FUNCIONES VIRTUALES Y POLIMORFISMO Introducción......................................................................................................................... Ligadura.............................................................................................................................. Tipos de Ligadura................................................................................................................ Funciones Virtuales............................................................................................................. Polimorfismo........................................................................................................................ Ejemplos.............................................................................................................................. 57 57 60 60 66 67 UNIDAD VI PLANTILLAS Y TRATAMIENTO DE EXCEPCIONES Introducción.......................................................................................................................... Plantillas................................................................................................................................ Concepto de Excepciones..................................................................................................... Lanzamiento de excepciones................................................................................................ 68 68 72 74 LECTURAS COMPLEMENTARIAS 79 80 BIBLIOGRAFIA 3 I OBJETIVOS DE LA MATERIA GENERAL El objetivo principal radica principalmente en permitir conocer, aplicar y desarrollar el paradigma de POO en el desarrollo de aplicaciones profesionales para la resolución de problemas, utilizando las herramientas de software, lo cual facilitara la inserción de los profesionales en el desarrollo de software. ESPECÍFICOS Conocer algunas técnicas de ingeniería de software usadas en el diseño y la implementación de programas en el lenguaje de programación C++ y Java. Aplicar las estructuras de datos más convenientes para solucionar problemas específicos. II COMPETENCIA DE LA MATERIA El objetivo de la asignatura es formar al alumno en los conceptos de la Programación Orientada a Objetos, haciendo especial énfasis en sus aspectos más prácticos. Esto supone que a la finalización del curso el alumno estará plenamente capacitado para desarrollar un programa siguiendo el paradigma orientado a objetos como alternativa a la programación procedimental. Para ello se introducirá al alumno en el lenguaje de programación más ampliamente usado el lenguaje de programación C++ y Java. COMPETENCIAS De forma más esquemática, las competencias que adquirió el alumno son las siguientes: 1. Conocer y manipular con destreza el concepto de abstracción de datos. 2. Diseñar, especificar e implantar tipos de datos abstractos. 3. Dominar con destreza los paradigmas de la orientación por objetos. 4. Conocer técnicas para la especificación, diagramación y desarrollo de sistemas 4 TEMA I TEMA No 1 INTRODUCCIÓN A LA PROGRAMACIÓN ORIENTADA A OBJETOS Introducción.La programación orientada a objetos (o POO), es un paradigma de desarrollo definido y utilizado por una serie de lenguajes de programación que busca afrontar el desarrollo de aplicaciones desde un enfoque distinto al estructurado. Toda la historia de los lenguajes de programación se ha desarrollado en base a una sola idea conductora: hacer que la tarea de realizar programas para ordenadores sea cada vez lo más simple, flexible y portable posible. La POO supone, no solo un nuevo paso hacia ese fin, sino que además, a nuestro juicio, es el más importante acaecido hasta el momento. Como nos comenta Eckel: "A medida que se van desarrollando los lenguajes, se va desarrollando también la posibilidad de resolver problemas cada vez más complejos. En la evolución de cada lenguaje, llega un momento en el que los programadores comienzan a tener dificultades a la hora de manejar programas que sean de un cierto tamaño y sofisticación." Esta evolución en los lenguajes, ha venido impulsada por dos motores bien distintos: Los avances tecnológicos Los avances conceptuales (de planteamiento) Los avances en cuanto a enfoque de la programación Evolución en cuanto a la conceptualización.- El primer avance en metodología de programación, vino con la Programación Estructurada (en este concepto vamos a incluir el propio y el de técnicas de Programación con Funciones también llamado procedural, ya que ambos se hallan íntimamente relacionados, no creemos, que se pueda concebir la programación estructurada sin el uso masivo de funciones). 5 La programación en ensamblador es lineal, es decir, las instrucciones se ejecutan en el mismo orden en que las escribimos. Podemos, sin embargo, alterar este orden haciendo saltos desde una instrucción a otro lugar del programa distinto a la instrucción que le sigue a la que se estaba procesando. Programación lineal.Cada línea de programa debe ir precedida de un identificador (una etiqueta) para poder referenciarla, para este ejemplo hemos utilizado números, aunque podría utilizarse cualquier otro identificador: 1. Hacer una variable igual a 0 2. Sumar 1 a esa variable 3. Mostrar la variable 4. Si la variable es 100 -> terminar, Si_no -> saltar a 1.: Programación estructurada.1. Hacer una variable igual a 0 2. Mientras que sea menor que 100 -> sumar 1 y mostrarla Lo importante aquí, es que cuando escribimos un programa usando las técnicas de programación estructurada, los saltos están altamente desaconsejados, por no decir prohibidos; en cambio en BASIC, por ejemplo, son muy frecuentes (todos conocemos el prolífico GOTO <nLínea>), lo que no es nada conveniente si queremos entender algo que escribimos hace tres meses de forma rápida y clara. Para evitar esto, junto con la programación estructurada aparece un concepto que nos permite abarcar programas más amplios con menor esfuerzo: el de función. La idea es muy simple: muchas veces realizo procesos que se repiten y en los que sólo cambia algún factor, si trato ese proceso como un subprograma al que llamo cada vez que lo necesito, y cada vez que lo llamo puedo cambiar ese factor, estaré reduciendo el margen de error, al reducir el número de líneas que necesito en mi programa, ya que no tengo que repetir todas esas líneas cada vez que quiera realizar el proceso, con una sola línea de llamada al subprograma será 6 suficiente; además, de haber algún fallo en este proceso el error queda circunscrito al trozo de código de la función. Así, las funciones podemos entenderlas como unas cajas negras, que reciben y devuelven valores. Solo tengo que programarlas una vez, las puedo probar por separado y comprobar que funcionan correctamente, una vez terminadas puedo olvidarme de cómo las hice y usarlas siempre que quiera. Programación Orientada al Objeto.Por último, llegamos al más reciente avance, la POO, que nos ofrece mucho mayor dominio sobre el programa liberándonos aún más de su control. Hasta ahora, el control del programa era tarea del programador. El programador tenía que controlar y mantener en su mente cada proceso que se realizaba y los efectos colaterales que pudieran surgir entre distintos procesos, lo que llamamos colisiones. En POO, el programa se controla a sí mismo y la mente del programador se libera enormemente pudiendo realizar aplicaciones mucho más complejas al exigir menor esfuerzo de atención, ya que los objetos son entidades autónomas que se controlan (si han sido creados correctamente) a sí mismos. Esto es posible principalmente porque los objetos nos impiden mezclar sus datos con otros métodos distintos a los suyos. En programación estructurada, una función trabaja sobre unos datos, y no debería modificar datos que no le corresponde hacer, pero de eso tiene que encargarse el programador, en POO es el propio sistema de trabajo el que impide que esto ocurra. Además, la re - usabilidad del código escrito es mucho mayor que con el uso de funciones, y las portabilidad es también mayor. ¿Qué es la POO? Comenzaremos dando una definición por exclusión de lo que es la POO, es decir, vamos a decir primero qué no es la POO, para, luego, intentar dar una definición de lo que es. LA POO no es: No es un lenguaje. De hecho las técnicas de POO pueden utilizarse en cualquier lenguaje conocido y los que están por venir, aunque estos últimos, al menos en los próximos años, incluirán facilidades para el manejo de objetos. Desde luego, que en los lenguajes que prevén el uso de objetos la implementación de las técnicas de POO resulta mucho más fácil y provechosa que los 7 otros. Pero del mismo modo a lo comentado en el punto anterior, se pueden utilizar estos lenguajes sin que los programas resultantes tengan nada que ver con la POO Como ya hemos comentado, la POO son un conjunto de técnicas que nos permiten incrementar enormemente nuestro proceso de producción de software; aumentando drásticamente nuestra productividad por un lado y permitiéndonos abordar proyectos de mucha mayor envergadura por otros. Usando estas técnicas, nos aseguramos la re-usabilidad de nuestro código, es decir, los objetos que hoy escribimos, si están bien escritos, nos servirán para "siempre". Hasta aquí, no hay ninguna diferencia con las funciones, una vez escritas, estas nos sirven siempre. Pero es que, y esto sí que es innovador, con POO podemos re-usar ciertos comportamientos de un objeto, ocultando aquellos otros que no nos sirven, o redefinirlos para que los objetos se comporten de acuerdo a las nuevas necesidades. Veamos un ejemplo simple: si tenemos un coche y queremos que sea más rápido, no construimos un coche nuevo; simplemente le cambiamos el carburador por otro más potente, cambiamos las ruedas por otras más anchas para conseguir mayor estabilidad y le añadimos un sistema turbo. Pero seguimos usando todas las otras piezas de nuestro coche. Desde el punto de vista de la POO ¿Qué hemos hecho? Hemos modificado dos cualidades de nuestro objeto (métodos): el carburador y las ruedas. Hemos añadido un método nuevo: el sistema turbo. En programación tradicional, nos hubiésemos visto obligados a construir un coche nuevo por completo. Dicho en términos de POO, si queremos construir un objeto que comparte ciertas cualidades con otro que ya tenemos creado, no tenemos que volver a crearlo desde el principio; simplemente, decimos qué queremos usar del antiguo en el nuevo y qué nuevas características tiene nuestro nuevo objeto. 8 Aún hay más, con POO podemos incorporar objetos que otros programadores han construido en nuestros programas, de igual modo a como vamos a una Ferretería y compramos piezas de madera para ensamblarlas y montar una estantería o una mesa. Pero, es que además podemos modificar los comportamientos de los objetos construidos por otros programadores sin tener que saber cómo los han construido ellos. Breve historia de la POO.Los conceptos de clase y herencia fueron implementados por vez primera en el lenguaje Simula 67 (el cual no es sino una extensión de otro más antiguo, llamado Algol 60), este fue diseñado en 1967 por Ole-Johan Dhal y Krysten Nygaard en la Universidad de Oslo y el Centro de Computación Noruego (Norsk Regnesentral). La historia de Simula, que es como se le llama coloquialmente, es tan frecuente como desafortunada. Fue diseñado como un lenguaje de propósito general y pasó por el mundo de la informática sin pena ni gloria durante años. Fue mucho después, con la aparición de otros lenguajes que se basaban en estos innovadores conceptos (Smalltalk y sobretodo C++), cuando se le reconoció a los creadores de Simula su gran mérito. Sin embargo, Simula sigue sin usarse porque estos conceptos han sido ampliados y han aparecido otros nuevos que le dan mayor potencia y flexibilidad a los conceptos originales de clase y herencia, conformando lo que hoy entendemos por Programación Orientada al Objeto. Aunque Simula fue el padre de todo este revuelo, ha sido Smalltalk quién dio el paso definitivo y es éste el que debemos considerar como el primer lenguaje de programación orientado a objetos. Smalltalk fue diseñado (cómo no) en el Palo Alto Research Center (PARC) de Xeros Corporation's, en California. El último gran paso, a nuestro juicio, lo dio Bjarne Stroustrup con la creación del C++, quizás el lenguaje de programación orientado a objetos más usado actualmente. Este, fue definido en 1986 por su autor en un libro llamado The C++ Programming Language, de cita y referencia obligadas cuando se habla de POO. 9 Fig. 1 Evolución de los lenguajes Orientados a objetos Llegados a este punto se hace necesario aclarar que los lenguajes de POO, podemos clasificarlos en puros e híbridos. Diremos que un lenguaje es POO puro, cuando se ajusta completamente a los principios que esta técnica propone y contempla la posibilidad de trabajar exclusivamente con clases. Diremos que un lenguaje es híbrido de POO y algún otro, cuando ese lenguaje, que normalmente existía antes de la aparición de la POO, incorpora en mayor o menor medida facilidades para trabajar con clases. De este modo, C++ es un lenguaje POO híbrido. De hecho, C++ no incorpora todas las características de un lenguaje POO, y no lo hace principalmente, porque es un lenguaje compilado y ello impide que se resuelvan ciertas referencias en tiempo de compilación necesarias para dotar a las clases de algunas de sus cualidades puramente POO (a menos que se le dote de un motor POO interno, el cual tiene en parte). C ha pasado a llamarse C++. Es la mejor implementación POO sobre un lenguaje compilado existente en este momento. Su punto más conflictivo no obstante no su total implementación POO, sino el hecho de permitir herencia múltiple, lo que, según los teóricos, no es muy aconsejable. 10 Pascal ha sido reciclado por Borland, pasando a llamarse Delphi. Tiene una implementación POO bastante buena, pero al no permitir sobrecarga de funciones pierde una de las características de POO: Polimorfismo. Basic ha pasado a llamarse Visual Basic en manos de Microsoft. Este, aun siendo uno de los lenguajes más famosos del momento, no es un lenguaje POO completo, ya que no incluye la característica más importante de la POO: la Herencia. Java es la estrella de los últimos años: es pura POO, impecablemente concebido e implementado por Sun, no dispone de sobrecarga de operadores, cualidad que de los citados aquí sólo dispone C++, No dispone de herencia múltiple como C++. Conceptos básicos.Estudiaremos los conceptos de Clase, Objeto, Herencia, Encapsulación, Polimorfismo, Funciones amigas, Mensaje, Métodos, Datos y Abstracción. Estas son las ideas más básicas que todo aquel que trabaja en POO debe comprender y manejar constantemente; es por lo tanto de suma importancia que los entienda claramente. Definición de Clase.Una clase, es simplemente una abstracción que hacemos de nuestra experiencia sensible. El ser humano tiende a agrupar seres o cosas “objetos” con características similares en grupos “clases”. Así, aun cuando existen por ejemplo multitud de vasos diferentes, podemos reconocer un vaso en cuanto lo vemos, incluso aun cuando ese modelo concreto de vaso no lo hayamos visto nunca. El concepto de vaso es una abstracción de nuestra experiencia sensible. Quizás el ejemplo más claro para exponer esto lo tengamos en las taxonomías; los biólogos han dividido a todo ser (vivo o inerte) sobre la tierra en distintas clases. Tomemos como ejemplo una pequeña porción del inmenso árbol taxonómico: 11 Ellos, llaman a cada una de estas parcelas reino, tipo, clase, especie, orden, familia, género, etc.; sin embargo, nosotros a todas las llamaremos del mismo modo: clase. Así, hablaremos de la clase animal, clase vegetal y clase mineral, o de la clase félidos y de las clases leo (león) y tigris (tigre). Cada clase posee unas cualidades que la diferencian de las otras. Así, por ejemplo, los vegetales se diferencian de los minerales “entre otras muchas cosas” en que los primeros son seres vivos y los minerales no. De los animales se diferencian en que las plantas son capaces de sintetizar clorofila a partir de la luz solar y los animales no. Situémonos en la clase felinos (felis), aquí tenemos varias subclases (géneros en palabras de los biólogos): león, tigre, pantera, gato, etc. cada una de estas subclases, tienen características comunes (por ello los identificamos a todos ellos como felinos) y características diferenciadoras (por ello distinguimos a un león de una pantera), sin embargo, ni el león ni la pantera en abstracto existen, existen leones y panteras particulares, pero hemos realizado una abstracción de esos rasgos comunes a todos los elementos de una clase, para llegar al concepto de león, o de pantera, o de felino. La clase león se diferencia de la clase pantera en el color de la piel, y comparte ciertos atributos con el resto de los felinos “uñas retráctiles por ejemplo” que lo diferencian del resto de los animales. Pero la clase león, también hereda de las clases superiores ciertas cualidades: columna 12 vertebral (de la clase vertebrados) y es alimentado en su infancia por leche materna (de la clase mamíferos). Vemos cómo las clases superiores son más generales que las inferiores y cómo, al ir bajando por este árbol, vamos definiendo cada vez más (dotando de más cualidades) a las nuevas clases. Hay cualidades que ciertas clases comparten con otras, pero no son exactamente iguales en las dos clases. Por ejemplo, la clase hombre, también deriva de la clase vertebrado, por lo que ambos poseen columna vertebral, sin embrago, mientras que en la clase hombre se halla en posición vertical, en la clase león la columna vertebral está en posición horizontal. En POO existe otro concepto muy importante asociado al de clase, el de "clase abstracta". Una clase abstracta es aquella que construimos para derivar de ella otras clases, pero de la que no se puede instanciar. Por ejemplo, la clase mamífero, no existe como tal en la naturaleza, no existe ningún ser que sea tan solo mamífero (no hay ninguna instanciación directa de esa clase), existen humanos, gatos, conejos, etc. Todos ellos son mamíferos, pero no existe un animal que sea solo mamífero. Del mismo modo, la clase que se halla al inicio de la jerarquía de clases, normalmente es creada sólo para que contenga aquellos datos y métodos comunes a todas las clases que de ella derivan: Son clases abstractas. En árboles complejos de jerarquías de clases, suele haber más de una clase abstracta. Este es un concepto muy importante: el de "clase abstracta". Como hemos dicho, una clase abstracta es aquella que construimos para derivar de ella otras clases, pero de la que no se puede instanciar. Por ejemplo, la clase mamífero, no existe como tal en la naturaleza, no existe ningún ser que sea tan solo mamífero (no hay ninguna instanciación directa de esa clase), existen humanos, gatos, conejos, etc. Todos ellos son mamíferos, pero no existe un animal que sea solo mamífero. Todos estos hombres comparten las características de la clase hombre, pero son diferentes entre sí, en estatura, pigmentación de la piel, color de ojos, complexión, etc. A cada uno de los hombres particulares los llamamos "objetos de la clase hombre". Decimos que son objetos de tipo hombre o que pertenecen a la clase hombre. Más técnicamente, Patricia Palacios o Leonardo da Vinci son 13 instanciaciones de la clase hombre; instanciar un objeto de una clase es crear un nuevo elemento de esa clase, cada niño que nace es una nueva instanciación a la clase hombre. Definición de Objeto.En POO, un objeto es un conjunto de datos y métodos Fig. 3 Representación visual de un objeto Los datos son lo que antes hemos llamado características o atributos, los métodos son los comportamientos que pueden realizar. Lo importante de un sistema POO es que ambos, datos y métodos están tan intrínsecamente ligados, que forman una misma unidad conceptual y operacional. En POO, no se pueden desligar los datos de los métodos de un objeto. Así es como ocurre en el mundo real. 14 Ejemplo 1: Tomemos la clase león de la que hablamos antes y veamos cuales serían algunos de sus datos y de sus métodos. Ejemplo 2: Pongamos otro ejemplo algo más próximo a los objetos que se suelen tratar en informática: un recuadro en la pantalla. El recuadro pertenecería a una clase a la llamaremos marco. Veamos sus datos y sus métodos. 15 Ejemplo 3: Vayamos con otro ejemplo más. Este es para aquellos que ya tienen conocimientos de informática, y en especial de lenguaje C o Java. Una clase es un nuevo tipo de dato y un objeto cada una de las asignaciones que hacemos a ese tipo de dato. int nEdad = 30; Hemos creado un objeto que se llama nEdad y pertenece a la clase integer (entero). Para crear un objeto de la clase marco lo haríamos de forma muy parecida: llamando a la función creadora de la clase marco: Marco oCajaMsg := new Marco(); Objeto.- Cuando se ejecuta un programa orientado a objetos, los objetos están recibiendo, interpretando y respondiendo a mensajes de otros objetos, Esto marca una clara diferencia con respecto a los elementos de datos pasivos de los sistemas tradicionales. En la POO un mensaje esta asociado con un método, de tal forma que cuando un objeto recibe un mensaje la respuesta a este mensaje es ejecutar el método asociado Por ejemplo cuando un usuario quiere maximizar una ventana de una aplicación de Windows, lo hace simplemente es pulsar el botón de la misma que realiza esa acción. Esto provoca que Windows envié un mensaje a la ventana para indicar que tiene que maximizarse. Como respuesta a este mensaje se ejecutara el método programado parar ese fin. 16 Herencia.La herencia permite el acceso automático a la información contenida en otra clase. De esta forma la reutilización de código esta garantizada. Con la herencia todas las clases están clasificadas en jerarquías estrictas. Cada clase tiene su superclase (la clase superior en la jerarquía), y cada clase puede tener una o más subclases (las clases inferiores en la jerarquía). Como todos entendemos lo que es la herencia biológica, continuaremos con nuestro ejemplo taxonómico del que hablábamos en el apartado anterior. La clase león, como comentábamos antes, hereda cualidades, métodos, en lenguaje POO de todas las clases predecesoras padres, en POO y posee métodos propios, diferentes a los del resto de las clases. Es decir, las clases van especializándose según se avanza en el árbol taxonómico. Cada vez que creamos una clase heredada de otra (la padre) añadimos métodos a la clase padre o modificamos alguno de los métodos de la clase padre. Veamos qué hereda la clase león de sus clases padre: La clase león hereda todos los métodos de las clases padre y añade métodos nuevos que forman su clase distinguiéndola del resto de las clases: por ejemplo el color de su piel. Observemos ahora algo crucial que ya apuntábamos antes: dos subclases distintas, que derivan de una misma clase padre común, pueden heredar los métodos de la clase padre tal y como estos han sido definidos en ella, o pueden modificar todos o algunos de estos métodos para adaptarlos a sus necesidades. En el ejemplo que exponíamos antes, en la clase león la alimentación es carnívora, mientras que en la clase hombre, se ha modificado éste dato, siendo su alimentación omnívora. 17 La herencia como puede intuir, es la cualidad más importante de la POO, ya que le permite reutilizar todo el código escrito para las superclases re-escribiendo solo aquellas diferencias que existan entre éstas y las subclases. Veamos ahora algunos aspectos más técnicos de la herencia: A la clase heredada se le llama, subclase o clase hija, y a la clase de la que se hereda superclase o clase padre. Al heredar, la clase heredada toma directamente el comportamiento de su superclase, pero puesto que ésta puede derivar de otra, y esta de otra, etc., una clase toma indirectamente el comportamiento de todas las clases de la rama del árbol de la jerarquía de clases a la que pertenece. Se heredan los datos y los métodos, por lo tanto, ambos pueden ser redefinidos en las clases hijas, aunque lo más común es redefinir métodos y no datos. Muchas veces las clases especialmente aquellas que se encuentran próximas a la raíz en el árbol de la jerarquía de clases son abstractas, es decir, sólo existen para proporcionar una base para la creación de clases más específicas, y por lo tanto no puede instanciarse de ellas; son las clases virtuales. Una subclase hereda de su superclase sólo aquellos miembros visibles desde la clase hija y por lo tanto solo puede redefinir estos. Una subclase tiene forzosamente que redefinir aquellos métodos que han sido definidos como abstractos en la clase padre o padres. Normalmente, como hemos comentado, se redefinen los métodos, aun cuando a veces se hace necesario redefinir datos de las clases superiores. Al redefinir un método queremos o bien sustituir el funcionamiento del método de la clase padre o bien ampliarlo. En el primer caso (sustituirlo) no hay ningún problema, ya que a la clase hija la dotamos con un método de igual nombre que el método que queremos redefinir en la clase padre y lo 18 implementamos según las necesidades de la clase hija. De este modo cada vez que se invoque este método de la clase hija se ejecutará su código, y no el código escrito para el método homónimo de la clase padre. Pero si lo que queremos es ampliar el funcionamiento de un método existente en la clase padre (lo que suele ser lo más habitual), entonces primero tiene que ejecutarse el método de la clase padre, y después el de la clase hija. Pero como los dos métodos tienen el mismo nombre, se hace necesario habilitar alguna forma de distinguir cuando nos estamos refiriendo a un método de la clase hija y cuando al del mismo nombre de la clase padre. En Java cada clase solo puede tener una superclase, lo que se denomina “Herencia Simple”. En otros lenguajes orientados a objetos, como C++, las clases pueden tener más una superclase, lo que se conoce como “Herencia múltiple”. En este caso una clase comparte los métodos y propiedades de varias clases, Esta característica proporciona un poder enorme a la hora de crear clases, pero complica excesivamente la programación. La solución a este problema en java son las interfaces. Una interfaz es una colección de nombres de métodos, sin incluir sus definiciones, que pueden ser añadidas a cualquier clase para proporcionar comportamientos adicionales no incluidos en los métodos propios o heredados. Por ejemplo una herencia simple puede ser la clase Profesor como un tipo clase derivado de Persona. La clase profesor es un tipo de (es-un) Persona, al que añade sus propias características. Persona Es un Profesor 19 Por ejemplo la clase Persona utilizando herencia múltiple. Como en ella se ilustra, el GerenteVentas hereda de Gerente y Vendedor; de modo similar, EstudianteTrabajador de Estudiante. Persona Empleado Estudiante Gerente Estudiante Trabajador Gerente de Ventas Abstracción.- Por medio de la abstracción conseguimos no detenernos en los detalles concretos de las cosas que no interesen en cada momento, sino generalizar y centrarse en los aspectos que permiten tener una visión global del problema. Objeto Real Objeto Abstracto Encapsulación.- Esta característica permite ver un objeto como una caja negra en la que se ha introducido de alguna manera toda la información relacionada con dicho objeto. Esto nos permite manipular los objetos como unidades básicas, permitiendo ocultar su estructura interna. 20 La abstracción y la Encapsulación están representadas por clases. La clase es una abstracción, porque en ella se define las propiedades o atributos de determinados conjuntos de objetos con características comunes, y es una Encapsulación porque constituye una caja negra que encierra tanto los datos que almacenan cada objeto como los métodos que permiten manipularlos. Polimorfismo.- Esta característica permite implementar múltiples formas de un mismo método, dependiendo cada una de ellas de la clase sobre las que se realice la implementación. Esto hace que se pueda acceder a una variedad de métodos distintos (todos con el mismo nombre) utilizando exactamente el mismo medio de acceso. 21 TEMA II PRINCIPIOS DE PROGRAMACIÓN ORIENTADA A OBJETOS CON C++ Y JAVA Introducción.Vivimos en un mundo de objetos, Cada objeto tiene atributos operaciones. Algunos objetos están más animados que otros. Se puede agrupar los objetos en clases. Ejemplo, una naranja es un objeto que pertenece a la clase frutas. La POO usa la notación de objeto del mundo real para desarrollar aplicaciones. La Clase define una categoría de objetos. Cada objeto es una instancia de una clase. Historia y características de C++.- El lenguaje C++ se comenzó a desarrollar en 1980. Su autor fue Bjarne. Stroustrup, también de la ATT. Al comienzo era una extensión del lenguaje C que fue denominada C with classes. Este nuevo lenguaje comenzó a ser utilizado fuera de la ATT en 1983. El nombre C++ es también de ese año, y hace referencia al carácter del operador incremento de C (++). Ante la gran difusión y éxito que iba obteniendo en el mundo de los programadores, la ATT comenzó a estandarizarlo internamente en 1987. En 1989 se formó un comité ANSI (seguido algún tiempo después por un comité ISO) para estandarizarlo a nivel americano e internacional. En la actualidad, el C++ es un lenguaje versátil, potente y general. Su éxito entre los programadores profesionales le ha llevado a ocupar el primer puesto como herramienta de desarrollo de aplicaciones. El C++ mantiene las ventajas del C en cuanto a riqueza de operadores y expresiones, flexibilidad, concisión y eficiencia. Además, ha eliminado algunas de las dificultades y limitaciones del C original. La evolución de C++ ha continuado con la aparición de Java, un lenguaje creado simplificando algunas cosas de C++ y añadiendo otras, que se utiliza para realizar aplicaciones en Internet. El C++ es a la vez un lenguaje procedural (orientado a algoritmos) y orientado a objetos. Como lenguaje procedural se asemeja al C y es compatible con él, aunque ya se ha dicho que presenta ciertas ventajas (las modificaciones menores, que se verán a continuación). Como lenguaje orientado a objetos se basa en una filosofía completamente diferente, que exige del programador un completo cambio de mentalidad. Las características propias de la Programación 22 Orientada a Objetos de C++ son modificaciones mayores que sí que cambian radicalmente su naturaleza. Características del C++. Modificaciones Menores.- Como ya se ha dicho, el C++ contiene varias modificaciones menores sobre el C original. Normalmente se trata de aumentar la capacidad del lenguaje y la facilidad de programación en un conjunto de detalles concretos basados en la experiencia de muchos años. Como el ANSI C es posterior a los primeros compiladores de C++, algunas de estas modificaciones están ya introducidas en el ANSI C. En cualquier caso, se trata de modificaciones que facilitan el uso del lenguaje, pero que no cambian su naturaleza. Hay que indicar que el C++ mantiene compatibilidad casi completa con C, de forma que el viejo estilo de hacer las cosas en C es también permitido en C++, aunque éste disponga de una mejor forma de realizar esas tareas. Cambio en la extensión del nombre de los ficheros.- El primer cambio que tiene que conocer cualquier programador es que los ficheros fuente de C++ tienen la extensión *.cpp (de C plus plus, que es la forma oral de llamar al lenguaje en inglés), en lugar de *.c. Esta distinción es muy importante, pues determina ni más ni menos el que se utilice el compilador de C o el de C++. La utilización de nombres incorrectos en los ficheros puede dar lugar a errores durante el proceso de compilación. Comentarios introducidos en el programa.- En C los comentarios empiezan por los caracteres /* y terminan con los caracteres */. Pueden comprender varias líneas y estar distribuidos de cualquier forma, pero todo aquello que está entre el /* (inicio del comentario) y el */ (fin del comentario) es simplemente ignorado por el compilador. Algunos ejemplos de formato de comentarios son los siguientes: 23 /* Esto es un comentario simple. */ /* Esto es un comentario más largo, distribuido en varias líneas. El texto se suele alinear por la izquierda. */ /************************************** * Esto es un comentario de varias * * líneas, encerrado en una caja para * * llamar la atención. * **************************************/ En C++ se admite el mismo tipo de comentarios que en C, pero además se considera que son comentarios todo aquel texto que está desde dos barras consecutivas (//) hasta el fin de la línea. Las dos barras marcan el comienzo del comentario y el fin de la línea, el final. Si se desea poner comentarios de varias líneas, hay que colocar la doble barra al comienzo de cada línea. Los ejemplos anteriores se podrían escribir del siguiente modo: // Esto es un comentario simple. // Esto es un comentario más largo, // distribuido en varias líneas. El // texto se suele alinear por la izquierda. //************************************* // Esto es un comentario de varias * // líneas, encerrado en una caja para * // llamar la atención. * //************************************* La ventaja de este nuevo método es que no se pueden comentar inadvertidamente varias líneas de un programa abriendo un indicador de comentario que no se cierre en el lugar adecuado. Abstracción de datos.La abstracción es el proceso en el cual separamos las propiedades más importantes de un objeto, de las que no lo son. Es decir, por medio de la abstracción definimos las características esenciales de un objeto del mundo real, los atributos y comportamientos que lo definen como tal, para después modelarlo en un objeto de software. 24 En el proceso de abstracción no debemos preocuparnos por la implementación de cada método o atributo, solamente debemos definirlo de forma general. Por ejemplo, supongamos que deseamos escribir un programa para representar el sistema solar, por medio de la abstracción, podemos ver a éste sistema como un conjunto de objetos, algunos de estos objetos son los planetas, que tienen un estado, dado por sus características físicas, digamos, tamaño, masa, entre otras, estos objetos tendrán también comportamientos: la translación y la rotación, pueden mencionarse como los más evidentes. Visualizar las entidades que deseamos trasladar a nuestros programas, en términos abstractos, resulta de gran utilidad para un diseño optimo de nuestro software, ya que nos permite comprender más fácilmente la programación requerida. En la tecnología orientada a objetos la herramienta principal para soportar la abstracción es la clase. Podemos definir a una clase como una descripción genérica de un grupo de objetos que comparten características comunes, dichas características se especificadas en sus atributos y comportamientos. En otras palabras, una clase es un molde o modelo en donde se especifican las características que definen a un objeto de manera general, a partir de una clase pedemos definir objetos particulares. En programación orientada a objetos se dice que un objeto es una instancia de la clase, es decir un objeto, es un caso particular del conjunto de objetos que comparten características similares, definidas en la clase. Un ejemplo servirá para clarificar estos conceptos. Definamos a la clase persona, esta clase tendrá ciertos atributos, por ejemplo edad, sexo y nombre entre otros. Las acciones o comportamientos que podemos definir para esta clase son, por ejemplo, respirar, caminar, comer, etcétera. Estas son, para nuestro caso, las características que definen a nuestra clase persona. Un objeto instanciado de está clase podría ser Patricia Palacios, de sexo Femenino y de 32 años de edad, quién se encuentra caminando. Estos atributos son conocidos formalmente en programación orientada a objetos como variables de instancia por que contienen el estado de un objeto particular en un momento dado, en este caso Patricia Palacios. Así mismo, los comportamientos son llamados métodos de instancia porque nos muestran el comportamiento de un objeto particular de la clase. 25 Hay que tener cuidado de no confundir un objeto con una clase, una gelatina de fresa y una gelatina de limón son objetos de la misma clase (o del mismo molde), pero con atributos (o sabores) diferentes. Clases y Objetos en C++.Una clase equivale a la generalización o abstracción de un tipo específico de objetos. Los polígonos con tres lados iguales podrían ser generalizados, por ejemplo, en una clase que llamaremos "trianguloEquilatero". Hablar de clase es, así, hablar de una determinada clase de objetos. En C++ una clase ("class", según la terminología del lenguaje) es un tipo definido por el usuario. De esta forma, y siguiendo con el ejemplo cinematográfico, el objeto abstracto "Sala de Cine" sería implementado como la clase "SalaDeCine" en C++, habiendo definido así un nuevo tipo de dato abstracto, que será manejado por el compilador de parecida forma a como lo hace con los tipos predefinidos (int, char, float, etc.). Podremos, así, como ya veremos, declarar un determinado objeto del tipo (o de la clase) "SalaDeCine". Un objeto es una encapsulación abstracta de información, junto con los métodos o procedimientos para manipularla. "un objeto contiene operaciones que definen su comportamiento y variables que definen su estado entre las llamadas a las operaciones". Bueno, pensemos en un ejemplo asequible: una sala de cine. Imaginemos un objeto del tipo "sala de cine" donde los datos, variables o información estarían constituidos por los espectadores; imaginemos también (y esto es importante) que los espectadores no pudieran moverse por sí solos dentro de la sala (como si estuvieran catatónicos). ¿Quiénes serían los manipuladores, métodos, procedimientos u operaciones encargados de manipular a los espectadores? Indudablemente los acomodadores de la sala, aunque también el personal directivo de la misma. Envió de mensajes.- Un mensaje representa una acción a tomar por un determinado objeto. En el ejemplo anterior, la orden "que comience la proyección de la película" podría ser un mensaje dirigido a un objeto del tipo "sala de cine". Otro mensaje podría ser la orden de "desalojo de la sala" en funciones nocontinuas. Notemos que el mensaje se refiere únicamente a la orden, y no tiene que ver con la 26 forma como ésta es respondida. En C++ un mensaje equivale al PROTOTIPO de una función miembro en la descripción de una clase. O sea, si pensamos en una posible función "especialmente restringida a un objeto" (que en C++ se denomina función miembro) tal como "empezarProyeccion", el mensaje estaría representado por el prototipo de tal función, y no por su definición o implementación específica. El "envío de un mensaje" a un objeto equivale, en C++, como ya he indicado, a una llamada a una función miembro de la clase correspondiente al objeto. En realidad las expresiones "métodos", "envío de mensajes", "instancias de variables", etc. pertenecen originariamente al lenguaje Smalltalk. Volvamos al ejemplo del cine y "lancemos mensajes": SalaDeCine cineParadox; SalaDeCine *punteroASalaDeCine; Hemos declarado por un lado un objeto denominado cineParadox, del tipo definido-por-el-usuario SalaDeCine. Seguidamente hemos declarado un puntero al mismo tipo de dato abstracto. El mensaje "que comience la proyección de la película", dirigido a la sala "Cine Paradox", podría ser codificado así: cineParadox.empezarProyeccion (); Esto es, el mensaje o llamada de función se ha dirigido directamente al objeto. Pero veamos el siguiente código: punteroASalaDeCine = new SalaDeCine( cinePalafox ); punteroASalaDeCine->empezarProyeccion; Vemos, pues, que la notación '.' sirve para enviar mensajes directamente a un objeto, mientras que '->' se usa para el envío de mensajes a los objetos a través de punteros que apunten a los mismos. Cabría notar aquí, redundando en lo apuntado anteriormente, que el prototipo de función empezarProyeccion () corresponde al mensaje, mientras que el método estaría constituido por la implementación de la definición de la función miembro empezarProyeccion (). El mensaje 27 corresponde al "qué", mientras que el método corresponde al "cómo". Así, el método podría definirse -en la clase SalaDeCine- de la forma: void SalaDeCine::empezarProyeccion() { // apaga música y luces y comienza proyección ponerMusicaDeFondo( OFF ); ponerLucesSala( OFF ); marchaProyector( ON ); } En este caso el mensaje empezarProyeccion podría ser enviado al objeto cineParadox por un objeto cronómetro con un horario determinado. Pudiera parecer, por otra parte, que el método o función miembro empezarProyeccion está compuesto por funciones del tipo usado en programación estructurada, pero en realidad se trata de mensajes dirigidos al mismo objeto (en este caso concreto, cineParadox), dado que, por ejemplo, el código marchaProyector( ON ); Equivale a: this->marchaProyector( ON ); En donde this es un puntero implícito al objeto Encapsulamiento.- ¿Qué es, por otro lado, la encapsulación? Lo que de inmediato nos sugiere el vocablo es la acción de encapsular, de encerrar algo en una cápsula, como los polvitos medicinales que nos venden en las farmacias. Podemos decir que la encapsulación se refiere a la capacidad de agrupar y condensar en un entorno con límites bien-definidos distintos elementos. El fenómeno de la encapsulación podría darse, por ejemplo, en el conjunto de herramientas y elementos heterogéneos que integran es un decir la caja de herramientas de un fontanero: la caja es la cápsula y todo lo que haya dentro es lo que hemos encapsulado. Pero no nos perdamos: la 28 cualidad de "encapsulación" la aplicaremos únicamente a abstracciones: o sea, afirmar que una caja de herramientas concreta encapsula un bocadillo, un martillo y un limón constituye una cierta trasgresión léxica. Cuando hablemos de encapsulación en general siempre nos referiremos, pues, a encapsulación abstracta. Las dos propiedades expuestas están, como vemos y en lo que a la POO concierne, fuertemente ligadas. Dicho de manera informal, primero generalizamos (la abstracción) y luego decimos: la generalización está bien, pero dentro de un cierto orden: hay que poner límites (la encapsulación), y dentro de esos límites vamos a meter, a saco, todo lo relacionado con lo abstraído: no sólo datos, sino también métodos, comportamientos, etc. Pero, bueno ¿y esto es POO? Pues sí, esta es una característica fundamental de POO. En lo que a C++ se refiere, la unidad de abstracción y encapsulación está representada por la Clase, (class). Por un lado es una abstracción pues, de acuerdo con la definición establecida anteriormente, es en ésta donde se definen las propiedades y atributos genéricos de determinados objetos con características comunes (recordemos el ejemplo de la sala de cine). La Clase es, por otro lado, una encapsulación porque constituye una cápsula o saco que encierra y amalgama de forma clara tanto los datos de que constan los objetos como los procedimientos que permiten manipularlos. Las Clases se constituyen, así, en abstracciones encapsuladas. Abordemos ahora el tema en su parte cruda y echémosle un vistazo a parte del código de lo que podría ser una descripción de una clase que denominaremos Racional: Class Racional { friend Racional& operator+( Racional, Racional ); // ... private: int numerador; int denominador; // ... void simplificaFraccion(); // ... public: // ... void estableceNumerador( int ); void estableceDenominador( int ); // ... 29 }; Observemos que en la clase Racional, abstracción del conjunto de los números racionales, de su representación formal (x/y: equis dividido por y) y operaciones (a/b + c/d), aparecen encapsulados tanto los datos internos (numerador, denominador) como las funciones miembro para el tratamiento de éstos (el operador suma de quebrados, el procedimiento para cambiar el denominador, el método para simplificar fracciones, etc.). Bueno, la verdad es que en el código anterior observamos muchas más cosas: el símbolo de comentario //, que anula lo escrito (a efectos de compilación) desde su aparición hasta el fin de la línea; las etiquetas private y public, que determinan cómo y quién accederá a los datos y funciones de la clase; la palabra clave friend, que indica la calificación como "amiga de la clase Racional" de una función que suma Racionales (¿Racionales? ¿alguna forma de typedef?) y a la que se dará el tratamiento de "operador suma" (operator +), devolviendo una "referencia" (&) a Racional (¿referencia a Racional? ¿alguna forma de puntero?). Vale, vale: son muchas cosas en las que todavía no podemos entrar y que en su momento veremos en detalle. Bástenos saber, por ahora, que tal código intenta describir el comportamiento y la esencia de un tipo de datos bien conocido por todos (el conjunto de los números racionales, de la forma x/y, donde x e y son números enteros) pero no incluido como tipo predefinido2 por la mayoría de compiladores. Esta carencia es la que intentamos suplir con el código expuesto: una vez descrita la clase (class), se habrá traspasado al compilador el conocimiento de, para él, un nuevo tipo de número: el "Racional". Retengamos de momento únicamente esta última idea pensando, como único alivio, que cuanto más veamos una "rareza", antes dejara ésta de serlo. Salvado el inciso, quizá puedan apreciarse mejor las características de abstracción y encapsulación notadas en la siguiente porción de la definición de la clase SalaDeCine, tomada del ejemplo "cinematográfico": Class SalaDeCine { private: int aforoSala; char *nombreSala; // ... public: void alarmaEnCasoDeIncendio(); 30 void empezarSesion(); // ... }; La clase SalaDeCine aglutina, según lo expuesto, tanto los datos correspondientes a las salas de cine en general (capacidad de la sala, nombre de la misma, etc.) como los métodos de respuesta a los mensajes dirigidos a los objetos "salas de cine" (alarma por incendio, etc.). O sea, la clase SalaDeCine es la abstracción resultante de la asimilación intelectual de todas las posibles salas de cine, vistas o soñadas: aquí tenemos la cualidad de abstracción. La misma clase encapsula, por otro lado, datos y funciones dentro de un límite bien definido: ¿cuál? ¿Las paredes del edificio? ¡No, vaya! El límite es el mismo que el que separa una sala de cine de un almacén de frutas: su propia definición, que en este caso coincide con el bloque de la clase. La cápsula esta formada por los brazos {} que encierran el cuerpo de la clase. Conviene notar que los tipos de datos incorporados al compilador (int, char, long, etc.) son realmente abstracciones encapsuladas de datos y métodos. Consideremos el siguiente código: float resta, minuendo, sustraendo; resta = minuendo - sustraendo; El operador (-) representa aquí la operación de substracción entre dos variables del tipo predefinido 'float'. Pero tal operador es, realmente, un mensaje que el objeto sustraendo (un número float) le envía al objeto minuendo (otro número float) , y el método de respuesta a tal mensaje está implementado en la encapsulación de tipo float predefinida en el compilador, que resta del minuendo el sustraendo y devuelve un valor de tipo float. Tal método es formalmente diferente del que podría ser implementado para la resta de dos variables de tipo int (no hay parte decimal, etc.), o aún de un tipo definido por el usuario (user-defined). Aclaremos esto: el compilador sabe que una operación sobre floats no tiene por Constructores Y Destructores.- Un constructor es un procedimiento especial de una clase que es llamado automáticamente siempre que se crea un objeto de esa clase. Su función es iniciar el objeto. 31 Un destructor es un procedimiento especial de una clase que es llamado Automáticamente siempre que se destruye un objeto de esa clase. Su función es realizar cualquier tarea final en el momento de destruir el objeto. Historia del java.- A pesar de lo que pueda se pueda en un principio pensar, Java no surgió inicialmente como un lenguaje de programación orientado a la web. Los orígenes se remontan al año 1991 cuando Mosaic (uno de los primeros browsers) o la World Wide Web no eran más que meras ideas interesantes. Los ingenieros de Sun Microsystems estaban desarrollando un lenguaje capaz de ejecutarse sobre productos electrónicos de consumo tales como electrodomésticos. Simultáneamente James Gosling, el que podría considerarse el padre de Java, estaba trabajando en el desarrollo de una plataforma software barata e independiente del hardware mediante C++. Por una serie de razones técnicas se decidió crear un nuevo lenguaje, al que se llamó Oak, que debía superar algunas de las deficiencias de C++ tales como problemas relacionados con la herencia múltiple, la conversión automática de tipos, el uso de punteros y la gestión de memoria El lenguaje Oak se utilizó en ciertos prototipos de electrónica de consumo pero en un principio no tuvo el éxito esperado dado que la tecnología quizás era demasiada adelantada a su tiempo. No obstante lo positivo de estos primeros intentos fue que se desarrollaron algunos de los elementos precursores de los actuales componentes Java; componentes tales como el sistema de tiempo de ejecución y la API (Application Program Interface “Interfaz para la Programación de Aplicaciones”). En 1994 eclosionó el fenómeno web y Oak fue rebautizado como Java. En un momento de inspiración, sus creadores decidieron utilizar el lenguaje para desarrollar un browser al que se llamó WebRunner, que fue ensayado con éxito, arrancando en ese momento el proyecto Java/HotJava. HotJava fue un browser totalmente programado en Java y capaz así mismo de ejecutar código Java. 32 A lo largo de 1995 tanto Java, su documentación y su código fuente como HotJava pudieron obtenerse para múltiples plataformas al tiempo que se introducía soporte para Java en la versión 2.0 del navegador Netscape. La versión beta 1 de Java despertó un inusitado interés y se empezó a trabajar para que Java fuera portable a todos los sistemas operativos existentes. En diciembre de 1995 cuando se dio a conocer la versión beta 2 de Java y Microsoft e IBM dieron a conocer su intención de solicitar licencia para aplicar la tecnología Java, su éxito fue ya inevitable. El 23 de enero 1996 se publicó oficialmente la versión Java 1.0 que ya se podía obtener descargándola de la web. A principios de 1997 aparece la versión 1.1 mejorando mucho la primera versión. Java 1.2 (Java 2) apareció a finales de 1998 incorporando nuevos elementos. Según Sun esta era la primera versión realmente profesional. En mayo del 2000 se lanza la versión 1.3 del J2SE (Java 2 Standar Edition) y hace unos meses, en febrero de este año, se lanzó la versión 1.4 (la versión 1.4.1 es ya una beta). ¿Qué es Java? 33 Java no es sólo un lenguaje de programación, Java es además un sistema de tiempo de ejecución, un juego de herramientas de desarrollo y una interfaz de programación de aplicaciones (API). Todos estos elementos así como las relaciones establecidas entre ellos se esquematizan en la siguiente figura. Fig. 2 Las interioridades de Java El desarrollador de software escribe programas en el lenguaje Java que emplean paquetes de software predefinidos en la API. Luego compila sus pr ogramas mediante el compilador Java y el resultado de todo ello es lo que se denomina bytecode compilado. Este bytecode es un fichero independiente de la plataforma que puede ser ejecutado por máquina virtual Java. La máquina 34 virtual puede considerarse como un microprocesador que se apoya encima de la arquitectura concreta en la que se ejecuta, interactuando con el sistema operativo y el hardware, la máquina virtual es por tanto dependiente de la plataforma del host pero no así el bytecode. Necesitaremos ta ntas máquinas virtuales como plataformas posibles pero el mismo bytecode podrá ejecutarse sin modificación alguna sobre todas ellas. Así pues, las claves de la portabilidad de Java son su sistema de tiempo de ejecución y su API. Los potentes recursos para el trabajo de ventanas y redes incluidos en la API de Java facilitan a los programadores el desarrollo de un software que resulte a la vez atractivo e independiente de la plataforma. Ejemplo.- 1) Ingrese dos Números enteros y determine cual es el mayor de ellos.(Completar) import java.io.*; public class Ejemplo1 { public static void main(String args[]) throws Exception { int a,b ,mayor; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); { System.out.println("Ingrese el primer numero...: " ); a= Integer.parseInt(br.readLine()); System.out.println("Ingrese el segundo numero...: " ); b= Integer.parseInt(br.readLine()); if(a > b) { mayor = a; }else{ mayor = b; } } } } 35 EJERCICIOS.- 1) Ingrese 3 números enteros positivos y determine cuál de ellos es el mayor y menor. 2) Genere la siguiente serie: X = 1/1 - 1/22 + 1/ 32 - 1/42 + - ….. 3) Dado un numero entero psitivo escribir en el orden inverso. 4302 36 ----------------> 2034 TEMA III SOBRECARGA Introducción.La sobrecarga es una propiedad que describe una característica adecuada que utiliza el mismo nombre de operación para representar operaciones similares que se comporta de modo diferente cuando se aplican a las clases diferentes. Por consiguiente, los nombres de las operaciones se pueden sobrecargar, esto es, las operaciones se definen en clase diferente y pueden tener nombres idénticos, aunque su código programa puede diferir. Si los nombres de una operación se utilizan para nuevas definiciones en clases de una jerarquía la operación a nivel inferior se dice que anula la operación a un nivel más alto. Actualmente la sobrecarga solo se aplica a operaciones. Aunque es posible extender la propiedad a atributos y relaciones especificas del modelo propuesto. Los lenguajes de programación convencionales soportan sobrecarga para algunas de las operaciones sobre algunos tipos de datos, como enteros, reales y caracteres. Los sistemas orientados a objetos dan un poco más en la sobrecarga y la hacen disponible para operaciones sobre cualquier tipo de objeto. Sobrecarga de funciones en C++ y java. Sobrecarga en c++: C++ permiten que sean definidas varias funciones del mismo nombre, siempre que estos nombres de funciones indiquen diferentes conjuntos de parámetros (por lo menos en lo que se refiere a sus tipos). Esta capacidad se llama homonimia de funciones. Cuando se llama a una función homónima, el compilador C++ selecciona de forma automática la función correcta examinando el número, tipos y orden de los argumentos de la llama. La homonimia de la función de utiliza para crear varias funciones del mismo nombre, que ejecutan tares similares, sobre tipos de datos diferentes. 37 #include<iostream.h> // existen dos metodos con el nombre (en ese caso se realiza la sobrecarga) y nos devuelven //dos resultados con dos tipos diferente (entero y Double). int cuadrado(int x){return x * x;} double cuadrado (double y) {return y * y;} main() { cout << "El cuadrado de 7 es " << cuadrado(7) << "\n el cuadrado de double 7.5 es " << cuadrado(7.5)<< "\n"; cin.get(); return 0; } Sobrecarga en Java: Para empezar, una breve reseña acerca de la sobrecarga de métodos. Decimos que un método ha sido “sobrecargado” cuando han sido creados múltiples métodos con el mismo nombre, pero especificando distintos argumentos (en número o tipo). Como ejemplo veamos este trozo de código: class Dog { public static void main(String[] args) { bark(); // Imprime sin parametros bark(100); // Imprime con parametros } public static void bark() { System.out.println("no parametros"); } public static void bark(int decibels) { System.out.println("existen parametros"); } } 38 En este caso, se ejecuta un método u otro dependiendo de si se especifica un parámetro en la llamado o no. Supongamos ahora una situación como la siguiente: class Animal {} class Dog extends Animal {} class TestOverload { public static void main (String[] args) { Animal a1 = new Animal(); Animal a2 = new Dog(); Dog d = new Dog(); makeSound(a1); makeSound(a2); makeSound(d); } public static void makeSound(Animal a) { System.out.println("Realiza sonidos de animal"); } public static void makeSound(Dog d) { System.out.println("Realiza sinidos de perro"); } } La razón es que Java determina qué versión del método sobrecargado va a ser llamada en tiempo de compilación. Así pues, aunque a referencia a2 apunta a una instancia del objeto Dog, el compilador utiliza una referencia de tipo Animal para determinar cual de los métodos será llamado. A continuación un ejemplo de lo peligroso que puede llegar a resultar esto. Un ejemplo en que la salida del programa no permite distinguir cual de los métodos está siendo ejecutado: class Animal { public void doMakeNoise() { System.out.println("Hacer como animal"); 39 } } class Dog extends Animal {} class TestOverload { public static void main (String[] args) { Animal a1 = new Animal(); Animal a2 = new Dog(); Dog d = new Dog(); makeSound(a1); makeSound(a2); makeSound(d); } public static void makeSound(Animal a) { a.doMakeNoise(); } public static void makeSound(Dog d) { d.doMakeNoise(); } } Esto se debe al polimorfismo y la llamada a doMakeNoise añadida en cada uno de los métodos makeSound. Evidentemente, esto no es un problema si la funcionalidad final es la misma (pero en este caso se necesitaría una reorganización de código). Lo importante, en cualquier caso, es tener en mente que Java determina el método sobrecargado a ejecutar en tiempo de compilación. Sobrecarga de operadores. Sobrecarga de operadores en C++: La sobrecarga de operadores es utilizada en la Programación Orientada a Objetos, esta hace posible manipular objetos de clases con operadores estándar +, *, [ ], y <<. Los tipos de operadores a tratar serán los binarios y unitarios. Para sobrecargar un operador, y aplicarlo en operaciones entre clases, se usa la función especial “operator” seguido del operador que se sobrecargara, la sintaxis es la siguiente: 40 tipo nombre_clase::operator # (lista de parámetros); Donde: tipo: es el tipo de retorno de la operación de sobrecarga, por lo general es la referencia del objeto. nombre_clase: es el nombre de clase en la que estamos aplicando sobrecarga. operator: función especial para sobrecarga. operador que estamos sobrecargando, debe ser uno de los que esta en la tabla mas abajo. Lista de parámetros: es la lista de objetos (en referencia) o variables que se procesaran en la sobrecarga, si la operación es unaria no tendrá parámetros, si la Operación es binaria tendrá un parámetro. Como ejemplo se muestra la sobrecarga del operador suma: numero& numero::operator + (numero &p){ numero *salida = new numero; //creando objeto de retorno salida->n = this->n + p.n; //operacion suma return *salida; //retorno } Operadores que se pueden sobrecargar Operadores Unitarios son: New delete new [ ] delete ++ Incremento. -- Decremento. () Llamada a función [ ] Indexación 41 Ejemplo.En este ejemplo se muestra la sobrecarga de operadores binarios (suma: +), operadores unarios (incremento: ++), y la interacción con variables normales (igualación: =). Sobre la clase numero: #include <iostream> //using namespace std; class numero { private: int n; public: numero() : n(0) {} //constructor sin parametros numero(int a) { this->n = a; } //constructor con parametros int ver() { return n; } //muestra valor en n numero& operator + (numero &p); //declaracion sobrecarga operador binario suma numero& operator =(int a); //declaracion sobrecarga operador igualacion numero& operator ++(); //declaracion sobrecarga operador unario incremento }; //definicion de la sobrecarga del operador de suma numero& numero::operator + (numero &p){ numero *salida = new numero; salida->n = this->n + p.n; return *salida; } //cuerpo de la sobrecarga del operador de valoración numero& numero::operator =(int a){ this->n=a; return *this; } //definicion de la sobrecarga del operador de incremento numero& numero::operator ++ (){ this->n ++; return *this; } // prueba para la clase numero int main(){ 42 numero x(4), y(2), z; //instancias x, y, z de objeto numero z = x + y; //implementando operador binario suma cout << "\n suma:\n \t x(" << x.ver()<<")+y("<<y.ver()<<") = z("<<z.ver()<<")"<<endl; z=2; //implementando operador de igualacion con un entero cout << "\n igualacion \"z=\" \n \t z="<<z.ver()<<endl; ++z; //implementando operador unario de incremento cout << "\n incremento \"++\" \n \t ++z="<<z.ver()<<endl; cin.get(); return 0; } Sobrecarga de operadores en java: En java sólo existe la sobrecarga de funciones (métodos); Los operadores que existen ya vienen sobrecargados por el compilador, (por ejemplo la “+” para sumar números o concatenar cadenas). public class sobrecarga { public static void main(String args[]) { int a , b; String c , d; a = 2; b = 3; c = "Hola "; d ="Mundo"; /* * El operador + es un perador sobrecargado para sumar numeros y concatenar cadenas */ System.out.println(a + b); System.out.println( c + d ); } } Ejemplos.class Numero{ friend ostream& operator<<(ostream&salida , const Numero&a); friend istream& operator>>(istream&salida, Numero &a); friend Numero operator+(const Numero &a,const Numero &b); friend Numero operator-(const Numero &a,const Numero &b); friend Numero operator/(const Numero &a,const Numero &b); friend Numero operator*(const Numero &a,const Numero &b); public: Numero(double x = 0); 43 void establecer(double x); private: double x;}; //Constructor Numero::Numero(double b) { establecer( b ); } void Numero::establecer(double a) { x = a; } //Sobrecarga de operadores, mediante funciones friend Numero operator+(const Numero &a ,const Numero &b) { Numero temporal; temporal.x = a.x + b.x; return temporal; } Numero operator-(const Numero &a ,const Numero &b) { Numero temporal; temporal.x = a.x - b.x; return temporal; } Numero operator/(const Numero &a ,const Numero &b) { if (b.x == 0){ cout << "error de division por 0. Detiene ejecucion." << endl; // exit(1); } Numero temporal; temporal.x = a.x / b.x; return temporal; } Numero operator*(const Numero &a ,const Numero &b) { Numero temporal; temporal.x = a.x * b.x; return temporal; } ostream &operator<<(ostream& salida , const Numero& a) { 44 salida << a.x ; return salida; } istream &operator>>(istream& entrada, Numero & a) { entrada >> a.x; return entrada; } //Probamos la clase.int main() { cout << "Tres numeros porfa: " << endl; Numero A; cin >> A; Numero B; cin >> B; Numero C; cin >> C; cout << "A = " << A << ", B = " << B << ", C = " << C << endl; Numero resultado; resultado = A - B + C; cout << "Resultado = A - B + C = " << resultado << endl; resultado = A / B ; cout << "Resultado = A / B = " << resultado << endl; resultado = A * resultado; cout << "Resultado = A * resultado = " << resultado << endl; resultado = B * resultado / A; cout << "Resultado = B * resultado / A = " << resultado << endl; cin.get(); return 0; } 45 TEMA IV HERENCIA Introducción.La herencia permite el acceso automático a la información contenida en otra clase. De esta forma la reutilización del código está garantizada. Con la herencia todas las clases están clasificadas en una jerarquía estricta. Cada clase tiene su superclase (la clase superior en la jerarquía). Herencia.La herencia supone una clase base y una jerarquía de clase que contienen las clase derivadas de la clase base. Las clases derivadas pueden heredar el código y os datos de su clase base, añadiendo su propio código especial y datos a ellas , incluso cambiar a aquellos elementos de la clase base que necesitan ser diferentes. Una clase hereda característica (datos y funciones) de otra clase. Herencia simple.- Es cuando una clase derivada hereda de una única clase, es decir una clase derivada sólo tiene un padre o ascendiente. Por su parte una clase base puede tener tantos descendientes como sean necesarios sin limitación alguna. Es un sistema jerárquico en forma arborescente, similar a la estructura de directorios de algunos sistemas operativos. La forma general de la herencia en C++ es: class <nombre_clase_derivada>: [<acceso>] <nombre_clase_heredada> { 46 // Cuerpo de la declaración de la clase }; El nombre_clase_heredada se refiere a una clase base declarada previamente. Ésta pude estar ya compilada, o puede que se declare en el mismo programa que la derivada, en este segundo caso se debe declarar la clase base antes de la derivada, o al menos declarar el nombre de la clase base como una referencia anticipada. El acceso puede ser private, protected o public. Si se omite se supone que el acceso es private5, de forma que si se quiere dar un acceso public o protected se debe hacer explícitamente. Los elementos private de la clase base son inaccesibles para la clase derivada, sea cual se el acceso. Si el acceso es public, todos los elementos public y protected de la clase base seguirán siendo public y protected respectivamente en la clase derivada. Si el acceso es private, entonces todos los elementos public y protected de la clase base pasarán a ser elementos private de la clase derivada. Si el acceso es protected todos los miembros public y protected de la clase base se convierten en elementos protected de la clase derivada. En la siguiente tabla se resumen los especificadores de acceso: Se puede añadir a la clase derivada datos y funciones miembro. Dentro de las funciones miembro de la clase derivada se puede llamar a las funciones miembro y manejar los datos miembro que estén en la sección pública y protegida de la clase base. En una clase derivada se heredan todos los datos miembro de la clase base excepto los estáticos. Algunas funciones miembro no se 47 heredan de forma automática. Éstas son los constructores, el destructor, las funciones amigas, las funciones estáticas de la clase, y el operador de asignación sobrecargado. Se va a ilustrar lo que se ha dicho hasta el momento de la herencia en los siguientes ejemplos: // Programa: Herencia Simple // Fichero: HER_SIM1.CPP #include <iostream.h> // Se define una clase sencilla class Primera { protected: int i, j; public: Primera() {i=j=0;} Primera(int a, int b) {i=a; j=b;} void Entra_ij(int a, int b) {i=a; j=b;} void Muestra_ij (void) {cout << "\n" << i << " " << j;} }; // Se tiene otra clase, Segunda, que es derivada de la clase Primera // Las variables i, j de Primera pasan a ser miembros protected de // Segunda, ya que el acceso es public class Segunda: public Primera { int k; public: void Entra_k(int a) {k=a;} void Muestra_k (void) {cout << "\n" << k;} }; // Se tiene otra clase, Tercera, que es derivada de la clase Segunda // Las variables i, j de Primera pasan a ser miembros protected de // Tercera, ya que el acceso es public. Sin embargo no tiene acceso a k de // Segunda, ya que esta variable es private. class Tercera: public Segunda { public: void f (void) { i=j=2;} }; void main (void) { Primera P(1,2); Segunda S; Tercera T; S.Entra_ij(3,4); 48 S.Entra_k(5); P.Muestra_ij(); // Saca 1 2 S.Muestra_ij(); // Saca 3 4 S.Muestra_k(); // Saca 5 T.f(); T.Muestra_ij(); // Saca 2 2 T.Entra_k(3); T.Muestra_k(); // Saca 3 S.Muestra_k(); // Saca 5 T.Entra_ij(5,6); T.Muestra_ij(); // Saca 5 6 P.Muestra_ij(); // Saca 1 2 } Ventajas e inconvenientes de la derivación Privada y Protegida.- Cuando se utiliza el especificador de acceso private o el especificador de acceso protected en la herencia, se está asegurando que sólo las partes públicas de la clase derivada podrán ser utilizadas por el resto del programa, y por las otras clases derivadas que se derivan a partir de ésta. El acceso private “corta” el acceso, no la herencia, a partir de la clase derivada. El acceso protected “corta” el acceso, no la herencia, desde el exterior, pero no desde las clases derivadas. Los inconvenientes llegan porque al utilizar estos tipos de accesos se está complicando el árbol de herencia, alcanzando una mayor complejidad la determinación por parte del programador, o de alguien que lea el programa, del derecho de acceso a los miembros de cada clase. Estos tipos de derivaciones se emplean muy poco, hay que ser muy cuidadoso cuando se utilizan, y tener unos buenos motivos. Se puede considerar similar a tener un miembro que es una instancia de otra clase diferente. Control de acceso a la clase base.Dado que una clase derivada se puede entender como un superconjunto de la clase base, que va a contener todos los miembros de la clase base, una instancia de la clase derivada se puede emplear de forma automática como si fuera una instancia de la clase base. El caso contrario no es cierto, no se puede tratar un objeto de la clase base como si fuera un objeto de una clase derivada, ya que el objeto de la clase base no tiene todos los miembros de la clase derivada. 49 El siguiente programa muestra como funciona el tema de las conversiones de un objeto de una clase derivada a uno de la clase base. // Programa: Herencia Simple: Conversiones entre clases // Fichero: HER_SIM3.CPP #include <iostream.h> class Primera { protected: int i, j; public: Primera() {i=j=0;} Primera(int a, int b) {i=a; j=b;} void Entra_ij(int a, int b) {i=a; j=b;} void Muestra_ij (void) {cout << "\n" << i << " " << j;} }; class Segunda: public Primera { int k; public: void Entra_k(int a) {k=a;} void Muestra_k (void) {cout << " " << k;} }; void main (void) { Primera P(1,2); Segunda S; S.Entra_ij(3,4); S.Entra_k(5); P.Muestra_ij(); // Saca 1 2 S.Muestra_ij(); // Saca 3 4 S.Muestra_k(); // Saca 5 P=S; // Correcto P.Muestra_ij(); // Saca 3 4 } Un punto mucho más interesante es la conversión de un puntero a una clase derivada, en un puntero a una clase objeto. Para ver este caso, se va a presentar el siguiente ejemplo. Se tiene la siguiente escala de empleados, que se va a representar como una estructura de clases derivadas. 50 // Programa: Empleados // Fichero: EMPLE1.CPP #include <iostream.h> #include <string.h> class Empleado { protected: char nombre[30]; public: Empleado(); Empleado(char *nom); char *Nombre() const; }; class aHoras: public Empleado { protected: float horas; float salario; public: aHoras ( char *nom ); void FijaSalario ( float s ); void FijaHoras ( float h ); void SacaNombre (void) const; }; class Gerente: public Empleado { float salario_semanal; public: Gerente ( char *nom ); void FijaSalario (float s); }; class Vendedor: public aHoras { float comision; 51 float ventas; public: Vendedor (char *nom ); void FijaComision ( float c ); void FijaVentas ( float v ); void SacaComision (void); }; // Funciones miembro de la clase Empleado Empleado::Empleado () { memset ( nombre, ' ', 30); } Empleado::Empleado( char * nom) { strncpy (nombre, nom, 30); } char *Empleado::Nombre() const { return (char *)nombre; } // Funciones miembro de la clase aHoras aHoras::aHoras ( char *nom ):Empleado (nom) { horas=0.0; salario=0.0; } void aHoras::FijaSalario (float s) { salario=s; } void aHoras::FijaHoras (float h) { horas=h; } void aHoras::SacaNombre (void) const { cout << "\nEl nombre del trabajador es: " << nombre; } // Funciones miembro de la clase Gerente Gerente::Gerente ( char *nom ):Empleado (nom) { salario_semanal=0.0; } void Gerente::FijaSalario (float s) { salario_semanal=s; } // Funciones miembro de la clase Vendedor Vendedor::Vendedor (char *nom ):aHoras(nom) { comision=0.0; ventas=0.0; 52 } void Vendedor::FijaComision (float c) { comision=c; } void Vendedor::FijaVentas (float v) { ventas=v; } void Vendedor::SacaComision (void) { cout << "\n" << comision; } void main(void) { Empleado *ptr_emp; aHoras a1("Juan Antonio P."); Gerente g1("Alberto Lamazares"); Vendedor v1("Ana Elena"); ptr_emp=&a1; cout << "\n" << ptr_emp->Nombre(); ptr_emp=&g1; cout << "\n" << ptr_emp->Nombre(); ptr_emp=&v1; cout << "\n" << ptr_emp->Nombre(); } Se puede utilizar un puntero a Empleado para apuntar directamente a una instancia de aHoras, a una instancia de Gerente, o a una instancia de Vendedor. Sin embargo, si la definición de la clase Vendedor hubiera sido: class Vendedor : aHoras { ... Para poder con un puntero a Empleado apuntar a una instancia de la clase Vendedor se hubiera tenido que hacer el siguiente casting. ptr_emp=(Empleado *)&v1; Esto es debido a que cuando en la herencia se utiliza el método de acceso private o protected no se puede convertir de forma implícita un puntero de la clase derivada a un puntero de la clase base. 53 Cuando se hace referencia a un objeto a través de un puntero, el tipo de puntero determina que funciones miembro se pueden llamar. Si se utiliza un puntero a una clase base para apuntar a un objeto de una clase derivada, sólo se podrán utilizar las funciones definidas en la clase base. Por ejemplo, estudiar el siguiente programa principal. void main(void) { Vendedor v1("Ana Elena"), *v2_ptr; aHoras *a1_ptr; v2_ptr=&v1; a1_ptr=&v1; a1_ptr->FijaHoras(40.0); // Se llama a aHoras::FijaHoras v2_ptr->FijaSalario(6.0); // Se llama a aHoras::FijaSalario // a1_ptr->FijaVentas(1000.0); // Error:no existe aHoras::FijaVentas v2_ptr->FijaVentas(1000.0); // Se llama a Vendedor::FijaVentas } Ambos punteros v2_ptr y a1_ptr apuntan al mismo objeto Vendedor. No se puede llamar a la función FijaVentas a través de a1_ptr porque la clase aHoras no tiene definida una función que se llame así. El caso contrario, que un puntero de una clase derivada apunte a un objeto de la clase base, se puede hacer mediante un casting apropiado, pero es muy peligroso y no es recomendable hacerlo, ya que no se está seguro a lo que se apunta. Así el siguiente caso: void main(void) { aHoras a1("Pepe Pérez"); Empleado *ptr = &a1; Vendedor *v1; v1=(Vendedor *) ptr; // Lícito pero incorrecto cout << "\n" << v1->Nombre(); // Saca Pepe Pérez v1->FijaComision(1.5); // La clase aHoras no tiene // el miembro FijaComision v1->SacaComision(); // Saca basura } 54 Constructores y destructores en herencia.Los constructores y destructores no son heredados por las clases derivadas. Sin embargo, una instancia de una clase derivada contendrá todos los miembros de la clase base, y éstos deben ser iniciados. En consecuencia, el constructor de la clase base debe ser llamado por el constructor de la clase derivada. Cuando se escribe un constructor para la clase derivada, se debe especificar una forma de inicio para la base. La forma de hacerlo es utilizar una sintaxis similar a la empleada con los miembros de inicio para los objetos miembro de una clase. Esto es, se coloca : después de la lista de argumentos del constructor de la clase derivada, y seguidamente el nombre de la clase base y la lista de argumentos. // Programa: Constructores en la herencia // Fichero: CONSTR.CPP #include <iostream.h> class B { int a, b, c; public: B(){a=b=c=0;} B( int a1, int a2, int a3 ) { a=a1; b=a2; c=a3;} void DarValores( int a1, int a2, int a3 ) { a=a1; b=a2; c=a3; } void MostrarEnteros (void) { cout << "\n" << a << " " << b << " " << c; } }; class C: public B { float x, y; public: C():B(0,0,0) {x=y=0.0;} C(float a1, float a2, int a3, int a4, int a5) : B(a3, a4, a5) { x=a1; y=a2; 55 } void MostrarFloat(void) { cout << "\n" << x << " " << y; } }; void main() { C b, c(1, 2, 3, 4, 5); b.MostrarEnteros(); b.MostrarFloat(); c.MostrarEnteros(); c.MostrarFloat(); } Cuando se declara un objeto de una clase derivada, el compilador ejecuta el constructor de la clase base en primer lugar, y después ejecuta el constructor de la clase derivada. Si la clase derivada tuviera objetos miembro, sus constructores se ejecutarían después del constructor de la clase base, pero antes que el constructor de la clase derivada. Por lo que respecta a los destructores en la jerarquía de clases, se llaman en orden inverso de la derivación, es decir de la clase derivada a la clases base. La última clase creada es la que se destruye en primer lugar. Herencia Múltiple.Una clase puede tener más de una clase base. Esto significa que una clase puede heredar de dos o más clases. A este fenómeno se le conoce como Herencia Múltiple. La sintaxis de la herencia múltiple es una extensión de la utilizada para la herencia simple. La manera de expresar este tipo de herencia es mediante una lista de herencia, que consta de las clases de las que se hereda separadas por comas. La forma general es: class < nombre_clase_derivada > : < lista_de_herencia > { // Cuerpo de la clase }; Ejemplo: Vamos a ilustrar la herencia múltiple con un sencillo ejemplo. 56 // Programa: Herencia Múltiple // Fichero: HER_MUL1.CPP #include <iostream.h> class Base1 { protected: int b1; public: void Inicia_B1(int v) { b1=v; }}; class Base2 { protected: int b2; public: void Inicia_B2(int v) { b2=v; } int Ret (void) { return b2; } }; class Der: public Base1, public Base2 { public: void print (void) { cout << "\nb1 = " << b1 << "\tb2 = " << Ret(); } }; void main (void) { Der d; d.Inicia_B2(78); d.Inicia_B1(34); d.print(); // Saca 34 y 78 } Este es un ejemplo muy sencillo en el que una clase, Der, hereda de otras dos clases Base1, Base2. En este programa no hay complicaciones de ningún tipo, y no es muy interesante. En la herencia múltiple las reglas de inicio de los miembros son una extensión de los vistos en la herencia simple. De igual forma, los mecanismos de herencia de los miembros están gobernados por las declaraciones de private, protected y public en las clases. El orden en el que se especifican las clases bases en la lista de herencia sólo afecta al orden en que se van a invocar los constructores, y destructores de las clases base desde los constructores y destructores de la clase derivada. Vamos a ver a continuación otro ejemplo de herencia múltiple, en el que las clases tengan constructores. 57 // Programa: Herencia Múltiple // Fichero: HER_MUL2.CPP #include <iostream.h> class A { protected: int a; public: A (int valor) { cout << "\nConstructor de A."; a=valor; } ~A () { cout << "\nDestructor de A."; } }; class B { protected: int b; public: B (int valor) { cout << "\nConstructor de B."; b=valor; } ~B () { cout << "\nDestructor de B."; } }; // C hereda de A y de B class C: public A, public B { public: C(int v1, int v2) : B(v2), A(v1) { cout << "\nConstructor de C."; } ~C () { cout << "\nDestructor de C."; } int operar (void) { return a*a+b*b; } }; void main (void) { C obj(4, 5); cout << "\n" << obj.operar(); }; 58 De lo que se deduce que las clases bases se construyen en el orden en que aparecen en la declaración de C. Una vez que las clases han recibido los valores iniciales, se ejecuta el constructor de la clase derivada. Mientras que ninguna de las clases base tenga constructor, o que todas las clases bases tengan un constructor por defecto, la clase derivada no tiene porque tener constructor. De todas formas es una buena práctica de programación que todas las clases tengan un constructor para de esta forma contar con un mecanismo que permita pasar argumentos a los constructores de sus clases bases. En cuanto a los destructores, decir que al igual que en la herencia simple, se llaman en orden inverso a la derivación. Los mecanismos de herencia múltiple permiten al programador situar la información donde sea necesario. En contra, la herencia simple puede situar parte de los datos donde no se requieran. Supóngase que se tiene el siguiente árbol de jerarquía, y sólo se desea que existan datos de otra clase X en las clases E y F. Con herencia simple se tendría el siguiente esquema: 59 Con herencia múltiple la solución sería: La herencia múltiple es más apropiada para definir objetos compuestos o una combinación de objetos. Mientras que la herencia simple es más útil para definir objetos que son más especializaciones de objetos generales. 60 TEMA V FUNCIONES VITUALES Y POLIMORFISMO Introducción.El Polimorfismo (implementado en C++ con funciones virtuales) es la tercera característica esencial de un lenguaje orientado a objetos, después de la abstracción de datos y la herencia. De hecho, nos provee de otra dimensión para la separación entre interfaz y la implementación, desacoplando el qué del cómo. El Polimorfismo permite mejorar la organización del código y su legibilidad así como la creación de programas extensibles que pueden "crecer" no sólo durante el desarrollo del proyecto, si no también cuando se deseen nuevas características. La encapsulación crea nuevos tipos de datos combinando características y comportamientos. El control de acceso separa la interfaz de la implementación haciendo privados (private) los detalles. Estos tipos de organización son fácilmente entendibles por cualquiera que venga de la programación procedimental. Pero las funciones virtuales tratan de desunir en términos de tipos. En el Capítulo 14, usted vió como la herencia permitía tratar a un objeto como su propio tipo o como a su tipo base. Esta habilidad es básica debido a que permite a diferentes tipos (derivados del mismo tipo base) ser tratados como si fueran un único tipo, y un único trozo de código es capaz de trabajar indistintamente con todos. Las funciones virtuales permiten a un tipo expresar sus diferencias con respecto a otro similar si ambos han sido derivados del mismo tipo base. Esta distinción se consigue modificando las conductas de las funciones a las que se puede llamar a través de la clase base. 61 En este capítulo aprenderá sobre las funciones virtuales, empezando con ejemplos simples que le mostrará lo "desvirtual" del programa. Ligadura.Todo el trabajo se realiza detrás del telón gracias al compilador, que instala los mecanismos necesarios de la ligadura dinámica cuando se crean funciones virtuales. Debido a que los programadores se suelen beneficiar de la comprensión del mecanismo de las funciones virtuales en C++, esta sección mostrará la forma en que el compilador implementa este mecanismo. La palabra reservada virtual le dice al compilador que no debe realizar ligadura estática. Al contrario, debe instalar automáticamente todos los mecanismos necesarios para realizar la ligadura dinámica. Esto significa que si se llama a play() para un objeto Brass a través una dirección a la clase base Instrument, se usará la función apropiada. Para que funcione, el compilador típico crea una única tabla (llamada VTABLE) por cada clase que contenga funciones virtuales. El compilador coloca las direcciones de las funciones virtuales de esa clase en concreto en la VTABLE. En cada clase con funciones virtuales el compilador coloca de forma secreta un puntero llamado vpointer (de forma abreviada VPTR), que apunta a la VTABLE de ese objeto. Cuando se hace una llamada a una función virtual a través de un puntero a la clase base (es decir, cuando se hace una llamada usando el polimorfismo), el compilador silenciosamente añade código para buscar el VPTR y así conseguir la dirección de la función en la VTABLE, con lo que se llama a la función correcta y tiene lugar la ligadura dinámica. Todo esto - establecer la VTABLE para cada clase, inicializar el VPTR, insertar código para la llamada a la función virtual - sucede automáticamente sin que haya que preocuparse por ello. Con las funciones virtuales, se llama a la función apropiada de un objeto, incluso aunque el compilador no sepa el tipo exacto del objeto. Almacenando información de tipo.Se puede ver que no hay almacenada información de tipo de forma explícita en ninguna de las clases. Pero los ejemplos anteriores, y la simple lógica, dicen que debe existir algún tipo de información almacenada en los objetos; de otra forma el tipo no podría ser establecido en tiempo de ejecución. Es verdad, pero la información de tipo está oculta. Para verlo, aquí está un ejemplo que muestra el tamaño de las clases que usan funciones virtuales comparadas con aquellas que no las usan: 62 //: C15:Sizes.cpp // Object sizes with/without virtual functions #include <iostream> using namespace std; class NoVirtual { int a; public: void x() const {} int i() const { return 1; } }; class OneVirtual { int a; public: virtual void x() const {} int i() const { return 1; } }; class TwoVirtuals { int a; public: virtual void x() const {} virtual int i() const { return 1; } }; int main() { cout << "int: " << sizeof(int) << endl; cout << "NoVirtual: " << sizeof(NoVirtual) << endl; cout << "void* : " << sizeof(void*) << endl; cout << "OneVirtual: " << sizeof(OneVirtual) << endl; cout << "TwoVirtuals: " << sizeof(TwoVirtuals) << endl; } ///:~ Sin funciones virtuales el tamaño del objeto es exactamente el que se espera: el tamaño de un único int. Con una única función virtual en OneVirtual, el tamaño del objeto es el tamaño de NoVirtual más el tamaño de un puntero a void. Lo que implica que el compilador añade un único puntero (el VPTR) en la estructura si se tienen una o más funciones virtuales. No hay diferencia de 63 tamaño entre OneVirtual y TwoVirtuals. Esto es porque el VPTR apunta a una tabla con direcciones de funciones. Se necesita sólo una tabla porque todas las direcciones de las funciones virtuales están contenidas en esta tabla. Este ejemplo requiere como mínimo un miembro de datos. Si no hubiera miembros de datos, el compilador de C++ hubiera forzado a los objetos a ser de tamaño no nulo porque cada objeto debe tener direcciones distintas (¿se imagina cómo indexar un array de objetos de tamaño nulo?). Por esto se inserta en el objeto un miembro "falso" ya que de otra forma tendríá un tamaño nulo. Cuando se inserta la información de tipo gracias a la palabra reservada virtual, ésta ocupa el lugar del miembro "falso". Intente comentar el int a en todas las clases del ejemplo anterior para comprobarlo. Tipos de ligaduras. Ligadura Estática: Se produce antes de la ejecución (durante la compilación). Ligadura Dinámica: La ligadura dinámica ocurre en la ejecución. Funciones virtuales.Para que la ligadura dinámica tenga efecto en una función particular, C++ necesita que se use la palabra reservada virtual cuando se declara la función en la clase base. La ligadura en tiempo de ejecución funciona unícamente con las funciones virtual es, y sólo cuando se está usando una dirección de la clase base donde exista la función virtual, aunque puede ser definida también en una clase base anterior. Para crear una función miembro como virtual, simplemente hay que preceder a la declaración de la función con la palabra reservada virtual. Sólo la declaración necesita la palabra reservada virtual, y no la definición. Si una función es declarada como virtual, en la clase base, será entonces virtual en todas las clases derivadas. La redefinición de una función virtual en una clase derivada se conoce como overriding. Hay que hacer notar que sólo es necesario declarar la función como virtual en la clase base. Todas las funciones de las clases derivadas que encajen con la declaración que esté en la clase base serán llamadas usando el mecanismo virtual. Se puede usar la palabra reservada virtual en las declaraciones de las clases derivadas (no hace ningún mal), pero es redundante y puede causar confusión. Para conseguir el comportamiento deseado de Instrument2.cpp, simplemente hay que añadir la palabra reservada virtual en la clase base antes de play(). 64 //: C15:Instrument3.cpp // Late binding with the virtual keyword #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. class Instrument { public: virtual void play(note) const { cout << "Instrument::play" << endl; } }; // Wind objects are Instruments // because they have the same interface: class Wind : public Instrument { public: // Override interface function: void play(note) const { cout << "Wind::play" << endl; } }; void tune(Instrument& i) { 65 // ... i.play(middleC); } int main() { Wind flute; tune(flute); // Upcasting } ///:~Instrument3.cpp Este archivo es idéntico a Instrument2.cpp excepto por la adición de la palabra reservada virtual y, sin embargo, el comportamiento es significativamente diferente: Ahora la salida es Wind::play. Extensibilidad Con play() definido como virtual en la clase base, se pueden añadir tantos nuevos tipos como se quiera sin cambiar la función play(). En un programa orientado a objetos bien diseñado, la mayoría de las funciones seguirán el modelo de play() y se comunicarán únicamente a través de la interfaz de la clase base. Las funciones que usen la interfaz de la clase base no necesitarán ser cambiadas para soportar a las nuevas clases. Aquí está el ejemplo de los instrumentos con más funciones virtuales y un mayor número de nuevas clases, las cuales trabajan de manera correcta con la antigua (sin modificaciones) función play(): //: C15:Instrument4.cpp // Extensibility in OOP #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. 66 class Instrument { public: virtual void play(note) const { cout << "Instrument::play" << endl; } virtual char* what() const { return "Instrument"; } // Assume this will modify the object: virtual void adjust(int) {} }; class Wind : public Instrument { public: void play(note) const { cout << "Wind::play" << endl; } char* what() const { return "Wind"; } void adjust(int) {} }; class Percussion : public Instrument { public: void play(note) const { cout << "Percussion::play" << endl; } 67 char* what() const { return "Percussion"; } void adjust(int) {} }; class Stringed : public Instrument { public: void play(note) const { cout << "Stringed::play" << endl; } char* what() const { return "Stringed"; } void adjust(int) {} }; class Brass : public Wind { public: void play(note) const { cout << "Brass::play" << endl; } char* what() const { return "Brass"; } }; class Woodwind : public Wind { public: void play(note) const { cout << "Woodwind::play" << endl; } char* what() const { return "Woodwind"; } 68 }; // Identical function from before: void tune(Instrument& i) { // ... i.play(middleC); } // New function: void f(Instrument& i) { i.adjust(1); } // Upcasting during array initialization: Instrument* A[] = { new Wind, new Percussion, new Stringed, new Brass, }; int main() { Wind flute; Percussion drum; Stringed violin; Brass flugelhorn; Woodwind recorder; tune(flute); 69 tune(drum); tune(violin); tune(flugelhorn); tune(recorder) f(flugelhorn); } ///:~Instrument4.cpp Se puede ver que se ha añadido otro nivel de herencia debajo de Wind, pero el mecanismo virtual funciona correctamente sin importar cuantos niveles haya. La función adjust() no está redefinida (override) por Brass y Woodwind. Cuando esto ocurre, se usa la definición más "cercana" en la jerarquía de herencia - el compilador garantiza que exista alguna definición para una función virtual, por lo que nunca acabará en una llamada que no esté enlazada con el cuerpo de una función (lo cual sería desatroso). El array A[] contiene punteros a la clase base Instrument, lo que implica que durante el proceso de inicialización del array habrá upcasting. Este array y la función f() serán usados en posteriores discusiones. En la llamada a tune(), el upcasting se realiza en cada tipo de objeto, haciendo que se obtenga siempre el comportamiento deseado. Se puede describir como "enviar un mensaje a un objeto y dejar al objeto que se preocupe sobre qué hacer con él". La función virtual es la lente a usar cuando se está analizando un proyecto: ¿Dónde deben estar las clases base y cómo se desea extender el programa? Sin embargo, incluso si no se descubre la interfaz apropiada para la clase base y las funciones virtuales durante la creación del programa, a menudo se descubrirán más tarde, incluso mucho más tarde cuando se desee ampliar o se vaya a hacer funciones de mantenimiento en el programa. Esto no implica un error de análisis o de diseño; simplemente significa que no se conocía o no se podía conocer toda la información al principio. Debido a la fuerte modularización de C++, no es mucho problema que esto suceda porque los cambios que se hagan en una parte del sistema no tienden a propagarse a otras partes como sucede en C. 70 Polimorfismo.Otro concepto interesante, con Importantes aportaciones en áreas tales como la flexibilidad o la legibilidad del software, es el de polimorfismo. Tras este termino, un tanto oscuro, subyace una idea bastante simple. En su más amplia expresión, el polimorfismo puede definirse como: "el mecanismo que permite definir e Invocar funciones idénticas en denominación e interfaz, pero con implementaron diferente". Esta definición introduce un aspecto muy importante del polimorfismo: la asociación, o vinculo, entre cada llamada a una de estas funciones polimorfismo y la implementación concreta finalmente invocada. Cuando este vinculo puede establecerse en tiempo de compilación, se suele hablar de vinculación estatica (static binding). Por contra, cuando la implementación a emplear, puede determinarse en tiempo de ejecución, el termino empleado es el de vinculación dinámica (dynamic binding). En C++, por ejemplo, la vinculación dinámica de las llamadas a funciones polimórficas (en C++ reciben el calificativo de funciones virtuales) se consigue en base a la posibilidad que ofrece este lenguaje de utilizar un puntero a objetos de una clase como puntero a objetos de cualquiera de las clases descendientes de la anterior. Así, cuando la llamada a una función virtual, definida en una clase y en una o varias de sus descendientes, se realiza sobre un objeto que viene referenciado mediante un puntero a la clase padre, el compilador es incapaz de determinar que implementación debe asociar a la llamada, ya que desconoce cual será la clase del objeto en el momento de su ejecución. Dicha determinación debe quedar aplazada, por tanto, hasta ese instante. Como se puede observar, el concepto de polimorfismo en C++, y en general en casi todos los lenguajes de programación basados en el paradigma de objeto, esta estrechamente ligado al concepto de herencia, dado que las funciones polimórficas sólo pueden definirse entre clases que guardan entre sí una relación de parentesco (clases con un antecesor común). Aunque el concepto de polimorfismo es una de las principales innovaciones del desarrollo orientado a objetos, posee antecedentes históricos en otros mecanismos más sencillos, como son la conversión forzada (casting) y la sobrecarga de identificadores, ideados con el fin de introducir un cierto grado de flexibilidad en el manejo de tipos de datos heterogéneos. En el siguiente apartado se desarrolla un pequeño ejemplo de aplicación de ambos conceptos, en el que quedan claramente de manifiesto las ventajas potenciales de una correcta utilización de los mismos. Para su implementación se ha elegido el lenguaje C ++, fundamentalmente por dos razones: #include <iostream.h> void show(int val) { 71 cout <<" Es un entero :"<< val << '\n'; } void show(char *val) { cout <<"Es un carácter: "<< val << '\n'; } main() { show (42); show ("A"); show (452.2); } TEMA VI PLANTILLAS Y TRATAMIENTO DE EXCEPCIONES Introducción.La herencia y la composición proporcionan una forma de retilizar código objeto. Las plantillas de C++ proporcionan una manera de reutilizar el código fuente. Aunque las plantillas (o templates) son una herramienta de programación de propósito general, cuando fueron introducidos en el lenguaje, parecían oponerse al uso de las jerarquías de clases contenedoras basadas en objetos (demostrado al final del Capítulo 15). Además, los contenedores y algoritmos del C++ Standard están construidos exclusivamente con plantillas y son relativamente fáciles de usar por el programador. Este capítulo no sólo muestra los fundamentos de los templates, también es una introducción a los contenedores, que son componentes fundamentales de la programación orientada a objetos lo cual se evidencia a través de los contenedores de la librería estándar de C++. Se verá que este libro ha estado usando ejemplos contenedores - Stash y Stack- para hacer más sencillo el concepto de los contenedores; en este capítulo se sumará el concepto del iterator. Aunque los contenedores son el ejemplo ideal para usarlos con las plantillas, en el Volumen 2 (que tiene un capítulo con plantillas avanzadas) se aprenderá que también hay otros usos para los templates Plantillas.- Ahora surge un nuevo problema. Tenemos un IntStack, que maneja enteros. Pero queremos una pila que maneje formas, o flotas de aviones, o plantas o cualquier otra cosa. Reinventar el código fuente cada vez no parece una aproximación muy inteligente con un lenguaje que propugna la reutilización. Debe haber un camino mejor. 72 Hay tres técnicas para reutilizar código en esta situación: el modo de C, presentado aquí como contraste; la aproximación de Smalltalk, que afectó de forma significativa a C++, y la aproximación de C++: los templates. La solución de C. Por supuesto hay que escapar de la aproximación de C porque es desordenada y provoca errores, al mismo tiempo que no es nada elegante. En esta aproximación, se copia el código de una Stack y se hacen modificaciones a mano, introduciendo nuevos errores en el proceso. Esta no es una técnica muy productiva. La solución de Smalltalk. Smalltalk (y Java siguiendo su ejemplo) optó por una solución simple y directa: Se quiere reutilizar código, pues utilicese la herencia. Para implementarlo, cada clase contenedora maneja elementos de una clase base genérica llamada Object (similar al ejemplo del final del capítulo 15). Pero debido a que la librería de Smalltalk es fundamental, no se puede crear una clase desde la nada. En su lugar, siempre hay que heredar de una clase existente. Se encuentra una clase lo más cercana posible a lo que se desea, se hereda de ella, y se hacen un par de cambios. Obviamente, esto es un beneficio porque minimiza el trabajo (y explica porque se pierde un montón de tiempo aprendiendo la librería antes de ser un programador efectivo en Smalltalk). Pero también significa que todas las clases de Smalltalk acaban siendo parte de un único árbol de herencia. Hay que heredar de una rama de este árbol cuando se está creando una nueva clase. La mayoría del árbol ya esta allí (es la librería de clases de Smalltalk), y la raiz del árbol es una clase llamada Object - la misma clase que los contenedores de Smalltalk manejan. Es un truco ingenioso porque significa que cada clase en la jerarquía de herencia de Smalltalk (y Java) se deriva de Object, por lo que cualquier clase puede ser almacenada en cualquier contenedor (incluyendo a los propios contenedores). Este tipo de jerarquía de árbol única basada en un tipo genérico fundamental (a menudo llamado Object, como también es el caso en Java) es conocido como "jerarquía basada en objectos". Se puede haber oido este témino y asumido que es un nuevo concepto fundamental de la POO, como el polimorfismo. Sin embargo, simplemente se refiere a la raíz de la jerarquía como Object (o algún témino similar) y a contenedores que almacenan Objects. Debido a que la librería de clases de Smalltalk tenía mucha más experiencia e historia detrás de la que tenía C++, y porque los compiladores de C++ originales no tenían librerías de clases contenedoras, parecía una buena idea duplicar la librería de Smalltalk en C++. Esto se hizo como experimento con una de las primeras implementaciónes de C++, y como representaba un 73 significativo ahorro de código mucha gente empezo a usarlo. En el proceso de intentar usar las clases contenedoras, descubrieron un problema. El problema es que en Smalltalk (y en la mayoría de los lenguajes de POO que yo conozco), todas las clases derivan automáticamente de la jerarquía única, pero esto no es cierto en C++. Se puede tener una magnifica jerarquía basada en objetos con sus clases contenedoras, pero entonces se compra un conjunto de clases de figuras, o de aviones de otro vendedor que no usa esa jerarquía. (Esto se debe a que usar una jerarquía supone sobrecarga, rechazada por los programadores de C). ¿Cómo se inserta un árbol de clases independientes en nuestra jerarquía? El problema se parece a lo siguiente: Contenedores.Debido a que C++ suporta múltiples jerarquías independientes, la jerarquía basada en objetos de Smalltalk no funciona tan bien. La solución parace obvia. Si se pueden tener múltiples jerarquías de herencia, entonces hay que ser capaces de heredar de más de una clase: La herencia múltiple resuelve el problema. Por lo que se puede hacer lo siguiente: Herencia múltiple.- 74 Ahora OShape tiene las características y el comportamiento de Shape, pero como también está derivado de Object puede ser insertado en el contenedor. La herencia extra dada a OCircle, OSquare, etc. es necesaria para que esas clases puedan hacer upcast hacia OShape y puedan mantener el comportamiento correcto. Se puede ver como las cosas se están volviendo confusas rápidamente. Los vendedores de compiladores inventaron e incluyeron sus propias jerarquías y clases contenedoras, muchas de las cuales han sido reemplazadas desde entonces por versiones de templates. La solución de la plantilla.Aunque una jerarquía basada en objetos con herencia múltiple es conceptualmente correcta, se vuelve difícil de usar se demostró lo que se consideraba una alternativa preferible a la jerarquía basada en objetos. Clases contenedoras que fueran creadas como grandes macros del preprocesador con argumentos que pudieran ser sustituidos con el tipo deseado. Cuando se quiera crear un contenedor que maneje un tipo en concreto, se hacen un par de llamadas a macros. Desafortunadamente, esta aproximación era confusa para toda la literatura existente de Smalltalk y para la experiencia de programación, y era un poco inmanejable. Básicamente, nadie la entendía. Mientras tanto, Stroustrup y el equipo de C++ de los Laboratorios Bell habían modificado su aproximación de las macros, simplificándola y moviéndola del dominio del preprocesador al compilador. Este nuevo dispositivo de sustitución de código se conoce como template (plantilla), y representa un modo completamente diferente de reutilizar el código. En vez de reutilizar código objeto, como en la herencia y en la composición, un template reutiliza código fuente. El contenedor no maneja una clase base genérica llamada Object, si no que gestiona un parámetro no especificado. Cuando se usa un template, el parámetro es sustituido por el compilador, parecido a la antigua aproximación de las macros, pero más claro y fácil de usar. hora, en vez de preocuparse por la herencia o la composición cuando se quiera usar una clase contenedora, se usa la versión en plantilla del contenedor y se crea una versión específica para el problema, como lo siguiente: 75 Figura: Contenedor de objetos El compilador hace el trabajo por nosotros, y se obtiene el contenedor necesario para hacer el trabajo, en vez de una jerarquía de herencia inmanejable. En C++, el template implementa el concepto de tipo parametrizado. Otro beneficio de la aproximación de las plantillas es que el programador novato que no tenga familiaridad o esté incómodo con la herencia puede usar las clases contenedoras de manera adecuada (como se ha estado haciendo a lo largo del libro con el vector). Concepto de excepciones.Las excepciones son situaciones anómalas que requieren un tratamiento especial. ¡¡No tienen por qué ser errores!! Si se consigue dominar su programación, la calidad de las aplicaciones que se desarrollen aumentará considerablemente. El funcionamiento general del mecanismo de lanzamiento y tratamiento de excepciones es el siguiente: Existe un método que invoca la ejecución de otro. Este método más interno se encuentra en una situación que puede considerarse como excepcional. Por lo tanto lanza una excepción. En este momento termina la ejecución del método más interno y se retorna inmediatamente al método llamador. El método llamador debe capturar la excepción y la trata. Parte del tratamiento de la excepción puede ser volver a lanzarla al método que invocó a este. La correcta programación de excepciones significa diseñar los algoritmos pensando únicamente en la forma habitual en la que deben ejecutarse, manejando las situaciones extraordinarias a 76 parte. De esta manera se consigue un diseño mucho más estructurado, legible, robusto y fácil de mantener. Ejemplo.Cuando solicitamos memoria al sistema, lo habitual es que exista suficiente memoria disponible y no haya ningún problema. Pero si queremos realizar una aplicación robusta deberemos de tener en cuenta la eventualidad de que dicha memoria no se conceda, lo cual complica enormemente un sencillo algoritmo. Veamos una función escrita en C que simplemente intenta asignar memoria dinámica para tres enteros: void SinExcepciones (void) { int *p1, *p2, *p3; p1 = (int*) malloc(sizeof(int)); if (p1 == NULL) { printf("No hay suficiente memoria"); abort(); } p2 = (int*) malloc(sizeof(int)); if (p2 == NULL) { printf("No hay suficiente memoria"); abort(); } p3 = (int*) malloc(sizeof(int)); if (p3 == NULL) { printf("No hay suficiente memoria"); abort(); 77 } } Si programamos en C++ y hacemos uso de las excepciones: void ConExcepciones (void) { int *p1, *p2, *p3; try { p1 = new int; // Comportamiento normal. p2 = new int; p3 = new int; } catch(...){ // Comportamiento excepcional. printf("No hay suficiente memoria"); abort(); } } El ANSI C++ especifica que si una instrucción new falla (no hay memoria disponible) debe lanzar la excepción bad_alloc (definida en el fichero de cabecera new). Las instrucciones que pueden provocar el lanzamiento de una excepción (la "zona crítica") se encierran en un bloque try. Si alguna de las instrucciones del bloque try provocara una excepción el flujo del programa se dirige al final de ese bloque, buscando un bloque catch que capture la excepción (si no lo encontrara, el programa terminaría -ver sección 6.4-). En este caso, la instrucción catch(...) captura cualquier excepción, y en particular, la excepción bad_alloc. El tratamiento que se efectúa es, en este caso, mostrar un mensaje de error y terminar la ejecución del programa. Lanzamiento de excepciones.El lanzamiento de una excepción se realiza llamando a la función throw(). Cuando se lanza una excepción, en realidad lo que se hace es crear un objeto de la clase que se le indique a throw(), y precisamente será dicho objeto la excepción en sí. Suele ser muy útil crear clases de excepciones propias para controlar las situaciones anómalas de nuestra aplicación. Por ejemplo, 78 En ObjGraf.h: //*************************************************/ // Definicion de la clase EFueraRango // Clase de excepcion por entrar en "fuera de rango": // salirse de los limites del PaintBox al calcular // las nuevas coordenadas de dibujo de la pelota. //*************************************************/ class EFueraRango {}; Con lo que podríamos lanzar una excepción de la siguiente manera: throw EfueraRango(); Nota: Aunque las excepciones sean clases, en C++ Builder existe la convención de que su nombre empiece por la letra E, y no por T como el resto de las clases. Aunque en el ejemplo anterior la clase creada para la excepción, no tiene ningún miembro (propiedades o métodos), se le pueden añadir. Éstos servirán para poder incorporar información en el objeto excepción, acerca de la situación en la que se produjo la excepción, que podrá ser utilizada por la sección de código que lo trate. Por ejemplo, si tuviéramos un método que lanzara una excepción por falta de memoria, podría ser interesante que incorporara una propiedad que indicara cual era el máximo de memoria disponible cuando se lanzó la excepción. Especificación de excepciones.C++ cuenta con una característica denominada especificación de excepciones, que sirve para enumerar, en una declaración, las excepciones que puede lanzar un método. Si queremos que un método pueda lanzar un determinado tipo de excepción deberemos especificarlo de esta manera. En ObjGraf.h, en la clase TObjGraf: //*************************************************/ // Definicion de la clase base TObjGraf //*************************************************/ 79 class TObjGraf { private: int FX; int FY; void SetX (int _X) throw (EFueraRango); // Si hay problemas,crean un void SetY (int _Y) throw (EFueraRango); // objeto de clase EFueraRango virtual int GetAncho (void) = 0; // Metodo virtual puro virtual int GetAlto (void) = 0; // Metodo virtual puro ... En ObjGraf.cpp: // Funciones de escritura de las propiedades virtuales X e Y void TObjGraf :: SetX (int _X) throw (EFueraRango) { if (_X < 0) { // Coordenada negativa FX = 0; // Ajustar al margen izquierdo throw EFueraRango(); // Lanzar excepcion } else if (_X > (PaintBox->Width - Ancho)) { // Demasiado alta FX = PaintBox->Width - Ancho; // Ajustar al margen derecho throw EFueraRango(); // Lanzar excepcion } else FX = _X; // Correcto: escribir sin modificar } void TObjGraf :: SetY (int _Y) throw (EFueraRango) { if (_Y < 0) { // Coordenada negativa FY = 0; // Ajustar al margen superior throw EFueraRango(); // Lanzar excepcion } else if (_Y > (PaintBox->Height - Alto)) { // Demasiado alta FY = PaintBox->Height - Alto; // Ajustar al margen inferior throw EFueraRango(); // Lanzar excepcion 80 } else FY = _Y; // Correcto: escribir sin modificar } Captura de excepciones.- El bloque susceptible de producir alguna excepción ("zona crítica") se encierra en un bloque try, la captura (discriminación de la excepción que se ha producido) se efectúa en una instrucción catch y su procesamiento se realiza a continuación, en el bloque catch. try { <bloque de instrucciones críticas> } catch (<tipo excepción1> <variable1>) { <manejador 1> } catch (<tipo excepción2> <variable2>) { ... } Podemos especificar tantos bloques catch para un bloque try como deseemos, en el momento que ocurra una excepción se ejecutará el bloque catch cuya clase concuerde con la de la excepción. Si se especifica catch (...) se capturará cualquier excepción. La excepción no tiene porqué lanzarse explícitamente en el bloque try sino que puede ser consecuencia de la ejecución de una función que se ha llamado dentro de ese bloque. En ObjGraf.cpp: void TPelota :: Mover (void) { Borrar (); try { X += FDirX * Velocidad; } catch (EFueraRango) { 81 FDirX = -FDirX; }; try { Y += FDirY * Velocidad; } catch (EFueraRango) { FDirY = -FDirY; }; Mostrar (); } En este ejemplo, la instrucción "crítica": X += FDirX * Velocidad; se traduce a: X = X + FDirX * Velocidad; y como X es una propiedad virtual, en realidad se trata de hacer: SetX (FX + FDirX * Velocidad); donde se observa que es el método SetX() el que puede provocar el lanzamiento de la excepción. En Ppal.cpp: 1. Dejar únicamente la siguiente declaración global: 2. 3. 4. TPelota *Pelota; Modificar las funciones FormCreate() y FormDestroy() para que queden: //---------------------------------------------------------- 5. 6. void __fastcall TPpalFrm::FormCreate(TObject *Sender) 7. { 8. Pelota = new TPelota (PaintBox, clYellow, 120, 70, 25); 9. } 10. //---------------------------------------------------------- 11. 12. void __fastcall TPpalFrm::FormDestroy(TObject *Sender) 13. { 14. 15. delete Pelota; } 16. Eliminar el manejador del evento OnPaint del componente PaintBox (borrando su cuerpo solamente). 17. Añadir a PpalFrm un TTimer (Carpeta System) y fijar Interval=100. En el evento OnTimer poner: 18. 82 //---------------------------------------------------------- 19. 20. void __fastcall TPpalFrm::Timer1Timer(TObject *Sender) 21. { 22. Pelota->Mover(); 23. } 24. //--------------------------------------------------------- LECTURAS COPLEMENTARIAS Disciplinas relacionadas con POA Por último, hay muchas disciplinas y campos de investigación relacionados de alguna manera con la orientación a aspectos, algunos persiguen los mismos objetivos y algunos otros. En este apartado se hará mención a algunos de ellos, de manera que se tenga una visión más amplia de por dónde se mueve la investigación en esta disciplina. Reflexión y protocolos de meta objetos.Un sistema reflectivo proporciona un lenguaje base y uno o más metalenguajes que proporcionan control sobre la semántica del lenguaje base y la implementación. Los metalenguajes proporcionan vistas de la computación que ningún componente del lenguaje base puede percibir. Los metalenguajes son de más bajo nivel que los lenguajes de aspectos en los que los puntos de enlace son equivalentes a los “ganchos” de la programación reflectiva. Estos sistemas se pueden utilizar como herramienta para la programación orientada a aspectos. Transformación de programas.El objetivo que persigue la transformación de programas es similar al de la orientación a aspectos. Busca el poder escribir programas correctos en un lenguaje demás alto nivel, y luego, transformarlos de forma mecánica en otros que tengan un comportamiento idéntico, pero cuya eficiencia sea mucho mejor. Con este estilo de programación algunas propiedades de las que el programador quiere implementar se escriben en un programa inicial. Otras se añaden gracias a que el programa inicial pasa a través de varios programas de transformación. Esta separación es similar a la que se produce entre los componentes y los aspectos. Programación subjetiva.La programación subjetiva y la orientada a aspectos son muy parecidas, pero no son exactamente lo mismo. La programación subjetiva soporta la combinación automática de métodos para un 83 mensaje dado a partir de diferentes sujetos. Estos métodos serían equivalentes a los componentes tal y como se perciben desde la POA .Hay una diferencia clave entre ambas disciplinas, y es que mientras que los aspectos de la POA tienden a ser propiedades que afectan al rendimiento o la semántica de los componentes, los sujetos de la programación subjetiva son características adicionales que se añaden a otros sujetos. BIBLIOGRAFIA AUTOR OBRA LUGAR EDITORIAL DE EDICIÓN AÑO JOYANES AGUILAR, LUIS “PROGRAMACION ORIENTADA A OBJETOS” MADRID MGGRAWHILL 1998 JOYANES AGUILAR, LUIS “FUNDAMENTOS DE LA PROGRAMACION “ MADRID MGGRAWHILL 2003 BOOCH, GRADY “DISENO ORIENTADO A OBJETOS CON APLICACIONES” MADRID MGGRAWHILL 1991 NEW YORK ADDISONWESLEY 1998 STAUGUARD,ANDREW “TECNICAS ESTRUCTURADAS Y ORIENTADAS A OBJETOS:UNA INTRODUCCION UTILIZANDO C++” SCHILDT,HERBERT “APLIQUE TURBO C++” MADRID MGGRAWHILL 1991 DEITEL Y DEITEL “COMO PROGRAMAR EN JAVA” MADRID MGGRAWHILL 2005 84