4 Programación

 

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.

    1. Existen caminos seguibles desde la entrada hasta la salida que conducen por cada parte del programa, es decir, no existen lazos infinitos ni instrucciones que no se ejecutan.

 

 

 

 

 

 

 

 

Filosofia de la programacion orientada a objetos

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.

CUATRO PROPIEDADES ESENCIALES

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.

Abstracción de Datos.

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.

  1. Abstracciones de objeto - Los objetos son las abstracciones que modelan la aplicación individual del dominio o entidades del espacio de solución.
  1. Abstracciones de clases - Las clases son las plantillas abstractas que modelan la aplicación similar del dominio o entidades del espacio de solución.
  1. Abstracciones de datos - Modela los datos que son usados cuando se comunican con el atributo de objetos y las clases.
  1. Abstracciones funcionales - Modela operaciones secuenciales considerando los procesos de abstracciones.
  1. Abstracciones de procesos - Modelan las operaciones concurrentes. Ambas abstracciones funcional y de procesos se usan cuando se comunican con las operaciones de objetos y las clases.
  1. Abstracciones de excepción - Las condiciones del error del modelo de excepción son el error manejado y se usan para crear objetos robustos y clases.

 

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.



Top de la Pagina

 

Polimorfismo

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á.



Top de la Pagina

Herencia

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.

Ejemplo de Clases Derivadas

 

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.



Top de la Pagina

Encapsulamiento

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.



Top de la Pagina

Control de acceso a una 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.



Top de la Pagina

Desarrollo

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.

 



Top de la Pagina

 

Las Clases con Estructuras

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.

 

 

 Top de la Pagina

Clases anidadas

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



Top de la Pagina

El apuntador this

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.



Top de la Pagina

Funciones miembro estáticas

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(); }



Top de la Pagina

Funciones miembro const

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(); }



Top de la Pagina

Funciones miembro en línea (inline)

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;

}



Top de la Pagina

Constructores

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);



Top de la Pagina

Destructores

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.

 

 Top de la PaginaCreació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.
\begin{picture}(150.00,132.00)
\put(14.00,119.00){\framebox (52.00,13.00)[cc]{P...
...00,52.00){\line(1,1){32.00}}
\put(71.00,51.00){\line(1,3){13.67}}
\end{picture}
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


Page 6

Info. y


Page 7

 

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:

  1. Dependientes de la máquina.
  2. Independientes de la máquina.

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;

Instrucciones especiales ("idioms")

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:

Reordenación del código

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

Ejecución en tiempo de compilación

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.

Eliminación de redundancias

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.

Reordenación de operaciones

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:

  1. Marcar las hojas con 0.
  2. Si (j,k) son las marcas de los hijos del nodo i, si j=k, asociar (k+1) al nodo i, en caso contrario asociarle max(j,k).

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)

Optimización de bucles

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.

Regiones

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:

  1. i=1;
  2. Seleccionar región Ri. Preparar un bloque I vacío.
  3. Ejecutar y eliminar redundancias dentro de cada bloque de la región. Construir los vectores booleanos para cada bloque.
  4. Sacar invariancias y reducir la fuerza dentro de la región, llenando el bloque I. Eliminar asignaciones muertas (asignaciones que nunca se usan). Esto cambia los vectores booleanos y crea variables temporales.
  5. Crear los tres vectores para la región entera. Sustituir todos sus bloques por el bloque región, que ya no debe ser optimizado. Insertar copias del bloque I entre todo predecesor y bloque de entrada de la región.
  6. i++; si i<=n, ir al paso 2.
  7. Realizar optimización de cuádruplas y eliminación de redundancias en los bloques básicos que no pertenecen a ninguna región.

Asignaciones muertas

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:

  1. Seguir las operaciones del bloque. Si aparece otra asignación a la misma variable sin un uso intermedio, la asignación está muerta. Si aparece un uso de esa variable, la asignación no está muerta. En caso contrario, ir al paso 2.
  2. Seguir todas las ramificaciones del programa a partir del bloque en que estamos y mirar las variables R, A, B de cada bloque por el que pasemos. Si encontramos B[i]=1 en un bloque, la asignación no está muerta. Si B[i]=0, pero A[i]=1, abandonamos este camino. Si se nos acaban los caminos o entramos en bucles, la asignación está muerta.

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.