Es aquella en que
una acción sigue a otra en secuencia. Las tareas se suceden de tal modo
que la salida de una es la entrada de la siguiente.
Breve Introducción:
La computadora automática debe su derecho a
existir, su utilidad, precisamente a su capacidad de efectuar muchos cálculos
que no pueden realizar los seres humanos. Deseamos que la computadora efectúe
lo que nunca podríamos hacer nosotros, y la potencia de las maquinas actuales
es tal, que inclusive los cálculos pequeños, por su tamaño, escapan al poder de
nuestra imaginación limitada.
Sin embargo debemos organizar él calculo de
manera tal que nuestros limitados poderes sean suficientes para asegurar que se
establecerá el efecto deseado. Esta organización incluye la composición de los
programas.
Los avances en la tecnología siempre van
parejos con progresos en los lenguajes de programación y con nuevas ayudas para
simplificar el uso del computador, con lo cual un numero mayor de usuarios se
beneficia del. Pero la necesidad de hacer programas para resolver problemas
específicos quizás nunca desaparecerá.
CONCEPTO DE PROGRAMACIÓN ESTRUCTURADA
El creciente empleo de los computadores ha
conducido a buscar un abaratamiento del desarrollo del software, paralelo a la
reducción del costo del hardware obtenido gracias a los avances tecnológicos.
Los altos costos del mantenimiento de las aplicaciones en producción normal
también han surgido en la necesidad de mejorar la productividad de los
programadores.
En la década del sesenta salieron a la luz
los principios de lo que más tarde se llamó Programación Estructurada,
posteriormente se liberó el conjunto de las llamadas "Técnicas para
mejoramiento de la productividad en programación" (en ingles Improved Programming Technologies,
abreviado IPTs), siendo la Programación Estructurada
una de ellas.
Los
programas computarizados pueden ser escritos con un alto grado de
estructuración, lo cual les permite ser más comprensibles en actividades tales
como pruebas, mantenimiento y modificación de los mismos. Mediante la
programación Estructurada todas las divisiones de control de un programa se
encuentran estandarizadas, de forma tal que es posible leer la codificación del
mismo desde su inicio hasta su terminación en forma continua, sin tener que
saltar de un lugar a otro del programa siguiendo el rastro de la lógica
establecida por el programador, como es la situación habitual con
codificaciones desarrolladas bajo otras técnicas.
En
programación Estructurada los programadores deben profundizar más que lo usual
al proceder a realizar el diseño original del programa, pero el resultado final
es más fácil de leer y comprender, el objetivo de un programador profesional al
escribir programas de una manera estructurada, es realizarlos utilizando
solamente un numero de divisiones de control estandarizados.
El
resultado de aplicar la sistemática y disciplinada manera de elaboración de
programas establecida por la Programación Estructurada es una programación de alta precisión como
nunca antes había sido lograda. Las pruebas de los programas, desarrollados
utilizando este método, se acoplan mas rápidamente y el resultado final con
programas que pueden ser leídos, mantenidos y modificados por otros
programadores con mucha mayor facilidad.
Definición
Lógica:
Programación Estructurada: Es una técnica en la cual la estructura de un
programa, la escritura de sus partes se realiza tan claramente como es posible
mediante el uso de tres estructuras lógicas de control:
·
Secuencia: Sucesión simple de
dos o más operaciones.
·
Selección: División condicional
de una o más operaciones.
·
Interacción: Repetición de una
operación mientras se cumple una condición.
Estos tres tipos de estructuras lógicas de control
pueden ser combinados para producir programas que manejen cualquier tarea de
procesamiento de información.
Un programa estructurado esta compuesto de segmentos,
los cuales puedan estar constituidos por unas pocas instrucciones o por una pagina o más de codificación. Cada segmento tiene solamente
una entrada y una salida, estos segmentos, asumiendo que no poseen lazos
infinitos y no tienen instrucciones que jamás se ejecuten, se denominan
programas propios. Cuando varios programas propios se combinan utilizando las
tres estructuras básicas de control mencionadas anteriormente, el resultado es
también un programa propio.
La Programación Estructurada esta basada en el Teorema de la Estructura*, el cual
establece que cualquier programa propio (un programa con una entrada y una
salida exclusivamente) es equivalente a un programa que contiene solamente las
estructuras lógicas mencionadas anteriormente.
Una característica importante en un programa
estructurado es que puede ser leído en secuencia, desde el comienzo hasta el
final sin perder la continuidad de la tarea que cumple el programa, lo
contrario de lo que ocurre con otros estilos de programación.
Esto es importante debido a que, es mucho más fácil comprender
completamente el trabajo que realiza una función determinada, si todas las
instrucciones que influyen en su acción están físicamente cerca y encerradas
por un bloque. La facilidad de lectura, de comienzo a fin, es una consecuencia
de utilizar solamente tres estructuras de control y de eliminar la instrucción
de desvío de flujo de control, excepto en circunstancias muy especiales tales
como la simulación de una estructura lógica de control en un lenguaje de
programación que no la posea.
VENTAJAS
POTENCIALES
Un programa escrito de acuerdo a estos principios no
solamente tendrá una estructura, sino también una excelente presentación.
Un programa escrito de esta forma tiende a ser mucho
más fácil de comprender que programas escritos en otros estilos.
La facilidad de comprensión del contenido de un
programa puede facilitar el chequeo de la codificación y reducir el tiempo de
prueba y depuración de programas. Esto ultimo es
cierto parcialmente, debido a que la programación estructurada concentra los
errores en uno de los factores más generador de fallas en programación: la
lógica.
Un programa que es fácil para leer y el cual esta
compuesto de segmentos bien definidos tiende a ser simple, rápido y menos
expuesto a mantenimiento. Estos beneficios derivan en parte del hecho que,
aunque el programa tenga una extensión significativa, en documentación tiende
siempre a estar al día, esto no suele suceder con los métodos convencionales de
programación.
La programación estructurada ofrece estos beneficios,
pero no se la debe considerar como una panacea ya que el desarrollo de
programas es, principalmente, una tarea de dedicación, esfuerzo y creatividad.
*TEOREMA DE LA ESTRUCTURA
El teorema de la estructura establece que un programa
propio puede ser escrito utilizando solamente las siguientes estructuras
lógicas de control: secuencia, selección e iteración.
Un programa de define como propio si cumple con los
dos requerimientos siguientes:
a.
a.
Tiene exactamente una entrada y una salida para control del programa.
La programación
Orientada a objetos (POO) es una forma especial de programar, más cercana a
como expresaríamos las cosas en la vida real que otros tipos de programación.
Con la POO tenemos que aprender a
pensar las cosas de una manera distinta, para escribir nuestros programas en
términos de objetos, propiedades, métodos y otras cosas que veremos rápidamente
para aclarar conceptos y dar una pequeña base que permita soltarnos un poco con
este tipo de programación.
Motivación
Durante años, los programadores
se han dedicado a construir aplicaciones muy parecidas que resolvían una y otra
vez los mismos problemas. Para conseguir que los esfuerzos de los programadores
puedan ser utilizados por otras personas se creó la POO. Que es una serie de
normas de realizar las cosas de manera que otras personas puedan utilizarlas y
adelantar su trabajo, de manera que consigamos que el código se pueda
reutilizar.
La POO no es difícil, pero es una
manera especial de pensar, a veces subjetiva de quien la programa, de manera
que la forma de hacer las cosas puede ser diferente según el programador.
Aunque podamos hacer los programas de formas distintas, no todas ellas son
correctas, lo difícil no es programar orientado a objetos sino programar bien.
Programar bien es importante porque así nos podemos aprovechar de todas las
ventajas de la POO.
Cómo se
piensa en objetos
Pensar en términos de objetos es
muy parecido a cómo lo haríamos en la vida real. Por ejemplo vamos a pensar en
un coche para tratar de modelizarlo en un esquema de
POO. Diríamos que el coche es el elemento principal que tiene una serie de
características, como podrían ser el color, el modelo o la marca. Además tiene
una serie de funcionalidades asociadas, como pueden ser ponerse en marcha,
parar o aparcar.
Pues en un esquema POO el coche
sería el objeto, las propiedades serían las características como el color o el
modelo y los métodos serían las funcionalidades asociadas como ponerse en
marcha o parar.
Por poner otro ejemplo vamos a
ver cómo modelizaríamos en un esquema POO una
fracción, es decir, esa estructura matemática que tiene un numerador y un
denominador que divide al numerador, por ejemplo 3/2.
La fracción será el objeto y
tendrá dos propiedades, el numerador y el denominador. Luego podría tener
varios métodos como simplificarse, sumarse con otra fracción o número, restarse
con otra fracción, etc.
Estos objetos se podrán utilizar
en los programas, por ejemplo en un programa de matemáticas harás uso de
objetos fracción y en un programa que gestione un taller de coches utilizarás
objetos coche. Los programas Orientados a objetos utilizan muchos objetos para
realizar las acciones que se desean realizar y ellos mismos también son
objetos. Es decir, el taller de coches será un objeto que utilizará objetos
coche, herramienta, mecánico, recambios, etc.
caracteristicas
Clases
en POO
Las clases son declaraciones de
objetos, también se podrían definir como abstracciones de objetos. Esto quiere
decir que la definición de un objeto es la clase. Cuando programamos un objeto
y definimos sus características y funcionalidades en realidad lo que estamos
haciendo es programar una clase. En los ejemplos anteriores en realidad
hablábamos de las clases coche o fracción porque sólo estuvimos definiendo,
aunque por encima, sus formas.
Propiedades
en clases
Las propiedades o atributos son
las características de los objetos. Cuando definimos una propiedad normalmente
especificamos su nombre y su tipo. Nos podemos hacer a la idea de que las
propiedades son algo así como variables donde almacenamos datos relacionados
con los objetos.
Métodos
en las clases
Son las funcionalidades asociadas
a los objetos. Cuando estamos programando las clases las llamamos métodos. Los
métodos son como funciones que están asociadas a un objeto.
Objetos
en POO
Los objetos son ejemplares de una
clase cualquiera. Cuando creamos un ejemplar tenemos que especificar la clase a
partir de la cual se creará. Esta acción de crear un objeto a partir de una
clase se llama instanciar (que viene de una mala
traducción de la palabra instace que en inglés
significa ejemplar). Por ejemplo, un objeto de la clase fracción es por ejemplo
3/5. El concepto o definición de fracción sería la clase, pero cuando ya
estamos hablando de una fracción en concreto 4/7, 8/1000 o cualquier otra, la
llamamos objeto.
Estados
en objetos
Cuando tenemos un objeto sus
propiedades toman valores. Por ejemplo, cuando tenemos un coche la propiedad
color tomará un valor en concreto, como por ejemplo rojo o gris metalizado. El
valor concreto de una propiedad de un objeto se llama estado.
La programación orientada a objetos (OOP, por
las siglas inglesas de Object-Oriented
Programming) es una nueva forma de programar que
proliferó a partir de los años ochenta y trata de encontrar solución a estos
problemas utilizando los siguientes conceptos:
La programación orientada a objetos introduce
nuevos conceptos, que a veces no son más que nombres nuevos aplicados a
conceptos antiguos, ya conocidos. Entre ellos destacan los siguientes:
En la programación orientada a objetos pura
no deben utilizarse llamadas de subrutinas, únicamente mensajes.
Por ello, a veces recibe el nombre de programación sin CALL, igual que la programación estructurada se llama
también programación
sin GOTO.
Sin embargo, no todos los lenguajes orientados a objetos prohíben la
instrucción CALL (o su equivalente), permitiendo realizar programación híbrida,
procedimental y orientada a objetos a la vez.
Existen cuatro propiedades esenciales
soportadas por el paradigma orientado a objetos : Abstracción, Encapsulación, Herencia, y Polimorfismo. Juntos
representan un poderoso conjunto de aspectos, que pueden ser aplicados para
resolver un problema mediante una conveniente aplicación de estos aspectos, uno
puede construir una estructura para resolver problemas nuevos que usan
componentes (las clases) desarrolladas para soluciones previas del problema.
Consiste en la
extracción de las propiedades fundamentales de un concepto. Permite no
preocuparse por los detalles no esenciales, es decir, no es necesario conocer
los detalles exactos de la implementación. Existe casi en todos los lenguajes
de programación. Las estructuras de datos y los tipos de datos son un ejemplo
de abstracción.
Una abstracción
tiene existencia conceptual más bien que concreta. Representa ideas, conceptos,
y propiedades generales sin la atención a detalles. Para el software de
computadora, esto significa sin la atención a los detalles de implementación,
evitando así la necesidad de confundirse con la sintaxis del lenguaje o la
elección de un lenguaje. El único interés es que un lenguaje particular soporte
la abstracción.
La abstracción
es muy importante en las fases iniciales de una solución del problema (con el
que uno se se esta enfrentando), donde se hace un
intento para comprender el espacio del problema y las técnicas requeridas para
una solución. Aunque se distribuyen eventualmente con detalles, la abstracción
hace lo posible para delegar esos detalles y para organizar los a todos de una
manera manejable mediante el uso de abstracciones
de capa. Una abstracción de capa
es una abstracción que esta variando los niveles de detalle. En el nivel más
alto o primer nivel hay muy poco detalle. La abstracción del nivel más alto se
expresan desde el punto de vista de un número pequeño de abstracciones de
niveles más inferiores. La particionalidad continúa
en cada nivel hasta que todos los detalles han sido incluidos.
Es por lo tanto
la abstracción un modelo que incluye todas las capacidades esenciales,
propiedades, o los aspectos que están siendo modelado
sin algunos detalles extraños.
Hay varios
tipos importantes de abstracción, dependiendo sobre qué esta siendo modelado.
Por lo tanto la
abstracción se define como la extracción de las propiedades esenciales de
un concepto. En un programa estructurado, es suficiente conocer que un
procedimiento dado realiza una tarea específica. El cómo se realiza la tarea no
es importante; mientras el procedimiento sea fiable, se puede utilizar sin
tener que conocer como funciona su interior. Esto se conoce como una
abstracción funcional. Con la abstracción de datos, las estructuras de datos e
ítems se pueden utilizar sin preocuparse sobre los detalles exactos de la
implementación.
En la POO el
polimorfismo se refiere al hecho de que una misma operación puede tener
diferente comportamiento en diferentes objetos. En otras palabras, diferentes
objetos reaccionan al mismo mensaje de manera diferente.
Por ejemplo,
supongamos un número de figuras geométricas que responden todas al mensaje
Dibujar. Cada objeto reacciona a este mensaje visualizando su figura en la
pantalla. Obviamente, el mecanismo real para dibujar los objetos difiere de una
figura a otra, pero todas las figuras realizan esta tarea en respuesta al mismo
mensaje.
Polimorfismo se
define como la calidad o estado de ser capaz de asumir formas diferentes. En la
solución de un problema orientado a objetos, polimorfismo puede aplicarse a
cualquier objetos u operaciones. El uso más común es
la operación polimorfismo, que es representada por enviar el mismo mensaje,
imprimirlo, a objetos diferentes y cada uno puede ser responder en su propia
manera.
Polimorfismo
puede examinarse desde el punto de vista de sus propiedades suplementarias. Una
primera propiedad del polimorfismo es el sobrecarga de
identificadores de mensaje y operadores. Polimorfismo es apoyado por la
ligadura de un método particular al identificador del mensaje durante la
ejecución de un sistema de software. Esta ligadura lenta, o ligadura dinámica,
es un aspecto importante de la solución de un problema orientado a objetos.
Una de las
características más importantes de la programación orientada a objetos es la
capacidad de que diferentes objetos responden a órdenes similares de modo
diferentes.
Identificador de mensaje y operadores sobrecargados. El
identificador de mensaje y operador invocan una operación específica sobre un
objeto. Cada uno establece la selección del significado uniforme de la
operación en particular que se desea realizar, es decir, son utilizados para
representar el concepto de una operación e identificar que se ejecutara o
realizará.
Es la propiedad
que permite a los objetos construirse a partir de otros objetos. Este principio
consiste en que cada clase puede dividirse en subclases, es decir, a partir de
una clase base se pueden derivar otras clases (clases derivadas) que comparten
características comunes con la clase de la que se derivan, además de tener sus
propias características particulares.
La Herencia
permite definir nuevas clase a partir de clases ya existentes. Si una clase
sólo recibe características de una clase base, la herencia es simple.
Si una clase
recibe propiedades de más de una clase base, la herencia es múltiple.
Las instancias
heredan (usualmente) todas, y únicamente, las características de las clases a
las que pertenecen, pero, también, es posible, en un sistema orientado a
objetos, que se permita que las clases hereden características de superclases
más generales. En ese caso, las características heredadas pueden ser ignoradas
(overriden) y se pueden agregar características
adicionales para tratar excepciones.
La herencia es
el acto de adquirir una posesión, condición, o característica de generaciones
pasadas. En la solución de un problema de computadora se habla de componentes
de software que heredan propiedades que describen otros componentes de
software. En la solución de un problema orientado a objetos un tipo de objeto
hereda propiedades que caracterizan otro tipo de objeto. Desde las propiedades
de objetos son dadas por la descripción de la clase, esto implica una jerarquía
de clases, donde una clase es un subclase de otra, la
clase padre. Los objetos que son los instancias de las
subclases tienen propiedades dadas dentro de la descripción de la subclase así
como también propiedades heredadas dadas dentro de la clase padre y todas las
clases antecesoras.
Así la herencia
provee la potencialidad para construir soluciones nuevas a problemas agregando
el incremento de la capacidad a soluciones existentes del problema mediante
subclases.
Las instancias
de una subclase representan una especialización de instancias descritas por una
clase padre. La instancia de la subclase tiene todos los atributos dados por la
clase padre, más los atributos adicionales o agregados de la subclase. La
instancia de la subclase responde al mismo conjunto de mensajes dados en la
clase padre, los mensajes adicionales se dan en la descripción de la subclase.
La respuesta de
la instancia de la subclase a los mensajes en la clase padre puede ser
diferente de la respuesta de una clase padre de una instancia al mismo mensaje.
No es valido considerar subclases de objetos que tengan menos atributos que los
objetos descritos por la clase padre.
Concepturalización
de la herencia
Una clase
utilizada para derivar nuevas clases se conoce como clase base (padre,
ascendiente), y una clase creada de otra clase se llama clase derivada (hija,
descendiente, subclase).
En un lenguaje
Orientado a Objetos la herencia se manifiesta con la creación de un tipo
definido por el usuario (Clase), que puede heredar las características de otra
clase ya existente o derivar las suyas a otra nueva clase. Cuando se hereda,
las clases derivadas reciben las características (estructuras de datos y
funciones) de la clase original , a las que se pueden
añadir nuevas características o modificar las características heredadas.
La herencia se
aplica para extender y reutilizar el código existente:
Herencia simple :
Se realiza
tomando una clase existente y derivando nuevas clases de ella (Figura 2.5 ). La clase derivada hereda las estructuras de datos y
funciones de la clase original, Además, se pueden añadir nuevos miembros a las
clases derivadas y los miembros heredados pueden ser modificados. Una clase
utilizada para derivar nuevas clases se denomina clase base (padre, superclase,
ascendiente). una clase creada de otra clase se
denomina clase derivada o subclase. A su vez una clase derivada puede ser
utilizada como una clase base para derivar más clases. Por consiguiente, se
pueden construir jerarquías de clases, en las que cada clase sirve como padre o
raíz de una nueva clase
Herencia Múltiple :
Es aquella en
la cual una clase derivada tiene más de una clase base. Aunque el concepto de
herencia múltiple es muy útil, el diseño de clases suele ser más complejo, y en
ocasiones es preferible realizar la aplicación con herencia múltiple mediante
emulación de herencia simple.
Las estructuras
de datos y los detalles de la realización de un objeto se hallan ocultos de
otros objetos del sistema. La única forma de acceder al estado de un objeto es
enviar un mensaje que haga que uno de los métodos se ejecute. Estrictamente
hablando, los atributos son escrituras taquigráficas para los métodos que
obtienen y colocan valores. Esto hace que los tipos de objetos sean
equivalentes a los tipos de datos abstractos en programación, en términos
generales.
El resultado de
encapsulación es una entidad con fronteras distintas,
una interface bien definida, y una representación
interna protegida. Para el software de computadora, una encapsulación
es un componente de software. La integridad del componente de software como una
encapsulación es dependiente de aspectos del lenguaje
de computadora en el que se implementa el componente.
Encapsulación es un concepto importante para el desarrollo de
soluciones del problema que son menos susceptibles a los errores. Un problema
es particionado en un número de componentes. Cada
componente es encapsulado para interactuar recíprocamente con los otros
componentes únicos de manera cuidadosamente prescribidas,
como definidas por su interface.
En un problema
orientado a objetos que resuelve la unidad de encapsulación
es el objeto. Los objetos son abstracciones encapsuladas.
Combinación de principios
La encapsulación es la combinación de los principios de
ingeniería de software de modularidad,
localización, y ocultamiento de información. Cada una de estas es muy
importante para el desarrollo de un sistema eficiente, formal y sostenible.
- Los módulos
- Son creados para abstracciones del objetto más que abstracciones funcionales
- Las operaciones
- Se deben asignarse y encapsularse dentroo del objeto y clase.
Una de las
características fundamentales de una clase es ocultar tanta información como
sea posible. Por consiguiente es necesario imponer ciertas restricciones de
acceso a los datos y funciones de una clase.
Una clase puede
contener partes públicas y partes privadas. Por defecto, todos los miembros
definidos en una clase son privados, aunque se puede especificar la palabra
reservada private. Si se desea hacer algunas o todas la partes de una clase públicas, es necesario
definirlas después de la palabra reservada public.
Todas los datos o funciones
definidos como públicos pueden ser accesados
solamente por funciones de la propia clase. No así aquellas que se definen como
publicas, que pueden ser accesadas desde cualquier
función del programa.
Dada que una
característica clave en la Programación Orientada a Objetos es el ocultamiento
de información, se debe tratar de eliminar o limitar al máximo tener variables
públicas. Las funciones de una clase, sin embargo, generalmente se hacen
privadas para poder manipular al objeto desde el lugar del programa que el
programador desee. Ver ejemplo clase rebanada.
Existen tres
clases de usuarios de una clase: la propia clase, usuarios genéricos y las
clases derivadas. Cada uno de ellos tiene diferentes privilegios de acceso que
se asocia con las palabras reservadas:
1.- private ------------- La propia clase
2.- public -------------- Usuarios genéricos
3.- protected ---------- Clases derivadas.
Declaración de variables y funciones miembro de una clase
class A {
int x;
float y;
public:
int z;
void f();
};
void main(void)
{
A vardemo;
}
vardemo es un objeto de tipo
<> que se compone de tres campos de datos (x, y, z) y de una función de
acceso (f). Los campos x,y
sólo podrían ser modificadas a través de la función f, para lo cual se
escribirá: vardemo.f();
Esto en
términos de programación orientada a objetos se leería: se envía el mensaje f() al objeto vardemo. En otras
palabras: se ejecuta la función f aplicada al objeto vardemo.
La definición
de las funciones miembro es muy similar a la definición ordinaria, ya conocida,
de función. Tienen un encabezado y un cuerpo y pueden tener tipos y argumentos.
sin embargo tienen dos características muy especiales:
Cuando se
define una función miembro, se utiliza el operador de resolución de ámbito :: para identificar la clase a la que pertenece la función.
Las funciones miembro (métodos) de las clase pueden acceder a los componentes
privados de la clase.
class CLS {
int x;
int y;
public:
void f() { cout << "\nX=" << x
<< " Y=" << y; }
};
class CLS {
int x;
int y;
public:
void f();
}
void CLS::f()
{
cout << "\nX=" << x << " Y="
<< y;
}
En el primer
caso la función está en línea (inline). Por cada
llamada a esta función, el compilador genera (vuelve a copiar) las diferentes
instrucciones de la función. En el segundo caso la función se llamará con una
llamada verdadera a función.
Una estructura
(struct) en C++ es también una clase en la que todos
sus miembros son por defecto públicos, a menos que sean modificados por las
palabras reservadas private o protected.
struct alfa {
private:
int x, y;
public:
//resto de la
estructura, prototipos de funciones
}
Objetos
En C++, un
objeto es un elemento declarado de un tipo de clase. Se conoce como una
instancia de una clase.
class rectangulo
{
int base, altura;
public:
void dimensiones (int b, int h) { base=b; altura=h; }
int area(void) { return base * altura; }
};
main ()
{
rectangulo MiRectangulo;
//Se declara el objeto
MiRectangulo.dimensiones(10, 5); //Se define el
tamaño del objeto
cout << "Area= " << MiRectangulo.area();
//Se calcula el área del objeto y se imprime
rectangulo * ap_TuRectangulo
= new rectangulo; //Se
declara apuntador al obj.
ap_TuRectangulo->dimensiones(10,
20); //Se define el tamaño del objeto
cout << "Area= " << ap_TuRectangulo->area(); //Se calcula el área del obj. se
imprime
}
Puede
observarse en el ejemplo anterior que los miembros a una clase se accesan por medio del operador punto (.) si se trata de una
variable de una variable (objeto) del tipo de la clase y por el operador flecha
(->) si se trata de un apuntador un objeto de la clase.
Clases vacías class vacia {};
con frecuencia en el
desarrollo de proyectos grandes, se necesitan comprobar implementaciones de
primeras versiones en las que algunas clases todavía no están totalmente
definidas o implementadas.
Una clase
declarada en el interior de otra clase se denomina clase anidada, y se puede
considerar como una clase miembro. Si una clase anidada es declarada en la
sección private de la clase circúndate, será solo
utilizable por miembros de datos de la clase
que la circunde. Si un
nombre de una clase anidada es accesible a una clase o función que no la
circunda, se debe aplicar el operador de resolución de ámbito (::) para
utilizar su nombre.
class Externa {
public:
class Interna {
public:
int x;
};
};
void main(void)
{
Externa::Interna valor;
int v = valor.x;
}
class clase_2 {
public:
int i;
};
class clase_1{
public:
int j;
clase_2
variable_2; //variable_2 es una clase anidada
};
void main(void)
{
clase_1
variable_1;
variable_1.variable_2.i=125;
}
Miembros
estáticos de una clase
Para un miembro
dato, la designación static significa que sólo existe
una instancia de ese miembro. Es compartido por todos los objetos de una misma
clase y existe incluso si ningún objeto de esa clase existe. Se le asigna una zona
fija de almacenamiento.
class Ejemplo {
static int
varest; //se declara miembro estático privado
public:
static int
valor; //se declara miembro estático público
};
int Ejemplo:: valor; //se define miembros
estático
int Ejemplo::varest;
void main(void)
{
Ejemplo objeto1;
objeto1.valor=1;
objeto1.valor=3;
Ejemplo::valor=3;
//se puede referenciar usando el identificador de clase. Es la manera ideal
de hacerlo
Ejemplo::valres=5;
//Acceso válido
objeto1.valres=6; // Acceso no válido
}
Tipos de funciones miembro
1.- Simples
2.- Estáticas
3.- const
4.-volatile
5.- inline
6.- const this
7.- volatile this
8.- Virtuales
9.- Especiales:
constructor y destructor
10.- Amigas
11.- opeator
Dentro de una
función miembro, this apunta al objeto asociado con
la invocación de la función miembro. A través de este apuntador, una función
puede saber qué objeto la está llamando.
complejo::complejo(float 1, float b)
{
this->r=a;
this->i=a;
}
Aunque su uso
se considera redundante e innecesario, puede ser útil en ciertas situaciones,
como por ejemplo para hacer una copia del objeto asociado con la invocación o
asignar un nuevo valor al objeto.
void clase_x
:: funcion_x(clase_x&
a, clase_x &b)
{ ...
a=*this; //Asigna el valor del objeto asociado al objeto a
...
*this=b; //Modifica el valor del objeto asociado
}
Acceso al
objeto mediante this
this -> nombre_miembro
*this es el objeto total real
this es la dirección del
objeto apuntado.
Sólo pueden accesar a otras funciones y datos estáticos declarados en
una clase, pero no pueden manipular funciones ni datos no estáticos, debido a
que no tienen asignado un apuntador this, a menos que
se pase explícitamente este apuntador this.
int v1, v2, v3; //Al ser globales son
estáticas
class Prueba{
public:
static void suma():
};
void Prueba::suma() { v1=v2+v3; }
void main(void)
{ Prueba p1;
p1.suma(); Prueba::suma(); }
Devuelven
objetos del tipo const.
class FunConst{
public:
const int f() { return 5; }
}
void main()
{ FunConst s; cons int i=s.f(); int y=s.f(); }
Al igual que
cualquier otra función de C++, las funciones miembro pueden ser funciones en
línea. Existen dos medios para hacer funciones miembro
en línea: aplicando la palabra reservada inline a la
definición o definiendo la función dentro de la declaración de la clase.
class clase_xy
{
int x,y;
public:
void Despliega_x
(void) { cout <<
"X= " << x; } //Función miembro en
línea
void Despliega_y(void);
};
inline void
clase_xy::Despliega_y(void) //Función miebro en línea
{
cout << "Y= " << y;
}
En C++ la
inicialización de objetos no se puede realizar en el momento en que son
declarados. Para ello se dispone de los constructores. Un constructor es una
función que sirve para construir un nuevo objeto y/o inicializarlo, asignando
valores a sus miembros dato.
Características
de los constructores:
- Tiene el
mismo nombre que la clase que inicializa.
- Puede
definirse inline o fuera de la clase que inicializa.
- No devuelven
valores
- Puede
admitir parámetros como cualquier otra función.
- Pueden
existir cero, uno o más constructores para una misma clase.
class rebanada {
int x_centro, y_centro, angulo_i, angulo_f, radio;
public:
rebanada(int x, int
y, int ai, int af, int
r) } //Constructor
x_centro=x; y=centro=y; angulo_i= ai; angulo_f=af;
radio=r;
}
...// Otros
métodos
}
Los
constructores pueden implicar diferentes escenarios: Crear objetos por
inicialización por defecto, crear objetos con inicialización específica, crear
objetos por copia de otro objeto.
Los
constructores no se pueden declarar static o virtual.
Se declaran normalmente en la sección pública, pero pueden declararse en
cualquier parte de una clase. Si no se declara específicamente un constructor,
C++ inserta automática e invisiblemente un constructor por defecto que no
acepta argumentos. Este constructor por defecto asigna espacio de
almacenamiento para construir un objeto de su clase y lo pone en ceros.
Constructores con argumentos
La mayoría de
los constructores en C++ toman argumentos. La función básica de un constructor
es inicializar un objeto antes de utilizarlo. Se pueden declarar constructores
múltiples, mientras tomen tipos o números de argumentos.
class complejo {
private:
double real,imag;
públic:
complejo() { real=0; imag=0 } //son constructores sobrecargados
complejo(double x) { real=x; imag=0; }
complejo( double x, double y ) { real=x; imag=y }
complejo(complejo& c) {real=c.real; imag=c.omag}
//constructor copiador
}
void main(void)
{
complejo c1;
complejo c2(1.2);
complejo c3(1.2, 2.4);
complejo c4=c2; //llama al
constructor copiador
}
Mecanismo alternativo de paso de argumentos
Consiste en
inicializar miembros dato como parámetros. Los constructores de complejo
quedarían:
complejo(double x=0,
double y=0) : real(x), imag(y) { }
complejo(complejo& c): real(c.real), imag(c.imag)
{ }
Constructores sobrecargados
Un constructor
puede tener el mismo nombre pero diferentes tipos de datos como argumentos. El
constructor correcto es llamado por el compilador según sean los datos de sus
argumentos. Ver ejemplo complejo.
Constructores copiadores
Es un
constructor que crea un objeto a partir de uno ya existente. Tiene sólo un
argumento: una referencia constante a un argumento de la misma clase.
class fibonacci
{
public:
fibonacci() { var1=0; var2=1; resultado= var1+var2; }
//constructor ordinario
fibonacci( const
fibonacci &p ) { var1=p.var1;
var2=p.var2; resultado=p.resultado;
}
}
Paso de objetos por valor
Cuando se
llama a una función que recibe a un objeto por valor, el compilador utiliza el
constructor de copias de los objetos en la pila como un argumento. El paso por
valor no significa que la función obtenga necesariamente una copia byte a byte de un objeto.
void lee_complejo(
Cuenta objeto)
{
cout << "El
objeto Cuenta es: " << objeto.LeerValor());
}
void main(void)
{
Contador objeto(10);
informe_cuenta(objeto);
}
Creación de
objetos
Forma
abreviada: Complejo c1, c2(10), c3(3.4); Complejo c4=c1;
Forma
explícita: Complejo c1=complejo(); Complejo c2=complejo(10);
Complejo
c3=complejo(3.4);
Un destructor
es una función miembro con igual nombre que la clase, pero precedido por un caracter tilde ^ .Una clase sólo tiene una función
destructor, no tiene argumentos y no devuelve ningún tipo. Al igual que las
demás funciones miembro puede estar definido dentro o fuera de la clase.
class String
{
private:
unsigned lon;
char *sstr;
public:
String(const char* s) { strcpy(sstr, s); }
//constructor
^String()
{ delete[] sstr; }
//destructor
// Otros
miembros
}
Los
destructores pueden ser públicos o privados, según si se declaran en la parte private o public de la clase. Si
es público se puede llamar desde cualquier parte del programa para destruir el
objeto. Si es privado no se permite la destrucción del objeto por el usuario.
Creación y supresión dinámica de
objetos
Los operadores
new y delete se pueden usar
para crear y destruir objetos de una clase, así como dentro de funciones constructoreas y destructoras. la
expresión new devuelve un apuntador a la dirección
base del objeto.
p=new int(9);
Cadena *Cad2 = new Cadena;
Un objeto
creado con new se destruye explícitamente mediante el
operador delete. Si new
falla al asignar almacenamiento, devuelve el valor 0.
class cadena {
char *datos;
public:
cadena (int lon)
{ datos= new char [lon]; } //constructor
^cadena(void) { delete
datos}; } //destructor
}
Argumentos por defecto (omisión)
En C++, se
pueden especificar los valores por defecto cuando se proporciona un prototipo
de una función. Estos argumentos por defecto son proporcionados al compialdor si no se da ningún tipo de argumento a la
llamada a la función. Si se pasa un valor a uno de los argumentos, se utiliza
ese valor, si no se utiliza el valor por defecto como argumento. Deben
cumplirse las siguientes reglas:
- Los
argumentos por defecto se pasan por valor, no por referencia.
- Los valores
por defecto pueden ser valores literales o declaraciones const. No pueden
ser variables.
- Todos los
argumentos por defecto deben estar situados al final del prototipo de la
función. Después del primer argumento, todos los demás
argumentos deben incluir
valores por defecto.
void Visualizar (int n, int base = 10 ) { cout << n << "," << base; }
v
void Visualizar (int n, int base = 10 ) { cout << n << "," << base; }
void f()
{
Visualizar(47);
Visualizar(47,10);
Visualizar(50, 16);
}
Otro ejemplo:
void funcion(int i, int j=2,
k=3, h=4);
void main()
{
funcion (1);
funcion (0, 1);
}
void funcion(int i, int j, int k, int h) { cout
<< i << "," << j << "," << k
<< "," << l; }
Lenguajes
Desarrollo de los Lenguajes
de Programación.
Con la idea de
facilitarnos las tareas que debemos de desempeñar los humanos, hemos venido
inventado diversas herramientas a lo largo de nuestra historia, que nos permiten
tener una mejor calidad de vida.
Los ordenadores
son uno más de los inventos del hombre, aunque debemos decir que las
tecnologías para su fabricación y explotación han tenido un desarrollo
sorprendente a partir de la segunda mitad del siglo XX. Esta herramienta por sí
sola no es capaz de efectuar ninguna tarea, es tan sólo un conjunto de
cables y circuitos que necesitan recibir instrucción por parte de los humanos
para desempeñar alguna tarea. El problema entonces, se puede fijar en ¿cómo
vamos a poder hacer que un conjunto de circuitos desempeñen una determinada
tarea y nos entreguen los resultados que nosotros esperamos?, es decir, ¿de qué
manera se puede lograr la comunicación entre el hombre y el ordenador?.
Así pues, tratando
de dar una solución al problema planteado, surgieron los lenguajes
de programación,
que son como un lenguaje cualquiera, pero simplificado y con ciertas normas,
para poder trasmitir nuestros deseos al ordenador.
Por otro lado,
como se sabe, un conjunto de circuitos no entendería ningún lenguaje que
nosotros conozcamos, por más sencillo que éste parezca. Los circuitos en todo
caso, sólo reconocen presencia o ausencia de energía, es decir que debemos
hablarle a la máquina en su propio lenguaje (presencia y ausencia de energía, 0
y 1), o nuestro lenguaje deberá de ser traducido a un lenguaje binario cuyo
alfabeto es el 0 y el 1, mediante las herramientas desarrolladas para llevar a
cabo esta tarea, las cuales reciben el nombre de traductores, y como veremos
más adelante, los hay de muchos tipos, dependiendo de características más
específicas del lenguaje a traducir y de la manera de llevar a cabo su
traducción.
Como ya habréis
entendido, para crear un lenguaje de programación, deberemos crear la herramienta
que lo traduce, y es justamente de ellas, de las que hablaremos a continuación,
para describir como han ido evolucionando en los últimos 50 años [BYTE 95].
A partir de los
años sesenta, empiezan a surgir diferentes lenguajes de programación, atendiendo a
diversos enfoques, características y propósitos, que más adelante
describiremos. Por lo pronto, puede decirse, que actualmente existen alrededor
de 2000 lenguajes
de programación
[KINNERSLEY 95] y continuamente, están apareciendo otros más nuevos, que
prometen hacer mejor uso de los recursos computacionales y facilitar el trabajo
de los programadores.
Tratando de resumir
un poco, presentaremos los siguientes cuadros evolutivos, donde aparecen los lenguajes
que por su uso y comercialización, han resultado ser los más populares a
lo largo de este medio siglo. [LABRA 98] [RUS 01]
Figura a. Evolución de los Lenguajes Imperativos y
Orientados a Objetos
Figura b. Evolución de los lenguajes declarativos
Como ya lo citamos
anteriormente y como se puede observar en las figuras a y b, la existencia de
tantos lenguajes
obedece a que cada uno de ellos está encaminado a resolver ciertas tareas, dentro
de la amplia problemática de la explotación de la información, o bien, a que su
arquitectura, o su forma de llevar a cabo la programación, tiene un
enfoque particular.
De acuerdo con el
estilo de programación, podemos clasificar los lenguajes en las
siguientes categorías:
Como un ejemplo ilustrativo vamos a escribir un programa en un lenguaje
de este tipo para calcular el factorial de un
número positivo x.
READ(x);
fac := 1 ;
for i = 1 to x
{
fac := fac * i ;
}
WRITELN(fac);
SELECT * FROM alumnos WHERE
sexo = "M" ORDER BY edad
Dentro de este paradigma, se encuentran dos estilos distintos de programación,
cada uno de los cuales posee su propia lógica [SANFÉLIX 00].
fac :: Integer -> Integer
fac 0 = 1
fac x = x * fac (x-1)
factorial (0, 1)
factorial (X, Fac) :- Y is
X-1, fac(Y, F2), Fac is F2 * X .
Además de estos elementos fundamentales, también existen otros 3 elementos
secundarios , que aunque son deseados, no son
indispensables para clasificar un lenguaje dentro de este estilo.
Ahora bien, si
tomamos como referencia las herramientas usadas en el proceso de traducción y
ejecución de los programas esbozada en la figura 2, vamos a tener la siguiente
clasificación de lenguajes[AHO 77]:
Programa
Fuente
Traductor
Programa Objeto
Escrito por el Programador Programa que Logra Es el que
Entiende
la
el
Entendimiento
Máquina
Figura 2
Finalmente,
existen otros conceptos tomados en cuenta para agrupar los lenguajes, que dan origen
a diversas clasificaciones, entre los que destacan las siguientes:
Esta gran cantidad
de lenguajes, señala
de manera clara que existe un esfuerzo continuo en la creación, y mejora de los
lenguajes de programación, en
aras, de hacer más fácil la tarea del programador y/o hacer un uso más
eficiente de los recursos computacionales.
La búsqueda de los
objetivos antes mencionados, así como la guerra mercantil de las
compañías dedicadas a la producción de herramientas de software, han
diversificado las opciones que los programadores pueden elegir. Sin embargo , hasta nuestros días, podemos decir que realmente
no existe ningún lenguaje, o grupo de ellos, que destaque en la totalidad de
las aplicaciones informáticas que se desarrollan actualmente, ya que cada uno,
tiene cualidades que lo hacen más convenientes para algunos propósitos,
pero al mismo tiempo, cuentan con inconvenientes para otros.
|
Tipos de lenguajes
Desde
un punto de vista más general los lenguajes se pueden clasificar en lenguajes de procedimiento o declarativos. En los primeros, con el
lenguaje se especifica paso a paso el procedimiento que ha de realizar el
ordenador para procesar la información, mientras que en los segundos se
declaran hechos que han de dirigir las respuestas del ordenador. El PASCAL y el C que estudiaremos en
este curso son de procedimiento,
mientras que por ejemplo el PROLOG, es declarativo. Una porción de programa
PROLOG es así:
. . .
hijo(X,Y) <- padre(Y,X) , varon (X).
hija(X,Y) <- padre(Y,X) , hembra (X).
abuelo(X,Z) <- padre(X,Y) , padre (Y,Z).
. . .
estableciendo relaciones lógicas que determinan una base de verdades con las que han
de ser coherentes las respuestas del programa. Exite
una diferencia grande en la programación con un lenguaje u otro según sea interpretado o compilado, si bien esta distinción
puede no ser inherente al lenguaje sino a su puesta en práctica en un
determinado ordenador. Un lenguaje es interpretado
cuando la transformación de las instrucciones de alto nivel a lenguaje máquina
se realiza sentencia a sentencia según se van ejecutando. Un lenguaje es compilado cuando esta trasformación
se realiza en bloque antes de que ninguna instrucción sea ejecutada. Por
ejemplo, el BASIC en general se suele interpretar y
el LISP siempre. En un lenguaje interpretado la puesta a punto de un programa
ha de realizarse secuencialmente puesto que las
distintas partes del programa no se pueden verificar hasta que entran en
ejecución. En el caso más común de lenguajes compilados, son varios los
subprocesos implicados en la transformación del código de alto nivel en
instrucciones ejecutables por la UCP.
A su vez el compilador realiza varias tareas:
Si un programa es
incorrecto sintácticamente, el compilador detectará
el error y lo comunicará con un mensaje relacionado con la regla del lenguaje
que se ha incumplido. Normalmente, estos son los errores más comunes y más
fáciles de detectar. El montador (Linker) unifica el código con el proveniente de otros
subprogramas con el que se intercambian datos. Para ello realiza una lista de
los datos que comparten todos los programas y asigna las direcciones comunes
donde cada uno deberá procesar esos datos. Esta unificación de direcciones será
imposible si algún procedimiento supuestamente existente en otro subprograma no
aparece o aparece de un modo no unívoco. También será causa de error que algún
dato compartido por subprogramas esté declarado de modo distinto en cada
programa. Estos errores son poco comunes y fácilmente detectable
con los mensajes de error proporcionados por el montador. Sin embargo, los
errores más comunes y más tediosos de eliminar son aquellos de programación que
dan lugar a sentencias sintácticamente correctas pero
que corresponde a acciones distintas a las deseadas. Desgraciadamente sólo se
detectan en la ejecución del programa. En la actualidad existe la posibilidad
de utilizar depuradores de programas (llamados en inglés debuggers por el origen de los
errores en los ordenadores primitivos ) que permiten
seguir la ejecución paso a paso de un programa aunque se obtenga el código máquina
por compilación. Esta herramienta facilita enormemente la depuración de los
programas pues permite conocer o modificar el valor de los datos manipulados en
el programa durante la ejecución.
|
Lenguajes
orientados a objetos
En
un sentido general se puede considerar la
orientación
a objetos
como un marco para la ingeniería
del software basado en objetos
y clases. Abarca desde los principios del análisis de un problema hasta
el
final de su implementación y su dominio de aplicación también es muy amplio.
Según varios
autores, el interés por la OO surgió en el contexto de la crisis del software
de los años 70 (la falta de
reusabilidad de software). Al hablar de
la OO, se suelen identificar las siguientes ventajas:
Desarrollo
rápido de sistemas.
Mejora
en la calidad y legibilidad del código.
Facilidad
de mantenimiento.
Aprovechamiento
del poder de los LPOO.
Reusabilidad de software y diseños.
Producción
de sistemas más resistentes al cambio.
La
POO y los LPOO juegan un papel importante dentro de las tecnologías OO. Según
la literatura, el
término
objeto
emergió paralelamente
en
varios campos de la Informática a principios de los años 70,
para hacer referencia a nociones superficialmente distintas aunque
relacionadas. La identificación de
la
importancia de la composición de sistemas en niveles de abstracción, la
ocultación de información y
el
desarrollo de mecanismos de tipos de datos abstractos en los años 70 tuvieron
una gran influencia
en
el desarrollo de la POO, aunque existe cierta polémica sobre como exactamente
estos avances
dieron lugar a lo que hoy en día se considera como POO. El LPOO Simula
apareció en 1962 (y más
tarde Simula67 en 1967) y, aunque no fue muy utilizado, ha sido reconocido como
el primer LPOO,
incorporando los conceptos de clase y objeto.
El
concepto de
POO
propiamente dicho fue presentado por Alan Kay, uno de los
inventores de
Smalltalk (el primer LPOO popular), algunos años más tarde:
Todo
es un objeto que almacena datos y al que se le puede hacer peticiones.
Un
programa es un conjunto de objetos
que intercambian mensajes.
Cada
objeto tiene su propia memoria que está compuesta por otros objetos.
Cada
objeto tiene un tipo de mensajes que puede recibir y procesar.
Todos
los objetos de un cierto tipo
pueden procesar los mismos mensajes.
Se
trata de una caracterización muy general que no se puede aplicar a muchos de
los LPOO más
utilizados hoy en día. Smalltalk tenía estas
características y fue concebido con el objetivo de ser un
LPOO
dinámico, que permitiera la adición de nuevas clases, objetos y comportamiento sobre la
marcha. En las actas del congreso HOPL II editadas en 1993 por la ACM (
Association
of Computing
|
POO
en
términos de una célula que permite el flujo de información en las
dos direcciones, pero en la cual lo que está dentro está oculto desde
fuera. En 1985 apareció el LPOO
Eiffel, diseñado para mejorar la productividad y calidad de programas OO, pero
no fue muy utilizado.
Para
que la POO se estableciera como un paradigma era necesario que los
programadores lo
adoptaran. Por eso fue muy efectiva la modificación de un LP ya existente para
incorporar los
conceptos (y beneficios) de la POO, sin perder la posibilidad de reutilizar
código fuente, como ocurrió
con C++ (que es una extensión de C que incluye los conceptos OO). Otros LP
han sido expandidos
también para incorporar estos conceptos: Modula2 se convirtió en Modula3, Ada
en Ada95, Lisp en
CLOS
(
Common Lisp Object System
)
vía Flavors, COBOL en Object
COBOL, etc. Como ejemplos de
LPOO
de nueva creación
se
pueden destacar Python, Java y C#.
Actualmente se pueden identificar
unos 140 LPOO que se usan de alguna forma u otra.
Otra
manera de ver la POO es como la evolución natural de la programación
imperativa, desde
la
programación sin estructura, pasando por la programación procedimental
y modular. En primer
lugar, la
programación
sin estructura
es
la más sencilla y cada programa consiste en una secuencia de
instrucciones que operan sobre datos globales o comunes a todas las partes del
programa. Lo que
ocurre es que, según va creciendo el programa, van surgiendo problemas. Por
ejemplo, si se necesita la
misma secuencia de instrucciones en varias partes del programa, hay que
copiarla. Para evitar este
problema, se empezaron a extraer estas secuencias, a darles un nombre, y a
ofrecer una técnica para
llamarlas y devolver el flujo de control desde ellas al programa principal junto
con los resultados. Así
aparecieron los procedimientos y funciones y la
programación procedimental
.
Con la incorporación
del paso de parámetros y procedimientos dentro de otros, se podían escribir
programas con más
estructura y menos probabilidades de errores. Así, en vez de ver un programa como
una secuencia de
instrucciones, se podía contemplar como una secuencia de llamadas a procedimientos.
La extensión
natural de este tipo de programación consistió en agrupar en módulos
procedimientos comunes a
varios programas, y así surgió la
programación
modular
En ella cada módulo tiene sus propios datos
y
estado, que se modifican con las llamadas al módulo. Por último, como se verá a
continuación, la
POO
soluciona algunos de los problemas de la programación modular: puede haber simultáneamente
múltiples versiones de un mismo objeto y cada una es responsable de su propia
creación y
destrucción.
Definición de POO y caracterización de los LPOO
Es
muy difícil definir la POO y listar todos los LPOO existentes porque hay
diferencias de opinión
sobre lo que significa exactamente el término POO y sobre cuáles son las
características de un lenguaje
de
este tipo. Sin entrar en este debate, basta proporcionar una definición general
de POO y a
continuación describir las características principales que se suelen relacionar con
los LPOO, para
comprender sendos conceptos
Definición
de POO
Método
de implementación en el que los programas están organizados como
colecciones de objetos, donde cada
uno es una instancia de alguna clase, y donde
todas las clases son miembros de una jerarquía de clases conectadas por
relaciones de herencia.
Una
vez definida la POO se pueden identificar las características principales que
definen a los LPOO
según la mayoría de los autores:
1
El
significado de los términos que se incluyen tanto en la definición como en la
caracterización se explican en las
secciones siguientes.
1.
La base de objetos y clases
2.
El encapsulamiento y la ocultación de información
3.
Las relaciones entre objetos: la
agregación y la herencia
4.
El ligamiento dinámico y el polimorfismo
5.
La interacción basada en el intercambio de mensajes entre objetos
A
continuación se procede a explicar cada una de estas cinco propiedades.
1. La base de objetos y clases
Objetos
Los
objetos
son la base de la POO. El mundo está lleno de objetos: el perro, la mesa, la televisión, la
bicicleta, etc. y todos ellos tienen dos características:
estado
y
comportamiento
.
Por ejemplo, los perros
tienen estado (raza, edad, sexo, color, nombre, etc.) y comportamiento
(ladran, muerden, saltan,
mueven la cola, etc.). También las bicicletas tienen estado (marca, número de
marchas, etc.) y
comportamiento (frenan, aceleran, etc.).
Definición
de objetos de
software
Modelos
de los objetos en el mundo real,
que también tienen estado,
representado por las variables, y comportamiento, representado por los
métodos (
método
es
una función asociada a un objeto), además de identidad.
Además,
se puede usar los objetos de
software para representar conceptos abstractos, como por
ejemplo, el evento generado por el sistema operativo cuando un usuario mueve el
ratón o presiona
una tecla del teclado.
Clases
Se
puede pensar en una
clase
como una plantilla, y en un objeto, como una
instancia
de
la clase.
Definición
de clase
Representación
de una estructura de datos abstracta junto con las operaciones
que se pueden realizar con ella.
Dentro
de lo que es el estado del objeto cabe distinguir entre las
variables de instancia
(datos distintos
en
cada instancia; por ejemplo, el color de la bicicleta) y las
variables de clase
(datos comunes a todas
las instancias; por ejemplo, el nombre de la empresa que construye todas
las bicicletas BH). También
debe hacerse la distinción entre
métodos de instancia
(que trabajan con el estado de una instancia de
la
clase) y
métodos de clase
(que funcionan igual en todas las instancias).
|
Ejemplo
La
clase Bicicleta sería la plantilla para representar todos los tipos
de
bicicleta y una instancia sería la bicicleta de alguien, con marca,
color, suspensión o no, y número de marchas específicas. Y aunque
todas las instancias de las bicicletas proceden de la misma clase,
son objetos independientes
con estados distintos.
public class Bicicleta {
private int no_ruedas = -1;
private int marchas = -1;
private static String marca = "Zipe";
public Bicicleta(int nr, int
nm){
no_ruedas = nr;
marchas = nm;
}
public int getNoRuedas(){
return(no_ruedas);
}
public static String getMarca(){
return(marca);
}
}
Variables
de
instancia
Variable
de
clase
Método
de
instancia
Método
de
clase
De
la misma manera que las empresas pueden aprovechar las características de las
bicicletas a la hora
de
construir otras nuevas (no sería eficiente empezar desde cero cada vez que se
quiere producir un
nuevo modelo), en la POO se puede aprovechar el hecho de que los objetos son de una cierta clase
para crear otros nuevos.
Como
una clase describe un conjunto de objetos
con características y comportamientos idénticos,
se
puede pensar en una clase como si fuera un tipo. La diferencia estriba en que
un programador
define una clase para un problema en concreto y no está obligado a usar un
tipo existente, vinculado
más bien con la estructura de la máquina que con el problema. Una vez
definida una clase, debe
representar una unidad de código útil que se pueda volver a usar en el futuro. No
es fácil de conseguir
al
principio, pero con la experiencia se pueden producir clases así.
Objetos vs. clases
En
algunos círculos se usa el término
objeto
solamente para hacer referencia a las instancias de una
clase y en otros, a la clase en sí. Sin embargo, para que pueda existir una
clase como instancia de otra,
debe introducirse el concepto de
metaclase
.
Definición de
metaclase
Clase
especial que se instancia para producir una clase. Cada
metaclase no tiene más que una
instancia que es la propia clase.
Se
pueden distinguir cuatro tipos de relaciones entre los términos objeto, clase y
metaclase en los
LPOO:
Comparación objeto/
clase/metaclase
Tipo
Elementos
Definición
LPOO
1
Objeto
Se
considera las clases como objetos
Self
2
Objeto
Clase
Los
objetos son instancias de las
clases pero no se puede
acceder a las clases desde los objetos
C++
3
Objeto
Clase
Metaclase
Los
objetos son instancias de las
clases y las clases de las
metaclases. Se puede acceder a las
clases desde los
objetos
Java
4
Objeto
Clase
Metaclase
Metaclase clase
Los
objetos son instancias de las
clases y las clases de las
metaclases. Se puede acceder a las
clases desde los
objetos. Aquí (a diferencia del tipo 3) la clase de
una
metaclase no es sí mismo.
Smalltalk
Info. y
Lenguaje C++
En la década de 1970 se volvió popular el
concepto de objeto entre los investigadores de los lenguajes de programación.
Un objeto es un conjunto de códigos, datos diseñados para emular o imitar una
entidad física o abstracta. Los objetos son eficientes como elementos de
programación por dos razones principales: representan una abstracción directa
de los elementos que se utilizan comúnmente y ocultan la mayor parte de la
complejidad de su implantación a los usuarios. Los primeros objetos que se
desarrollaron fueron aquellos que estaban más íntimamente ligados a las computadoras,
como INTERGER, ARRAY y STACK. Además se diseñaron lenguajes como el SmallTalk el cual es ya ortodoxo, donde todo bebía
definirse como un objeto.
Un lenguaje de programación orientado a
objetos debe proveer mecanismos suficientes para implementar todos los
conceptos de la POO de manera adecuada. C++, además de contar con dichos
mecanismos, contiene otras herramientas que permiten escribir programas en
diferentes estilos de programación. Por esta razón, el lenguaje C++ es muy
flexible para una amplia gama de aplicaciones.
La POO contiene conceptos muy diferentes a
los correspondientes en otros tipos programación. Por esta razón, cuando se
inicia el aprendizaje de un lenguaje orientado a objetos, especialmente C++, es
importante enfocarse en los conceptos y no perderse en los detalles técnicos
del lenguaje. El propósito de aprender POO es llegar a ser un mejor
programador, i.e., disenar e implementar exitosamente
sistemas nuevos y mantener los creados antes. Para lograr lo anterior, una
apreciación global de la metodología de programación es mucho más importante
que entender los detalles. Los detalles se aprenderán con el tiempo y la
práctica.
Actualmente existe un debate entre si es
mejor aprender C antes que C++. Algunas personas, entre ellos B. Stroustrup [7],
consideran que es mejor ir directamente a C++. La razón es que el lenguaje C es
de más bajo nivel y los conceptos de programación son muy distintos entre C y
C++. C++ es más simple y reduce la necesidad de enfocarse en las técnicas de
bajo nivel. Una vez aprendiendo C++, es fácil internarse en los detalles
técnicos de bajo nivel del lenguaje C.
Estas notas están organizadas de la siguiente
manera: en la Parte I se definen de manera ``formal'' los conceptos de la
programación orientada a objetos. En la Parte II se inicia con un vistazo
general por las herramientas que ofrece C++ para la programación. Después se
entra con más detalles en la implementación con C++ de los conceptos definidos
en la parte I. Finalmente, en la Parte II se dan algunos tópicos especiales, en
este primera versión se habla un poco de ingeniería de sofware.
En la mayoría de los casos no se profundiza
en cada uno de los conceptos presentados, pero se dan diferentes referencias en
donde es posible encontrar una revisión más detallada. Se recomienda acudir a
las referencias siempre que sea necesario. Por otro lado, los ejemplos tratan
de aclarar dichos conceptos y dar una idea general de como utilizarlos.
4.4 Mantenimiento y actualización
La optimización de código puede realizarse
durante la propia generación o como paso adicional, ya sea intercalado entre el
análisis semántico y la generación de código (se optimizan las cuádruplas) o situado
después de ésta (se optimiza a posteriori el código generado).
Hay teoremas (Aho,
1970) que demuestran que la optimización perfecta es indecidible.
Por tanto, las optimizaciones de código en realidad proporcionan mejoras, pero
no aseguran el éxito total.
Clasificación de optimizaciones:
Optimización y depuración suelen ser
incompatibles. Por ejemplo, si se elimina totalmente una instrucción, puede ser
imposible poner una parada en ella para depuración. Ejemplo:
x = x;
ALgunas máquinas tienen instrucciones especiales que permiten
acelerar ciertos procesos. Por ejemplo:
Ej: if (x&4
|| x&8) ... se puede
representar:
TEST X,12
JZ L
...
L:
En muchas máquinas, la multiplicación en
punto fijo de dos operandos de longitud 1 da un
operando de longitud 2, mientras la división necesita un operando de longitud 2
y otro de longitud 1 para dar un cociente y un resto de longitud 1. Reordenar
las operaciones puede optimizar. Por ejemplo: sea la expresión a=b/c*d;
MOV AX,B
XOR DX,DX
DIV AX,C
MUL AX,D
MOV A,AX
Si la reordenamos así: a=b*d/c;, aprovechando que la multiplicación y la división son
asociativas, tenemos:
MOV AX,B
MUL AX,D
DIV AX,C
MOV A,AX
Ahorramos una instrucción. Veamos otro
ejemplo:
a=b/c;
d=b%c;
Los dos códigos siguientes son equivalentes.
Puede tratarse como un caso particular del manejo de registros. La realización
de la primera división debería guardar constancia de que DX contiene el
resultado del resto.
MOV AX,B MOV AX,B
XOR DX,DX XOR DX,DX
DIV AX,C DIV AX,C
MOV A,AX MOV A,AX
MOV AX,B MOV D,DX
XOR DX,DX
DIV AX,C
MOV D,DX
Ejemplo:
int i;
float f;
i = 2+3; (+,2,3,t1) (=,5,,i)
(=,t1,,i)
i = 4; (=,4,,i) (=,4,,i)
f = i+2.5; (CIF,i,,t2) (=,6.5,,f)
(+,t2,2.5,t3)
(=,t3,,f)
La ejecución se aplica principalmente a las
operaciones aritméticas (+-*/) y a las conversiones de tipo.
La tabla de símbolos puede contener el valor
conocido del identificador (ej., i=4),
o bien podemos tener una subtabla T con pares (id, valor).
Algoritmo para tratar la ejecución en tiempo
de compilación:
Ejemplo:
if (false) f = 1/0;
Esta instrucción debe dar un
aviso, pero no un error. De hecho, una optimización adicional de código la
eliminaría totalmente.
En el ejemplo:
(+,2,3,t1) Elim, T = {(t1,5)}
(=,t1,,i) Sust por (=,5,,i), T = {(t1,5),(i,5)}
(=,4,,i) T = {(t1,5),(i,4)}
(CIF,i,,t2) Sust por (CIF,4,,t2),
Elim, T = {(t1,5),(i,4),(t2,4.0)}
(+,t2,2.5,t3) Sust por (+,4.0,2.5,t3)
Elim, T = {(t1,5),(i,4),(t2,4.0),(t3,6.5)}
(=,t3,,f) Sust por (=,6.5,,f)
Y quedan las cuádruplas optimizadas: (=,5,,i), (=,4,,i), (=,6.5,,f).
En cuanto sea posible que los valores de las
variables cambien, el compilador debe "olvidar" el valor de las
variables (inicializar la tabla T, total o parcialmente). Esto puede ocurrir si
aparece:
Este proceso no exige la generación de las
cuádruplas, puede realizarse directamente durante las rutinas semánticas asociadas
al análisis sintáctico, especialmente si es Bottom-up.
Problema con la ejecución en tiempo de
compilación: si tenemos un "cross-compiler",
la precisión puede ser menor en el ordenador que compila que en el que ejecuta.
Ejemplo:
int a,b,c,d;
a = a+b*c; (*,b,c,t1) (*,b,c,t1)
(+,a,t1,t2) (+,a,t1,t2)
(=,t2,,a) (=,t2,,a)
d = a+b*c; (*,b,c,t3)
(+,a,t3,t4) (+,a,t1,t4)
(=,t4,,d) (=,t4,,d)
b = a+b*c; (*,b,c,t5)
(+,a,t5,t6)
(=,t6,,b) (=,t4,,b)
Una solución: el programador podría reescribir su programa así:
int a,b,c,d,e;
e = b*c; (*,b,c,t1)
(=,t1,,e)
a = a+e; (+,a,e,t2)
(=,t2,,a)
d = a+e; (+,a,e,t3)
(=,t3,,d)
b = d; (=,d,,b)
Desventaja: esta forma de programar puede ser
más larga y menos legible. Además, hay redundancias que el programador no puede
eliminar. Por ejemplo:
array X[0:4, 0:9];
X[i,j]:=X[i,j]+1; (*,i,10,t1) (*,i,10,t1)
(+,t1,j,t2) (+,t1,j,t2)
(+,X[t2],1,t3) (+,X[t2],1,t3)
(*,i,10,t4) (:=,t3,,X[t2])
(+,t4,j,t5)
(:=,t3,,X[t5])
Algoritmo para eliminar redundancias:
Prueba: si j<k<i,
y la cuádrupla k cambiara alguno de los operandos de
la cuádrupla i, entonces dep(i)>k. Pero dep(j)<=k, luego dep(i)>dep(j) y no se podría eliminar (i).
Ejercicio: aplicar el algoritmo a los dos
ejemplos anteriores.
El uso de tripletes simplifica el proceso, y
aún más si son indirectos.
Tener en cuenta la conmutatividad de algunas
operaciones puede mejorar el proceso, pues las cuádruplas (*,a,b,-) y (*,b,a,-) serían
equivalentes. Para facilitar el reconocimiento, se puede adoptar un orden
canónico para los operandos de las operaciones
conmutativas. Por ejemplo: términos que no son variables ni constantes, luego
variables indexadas por orden alfabético, luego variables sin indexar por orden
alfabético, finalmente constantes. Esto mejora también la ejecución en tiempo
de compilación. Por ejemplo, si tenemos las instrucciones
a=1+c+d+3; a=c+d+1+3;
b=d+c+2; b=c+d+2;
la reordenación nos permite efectuar en tiempo de
compilación la operación 1+3, y reconocer c+d como
parte común de las dos instrucciones. Esto no es completo, sin embargo, ya que
a=1+c+d+3; a=c+d+1+3;
b=d+c+c+d; b=c+c+d+d;
la reordenación no nos permite reconocer que c+d, evaluado en la primera instrucción, puede aplicarse a
la segunda.
Otra mejora podría ser la utilización de los
operadores monádicos para aumentar el número de
cuádruplas equivalentes. Por ejemplo:
a = c-d; (-,c,d,t1) (-,c,d,t1)
(=,t1,,a) (=,t1,,a)
b = d-c; (-,d,c,t2) (-,t1,,t2)
(=,t2,,b) (=,t2,,b)
que no disminuye el número de cuádruplas, pero sustituye
una operación diádica por una monádica, que
usualmente son más eficientes.
Las variables intermedias para resultados
parciales pueden reutilizarse para minimizar la memoria (aunque eso puede ir en
contra de las optimizaciones anteriores). Por ejemplo: sea la expresión (a*b)+(c+d). Sus cuádruplas
equivalentes serían:
(*,a,b,t1)
(+,c,d,t2)
(+,t1,t2,t1)
En este caso, utilizamos dos variables
auxiliares (t1, t2). Pero si aprovechamos la asociatividad
de la suma para reordenar de esta manera:
(*,a,b,t1)
(+,t1,c,t1)
(+,t1,d,t1)
necesitaremos sólo una variable auxiliar.
El número mínimo de variables auxiliares se
puede calcular construyendo un grafo de la expresión y aplicando las siguientes
reglas:
Por ejemplo, el grafo de (a*b)+(c+d) es:
+(2)
-----------------
*(1) +(1)
--------- ---------
a(0) b(0) c(0) d(0)
Pero el grafo de ((a*b)+c)+d
es:
+(1)
----------------
+(1) d(0)
-----------------
*(1) c(0)
---------
a(0) b(0)
También se puede aprovechar la
conmutatividad, como en el ejemplo:
(a+b)+(c*d) a+(c*d)+b
+(2) +(1)
----------------- -----------------
+(1) *(1) +(1) b(0)
--------- --------- ---------
a(0) b(0) c(0) d(0) a(0) *(1)
---------
c(0) d(0)
Una operación es invariante respecto a un
bucle, si ninguno de los operandos de los que depende
cambia de valor durante la ejecución del bucle. La optimización consiste en
sacar la operación fuera del bucle.
Otra optimización es la reducción de la
fuerza de una operación (sustituir una operación fuerte por otra más débil,
como la multiplicación por la suma o la diferencia por el cambio de signo, como
en el apartado anterior). Por ejemplo:
for (i=a; i<c; i+=b) {... d=i*k; ...}
donde b,k son invariantes
respecto al bucle. (b podría ser una expresión, en
cuyo caso todos sus operandos deben ser invariantes).
Además, i no se modifica dentro del bucle, excepto en la instrucción de cierre,
i+=b, y d no se usa ni modifica antes de la
instrucción indicada y no se modifica después. En este caso, podemos sustituir
el código generado por su equivalente:
d=a*k;
t1=b*k;
for (i=a; i<c; i+=b, d+=t1) {...}
con lo que hemos reducido la fuerza de una multiplicación
a una suma (dentro del bucle).
Esto no se debe hacer si i o k son reales,
pues podría perderse precisión al sumar i veces en vez de multiplicar una. Pero
sí se puede hacer si i,k son
enteros.
Otro
ejemplo:
for (i=0; i<10; i++) {... a=(b+c*i)*d; ...}
INIT: (=,0,,i)
LOOP: ...
(*,c,i,t1)
(+,b,t1,t2)
(*,t2,d,t3)
(=,t3,,a)
...
INCR: (+,i,1,i)
donde b,c,d son invariantes
respecto al bucle, e i es la variable del bucle. Supongamos que se cumplen
todas las condiciones. Podemos aplicar reducción de fuerza a la primera
cuádrupla del bucle así:
INIT: (=,0,,i)
(*,c,0,t1)
(*,c,1,t4)
LOOP: ...
(+,b,t1,t2)
(*,t2,d,t3)
(=,t3,,a)
...
INCR: (+,i,1,i)
(+,t1,t4,t1)
Ahora t1 desempeña el mismo papel que i. Se
le asigna un valor inicial y en cada paso del bucle se le incrementa en t4. Por
tanto, podemos aplicar reducción de fuerza a la cuádrupla siguiente:
INIT: (=,0,,i)
(*,c,0,t1)
(*,c,1,t4)
(+,b,t1,t2)
LOOP: ...
(*,t2,d,t3)
(=,t3,,a)
...
INCR: (+,i,1,i)
(+,t1,t4,t1)
(+,t2,t4,t2)
Ahora pasa lo mismo con t2, luego podemos
aplicar reducción de fuerza a la siguiente cuádrupla:
INIT: (=,0,,i)
(*,c,0,t1)
(*,c,1,t4)
(+,b,t1,t2)
(*,t2,d,t3)
(*,t4,d,t5)
LOOP: ...
(=,t3,,a)
...
INCR: (+,i,1,i)
(+,t1,t4,t1)
(+,t2,t4,t2)
(+,t3,t5,t3)
Todavía podemos optimizar más notando que
ahora t1 y t2 no se emplean dentro del bucle, luego no es necesario
incrementarlas:
INIT: (=,0,,i)
(*,c,0,t1)
(*,c,1,t4)
(+,b,t1,t2)
(*,t2,d,t3)
(*,t4,d,t5)
LOOP: ...
(=,t3,,a)
...
INCR: (+,i,1,i)
(+,t3,t5,t3)
Si sacamos operaciones fuera de un bucle,
pueden quedar dentro de otro bucle más externo. El proceso podría repetirse.
Si hay alguna llamada de subrutina dentro del
bucle, es difícil saber si se cambia alguna de las variables (podrían ser
globales o pasarse como argumento por referencia). En tal caso, sólo pueden
aplicarse las optimizaciones si el compilador sabe qué variables se cambian.
Esto suele ocurrir sólo para ciertas funciones y subrutinas predefinidas.
Para realizar las optimizaciones pueden hacer
falta dos pasos: uno primero, en el que se analizan los bucles y se obtiene
información sobre las variables que cambian, y otro segundo, en el que se
realiza la optimización propiamente dicha. Pero también se puede fusionar el
proceso con el analizador semántico y el generador de código y hacerlo todo en
un solo paso. Para esto, a veces hay que retrasar o
cambiar de orden algunas de las operaciones del bucle.
Por ejemplo, podríamos generar un código como el siguiente:
GOTO INIT
LOOP: ...
INCR: ...
GOTO TEST
INIT: ...
TEST: IF (no fin de bucle) GOTO LOOP
con lo que INIT y INCR (que son los que cambian con la
optimización) quedan al final.
Hay que tener cuidado con estas
optimizaciones. Si el bucle se ejecuta normalmente 0 (o 1) veces, y es muy raro
que se entre en él, las optimizaciones anteriores degradarán (o dejarán
invariante) la eficiencia.
Supongamos que tenemos un programa dividido
en bloques básicos. con ellos podemos formar un grafo
donde los nodos son los bloques, los arcos indican sucesión de ejecución.
Llamamos "región fuertemente
conexa" (o simplement región) a un subgrafo del programa en el que existe un camino de
cualquier nodo del subgrafo a otro nodo del subgrafo. Ejemplo:
---------------------
| ------ -- |
v v | |v |
1 -> 2 -> 3 -> 5 -> 6 -> 7 -> 8
| ^
|--> 4 ---|
En la figura hay cinco regiones: (6), (3,5),
(2,3,5,6,7), (2,3,4,6,7), (2,3,4,5,6,7).
Llamamos "bloque de entrada" de una
región a un bloque al que entra un arco desde fuera de la región. (3,5) tiene
un bloque de entrada: 3. (2,3,5,6,7) tiene dos bloques
de entrada: 2 y 6.
Llamamos "predecesor" de una región
a un bloque situado fuera de la región del que sale un arco que lleva a un
bloque de entrada de la región. (3,5) tiene un predecesor: 2. (2,3,5,6,7) tiene dos predecesores: 1 y 4.
Construimos una lista R={R1,R2,...,Rn} de regiones tales que Ri!=Rj si i!=j, y i<j
=> Ri y Rj no tienen
bloques en común o bien Ri es un subconjunto de Rj. En el ejemplo, una lista válida sería:
(6),(3,5),(2,3,5,6,7),(2,3,4,5,6,7). Otra lista válida sería:
(6),(2,3,4,6,7),(2,3,4,5,6,7). (2,3,4,6,7) y
(2,3,5,6,7) no pueden estar juntas en una lista válida.
¿Cuál elegir? Conviene que estén los bucles,
que tienen normalmente un solo nodo predecesor y un solo nodo de entrada.
Cuando haya dos posibilidades, preferiremos las regiones con esta propiedad.
Para cada bloque definiremos las siguientes
variables booleanas:
El O lógico de R[i] de todos los bloques de
una región nos da R[i] para la región. Lo mismo con A[i].
La optimización se aplica sucesivamente a
cada región de la lista, de la primera a la última. Al optimizar cada región,
se crean bloques nuevos de inicialización. En el ejemplo:
---------------------------
| ------ -- |
v v | |v |
1 -> 2 -> 3 -> 5 -> I1 -> 6 -> 7 -> 8
| ^
|--> 4 -> I2 ---|
Los bloques I se añaden a las regiones
correspondientes, para que entren en las nuevas optimizaciones. Todos los
bloques de la región recién tratada se sustituyen por uno solo (R1, en este
caso), calculando las variables booleanas aplicables
a la región. Este bloque ya no debe ser optimizado. (R1), (3,5),
(2,3,5,I1,R1,7), (2,3,4,5,I1,I2,R1,7).
Pasamos a la región R2 (3,5), optimizamos:
----------------------------------
| ------ -- |
v v | |v |
1 -> 2 -> I3 -> 3 -> 5 -> I1 -> R1 -> 7 -> 8
| ^
|--> 4 -> I2 ---|
y sustituimos:
------------------------------
| -- |
v |v |
1 -> 2 -> I3 -> R2 -> I1 -> R1 -> 7 -> 8
| ^
|-> 4 -> I2 -|
Las
regiones serán: (R1), (R2), (2,I3,R2,I1,R1,7), (2,I3,R2,4,I1,I2,R1,7). Etcétera. En principio, sólo hacen falta los vectores booleanos correspondientes a las variables que se utilizan
dentro de la región que se está optimizando. El algoritmo es como sigue:
Surgen si el resultado de la asignación no se
utiliza posteriormente o si la asignación es recursiva (ej.,
i++) y sólo se usa en dichas definiciones recursivas.
Todas esas asignaciones pueden eliminarse. Suelen surgir como consecuencia de
la reducción de fuerza y optimizaciones semejantes (como se vio).
Para ver si una asignación está muerta se
puede utilizar el algoritmo:
a optimización de la ejecución de
una aplicación son en una serie de pasos a realizar a partir del código fuente.
Consiste en mejorar las líneas del fuente, de modo que
resulte un código más rápido de ejecutar. El tiempo de ejecución de un programa
siempre dependerá en gran medida de la arquitectura de la computadora en la que
se esté ejecutando. No es lo mismo hablar de ejecución en máquinas con niveles
de caché intermedio a otras que no los tenga, como algunas máquinas con Celeron [Intel-1998].
Son muchos los tipos de optimización de código que pueden realizar los
distintos compiladores. Cuando se hace mucho refinamiento de código, llamados
compiladores optimizadores, una parte significativa
del tiempo de compilación se ocupa en esta fase. Sin embargo, hay
optimizaciones sencillas que mejoran sensiblemente el tiempo de ejecución del
programa objeto sin retardar demasiado la compilación.
Para efectos de esta clase, solamente se aplicarán las optimizaciones más
simples. El programador al realizar la codificación tiene que pensar en la
calidad: debe ser conciso, simple al construir el código, y como
recomendaciones adicionales utilizar estas técnicas:
1. Elegir un buen algoritmo
2. Elegir una apropiada estructura de datos
3. Reutilización de código
4. Tipos de variables a usar
5. Claridad en el código
6. Perspectiva
7. Conocer las opciones del compilador para optimizar
8. Desenvolvimiento de Ciclos (Loop
Unrolling)
9. Fusión de Ciclos
10. Intercambio de Ciclos
11. Distribución de Ciclos
12. Reducción de Esfuerzo (Strength
Reduction)
1. Elegir un buen algoritmo
El típico paso en el desarrollo de la optimización es la selección de un
algoritmo robusto y eficiente, tal como lo menciona Knuth en su Art of Computer
Programming [Knuth-1997].
Ejemplos de esto pueden ser el utilizar un algoritmo de búsqueda secuencial,
que se puede reemplazar por un algoritmo de búsqueda binaria.
2. Elegir una
apropiada estructura de datos
También es importante elegir una estructura de datos apropiada, a fin de
optimizar el uso del espacio interno del código de los programas y la
legibilidad de los mismos. En una aplicación donde se hacen muchas inserciones
y cancelaciones en lugares aleatorios, sería recomendable utilizar una lista
ligada. Si se realizan algunas búsquedas binarias, utilizar un arreglo sería lo
mejor para este caso. Para algunos casos utilizar estructuras arbóreas resulta
muy adecuado mientras que en otros casos resulta demasiado complejo.
3. Reutilización de
código
Para reducir el tiempo y esfuerzo, pueden reutilizarse rutinas ya optimizadas
(por ejemplo, las bibliotecas científicas y matemáticas). Si dentro del
programa se requiere del cálculo de la inversa de una matriz de orden n, el
programador puede mandar llamar una rutina ya optimizada (y a veces paralelizada).
4.Tipos de variables a
usar
Debe tenrse en mente el tipo de variable que se esta
empleando para que las comparaciones y asignaciones que pueden ser lógicas o
enteras sean representadas por este tipo de datos.
5. Claridad código
Un código claro y con comentarios hace que los programas sean fáciles de leer y
entender para modificarlos, por ejemplo, en un examen final. Incluso, el
utilizar sangría resulta necesario. El código es claro y legible es más fácil
de optimizar para el compilador.
Un consejo es evitar en lo posible las uniones entre tipos de datos diferentes
(enteros y reales).
Es muy cómodo escribir en forma modular. Para estos casos es conveniente evitar
el uso de variables globales, y que la mayoría de las operaciones o tareas usen
variables locales. En programas en C, se recomienda declarar cualquier variable
fuera de una función como estática, a menos que esa variable sea referenciada
por otro archivo fuente debe definirse como que es externa al procedimiento.
6. Perspectiva
Es de interés obtener una idea de cuánto tiempo toman ciertas operaciones, o
sea, el costo del programa. Es útil saber qué tan lento este abriendo un
archivo, leyendo o escribiendo cantidades significativas de datos, comenzando
un nuevo proceso, buscando, ordenando, haciendo operaciones alrededor de
arreglos y copiando grandes cantidades de datos. Las operaciones rápidas son
elementos básicos del lenguaje, como asignar a una variable, referenciando un apuntador, o agregando dos enteros.
Ninguna de estas operaciones duran mucho tiempo en ellas mismas (algunos picosegundos) pero todos los lenguajes de programación
permiten que las secciones de código sean ejecutadas en varias ocasiones.
7. Conocer las opciones del compilador
para optimizar
La mayoría de los compiladores tienen diferentes niveles de optimización. Los
niveles más sofisticados de optimización pueden hacer la ruptura del código mal
escrito (pero compilable). Cada compilador ofrece
diversas técnicas de optimización en cada nivel indicadas por el usuario, y es
importante que el usuario conozca dichas opciones para que su programa sea
optimizado eficientemente sin afectar su resultado y desempeño.
8. Desenvolvimiento de Ciclos
(Loop Unrolling)
El desenvolvimiento de ciclos es un proceso de expansión que crea un ciclo más
grande haciendo una réplica del cuerpo del ciclo original.
9. Fusión de Ciclos
Es una técnica en la que se combinan dos ciclos adyacentes que tienen el mismo
rango en la variable, para formar un solo ciclo.
10. Intercambio de
Ciclos
Permuta dos ciclos anidados: el ciclo externo se convierte en el ciclo interno
y viceversa, con el propósito de mejorar el acceso a memoria
11. Distribución de
Ciclos
Consiste en dividir un ciclo en varios ciclos, y así incrementar la posibilidad
del paralelismo y de canalización de código.
12. Reducción de
Esfuerzo (Strength Reduction)
Sustituye, por ejemplo, una expresión que hace menos eficiente la ejecución por
otra que realice la misma operación pero con mayor eficiencia.