Diríjase a CEDRO (Centro Español de Derechos Reprográficos, wwww.cedro.org) si necesita fotocopiar, escanear o hacer copias digitales de algún fragmento de esta obra.
Anagrama «LUCHA CONTRA LA PIRATERIA» propiedad de Unión Internacional de Escritores.
PRESENTACIÓN
Con la edición del presente volumen queremos facilitar al opositor poder presentarse a las pruebas selectivas para ingresar en el Cuerpo de Técnicos Auxiliares de Informática de la Administración General del Estado. Esta edición se ha realizado combinando los programas de la última convocatoria del turno libre (2008) y los de la promoción interna de los últimos años. En este Temario 2 encontrará los temas del Bloque III (Desarrollo de Sistemas) y del Bloque IV (Sistemas y Comunicaciones). En la elaboración de los temas hemos pretendido facilitar al opositor no sólo la comprensión de los conceptos básicos y los más más complejos, sino también su memorización. Para conseguir tal objetivo, cada tema presenta la siguiente estructura: —
Títulos o epígrafes que lo integran.
—
Referencias legislativas, al comienzo de cada tema, que deberán tenerse en cuenta para su estudio.
—
Guión-resumen, que proporciona una visión escueta del contenido del tema, a través de la enumeración de todos sus apartados.
—
Exposición del tema, en el que se desarrolla y analiza el contenido del programa.
—
Gráficos-diagramas, esquemas, etc., que intentan resumir y exponer de forma ordenada las partes de cada tema a las que deberá prestar mayor atención.
Le recordamos que puede adquirir, junto con este temario, el Temario 1 y los volúmenes de Cuestionarios y Supuestos Prácticos, que completan la colección. En nuestra página, www.adams.es, podrá estar al día de todo en cuanto a información sobre oposiciones, productos y servicios relacionados se refiere, y a través de [email protected] le ofrecemos la posibilidad de dirigirnos cualquier consulta o sugerencia. Además, puede usted acudir a la página web del Ministerio de Hacienda y Administraciones Públicas (www.minhap.es) para conocer los cambios legislativos, de procesos selectivos o de otro orden que se produzcan. Esperando haber cumplido el objetivo propuesto, expresamos al opositor nuestros mejores deseos de éxito en la tarea emprendida.
ADAMS
5
CONTENIDO III. Desarrollo de Sistemas Tema 1.
Modelo conceptual de datos. Entidades, atributos y relaciones. Reglas de modelizacion. Diagramas de flujo de datos. Reglas de construcción. Descomposición en niveles. Flujogramas.
Tema 2.
Diseño de bases de datos. Diseño lógico y físico. El modelo lógico relacional. Normalización.
Tema 3.
Lenguajes de programación. Representación de tipos de datos. Operadores. Instrucciones condicionales. Bucles y recursividad. Procedimientos, funciones y parámetros. Vectores y registros. Estructura de un programa.
Tema 4.
Lenguajes de interrogación de bases de datos. Estándar ANSI SQL.
Tema 5.
Diseño y programación orientada a objetos. Elementos y componentes software: objetos, clases, herencia, métodos, sobrecarga. Ventajas e inconvenientes. Patrones de diseño y lenguaje de modelado unificado (UML).
Tema 6.
Lenguajes de programación. Características, elementos y funciones en entornos Java, C, y C++ y .Net.
Tema 7.
Arquitectura cliente/servidor. Modelo de 2 capas. Modelo de 3 capas. Componentes y operación. Arquitecturas de servicios web (WS).
Tema 8.
Aplicaciones Web. Tecnologías de programación. JavaScript, applets, servlets y servicios web. Lenguajes de descripción de datos: HTML, XML y sus derivaciones. Navegadores y lenguajes de programacion web. Lenguajes de script.
Tema 9.
Accesibilidad, diseño universal y usabilidad. Acceso y usabilidad de las tecnologias, productos y servicios relacionados con la sociedad de la información. Confidencialidad y disponibilidad de la información en puestos de usuario final.
Tema 10.
Herramientas CASE: características. Repositorios: estructura y actualización. Generación de código y documentación. Programas para control de versiones.
IV. Sistemas y comunicaciones Tema 1.
Administración del Sistema operativo y software de base. Funciones y responsabilidades. Control de cambios de los programas de una instalación.
Tema 2.
Administración de bases de datos. Funciones y responsabilidades. Administración de servidores de correo electrónico. Protocolos de Correo electrónico.
Tema 3.
Administracion de redes de área local. Gestión de usuarios. Gestión de dispositivos. Monitorización y control de tráfico.
7
Tema 4.
Conceptos de seguridad de los sistemas de información. Seguridad física. Seguridad lógica. Amenazas y vulnerabilidades. Infraestructura física de un CPD: acondicionamiento y equipamiento. Sistemas de gestión de incidencias. Control remoto de puestos de usuario.
Tema 5.
Comunicaciones. Medios de transmisión. Modos de comunicación. Equipos terminales y equipos de interconexión y conmutación.
Tema 6.
Redes de Comunicaciones. Redes de Conmutación y Redes de Difusión. Comunicaciones móviles y redes inalámbricas.
Tema 7.
El modelo de referencia de interconexión de sistemas abiertos (OSI) de ISO. Protocolos TCP/IP.
Tema 8.
Internet: arquitectura de red. Origen, evolución y estado actual. Servicios: correo electrónico, WWW, FTP, Telnet y otros. Protocolos HTTP, HTTPS y SSL/TLS.
Tema 9.
Seguridad y protección en redes de comunicaciones. Sistemas de cortafuegos. Redes Privadas Virtuales (VPN). Seguridad en el puesto del usuario.
Tema 10.
Redes locales. Tipología. Métodos de acceso. Dispositivos de interconexión.
8
III. Desarrollo de Sistemas
Tema 1 Modelo conceptual de datos. Entidades, atributos y relaciones. Reglas de modelización. Diagramas de flujo de datos. Reglas de construcción. Descomposición en niveles. Flujogramas.
Desarrollo de Sistemas
Guión-resumen
1. Modelo conceptual de datos 2. Entidades, atributos y relaciones 2.1. Concepto de entidad 2.2. Concepto de relación 2.3. Concepto de atributo 3. Análisis entidad/relación (reglas de modelización) 4. Diagramas de flujo de datos: reglas de construcción 4.1. Organigramas 4.2. Ordinogramas 4.3. Pseudocódigo 4.4. Paso de pseudocódigo a diagrama de flujo y viceversa
1-2
5. Descomposición en niveles. Flujogramas 5.1. Diagramas de Flujo de Datos (DFD) 5.2. Modelos de datos 5.3. Diagramas de datos (DED) 6. Conclusiones
Modelo conceptual de datos
1.
Modelo conceptual de datos
Un Modelo de datos es una representación gráfica orientada a la obtención de las estructuras de datos de una forma metódica y a la vez sencilla. El modelo se suele representar con el modelo Entidad/Relación de Chen. Este modelo percibe el mundo real como una serie e objetos que se relaciona entre sí y pretende representarlos gráficamente mediante un mecanismo de abstracción basado en símbolos, reglas y métodos El diseño conseguido no es el nexo de unión entre el mundo del usuario (nivel externo) y el mundo del ordenador (nivel interno). Sólo es una representación de las propiedades lógicas de los datos y por tanto, dicha información no es accesible directamente por el SGBD. Es un método de representación abstracta del mundo real y por lo tanto no es directamente traducible a una SGBD, sino que necesita una traducción al modelo relacional de dicho SGBD. En esta etapa se debe construir un esquema de la información que se usa en la empresa, independientemente de cualquier consideración física. A este esquema se le denomina esquema conceptual. Al construir el esquema, los diseñadores descubren la semántica (significado) de los datos de la empresa: encuentran entidades, atributos y relaciones. El objetivo es comprender: —
La perspectiva que cada usuario tiene de los datos.
—
La naturaleza de los datos, independientemente de su representación física.
—
El uso de los datos a través de las áreas de aplicación.
El esquema conceptual se puede utilizar para que el diseñador transmita a la empresa lo que ha entendido sobre la información que ésta maneja. Para ello, ambas partes deben estar familiarizadas con la notación utilizada en el esquema. La más popular es la notación del modelo entidad-relación, que se describirá en el capítulo dedicado al diseño conceptual. El esquema conceptual se construye utilizando la información que se encuentra en la especificación de los requisitos de usuario. El diseño conceptual es completamente independiente de los aspectos de implementación, como puede ser el SGBD que se vaya a usar, los programas de aplicación, los lenguajes de programación, el hardware disponible o cualquier otra consideración física. Durante todo el proceso de desarrollo del esquema conceptual éste se prueba y se valida con los requisitos de los usuarios. El esquema conceptual es una fuente de información para el diseño lógico de la base de datos.
2.
Entidades, atributos y relaciones
El Modelo Entidad/Relación tiene sus estructura propias que son conocidas con el nombre de Diagramas Entidad/ Relación Los elementos que componen dicho Modelo son las entidades, los atributos y las relaciones.
2.1.
Concepto de entidad
Cualquier tipo de objeto (físico) o concepto del cual se pueda extraer información. Las entidades se representan mediante rectángulos. Por ejemplo un bote de bebida es un objeto del que emana gran cantidad de información: 1-3
Desarrollo de Sistemas
la marca, el sabor, la capacidad, los ingredientes, la caducidad, etc. A este paso se le denomina abstracción; después, toda esta información hay que organizarla; ¿cómo?, en tablas, por ejemplo. Una entidad sólo aparece una vez en el modelo conceptual. Se denomina ocurrencia de entidad a la implementación concreta de una entidad.
2.2.
Concepto de relación
Es la asociación entre dos o más entidades. Se representan gráficamente entre rombos con el nombre dentro. Al número de participantes se le llama grado de la relación (binaria, ternaria, etc).
2.3.
Concepto de atributo
Es una característica de interés de la entidad, por ejemplo la marca, la capacidad o el sabor. Cada atributo tiene un conjunto de valores asociados que se les denomina dominio y se encarga de definir todos los valores posibles que puede tomar un atributo. Los atributos pueden ser simples o compuestos. Se dice que un atributo es simple cuando éste sólo tiene un componente, por lo cual no se puede dividir en partes más pequeñas. El atributo es compuesto cuando éste consta de componentes que a su vez se pueden dividir en partes más pequeñas. Se representa con una elipse. En función de sus características respecto a la Entidad se distinguen dos tipos de atributos: Atributo descriptor. Caracteriza una ocurrencia pero no la distingue del resto. Se representa por una elipse o círculo sin relleno. La clave primaria o identificador. Conjunto de atributos pertenecientes a la misma entidad y que hacen único el acceso a cada ocurrencia de la entidad. Se representa mediante un círculo relleno. Es posible pensar en la existencia de varias claves sobre la misma entidad. Al conjunto de todas ellas se las denomina Claves Candidatas. Sólo una de ellas conformará la Clave Primaria. Por el hecho de estar formada por un solo atributo o varios se llamará clave simple o múltiple (compuesta).
personas
nombre apellidos dirección tfno localidad…
1-4
compran
es una tabla de relación donde se guardan el código de las personas y el código de la casa
casas
precio m2 exterior zona descripción
Modelo conceptual de datos
3.
Análisis entidad/relación (reglas de modelización)
Es el modelo más utilizado para el diseño conceptual de bases de datos. El análisis entidad-relación (E/R) abstrae las tablas en forma de objetos y enlaza dichos objetos mediante punteros de relación de la misma forma que enlazaríamos objetos si estuvieran en la memoria de nuestro PC. Veamos un ejemplo: nos plantean un problema que consiste en crear una base de datos para llevar el control de una gestoría de inmuebles. Necesitaremos como mínimo dos entidades, una donde quede plasmada la información de las personas (clientes) y otra donde quede plasmada la información de las viviendas (casas). En un principio son dos entidades diferentes pero si se pretende acceder a la información para visualizar las casas vendidas y sus compradores, no queda más remedio que relacionarlas. Una acción que vincula a las personas con las viviendas es, por ejemplo, que éstas son compradas por parte de las anteriores. El análisis entidad/relación quedaría así: Ahora lo abstraemos a sus respectivas tablas: casas
personas idpersona
nombre
direcc
Tfno
idpersona
decripción
1
Juan
C/Pez nº 20
917885522
1
Chalet
2
María
C/Córdoba nº 60 913225577
2
Piso
3
Pedro
Avda Jucal nº 12 917184051
3
Caserón
Idpersona
Idcasa
1
1
2
1
3
2
3
3
metros
Zona
250 La Moraleja 90 Pinar Chamartín 300 El clavín
tabla de referencia (ventas)
Juan y María comparten chalé. Pedro ha comprado el caserón y el piso. Ahora vamos a añadir otra entidad.
personas
compran
casas
coches
1-5
Desarrollo de Sistemas
A) Identificador único Los identificadores de casa y persona están duplicados. Esto puede generar problemas cuando queramos expresar relaciones con la tabla de coches. Para solucionar esto necesitamos dos cosas: 1.
Identificador único: nunca debe de haber dos identificadores iguales para objetos (instancias) del mismo o diferente tipo.
2.
Que en el identificador vaya implícita la información sobre el tipo de objeto: los identificadores de personas empiezan por 1000, los de casas por 2000 y los de coches por 3000. personas
idpersona
casas
nombre
direcc
Tfno
idpersona
decripción
1001
Juan
C/Pez nº 20
917885522
2001
Chalet
1002
María
C/Córdoba nº 60 913225577
2002
Piso
1003
Pedro
Avda Jucal nº 12 917184051
2003
Caserón
Idpersona
Id objeto
1001
2001
1002
2001
tabla de referencias (ventas)
metros
Zona
250 La Moraleja 90 Pinar Chamartín
idcoche
300 El clavín
marca
modelo
3001
Bmw
850
Mercedes
400
Audi
A3
1003
2002
3002
1003
2003
3003
1001
3003
1002
3003
1003
3001
1003
3002
coches
De este modo podemos tener tablas (tipos) que almacenan diferentes objetos (instancias) de diferente tipo. B) Relaciones recursivas La relación recursiva es la que relaciona objetos consigo mismo. Un claro ejemplo es el de una empresa que tiene varios departamentos y éstos, a su vez, tienen subdepartamentos. empresa finanzas contabilidad
producción facturación
el análisis entidad/relación quedaría así:
empresa
1-6
departamentos
Modelo conceptual de datos
En este caso no hay una tabla de relación, pues una empresa puede tener varios departamentos, pero un departamento no tiene varias empresas. C) Simbología de cardinalidad La cardinalidad es la proporción de relaciones entre objetos. relación forzosa 1 a 1 relación de 1 a muchos (n) relación de 0 ó 1 a 1 relación de 0 ó 1 a muchos relación de 0 ó 1 a 0 o muchos
Una empresa puede tener varios departamentos (1 a n). Un departamento puede tener o no varios subdepartamentos (1 ó 0 a n) y así con el resto de las combinaciones. No todos los analistas y programadores utilizan la misma simbología, aunque ésta es la más utilizada. El modelo entidad/relación ha dado origen al modelo relacional, que es el de mayor aceptación en la actualidad. D) Metodología de diseño conceptual Como hemos comentado anteriormente, es el primer paso para crear la base de datos. Este esquema se construye mediante la información que nos da el usuario, examinando programas, formularios e informes ya existentes que se utilizan en la empresa y siguiendo el flujo de información con la que trabajan esos usuarios. A estas visiones de la información se las llama vistas. Puede haber varias vistas recogidas de varios usuarios, por ejemplo de distintos departamentos, por lo cual se suelen crear varios esquemas conceptuales, llamados locales. Cada uno de estos esquemas se compone de entidades, relaciones, atributos, dominios de atributos e identificadores. Los pasos que se deben seguir para la creación de estos esquemas son: 1.
Identificar las entidades: se buscan objetos existentes, como los empleados, clientes, proveedores, etc.
2.
Identificar las relaciones: una vez identificadas las entidades se debe buscar la forma de relacionarlas y qué tipo de cardinalidad surge. 1-7
Desarrollo de Sistemas
4.
3.
Identificar los atributos y asociarlos a entidades y relaciones: se buscan nombres para plasmar la información, por ejemplo idempleado, nombre, apellidos o mejor apellido1 y apellido2, dirección, etc. La mejor técnica para encontrar los atributos es hacerse la pregunta “si necesito visualizar... ¿qué atributo necesito?” Así, por ejemplo, si queremos un listado de las empleadas, necesitaremos un atributo, por ejemplo, mujer. Si necesito un listado de los empleados más veteranos, necesitaremos un atributo fecha_incorporación, etc. De cada atributo se debe ir anotando el nombre que se le asignará, el tamaño, el tipo de dato que albergará, si se permiten valores nulos, etc.
4.
Determinar los dominios de los atributos: valores que puede tomar ese atributo, su tamaño, formato y tipos de operaciones que se pueden realizar sobre ellos.
5.
Determinar los identificadores: cada entidad por lo menos ha de tener un identificador o clave. A las entidades que no tienen identificador se las denomina débiles.
6.
Determinar las jerarquías de generalización (si las hay): en este paso se deben analizar las entidades existentes, pues pueden dar origen a otras entidades o subentidades.
7.
Dibujar el diagrama entidad-relación: cuando tengamos identificados los conceptos anteriores se procede a dibujar la estructura de las base de datos mediante un diagrama.
8.
Revisar el esquema conceptual local con el usuario: presentarle el esquema al usuario (o cliente) para revisarlo y comentar los posibles cambios.
Diagramas de flujo de datos: reglas de construcción
Para el diseño de algoritmos se utilizan técnicas de representación. Una de estas técnicas son los denominados diagramas de flujo, que se definen como la representación gráfica que, mediante el uso de símbolos estándar conectados o unidos mediante líneas de flujo, muestran la secuencia lógica de las operaciones o acciones que debe realizar un ordenador, así como la corriente o flujo de datos en la resolución de un programa. Los diseños deben de ser normalizados para facilitar el intercambio de documentación entre el personal informático (analistas y programadores). Para ello existen normas en las que basarse, dictadas por distintas organizaciones, como la ISO (International Standard Organization), ANSI (American National Standard Institute), etc. Los diagramas de flujo se pueden clasificar en dos grandes grupos: a)
Organigramas.
b)
Ordinogramas.
Una de las principales diferencias entre ambos, es que pertenecen a distintas fases o etapas de la resolución de un programa. Mientras que los organigramas corresponden a la fase de análisis, los ordinogramas corresponden a la fase de diseño. 1-8
Modelo conceptual de datos
4.1.
Organigramas
También denominados diagramas de flujo de sistemas o diagramas de flujo de configuración. Son representaciones gráficas del flujo de datos e información entre los periféricos o soportes físicos (de entrada/salida) que maneja un programa. Todo organigrama debe reflejar: a)
Las distintas áreas o programas en los que se divide la solución del problema, así como el nombre de cada uno de ellos.
b)
Las entradas y salidas de cada área, indicando los soportes que serán utilizados para el almacenamiento tanto de los datos pendientes de elaborar o procesar como de los resultados obtenidos.
c)
El flujo de los datos.
Todo ello debe proporcionar: —
Una visión global de la solución del problema.
—
Una fácil realización de futuras correcciones.
—
Un control de todas las posibles soluciones
Los organigramas deben respetar las siguientes reglas de representación: —
En la parte central del diseño debe figurar el símbolo de proceso.
—
En la parte superior del diseño, y siempre por encima del símbolo de proceso, deben figurar los soportes de entrada.
—
En la parte inferior del diseño, y siempre por debajo del símbolo de proceso, deben figurar los soportes de salida.
—
A izquierda y derecha del diseño y, por tanto, a ambos lados del símbolo de proceso, figurarán los soportes que son tanto de entrada como de salida.
La simbología que se utiliza es la siguiente: A) Símbolos de soporte de información SÍMBOLO
DENOMINACIÓN
TIPO
DE DISPOSITIVO
Teclado
Entrada
Soporte magnético
Entrada
1-9
Desarrollo de Sistemas
SÍMBOLO
DENOMINACIÓN
TIPO
DE DISPOSITIVO
Pantalla/crt
Salida
Impresora
Salida
Tarjeta perforada
Entrada/Salida
Cinta de papel
Entrada/Salida
Disco magnético
Entrada/Salida
Disco magnético
Entrada/Salida
Cinta magnética
Entrada/Salida
Cinta magnética
Entrada/Salida
Disco flexible
Entrada/Salida
Tambor magnético
Entrada/Salida
B) Símbolos de proceso SÍMBOLO
FUNCIÓN Proceso u operación. Clasificación u ordenación de datos en un fichero. Fusión o mezcla de dos o más ficheros en uno solo. Partición o extracción de datos de un fichero. Manipulación de uno o varios ficheros (intercalación).
1-10
Modelo conceptual de datos
C) Líneas de flujo de datos SÍMBOLO
FUNCIÓN Dirección del proceso o flujo de datos. Líneas de teleproceso (transmisión de datos). Línea conectora. Permite la unión entre unidades o elementos de información.
4.2.
Ordinogramas
También denominados diagramas de flujo de programas. Son representaciones gráficas que muestran la secuencia lógica y detallada de las operaciones que se van a realizar en la ejecución de un programa. Se puede decir que los diseños resultantes, por estética, deben guardar cierto equilibrio y simetría, facilitando así, en la medida en la que sea posible, su entendimiento y comprensión, procurando limitar al máximo el uso de comentarios aclaratorios. El diseño de todo ordinograma debe reflejar: a)
Un principio o inicio que marca el comienzo de ejecución del programa y que viene determinado por la palabra “INICIO”.
b)
La secuencia de operaciones, lo más detallada posible y siguiendo siempre el orden en el que se deberán ejecutar (de arriba-abajo y de izquierda-derecha).
c)
Un fin que marca la finalización de ejecución del programa y que viene determinado por la palabra “FIN”.
Las reglas que hay que seguir para la confección de un ordinograma son las siguientes: a)
Todos los símbolos utilizados en el diseño deben estar conectados por medio de líneas de conexión o líneas de flujo de datos.
b)
Queda terminantemente prohibido el cruce de líneas de conexión, pues ello indica que el ordinograma no está correctamente diseñado.
c)
A un símbolo de proceso pueden llegarle varias líneas de conexión o flujo, pero de él sólo puede salir una.
d)
A un símbolo de decisión pueden llegarle varias líneas de conexión o flujo de datos, pero de él sólo puede salir una línea de cada una de las dos posibilidades existentes (verdadero o falso). 1-11
Desarrollo de Sistemas
e)
A un símbolo de inicio de proceso no llega ninguna línea de conexión o flujo y de él sólo puede partir una línea de conexión.
f)
A un símbolo de final de proceso o ejecución de programa pueden llegar muchas líneas de conexión pero de él no puede partir ninguna.
La simbología utilizada es la siguiente: A) Símbolos de operación o proceso SÍMBOLO
FUNCIÓN Terminal (marca el inicio, final o una parada necesaria realizada en la ejecución del programa). Operación de E/S en general (utilizada para mostrar la introducción de datos desde un periférico a la memoria del ordenador y la salida de resultados desde la memoria del ordenador a un periférico). Proceso u operación en general (utilizado para mostrar cualquier tipo de operación durante el proceso de elaboración de los datos depositados en la memoria). Subprograma o subrutina (utilizado para realizar una llamada a un subprograma o proceso, es decir, un módulo independiente cuyo objetivo es realizar una tarea y devolver el control de ejecución del programa al módulo principal).
B) Símbolos de decisión SÍMBOLO
FUNCIÓN Decisión de dos salidas (indica operaciones lógicas o comparativas seleccionando en función del resultado entre dos caminos alternativos que se pueden seguir). Decisión múltiple con “n” salidas (indica el camino que se puede seguir entre varias posibilidades según el resultado de la operación lógica o comparación establecida). Bucle definido, empleado para modificar una instrucción del bloque de instrucciones que a su vez producen una alteración o modificación en el comportamiento del programa.
1-12
Modelo conceptual de datos
C) Líneas de flujo SÍMBOLO
FUNCIÓN Flechas indicadoras de la dirección del flujo de datos. Línea conectora, también llamada línea de flujo de datos (permite la conexión entre los diferentes símbolos utilizados en el diseño).
D) Símbolos de conexión SÍMBOLO
FUNCIÓN Conector (este símbolo es utilizado para el reagrupamiento de líneas de flujo).
Nº
Nº
Conector de líneas de flujo en la misma página (utilizado para enlazar dos partes cualesquiera del diseño a través de un conector de salida y un conector de entrada). Conector de líneas de flujo en distintas páginas (utilizado para enlazar dos partes cualesquiera del diseño a través de un conector de salida y un conector de entrada).
E) Símbolos de comentarios SÍMBOLO
FUNCIÓN Permite escribir comentarios a lo largo del diseño realizado.
4.3. Pseudocódigo Para evitar el exceso de espacio de los métodos anteriores, y hacer una primera visión del desarrollo de un programa, nació el pseudocódigo, que consiste en una técnica para expresar mediante lenguaje natural (sujetas a unas determinadas normas) el lógico desarrollo de un programa, es decir, su flujo de control. Cuentan con la ventaja de poder ser desarrollados con mayor facilidad y en poco tiempo, y luego sirven de soporte a la programación real al ser utilizados como base para la codificación del algoritmo en el lenguaje de programación que más nos interese. 1-13
Desarrollo de Sistemas
El pseudocódigo ha de ser considerado como una herramienta para el diseño de programas y no como una notación para la descripción de los mismos. Gracias a su flexibilidad permite obtener la solución de un problema mediante aproximaciones sucesivas, es decir, mediante el denominado diseño descendente. Todo pseudocódigo debe permitir la descripción de: •
Las instrucciones de Entrada/Salida.
•
Las Instrucciones de proceso.
•
Las sentencias de control de flujo de ejecución.
•
Acciones compuestas que hay que refinar posteriormente.
•
Cualquier proceso relacionado con los datos: —
Describir datos.
—
Definir tipos de datos.
—
Definir y usar constantes y variables.
—
Archivos.
—
Objetos.
4.3.1. Acciones simples También denominadas instrucciones primitivas, son aquellas que el procesador ejecuta de forma inmediata: •
Asignación
variable_valor
•
Entrada
leer variable
•
Salida
escribir expresión
4.3.2. Sentencias de control También denominadas sentencias estructuradas, son aquellas que controlan el flujo de ejecución de otras instrucciones. •
Secuencial. Se ejecutan en el mismo orden en el que aparecen escritas. Leer nota suma=suma+nota media=suma/5 escribir media
1-14
Modelo conceptual de datos
•
Alternativa —
Simple Si condicion entonces Acción1 Acción2 finsi
—
Doble Si condicion entonces Acción1 Acción2 Sino Acción3 Acción4 finsi
—
Múltiple Según sea expresión V1 hacer a1;a2 V2 hacer a3;a4 ….. otro hacer a5;a6 Fin según
•
Repetitiva —
Mientras Mientras condición hacer Acción1 Acción2 finMientras
1-15
Desarrollo de Sistemas
—
Repetir Repetir Acción1 Acción2 Hasta que condición
—
Para
Para contador de min a max con incremento in hacer Acción1 Acción2 FinPara
4.3.3. Acciones compuestas Es una acción que ha de ser realizada dentro del algoritmo, pero que aún no está resuelta en términos de acciones simples y sentencias de control. Se denominan subprogramas.
4.3.4
Comentarios
Son líneas aclarativas cuyo fin es el de aclarar la comprensión del programa. Estas líneas son ignoradas a la hora de ejecutar el programa. Se escribirán en cualquier línea a partir de un símbolo que los identifique, como por ejemplo **. ** Comentario aclarativo de cualquier aspecto del programa
4.3.5. Datos del programa La finalidad primordial de un programa es la de realizar cálculos con una serie de datos. La información, ya sea de entrada o generada como salida, debe guardarse en algún lugar. Es necesario saber previamente cuál será su cantidad, sus nombres y sus tipos con lo que podré limitar el conjunto de operaciones que se podrán realizar con ellos a lo largo del programa. Este conjunto de datos se denomina entorno. 1-16
Modelo conceptual de datos
Entorno: I es numérica entera Euros es numérica real Apellidos es alfanumérica
4.3.6. Programa Es la solución final de un problema. Consiste en la unión del entorno y del algoritmo precedido cada uno de una etiqueta. Programa Nombre_Programa Entorno: ** Descripción de los datos … Algoritmo: ** Descripción de las acciones … FinPrograma
Idéntico para los programas
SubPrograma Nombre_Programa Entorno: ** Descripción de los datos … Algoritmo: ** Descripción de las acciones … FinSubPrograma
1-17
Desarrollo de Sistemas
4.4.
Paso de pseudocódigo a diagrama de flujo y viceversa • PSEUDOCÓDIGO
Variable=Expresión
ORDINOGRAMA
Variable_Expresi–n
Leer Variable
Escribir Expresión
Acción 1
Acción 2
Acción 3
Acci–n1
Acci–n2
Acci–n3
Sin condición entonces Acción 1 Acción 2 Finsi Acciones
1-18
Modelo conceptual de datos
Sin condición entonces Acción 1 Acción 2 Condición
Sino Acción 3
Acciones
Acciones
Acción 4 Finsi
Según sea expresión V1 hacer a1;a2 V1 hacer a3;a4 … Acciones
Acciones
Acciones
otro hacer a5;a6 Fin según
Mientras condición hacer Acción1 Acción2 FinMientras Acciones
1-19
Desarrollo de Sistemas
Repetir Acción1
Acciones
Acción2 Hasta que condición
C_inicio
Para C de inicio a fin con incremento inc hacer
fin
Acción1 Acción2
Acciones
FinPara C_C+Inc
Acción compuesta
**Comentario
1-20
Nombre
Comentario
Modelo conceptual de datos
Programa NombrePrograma
Inicio
Entorno **Datos
Acciones
Algoritmo Acción1 Acción2
..... Acciones
… FinPrograma
5.
Fin
Descomposición en niveles. Flujogramas
5.1.
Diagramas de Flujo de Datos (DFD)
Se utilizan fundamentalmente en la fase de Análisis para la elaboración del modelo lógico de procesos. En un DFD se representa, definiendo los límites del sistema en estudio, el movimiento o flujo de la información a través del sistema, los procesos que transforman dicha información, y los almacenamientos intermedios que son necesarios; todo ello desde un punto de vista puramente lógico, sin tener en cuenta ni representar ninguna restricción o aspecto físico ni ningún tipo de secuencia u orden de ejecución. Los DFD se utilizan para representar los sucesivos niveles de descomposición realizados durante el análisis del sistema, comenzando con una descripción muy general del sistema denominada de nivel 0 (DFD-0) a partir de la cual se producen sucesivos diagramas con niveles de detalle cada vez mayor (DFD-1, DFD-2, etc). Así, en un DFD-0, también conocido como diagrama de contexto, figura un único proceso que representa al sistema completo y las entidades externas que interaccionan con él; en el DFD-1 se representan los subsistemas; en los DFD-2 (uno por subsistema) las funciones de cada subsistema, en el DFD-3 las subfunciones asociadas a los eventos del sistema; y, finalmente, en el DFD4, último nivel al que normalmente llegará la descomposición, los procesos necesarios para el tratamiento de cada subfunción. Los componentes que aparecen en un DFD son: —
Las Entidades Externas (simbolizadas mediante rectángulos o elipses e identificadas por su nombre) que representan otros sistemas, organizaciones o personas externas al sistema pero que interaccionan con él recibiendo o aportando información. Las entidades externas proporcionan la conexión del sistema con el mundo exterior.
—
Los Procesos (rectángulos que encierran la identificación del proceso) que representan las actividades que suponen transformación o
1-21
Desarrollo de Sistemas
manipulación de datos. Un proceso no puede ser ni fuente ni sumidero de datos, por lo que un proceso siempre tendrá al menos un flujo de datos de entrada y al menos uno de salida. —
Los Almacenes de Datos (dos líneas paralelas con una identificación) que representan depósitos de información dentro del sistema, ya sean permanentes por afectar a información que el sistema debe guardar, o transitorios, por ser información que se utiliza en un proceso diferente del que la produce con el que no está sincronizado (recuérdese que no se tienen en cuenta restricciones físicas). Siempre deberá existir un proceso entre una entidad externa y un almacén de datos. Los almacenes de datos sirven de enlace del DFD con el modelo lógico de datos: cada almacén principal de un DFD representa un conjunto completo de entidades del modelo de datos (una o varias entidades), y cada entidad del modelo de datos pertenece a un único almacén principal de un DFD; esto facilitará las validaciones cruzadas entre los dos diagramas.
—
Los Flujos de Datos (flechas con la identificación de la información que transportan), que representan la comunicación entre procesos, almacenes y entidades externas. Los flujos de datos portan información, no son activadores de procesos.
5.2.
Modelos de datos
Los modelos de datos se usan en diferentes fases del desarrollo. Nos referiremos aquí al modelo conceptual de datos, descripción de alto nivel utilizada en la fase de Planificación de Sistemas, para la que se recomienda utilizar la técnica del modelo entidad relación, y al modelo lógico de datos elaborado en la fase de análisis de sistemas, para la que se recomienda la técnica del diagrama de estructura de datos (DED). En ambos casos se trata de representaciones del nivel lógico de los datos, válidas para describir las necesidades de información mediante estructuras no redundantes, sin inconsistencias, seguras e íntegras, y desprovistas de todo tipo de condicionantes, como pudieran ser los impuestos por los procesos que deba sufrir, o de tipo físico, como almacenamientos, etc. Será en la fase de diseño del sistema cuando el modelo lógico de datos servirá de base para especificar las estructuras de datos físicas (esquema interno) que deben implantarse para el almacenamiento de datos, normalmente sobre un SGBDR (Sistema Gestor de Bases de Datos Relacional) y las visiones específicas de los datos (esquema externo) precisadas por los diferentes componentes o programas del sistema. Las dos técnicas son muy similares en cuanto a los elementos que las constituyen, por lo que su descripción se centrará en la del modelo entidad-relación, señalándose seguidamente las diferencias para el diagrama de estructura de datos. En un modelo entidad-relación (MER) aparecen los elementos siguientes: •
1-22
Las Entidades, simbolizadas con cajas rectangulares e identificadas por un nombre, que representan objetos o conceptos del mundo acerca de los cuales el sistema precisa manejar información. Dicha
Modelo conceptual de datos
información se modeliza como atributos de dichas entidades. Por ejemplo, un sistema de gestión de pedidos representará la entidad CLIENTE con, entre otros atributos, el nombre del cliente, la dirección, teléfono, etc.; la entidad PEDIDO, con atributos número de pedido, estado del mismo; la entidad PRODUCTO, serie, precio, etc. •
5.3.
Las Relaciones entre entidades, simbolizadas por rombos sobre las líneas que enlazan las entidades relacionadas e identificadas por un nombre, que representan las interrelaciones existentes entre entidades. Las relaciones se caracterizan por su cardinalidad (1:1, 1:N ó N:M), según sean una o varias las ocurrencias de las entidades de cada lado que participan en las instancias de la relación (así un pedido puede consistir de varios [M] productos y cualquier producto puede figurar en pedidos diferentes [N]). Las relaciones pueden ser obligatorias (cuando exigen al menos una ocurrencia de cada una de las entidades que participan), opcionales (cuando la ocurrencia de la entidad opcional no es necesaria) o exclusiva (cuando la ocurrencia de una de las relaciones de la entidad implica que no tiene lugar la ocurrencia de otras posibles relaciones con otras entidades).
Diagramas de datos (DED) Las diferencias fundamentales en el diagrama de estructura de datos son:
6.
•
En el DED sólo se admiten relaciones de cardinalidad 1:N. Las relaciones 1:1 dan lugar a una única entidad. Las relaciones N:M se representan definiendo una entidad adicional sin correspondencia con un objeto real que sirve de enlace con las dos entidades originales mediante relaciones (1:N) y (1:M) respectivamente.
•
Las relaciones son de tipo binario, esto es, sólo entre dos entidades, mientras que en el modelo entidad relación pueden existir relaciones entre más de dos entidades, razón por la cual su riqueza descriptiva es mayor y se prefiere este modelo para representar modelos del nivel conceptual.
Conclusiones
En línea con lo expresado en los apartados anteriores, y con objeto de dotar a las diferentes Unidades de Tecnología de la Información de la Administración de un entorno que facilite la construcción de sus sistemas de información siguiendo prácticas metodológicas, el Consejo Superior de Informática y para el impulso de la Administración Electrónica, en el desarrollo de su línea estratégica de mejora de la calidad y productividad en el desarrollo de software, promovió la elaboración de una metodología para el desarrollo de sistemas de información para su uso en proyectos informáticos de las Administraciones Públicas. Fruto de ello es la metodología MÉTRICA, actualmente en su versión 3, conocida como MÉTRICA 3. La metodología esta constituida por fases. Cada una de esta fases, a su vez, se estructura en módulos de contenido homogéneo para los que se des-
1-23
Desarrollo de Sistemas
criben las actividades y tareas a realizar, así como los productos a obtener y una recomendación sobre la posible o posibles técnicas a utilizar en cada punto. La identificación de los productos a obtener en cada momento facilita la introducción de hitos en el proyecto de desarrollo, elemento imprescindible para la planificación y el seguimiento y control de la ejecución del proyecto. Por otro lado, estos productos permiten enlazar con las actividades de garantía de calidad previstas en el Plan General de Garantía de Calidad que se describen más adelante en este tema. Aún siendo altamente formal en su planteamiento, MÉTRICA 3 es una metodología que pretende tener un carácter flexible en su adaptación a una amplia variedad de proyectos de desarrollo de sistemas de información, debiéndose en cada caso, en función de las características específicas de cada proyecto, adoptar el modelo de ciclo de vida que es más apropiado para efectuar el desarrollo y, a partir de esta elección, decidir qué actividades han de efectuarse, qué productos obtenerse, etc, con qué énfasis y en qué secuencia. La utilización de herramientas CASE que soporten, de una manera completa, las técnicas propuestas en la metodología, permitirá optimizar considerablemente el esfuerzo de desarrollo. En la siguiente web puedes consultar: http://administracionelectronica.gob.es/pae_Home/pae_Documentacion/ pae_Metodolog/pae_Metrica_v3#.VaTZ9vvhdM4.
1-24
Tema 2 Diseño de bases de datos. Diseño lógico y físico. El modelo lógico relacional. Normalización.
Desarrollo de Sistemas
Guión-resumen
1. Diseño de bases de datos 1.1. Diseño conceptual de bases de datos
4. Normalización 5. Integridad de la base de datos 5.1. Integridad del dominio
2. Diseño lógico y físico
5.2. Integridad de entidad
2.1. Diseño lógico 2.2. Diseño físico 3. El modelo lógico relacional 3.1. Introducción 3.2. Metodología de diseño lógico en el modelo relacional
2-2
5.3. Integridad referencial 6
El Modelo físico Relacional 6.1. Introducción 6.2. Metodología de diseño físico para bases de datos relacionales
Diseño de bases de datos
1.
Diseño de bases de datos
1.1.
Diseño conceptual de bases de datos
Es una etapa bastante compleja sobre todo para el diseñador de la base de datos. En esta etapa se tiene que construir un esquema de la información que utiliza la empresa, independientemente de cualquier consideración física (esquema conceptual). El diseñador debe comprender muy bien los datos que utiliza la empresa para de ahí poder obtener las tablas, las relaciones, los tipos de campos, etc. El objetivo es comprender: —
La naturaleza de los datos.
—
La perspectiva que cada usuario tiene de los datos.
—
El uso de los datos a través de las áreas de aplicación.
Este es un período de mucha conversación entre el diseñador y la empresa; el objetivo es que las dos partes entiendan la notación utilizada en el esquema. La más popular es la notación del modelo entidad/relación. El esquema conceptual se construye utilizando la información que se encuentra en la especificación de los requisitos de usuario. Este diseño es completamente independiente de los aspectos de implementación, como puede ser el SGBD que se vaya a usar, los programas de aplicación, los lenguajes de programación, el hardware disponible o cualquier otra consideración física. Durante todo el proceso de desarrollo de este esquema se deben de realizar pruebas y validarlas con los requisitos de los usuarios. El esquema conceptual es una fuente de información para el posterior diseño lógico de la base de datos.
2-3
2-4
ENTRE NIVELES
INDEPENDENCIA
MODELOS
Es una descripción de la implementación en memoria secundaria. FÍSICO *Obterner tablas. *Estructuras de datos. *Restricciones. *Modelos de seguridad.
Es una fuente de info para el físico.
LÓGICO Normalizar en una técnica que el módelo lógico relacional utiliza para evitar las redundancias.
Construir un esquema de la información siguiendo un módelo lógico independiene del SGBD.
INTERNO
CONCEPTUAL
EXTERNO
ANSI/SPARK
MODELO RELACIONAL
Cada elemento del modelo LÓGICO ha de convertirse en un objeto del modelo FÍSICO. Ha de describir cómo se implementa la BBDO en Memoria Secundaria (LDD). CREATE TABLE CREATE INDEX
Especifica CÓMO y DÓNDE van a guardarse los datos.
Que ha de convertirse en un Modelo Lógico de Datos (Relacional), en el que especificamos QUÉ ha de guardarse.
Crear una vición conceptual/abstracta.
MODELO ENTIDAD/RELACIÓN
Reflejar la estructura y las relaciones existentes entre los datos que queremos guardar. Aislar el nivel externo del interno. Se ha de crear el esquema conceptual que ha de: Identificar las estructuras de datos. Identificar las relaciones. Identificar las restricciones. Los pasos que habitualmente se dan son utilizar dos modelos y usar uno para crear el otro.
Guardar vista la de la información de los usuarios y/o aplicaciones. Aislar al usuario de los datos reales dentro del sistema. Aumentar la seguridad de los datos.
Desarrollo de Sistemas
Diseño de bases de datos
2. 2.1.
Diseño lógico y físico Diseño lógico
El diseño lógico es el proceso de construir un esquema de la información que utiliza la empresa, basándose en un modelo de base de datos específico, independiente del SGBD concreto que se vaya a utilizar y de cualquier otra consideración física. En esta etapa, se transforma el esquema conceptual en un esquema lógico que utilizará las estructuras de datos del modelo de base de datos en el que se basa el SGBD que se vaya a utilizar, como puede ser el modelo relacional, el modelo de red, el modelo jerárquico o el modelo orientado a objetos. Conforme se va desarrollando el esquema lógico, éste se va probando y validando con los requisitos de usuario. La normalización es una técnica que se utiliza para comprobar la validez de los esquemas lógicos basados en el modelo relacional, ya que asegura que las relaciones (tablas) obtenidas no tienen datos redundantes. Esta técnica se presenta en el capítulo dedicado al diseño lógico de bases de datos. El esquema lógico es una fuente de información para el diseño físico. Además, juega un papel importante durante la etapa de mantenimiento del sistema, ya que permite que los futuros cambios que se realicen sobre los programas de aplicación o sobre los datos, se representen correctamente en la base de datos. Tanto el diseño conceptual, como el diseño lógico, son procesos iterativos, tienen un punto de inicio y se van refinando continuamente. Ambos se deben ver como un proceso de aprendizaje en el que el diseñador va comprendiendo el funcionamiento de la empresa y el significado de los datos que maneja. El diseño conceptual y el diseño lógico son etapas clave para conseguir un sistema que funcione correctamente. Si el esquema no es una representación fiel de la empresa, será difícil, sino imposible, definir todas las vistas de usuario (esquemas externos), o mantener la integridad de la base de datos. También puede ser difícil definir la implementación física o el mantener unas prestaciones aceptables del sistema. Además, hay que tener en cuenta que la capacidad de ajustarse a futuros cambios es un sello que identifica a los buenos diseños de bases de datos. Por todo esto, es fundamental dedicar el tiempo y las energías necesarias para producir el mejor esquema que sea posible
2.2.
Diseño físico
Proceso que produce la descripción de la implementación de la base de datos en memoria secundaria: estructura de almacenamiento y métodos de acceso a los datos. Antes de empezar esta etapa se tiene que tener ya decidido el SGBD a utilizar, pues el esquema físico se adapta a él. En definitiva, el esquema físico es la implementación del esquema lógico. La mayoría de las veces se suele modificar el esquema físico para mejorar la base de datos (al fin y al cabo, es el momento cumbre) con lo cual el esquema lógico también suele sufrir modificaciones. Concretamente el diseño físico en el modelo relacional consiste en: 2-5
Desarrollo de Sistemas
3.
—
Obtener un conjunto de relaciones (tablas) y las restricciones que se deben cumplir sobre ellas.
—
Determinar las estructuras de almacenamiento y los métodos de acceso que se van a utilizar para conseguir unas prestaciones óptimas.
—
Diseñar el modelo de seguridad del sistema.
El modelo lógico relacional
A continuación se describen los pasos para llevar a cabo el diseño lógico. Ya que aquí se trata el diseño de bases de datos relacionales, en esta etapa se obtiene un conjunto de relaciones (tablas) que representen los datos de interés. Este conjunto de relaciones se valida mediante la normalización.
3.1.
Introducción
El objetivo del diseño lógico es convertir los esquemas conceptuales locales en un esquema lógico global que se ajuste al modelo de SGBD sobre el que se vaya a implementar el sistema. Mientras que el objetivo fundamental del diseño conceptual es la comprensión y expresividad de los esquemas conceptuales locales, el objetivo del diseño lógico es obtener una representación que use, del modo más eficiente posible, los recursos que el modelo de SGBD posee para estructurar los datos y para modelar las restricciones. Los modelos de bases de datos más extendidos son el modelo relacional, el modelo de red y el modelo jerárquico. El modelo orientado a objetos es también muy popular, pero no existe un modelo estándar orientado a objetos. El modelo relacional (y los modelos previos) carecen de ciertos rasgos de abstracción que se usan en los modelos conceptuales. Por lo tanto, un primer paso en la fase del diseño lógico consistirá en la conversión de esos mecanismos de representación de alto nivel en términos de las estructuras de bajo nivel disponibles en el modelo relacional.
3.2.
Metodología de diseño lógico en el modelo relacional
La metodología que se va a seguir para el diseño lógico en el modelo relacional consta de dos fases, cada una de ellas compuesta por varios pasos que se detallan a continuación: 1.
2-6
Construir y validar los esquemas lógicos locales para cada vista de usuario. •
Convertir los esquemas conceptuales locales en esquemas lógicos locales.
•
Derivar un conjunto de relaciones (tablas) para cada esquema lógico local.
•
Validar cada esquema mediante la normalización.
•
Validar cada esquema frente a las transacciones del usuario.
Diseño de bases de datos
2.
•
Dibujar el diagrama entidad-relación.
•
Definir las restricciones de integridad.
•
Revisar cada esquema lógico local con el usuario correspondiente.
Construir y validar el esquema lógico global. •
Mezclar los esquemas lógicos locales en un esquema lógico global.
•
Validar el esquema lógico global.
•
Estudiar el crecimiento futuro.
•
Dibujar el diagrama entidad-relación final.
•
Revisar el esquema lógico global con los usuarios.
En la primera fase, se construyen los esquemas lógicos locales para cada vista de usuario y se validan. En esta fase se refinan los esquemas conceptuales creados durante el diseño conceptual, eliminando las estructuras de datos que no se pueden implementar de manera directa sobre el modelo que soporta el SGBD, en el caso que nos ocupa, el modelo relacional. Una vez hecho esto, se obtiene un primer esquema lógico que se valida mediante la normalización y frente a las transacciones que el sistema debe llevar a cabo, tal y como se refleja en las especificaciones de requisitos de usuario. El esquema lógico ya validado se puede utilizar como base para el desarrollo de prototipos. Una vez finalizada esta fase, se dispone de un esquema lógico para cada vista de usuario que es correcto, comprensible y sin ambigüedad.
3.2.1. Convertir los esquemas conceptuales locales en esquemas lógicos locales En este paso, se eliminan de cada esquema conceptual las estructuras de datos que los sistemas relacionales no modelan directamente: a)
Eliminar las relaciones de muchos a muchos, sustituyendo cada una de ellas por una nueva entidad intermedia y dos relaciones de uno a muchos de esta nueva entidad con las entidades originales. La nueva entidad será débil, ya que sus ocurrencias dependen de la existencia de ocurrencias en las entidades originales.
b)
Eliminar las relaciones entre tres o más entidades, sustituyendo cada una de ellas por una nueva entidad (débil) intermedia que se relaciona con cada una de las entidades originales. La cardinalidad de estas nuevas relaciones binarias dependerá de su significado.
c)
Eliminar las relaciones recursivas, sustituyendo cada una de ellas por una nueva entidad (débil) y dos relaciones binarias de esta nueva entidad con la entidad original. La cardinalidad de estas relaciones dependerá de su significado. 2-7
Desarrollo de Sistemas
d)
Eliminar las relaciones con atributos, sustituyendo cada una de ellas por una nueva entidad (débil) y las relaciones binarias correspondientes de esta nueva entidad con las entidades originales. La cardinalidad de estas relaciones dependerá del tipo de la relación original y de su significado.
e)
Eliminar los atributos multievaluados, sustituyendo cada uno de ellos por una nueva entidad (débil) y una relación binaria de uno a muchos con la entidad original.
f)
Revisar las relaciones de uno a uno, ya que es posible que se hayan identificado dos entidades que representen el mismo objeto (sinónimos). Si así fuera, ambas entidades deben integrarse en una sola.
g)
Eliminar las relaciones redundantes. Una relación es redundante cuando se puede obtener la misma información que ella aporta mediante otras relaciones. El hecho de que haya dos caminos diferentes entre dos entidades no implica que uno de los caminos corresponda a una relación redundante, eso dependerá del significado de cada relación.
Una vez finalizado este paso, es más correcto referirse a los esquemas conceptuales locales refinados como esquemas lógicos locales, ya que se adaptan al modelo de base de datos que soporta el SGBD escogido.
3.2.2. Derivar un conjunto de relaciones (tablas) para cada esquema lógico local En este paso, se obtiene un conjunto de relaciones (tablas) para cada uno de los esquemas lógicos locales en donde se representen las entidades y relaciones entre entidades, que se describen en cada una de las vistas que los usuarios tienen de la empresa. Cada relación de la base de datos tendrá un nombre, y el nombre de sus atributos aparecerá, a continuación, entre paréntesis. El atributo o atributos que forman la clave primaria se subrayan. Las claves ajenas, mecanismo que se utiliza para representar las relaciones entre entidades en el modelo relacional, se especifican aparte indicando la relación (tabla) a la que hacen referencia. A continuación, se describe cómo las relaciones (tablas) del modelo relacional representan las entidades y relaciones que pueden aparecer en los esquemas lógicos. a)
Entidades fuertes. Crear una relación para cada entidad fuerte que incluya todos sus atributos simples. De los atributos compuestos incluir sólo sus componentes. Cada uno de los identificadores de la entidad será una clave candidata. De entre las claves candidatas hay que escoger la clave primaria; el resto serán claves alternativas. Para escoger la clave primaria entre las claves candidatas se pueden seguir estas indicaciones: •
2-8
Escoger la clave candidata que tenga menos atributos.
Diseño de bases de datos
•
Escoger la clave candidata cuyos valores no tengan probabilidad de cambiar en el futuro.
•
Escoger la clave candidata cuyos valores no tengan probabilidad de perder la unicidad en el futuro.
•
Escoger la clave candidata con el mínimo número de caracteres (si es de tipo texto).
•
Escoger la clave candidata más fácil de utilizar desde el punto de vista de los usuarios.
b)
Entidades débiles. Crear una relación para cada entidad débil incluyendo todos sus atributos simples. De los atributos compuestos incluir sólo sus componentes. Añadir una clave ajena a la entidad de la que depende. Para ello, se incluye la clave primaria de la relación que representa a la entidad padre en la nueva relación creada para la entidad débil. A continuación, determinar la clave primaria de la nueva relación.
c)
Relaciones binarias de uno a uno. Para cada relación binaria se incluyen los atributos de la clave primaria de la entidad padre en la relación (tabla) que representa a la entidad hijo, para actuar como una clave ajena. La entidad hijo es la que participa de forma total (obligatoria) en la relación, mientras que la entidad padre es la que participa de forma parcial (opcional). Si las dos entidades participan de forma total o parcial en la relación, la elección de padre e hijo es arbitraria. Además, en caso de que ambas entidades participen de forma total en la relación, se tiene la opción de integrar las dos entidades en una sola relación (tabla). Esto se suele hacer si una de las entidades no participa en ninguna otra relación.
d)
Relaciones binarias de uno a muchos. Como en las relaciones de uno a uno, se incluyen los atributos de la clave primaria de la entidad padre en la relación (tabla) que representa a la entidad hijo, para actuar como una clave ajena. Pero ahora, la entidad padre es la de “la parte del muchos” (cada padre tiene muchos hijos), mientras que la entidad hijo es la de “la parte del uno” (cada hijo tiene un solo padre).
Una vez obtenidas las relaciones con sus atributos, claves primarias y claves ajenas, sólo queda actualizar el diccionario de datos con los nuevos atributos que se hayan identificado en este paso.
3.2.3. Validar cada esquema mediante la normalización La normalización se utiliza para mejorar el esquema lógico, de modo que satisfaga ciertas restricciones que eviten la duplicidad de datos. La normalización garantiza que el esquema resultante se encuentra más próximo al modelo de la empresa, que es consistente y que tiene la mínima redundancia y la máxima estabilidad. 2-9
Desarrollo de Sistemas
La normalización es un proceso que permite decidir a qué entidad pertenece cada atributo. Uno de los conceptos básicos del modelo relacional es que los atributos se agrupan en relaciones (tablas) porque están relacionados a nivel lógico. En la mayoría de las ocasiones, una base de datos normalizada no proporciona la máxima eficiencia, sin embargo, el objetivo ahora es conseguir una base de datos normalizada por las siguientes razones: —
Un esquema normalizado organiza los datos de acuerdo a sus dependencias funcionales, es decir, de acuerdo a sus relaciones lógicas.
—
El esquema lógico no tiene por qué ser el esquema final. Debe representar lo que el diseñador entiende sobre la naturaleza y el significado de los datos de la empresa. Si se establecen unos objetivos en cuanto a prestaciones, el diseño físico cambiará el esquema lógico de modo adecuado. Una posibilidad es que algunas relaciones normalizadas se desnormalicen. Pero la desnormalización no implica que se haya malgastado tiempo normalizando, ya que mediante este proceso el diseñador aprende más sobre el significado de los datos. De hecho, la normalización obliga a entender completamente cada uno de los atributos que se han de representar en la base de datos.
—
Un esquema normalizado es robusto y carece de redundancias, por lo que está libre de ciertas anomalías que éstas pueden provocar cuando se actualiza la base de datos.
—
Los equipos informáticos de hoy en día son mucho más potentes, por lo que en ocasiones es más razonable implementar bases de datos fáciles de manejar (las normalizadas), a costa de un tiempo adicional de proceso.
—
La normalización produce bases de datos con esquemas flexibles que pueden extenderse con facilidad.
El objetivo de este paso es obtener un conjunto de relaciones que se encuentren en la forma normal de Boyce-Codd. Para ello, hay que pasar por la primera, segunda y tercera formas normales. El proceso de normalización se describe en el apartado 7.3.
3.2.4. Validar cada esquema frente a las transacciones del usuario El objetivo de este paso es validar cada esquema lógico local para garantizar que puede soportar las transacciones requeridas por los correspondientes usuarios. Estas transacciones se encontrarán en las especificaciones de requisitos de usuario. Lo que se debe hacer es tratar de realizar las transacciones de forma manual utilizando el diagrama entidad-relación, el diccionario de datos y las conexiones que establecen las claves ajenas de las relaciones (tablas). Si todas las transacciones se pueden realizar, el esquema queda validado. Pero si alguna transacción no se puede realizar, seguramente será porque alguna entidad, relación o atributo no se ha incluido en el esquema.
2-10
Diseño de bases de datos
3.2.5. Dibujar el diagrama entidad-relación En este momento, se puede dibujar el diagrama entidad-relación final para cada vista de usuario que recoja la representación lógica de los datos desde su punto de vista. Este diagrama habrá sido validado mediante la normalización y frente a las transacciones de los usuarios.
3.2.6. Definir las restricciones de integridad Las restricciones de integridad son reglas que se quieren imponer para proteger la base de datos, de modo que no pueda llegar a un estado inconsistente. Hay cinco tipos de restricciones de integridad. a)
Datos requeridos. Algunos atributos deben contener valores en todo momento, es decir, no admiten nulos.
b)
Restricciones de dominios. Todos los atributos tienen un dominio asociado, que es el conjunto los valores que cada atributo puede tomar.
c)
Integridad de entidades. El identificador de una entidad no puede ser nulo, por lo tanto, las claves primarias de las relaciones (tablas) no admiten nulos.
d)
Integridad referencial. Una clave ajena enlaza cada tupla de la relación hijo con la tupla de la relación padre que tiene el mismo valor en su clave primaria. La integridad referencial dice que si una clave ajena tiene un valor (si es no nula), ese valor debe ser uno de los valores de la clave primaria a la que referencia. Hay varios aspectos a tener en cuenta sobre las claves ajenas para lograr que se cumpla la integridad referencial. 1.
¿Admite nulos la clave ajena? Cada clave ajena expresa una relación. Si la participación de la entidad hijo en la relación es total, entonces la clave ajena no admite nulos; si es parcial, la clave ajena debe aceptar nulos.
2.
¿Qué hacer cuando se quiere borrar una ocurrencia de la entidad padre que tiene algún hijo? O lo que es lo mismo, ¿qué hacer cuando se quiere borrar una tupla que está siendo referenciada por otra tupla a través de una clave ajena? Hay varias respuestas posibles: •
Restringir: no se pueden borrar tuplas que están siendo referenciadas por otras tuplas.
•
Propagar: se borra la tupla deseada y se propaga el borrado a todas las tuplas que le hacen referencia.
•
Anular: se borra la tupla deseada y todas las referencias que tenía se ponen, automáticamente, a nulo (esta respuesta sólo es válida si la clave ajena acepta nulos).
2-11
Desarrollo de Sistemas
3.
e)
•
Valor por defecto: se borra la tupla deseada y todas las referencias toman, automáticamente, el valor por defecto (esta respuesta sólo es válida si se ha especificado un valor por defecto para la clave ajena).
•
No comprobar: se borra la tupla deseada y no se hace nada para garantizar que se sigue cumpliendo la integridad referencial.
¿Qué hacer cuando se quiere modificar la clave primaria de una tupla que está siendo referenciada por otra tupla a través de una clave ajena? Las respuestas posibles son las mismas que en el caso anterior. Cuando se escoge propagar, se actualiza la clave primaria en la tupla deseada y se propaga el cambio a los valores de clave ajena que le hacían referencia.
Reglas de negocio. Cualquier operación que se realice sobre los datos debe cumplir las restricciones que impone el funcionamiento de la empresa.
Todas las restricciones de integridad establecidas en este paso se deben reflejar en el diccionario de datos para que puedan ser tenidas en cuenta durante la fase del diseño físico.
3.2.7. Revisar cada esquema lógico local con el usuario correspondiente Para garantizar que cada esquema lógico local es una fiel representación de la vista del usuario lo que se debe hacer es comprobar con él que lo reflejado en el esquema y en la documentación es correcto y está completo. • Relación entre el esquema lógico y los diagramas de flujo de datos El esquema lógico refleja la estructura de los datos a almacenar que maneja la empresa. Un diagrama de flujo de datos muestra cómo se mueven los datos en la empresa y los almacenes en donde se guardan. Si se han utilizado diagramas de flujo de datos para modelar las especificaciones de requisitos de usuario, se pueden utilizar para comprobar la consistencia y completitud del esquema lógico desarrollado. Para ello: —
Cada almacén de datos debe corresponder con una o varias entidades completas.
—
Los atributos en los flujos de datos deben corresponder a alguna entidad.
Los esquemas lógicos locales obtenidos hasta este momento se integrarán en un solo esquema lógico global en la siguiente fase para modelar los datos de toda la empresa.
3.2.8. Mezclar los esquemas lógicos locales en un esquema lógico global En este paso, se deben integrar todos los esquemas locales en un solo esquema global. En un sistema pequeño, con dos o tres vistas de usuario y unas pocas entidades y relaciones, es relativamente sencillo comparar los 2-12
Diseño de bases de datos
esquemas locales, mezclarlos y resolver cualquier tipo de diferencia que pueda existir. Pero en los sistemas grandes, se debe seguir un proceso más sistemático para llevar a cabo este paso con éxito: 1.
Revisar los nombres de las entidades y sus claves primarias.
2.
Revisar los nombres de las relaciones.
3.
Mezclar las entidades de las vistas locales.
4.
Incluir (sin mezclar) las entidades que pertenecen a una sola vista de usuario.
5.
Mezclar las relaciones de las vistas locales.
6.
Incluir (sin mezclar) las relaciones que pertenecen a una sola vista de usuario.
7.
Comprobar que no se ha omitido ninguna entidad ni relación.
8.
Comprobar las claves ajenas.
9.
Comprobar las restricciones de integridad.
10. Dibujar el esquema lógico global. 11. Actualizar la documentación.
3.2.9. Validar el esquema lógico global Este proceso de validación se realiza, de nuevo, mediante la normalización y mediante la prueba frente a las transacciones de los usuarios. Pero ahora sólo hay que normalizar las relaciones que hayan cambiado al mezclar los esquemas lógicos locales y sólo hay que probar las transacciones que requieran acceso a áreas que hayan sufrido algún cambio.
3.2.10. Estudiar el crecimiento futuro En este paso, se trata de comprobar que el esquema obtenido puede acomodar los futuros cambios en los requisitos con un impacto mínimo. Si el esquema lógico se puede extender fácilmente, cualquiera de los cambios previstos se podrá incorporar al mismo con un efecto mínimo sobre los usuarios existentes.
3.2.11. Dibujar el diagrama entidad-relación final Una vez validado el esquema lógico global, ya se puede dibujar el diagrama entidadrelación que representa el modelo de los datos de la empresa que son de interés. La documentación que describe este modelo (incluyendo el esquema relacional y el diccionario de datos) se debe actualizar y completar.
2-13
Desarrollo de Sistemas
3.2.12. Revisar el esquema lógico global con los usuarios Una vez más, se debe revisar con los usuarios el esquema global y la documentación obtenida para asegurarse de que son una fiel representación de la empresa.
4.
Normalización
Este proceso tiene como objetivo comprobar que las tablas que forman la base de datos cumplen unas determinadas condiciones. La principal condición es evitar la redundancia (se entiende por redundancia la repetición de los datos albergados en la BD) y una cierta coherencia en la configuración mediante un esquema relacional de las entidades y relaciones del modelo conceptual (diagrama E-R). Gracias a la normalización se pueden evitar errores de diseño y anomalías en la actualización y borrado en las tablas de la base de datos, facilitando la gestión del administrador de la misma y de los desarrolladores de aplicaciones. Esquema relacional: conjunto de tablas con sus atributos. Se pretende comprobar si ese esquema es funcionalmente mejorable por medio de las reglas de normalización. Se dice que una tabla está en una determinada forma normal si satisface un cierto número de restricciones impuestas por esa regla. El número de estas reglas puede variar hasta seis (dependiendo de autores) pero hay tres de ellas que resultan básicas. Una tabla normalizada de acuerdo a la primera regla se dice que está en la primera forma normal (1NF). Una tabla normalizada de acuerdo a la segunda regla se dice que está en la segunda forma normal (2NF). Una tabla debe cumplir la primera forma antes que la segunda, y la segunda antes que la tercera pero después que la primera. Las normalizaciones mayores tratan con situaciones específicas y especiales, que los programadores suelen adaptar de forma individual. Primeramente veamos unos errores muy comunes en el diseño de bases de datos: Tenemos una base de datos con la cual pretendemos “controlar” a los propietarios de vehículos de una determinada localidad. Necesitamos, en principio, una tabla para almacenar dicha información.
2-14
Diseño de bases de datos
DNI
Nombre
Apellidos
Dirección
Marca
Modelo
Matrícula
5042211
Jorge
Arrainz
C/ Ronda
Audi
A3
abc9090
4012123
Eva
Vall
C/ Tórtola
SEAT
Ibiza
11221bb
5042211
Jorge
Arrainz
C/ Ronda
BMW
850
m-2277-uv
3555655
Ana
Martín
C/ Jadraque
Mercedes
600
434344bb
8989888
Alicia
Márquez
C/ Hita
SEAT
Ibiza
454544as
El error más visible, como se puede apreciar, es la duplicidad de datos. Jorge, que es propietario de dos vehículos ocupa dos registros (1º y 3º), por lo cual se repiten varios campos: DNI, Nombre, Apellidos y Dirección. ¿No sería ocupar espacio de almacenamiento en demasía? Pensemos en otra posibilidad. DNI
Nomb
Marca
Model
Matric
5042211
Jorge
Audi
A3
abc9090
4012123
Eva
SEAT
Ibiza
11221bb
3555655
Ana
Mercedes
600
434344bb
8989888
Alicia
SEAT
Ibiza
454544as
Marca2 BMW
Modelo2 850
Matricu2 m-2277uv
Esta tabla resulta aún peor. Al intentar evitar la duplicidad de los datos en la tabla, se está introduciendo duplicidad en la misma estructura de la tabla. Además debemos preguntarnos ¿cuántas personas son propietarias de dos o más vehículos? ¿En cuántas filas los campos Marca2, Modelo2 y Matrícula2 quedarían vacíos? En miles, pues la inmensa mayoría de gente no tiene dos o más vehículos por persona. Primera forma normal Una tabla no debe contener grupos repetidos. Basándonos en el segundo ejemplo anterior, no podríamos tener los campos matrícula y matrícula2, modelo y modelo2, etc. 2-15
Desarrollo de Sistemas
Por lo cual un propietario con dos o más vehículos se almacenaría así: DNI
Nombre
Apellidos
Dirección
Marca
Modelo
Matrícula
5042211
Jorge
Arrainz
C/ Ronda
Audi
A3
abc9090
4012123
Eva
Vall
C/ Tórtola
SEAT
Ibiza
11221bb
5042211
Jorge
Arrainz
C/ Ronda
BMW
850
m-2277-uv
3555655
Ana
Martín
C/ Jadraque
Mercedes
600
434344bb
8989888
Alicia
Márquez
C/ Hita
SEAT
Ibiza
454544as
La primera forma normal no requiere que se divida la tabla en otras. En vez de eso, convierte algunas de las columnas de la tabla en filas adicionales. Ventajas que tiene, siempre dentro de que se puede mejorar, claro está: —
Carece de campos vacíos.
—
Carece de limitaciones. Si un propietario tiene, por ejemplo, seis vehículos, tendríamos que haber utilizado seis campos matrícula, seis campos modelo, etc. De esta forma, el propietario aparecería en seis filas.
Una vez que la tabla se encuentra en la primera forma normalizada, se debe pasar a la segunda regla de normalización. Segunda forma normal La segunda regla de normalización dice que cualquier campo que no dependa totalmente de la clave principal se debe mover a otra tabla. Aplicándolo a nuestro ejemplo: Tabla propietarios DNI
2-16
Nombre
Apellidos
Dirección
5042211
Jorge
Arrainz
C/ Ronda
4012123
Eva
Vall
C/ Tórtola
3555655
Ana
Martín
C/ Jadraque
8989888
Alicia
Marquez
C/ Hita
Diseño de bases de datos
Tabla vehículos DNI
Marca
Modelo
Matrícula
5042211
Audi
A3
abc9090
4012123
SEAT
Ibiza
11221bb
5042211
BMW
850
m-2277-uv
3555655
Mercedes
600
434344bb
8989888
SEAT
Ibiza
454544as
Ahora las tablas quedan mejor estructuradas. Por medio del campo clave DNI podemos acceder a la información de un propietario y/o de su vehículo/s, y los datos apenas se repiten, salvo el DNI que es clave primaria y que en una relación de uno a varios (un propietario, uno o más vehículos) es imposible de evitar. Vamos a ampliar la tabla de vehículos con dos nuevos campos: cilindrada e impcircu (impuesto de circulación, según cilindrada). Tabla vehículos DNI
Marca
Modelo
Matrícula
Cilindrada
impcircu
5042211
Audi
A3
abc9090
1900
105
4012123
SEAT
Ibiza
11221bb
1600
82
5042211
BMW
850
m-2277-uv
3000
203
3555655
Mercedes
600
434344bb
3000
203
8989888
SEAT
Ibiza
454544as
1600
82
Ahora imaginémonos que cambian el importe del campo de impuestos para algunas determinadas cilindradas. La tabla necesita una modificación, pero esa modificación se va a repetir innecesariamente en algún registro (SEAT Ibiza), ya que existen registros iguales. Tercera forma normal Esta tercera regla dice que no debe haber dependencias entre campos que no sean clave. Aquí el problema reside en que los relativos a las caracte2-17
Desarrollo de Sistemas
rísticas del vehículo se repiten en demasía y pecan de tener una dependencia de un campo no clave, impcircu. Volvamos a normalizar. Tabla vehículos Idvehículo
Marca
Modelo
Cilindrada
Au31901
Audi
A3
1900
Seibi1603
SEAT
Ibiza
1600
Bm850i
BMW
850
3000
Me600se
Mercedes
600
3000
Tabla impuestos Cilindrada
Impuesto_circulación
EmisiónCO2
Características
1100
50
6%
Bajo consumo…
1200
55
8%
Motor ecológico…
1600
82
15%
Versiones con turbo…
1900
105
18%
Consumo alto, turbo…
3000
203
25%
Alto consumo…
Esta tabla, además de no repetir tanta marca y cilindrada, muestra más información a cerca del tipo de motor. El campo cilindrada es la clave principal. Tabla propietarios DNI
2-18
Nombre
Apellidos
Dirección
5042211
Jorge
Arrainz
C/ Ronda
4012123
Eva
Vall
C/ Tórtola
3555655
Ana
Martín
C/ Jadraque
8989888
Alicia
Marquez
C/ Hita
Diseño de bases de datos
Esta tabla pasaríamos a normalizarla, para que no aparezcan varias veces los datos personales de un mismo propietario, sino gracias al DNI poder acceder a la tabla de los datos personales. Tabla contribuyentes DNI
IDvehículo
5042211
Au31901
4012123
Seibi1603
3555655
Me600se
8989888
Seibi1603
5042211
Bm850i
Con esta tabla tenemos acceso a los datos personales y a los datos del vehículo, y como las otras tablas están relacionadas entre sí, podemos acceder a la información de todas las tablas a la vez. La segunda y tercera forma son prácticamente iguales (de hecho algunos autores las tratan como una sola). El diagrama de relaciones de una base de datos normalizada quedaría así:
La mejor forma es la intuición y el sentido común para dividir los datos en tablas diferentes. Use una tabla diferente para cada entidad; posteriormente establezca las relaciones entre las tablas, siempre que se pueda. Si alguna tabla no tiene campo común con otra tabla para poder relacionarlas, deberá crearse una tabla intermedia que contenga dos campos; los 2-19
Desarrollo de Sistemas
campos clave de ambas tablas. Algo parecido a la tabla contribuyentes de nuestro ejemplo, que permite relacionar la tabla propietarios con las demás. Forma Normal de Boyce-Codd (BCFN) Una relación está en la forma normal de Boyce-Codd si, y sólo si, todo determinante es una clave candidata. La 2FN y la 3FN eliminan las dependencias parciales y las dependencias transitivas de la clave primaria. Pero este tipo de dependencias todavía pueden existir sobre otras claves candidatas, si éstas existen. La BCFN es más fuerte que la 3FN, por lo tanto, toda relación en BCFN está en 3FN. La violación de la BCFN es poco frecuente ya que se da bajo ciertas condiciones que raramente se presentan. Se debe comprobar si una relación viola la BCFN, si tiene dos o más claves candidatas compuestas que tienen al menos un atributo en común. Respecto a la normalización y vínculos Cada vez que conecta dos tablas con una clave común, DBMS (Data Base Manager System o lo que es lo mismo, Sistema Gestor de Bases de Datos) debe llevar a cabo una operación conocida como unión. Esta unión se realiza por medio de las claves principal (Primary Key) y externa (Foreign Key). Esta operación de unir “momentáneamente” tablas es muy costosa, debido a su lentitud, por lo cual se aconseja usarla lo menos posible. Es normal que algunas bases de datos utilicen incluso seis uniones para acceder a una información, aunque SQL permite hasta 16 uniones. En verdad existe un conflicto entre normalización y uniones. Si creamos un gran número de tablas, tendremos que crear un gran número de uniones. Hay muchos administradores y programadores de bases de datos que prefieren repetir un “poco” la información utilizando menos tablas con tal de poder acceder más rápidamente a los datos. Que quede claro que para esto no hay reglas, sólo el criterio personal y estudiar muy bien el tipo y número de accesos o peticiones que se van a realizar sobre las tablas.
5.
Integridad de la base de datos
Diseñar la base de datos es sólo el primer paso, el mayor problema es que la bases de datos se mantenga en perfecto estado, operativa. Para el mantenimiento y protección de la base de datos los SGBD se ayudan de unas determinadas reglas de integridad y los administradores y programadores son los encargados de aplicarlas.
5.1.
Integridad del dominio
Es una regla de integridad muy simple que indica que cada columna (campo) debe tener un tipo único de datos. Por ejemplo, si la columna suel2-20
Diseño de bases de datos
do está definida como campo de tipo numérico, el usuario no podrá introducir fechas en él. El gestor devolverá un error y el usuario deberá actuar en consecuencia.
5.2.
Integridad de entidad
Significa que cada entidad (tabla) deberá tener una clave principal válida. Por ejemplo, si se permite valores nulos (null) para esa clave principal, obviamente no se podrán conectar otras tablas a esta fila. Que ocurra esto es muy difícil, pues ningún DBMS permite que si el campo es clave, pueda contener un valor null. A la hora de creación de la tabla y a la hora de introducir datos, obligan a cumplimentar dicho campo.
5.3.
Integridad referencial
Es una regla de integridad que se encarga de asegurar que las distintas relaciones entre tablas tengan siempre validez. Por ejemplo, tenemos una tabla llamada Vendedores y otra llamada Ventas. En la tabla Vendedores el campo clave es el campo DNI. En la tabla Ventas habrá que tener un campo DNI para conocer qué empleado ha realizado la venta, además de ser el campo que relaciona esta tabla con la de Vendedores. Tomemos como ejemplo que, a la hora de dar de alta un pedido (tabla Pedidos), el usuario, sin querer, introduzca un DNI de vendedor que no exista en la tabla de Vendedores. A la hora de pedir información de quién efectuó esa venta, no podríamos conocer los datos del vendedor, más que nada porque dicho vendedor no existe. Si exigimos integridad referencial, cuando procedemos a introducir un DNI en la tabla de Ventas que no está dado de alta en la tabla de Vendedores, el gestor envía automáticamente un mensaje indicando el error de integridad. Lo mismo pasaría si damos de baja a un vendedor en la tabla de Vendedores. ¿Qué sucedería con sus ventas? Algunos gestores tienen la posibilidad de activar la opción de borrado o actualización en cascada. Si se borra o modifica la clave principal, todos sus registros de la tabla relacionada se borrarán o modificarán de forma automática. SQL Server no soporta actualizaciones en cascada porque usa un mecanismo aún mejor: desencadenantes. Un desencadenante es un procedimiento que se invoca automáticamente, como un evento. Por ejemplo, si un vendedor realiza una nueva venta, se puede usar un desencadenante para que el importe de la misma se acumule en el campo total_venta_acumulada de la tabla Vendedores. Un desencadenante es un conjunto de sentencias de programación, como son los bucles, las sentencias de control de flujo, las variables, etc., mezclados con SQL. Como es de imaginar, la integridad referencial se debe indicar antes de introducir la información, en caso contrario, nos podría dar bastantes errores de datos que no coincidan en ambas tablas, errores que el usuario, con paciencia, debe solucionar. 2-21
Desarrollo de Sistemas
6
El Modelo físico Relacional
6.1.
Introducción
El diseño de una base de datos se descompone en tres etapas: diseño conceptual, lógico y físico. La etapa del diseño lógico es independiente de los detalles de implementación y dependiente del tipo de SGBD que se vaya a utilizar. La salida de esta etapa es el esquema lógico global y la documentación que lo describe. Todo ello es la entrada para la etapa que viene a continuación, el diseño físico. Mientras que en el diseño lógico se especifica qué se guarda, en el diseño físico se especifica cómo se guarda. Para ello, el diseñador debe conocer muy bien toda la funcionalidad del SGBD concreto que se vaya a utilizar y también el sistema informático sobre el que éste va a trabajar. El diseño físico no es una etapa aislada, ya que algunas decisiones que se tomen durante su desarrollo, por ejemplo para mejorar las prestaciones, pueden provocar una reestructuración del esquema lógico.
6.2.
Metodología de diseño físico para bases de datos relacionales
El objetivo de esta etapa es producir una descripción de la implementación de la base de datos en memoria secundaria. Esta descripción incluye las estructuras de almacenamiento y los métodos de acceso que se utilizarán para conseguir un acceso eficiente a los datos. El diseño físico se divide de cuatro fases, cada una de ellas compuesta por una serie de pasos: 1)
Traducir el esquema lógico global para el SGBD específico.
2)
Diseñar la representación física.
3)
4)
2-22
•
Analizar las transacciones.
•
Escoger las organizaciones de ficheros.
•
Escoger los índices secundarios.
•
Considerar la introducción de redundancias controladas.
•
Estimar la necesidad de espacio en disco.
Diseñar los mecanismos de seguridad. •
Diseñar las vistas de los usuarios.
•
Diseñar las reglas de acceso.
Monitorizar y afinar el sistema.
Diseño de bases de datos
6.2.1. Traducir el esquema lógico global La primera fase del diseño lógico consiste en traducir el esquema lógico global en un esquema que se pueda implementar en el SGBD escogido. Para ello, es necesario conocer toda la funcionalidad que éste ofrece. Por ejemplo, el diseñador deberá saber: —
Si el sistema soporta la definición de claves primarias, claves ajenas y claves alternativas.
—
Si el sistema soporta la definición de datos requeridos (es decir, si se pueden definir atributos como no nulos).
—
Si el sistema soporta la definición de dominios.
—
Si el sistema soporta la definición de reglas de negocio.
—
Cómo se crean las relaciones base.
• Diseñar las relaciones base para el SGBD específico Las relaciones base se definen mediante el lenguaje de definición de datos del SGBD. Para ello, se utiliza la información producida durante el diseño lógico: el esquema lógico global y el diccionario de datos. El esquema lógico consta de un conjunto de relaciones y, para cada una de ellas, se tiene: —
El nombre de la relación.
—
La lista de atributos entre paréntesis.
—
La clave primaria y las claves ajenas, si las tiene.
—
Las reglas de integridad de las claves ajenas.
En el diccionario de datos se describen los atributos y, para cada uno de ellos, se tiene: —
Su dominio: tipo de datos, longitud y restricciones de dominio.
—
El valor por defecto, que es opcional.
—
Si admite nulos.
—
Si es derivado y, en caso de serlo, cómo se calcula su valor.
A continuación, se muestra un ejemplo de la definición de la relación INMUEBLE con el estándar SQL. CREATE CREATE CREATE CREATE CREATE CREATE
CREATE DOMAIN poblacion AS VARCHAR(15); CREATE DOMAIN tipo AS VARCHAR(1) CHECK(VALUE IN (‘A’,`C’,`D’,`P’,`V’)); CREATE DOMAIN hab AS SMALLINT CHECK(VALUE BETWEEN 1 AND 15); CREATE DOMAIN alquiler AS DECIMAL(6,2) CHECK(VALUE BETWEEN 0 AND 9999); CREATE TABLE inmueble ( inum INUM NOT NULL, CALLE NOT NULL, calle area AREA, poblacion POBLACION NOT NULL, tipo TIPO NOT NULL DEFAULT `P’, hab HAB NOT NULL DEFAULT 4, alquiler ALQUILER NOT NULL DEFAULT 350, pnum PNUM NOT NULL, enum ENUM, onum ONUM NOT NULL, PRIMARY KEY (inum), FOREIGN KEY (pnum) REFERENCES propietario ON DELETE no action ON UPDATE cascade, FOREIGN KEY (enum) REFERENCES plantilla ON DELETE set null ON UPDATE cascade, FOREIGN KEY (onum) REFERENCES oficina ON DELETE no action ON UPDATE cascade );
• Diseñar las reglas de negocio para el SGBD específico Las actualizaciones que se realizan sobre las relaciones de la base de datos deben observar ciertas restricciones que imponen las reglas de negocio de la empresa. Algunos SGBD proporcionan mecanismos que permiten definir estas restricciones y vigilan que no se violen. Por ejemplo, si no se quiere que un empleado tenga más de diez inmuebles asignados, se puede definir una restricción en la sentencia CREATE TABLE de la relación INMUEBLE: CONSTRAINT inmuebles_por_empleado CHECK (NOT EXISTS (SELECT enum FROM inmueble GROUP BY enum HAVING COUNT(*)>10))
2-24
Diseño de bases de datos
Otro modo de definir esta restricción es mediante un disparador (trigger): CREATE TRIGGER inmuebles_por_empleado ON inmueble FOR INSERT,UPDATE AS IF ((SELECT COUNT(*) FROM inmueble i WHERE i.inum=INSERTED.inum)>10) BEGIN PRINT “Este empleado ya tiene 10 inmuebles asignados” ROLLBACK TRANSACTION END
Hay algunas restricciones que no las pueden manejar los SGBD, como por ejemplo “a las 20:30 del último día laborable de cada año archivar los inmuebles vendidos y borrarlos”. Para estas restricciones habrá que escribir programas de aplicación específicos. Por otro lado, hay SGBD que no permiten la definición de restricciones, por lo que éstas deberán incluirse en los programas de aplicación. Todas las restricciones que se definan deben estar documentadas. Si hay varias opciones posibles para implementarlas, hay que explicar por qué se ha escogido la opción implementada.
6.2.2. Diseñar la representación física Uno de los objetivos principales del diseño físico es almacenar los datos de modo eficiente. Para medir la eficiencia hay varios factores que se deben tener en cuenta: —
Productividad de transacciones. Es el número de transacciones que se quiere procesar en un intervalo de tiempo.
—
Tiempo de respuesta. Es el tiempo que tarda en ejecutarse una transacción. Desde el punto de vista del usuario, este tiempo debería ser el mínimo posible.
—
Espacio en disco. Es la cantidad de espacio en disco que hace falta para los ficheros de la base de datos. Normalmente, el diseñador querrá minimizar este espacio.
Lo que suele suceder, es que todos estos factores no se pueden satisfacer a la vez. Por ejemplo, para conseguir un tiempo de respuesta mínimo, puede ser necesario aumentar la cantidad de datos almacenados, ocupando más espacio en disco. Por lo tanto, el diseñador deberá ir ajustando estos factores para conseguir un equilibrio razonable. El diseño físico inicial no será el definitivo, sino que habrá que ir monitorizándolo para observar sus prestaciones e ir ajustándolo como sea oportuno. Muchos SGBD proporcionan herramientas para monitorizar y afinar el sistema. Hay algunas estructuras de almacenamiento que son muy eficientes para cargar grandes cantidades de datos en la base de datos, pero no son eficientes para el resto de operaciones, por lo que se puede escoger dicha estructura de almacenamiento para inicializar la base de datos y cambiarla, a continuación, 2-25
Desarrollo de Sistemas
para su posterior operación. Los tipos de organizaciones de ficheros disponibles varían en cada SGBD. Algunos sistemas proporcionan más estructuras de almacenamiento que otros. Es muy importante que el diseñador del esquema físico sepa qué estructuras de almacenamiento le proporciona el SGBD y cómo las utiliza. Para mejorar las prestaciones, el diseñador del esquema físico debe saber cómo interactúan los dispositivos involucrados y cómo esto afecta a las prestaciones: —
Memoria principal. Los accesos a memoria principal son mucho más rápidos que los accesos a memoria secundaria (decenas o centenas de miles de veces más rápidos). Generalmente, cuanta más memoria principal se tenga, más rápidas serán las aplicaciones. Sin embargo, es aconsejable tener al menos un 5% de la memoria disponible, pero no más de un 10%. Si no hay bastante memoria disponible para todos los procesos, el sistema operativo debe transferir páginas a disco para liberar memoria (paging). Cuando estas páginas se vuelven a necesitar, hay que volver a traerlas desde el disco (faltas de página). A veces, es necesario llevar procesos enteros a disco (swapping) para liberar memoria. El hacer estas transferencias con demasiada frecuencia empeora las prestaciones.
—
CPU. La CPU controla los recursos del sistema y ejecuta los procesos de usuario. El principal objetivo con este dispositivo es lograr que no haya bloqueos de procesos para conseguirla. Si el sistema operativo, o los procesos de los usuarios, hacen muchas demandas de CPU, ésta se convierte en un cuello de botella. Esto suele ocurrir cuando hay muchas faltas de página o se realiza mucho swapping.
—
Entrada/salida a disco. Los discos tienen una velocidad de entrada/salida. Cuando se requieren datos a una velocidad mayor que ésta, el disco se convierte en un cuello de botella. Dependiendo de cómo se organicen los datos en el disco, se conseguirá reducir la probabilidad de empeorar las prestaciones. Los principios básicos que se deberían seguir para repartir los datos en los discos son los siguientes:
—
•
Los ficheros del sistema operativo deben estar separados de los ficheros de la base de datos.
•
Los ficheros de datos deben estar separados de los ficheros de índices.
•
Los ficheros con los diarios de operaciones deben estar separados del resto de los ficheros de la base de datos.
Red. La red se convierte en un cuello de botella cuando tiene mucho tráfico y cuando hay muchas colisiones.
Cada uno de estos recursos afecta a los demás, de modo que una mejora en alguno de ellos puede provocar mejoras en otros. • Analizar las transacciones Para realizar un buen diseño físico es necesario conocer las consultas y las transacciones que se van a ejecutar sobre la base de datos. Esto incluye tanto información cualitativa, como cuantitativa. Para cada transacción, hay que especificar: 2-26
Diseño de bases de datos
—
La frecuencia con que se va a ejecutar.
—
Las relaciones y los atributos a los que accede la transacción, y el tipo de acceso: consulta, inserción, modificación o eliminación. Los atributos que se modifican no son buenos candidatos para construir estructuras de acceso.
—
Los atributos que se utilizan en los predicados del WHERE de las sentencias SQL. Estos atributos pueden ser candidatos para construir estructuras de acceso dependiendo del tipo de predicado que se utilice.
—
Si es una consulta, los atributos involucrados en el join de dos o más relaciones. Estos atributos pueden ser candidatos para construir estructuras de acceso.
—
Las restricciones temporales impuestas sobre la transacción. Los atributos utilizados en los predicados de la transacción pueden ser candidatos para construir estructuras de acceso.
• Escoger las organizaciones de ficheros El objetivo de este paso es escoger la organización de ficheros óptima para cada relación. Por ejemplo, un fichero desordenado es una buena estructura cuando se va a cargar gran cantidad de datos en una relación al inicializarla, cuando la relación tiene pocas tuplas; también cuando en cada acceso se deben obtener todas las tuplas de la relación, o cuando la relación tiene una estructura de acceso adicional, como puede ser un índice. Por otra parte, los ficheros dispersos (hashing) son apropiados cuando se accede a las tuplas a través de los valores exactos de alguno de sus campos (condición de igualdad en el WHERE). Si la condición de búsqueda es distinta de la igualdad (búsqueda por rango, por patrón, etc.), la dispersión no es una buena opción. Hay otras organizaciones, como la ISAM, InnoDB para MySQL o los árboles B+ para Oracle. Las organizaciones de ficheros elegidas deben documentarse, justificando en cada caso la opción escogida. • Escoger los índices secundarios Los índices secundarios permiten especificar caminos de acceso adicionales para las relaciones base. Por ejemplo, la relación INMUEBLE se puede haber almacenado en un fichero disperso a través del atributo inum. Si se accede a menudo a esta relación a través del atributo alquiler, se puede plantear la creación de un índice sobre dicho atributo para favorecer estos accesos. Pero hay que tener en cuenta que estos índices conllevan un coste de mantenimiento que hay que sopesar frente a la ganancia en prestaciones. A la hora de seleccionar los índices, se pueden seguir las siguientes indicaciones: —
Construir un índice sobre la clave primaria de cada relación base.
—
No crear índices sobre relaciones pequeñas.
—
Añadir un índice sobre los atributos que se utilizan para acceder con mucha frecuencia. 2-27
Desarrollo de Sistemas
—
Añadir un índice sobre las claves ajenas que se utilicen con frecuencia para hacer joins.
—
Evitar los índices sobre atributos que se modifican a menudo.
—
Evitar los índices sobre atributos poco selectivos (aquellos en los que la consulta selecciona una porción significativa de la relación).
—
Evitar los índices sobre atributos formados por tiras de caracteres largas.
Los índices creados se deben documentar, explicando las razones de su elección. • Considerar la introducción de redundancias controladas En ocasiones puede ser conveniente relajar las reglas de normalización introduciendo redundancias de forma controlada, con objeto de mejorar las prestaciones del sistema. En la etapa del diseño lógico se recomienda llegar, al menos, hasta la tercera forma normal para obtener un esquema con una estructura consistente y sin redundancias. Pero, a menudo, sucede que las bases de datos así normalizadas no proporcionan la máxima eficiencia, con lo que es necesario volver atrás y desnormalizar algunas relaciones, sacrificando los beneficios de la normalización para mejorar las prestaciones. Es importante hacer notar que la desnormalización sólo debe realizarse cuando se estime que el sistema no puede alcanzar las prestaciones deseadas. Y, desde luego, la necesidad de desnormalizar en ocasiones no implica eliminar la normalización del diseño lógico: la normalización obliga al diseñador a entender completamente cada uno de los atributos que se han de representar en la base de datos. Por lo tanto, hay que tener en cuenta los siguientes factores: —
La desnormalización hace que la implementación sea más compleja.
—
La desnormalización hace que se sacrifique la flexibilidad.
—
La desnormalización puede hacer que los accesos a datos sean más rápidos, pero ralentiza las actualizaciones.
Por regla general, la desnormalización de una relación puede ser una opción viable cuando las prestaciones que se obtienen no son las deseadas y la relación se actualiza con poca frecuencia, pero se consulta muy a menudo. Las redundancias que se pueden incluir al desnormalizar son de varios tipos: se pueden introducir datos derivados (calculados a partir de otros datos), se pueden duplicar atributos o se pueden hacer joins de relaciones. El incluir un atributo derivado dependerá del coste adicional de almacenarlo y mantenerlo consistente con los datos de los que se deriva, frente al coste de calcularlo cada vez que se necesita. No se pueden establecer una serie de reglas que determinen cuándo desnormalizar relaciones, pero hay algunas situaciones muy comunes en donde puede considerarse esta posibilidad:
2-28
Diseño de bases de datos
—
Combinar relaciones de uno a uno. Cuando hay relaciones (tablas) involucradas en relaciones de uno a uno, se accede a ellas de manera conjunta con frecuencia y casi no se les accede separadamente, se pueden combinar en una sola relación (tabla).
—
Duplicar atributos no clave en relaciones de uno a muchos para reducir los joins. Para evitar operaciones de join, se pueden incluir atributos de la relación (tabla) padre en la relación (tabla) hijo de las relaciones de uno a muchos.
—
Tablas de referencia. Las tablas de referencia (lookup) son listas de valores, cada uno de los cuales tiene un código. Por ejemplo, puede haber una tabla de referencia para los tipos de inmueble, con las descripciones de estos tipos y un código asociado. Este tipo de tablas son un caso de relación de uno a muchos. En la relación INMUEBLE habrá una clave ajena a esta tabla para indicar el tipo de inmueble. De este modo, es muy fácil validar los datos, además de que se ahorra espacio escribiendo sólo el código y no la descripción para cada inmueble, además de ahorrar tiempo cuando se actualizan las descripciones. Si las tablas de referencia se utilizan a menudo en consultas críticas, se puede considerar la introducción de la descripción junto con el código en la relación (tabla) hijo, manteniendo la tabla de referencia para validación de datos.
—
Duplicar claves ajenas en relaciones de uno a muchos para reducir los joins. Para evitar operaciones de join, se pueden incluir claves ajenas de una relación (tabla) en otra relación (tabla) con la que se relaciona (habrá que tener en cuenta ciertas restricciones).
—
Duplicar atributos en relaciones de muchos a muchos para reducir los joins. Durante el diseño lógico se eliminan las relaciones de muchos a muchos introduciendo dos relaciones de uno a muchos. Esto hace que aparezca una nueva relación (tabla) intermedia, de modo que si se quiere obtener la información de la relación de muchos a muchos, se tiene que realizar el join de tres relaciones (tablas). Para evitar algunos de estos joins se pueden incluir algunos de los atributos de las relaciones (tablas) originales en la relación (tabla) intermedia.
—
Introducir grupos repetitivos. Los grupos repetitivos se eliminan en el primer paso de la normalización para conseguir la primera forma normal. Estos grupos se eliminan introduciendo una nueva relación (tabla), generando una relación de uno a muchos. A veces, puede ser conveniente reintroducir los grupos repetitivos para mejorar las prestaciones.
—
Creación de Vistas Materializadas. Son vistas sobre tablas muy grandes en las que hay que definir y/o programar cuándo han de refrescarse para que sean consistentes los datos redundantes con los originales. Su razón de ser es la de otorgar velocidad de acceso ante una tabla con muchas filas (millones de filas) y sobre la que hay que realizar una consulta muy costosa en cuanto a ordenaciones o condiciones a cumplir cuya ejecución en tiempo real lo hace inviable. De este modo se accede a una tabla que es una redundancia de una tabla y que cumple unas ciertas condiciones. 2-29
Desarrollo de Sistemas
Todas las redundancias que se introduzcan en este paso se deben documentar y razonar. El esquema lógico se debe actualizar para reflejar los cambios introducidos. • Estimar la necesidad de espacio en disco En caso de que se tenga que adquirir nuevo equipamiento informático, el diseñador debe estimar el espacio necesario en disco para la base de datos. Esta estimación depende del SGBD que se vaya a utilizar y del hardware. En general, se debe estimar el número de tuplas de cada relación y su tamaño. También se debe estimar el factor de crecimiento de cada relación.
6.2.3. Diseñar los mecanismos de seguridad Los datos constituyen un recurso esencial para la empresa, por lo tanto su seguridad es de vital importancia. Durante el diseño lógico se habrán especificado los requerimientos en cuanto a seguridad que en esta fase se deben implementar. Para llevar a cabo esta implementación, el diseñador debe conocer las posibilidades que ofrece el SGBD que se vaya a utilizar. • Diseñar las vistas de los usuarios El objetivo de este paso es diseñar las vistas de los usuarios correspondientes a los esquemas lógicos locales. Las vistas, además de preservar la seguridad, mejoran la independencia de datos, reducen la complejidad y permiten que los usuarios vean los datos en el formato deseado. • Diseñar las reglas de acceso El administrador de la base de datos asigna a cada usuario un identificador que tendrá una palabra secreta asociada por motivos de seguridad. Para cada usuario o grupo de usuarios se otorgarán permisos para realizar determinadas acciones sobre determinados objetos de la base de datos. Por ejemplo, los usuarios de un determinado grupo pueden tener permiso para consultar los datos de una relación base concreta y no tener permiso para actualizarlos.
6.2.4. Monitorizar y afinar el sistema Una vez implementado el esquema físico de la base de datos, se debe poner en marcha para observar sus prestaciones. Si éstas no son las deseadas, el esquema deberá cambiar para intentar satisfacerlas. Una vez afinado el esquema, no permanecerá estático, ya que tendrá que ir cambiando conforme lo requieran los nuevos requisitos de los usuarios. Los SGBD proporcionan herramientas para monitorizar el sistema mientras está en funcionamiento.
2-30
Tema 3 Lenguajes de programación. Representación de tipos de datos. Operadores. Instrucciones condicionales. Bucles y recursividad. Procedimientos, funciones y parámetros. Vectores y registros. Estructura de un programa.
Desarrollo de Sistemas
Guión-resumen
1. Lenguaje de Programación 1.1. Introducción
5. Clasificación general de instrucciones
1.2. Concepto de programa
5.1. Instrucciones de definición de datos
1.3. Características de los programas
5.2. Instrucciones primitivas
1.4. Los lenguajes de programación
5.3. Instrucciones compuestas
2. Elementos de un programa
5.4. Instrucciones de control
2.1. Objetos
6. Recursividad
2.2. Identificadores
7. Procedimientos, funciones y parámetros
2.3. Datos 2.4. Constantes 2.5. Variables 2.6. Operadores 2.7. Expresiones 2.8. Sentencias 2.9. Comentarios 3. Tipos de Datos 3.1. Datos básicos 3.2. Dato derivado (puntero) 3.3. Datos estructurados
3-2
4. Operadores
7.1. Procedimientos 7.2. Funciones 7.3. Parámetros 8. Vectores y registros 8.1. Vectores 8.2. Registros 9. Estructura de un programa 9.1. Elementos auxiliares de programación 9.2. Prueba de programas 9.3. Verificación y validación
Lenguajes de programación
1.
Lenguaje de Programación
1.1.
Introducción
Los ordenadores son máquinas que disponen de gran rapidez para efectuar operaciones, poseen precisión y memoria pero son carentes de inteligencia natural. Son máquinas preparadas para realizar el proceso que se le indique mediante un programa (un conjunto de instrucciones). Estas instrucciones que forman el programa deben traducirse de un lenguaje comprensible por los humanos a un lenguaje comprensible para la máquina. El componente principal del ordenador, el único que realiza el trabajo de cálculo (computación), es la Unidad Central de Proceso (CPU). Es la CPU la que se encarga de ejecutar los programas.
1.2.
Concepto de programa
Un programa es una serie o secuencia de instrucciones que el ordenador debe ejecutar para realizar la tarea prevista por el programador. Cuando nos planteamos un problema complejo y queremos resolverlo con la utilización del ordenador, necesitamos descomponerlo en una serie de tareas simples que se irán repitiendo a lo largo de un proceso hasta la resolución del problema; el ordenador ha realizado una tarea compleja, a partir de instrucciones simples. Dicho conjunto de tareas simples sería el programa, y su elaboración es lo que entendemos por programación. Cuando nos disponemos a programar una de las primeras decisiones que hemos de tomar es la elección del lenguaje a emplear, es decir la forma en la que el programador tiene que escribir las operaciones a realizar por el ordenador. Veamos a continuación algunas definiciones útiles para seguir avanzando: —
Se denomina instrucción al conjunto de reglas o normas dadas para la realización o empleo de algo. En informática, instrucción es la información que indica a un ordenador una acción elemental que ha de realizar. Para poder realizar algún proceso de utilidad debe indicarse al ordenador un conjunto organizado de instrucciones.
—
Un algoritmo es un conjunto ordenado de operaciones necesarias para resolver un problema.
—
Un programa es un conjunto ordenado de instrucciones, perfectamente legibles por el ordenador, que le permiten realizar un trabajo o resolver un problema. El programa es la descripción de un algoritmo en un lenguaje inteligible por la máquina.
Los programas contienen frecuentemente conjuntos de instrucciones que pueden intervenir varias veces en la ejecución del mismo o que realizan una tarea específica. Es posible agrupar dichas instrucciones de modo que formen una unidad independiente del programa a la cual se haga referencia en aquellos puntos del programa donde sea necesario. En esto consiste una subrutina. En la traducción del programa a lenguaje máquina se establecerá el mecanismo para que al encontrar la referencia a ese conjunto de instrucciones, la subrutina, aquéllas se ejecuten como si estuvie-
3-3
Desarrollo de Sistemas
ran en el propio programa. Las subrutinas pueden ser llamadas por un solo programa pero también puede hacerse referencia a las mismas desde programas distintos.
1.3.
Características de los programas
La determinación de la calidad de los programas estará en función de las ventajas de su utilización; para ello existen unas características que determinan a priori si el programa tendrá vida larga y productiva: —
Legible. Todo programa debe ser de fácil comprensión no sólo por los futuros usuarios, sino por cualquier programador.
—
Flexible. Capaz de adaptarse con facilidad a los cambios que puedan producirse en el planteamiento inicial.
—
Portable. Facilidad para compilarse o interpretarse en distintas máquinas y sistemas operativos, también un factor a tener en cuenta sería su facilidad para ser traducido a otros lenguajes de programación.
—
Fiable. El programa debe ser capaz de recuperar el control cuando su utilización no sea la adecuada.
—
Eficaz. El programa ha de utilizar eficazmente los recursos de que disponga, tanto del sistema operativo como del equipo en que trabaje. Características de los programas:
1.4.
Legibilidad
(comprendido por diferentes programadores)
Flexibilidad
(adaptarse a los cambios)
Portabilidad
(compilable dif. Máquinas / S.O / lenguajes)
Fiabilidad
(recuperación del control por errores)
Eficiencia
(aprovechamiento recursos S.O.)
Los lenguajes de programación
El lenguaje de programación es la forma en la que el programador escribe las operaciones que el ordenador debe realizar. Es el conjunto de notaciones, símbolos y reglas sintácticas para posibilitar la escritura de un algoritmo que posteriormente será interpretado por el hardware de un ordenador. La CPU esta preparada para manejar unas instrucciones escritas en un tipo de lenguaje muy simple llamado lenguaje máquina. Cada modelo de CPU posee su propio lenguaje máquina, y puede ejecutar un programa sólo si está escrito en ese lenguaje. Para poder ejecutar programas escritos en otros lenguajes, es necesario primero trasladarlos a lenguaje máquina a través de un proceso de compilación o interpretación. 3-4
Lenguajes de programación
Interfaz grafica del programa
Código fuente del programa
Un lenguaje de programación es una notación o conjunto de símbolos y caracteres combinados conforme una sintaxis ya predefinida. En función de su parecido con el lenguaje natural, podemos hablar de lenguajes de bajo nivel y lenguajes de alto nivel. En los primeros la sintaxis está más próxima al lenguaje máquina que al lenguaje humano y en los de alto nivel es todo lo contrario. Cuando un programa es ejecutado directamente por el ordenador, es decir, está en código máquina, decimos que es un lenguaje de bajo nivel. Casi todos los programas son escritos por los programadores en lenguajes de alto nivel. Nota. Los lenguajes de programación, en especial C, C++, Java y la plataforma .Net serán estudiados con detalle en el tema 6.
2.
Elementos de un programa
En la elaboración de un programa utilizamos diferentes elementos que cubrirán diferentes funciones. Los diferentes elementos son:
2.1.
—
Objetos.
—
Identificadores.
—
Datos.
—
Constantes.
—
Variables.
—
Operadores.
—
Expresiones.
—
Sentencias.
—
Comentarios.
Objetos
Llamaremos objetos a todos los elementos que utilizaremos en la programación, susceptibles de ser manejados por las instrucciones y sentencias del programa. 3-5
Desarrollo de Sistemas
Todas las cosas pueden ser objetos, siempre que puedan ser individualizables e identificables, pudiendo ser cosas reales o abstractas, pero siempre representarán un papel definido en el problema. Los objetos tendrán tres atributos que los diferencien: el nombre que los identifique e individualice; el tipo, que determina el contenido o la clase de objeto; y su valor, que será el contenido en concreto que lo distingue de otros objetos de la misma clase.
2.2.
Identificadores
Los identificadores o etiquetas son palabras escogidas por el programador para designar los distintos elementos de un programa tales como las variables, funciones, etc.
2.3.
Datos
Consideramos como datos toda la información que se va a procesar en el ordenador. Serían datos los objetos anteriormente mencionados susceptibles de manipulación por las instrucciones del programa; también consideramos como datos aquellos elementos simples que utilizamos para comunicarnos, pudiendo ser de tipo: —
2.4.
Carácter. Son las unidades utilizadas en la información: •
alfabéticas (a, A, b, B, ...)
•
numéricas (0, 1, 2, ...)
•
especiales (+, -, *, ...)
—
Numérico. Son los valores o cantidades representados por los caracteres numéricos. Pueden ser de tipo entero o real.
—
Alfanumérico. Son conjuntos de caracteres alfabéticos, numéricos o especiales.
—
Lógico o booleano. Son datos lógicos que sólo pueden tomar dos valores: 1/0, cierto/falso, sí/no, ...
Constantes
Las constantes son aquellos datos utilizados en los programas que permanecerán invariables en todo el proceso de ejecución del programa, no pudiéndose alterar su valor o composición ni por parte del usuario ni del ordenador.
2.5.
Variables
Las variables son datos cuyo valor es modificable durante la ejecución del programa. Dicha variación sólo afectará al valor de la variable, no así al lugar
3-6
Lenguajes de programación
que ocupe en la memoria del ordenador, al tipo de información que represente ni a la identificación de la misma. Son datos cuya información va a ir cambiando a lo largo de la ejecución del programa. Pueden ser de diversos tipos: —
Inicializadas o no inicializada, según se les asigne o no un valor inicial de partida.
—
Globales o locales, según tengan validez para todo el programa o sólo para una parte del mismo.
2.6.
Operadores
Los operadores son símbolos que representan las distintas operaciones que pueden realizarse sobre los datos. Pueden clasificarse en: —
Aritméticos: suma, resta, multiplicación, etc.
—
Alfanuméricos: concatenación.
—
De asignación: establece los valores de las variables.
—
De relación: permiten comparaciones: igual que, menor o igual que...
—
Lógicos: AND, OR, OR exclusiva, NOT...
2.7.
Expresiones
En programación, una expresión es una combinación de operadores y datos u objetos (constantes o variables) cuyo resultado es un valor, que puede, por ejemplo, ser asignado a una variable. Los conceptos que estamos considerando (expresiones, variables, constantes, etc.) son muy similares a los convenios matemáticos.
2.8.
Sentencias
Las sentencias son las actuaciones que deseamos que ejecute el programa; se corresponderán con las operaciones estudiadas en el algoritmo, y estarán de acuerdo con la sintaxis establecida en el lenguaje concreto de programación que se esté utilizando. Podemos clasificar las sentencias utilizadas en la programación como instrucciones de: —
Asignación. Fijarán el valor que deban tomar las variables en un momento determinado.
—
Entrada. Permitirán la recogida de datos desde un dispositivo externo: teclado, ratón, etc., asignando valores a las variables.
—
Salida. Lo utilizaremos para visualizar externamente los valores o datos del programa: pantalla, archivo, impresora, etc. 3-7
Desarrollo de Sistemas
2.9.
—
Condicionales. Nos permitirá la comparación del valor de variables, posibilitando la toma de decisiones por parte del ordenador.
—
Repetitivas. Son aquellas que posibilitarán la repetición de un conjunto de sentencias; dicha repetición se realizará un determinado número de veces, o mientras se cumpla una condición.
—
Salto.
Comentarios
Son líneas de texto que el compilador o el intérprete no consideran como parte del código, con lo cual no están sujetas a restricciones de sintaxis y sirven para aclarar partes de código en posteriores lecturas y, en general, para anotar cualquier cosa que el programador considere oportuno. Un programador debe tener como prioridad documentar nuestro código fuente ya que al momento de depurar nos ahorrará mucho tiempo de análisis para su corrección o estudio. Se deben documentar los programas con encabezados de texto (encabezados de comentarios) en donde describen la función que va a realizar dicho programa, la fecha de creación, el nombre del autor y en algunos casos las fechas de revisión y el nombre del revisor. Se puede hacer uso de llamadas a subprogramas dentro de una misma aplicación por lo que cada subprograma debería estar documentado, describiendo la función que realizan cada uno de estos subprogramas dentro de la aplicación.
3.
Tipos de Datos
Se llama estructura de datos al conjunto de datos con una misma denominación, y que se utilizan como una sola unidad. Las estructuras de datos pueden ser internas o externas dependiendo del lugar de almacenamiento, memoria central del ordenador o dispositivo de almacenamiento externo: Estructuras de datos internas. Los arrays son estructuras de datos internas, compuestos por un número fijo de elementos del mismo tipo; son almacenados en posiciones consecutivas de memoria y pueden ser llamados como una sola unidad o conjunto compacto, pero también pueden ser llamados como variables independientes considerándolos de forma individualizada. Estructuras de datos externas, son archivos o ficheros almacenados en un dispositivo de almacenamiento externo; son necesarios como forma de almacenamiento de información de forma masiva y permanente necesarios para el funcionamiento de un programa. Dichos archivos se componen de registros equivalentes a fichas con los datos que componen la información a tratar; los archivos son conjuntos de registros del mismo tipo que el programa utilizará y modificará a medida que se vaya ejecutando. Para el diseño de un programa es importante establecer cuáles son las estructuras de los datos que se van a utilizar, con el objeto de establecer las 3-8
Lenguajes de programación
operaciones que sobre dichos datos se pueden realizar. Para ello, debemos proporcionar al sistema información sobre los mismos. Es decir, que los datos manejados en un programa deben llevar asociados un identificador, un tipo y un valor. •
Identificador. Es el nombre utilizado en un programa para referenciar un dato. Existen ciertas normas generales para su empleo, siendo posible destacar las siguientes: a)
Pueden estar constituidos por letras y dígitos y en algunos casos por el carácter subrayado (_).
b)
Deben comenzar por una letra.
c)
No deben contener espacios.
d)
El número máximo de caracteres y nombres reservados que se pueden emplear dependen del compilador utilizado.
e)
El nombre asignado debe tener relación con la información que contiene, pudiéndose emplear abreviaturas que sean significativas.
•
Tipo. Establece el rango o intervalo de valores que puede tomar el dato. El tipo determina el espacio de memoria que se reservará para el dato. Un tipo de datos define un conjunto de valores y las operaciones sobre estos valores. Casi todos los lenguajes de programación explícitamente incluyen la notación del tipo de datos, aunque lenguajes diferentes pueden usar terminología diferente. La mayor parte de los lenguajes de programación permiten al programador definir tipos de datos adicionales, normalmente combinando múltiples elementos de otros tipos y definiendo las operaciones del nuevo tipo de dato.
•
Valor. Elemento que debe pertenecer al rango o intervalo de valores según el tipo definido. Entero Real
Numéricos Datos Básicos
Carácter Lógico
Dato derivado
Puntero
Estáticos
Datos estructurados
Internos
Lineales
Tabla
Lineales
Lista Pila Cola
No lineales
Árbol Grafo
Dinámicos
Externos
Fichero Base de datos
3-9
Desarrollo de Sistemas
3.1.
Datos básicos —
Numéricos. Se utilizan para contener magnitudes y pueden ser de dos tipos: enteros y reales.
—
Carácter. Se emplean para representar un carácter dentro de un conjunto definido por el fabricante del equipo, de forma que cada uno de ellos se corresponde con un valor numérico entero sin signo, siguiendo un determinado código (EBCDIC, ASCII...).
—
Lógico. Se emplean para representar únicamente dos valores, 1 ó 0, Verdadero o Falso.
3.2.
Dato derivado (puntero)
Se emplea para contener la dirección de memoria de otra variable. Una variable tipo puntero debe ser definida con el mismo tipo de la variable que va a referenciar o apuntar. Este tipo de variable es de gran utilidad para realizar operaciones con estructuras y para llamadas a módulos.
3.3.
Datos estructurados Podemos encontrar varios tipos de datos estructurados: •
Datos internos Son los que residen en la memoria principal del ordenador. Por ejemplo, una tabla bidimensional de números reales.
•
Datos externos Son los que residen en un soporte externo a la memoria principal, es decir, memoria auxiliar; por ejemplo un fichero guardado en disco duro.
•
Datos estáticos Son aquellos cuyo tamaño queda definido en la compilación del programa y no se puede modificar durante la ejecución del mismo. Por ejemplo una tabla de alumnos.
•
Datos dinámicos Son aquellos cuyo tamaño puede ser modificado durante la ejecución del programa. Por ejemplo, una lista, una pila, una cola, etc.
•
Datos lineales Son los que pueden estar enlazados con un solo elemento anterior y uno sólo posterior. Por ejemplo una pila.
3-10
Lenguajes de programación
•
Datos no lineales Son los que pueden enlazarse con más de un elemento anterior y más de uno posterior. Por ejemplo, un árbol.
•
Datos compuestos Son los formados por el programador a base de tipos de datos básicos y derivados, pudiendo ser internos o externos.
4.
Operadores
Los operadores son símbolos que sirven para conectar los datos facilitando la realización de diversas clases de operaciones. Pueden ser: OPERADOR Paréntesis
Aritméticos
Alfanuméricos
Relacionales
Lógicos
SÍMBOLO ( )
SIGNIFICADO Paréntesis
** , ^ * / div , \ % , mod + -
Potencia Producto División División entera Módulo (resto de la división entera) Signo positivo o suma Signo negativo o resta
+ -
Concatenación Concatenación eliminando espacios
= = , = ! = , < > < <= > >= ! , NOT, no &&, AND, y I I , OR, o
Igual a Distinto a Menor que Menor o igual que Mayor que Mayor o igual que Negación Conjunción Disyunción
• Orden de prioridad de los operadores Dentro de las expresiones hay que respetar un orden de prioridad entre operadores que depende del lenguaje utilizado, pero de forma general se puede establecer el siguiente orden: 1. Paréntesis (comenzando por los más internos). 3-11
Desarrollo de Sistemas
2. Signo. 3. Potencia. 4. Producto, división y módulo (con la misma prioridad). 5. Suma y resta (con la misma prioridad). 6. Concatenación. 7. Relacionales. 8. Negación. 9. Conjunción. 10. Disyunción.
5.
Clasificación general de instrucciones
Una instrucción puede ser considerada como un hecho o suceso de duración limitada que genera unos cambios previstos en la ejecución de un programa, por lo que debe ser una acción previamente estudiada y definida.
5.1.
Instrucciones de definición de datos
Son aquellas instrucciones utilizadas para informar al procesador del espacio que debe reservar en memoria, con la finalidad de almacenar un dato mediante el uso de variables simples o estructuras de datos más complejas como, por ejemplo, tablas. La definición consiste en indicar un nombre a través del cual haremos referencia al dato y un tipo a través del cual informaremos al procesador de las características y espacio que deberá reservar en memoria.
Ejemplo.- int x;
5.2.
Instrucciones primitivas
Se consideran como tal las instrucciones de asignación y las instrucciones de entrada/salida. •
Instrucciones de entrada Son aquellas instrucciones encargadas de recoger el dato de un periférico o dispositivo de entrada (por ejemplo el teclado, un ratón, un escáner, etc.) y seguidamente almacenarlo en memoria en una variable previamente definida, para la cual se ha reservado suficiente espacio en memoria.
Ejemplo.- scanf (“%d”, &x); 3-12
Lenguajes de programación
En el supuesto de leer varios valores consecutivos, con la intención de almacenarlos en variables diferentes, lo indicaremos situando uno a continuación del otro separados por comas. •
Instrucciones de asignación Son aquellas instrucciones cuyo cometido es almacenar un dato o valor simple obtenido como resultado de evaluar una expresión, en una variable previamente definida y declarada.
Ejemplo.- x=9; •
Instrucciones de salida Son aquellas instrucciones encargadas de recoger datos procedentes de variables o los resultados obtenidos de expresiones evaluadas y depositarlos en un periférico o dispositivo de salida (por ejemplo, la pantalla, una impresora, un plotter, etc.).
Ejemplo.- printf (“El resultado es: %d”, x); En el supuesto de escribir varios valores o resultados de forma consecutiva, lo indicaremos situando uno a continuación del otro y separados por comas, de la misma forma que ocurría con las instrucciones de entrada.
5.3.
Instrucciones compuestas
Son aquellas instrucciones que no pueden ser ejecutadas directamente por el procesador, y están constituidas por un bloque de acciones agrupadas en subrutinas, subprogramas, funciones o módulos.
5.4.
Instrucciones de control
Son utilizadas para controlar la secuencia de ejecución de un programa, así como determinados bloques de instrucciones. Se clasifican en:
5.4.1. Instrucciones de Salto Son aquellas que alteran o rompen la secuencia normal de ejecución de un programa, permitiendo la posibilidad de retornar el control de ejecución al punto de llamada. Pueden ser: • Condicionales Son aquellas que alteran la secuencia de ejecución sólo cuando una condición especificada sea cierta. 3-13
Desarrollo de Sistemas
• Incondicionales Son aquellas que alteran la secuencia normal de ejecución siempre, ya que no van acompañadas de ninguna condición para que se produzcan.
Es necesario indicar, que si bien muchos lenguajes de programación las mantienen entre sus instrucciones por mera compatibilidad no es deontológicamente correcto utilizarlas porque rompen toda lógica de seguimiento del programa cuando hablamos de programación tanto estructurada como orientada a objetos. En la actualidad estas instrucciones son utilizadas en el lenguaje 3-14
Lenguajes de programación
ensamblador que utiliza las instrucciones JMP, que es el acrónimo de salto.
5.4.2. Instrucciones condicionales Son aquellas que controlan la ejecución o la no ejecución de una o más instrucciones en función de que se cumpla o no una condición previamente establecida. •
Sí
No
Alternativa simple
if (condición) { instrucciones } •
Alternativa doble
if(condición){ instrucciones }else{ instrucciones } Sí
•
No
Alternativa múltiple Es similar a la doble pero con más de dos posibilidades de salida del flujo del programa.
switch(x){ case 1: instrucciones break; case 2: instrucciones break;
} else if(condición){ instrucciones
… case n:
}
instrucciones break; default: break;
… else { instrucciones }
•
}
Alternativa anidada (condicionales anidados)
if(condición){ instrucciones if(condición){ instrucciones if(condición){ instrucciones } } } 5.4.3. Instrucciones repetitivas (bucles) Son aquellas que permiten variar o alterar la secuencia de un programa haciendo posible que un grupo de acciones se ejecute más de una vez de forma consecutiva. Reciben el nombre de bucles.
No
Sí
•
Estructura Mientras
while(condicion) { … instrucciones
3-16
Lenguajes de programación
…. } Permite ejecutar un bloque de instrucciones entre 0 y n veces. •
Estructura Repetir-Mientras
do{ … instrucciones … } while(condición); Permite ejecutar un bloque de instrucciones entre 1 y n veces.
•
Sí
No
Estructura Para
for (inicialización; condición _ final; incremento o decremento){ … instrucciones … } Permite ejecutar un bloque de instrucciones una cantidad de veces fijada de antemano.
6.
Recursividad
La recursividad es una técnica muy potente de programación que puede utilizarse en sustitución de las iteraciones (instrucciones repetitivas) o bucles en ciertos problemas. Esta técnica consiste, básicamente, en permitir que un subprograma se llame a sí mismo. El uso de la recursividad es adecuado en problemas a resolver que tengan una clara, definición recursiva (como, por ejemplo, el factorial de un número) y permite desarrollar programas más simples y elegantes. A pesar de ello la recursividad es menos eficiente que los bucles. Diremos entonces que un método es recursivo si entre sus instrucciones tiene una llamada a sí mismo, en la cual mantiene una pila con los valores procesados.
3-17
Desarrollo de Sistemas
función f (parámetros) { … instrucciones; … f(parámetros); } Ejemplo del Cálculo del factorial de 5 escrito en Java:
public class Factoriales { static int factorial(int numero){ if ( numero <= 1 ) { return 1; } else { return numero*factorial(numero-1); } } public static void main(String args[]){ System.out.println(factorial(5)); } }
7.
Procedimientos, funciones y parámetros
El diseño descendente o top-down consiste en una serie de descomposiciones sucesivas del programa inicial, que describen el refinamiento progresivo del repertorio de instrucciones que configuran un programa. Un programa diseñado con top-down quedará claramente constituido por dos partes claramente diferenciadas:
3-18
•
Programa principal: describe la solución completa del problema y consta principalmente de llamadas a subprogramas. También puede contener otras instrucciones.
•
Subprogramas: se encuentran agrupados en diferente lugar que el programa principal. Su estructura básica coincide con la de un programa, con algunas diferencias en su encabezamiento y finalización. La función de un subprograma es resolver de modo independiente una parte del problema. Un subprograma sólo se ejecuta cuando es llamado por el programa principal o por otro subprograma.
Lenguajes de programación
Esta división en módulos se debe terminar cuando en los módulos se definan tareas específicas a realizar. La parte de interconexión entre las subrutinas no debe de tener problemas si el paso de datos (parámetros) entre ellos se especifica claramente. Estos módulos pueden estar ya creados por otros programadores y almacenados en librerías, con lo cual nosotros solo tendríamos que invocarlos para poder usarlos. Como consecuencia de esta técnica de diseño surgen conceptos como:
7.1.
—
Parámetros.
—
Procedimientos.
—
Funciones.
—
Recursividad.
Procedimientos
Un procedimiento es un subprograma que realiza una tarea específica y que puede ser definido mediante cero, uno o “n” parámetros. Tanto la entrada como la devolución de resultados desde el procedimiento al programa llamador se realizará a través de los parámetros. El nombre de un procedimiento no está asociado a ninguno de los resultados que obtiene. Esta compuesto por un grupo de sentencias a las que se asigna un nombre (identificador) y constituye una unidad de programa. La tarea determinada al procedimiento se ejecuta siempre que se encuentra el nombre del procedimiento. La declaración indica las instrucciones a ejecutar. Su sintaxis es:
procedimiento nombreproc (lista de parametros) declaraciones locales inicio cuerpo del procedimiento (instrucciones) fin. Un procedimiento es llamado en un programa o dentro de otro procedimiento directamente por su nombre. Supongamos que queremos calcular el valor medio de tres datos. Definimos un procedimiento “lolo”:
Este supuesto programa nos imprime la media de tres elementos. Nos permiten su utilización sistemática tantas veces como queramos sin necesidad de escribir las instrucciones tantas veces como veces queremos utilizarla. Podría ser utilizada en cualquier lugar de nuestro programa haciendo una llamada del siguiente tipo: lolo(7,6,2) y nos imprimirá en pantalla: 5. Pero en este caso el modulo que ha llamado a este procedimiento no recibe ningún valor de retorno.
7.2.
Funciones
Una función es un subprograma que realiza una tarea específica y devuelve un resultado en el propio nombre de la función. Podría ser definida como un conjunto de instrucciones que permiten procesar las variables para obtener un resultado. Recibe como parámetros (argumentos) datos y devuelve un único resultado. Esta característica le diferencia esencialmente de un procedimiento. Su formato es el siguiente:
tipo_develto funcion nombrefuncion (p1,p2,...) : declaraciones locales inicio cuerpo de la función nombrefuncion // valor a devolver fin Una función es llamada por medio de su nombre, en una sentencia de asignación o en una sentencia de salida. Supongamos que queremos calcular el valor medio de tres datos. Definimos una función “lolo”:
int lolo(par1, par2, par3){ //inicio función aux= (par1+ par2+ par3)/3 return(aux) }fin función Este supuesto programa nos permitiría calcular la media de tres elementos. Lo interesante de utilizar este tipo de funciones es que ellas nos permiten su utilización sistemática tantas veces como queramos sin necesidad de escribir las instrucciones tantas veces como veces queremos utilizarla. Esta función suma podría ser utilizada en cualquier lugar de nuestro programa haciendo una llamada del siguiente tipo: lolo(7,6,2) El modulo llamante recibiría como valor de retorno: 15
3-20
Lenguajes de programación
7.3.
Parámetros
Un subprograma puede realizar, al igual que un programa, operaciones de E/S. Sin embargo, es frecuente que sus datos de entrada y sus resultados provengan y sean enviados del y al programa o subprograma llamante, respectivamente. Para ello, se utilizan los parámetros. Cada vez que se realiza una llamada a un subprograma, los datos de entrada le son pasados por medio de una variable, y, de forma análoga, cuando termina la ejecución del subprograma, los resultados se devuelven mediante esas mismas variables o mediante otras. Existen dos tipos de parámetros: •
Parámetros formales: variables locales de un subprograma, utilizados tanto en la emisión de datos como en la recepción.
•
Parámetros actuales: variables y datos enviados en cada llamada de subprograma, por el programa o subprograma llamante.
Los parámetros formales son siempre fijos para cada subprograma, mientras que los parámetros actuales pueden ser modificados en cada llamada. En cualquier caso, tiene que haber una correspondencia entre los parámetros formales y los actuales. El proceso de emisión y recepción de datos y resultados mediante variables de enlace se denomina paso de parámetros. Se pueden pasar parámetros de dos formas diferentes:
8. 8.1.
—
Por valor: en este caso, el parámetro actual no puede ser modificado por su programa, el cual copia su valor en el parámetro formal correspondiente para poder utilizarlo.
—
Por referencia: cuando un parámetro actual se pasa por referencia se proporciona la dirección o referencia de la variable, con lo que el subprograma llamante la utiliza como propia, modificándola si es necesario. La utilización de esta forma de pasar parámetros supone un ahorro de memoria, debido a que la variable local correspondiente no existe físicamente, sino que es asociada a la global en cada llamada. Es evidente que también supone el riesgo de modificar de forma indeseada una variable global.
Vectores y registros Vectores
Un array o vector es una estructura de datos constituida por un número fijo de elementos, todos ellos del mismo tipo y ubicados en direcciones de memoria físicamente contiguas. 3-21
Desarrollo de Sistemas
A las estructuras de datos cuyos elementos son del mismo tipo, con las mismas características, y que se referencian bajo un nombre o identificador común, reciben el nombre de vectores. Generalmente se definen los vectores como tablas de dimensión uno (unidimensionales). Al igual que las tablas unidimensionales, una tabla bidimensional es un conjunto de elementos del mismo tipo y características, que se referencian bajo un mismo nombre. Este tipo de estructuras también reciben el nombre de matrices. También reciben el nombre de poliedros aquellas tablas de tres o más dimensiones que al igual que las tablas unidimensionales y bidimensionales están constituidas por elementos del mismo tipo y características.
8.2.
Registros
Como ya hemos definido en otros capítulos un fichero es un conjunto de información relacionada entre sí y estructurada en unidades más pequeñas. A estas unidades mas pequeñas en las cuales podemos descomponer un fichero se les denomina “registros” que forman un bloque que puede ser manipulado de forma unitaria. Se pueden clasificar en: •
Registro lógico: las estructuras de datos homogéneas referentes a una misma entidad o cosa, dividida a su vez en elementos más pequeños denominados campos que pueden ser del mismo o diferente tipo. El registro es considerado en sí mismo como una unidad de tratamiento dentro del fichero.
•
Registro físico: también llamado bloque, es la cantidad de información que el sistema puede transferir como unidad, en una sola operación de E/S, entre la memoria principal del ordenador y los periféricos o dispositivos de almacenamiento. El tamaño del bloque o registro físico dependerá de las características del ordenador.
•
Registro bloqueado: un registro físico puede constar de un número variable de registros lógicos. Por tanto, suponiendo que utilizáramos como soporte de almacenamiento el disco, se podrían transferir varios registros lógicos de la memoria al disco y del disco a la memoria en una sola operación de E/S. Este fenómeno recibe el nombre de bloqueo y el registro físico así formado se llama bloque. Se conoce como factor de bloqueo al número de registros lógicos contenidos en un bloque o registro físico.
•
Registro expandido: es justamente el concepto contrario de registro bloqueado, es decir, cuando el registro lógico ocupa varios bloques se le da la denominación de registro expandido.
Los ficheros son estructuras de datos jerarquizadas.
3-22
Lenguajes de programación
9.
Estructura de un programa
Todo programa está constituido por un conjunto de órdenes o instrucciones capaces de manipular un conjunto de datos. Estas órdenes o instrucciones pueden ser divididas en tres grandes bloques claramente diferenciados, correspondientes cada uno de ellos a una parte del diseño de un programa: •
Entrada de datos En este bloque se engloban todas aquellas instrucciones que toman datos de un dispositivo o periférico externo, depositándolos posteriormente en memoria central o principal para poder ser procesados.
•
Proceso o algoritmo Engloba todas aquellas instrucciones encargadas de procesar la información o aquellos datos pendientes de elaborar y que previamente habían sido depositados en memoria principal para su posterior tratamiento. Finalmente, todos los resultados obtenidos en el tratamiento de dicha información son depositados nuevamente en memoria principal, quedando de esta manera disponibles. En definitiva, se puede considerar como una especie de caja negra capaz de albergar unos datos de entrada, realizar unos cálculos previamente definidos y proporcionar unos resultados adecuados. De cara al usuario del programa, cómo se realicen las operaciones de cálculo o se procesen los datos de entrada no es importante, siempre y cuando los resultados obtenidos sean los correctos.
•
Salida de datos o resultados Este bloque está formado por todas aquellas instrucciones que toman los resultados depositados en memoria principal una vez procesados los datos de entrada, enviándolos seguidamente a un dispositivo o periférico externo.
9.1.
Elementos auxiliares de programación
Los elementos auxiliares de programación son variables o conjuntos de variables creadas por el programador con el fin de facilitar la resolución del problema; no forman parte de los datos originales del problema. Entre los elementos auxiliares de programación podemos considerar:
9.1.1. Contadores Contadores son variables que se incrementan o decrementan en una cantidad constante cuando el flujo de control del programa pasa por una determinada posición; en dicho punto encontramos una sentencia del tipo: variable = variable ± incremento
Un contador suele estar asociado a un bucle que determina el número de iteraciones y por tanto de incrementos se tienen que producir para alcan3-23
Desarrollo de Sistemas
zar la condición de salida del bucle y continuar; dicho contador no necesariamente es un número de veces, sino un valor determinado a alcanzar por la variable.
9.1.2. Acumuladores Acumuladores son variables que cambian su valor en dependiendo de una función matemática determinada en el programa; esta variable se diferencia de la variable tipo contador en que la variación no es fija, sino que adopta el valor determinado por una ecuación. La sentencia que lo determina es del tipo: variable = variable expresión (operación aritmética)
9.1.3. Interruptores Un interruptor o switch es una variable que solo puede tomar uno entre dos valores posibles: 1/0, si/no, on/off, verdadero/falso...; este tipo de variable permite la toma de decisión en el programa, de forma que cambie de valor cada vez que el flujo del programa pase por un punto determinado, mediante la siguiente sentencia: switch = 1 ... switch = switch * -1
9.2.
Prueba de programas
El objetivo específico de la fase de pruebas es encontrar la mayor cantidad posible de errores. La prueba ideal de un sistema sería exponerlo en todas las situaciones posibles. Así garantizaríamos su respuesta ante cualquier caso que se le presente en la ejecución real. Esto es imposible desde todos los puntos de vista. Probar un programa es someterle a todas las posible variaciones de los datos de entrada, tanto si son válidos como si no lo son. Probar es buscarle los fallos a un programa. La fase de pruebas absorbe una buena porción de los costes de desarrollo de software.
9.2.1. Prueba de unidades La prueba de unidades se plantea a pequeña escala, y consiste en ir probando uno a uno los diferentes módulos que constituyen una aplicación. Los criterios más habituales son los denominados de caja negra y de caja blanca. Se dice que una prueba es de caja negra cuando prescinde de los detalles del código y se limita a lo que se ve desde el exterior. Intenta descubrir casos y circunstancias en los que el módulo no hace lo que se espera de él. Por oposición al término "caja negra" se suele denominar "caja blanca" al caso contrario, es decir, cuando lo que se mira con lupa es el código que 3-24
Lenguajes de programación
está ahí escrito y se intenta que falle. Quizás sea más propia la denominación de "pruebas de caja transparente". A) Caja blanca o pruebas estructurales o pruebas de caja transparente Las pruebas de caja blanca consisten en probar el código fuente que forma la aplicación. Con la cobertura intentamos formalizar el código probándolo todo de principio a fin. Las pruebas de caja blanca nos sirven para aseverarnos de que un programa hace de modo adecuado los diferentes pasos para los cuales ha sido creado, pero no nos aseveran que haga bien lo que deseamos que haga el programa en su globalidad, la finalidad para la cual ha sido creado el programa. Tenemos varios tipos de coberturas: —
Cobertura de segmentos o cobertura de sentencias. Por segmento se entiende una secuencia de sentencias sin puntos de decisión. Como el ordenador está obligado a ejecutarlas una tras otra, es lo mismo decir que se han ejecutado todas las sentencias o todos los segmentos. El número de sentencias de un programa es finito. Se puede diseñar un plan de pruebas que vaya ejercitando más y más sentencias, hasta que hayamos probado todas.
—
Cobertura de ramas. Se trata de una ampliación de la cobertura de segmentos consistente en recorrer todas las posibles salidas de los puntos de decisión.
—
Cobertura de condición/decisión. Se trata de nuevo de una ampliación de la cobertura de ramas consistente en probar todos las posibles condiciones aunque estas estean formadas por expresiones complejas, como es el caso de una expresión dentro de un “if” que contenga un “||” (o).
—
Cobertura de bucles. Se trata de probar las sentencias de control de iteración, que tanta cantidad de errores suelen acarrear. Pueden llevar consigo expresión booleanas complejas.
En la práctica conviene acercarse al 100% de los segmentos del programa, logrando una buena cobertura de ramas, esto supone, juegos para probar entre el 60 y el 80% del código y dependiendo de la criticidad de la aplicación (sanitarias, centrales nucleares, aplicaciones militares) superar el 90% de segmentos. La ejecución de pruebas de caja blanca puede llevarse a cabo con un depurador (que permite le ejecución paso a paso o el uso de puntos de interrupción el los lugares que el programador desee). Esta tarea es muy tediosa, pero puede ser automatizada a través de la generación de los diversos valores que toman los distintos elementos del programa como son por ejemplo las ventanas de inspección. 3-25
Desarrollo de Sistemas
B) Caja negra o pruebas de caja opaca o pruebas funcionales o pruebas de entrada/salida o pruebas inducidas por los datos Un programa puede estar perfecto en todos sus términos, y sin embargo no servir a la función que se pretende. Las pruebas de caja negra se centran en lo que se espera de un módulo, es decir, intentan encontrar casos en que el módulo no se atiene a su especificación. Por ello se denominan pruebas funcionales, y el probador se limita a suministrarle datos como entrada y estudiar la salida, sin preocuparse de lo que pueda estar haciendo el módulo por dentro. Las pruebas de caja negra se apoyan en la especificación de requisitos del módulo. De hecho, se habla de “cobertura de especificación” para dar una medida del número de requisitos que se han probado. El problema con las pruebas de caja negra no suele estar en el número de funciones proporcionadas por el módulo (que siempre es un número muy limitado en diseños razonables); sino en los datos que se le pasan a estas funciones. El conjunto de datos posibles suele ser muy amplio (por ejemplo, un entero). A la vista de los requisitos de un módulo, se sigue una técnica algebraica conocida como “clases de equivalencia”. Esta técnica trata cada parámetro como un modelo algebraico donde unos datos son equivalentes a otros. Si logramos partir un rango excesivamente amplio de posibles valores reales a un conjunto reducido de clases de equivalencia, entonces es suficiente probar un caso de cada clase, pues los demás datos de la misma clase son equivalentes. Una forma de identificar estas clases de equivalencia son aquellas que respondan a: •
Por debajo/en el/por encima del rango especificado para un tipo de dato.
•
Por debajo/en el/por encima de un valor concreto.
•
Que se encuentre en el conjunto o fuera de él.
•
Que sea verdadero o falso.
•
Utilización de los mismos criterios para los datos de salida.
Lograr una buena cobertura con pruebas de caja negra es un objetivo deseable; pero no suficiente a todos los efectos. Un programa puede pasar con holgura millones de pruebas y sin embargo tener defectos internos que surgen en el momento más inoportuno.
9.2.2
Pruebas de integración
Las pruebas de integración y de aceptación son pruebas a mayor escala, que puede llegar a dimensiones industriales cuando el número de módulos es muy elevado, o la funcionalidad que se espera del programa es muy compleja. 3-26
Lenguajes de programación
Las pruebas de integración se centran en probar la coherencia semántica entre los diferentes módulos, tanto de semántica estática (se importan los módulos adecuados; se llama correctamente a los procedimientos proporcionados por cada módulo), como de semántica dinámica (un módulo recibe de otro lo que esperaba). Normalmente estas pruebas se van realizando por etapas, englobando progresivamente más y más módulos en cada prueba. Las pruebas de integración se pueden empezar en cuanto tenemos unos pocos módulos, aunque no terminarán hasta disponer de la totalidad. En un diseño descendente (topdown) se empieza a probar por los módulos más generales; mientras que en un diseño ascendente se empieza a probar por los módulos de base. El planteamiento descendente tiene la ventaja de estar siempre pensando en términos de la funcionalidad global. El planteamiento ascendente evita tener que escribirse módulos ficticios, pues vamos construyendo pirámides más y más altas con lo que vamos teniendo. Las pruebas de integración se llevan a cabo durante la construcción del sistema, involucran a un número creciente de módulos y terminan probando el sistema como conjunto. Estas pruebas se pueden plantear desde un punto de vista estructural o funcional. Las pruebas estructurales de integración son similares a las pruebas de caja blanca; pero trabajan a un nivel conceptual superior. En lugar de referirnos a sentencias del lenguaje, nos referiremos a llamadas entre módulos. Se trata pues de identificar todos los posibles esquemas de llamadas y ejercitarlos para lograr una buena cobertura de segmentos o de ramas. Las pruebas funcionales de integración son similares a las pruebas de caja negra. Aquí trataremos de encontrar fallos en la respuesta de un módulo cuando su operación depende de los servicios prestados por otros módulos. Según nos vamos acercando al sistema total, estas pruebas se van basando más y más en la especificación de requisitos del usuario. Las pruebas finales de integración cubren todo el sistema y pretenden cubrir plenamente la especificación de requisitos del usuario. Además, a estas alturas ya suele estar disponible el manual de usuario, que también se utiliza para realizar pruebas hasta lograr una cobertura aceptable. En todas estas pruebas funcionales se siguen utilizando las técnicas de partición en clases de equivalencia y análisis de casos límite.
9.2.3. Pruebas de aceptación Las pruebas de aceptación son las que se plantea el cliente final, que decide qué pruebas va a aplicarle al producto antes de darlo por bueno y pagarlo. El objetivo del que prueba es encontrar los fallos lo antes posible, en todo caso antes de pagarlo y antes de poner el programa en producción.
3-27
Desarrollo de Sistemas
Estas pruebas las realiza el cliente. Son básicamente pruebas funcionales, sobre el sistema completo, y buscan una cobertura de la especificación de requisitos y del manual del usuario. Estas pruebas no se realizan durante el desarrollo, pues sería impresentable de cara al cliente; sino una vez pasadas todas las pruebas de integración por parte del desarrollador. —
Las pruebas alfa consisten en invitar al cliente a que venga al entorno de desarrollo a probar el sistema. Se trabaja en un entorno controlado y el cliente siempre tiene un experto a mano para ayudarle a usar el sistema y para analizar los resultados.
—
Las pruebas beta vienen después de las pruebas alfa, y se desarrollan en el entorno del cliente, un entorno que está fuera de control. Aquí el cliente se queda a solas con el producto y trata de encontrarle fallos de los que informa al desarrollador.
Las pruebas alfa y beta son habituales en productos que se van a vender a muchos clientes. Algunos de los potenciales compradores se prestan a estas pruebas bien por ir entrenando a su personal con tiempo, bien a cambio de alguna ventaja económica. La experiencia muestra que estas prácticas son muy eficaces.
9.3.
Verificación y validación La verificación y validación tiene por objetivo: —
Detectar y corregir los defectos cuanto antes, disminuir los riesgos y las desviaciones.
—
Mejorar la calidad y la fiabilidad de los productos “software”.
—
Mejorar la visibilidad de la gestión y valorar rápidamente los cambios propuestos.
—
Detectar y corregir los defectos en el ciclo de vida del “software”.
—
Valorar rápidamente los cambios propuestos y sus consecuencias.
No es por ejemplo un objetivo deducir el tamaño y el tiempo de ejecución del “software”. Verificación y validación no son términos equivalentes que puedan usarse indistintamente cuando se habla del software. VERIFICACIÓN VALIDACIÓN
productos correctos los productos correctos acordes con los requerimientos
El objetivo de la validación del software es la corrección del producto final respecto a las necesidades del usuario. La técnica más tradicional de validación son las pruebas del software. 3-28
Lenguajes de programación
Las principales técnicas de verificación son las revisiones y auditorias de “software”. Entre las actividades de verificación se encuentran comprobar la adecuación de los requisitos, determinar la adecuación del diseño y aplicar los datos de prueba. Una de las actividades de la verificación seria realizar análisis de valores límites. El objetivo de las revisiones técnicas es evaluar un producto intermedio para ver que se ajusta a las especificaciones, para comprobar que el desarrollo se está haciendo de acuerdo con los planes y que los cambios en el producto se realizan adecuadamente. Uno de los objetivos principales de las inspecciones es comprobar si el producto satisface sus especificaciones o los atributos de calidad fijados. El objetivo de la inspección es detectar y registrar los defectos. Uno de los objetivos principales del” walktrhough” es la evaluación de un producto para mejorarlo. El objetivo del “walktrough” es la evaluación de un producto para buscar defectos, omisiones y contradicciones, para mejorar el producto, para evaluar conformidad con normas y considerar posibles soluciones y alternativas a los problemas encontrados.
9.4.
Otras pruebas —
Aleatorias: basadas en que la probabilidad de descubrir un error es similar si se eligen pruebas al azar que si se utilizan criterios de cobertura.
—
Solidez (robustness testing): probando la reacción del sistema ante datos de entrada erróneos.
—
Aguante (stress testing): se trata de probar hasta donde aguanta un programa por razones internas, como puede ser trabajar con una carga de CPU del 90%, un disco con el 90% de espacio ocupado, con memoria ocupada forzando “swapping”.
—
Prestaciones (performance testing): se miden parámetros de consumo de consumo en cuanto a tiempo de respuesta, memoria ocupada, espacio en disco...
—
Interoperabilidad (interoperability testing): Buscan problemas de comunicación entre nuestra aplicación y otras con las que debe trabajar.
3-29
Tema 4 Lenguajes de interrogación de bases de datos. Estándar ANSI-SQL.
Desarrollo de Sistemas
Guión-resumen
1. Lenguajes de interrogación de bases de datos 2. Subconjuntos de ANSI-SQL 2.1. Formas de utilizar ANSI-SQL 2.2. Sentencias SQL 3. Consultas de selección 3.1. Consultas básicas 3.2. Agrupamiento de registros y cálculo de totales con funciones agregadas 3.3. Subconsultas 3.4. Unión de consultas 3.5. Consultas de Combinación entre tablas 4. Funciones 4.1. Funciones colectivas 4.2. Funciones escalares
4-2
5. Mantenimiento de los datos. DML 6. Definición de los datos. DDL 6.1. Crear objetos 6.2. Modificar objetos 6.3. Eliminar objetos 7. Conceptos de interés 7.1. Variables 7.2. Control de ejecución 7.3. Transacciones 7.4. Cursores 7.5. Procedimientos almacenados (Store procedure) 7.6. Desencadenadores (triggers) 7.7. Bloqueos
Lenguajes de interrogación de bases de datos
1.
Lenguajes de interrogación de bases de datos
SQL (Structure Query Language o lenguaje estructurado de consulta) es un lenguaje que permite realizar operaciones diversas sobre datos almacenados en bases de datos relacionales, en los que la información se almacena en tablas bidimensionales, con los datos dispuestos en filas y columnas. Las sentencias de SQL permiten manejar conjuntos de registros, en lugar de un registro cada vez. La mayoría de los gestores, tanto los basados en una arquitectura cliente/servidor como otros entornos de programación, usan SQL como medio para acceder a los datos. SQL tiene una estructura relativamente simple, que le otorga una gran flexibilidad y potencia. El número de sentencias existentes en este lenguaje es muy reducido, por lo que facilita el aprendizaje del mismo. La versión original fue desarrollada por IBM y se denominaba SEQUEL; tenía unas pocas palabras reservadas utilizables con una sintaxis muy sencilla. Cada nuevo producto ha ido incorporando nuevas palabras reservadas, dando paso a nuevos “dialectos” de un mismo lenguaje (SQL de ORACLE, SQL/400 de IBM, Transact SQL Server de Microsoft, etc.). SQL ha sido estandarizado para lograr así un lenguaje más o menos común para todos los gestores de bases de datos, pero cada uno de estos lenguajes tiene algunos mandatos “propios” que no están incluidos en la lista de palabras reservadas por el American National Standard Institute SQL, o lo que es lo mismo, ANSI-SQL. Los mandatos se escriben en inglés, y no importa que estén en mayúsculas, minúsculas o intercaladas. Hay que respetar en todo momento el orden sintáctico de las sentencias, no sólo escribir los mandatos correctamente.
2.
Subconjuntos de ANSI-SQL
El uso principal de ANSI-SQL es consultar y modificar los datos almacenados en bases de datos relacionales, aunque el lenguaje permite realizar otras tareas. La clasificación de estas tareas permite hacer lo propio con el lenguaje, que se clasifica en: —
Consulta de datos (Data Query Language). DQL. Consta de sentencias que se encargan de visualizar, organizar y seleccionar los datos de las tablas. La sentencia principal es SELECT.
—
Manipulación de datos (Data Manipulation Language). DML. Son sentencias que permiten añadir, modificar y borrar filas sobre las tablas. Estas sentencias son INSERT (para añadir), UPDATE (para modificar) y DELETE (para borrar).
4-3
Desarrollo de Sistemas
—
Definición de datos (Data Definition Language). DDL. Son sentencias para crear, modificar, renombrar o borrar objetos (CREATE, ALTER, RENAME y DROP), otorgar restricciones a los campos de las tablas (CHECK, CONSTRAINT y NOT NULL), establecer relaciones entre tablas (PRIMARY KEY, FOREIGN KEY y REFERENCES).
—
Control de datos (DATA CONTROL LANGUAGE). DCL. Controla la seguridad de los datos; por ejemplo, otorga permisos a usuarios para acceder a los datos. Las sentencias que realizan esto son GRANT Y REVOKE.
—
Procesado de transacciones (Transaction-Processing Language). TPL. Son sentencias encargadas de vigilar mandatos del DML para que funcionen de forma coherente. COMMIT, ROLLBACK y BEGIN TRANSACTION.
—
Control de cursores (Cursor-Control Language). CCL. Opera sobre filas individuales de una tabla, resultado que afecta a varios registros; FETCH INTO, UPDATE WHERE CURRENT, DECLARE CURSOR.
2.1.
Formas de utilizar ANSI-SQL
Los mandatos de SQL pueden ejecutarse en diferentes entornos y lenguajes de programación. Los métodos de uso son: •
Estáticos: —
SQL interactivo Las sentencias se escriben directamente por parte del usuario y el gestor las responde de manera directa. Un gestor, por ejemplo Access, nos brinda la posibilidad de ejecutar los mandatos directamente (dentro de la sección de consultas, vista SQL), en lugar de utilizar las opciones de los menús.
—
SQL inmerso en programas (Embedded SQL) Lenguajes de programación como C, Visual Basic, ASP o JavaScript permiten insertar mandatos de SQL entre sus líneas de código. Cuando se ejecuta el programa, un precompilador interpreta estas órdenes sql y las envía al Gestor de la base de datos. Por ejemplo SQLJ permite embeber sentencia SQL en programas escritos en Java. Un programa con SQL embebido es mucho más potente y rápido que si se utiliza el código del propio lenguaje, pues sql es el lenguaje que utilizan la mayoría de los gestores y la orden la ejecuta directamente.
—
SQL modular Permite compilar sentencias SQL por separado del lenguaje de programación, para posteriormente enlazarlas (link) con el resto de módulos del programa.
4-4
Lenguajes de interrogación de bases de datos
•
Dinámico: —
SQL dinámico Se dice que los anteriores son “sql estáticos”, pues los mandatos ya están escritos. Un ejemplo sería que, una vez que está corriendo un programa, éste permitiera al usuario escribir un sentencia sql y enviarla al gestor.
2.2.
Sentencias SQL Una sentencia SQL está compuesta por: —
Palabras predefinidas: tienen un significado propio. Todas las sentencias empiezan por una palabra predefinida. Ejemplos: SELECT, ORDER BY, etc.
—
Nombres de campos y de tablas: son meros identificadores inventados al crear la tabla y sus campos.
—
Contantes (literales): representan un valor predeterminado. Los datos alfanuméricos (texto) van entre apóstrofes.
—
Delimitadores: sirven para delimitar o separar a los anteriores. Son los paréntesis, las comas, espacio en blanco, etc.
—
Tipos de datos: •
Numéricos: VALOR
LONGITUD
DESCRIPCIÓN
BIT
1 byte
Valores enteros con un valor de 0 o 1
SMALLINT
2 bytes
Un entero corto entre -32,768 y 32,767
INT
4 bytes
Un entero largo entre -2,147,483,648 y 2,147,483,647
DECIMAL
4 bytes
Números de precisión y escala fija con valores de -10^38+1 a 10^38-1
REAL
4 bytes
Valores de precisión flotante desde -3,40E+38 a 3,40E+38
FLOAT
8 bytes
Valores de precisión flotante desde -1,79E+308 a 1,79E308
4-5
Desarrollo de Sistemas
•
Alfanuméricos: VALOR
•
LONGITUD
DESCRIPCIÓN
CHAR
1 byte por carácter
Campos de caracteres de longitud fija por carácter Unicode con un tamaño máximo de 8.000 caracteres.
NCHAR
1 byte por carácter
Datos Unicode de longitud fija con un tamaño máximo de 4.000 caracteres.
VARCHAR
1 byte por carácter
Campos de caracteres de longitud variable no Unicode con un tamaño máximo de 8.000 caracteres.
NVARCHAR
1 byte por carácter
Datos Unicode de longitud variable con un tamaño máximo de 4.000 caracteres.
TEXT
1 byte por carácter
Campos de caracteres de longitud variable no Unicode con un tamaño máximo de 2.147.483.647 caracteres.
LONGITUD
DESCRIPCIÓN
Fecha y hora: VALOR
•
DATETIME
8 bytes
Valores de fecha y hora desde el 1 de enero de 1753 al 31 de Diciembre de 9999.
TIMESTAMP
8 bytes
Captura del sistema, un Instante.
Binarios: VALOR BINARY
4-6
LONGITUD 1 byte
DESCRIPCIÓN Datos binarios de longitud fija con un tamaño máximo de 8.000 bytes.
Lenguajes de interrogación de bases de datos
—
Operadores: •
Lógicos: OPERADOR
•
USO
AND
Es el "y" lógico. Evalúa dos condiciones y devuelve un valor de verdad solo si ambas son ciertas.
OR
Es el "o" lógico. Evalúa dos condiciones y devuelve un valor de verdad si alguna de las dos es cierta.
NOT
Negación lógica. Devuelve el valor contrario de la expresión.
Relacionales o de comparación: OPERADOR
•
USO
<
Menor que.
>
Mayor que.
<>
Distinto de.
<=
Menor o igual que.
>=
Mayor o igual que.
=
Igual que.
BETWEEN
Utilizado para especificar un intervalo de valores.
LIKE
Utilizado en la comparación de un modelo.
IN
Utilizado para especificar registros de una base de datos.
Aritméticos: OPERADOR
USO
()
Paréntesis
*
Multiplicación
/
División
+
Suma
-
Resta
%
Módulo
4-7
Desarrollo de Sistemas
—
Cláusulas: Las cláusulas son condiciones de modificación utilizadas para definir los datos que desea seleccionar o manipular. CLÁUSULA
—
FROM
Utilizada para especificar la tabla de la cual se van a seleccionar los registros.
WHERE
Utilizada para especificar las condiciones que deben reunir los registros que se van a seleccionar.
GROUP BY
Utilizada para separar los registros seleccionados en grupos específicos.
HAVING
Utilizada para expresar la condición que debe satisfacer cada grupo.
ORDER BY
Utilizada para ordenar los registros seleccionados de acuerdo con un orden específico.
Funciones de Agregado: Las funciones de agregado se usan dentro de una clausura SELECT en grupos de registros para devolver un único valor que se aplica a un grupo de registros. FUNCIÓN
4-8
DESCRIPCIÓN
DESCRIPCIÓN
AVG
Utilizada para calcular el promedio de los valores de un campo determinado.
COUNT
Utilizada para devolver el número de registros de la selección.
SUM
Utilizada para devolver la suma de todos los valores de un campo determinado.
MAX
Utilizada para devolver el valor más alto de un campo especificado.
MIN
Utilizada para devolver el valor más bajo de un campo especificado
Lenguajes de interrogación de bases de datos
3.
Consultas de selección
SELECT [ predicado ] { * | tabla.* | [ tabla. ] campo1 [ AS alias1 ] [ , tabla.]campo2 [ AS alias2 ] [ , ... ] } FROM tabla [ , ... ] [ WHERE criterio ] [ NOT ] [ IN ] [ ( valor1, [ valor2, [ ... ] ] ) ] [ GROUP BY expresion_group ] [ HAVING criterio ] [ ORDER BY expresion_order [ ASC | DESC ] ] [ cláusula_subconsulta [cláusula_subconsulta [ ... ] ] ] Predicado
Palabra clave (ALL, DISTINCT, TOP) que puede seguir a la cláusula SELECT para restringir el número de registros que se obtienen.
Tabla
Nombre de la tabla de la que vamos a obtener los campos.
campo_n
Nombre de los campos que van a ser mostrados.
AS
The keyword that is used to signify that an alias is to be used in the place of the field name.
Alias
Nombre alternativo utilizado al mostrar los campos.
Criterio
Condición que determina que registros van a aparecer en la consulta.
NOT
Palabra clave que, utilizada como parte de un criterio o junto con el IN, permite indicar qué valores NO han de obtenerse en una consulta.
IN
Palabra clave que permite indicar una lista de valores dentro de la cual vamos a buscar.
Valor1
Parámetro usado en la cláusula IN para indicar la lista de valores en la que queremos buscar.
Expresion_group
Parámetro que especifica por qué campo(s) vamos a crear grupos.
Expresion_order
Parámetro que indica qué campo(s) vamos a utilizar para ordenar los resultados y con qué criterio.
ASC | des
Especifica que los resultados de la consultas van a ser ordenados en orden ASC Ascendente o DESC Descendente.
Clausula_subconsulta Una consulta anidada.
4-9
Desarrollo de Sistemas
Las consultas de selección se utilizan para indicar al motor de datos que devuelva información de las bases de dato. Si la tabla estuviera vacía, no mostraría datos o iría acompañado de un mensaje indicando los 0 registros encontrados. Todos los ejemplos de consultas van a utilizar la tabla árboles: CLAVE
3.1.
TIPO ALTURA PRECIO
VENDIDO
FECHA
1 pino
2 10,00 €
No 01/01/2008
2 abeto
3 12,00 €
Sí 01/01/2008
3 pino
2 15,00 €
No 01/01/2008
4 cerezo
3 14,00 €
No 01/01/2008
5 pino
4 13,00 €
Sí 01/01/2008
6 abeto
3 18,00 €
No 02/01/2008
7 pino
2 15,00 €
No 02/01/2008
8 pino
5 14,00 €
No 02/01/2008
9 abeto
6 16,00 €
No 02/01/2008
10 cerezo
4 17,00 €
Sí 02/01/2008
Consultas básicas SELECT campos FROM tabla
TIPO ALTURA pino
2
abeto
3
pino
2
Podemos preceder del nombre del campo del nombre de la tabla en el caso de que varios campos coincidan en el nombre.
cerezo
3
pino
4
SELECT Clientes.Nombre, Clientes.Teléfono FROM Clientes
abeto
3
pino
2
pino
5
abeto
6
cerezo
4
Donde campos es la lista de campos que se deseen recuperar y tabla es el origen de los mismos, por ejemplo: SELECT Tipo, Altura FROM Arboles
4-10
Lenguajes de interrogación de bases de datos
En determinadas ocasiones nos puede interesar incluir una columna con un texto fijo en una consulta de selección; por ejemplo, supongamos que tenemos una tabla de empleados y deseamos recuperar sus tarifas semanales, podríamos realizar la siguiente consulta: SELECT Tipo,’Total:’, [Precio]+10 FROM arboles;
TIPO
EXPR1001
EXPR1002
pino
Total:
20,00 €
abeto
Total:
22,00 €
pino
Total:
25,00 €
...
...
...
TIPO
Total:
pino
20,00 €
abeto
22,00 €
pino
25,00 €
cerezo
24,00 €
...
...
También podemos darle una Alias al nuevo campo calculado del siguiente modo: SELECT Tipo, [Precio]+10 AS Total FROM arboles; Adicionalmente se puede especificar el orden en que se desean recuperar los registros de las tablas mediante la cláusula ORDER BY lista de campos. En donde lista de campos representa los campos a ordenar. Ejemplo:
TIPO ALTURA PRECIO
SELECT Tipo, Precio FROM arboles ORDER BY Precio; Se pueden ordenar los registros por más de un campo, como por ejemplo: SELECT CodigoPostal, Nombre, Telefono FROM Clientes ORDER BY CodigoPostal, Nombre
pino
2 10,00 €
abeto
3 12,00 €
pino
4 13,00 €
pino
5 14,00 €
cerezo
3 14,00 €
pino
2 15,00 €
pino
2 15,00 €
pino
6 16,00 €
cerezo
4 17,00 €
abeto
3 18,00 €
4-11
Desarrollo de Sistemas
TIPO ALTURA PRECIO
Incluso se puede especificar el orden de los registros: ascendente mediante la cláusula (ASC - se toma este valor por defecto) o descendente (DESC) SELECT Tipo, Altura,Precio FROM arboles ORDER BYAltura DESC ,Precio;
abeto
2 10,00 €
pino
3 12,00 €
pino
4 13,00 €
cerezo
5 14,00 €
abeto
3 14,00 €
cerezo
2 15,00 €
abeto
2 15,00 €
pino
6 16,00 €
pino
4 17,00 €
pino
3 18,00 €
El predicado se incluye entre la cláusula y el primer nombre del campo a recuperar, los posibles predicados son: TIPO pino abeto pino cerezo pino abeto pino pino abeto cerezo
—
ALL: si no se incluye ninguno de los predicados se asume ALL. El Motor de base de datos selecciona todos los registros que cumplen las condiciones de la instrucción SQL y devuelve todos y cada uno de sus campos. No es conveniente abusar de este predicado ya que obligamos al motor de la base de datos a analizar la estructura de la tabla para averiguar los campos que contiene, es mucho más rápido indicar el listado de campos deseados. SELECT ALL Tipo FROM arboles SELECT * Tipo FROM arboles
4-12
Lenguajes de interrogación de bases de datos
—
TOP: devuelve un cierto número de registros que entran entre al principio o al final de un rango especificado por una cláusula ORDER BY (sino devolvería 4 registros elegidos arbitrariamente). Supongamos que queremos recuperar los 4 primeros registros de árboles ordenados por altura en descendente:
ALTURA 6 5 4 3
SELECT TOP 4 Altura FROM arboles ORDER BY Altura DESC
El predicado TOP no distingue entre valores iguales. No confundamos la anterior consulta con la consulta “obtener las cuatro alturas mayores de la tabla”. Vemos que el 4 sale dos veces. Tendríamos que utilizar DISTINCT para eliminar duplicados.
ALTURA 6 5 4
SELECT DISTINCT TOP 4 Altura FROM arboles ORDER BY Altura DESC
—
DISTINCT: omite los registros que contienen datos duplicados en los campos seleccionados. DISTINCT devuelve aquellos registros cuyos campos indicados en la cláusula SELECT posean un contenido diferente. SELECT DISTINCT Tipo FROM arboles
3
TIPO abeto cerezo pino
La cláusula WHERE puede usarse para determinar qué registros de las tablas enumeradas en la cláusula FROM aparecerán en los resultados de la instrucción SELECT. Después de escribir esta cláusula se deben especificar las condiciones expuestas en los dos primeros apartados de este capítulo. Si no se emplea esta cláusula, la consulta devolverá todas las filas de la tabla. WHERE es opcional, pero cuando aparece debe ir a continuación de FROM. SELECT Apellidos, Salario FROM Empleados WHERE Salario > 21000 SELECT Id, Existencias FROM Productos WHERE Existencias <= Pedido SELECT Apellidos, Nombre FROM Empleados WHERE Apellidos = 'King' SELECT * FROM Empleados WHERE Edad > 25 AND Edad < 50 SELECT * FROM Empleado WHERE (Edad > 25 AND Edad < 50) OR SUELDO=1 SELECT * FROM Empleados WHERE NOT Estado = 'Soltero' SELECT * FROM Empleados WHERE (Sueldo > 100 AND Sueldo < 500) OR (Provincia = 'Madrid' AND Estado = 'Casado') 4-13
Desarrollo de Sistemas
Intervalos de Valores: para indicar que deseamos recuperar los registros, según el intervalo de valores de un campo, emplearemos el operador BETWEEN cuya sintaxis es: campo [NOT] BETWEEN valor1 AND valor2 (la condición Not es opcional) En este caso la consulta devolvería los registros que contengan en “campo” un valor incluido en el intervalo valor1, valor2 (ambos inclusive). Si anteponemos la condición Not devolverá aquellos valores no incluidos en el intervalo. SELECT * FROM Pedidos WHERE CodPostal BETWEEN 28000 AND 28999 (Devuelve los pedidos realizados en la provincia de Madrid) SELECT Apellidos, Salario FROM Empleados WHERE Salario BETWEEN 200 AND 300 SELECT Apellidos, Salario FROM Empl WHERE Apellidos BETWEEN 'Lon' AND 'Tol'; El Operador LIKE: se utiliza para comparar una expresión de cadena con un modelo en una expresión SQL. Su sintaxis es: expresión LIKE modelo En donde expresión es una cadena modelo o campo contra el que se compara expresión. Se puede utilizar el operador LIKE para encontrar valores en los campos que coincidan con el modelo especificado. Por modelo puede especificar un valor completo (Ana María), o se pueden utilizar caracteres comodín como los reconocidos por el sistema operativo para encontrar un rango de valores (LIKE ‘An%’). CARÁCTER
4-14
COMODÍN
DESCRIPCIÓN
%
Cualquier cadena compuesta por uno o más caracteres.
_ (underscore)
Cualquier carácter (sólo uno).
[ rango]
Cualquier carácter especificado dentro del rango.
[ ^ rango]
Cualquier carácter que no aparezca dentro del rango..
#
Cualquier dígito.
a-z
Rango de valores de la ‘a’ a la ‘z’ ambos inclusive
Lenguajes de interrogación de bases de datos
El operador LIKE se puede utilizar en una expresión para comparar un valor de un campo con una expresión de cadena. Por ejemplo, si introduce LIKE ‘C%’ en una consulta SQL, la consulta devuelve todos los valores de campo que comiencen por la letra C. El ejemplo siguiente devuelve los datos que comienzan con la letra P seguido de cualquier letra entre A y F y de tres dígitos: LIKE 'P[A-F]###' Este ejemplo devuelve los campos cuyo contenido empiece con una letra de la A a la D seguidas de cualquier cadena: LIKE '[A-D]%' En la tabla siguiente se muestra cómo utilizar el operador LIKE para comprobar expresiones con diferentes modelos. El Operador IN. Este operador devuelve aquellos registros cuyo campo indicado coincide con alguno de los en una lista. Su sintaxis es: expresión [NOT] IN (valor1, valor2, . . .) SELECT * FROM Pedidos WHERE Provincia IN ('Madrid', 'Barcelona', 'Sevilla') SELECT Nombre FROM Empleados WHERE Ciudad IN ('Sevilla', 'Los Angeles')
3.2.
Agrupamiento de registros y cálculo de totales con funciones agregadas
La cláusula GROUP BY combina los registros con valores idénticos, en la lista de campos especificados, en un único registro. Para cada grupo se calcula un totalizado si se incluye una función SQL agregada, como por ejemplo SUM o COUNT dentro de la instrucción SELECT. GROUP BY es opcional. A menos que contenga un dato Memo u Objeto OLE, un campo de la lista de campos GROUP BY puede referirse a cualquier campo de las tablas que aparecen en la cláusula FROM, incluso si el campo no está incluido en la instrucción SELECT, siempre y cuando la instrucción SELECT incluya al menos una función SQL agregada. Todos los campos de la lista de campos de SELECT deben, o bien incluirse en la cláusula GROUP BY o, como argumentos de una función SQL agregada. SELECT Id_Familia, SUM(Stock) FROM Productos GROUP BY Id_Familia Una vez que GROUP BY ha combinado los registros, HAVING muestra cualquier registro agrupado por la cláusula GROUP BY que satisfaga las condiciones de la cláusula HAVING. Se utiliza la cláusula WHERE para excluir aquellas filas que no desea agrupar, y la cláusula HAVING para filtrar sobre los valores calculados como totales. 4-15
Desarrollo de Sistemas
SELECT Id_Familia SUM(Stock) FROM Productos WHERE NombreProducto LIKE BOS% GROUP BY Id_Familia HAVING SUM(Stock) > 100 —
AVG: función agregada que calcula la media aritmética de un conjunto de valores contenidos en un campo especificado de una consulta. Su sintaxis es AVG (expr) donde expr representa el campo que contiene los datos numéricos para los que se desea calcular la media o una expresión que realiza un cálculo utilizando los datos de dicho campo. La media calculada por AVG es la media aritmética (la suma de los valores dividido por el número de valores). La función AVG no incluye ningún campo Null en el cálculo. SELECT AVG (Gastos) AS Promedio FROM Pedidos WHERE Gastos > 100
—
COUNT: calcula el número de registros que hemos obtenido al ejecutar la consulta. Su sintaxis es COUNT(expr) donde expr contiene el nombre del campo que desea contar. Los operandos de expr pueden incluir el nombre de un campo de una tabla, una constante o una función (la cual puede ser intrínseca o definida por el usuario pero no otras de las funciones agregadas de SQL). Puede contar cualquier tipo de datos incluso texto. Aunque expr puede realizar un cálculo sobre un campo, COUNT simplemente cuenta el número de registros sin tener en cuenta qué valores se almacenan en los registros. La función COUNT no cuenta los registros que tienen campos null a menos que expr sea el carácter comodín asterisco (*). Si utiliza un asterisco, COUNT calcula el número total de registros, incluyendo aquellos que contienen campos null. COUNT (*) es considerablemente más rápida que COUNT (Campo). No se debe poner el asterisco entre dobles comillas ('*'). SELECT COUNT(*) AS Total FROM Pedidos Si expr identifica a múltiples campos, la función COUNT cuenta un registro sólo si al menos uno de los campos no es Null. Si todos los campos especificados son Null, no se cuenta el registro. Hay que separar los nombres de los campos con ampersand (&). SELECT COUNT(FechaEnvío & Transporte) AS Total FROM Pedidos Podemos hacer que el gestor cuente los datos diferentes de un determinado campo: SELECT COUNT(DISTINCT Localidad) AS Total FROM Pedidos
—
4-16
MAX y MIN: devuelven el mínimo o el máximo de un conjunto de valores contenidos en un campo específico de una consulta. Su sintaxis es MIN(expr) y MAX(expr), en donde expr es el campo sobre
Lenguajes de interrogación de bases de datos
el que se desea realizar el cálculo. Expr puede incluir el nombre de un campo de una tabla, una constante o una función (la cual puede ser intrínseca o definida por el usuario pero no otras de las funciones agregadas de SQL). SELECT MIN(Gastos) AS ElMin FROM Pedidos WHERE Pais = 'España' SELECT MAX(Gastos) AS ElMax FROM Pedidos WHERE Pais = 'España' —
SUM: devuelve la suma del conjunto de valores contenido en un campo específico de una consulta. Su sintaxis es: SUM(expr) en donde expr representa el nombre del campo que contiene los datos que desean sumarse o una expresión que realiza un cálculo utilizando los datos de dichos campos. Los operandos de expr pueden incluir el nombre de un campo de una tabla, una constante o una función (la cual puede ser intrínseca o definida por el usuario pero no otras de las funciones agregadas de SQL). SELECT SUM(PrecioUnidad * Cantidad) AS Total FROM DetallePedido
Ejemplos de Totalizados sobre la tabla árboles. SELECT Count(Clave) AS CuentaDeClave FROM arboles Contamos el campo clave y nos devuelve 10 registros. No hemos agrupado por ningún campo, razón por la cual los diez registros forman un único grupo y obtenemos un único totalizado.
CONSULTA1 CuentaDeClave 10
SELECT Count(Clave) AS CuentaDeClave,Tipo FROM arboles GROUP BY Tipo; Ídem al anterior, pero agrupando por Tipo. Como tenemos tres tipos distintos de árboles, por cada grupo obtenemos un total que nos dice cuántos registros hay en cada grupo.
CONSULTA1 CuentaDeClave
Tipo
3
Abeto
2
Cerezo
5
Pino
SELECT Tipo, Count(Clave) AS CuentaDeClave, Max(Altura) AS MáxDeAltura, Min(Altura) AS MínDeAltura, Avg(Altura) AS PromedioDeAltura FROM arboles GROUP BYTipo; CONSULTA1 Tipo
CuentaDeClave
MáxDeAltura
MínDeAltura
PromedioDeAltura
Abeto
3
6
3
4
Cerezo
2
4
3
3,5
Pino
5
5
2
3
4-17
Desarrollo de Sistemas
SELECT Tipo,Altura, Sum(Precio) AS SumaDePrecio FROM arboles GROUP BY Tipo,Altura; CONSULTA1 Tipo
Altura
Suma De Precio
Abeto
3
30,00 €
Abeto
6
16,00 €
Cerezo
3
14,00 €
Cerezo
4
17,00 €
Pino
2
40,00 €
Pino
4
13,00 €
Pino
5
14,00 €
Primer ejemplo de un totalizado en el que se agrupa por dos campos a la vez, El tipo de árbol y la Altura del mismo. Se han de hacer grupos en los que coincidan el tipo y la altura. Por ello obtenemos abetos de 3 y de 6.
SELECT Tipo, Sum(Precio) AS SumaDePrecio FROM arboles WHERE Tipo="pino" GROUP BY arboles.Tipo; CONSULTA1 Tipo
Suma De Precio
En este caso añadimos un criterio con Where para limitar el número de registros que intervienen en la consulta. Sólo quiero totalizar los pinos.
67,00 €
Pino
SELECT Tipo, Sum( Precio) AS Suma arboles GROUP BYTipo HAVING Sum(.Precio)>40 CONSULTA1 Tipo
Suma
Abeto
46,00 €
Pino
67,00 €
3.3.
Y ahora ponemos un criterio al totalizado utilizando HAVING. Sólo quiero aquellas sumas mayores de 40.
Subconsultas
Una subconsulta es una instrucción SELECT anidada dentro de una instrucción SELECT, SELECT...INTO, INSERT...INTO, DELETE, o UPDATE o dentro de otra subconsulta. Puede utilizar tres formas de sintaxis para crear una subconsulta: comparación [ANY | ALL | SOME] (instrucción sql) expresión [NOT] IN (instrucción sql) 4-18
Lenguajes de interrogación de bases de datos
[NOT] EXISTS (instrucción sql) Tablas que van a utilizarse en los ejemplos de consultas anidadas. PADRES
HIJOS
Dni
Nombre
Altura
Fecha Nacimiento
1
Luis
123
12/01/1987
1
p1
12
1
2
Ana
145
13/04/2000
2
p2
10
3
3
Jose
167
17/08/2001
3
p3
6
2
4
Alberto
187
23/04/1998
4
p1
7
4
5
Ana María
123
26/02/1986
5
p2
5
3
6
Alba
156
17/08/2001
6
p3
8
5
7
Santiago
159
23/04/1998
7
p1
9
4
8
Adolfo
100
01/01/2000
8
p2
4
6
9
p3
12
7
10
p1
19
1
11
p2
9
2
12
p3
8
3
13
p1
5
2
14
p2
4
4
15
p3
6
5
Factura Producto Cantidad
Dni
Se puede utilizar una subconsulta en lugar de una expresión en la lista de campos de una instrucción SELECT o en una cláusula WHERE o HAVING. En una subconsulta, se utiliza una instrucción SELECT para proporcionar un conjunto de uno o más valores especificados para evaluar en la expresión de la cláusula WHERE o HAVING. Se puede utilizar el predicado ANY o SOME, los cuales son sinónimos, para recuperar registros de la consulta principal, que satisfagan la comparación con cualquier otro registro recuperado en la subconsulta. El ejemplo siguiente devuelve todos los productos cuyo precio unitario es mayor que el de cualquier producto vendido con un descuento igual o mayor al 25 por ciento:
4-19
Desarrollo de Sistemas
SELECT * FROM PADRES WHERE DNI = ANY (SELECT DNI FROM HIJOS WHERE PRODUCTO LIKE 'P3');
Dni
Nombre
Altura
Fecha Nacimiento
2
Ana
145
13/04/2000
Este ejemplo nos permite mostrar toda la información disponible de aquellos padres que han comprado el producto p3.
2
Jose
167
17/08/2001
5
Ana María
123
26/02/1986
7
Santiago
159
23/04/1998
El predicado ALL se utiliza para recuperar únicamente aquellos registros de la consulta principal que satisfacen la comparación con todos los registros recuperados en la subconsulta. Esto es mucho más restrictivo. SELECT * FROM Padres WHERE Dni<>All (SELECT dni from Hijos where Producto like 'P3');
Dni
Nombre
Altura
Fecha Nacimiento
1
Luis
123
12/01/1987
4
Alberto
187
23/04/1998
6
Alba
156
17/08/2001
El predicado IN se emplea para recuperar únicamente aquellos registros de la consulta principal para los que algunos registros de la subconsulta contienen un valor igual. SELECT * FROM Padres WHERE DNI IN (SELECT dni from Hijos where Producto like 'P3'); Este ejemplo nos permite mostrar toda la información disponible de aquellos padres que han comprado el producto p3. Como podemos observar es idéntica a la anterior.
Dni
Nombre
Altura
Fecha Nacimiento
2
Ana
145
13/04/2000
3
Jose
167
17/08/2001
5
Ana María
123
26/02/1986
7
Santiago
159
23/04/1998
=ANY es igual que IN Igualmente se puede utilizar NOT IN para recuperar únicamente aquellos registros de la consulta principal para los que no hay ningún registro de la subconsulta que contenga un valor igual. SELECT * FROM PADRES WHERE DNI IN (SELECT DNI FROM HIJOS WHERE PRODUCTO LIKE 'P3');
4-20
Dni
Nombre
Altura
Fecha Nacimiento
1
Luis
123
12/01/1987
4
Alberto
187
23/04/1998
6
Alba
156
17/08/2001
Lenguajes de interrogación de bases de datos
El predicado EXISTS (con la palabra reservada NOT opcional) se utiliza en comparaciones de verdad/falso para determinar si la subconsulta devuelve algún registro. SELECT * FROM PADRES WHERE EXISTS (SELECT * FROM HIJOS WHERE HIJOS.DNI=PADRES.DNI)
Dni
Nombre
Altura
Fecha Nacimiento
1
Luis
123
12/01/1987
2
Ana
145
13/04/2000
3
Jose
167
17/08/2001
4
Alberto
187
23/04/1998
5
Ana María
123
26/02/1986
6
Alba
156
17/08/2001
7
Santiago
159
23/04/1998
Esta consulta es equivalente a esta otra: SELECT * FROM padres WHERE DNI IN (SELECT DNI FROM HIJOS)
Utilizando NOT podemos buscar los padres que no tienen hijos. SELECT * FROM PADRES WHERE NOT EXISTS (SELECT * FROM HIJOS WHERE HIJOS.DNI=PADRES.DNI); Que es equivalente a: SELECT * FROM PADRES WHERE PADRES.DNI NOT IN (SELECT DNI FROM HIJOS);
Dni
Nombre
Altura
Fecha Nacimiento
8
Adolfo
100
01/01/2000
Se puede utilizar también alias del nombre de la tabla en una subconsulta para referirse a tablas listadas en la cláusula FROM fuera de la subconsulta. El ejemplo siguiente devuelve los nombres de los empleados cuyo salario es igual o mayor que el salario medio de todos los empleados con su mismo título. A la tabla Empleados se le ha dado el alias T1: SELECT Apellido, Nombre, Titulo, Salario FROM Empleados AS T1 WHERE Salario >= (SELECT AVG(Salario) FROM Empleados WHERE T1.Titulo =Empleados.Titulo) ORDER BY Titulo En el ejemplo anterior, la palabra reservada AS es opcional. SELECT Apellidos, Nombre, Cargo, Salario FROM Empleados WHERE Cargo LIKE 'Agente Ven*' AND Salario >ALL (SELECT Salario FROM Empleados WHERE Cargo LIKE '*Jefe*' OR Cargo LIKE *Director*' ) (Obtiene una lista con el nombre, cargo y salario de todos los agentes de ventas cuyo salario es mayor que el de todos los jefes y directores.) 4-21
Desarrollo de Sistemas
SELECT DISTINCT NombreProducto, Precio_Unidad FROM Productos WHERE PrecioUnidad = (SELECT PrecioUnidad FROM Productos WHERE NombreProducto = 'Almíbar anisado') (Obtiene una lista con el nombre y el precio unitario de todos los productos con el mismo precio que el almíbar anisado.) SELECT Nombre, Apellidos FROM Empleados AS E WHERE EXISTS (SELECT * FROM Pedidos AS O WHERE O.IdEmpleado = E.IdEmpleado) (Selecciona el nombre de todos los empleados que han reservado al menos un pedido.) SELECT DISTINCT Pedidos.Id_Producto, Pedidos.Cantidad, (SELECT Productos.Nombre FROM Productos WHERE Productos.IdProducto = Pedidos.IdProducto) AS ElProducto FROM Pedidos WHERE Pedidos.Cantidad = 150 ORDER BY Pedidos.Id_Producto (Recupera el Código del Producto y la Cantidad pedida de la tabla pedidos, extrayendo el nombre del producto de la tabla de productos.) SELECT NumVuelo, Plazas FROM Vuelos WHERE Origen = 'Madrid' AND Exists (SELECT T1.NumVuelo FROM Vuelos AS T1 WHERE T1.PlazasLibres > 0 AND T1.NumVuelo=Vuelos.NumVuelo) (Recupera números de vuelo y capacidades de aquellos vuelos con destino Madrid y plazas libres) Supongamos ahora que tenemos una tabla con los identificadores de todos nuestros productos y el stock de cada uno de ellos. En otra tabla se encuentran todos los pedidos que tenemos pendientes de servir. Se trata de averiguar qué productos no se pueden servir por falta de stock. SELECT PedidosPendientes.Nombre FROM PedidosPendientes GROUP BY PedidosPendientes.Nombre
4-22
Lenguajes de interrogación de bases de datos
HAVING SUM(PedidosPendientes.Cantidad > (SELECT Productos.Stock FROM Productos WHERE Productos.IdProducto = PedidosPendientes.IdProducto)) Supongamos que en nuestra tabla de empleados deseamos buscar todas las mujeres cuya edad sea mayor a la de cualquier hombre: SELECT Empleados.Nombre FROM Empleados WHERE Sexo = 'M' AND Edad > ANY (SELECT Empleados.Edad FROM Empleados WHERE Sexo ='H' ) o lo que sería lo mismo: SELECT Empleados.Nombre FROM Empleados WHERE Sexo = 'M' AND Edad > (SELECT Max( Empleados.Edad )FROM Empleados WHERE Sexo ='H' )
3.4.
Unión de consultas
Se utiliza la operación UNION para crear una consulta de unión, combinando los resultados de dos o más consultas o tablas independientes. Su sintaxis es: [TABLE] consulta1 UNION [ALL] [TABLE] consulta2 [UNION [ALL] [TABLE] consultaN [ ... ]] En donde: consulta1, consulta2, consultaN son instrucciones SELECT, el nombre de una consulta almacenada o el nombre de una tabla almacenada precedido por la palabra clave TABLE. Puede combinar los resultados de dos o más consultas, tablas e instrucciones SELECT, en cualquier orden, en una única operación UNION. El ejemplo siguiente combina una tabla existente llamada Nuevas Cuentas y una instrucción SELECT: TABLE [Nuevas Cuentas] UNION ALL SELECT * FROM Clientes WHERE [Cantidad pedidos] > 1000 Si no se indica lo contrario, no se devuelven registros duplicados cuando se utiliza la operación UNION, no obstante puede incluir el predicado ALL para asegurar que se devuelven todos los registros. Esto hace que la consulta se ejecute más rápidamente. Todas las consultas en una operación UNION deben pedir el mismo número de campos, no obstante los campos no tienen por qué tener el mismo tamaño o el mismo tipo de datos. Se puede utilizar una cláusula GROUP BY y/o HAVING en cada argumento consulta para agrupar los datos devueltos. Puede utilizar una cláusula ORDER BY al final del último argumento consulta para visualizar los datos devueltos en un orden específico. SELECT [Nombre de compañía], Ciudad FROM Proveedores 4-23
Desarrollo de Sistemas
WHERE País = 'Brasil' UNION SELECT [Nombre de compañía], Ciudad FROM Clientes WHERE País = "Brasil" (Recupera los nombres y las ciudades de todos proveedores y clientes de Brasil) SELECT [Nombre de compañía], Ciudad FROM Proveedores WHERE País = 'Brasil' UNION SELECT [Nombre de compañía], Ciudad FROM Clientes WHERE País = 'Brasil' ORDER BY Ciudad (Recupera los nombres y las ciudades de todos proveedores y clientes radicados en Brasil, ordenados por el nombre de la ciudad) SELECT [Nombre de compañía], Ciudad FROM Proveedores WHERE País = 'Brasil' UNION SELECT [Nombre de compañía], Ciudad FROM Clientes WHERE País = 'Brasil' UNION SELECT [Apellidos], Ciudad FROM Empleados WHERE Región = 'América del Sur' (Recupera los nombres y las ciudades de todos los proveedores y clientes de Brasil y los apellidos y las ciudades de todos los empleados de América del Sur) TABLE [Lista de clientes] UNION TABLE [Lista de proveedores] (Recupera los nombres y códigos de todos los proveedores y clientes)
3.5.
Consultas de Combinación entre tablas
Las vinculaciones entre tablas se realiza mediante la cláusula INNER que combina registros de dos tablas siempre que haya concordancia de valores en un campo común. Su sintaxis es: Se puede utilizar una operación INNER JOIN en cualquier cláusula FROM. Esto crea una combinación por equivalencia, conocida también como unión interna. Las combinaciones equivalentes son las más comunes; éstas combinan los registros de dos tablas siempre que haya concordancia de valores en un campo común a ambas tablas. Se puede utilizar INNER JOIN con las tablas Departamentos y Empleados para seleccionar todos los empleados de cada departamento. Por el contrario, para seleccionar todos los departamentos (incluso si alguno de ellos no tiene ningún empleado asignado) se emplea LEFT OUTER JOIN (la tabla resultado incluye todas las filas de la tabla especificada a la izquierda o tabla dominante. Si los campos de la tabla no tienen correspon4-24
Lenguajes de interrogación de bases de datos
dencia en la subordinada, se completan con valores NULL) o todos los empleados (incluso si alguno no está asignado a ningún departamento), en este caso RIGHT OUTER JOIN (la tabla resultado incluye todas las filas de la tabla situada a la derecha o tabla dominante. Si los campos de la tabla no tienen correspondencia en la subordinada, se completan con valores NULL). Si se intenta combinar campos que contengan datos Memo u Objeto OLE, se produce un error. Se pueden combinar dos campos numéricos cualesquiera, incluso si son de diferente tipo de datos. El ejemplo siguiente muestra cómo podría combinar las tablas Categorías y Productos basándose en el campo IDCategoria: SELECT Nombre_Categoría, NombreProducto FROM Categorias INNER JOIN Productos ON Categorias.IDCategoria = Productos.IDCategoria En el ejemplo anterior, IDCategoria es el campo combinado, pero no está incluido en la salida de la consulta ya que no está incluido en la instrucción SELECT. Para incluir el campo combinado, incluir el nombre del campo en la instrucción SELECT, en este caso, Categorias.IDCategoria. También se pueden enlazar varias cláusulas ON en una instrucción JOIN, utilizando la sintaxis siguiente: SELECT campos FROM tabla1 INNER JOIN tabla2 ON tb1.campo1 comp tb2.campo1 AND ON tb1.campo2 comp tb2.campo2) OR ON tb1.campo3 comp tb2.campo3) También puede anidar instrucciones JOIN utilizando la siguiente sintaxis: SELECT campos FROM tb1 INNER JOIN (tb2 INNER JOIN [( ]tb3 [INNER JOIN [( ]tablax [INNER JOIN ...)] ON tb3.campo3 comp tbx.campox)] ON tb2.campo2 comp tb3.campo3) ON tb1.campo1 comp tb2.campo2 Un LEFT OUTER JOIN o un RIGHT OUTER JOIN puede anidarse dentro de un INNER JOIN, pero un INNER JOIN no puede anidarse dentro de un LEFT OUTER JOIN o un RIGHT OUTER JOIN. 4-25
Desarrollo de Sistemas
Por ejemplo: SELECT DISTINCT SUM([Precio unidad] * [Cantidad]) AS [Ventas], [Nombre] & " " & [Apellidos] AS [Nombre completo] FROM [Detalles de pedidos], Pedidos, Empleados, Pedidos INNER JOIN [Detalles de pedidos] ON Pedidos. [ID de pedido] = [Detalles de pedidos].[ID de pedido], Empleados INNER JOIN Pedidos ON Empleados.[ID de empleado] = Pedidos.[ID de empleado] GROUP BY [Nombre] & " " & [Apellidos] Crea dos combinaciones equivalentes: una entre las tablas Detalles de pedidos y Pedidos, y la otra entre las tablas Pedidos y Empleados. Esto es necesario ya que la tabla Empleados no contiene datos de ventas y la tabla Detalles de pedidos no contiene datos de los empleados. La consulta produce una lista de empleados y sus ventas totales. Si empleamos la cláusula INNER en la consulta se seleccionarán sólo aquellos registros de la tabla de la que hayamos escrito a la izquierda de INNER JOIN que contengan al menos un registro de la tabla que hayamos escrito a la derecha. Para solucionar esto tenemos dos cláusulas que sustituyen a la palabra clave INNER, estas cláusulas son LEFT OUTER y RIGHT OUTER. LEFT OUTER toma todos los registros de la tabla de la izquierda aunque no tengan ningún registro en la tabla de la izquierda. RIGHT OUTER realiza la misma operación pero al contrario, toma todos los registros de la tabla de la derecha aunque no tenga ningún registro en la tabla de la izquierda. •
Consultas de Auto combinación La auto combinación se utiliza para unir una tabla consigo misma, comparando valores de dos columnas con el mismo tipo de datos. La sintaxis en la siguiente: SELECT alias1.columna, alias2.columna, ... FROM tabla1 as alias1, tabla2 as alias2 WHERE alias1.columna = alias2.columna AND otras condiciones Por ejemplo, para visualizar el número, nombre y puesto de cada empleado, junto con el número, nombre y puesto del supervisor de cada uno de ellos se utilizaría la siguiente sentencia: SELECT t.num_emp, t.nombre, t.puesto, t.num_sup,s.nombre, s.puesto FROM empleados AS t, empleados AS s WHERE t.num_sup = s.num_emp
•
Consultas de Combinaciones no Comunes La auto combinación se utiliza para unir una tabla consigo misma, comparando La mayoría de las combinaciones están basadas en la igualdad de valores de las columnas que son el criterio de la combinación. Las no comunes se basan en otros operadores de combinación, tales como NOT, BETWEEN, <>, etc.
4-26
Lenguajes de interrogación de bases de datos
La auto combinación se utiliza para unir una tabla consigo misma, comparando, por ejemplo, para listar el grado salarial, nombre, salario y puesto de cada empleado ordenando el resultado por grado y salario habría que ejecutar la siguiente sentencia: SELECT grados.grado,empleados.nombre, empleados.salario, empleados.puesto FROM empleados, grados WHERE empleados.salario BETWEEN grados.salarioinferior AND grados.salariosuperior ORDER BY grados.grado, empleados.salario Para listar el salario medio dentro de cada grado salarial habría que lanzar esta otra sentencia: SELECT grados.grado, AVG(empleados.salario) FROM empleados, grados WHERE empleados.salario BETWEEN grados.salarioinferior AND grados.salariosuperior GROUP BY grados.grado •
SELF JOIN SELF JOIN es una técnica empleada para conseguir el producto cartesiano de una tabla consigo misma. Su utilización no es muy frecuente, pero pongamos algún ejemplo de su utilización: Supongamos la siguiente tabla (el campo autor es numérico, aunque para ilustrar el ejemplo utilice el nombre): CÓDIGO (CÓDIGO
DEL LIBRO)
AUTOR (NOMBRE
B0012
1. Francisco López
B0012
2. Javier Alonso
B0012
3. Marta Rebolledo
C0014
1. Francisco López
C0014
2. Javier Alonso
D0120
2. Javier Alonso
D0120
3. Marta Rebolledo
DEL
AUTOR)
4-27
Desarrollo de Sistemas
Queremos obtener, para cada libro, parejas de autores: SELECT A.Codigo, A.Autor, B.Autor FROM Autores A, Autores B WHERE A.Codigo = B.Codigo El resultado es el siguiente: AUTOR
AUTOR
1. Francisco López
1. Francisco López
1. Francisco López
2. Javier Alonso
1. Francisco López
3. Marta Rebolledo
2. Javier Alonso
2. Javier Alonso
2. Javier Alonso
1. Francisco López
2. Javier Alonso
3. Marta Rebolledo
3. Marta Rebolledo
3. Marta Rebolledo
3. Marta Rebolledo
2. Javier Alonso
3. Marta Rebolledo
1. Francisco López
1. Francisco López
1. Francisco López
1. Francisco López
2. Javier Alonso
2. Javier Alonso
2. Javier Alonso
2. Javier Alonso
1. Francisco López
2. Javier Alonso
2. Javier Alonso
2. Javier Alonso
3. Marta Rebolledo
3. Marta Rebolledo
3. Marta Rebolledo
3. Marta Rebolledo
2. Javier Alonso
Como podemos observar, las parejas de autores se repiten en cada uno de los libros, podemos omitir estas repeticiones de la siguiente forma: SELECT A.Codigo, A.Autor, B.Autor FROM Autores A, Autores B WHERE A.Codigo = B.Codigo AND A.Autor < B.Autor
4-28
Lenguajes de interrogación de bases de datos
El resultado ahora es el siguiente: AUTOR
AUTOR
1. Francisco López
2. Javier Alonso
1. Francisco López
3. Marta Rebolledo
1. Francisco López
2. Javier Alonso
2. Javier Alonso
3. Marta Rebolledo
Ahora tenemos un conjunto de resultados en formato Autor-CoAutor. Si en la tabla de empleados quisiéramos extraer todas las posibles parejas que podemos realizar, utilizaríamos la siguiente sentencia: SELECT Hombres.Nombre, Mujeres.Nombre FROM Empleados Hombre, Empleados Mujeres WHERE Hombre.Sexo = 'Hombre' AND Mujeres.Sexo = 'Mujer' AND Hombres.Id <>Mujeres.Id Para concluir supongamos la tabla siguiente: ID
NOMBRE
SUBJEFE
1
Marcos
6
2
Lucas
1
3
Ana
2
4
Eva
1
5
Juan
6
6
Antonio
Queremos obtener un conjunto de resultados con el nombre del empleado y el nombre de su jefe: SELECT Emple.Nombre, Jefes.Nombre FROM Empleados Emple, Empleados Jefe WHERE Emple.SuJefe = Jefes.Id
4.
Funciones
Una función representa un valor único que se obtiene aplicando unas determinadas operaciones a otros valores (argumentos). Hay dos tipos de funciones: 4-29
Desarrollo de Sistemas
4.1.
Funciones colectivas Dan como resultado un único valor después de aplicar la función a un grupo de valores. SELECT AVG(sueldo), SUM (sueldo) MAX (sueldo) MIN (sueldo) COUNT (sueldo) FROM (empleados).
Da como resultado un único registro con cinco columnas. La primera columna dará como resultado la media aritmética de los sueldos de los empleados, la segunda el total del sueldo de todos los empleados, la tercera el sueldo máximo, la cuarta el sueldo mínimo y la quinta el total de registros que tiene la tabla.
4.2.
Funciones escalares Operan sobre un único dato y devuelven un único valor como resultado. FUNCIÓN
•
DESCRIPCIÓN
DATE
Obtiene la fecha y hora actual del sistema.
DAY (fecha)
Obtiene el día de una fecha como un valor entero.
MONTH(fecha)
Obtiene el mes de una fecha como un valor entero.
YEAR(fecha)
Obtiene el año de una fecha como un valor entero.
Funciones matemáticas FUNCIÓN
4-30
DESCRIPCIÓN
ABS(x)
Valor absoluto. Convierte números negativos en positivos, o deja sólo números positivos.
ACOS(x)
Obtiene el arcoseno.
ATAN(x)
Obtiene la arcotangente.
CEIL(x)
Obtiene el menor entero, mayor o igual que x. Redondeo hacia arriba.
COS(x)
Obtiene el coseno trigonométrico.
COT(x)
Obtiene la cotangente trigonométrica.
EXP(x)
Obtiene el valor del exponente.
FLOOR(x)
Obtiene el mayor entero, menor o igual que x. Redondeo hacia abajo.
INT(x)
Devuelve la parte entera.
Lenguajes de interrogación de bases de datos
FUNCIÓN
•
DESCRIPCIÓN
LOG(x)
Obtiene el logaritmo natural.
LOG10(x)
Obtiene el logaritmo base 10.
MOD(x,y)
Obtiene el resto de dividir x entre y.
PI
Obtiene el valor de la constante pi.
POWER(x,y)
Obtiene el valor de x elevado a y.
ROUND(x,y)
Redondea x a y lugares decimales. Si se omite y, x se redondea al entero más próximo.
SIGN(x)
Devuelve +1 si la x es positivo, 0 si es cero y -1 si es negativo.
SIN(x)
Obtiene el seno trigonométrico.
SQUARE(x)
Obtiene el cuadrado.
SQRT(x)
Obtiene la raíz cuadrada.
TAN(x)
Obtiene la tangente.
Funciones de cadena FUNCIÓN
DESCRIPCIÓN
ASCII(x)
Obtiene el código ASCII de x.
CHAR(x)
Obtiene el carácter ASCII cuyo código entero corresponde a x.
LEFT(x,y)
Obtiene los y caracteres de la izquierda de x.
LEN(x)
Obtiene el número de caracteres de x.
LOWER(x)
Obtiene x con todos sus caracteres convertidos a minúsculas.
LTRIM(x)
Obtiene x quitándoles los espacios iniciales.
REPLACE(x,y,z)
Encuentra todas las apariciones de y en x, reemplazándolas por z.
RIGHT(x,y)
Obtiene los y caracteres de la derecha de x.
RTRIM(x)
Obtiene x sin espacios a la derecha.
SPACE(x)
Obtiene x espacios.
SUBSTRING(x,y,z)
Obtiene z caracteres de x, comenzando en la posición y.
UPPER(x)
Obtiene x con todos sus caracteres convertidos a mayúsculas.
4-31
Desarrollo de Sistemas
•
Funciones de conversación FUNCIÓN
5.
DESCRIPCIÓN
CTOD(x)
Convierte una cadena de caracteres a una fecha. Se puede utilizar un segundo parámetro para especificar el formato de la fecha devuelta: 0 (por defecto devuelve MM/DD/YY, 1 devuelve DD/MM/YY y 2 devuelve YY/MM/DD.
CAST(x,y)
Convierte la cadena y, pasada como argumento, en el tipo especificado x (si es posible).
DTOC(x)
Convierte una fecha x, a una cadena de caracteres. Un segundo parámetro opcional determina el formato del resultado: 0 (por defecto) devuelve MM/DD/YY,1 devuelve DD/MM/YY, 2 devuelve YY/MM/DD, 10 devuelve MM/DD/YYYY, 11 devuelve DD/MM/YYYY, 12 devuelve YYYY/MM/DD. Puede existir un tercer parámetro opcional para determinar el carácter que se quiere utilizar como separador. Si no se especifica se toma el (/).
STR(x,y)
Convierte un número x en una cadena. Devuelve y posiciones (incluyendo el punto decimal). Opcionalmente se puede incluir un tercer parámetro para indicar el número de dígitos a la derecha del punto decimal.
STRVAL(x)
Convierte un valor de cualquier tipo a una cadena de caracteres.
Mantenimiento de los datos. DML
El Lenguaje de Manipulación de Datos (DML) se compone de las instrucciones para crear y recuperar datos. Son sentencias que no devuelven ningún registro. Son las encargadas de mantener actualizados los datos que están almacenados en las tablas. •
DELETE La sentencia DELETE se utiliza para borrar registros de una tabla de la base de datos. No es posible eliminar el contenido de algún campo en concreto. Su sintaxis es: DELETE FROM Tabla [WHERE { condición }] La cláusula WHERE sigue el mismo formato que la vista en la sentencia SELECT y determina qué registros se borrarán. Cada sentencia DELETE borra los registros que cumplen la condición impuesta o todos si no se indica cláusula WHERE. DELETE FROM Empleados
4-32
Lenguajes de interrogación de bases de datos
Con el ejemplo anterior se borrarían todos los registros de la tabla Empleados. Se llama vaciado y sólo quedaría la estructura de la tabla. DELETE FROM Empleados WHERE Cargo = 'Vendedor' •
INSERT La sentencia INSERT se utiliza para añadir registros a las tablas de la base de datos. Puede ser de dos tipos: Insertar un único registro o insertar en una tabla los registros contenidos en otra tabla. Para insertar un único registro, la sintaxis es la siguiente: INSERT INTO Tabla (campo1, campo2, ..., campoN) VALUES (valor1, valor2, ..., valorN) Esta sentencia graba en el campo1 el valor1, en el campo2 y valor2 y así sucesivamente. Para insertar registros de otra tabla, la sintaxis es la siguiente: INSERT INTO Tabla (campo1, campo2, , campoN) SELECT TablaOrigen.campo1, TablaOrigen.campo2,,TablaOrigen.campoN FROM TablaOrigen En este caso se seleccionarán los campos 1,2,..., n de la TablaOrigen y se grabarán en los campos 1,2,.., n de la Tabla. La condición SELECT puede incluir la cláusula WHERE para filtrar los registros a copiar. Si Tabla y TablaOrigen poseen la misma estructura podemos simplificar la sintaxis a: INSERT INTO Tabla SELECT TablaOrigen.* FROM TablaOrigen De esta forma los campos de TablaOrigen se grabarán en Tabla, para realizar esta operación es necesario que todos los campos de TablaOrigen estén contenidos con igual nombre en Tabla. Con otras palabras que Tabla posea todos los campos de TablaOrigen (igual nombre e igual tipo). En este tipo de sentencia hay que tener especial atención con los campos contadores o autonuméricos puesto que al insertar un valor en un campo de este tipo se escribe el valor que contenga su campo homólogo en la tabla origen, no incrementándose como le corresponde. Si la tabla destino contiene una clave principal, hay que asegurarse que es única, y con valores no nulos; si no es así, no se agregarán los registros. INSERT INTO Empleados (Nombre, Apellido, Cargo) VALUES ('Luis', 'Sánchez', 'Becario')
4-33
Desarrollo de Sistemas
INSERT INTO Empleados SELECT * FROM Vendedores WHERE Provincia = 'Madrid' INSERT INTO Oils (OilName, Latiname, Simple) VALUES(‘Super’, NULL, NULL) INSERT INTO MyOils (OilName, LatinName) SELECT OilName, LatinName FROM Oils WHERE (LEFT(OilName), 2) = ‘98’) •
UPDATE La sentencia UPDATE se utiliza para cambiar el contenido de los registros de una tabla de la base de datos. Su sintaxis es: UPDATE Tabla SET Campo1=Valor1, Campo2=Valor2, ... CampoN=ValorN [WHERE { condición }] UPDATE es especialmente útil cuando se desea cambiar un gran número de registros o cuando éstos se encuentran en múltiples tablas. Puede cambiar varios campos a la vez. La cláusula WHERE sigue el mismo formato que la vista en la sentencia SELECT y determina qué registros se modificarán. El ejemplo siguiente incrementa los valores Cantidad pedidos en un 10% y los valores Transporte en un 3% para aquellos que se hayan enviado al Reino Unido: UPDATE Pedidos SET Pedido = Pedidos * 1.1, Transporte = Transporte * 1.03 WHERE PaisEnvío = 'ES' UPDATE Empleados SET Grado = 5 WHERE Grado = 2 UPDATE Productos SET Precio = Precio * 1.1 WHERE Proveedor = 8 AND Familia = 3 Si en una consulta de actualización suprimimos la cláusula WHERE todos los registros de la tabla señalada serán actualizados. UPDATE Empleados SET Salario = Salario * 1.1 UPDATE Libros SET Precio = (SELECT AVG(Precio) FROM Libros WHERE Precio IS NOT NULL) WHERE Precio IS NULL
4-34
Lenguajes de interrogación de bases de datos
Con esta última sentencia se ha puesto precio a todos los libros que no lo tenían. Ese precio ha sido el resultante de calcular la media entre los libros que sí lo tenían. •
SELECT...INTO Esta sentencia se utiliza para seleccionar registros e insertarlos en una tabla nueva. Su sintaxis es: SELECT campo1, campo2, ..., campoN INTO NuevaTabla FROM TablaOrigen [WHERE { condición }] Las columnas de la nueva tabla tendrán el mismo tipo y tamaño que las columnas origen, y se llamarán con el nombre de alias de la columna origen o en su defecto con el nombre de la columna origen, pero no se transfiere ninguna otra propiedad del campo o de la tabla como por ejemplo las claves e índices. La sentencia SELECT puede ser cualquier sentencia SELECT sin ninguna restricción, puede ser una consulta multitabla, una consulta de resumen, una UNION ... SELECT * INTO Programadores FROM Empleados WHERE Categoria = 'Programador' Esta consulta crea una tabla nueva llamada Programadores con igual estructura que la tabla Empleados y copia aquellos registros cuyo campo Categoria sea “Programador”. Por ejemplo: Queremos enviarle a un representante una tabla con todos los datos personales de sus clientes para que les pueda enviar cartas etc... SELECT Numclie AS Codigo, Nombre, Direccion, Telefono INTO Susclientes FROM Clientes WHERE Repclie = '103' En el ejemplo anterior la nueva tabla tendrá cuatro columnas llamadas Codigo, Nombre, Direccion, Telefono y contendrá las filas correspondientes a los clientes del representante 103.
•
MERGE Se utiliza para seleccionar filas de una o más fuentes para la actualización o inserción en una tabla o vista. Puede especificar las condiciones para determinar si se debe actualizar o insertar en la tabla de destino o vista. MERGE es una afirmación determinista. No se puede actualizar la misma fila de la tabla de destino varias veces en la misma instrucción MERGE. Esta sentencia se utiliza para seleccionar registros e insertarlos en una tabla nueva. Su sintaxis es:
4-35
Desarrollo de Sistemas
MERGE INTO tabla_destino USING tabla_origen ON (condición) WHEN MATCHED THEN UPDATE SET campo1 = valor1, campoN = valorN WHEN NOT MATCHED THEN INSERT (campo1, ...., campoN) VALUES (valor1, ..., valorN); Veamos un ejemplo: MERGE INTO clientes cli USING datos_cli dac ON (cli.cliente_id = dac.cliente_id) WHEN MATCHED THEN UPDATE SET cli.nombre = dac.nombre, cli.direccion = dac.direccion WHEN NOT MATCHED THEN INSERT (cliente_id, nombre, direccion) VALUES (dac.cliente_id, dac.nombre, dac.direccion); En este ejemplo cuando encontremos un cliente que exista en ambas tablas mediante su "id" actualizará su nombre y dirección. En caso de que no exista alguno de los clientes que exista en la tabla "datos_cli" que no exista en la tabla "clientes" los insertará.
6.
Definición de los datos. DDL
El Lenguaje de Definición de Datos (DDL) consta de las sentencias utilizadas para crear los objetos dentro de la base de datos y cambiar las propiedades y atributos de la propia base de datos.
4-36
Lenguajes de interrogación de bases de datos
COMANDO
6.1.
DESCRIPCIÓN
CREATE
Utilizado para crear objetos de base de datos.
DROP
Utilizado para eliminar un objeto de la base de datos.
ALTER
Utilizado para modificar o alterar un objeto de la base de datos.
TRUNCATE
Elimina el contenido de una tabla, la vacía pero no modifica su estructura.
Crear objetos
Los objetos de base de datos se crean utilizando la sentencia CREATE. Su sintaxis exacta varía para cada objeto. SINTAXIS
•
OBJETO
CREADO
CREATE DATABASE nombre
Crea una base de datos.
CREATE DEFAULT nombre AS expresión
Crea una propiedad determinada.
CREATE FUNCTION nombre RETURNS valor AS sentencias
Crea una función definida por el usuario.
CREATE INDEX nombre ON tabla (columnas)
Crea un índice sobre una tabla.
CREATE PROCEDURE nombre AS sentencias
Crea un procedimiento almacenado.
CREATE RULE nombre AS expresión
Crea una regla de base de datos.
CREATE TABLE nombre (definición)
Crea una tabla.
CREATE TRIGGER nombre {FOR | AFTER | INSTEAD OF} acción AS sentencias
Crea un desencadenador.
CREATE VIEW nombre AS sentencia_select
Crea una vista.
Crear una tabla De las sentencias CREATE descritas, la más compleja es la sentencia CREATE TABLE, a causa del número de elementos diferentes que comprenden una definición de tabla. Debe añadir columnas, por supuesto, y cada definición de columna debe tener al menos un nombre y un tipo de datos. Opcionalmente puede especificar si la columna admite nulos, su valor por defecto, cualquier restricción aplicable a la columna...etc. Su sintaxis simplificada es:
4-37
Desarrollo de Sistemas
CREATE TABLE Tabla (campo1 tipo (tamaño) índice1 , campo2 tipo (tamaño) índice2 , ..., índice multicampo , ... ) En donde: PARTE
DESCRIPCIÓN
Tabla
Es el nombre de la tabla que se va a crear.
campo1 campo2
Es el nombre del campo o de los campos que se van a crear en la nueva tabla. La nueva tabla debe contener, al menos, un campo.
Tipo
Es el tipo de datos de campo en la nueva tabla. (Ver Tipos de Datos)
Tamaño
Es el tamaño del campo y sólo se aplica para campos de tipo texto.
índice1 índice2
Es una cláusula CONSTRAINT que define el tipo de índice a crear. Esta cláusula es opcional.
índice multicampos
Es una cláusula CONSTRAINT que define el tipo de índice multicampos a crear. Un índice multicampo es aquel que está indexado por el contenido de varios campos. Esta cláusula es opcional.
CREATE TABLE Empleados (Nombre TEXT (25) , Apellidos TEXT (50)) Crea una nueva tabla llamada Empleados con dos campos, uno llamado Nombre de tipo texto y longitud 25 y otro llamado apellidos con longitud 50. CREATE TABLE Empleados (Nombre TEXT (10), Apellidos TEXT, Fecha_Nacimiento DATETIME) CONSTRAINT IndiceGeneral UNIQUE ([Nombre], [Apellidos], [Fecha_Nacimiento]) Crea una nueva tabla llamada Empleados con un campo Nombre de tipo texto y longitud 10, otro con llamado Apellidos de tipo texto y longitud predeterminada y uno más llamado Fecha_Nacimiento de tipo Fecha/Hora. También crea un índice único (no permite valores repetidos) formado por los tres campos. CREATE TABLE Empleados (ID INT CONSTRAINT IndicePrimario PRIMARY, Nombre TEXT, Apellidos TEXT, Fecha_Nacimiento DATETIME) Crea una tabla llamada Empleados con un campo Texto de longitud predeterminada llamado Nombre y otro igual llamado Apellidos, crea otro campo llamado Fecha_Nacimiento de tipo Fecha/Hora y el campo ID de tipo entero el que establece como clave principal.
4-38
Lenguajes de interrogación de bases de datos
•
La cláusula CONSTRAINT Se utiliza la cláusula CONSTRAINT en las instrucciones ALTER TABLE y CREATE TABLE para crear o eliminar índices. Existen dos sintaxis para esta cláusula dependiendo de si desea crear o eliminar un índice de un único campo o si se trata de un campo multiíndice. Para los índices de campos únicos: CONSTRAINT nombre {PRIMARY KEY | UNIQUE | REFERENCES tabla externa [(campo externo1, campo externo2)]} Para los índices de campos múltiples: CONSTRAINT nombre {PRIMARY KEY (primario1[, primario2 [, ...]]) | UNIQUE (único1[, único2 [, ...]]) | FOREIGN KEY (ref1[, ref2 [, ...]]) REFERENCES tabla externa [(campo externo1 [,campo externo2 [, ...]])]} PARTE
DESCRIPCIÓN
nombre
Es el nombre del índice que se va a crear.
primarioN
Es el nombre del campo o de los campos que forman el índice primario.
únicoN
Es el nombre del campo o de los campos que forman el índice de clave única.
refN
Es el nombre del campo o de los campos que forman el índice externo (hacen referencia a campos de otra tabla).
tabla externa
Es el nombre de la tabla que contiene el campo o los campos referenciados en refN
campos externosN
Es el nombre del campo o de los campos de la tabla externa especificados por ref1, ref2, ..., refN
4-39
Desarrollo de Sistemas
Si se desea crear un índice para un campo cuando se esta utilizando las instrucciones ALTER TABLE o CREATE TABLE la cláusula CONSTRAINT debe aparecer inmediatamente después de la especificación del campo indexado. Si se desea crear un índice con múltiples campos cuando se está utilizando las instrucciones ALTER TABLE o CREATE TABLE la cláusula CONSTRAINT debe aparecer fuera de la cláusula de creación de tabla. TIPO
DESCRIPCIÓN
DE ÍNDICE
UNIQUE
Genera un índice de clave única. Lo que implica que los registros de la tabla no pueden contener el mismo valor en los campos indexados.
PRIMARY KEY
Genera un índice primario el campo o los campos especificados. Todos los campos de la clave principal deben ser únicos y no nulos, y cada tabla sólo puede contener una única clave principal.
FOREIGN KEY
Genera un índice externo (toma como valor del índice campos contenidos en otras tablas). Si la clave principal de la tabla externa consta de más de un campo, se debe utilizar una definición de índice de múltiples campos, listando todos los campos de referencia, el nombre de la tabla externa, y los nombres de los campos referenciados en la tabla externa en el mismo orden que los campos de referencia listados. Si los campos referenciados son la clave principal de la tabla externa, no tiene que especificar los campos referenciados, el motor se comporta como si la clave principal de la tabla externa fueran los campos referenciados.
CREATE TABLE Empleados (DNI CHAR(10), nombemp CHAR(15), Apellemp CHAR(40), sueldo DECIMAL, CONSTRAINT pk_dni PRIMARY KEY (DNI)) Donde pk_dni es el nombre de la restricción (nombre de uso interno para el gestor) y DNI es el campo a vincular (índice primario). CREATE TABLE Ventas (numventa INT IDENTITY (100,1), DNI CHAR(10), Fecha DATETIME, codart CHAR(5), CONSTRAINT pk_dni FOREIGN KEY (DNI)) Esta orden crea una tabla Ventas en la cual se otorga al campo DNI el atributo de clave externa (índice externo). El campo numventa es autonumérico, empieza a contar desde 100 con un incremento de 1. •
Crear una vista Es una sentencia SELECT a la que se otorga un nombre y se guarda en el catálogo. El resultado es una tabla y pueden realizarse operaciones sobre ella, pero no es propiamente una tabla, ni tiene datos propios. Se pueden utilizar sobre ella los mandatos SELECT habituales. Sirve para preservar los datos de la tabla “verdadera”.
4-40
Lenguajes de interrogación de bases de datos
CREATE VIEW suspensos AS SELECT * FROM alumnos WHERE nota < 5 Crea una vista con los suspensos de la tabla alumnos. CREATE VIEW VistaSimple AS SELECT IDRelacionada, DescripcionSimple, DescripcionRelacionada FROM TablaRelacionada INNER JOIN TablaSimple ON TablaRelacionada.IDSimple = TablaSimple.IDSimple •
Crear un índice Los índices son unos atributos de ordenación interna para campos de tipo fecha, numéricos y alfanuméricos (no memo). Las búsquedas basadas en campos indexados se realizan de una forma más rápida, entre otras ventajas. No es bueno que existan muchos campos indexados en una misma tabla, pues debido a la longitud que tomaría el registro interno, puede incluso hacer que una búsqueda sea más tediosa o lenta, justo el efecto contrario a lo que se pretende. Existen dos tipos de índices: índice único, para campos que no permiten valores iguales en una misma columna, también llamado sin duplicados (UNIQUE) y los llamados índices con duplicados. Su sintaxis es: CREATE [ UNIQUE ] INDEX índice ON tabla (campo [ASC | DESC][, campo [ASC | DESC], ...]) [WITH { PRIMARY | DISALLOW NULL | IGNORE NULL }]
En donde: PARTE
DESCRIPCIÓN
TABLA
Es el nombre del índice a crear. Es el nombre de una tabla existente en la que se creará el índice.
CAMPO
Es el nombre del campo o lista de campos que constituyen el índice.
ASC | DESC
Indica el orden de los valores de los campos. ASC indica un orden ascendente (valor predeterminado) y DESC un orden descendente.
UNIQUE
Indica que el índice no puede contener valores duplicados.
DISALLOW NULL
Prohíbe valores nulos en el índice.
IGNORE NULL
Excluye del índice los valores nulos incluidos en los campos que lo componen.
PRIMARY
Asigna al índice la categoría de clave principal, en cada tabla sólo puede existir un único índice que sea "Clave Principal". Si un índice es clave principal implica que no puede contener valores nulos ni duplicados.
ÍNDICE
4-41
Desarrollo de Sistemas
CREATE INDEX MiIndice ON Empleados (Prefijo, Telefono) Crea un índice llamado MiIndice en la tabla Empleados con los campos Prefijo y Telefono. CREATE UNIQUE INDEX MiIndice ON Empleados (ID) WITH DISALLOW NULL Crea un índice en la tabla Empleados utilizando el campo ID, obligando que el campo ID no contenga valores nulos ni repetidos.
6.2.
Modificar objetos
Del mismo modo que la sentencia CREATE crea un nuevo objeto, la sentencia ALTER proporciona el mecanismo para alterar una definición de objeto. No todos los objetos creados por una instrucción CREATE tienen su correspondiente sentencia ALTER. Su sintaxis exacta varía para cada objeto. SINTAXIS
OBJETO
• CREADO
ALTER DATABASE nombre especif_archivo
Modifica los archivos utilizados para almacenar la base de datos.
ALTER FUNCTION nombre RETURNS valor AS sentencias
Cambia las sentencias SQL que componen la función.
ALTER PROCEDURE nombre AS sentencias
Cambia las sentencias SQL que componen el procedimiento almacenado.
ALTER TABLE nombre (definición)
Cambia la definición de una tabla.
ALTER TRIGGER nombre {FOR | AFTER | INSTEAD OF} acción AS sentencias
Cambia las sentencias SQL que componen el desencadenador.
ALTER VIEW nombre AS sentencia_select
Cambia la sentencia SELECT que crea la vista.
Modificar una tabla La sentencia ALTER TABLE es compleja por la misma razón que la sentencia CREATE TABLE: hay varias partes diferentes en una definición de tabla. Su sintaxis simplificada es: ALTER TABLE Tabla {ADD | ALTER {COLUMN tipo de campo[(tamaño)] [CONSTRAINT índice] CONSTRAINT índice multicampo} | DROP {COLUMN campo [CONSTRAINT nombre del índice]} } 4-42
Lenguajes de interrogación de bases de datos
En donde: PARTE
DESCRIPCIÓN
tabla
Es el nombre de la tabla que se desea modificar.
campo
Es el nombre del campo que se va a añadir o eliminar.
tipo
Es el tipo de campo que se va a añadir.
tamaño
El tamaño del campo que se va a añadir (sólo para campos de texto).
índice
Es el nombre del índice del campo (cuando se crean campos) o el nombre del índice de la tabla que se desea eliminar.
índice multicampo
Es el nombre del índice del campo multicampo (cuando se crean campos) o el nombre del índice de la tabla que se desea eliminar.
OPERACIÓN
DESCRIPCIÓN
ADD COLUMN
Se utiliza para añadir un nuevo campo a la tabla, indicando el nombre, el tipo de campo y opcionalmente el tamaño (para campos de tipo texto).
ADD
Se utiliza para agregar un índice de multicampos o de un único campo.
DROP COLUMN
Se utiliza para borrar un campo. Se especifica únicamente el nombre del campo.
DROP
Se utiliza para eliminar un índice. Se especifica únicamente el nombre del índice a continuación de la palabra reservada CONSTRAINT.
ALTER TABLE Empleados ADD COLUMN Salario DECIMAL Agrega un campo Salario de tipo Numérico a la tabla Empleados. ALTER TABLE Empleados DROP COLUMN Salario. Elimina el campo Salario de la tabla Empleados. ALTER TABLE Pedidos ADD CONSTRAINT RelacionPedidos FOREIGN KEY (ID_Empleado) REFERENCES Empleados (ID_Empleado) Agrega un índice externo a la tabla Pedidos. El índice externo se basa en el campo ID_Empleado y se refiere al campo ID_Empleado de la tabla Empleados. En este ejemplo no es necesario indicar el campo 4-43
Desarrollo de Sistemas
junto al nombre de la tabla en la cláusula REFERENCES, pues ID_Empleado es la clave principal de la tabla Empleados. ALTER TABLE Pedidos DROP CONSTRAINT RelacionPedidos Elimina el índice de la tabla Pedidos. ALTER TABLE TablaSimple ADD COLUMN ColumnaNueva VARCHAR(20) Agrega un campo ColumnaNueva de tipo carácter variable a la tabla TablaSimple. ALTER TABLE TablaSimple ALTER COLUMN ColumnaNueva VARCHAR(10) Modifica la longitud del campo ColumnaNueva de la tabla TablaSimple. ALTER TABLE TablaSimple DROP COLUMN ColumnaNueva Elimina el campo ColumnaNueva de la tabla TablaSimple.
6.3.
Eliminar objetos
La sentencia DROP elimina un objeto de la base de datos. Al contrario que las sentencias CREATE y ALTER, todas las sentencias DROP tienen la misma sintaxis: DROP tipo_objeto nombre Donde tipo_objeto puede ser cualquier objeto de la base de datos. DROP DATABASE Alumnos DROP TABLE Productos DROP INDEX MiIndice DROP VIEW Suspensos La sentencia TRUNCATE elimina el contenido de una tabla dejándola vacía de filas sin modificar su estructura. La sintaxis es la siguiente: TRUNCATE tabla;
7. 7.1.
Conceptos de interés Variables
Las variables se identifican por el prefijo @; por ejemplo, @miVariable. Tienen dos niveles de ámbito: local y global, identificando las variables globales con una doble@: @@VERSION.
4-44
Lenguajes de interrogación de bases de datos
•
Variables locales Las variables locales se crean utilizando la sentencia DECLARE, con la siguiente sintaxis: DECLARE @variable_local tipo_datos Se pueden crear varias variables locales con una sola instrucción DECLARE separándolas con comas: DECLARE @var1 INT, @var2 INT Cuando se crea una variable local, inicialmente tiene el valor NULL. Puede asignar un valor a una variable de las siguientes formas: —
Utilizando el comando SET con una constante o expresión: SET @miVariableChar = ‘Hola, mundo’
—
Utilizando el comando SELECT con una constante o expresión: SELECT @miVariableChar = ‘Hola, mundo’
—
Utilizando el comando SELECT con otra sentencia SELECT: SELECT @miVariableChar = MAX(OilName) FROM Oils
Observe que en la tercera forma (el SELECT con otro SELECT), el operador de asignación (=) reemplaza a la segunda palabra reservada SELECT; no se repite. •
Variables globales Las variables globales, identificadas con un doble signo @ (@@VERSION) las proporciona SQL y el usuario no puede crearlas. Existen docenas de variables globales. La mayoría de ellas proporcionan información sobre el estado actual de SQL.
•
Utilizar variables Las variables pueden utilizarse en expresiones a lo largo y ancho del lenguaje SQL. En cualquier caso, no pueden utilizarse en lugar de un nombre de objeto o palabra reservada. Así las siguientes sentencias son correctas: DECLARE @elAceite CHAR(20) SET @elAceite = ‘Basil’ -- Se ejecutará este comando SELECT OilName, Descripction FROM Oils WHERE OilName = @elAceite
4-45
Desarrollo de Sistemas
Sin embargo, las siguientes sentencias SELECT provocarán errores: DECLARE @elComando CHAR(10), @elCampo CHAR(10) SET @elComando = ‘SELECT’ SET @elCampo = ‘OilName’ -- Este comando fallará @elComando * from Oils -- Igual que éste SELECT @elCampo FROM Oils
7.2.
Control de ejecución
A menos que especifique lo contrario, SQL procesa las sentencias desde el comienzo del script hasta el final, pasando por todas ellas. Esto no es siempre lo que usted necesita. Algunas veces querrá que se ejecute una instrucción únicamente si son verdaderas ciertas condiciones. Otras querrá que una instrucción se ejecute un número de veces, o se repita hasta que se cumpla alguna condición. Los comandos de flujo de SQL le proporcionan la posibilidad de controlar la ejecución de esta forma. Cuando comience a manipular el modo en que SQL ejecuta las instrucciones, es conveniente tratar un conjunto de sentencias en bloque. SQL se lo permite mediante la pareja de comando BEGIN...END. Escribir el comando BEGIN tras cualquier comando de control de flujo provoca que SQL aplique el comando a todas las sentencias entre el BEGIN y su correspondiente END. •
PROCESAMIENTO CONDICIONAL IF...ELSE La sentencia IF es la más sencilla entre los comandos de fluido condicionales. Si la expresión lógica que sigue al comando IF se evalúa a TRUE, se ejecutarán la sentencia o bloque de sentencias que lo siguen. Si la expresión lógica se evalúa a FALSE, se salta la sentencia o bloque de sentencias que los sigan. El comando opcional ELSE le permite especificar una sentencia o grupo de sentencias a ejecutar sólo si la expresión lógica se evalúa a FALSE. DECLARE @primeraLetra CHAR(2) SELECT @primeraLetra = LEFT(MIN(OilName), 1) FROM Oils
4-46
Lenguajes de interrogación de bases de datos
IF @primeraLetra = ‘A’ PRINT ‘Es una A’ ELSE PRINT ‘No es una A’ CASE En la mayoría de los lenguajes de programación, CASE es una forma sofisticada de la sentencia IF que le permite especificar múltiples expresiones lógicas en una única sentencia. En SQL, CASE es una función, no un comando. No se utiliza por sí mismo como IF; en su lugar, se utiliza como parte de una sentencia SELECT o UPDATE. Las sentencias que incluyen CASE pueden hacerlo en una de sus dos formas sintácticas, dependiendo de si la expresión a evaluar cambia. La forma más simple asume que la expresión lógica a evaluar siempre tiene la siguiente forma: Valor = expresión El valor puede ser tan complejo como quiera. Puede utilizar una constante, un nombre de columna o una expresión compleja, o cualquier cosa que necesite. El operador de comparación siempre es la igualdad. La sintaxis simple de CASE es: CASE valor WHEN expresión_uno THEN resultado_expresión_uno WHEN expresión_dos THEN resultado_expresión_dos ... WHEN expresión_n THEN resultado_expresión_n [ELSE resultado_expresión_else] END En esta forma del CASE, se obtiene solo el resultado_expresión si la expresión que sigue a la palabra clave WHEN es lógicamente igual al valor especificado. Puede tener cualquier número de cláusulas WHEN en la expresión. La cláusula ELSE es opcional y actúa como un resultado “comodín” (se ejecuta sólo si todas las cláusulas WHEN se evalúan a FALSE). Compara un valor contra varios valores diferentes es extremadamente común, pero en algunas ocasiones necesitará más flexibilidad. En este caso, puede utilizar la llamada sintaxis CASE de búsqueda, con esta forma: 4-47
Desarrollo de Sistemas
CASE WHEN expresión_lógica_uno THEN resultado_expresión_uno WHEN expresión_ lógica_dos THEN resultado_expresión_dos ... WHEN expresión_ lógica_n THEN resultado_expresión_n [ELSE resultado_expresión_else] END Utilizar un CASE simple: SELECT OilName, CASE PlantPartID WHEN 1 THEN ‘Uno’ WHEN 2 THEN ‘Dos’ WHEN 3 THEN ‘Tres’ WHEN 7 THEN ‘Siete’ WHEN 8 THEN ‘Ocho’ END AS Categoria FROM Oils ORDER BY Categoria Utilizar un CASE de búsqueda: SELECT TOP 10 OilName, LatinName CASE WHEN LEFT(OilName,1) = ‘B’ THEN ‘Nombre B’ WHEN LEFT(LatinName,1) = ‘C’ THEN ‘Nombre Latino C’ ELSE ‘Ninguno de los dos’ FROM Oils ORDER BY OilName •
BUCLES El último comando de control de flujo le permite hacer que una sentencia o bloque de sentencias se ejecuten hasta que se cumpla determinada condición.
4-48
Lenguajes de interrogación de bases de datos
Bucle WHILE simple La forma más simple del bucle WHILE especifica una expresión lógica y una sentencia o bloque de sentencias. Las instrucciones se repiten hasta que la expresión lógica se evalúa a FALSE. Si la expresión lógica es FALSE la primera vez que se evalúa la sentencia WHILE, la instrucción o grupo de instrucciones no se ejecutará nunca. DECLARE @contador INT SET @contador = 1 WHILE @contador < 11 BEGIN PRINT @contador SET @contador = @contador + 1 END Bucle WHILE complejo La sintaxis de la sentencia WHILE puede realizar procesos más complejos que el mostrado en el ejemplo anterior. La cláusula BREAK sale del bucle; la ejecución continúa por la sentencia que sigue a la cláusula END del bloque de sentencias WHILE. La cláusula CONTINUE devuelve la ejecución al comienzo del bucle, ocasionando que las sentencias que le siguen en el bloque de instrucciones no se ejecuten. Ambas sentencias BREAK y CONTINUE se suelen ejecutar condicionalmente, dentro de una instrucción IF. Utilizar WHILE…BREAK: DECLARE @contador INT SET @contador = 1 WHILE @contador < 25 BEGIN PRINT @contador SET @contador = @contador + 1 IF @contador > 10 BREAK END Utilizar WHILE...CONTINUE:
4-49
Desarrollo de Sistemas
DECLARE @contador INT SET @contador = 0 WHILE @contador < 11 BEGIN SET @contador = @contador + 1 IF (@contador % 2) = 0 CONTINUE PRINT @contador END
7.3.
Transacciones
Una transacción es una serie de cambios en la base de datos que deben ser tratadas como una sola. En otras palabras, que se realicen todos o que no se haga ninguno, pues de lo contrario se podrían producir inconsistencias en la base de datos. Cuando no se tiene activada una transacción el gestor de base de datos ejecuta inmediatamente cada sentencia INSERT, UPDATE o DELETE que se le encomiende, sin posibilidad de deshacer los cambio en caso de ocurrir cualquier percance. Cuando se activa una transacción los cambios que se van realizando quedan en un estado de provisionalidad hasta que se realiza un COMMIT, el cual hará definitivos los cambios o hasta realizar un ROLLBACK que deshará todos los cambios producidos desde que se inició la transacción.
7.4.
Cursores
Una de las características que definen las bases de datos relacionales es que las operaciones se ejecutan sobre un conjunto de filas. Un conjunto puede estar vacío, o contener una sola fila, pero aún así se considera un conjunto. Esto es necesario y útil en operaciones relacionales, pero en algunas ocasiones puede no ser conveniente para las aplicaciones. Por ejemplo, dado que no hay un modo de apuntar a una fila específica de un conjunto, mostrar cada vez una fila al usuario puede ser difícil. Para manejar estas situaciones, SQL admite los cursores. Un cursor es un objeto que apunta a una fila específica dentro de un conjunto. Dependiendo de la naturaleza del cursor que cree, puede mover el cursor por el conjunto y modificar o borrar datos. Su sintaxis es: DECLARE nombre-cursor CURSOR FOR especificación-consulta DECLARE MiCursor CURSOR FOR SELECT num_emp, nombre, puesto, salario FROM Empleados WHERE num_dept = 'informatica' 4-50
Lenguajes de interrogación de bases de datos
Este comando es meramente declarativo, simplemente especifica las filas y columnas que se van a recuperar. La consulta se ejecuta cuando se abre o se activa el cursor. •
Variables cursor SQL permite declarar variables de tipo CURSOR. En este caso, la sintaxis estándar DECLARE no crea el cursor, debe explícitamente establecer (SET) la variable al cursor. DECLARE MiCursor CURSOR FOR SELECT OilName FROM Oils DECLARE @miVariableCursor CURSOR SET @miVariableCursor = MiCursor
•
Abrir un cursor Al declarar un cursor crea el objeto cursor, pero no crea el conjunto de registros que manipulará el cursor (el conjunto de cursor). El conjunto de cursor no se crea hasta que abre el cursor. Para abrir o activar un cursor se utiliza el comando OPEN, la sintaxis es la siguiente: OPEN nombre_cursor Al abrir el cursor se evalúa la consulta que aparece en su definición, utilizando los valores actuales de cualquier parámetro referenciado en la consulta, para producir una colección de filas. El puntero se posiciona delante de la primera fila de datos (registro actual), esta sentencia no recupera ninguna fila.
•
Cerrar un cursor Una vez ha terminado de utilizar un cursor, deberá cerrarlo. La sentencia CLOSE libera los recursos utilizados en mantener el conjunto de cursor. Este comando hace desaparecer el puntero sobre el registro actual. La sintaxis es: CLOSE nombre_cursor
•
Liberar un cursor Para liberar un cursor, se utiliza la sentencia DEALLOCATE. Este comando borra el identificador del cursor o la variable cursor, pero no borra necesariamente el cursor. El cursor en sí mismo no se elimina hasta que todos los identificadores que lo referencian se hayan liberado o salgan fuera de su ámbito o se elimine el cursor. La sintaxis es: DEALLOCATE nombre_cursor
4-51
Desarrollo de Sistemas
Por ejemplo: -- Crea el cursor DECLARE MiCursor CURSOR FOR SELECT * FROM Oils -- Crea una variable de cursor DECLARE @variableCursor CURSOR -- Crea el conjunto de cursor OPEN MiCursor -- Asigna la variable al cursor SET @variableCursor = MiCursor -- Liberar el cursor DEALLOCATE MiCursor Después de liberar el cursor, el identificador MiCursor deja de estar asociado con el conjunto de cursor, pero dado que el conjunto de cursor aun está referenciado por la variable @variableCursor, el cursor y conjunto de cursor no se liberan. A menos que explícitamente libere también la variable cursor, el cursor y conjunto de cursor continuarán existiendo hasta que la variable salga de su ámbito o se elimine definitivamente el cursor. •
Eliminar un cursor Para eliminar el cursor se utiliza el comando DROP CURSOR. Su sintaxis es la siguiente: DROP CURSOR nombre_cursor
•
Manipular filas mediante un cursor Los cursores no serían interesantes si no pudiera hacer algo con ellos. Existen tres comandos diferentes para trabajar con cursores: FETCH, UPDATE y DELETE. El comando FETCH recupera una fila específica del conjunto del cursor. En su forma más simple el comando FETCH tiene la sintaxis: FETCH cursor_o_variable Este comando obtiene la fila en la cual está posicionado el cursor (la fila actual). En lugar de obtener una fila directamente, el comando FETCH le permite almacenar los valores obtenidos de las columnas en variables. Para almacenar los resultados del FETCH en una variable, utilice la siguiente sintaxis:
4-52
Lenguajes de interrogación de bases de datos
FETCH cursor_o_variable INTO lista_variables La lista_variables es una lista separada por comas de identificadores de variable. Debe declarar las variables antes de ejecutar el comando FETCH. La lista_variables debe incluir una variable por cada columna de la sentencia SELECT que define el cursor, y los tipos de datos de las variables deben ser igual o compatibles con los tipos de datos de la columna. -- Crea el cursor y algunas variables DECLARE CursorSimple CURSOR FOR SELECT OilName, Latíname FROM Oils DECLARE @Nombre CHAR(20), @NombreLatin CHAR(50) -- Crea el conjunto de cursor OPEN CursorSimple -- Recupera los valores en variables FETCH CursorSimple INTO @Nombre, @NombreLatin -- Muestra los resultados PRINT RTRIM(@Nombre) + ‘ es el nombre’ PRINT RTRIM(@NombreLatin) + ‘ es el nombre latín’ -- Cierra el conjunto de resultados CLOSE CursorSimple -- Libera el cursor DEALLOCATE CursorSimple En el ejemplo anterior hemos utilizado la sentencia FETCH para obtener la fila actual. La sintaxis de la sentencia FETCH proporciona también un número de palabras reservadas para especificar una fila diferente. Cuando utiliza una de estas palabras clave la sentencia FETCH obtendrá la fila especificada y convierte esa fila en la actual. Tres palabras clave le permiten especificar una posición absoluta en el conjunto de cursor. Las palabras reservadas FIRST y LAST obtienen la primera y última fila respectivamente, mientras que ABSOLUTE n especifica una fila n filas desde el comienzo (si n es positivo) o el final (si n es negativo) del conjunto de cursor. Puede expresar el valor de n como una constante (3) o una variable (@laFila). -- Crea el cursor y algunas variables DECLARE CursorSimple CURSOR FOR SELECT OilName FROM Oils 4-53
Desarrollo de Sistemas
DECLARE @Nombre CHAR(20) -- Crea el conjunto de cursor OPEN CursorSimple -- Recupera la primera fila en la variable FETCH FIRST FROM CursorSimple INTO @Nombre -- Muestra los resultados PRINT RTRIM(@Nombre) + ‘es el primer nombre’ -- Recupera la quinta fila FETCH ABSOLUTE 5 FROM CursorSimple INTO @Nombre -- Muestra los resultados PRINT RTRIM(@Nombre) + ‘es el quinto nombre’ -- Cierra el conjunto de resultados CLOSE CursorSimple -- Libera el cursor DEALLOCATE CursorSimple Además de las palabras clave que le permiten recuperar una fila basándose en su posición absoluta, la sentencia FETCH le proporciona tres palabras clave que le permiten recuperar una fila basándose en su posición relativa con respecto a la fila actual. FETCH NEXT obtiene la siguiente fila, FETCH PRIOR obtiene la fila anterior, y FETCH RELATIVE n obtiene una fila n filas desde la fila actual. Como FETCH ABSOLUTE n, FETCH RELATIVE n puede especificar el número de filas antes de la fila actual, si n es negativo, o después de la fila actual, si n es positivo. DECLARE CursorSimple CURSOR FOR SELECT OilName FROM Oils DECLARE @Nombre CHAR(20) OPEN CursorSimple -- Recupera la fila en la variable FETCH FIRST FROM CursorSimple INTO @Nombre -- Muestra los resultados PRINT RTRIM(@Nombre) + ‘es el primer nombre’
4-54
Lenguajes de interrogación de bases de datos
-- Recupera la siguiente fila FETCH RELATIVE 1 FROM CursorSimple INTO @Nombre -- Muestra los resultados PRINT RTRIM(@Nombre) + ‘es el siguiente nombre’ CLOSE CursorSimple DEALLOCATE CursorSimple •
Monitorizar un cursor
@@FETCH_STATUS obtiene información sobre el último comando FETCH ejecutado. VALOR
DE
RETORNO
SIGNIFICADO
0
El comando FETCH se ejecutó correctamente.
-1
El comando FETCH falló.
-2
La fila leída desapareció.
--Abrir un cursor y recorrerlo DECLARE EmployeeCursor CURSOR FOR SELECT LastName, FirstName FROM Employees WHERE LastName LIKE 'B*' OPEN EmployeeCursor FETCH NEXT FROM EmployeeCursor WHILE @@FETCH_STATUS = 0 BEGIN FETCH NEXT FROM EmployeeCursor END CLOSE EmployeeCursor DEALLOCATE EmployeeCursor --Recorrer un cursor guardando los valores en variables DECLARE @au_lname VARCHAR(40) 4-55
Desarrollo de Sistemas
DECLARE @au_fname VARCHAR(20) DECLARE authors_cursor CURSOR FOR SELECTau_lname, au_fname FROM authors WHERE au_lname LIKE "B*" ORDER BY au_lname, au_fname OPEN authors_cursor FETCH NEXT FROM authors_cursor INTO @au_lname, @au_fname WHILE @@FETCH_STATUS = 0 BEGIN PRINT "Author: " + @au_fname + " " + @au_lname FETCH NEXT FROM authors_cursor INTO @au_lname, @au_fname END CLOSE authors_cursor DEALLOCATE authors_cursor
7.5.
Procedimientos almacenados (Store procedure)
Es una colección de sentencias SQL precompiladas que pueden devolver y tomar parámetros, algo así como un fichero de ejecución por lotes. Son lotes de sentencias almacenadas en el servidor. Los procedimientos almacenados no son la única forma de ejecutar sentencias SQL. Hemos visto los script SQL, pero los procedimientos almacenados se ejecutan de forma optimizada, dando lugar a una ejecución más rápida. Los procedimientos almacenados proporcionan dos métodos de comunicación con procesos externos: parámetros y valores de retorno. Los parámetros son una clase especial de variable local declarada como parte del procedimiento almacenado. Puede utilizar parámetros para pasar información al procedimiento almacenado (parámetros de entrada) o recibir valores desde el procedimiento almacenado (parámetros de salida). Un valor de retorno es similar al resultado de una función, y pueden asignarse a una variable local de la misma forma. Los valores de retorno siempre son enteros. Pueden utilizarse teóricamente para devolver cualquier resultado, pero por convención se utilizan para devolver el estado de la ejecución del procedi-
4-56
Lenguajes de interrogación de bases de datos
miento almacenado. Por ejemplo, un procedimiento almacenado podría devolver 0 si todo fue bien, o -1 si hubo algún error. Los procedimientos almacenados más sofisticados pueden devolver valores de retorno diferentes para indicar la naturaleza del error encontrado. Es importante no confundir los parámetros y los códigos de retorno con cualquier otro conjunto de resultados que podría devolver un procedimiento almacenado. Un procedimiento almacenado puede contener cualquier número de sentencias SELECT que devolverían conjuntos de resultados. No tiene que utilizar un parámetro para recibirlos, se devuelven a la aplicación de forma independiente. Los procedimientos almacenados vienen de dos formas: los procedimientos de sistema creados por el propio SQL (todos ellos comienzan con los caracteres sp_) que nos devuelven información acerca del sistema, sus tablas, contenidos y estructura de los campos, almacenamiento de los datos, etc. (por ejemplo, sp_tables, sp_columns, sp_spaceused, sp_who, sp_helpdb, etc.) y los procedimientos almacenados definidos por el usuario. •
Utilizar procedimientos almacenados Se utiliza la sentencia EXECUTE para invocar un procedimiento almacenado tanto de sistema como definido por el usuario. Si el procedimiento almacenado no tiene parámetros o si no devuelve ningún resultado, la sintaxis es muy simple: EXECUTE nombre_procedimiento Por ejemplo: EXECUTE sp_helpdb Si el procedimiento almacenado acepta parámetros de entrada puede indicárselos por posición o por nombre. Para indicar parámetros por posición, simplemente lístelos después del nombre del procedimiento almacenado, separando cada parámetro individual con comas: EXECUTE nombre_procedimiento parámetro [, parámetro…] Por ejemplo: EXECUTE sp_dboption ‘MiBasedeDatos’, ‘read only’
•
Crear procedimientos almacenados Los procedimientos almacenados se crean utilizando la sentencia CREATE PROCEDURE. Su sintaxis es: CREATE PROCEDURE nombre_procedimiento [lista_parámetros] AS sentencias_procedimiento Cada parámetro en la lista_parámetros tiene la estructura:
4-57
Desarrollo de Sistemas
@nombre_parámetro tipo_dato [= valor_defecto] [OUTPUT] Los nombres de parámetros comienzan siempre con @, como una variable local. De hecho, los parámetros son variables locales; sólo son visibles dentro del procedimiento almacenado. El valor_defecto es el valor que utilizará el procedimiento almacenado si el usuario no especifica el valor del parámetro de entrada en la llamada al procedimiento almacenado. La palabra reservada OUTPUT, también opcional, define los parámetros que se devolverán al script de llamada. Las sentencias_procedimiento que siguen al AS en la sentencia CREATE definen las acciones a ejecutar cuando se llame al procedimiento almacenado. Los procedimientos almacenados pueden llamar a otros procedimientos almacenados, en un proceso conocido como anidamiento. Crear un procedimiento almacenado simple CREATE PROCEDURE SPSimple AS SELECT OilName, LatinName FROM Oils Para ejecutar este procedimiento almacenado: EXECUTE SPSimple Crear un procedimiento almacenado con un parámetro de entrada CREATE PROCEDURE SPInput @OilName CHAR(50) AS SELECT OilName, LatinName FROM Oils WHERE OilName = @OilName Para ejecutar este procedimiento almacenado: EXECUTE SPInput ‘Basil’ Crear un procedimiento almacenado con un valor por defecto CREATE PROCEDURE SPDefault @OilName CHAR(50) = ‘Fennel’ AS SELECT OilName, LatinName FROM Oils WHERE OilName = @OilName Para ejecutar este procedimiento almacenado: EXECUTE SPDefault Crear un procedimiento almacenado con un parámetro de salida CREATE PROCEDURE SPOutput @VarSalida CHAR(6) OUTPUT AS SET @VarSalida = ‘Salida’
4-58
Lenguajes de interrogación de bases de datos
Para ejecutar este procedimiento almacenado: DECLARE @miSalida CHAR(6) EXECUTE SPOutput @miSalida OUTPUT SELECT @miSalida Los valores de retorno se definen utilizando la sentencia RETURN, que tiene la forma: RETURN (int) En la sentencia RETURN, int es un valor entero. Como vimos anteriormente, los valores de retorno se utilizan la mayoría de las veces para devolver el estado de ejecución de un procedimiento almacenado, con 0 indicando ejecución correcta, y cualquier otro número indicando un error. Puede comprobar los errores utilizando la variable global @@ERROR, que devuelve el estado de ejecución del comando SQL más reciente: 0 para ejecución correcta, o un número distinto de cero indicando el error que ha ocurrido. Crear un procedimiento almacenado con un parámetro de entrada CREATE PROCEDURE SPError AS -- Crea una variable para almacenar el código de error DECLARE @codigoRetorno INT SELECT OilName, LatinName FROM Oils -- Atrapa cualquier error SET @codigoRetorno = @@ERROR RETURN (@codigoRetorno) Para ejecutar este procedimiento almacenado: DECLARE @elError INT EXECUTE @elError = SPError SELECT @elError AS ‘Valor retorno’
7.6.
Desencadenadores (triggers)
Un desencadenador o trigger es un tipo especial de procedimiento almacenado que se ejecuta desatendidamente y automáticamente cuando un usuario realiza una acción con la tabla de una base de datos que lleve asociado este trigger. Se pueden crear triggers para las sentencias de SQL INSERT, UPDATE Y DELETE. 4-59
Desarrollo de Sistemas
SQL impone algunas restricciones en el proceso que pueden ejecutar los desencadenadores. No puede CREATE, ALTER o DROP una base de datos utilizando un desencadenador; ni restaurar una base de datos o archivo de transacciones; y no puede ejecutar ciertas operaciones que cambien la configuración de SQL. •
Utilizar el comando CREATE TRIGGER Los desencadenadores se crean utilizando la sentencia CREATE TRIGGER. Su sintaxis es: CREATE TRIGGER nombre_desencadenador ON tabla FOR lista_comandos AS sentencias_sql La Lista de comandos es cualquier combinación de los comandos INSERT, UPDATE o DELETE. Si indica más de un comando, sepárelos con comas. Las sentencias_sql que siguen a la palabra reservada AS definen el proceso a ejecutar por el desencadenador, igual que en los procedimientos almacenados excepto que un desencadenador no admite parámetros. CREATE TRIGGER afterUpdate ON Oils FOR UPDATE AS INSERT INTO TriggerMessages (TriggerName, MessageText) VALUES (‘afterUpdate’, ‘enviado por el desencadenador afterUpdate’)
•
Utilizar la función UPDATE SQL proporciona una función especial, UPDATE, que puede utilizarse dentro de un desencadenador para comprobar si se ha modificado una columna específica de una fila. La sintaxis es: UPDATE (nombre_columna) Devolverá TRUE si se ha modificado el valor de los datos para la columna especificada para cualquiera de los comandos INSERT o UPDATE. CREATE TRIGGER UpdateFunc ON Oils FOR UPDATE AS IF UPDATE (Descripcion) INSERT INTO TriggerMessages (TriggerName, MessageText)
4-60
Lenguajes de interrogación de bases de datos
VALUES (‘UpdateFunc’, ‘Descripción modificada’) IF UPDATE (OilName) INSERT INTO TriggerMessages (TriggerName, MessageText) VALUES (‘UpdateFunc’, ‘OilName modificado’) •
Utilizar las tablas Inserted y Deleted SQL crea dos tablas para ayudarle a manipular los datos durante la ejecución del desencadenador. Las tablas Inserted y Deleted son tablas temporales residentes en memoria que contienen los valores de las filas afectadas por el comando que invocó al desencadenador. Cuando se llama un desencadenador desde un comando DELETE, la tabla Deleted contendrá las filas que se borraron de la tabla. Para un comando INSERT, la tabla Inserted contendrá una copia de las nuevas filas. Físicamente, una sentencia UPDATE es un DELETE seguido de un INSERT, así que la tabla Deleted contendrá los valores antiguos, y la tabla Inserted los valores nuevos. Puede hacer referencia a los contenidos de estas tablas desde dentro del desencadenador pero no modificarlas.
7.7.
Bloqueos
Los bloqueos nos proporcionan información acerca de qué recursos individuales están bloqueados. Los bloqueos en filas leídas o modificadas durante una transacción se utilizan para evitar que varias transacciones utilicen simultáneamente los mismos recursos y puedan estropear los datos. Por ejemplo, si una transacción mantiene un bloqueo exclusivo en una fila de una tabla ninguna otra transacción podrá modificar esa fila hasta que se libere el bloqueo. Para lograr estos objetivos el SQL tiene cuatro modos de asilamiento (isolation levels). •
¿Qué se bloquea? SQL dispone de varios niveles de bloqueo lo que permite a una transacción bloquear diferentes tipos de recursos. Para minimizar el costo de los bloqueos y aumentar la simultaneidad, SQL bloquea automáticamente los recursos en el nivel apropiado para la tarea. El bloqueo de menor granularidad, como es el caso de las filas, aumenta la simultaneidad. Sin embargo, se produce una sobrecarga mayor porque cuantas más filas se bloquean, más bloqueos se deben mantener y esto requiere que nuestro servidor utilice recursos adicionales del sistema. Bloquear con una granularidad mayor, como las tablas, es costoso en términos de simultaneidad debido a que bloquear una tabla completa restringe los accesos de las demás transacciones a cualquier parte de la tabla, pero produce una sobrecarga menor (menos recursos utilizados) debido a que se mantienen menos bloqueos.
4-61
Desarrollo de Sistemas
Veamos que tipo de recursos puede bloquear SQL: RID
Identificador de fila. Se utiliza para bloquear una sola fila de una tabla.
CLAVE
Bloqueo de una fila en un índice. Se utilizan únicamente en transacciones que operan en el nivel de transacción serializable.
PÁGINA
La página de datos o página de índices (8 Kb).
EXTENSIÓN
Grupo contiguo de ocho páginas de datos o páginas de índice.
TABLA
Tabla completa, con todos los datos e índices.
BASE DE DATOS
Base de datos.
Estos recursos se pueden bloquear con diferentes modos de bloqueo que determinarán cómo transacciones simultáneas pueden tener acceso a esos recursos. •
¿Cómo se bloquea? Una vez que hemos visto los recursos sobre los que SQL mantiene bloqueos, vamos a ver los tipos de bloqueo de los que disponemos: —
SHARED (S): Compartido.
—
UPDATE (U): Actualizar.
—
EXCLUSIVE (X): Exclusivo.
—
BULK UPDATE (BU): Actualización masiva.
—
SCHEMA: Esquema.
Estos tipos de bloqueo son los que SQL utiliza para alcanzar los cuatro niveles de aislamiento. El nivel de aislamiento estándar es el COMMITED READ aunque con la instrucción: SET TRANSACTION ISOLATION LEVEL { COMMITTED READ | UNCOMMITTED READ | REPEATABLE READ | SERIALIZABLE } podemos cambiar este comportamiento. •
Bloqueo compartido Los bloqueos compartidos (SHARED, S) se utilizan para operaciones de lectura de datos.
4-62
Lenguajes de interrogación de bases de datos
Durante los bloqueos compartidos (S) varias transacciones concurrentes pueden leer un recurso pero no pueden modificar ese recurso mientras ese bloqueo compartido exista. Si no hemos cambiado el nivel de aislamiento de nuestra transacción, cosa que en general no haremos, en cuanto se haya producido la lectura de los datos los recursos bloqueados quedan libres. Si hemos colocado el nivel de aislamiento de nuestra transacción en REPEATABLE READ o en SERIALIZABLE, el recurso quedará bloqueado hasta que termine la transacción en la que estamos trabajando. •
Bloqueo de actualización Los bloqueos de actualización (UPDATE, U) se utilizan cuando el SQL tiene intención de modificar una fila o una página y posteriormente promociona este bloqueo a un bloqueo exclusivo (X). Este tipo de bloqueos se utiliza para evitar el problema de los interbloqueos. Veámoslo con un ejemplo: Supongamos que tenemos dos transacciones que intentarán actualizar la misma fila. Cada una de nuestras transacciones obtendrá un bloqueo compartido (S) sobre la fila, la leerá y posteriormente intentará obtener sobre esa fila un bloqueo exclusivo. Pero la obtención de un bloqueo exclusivo no es compatible con la existencia de un bloqueo compartido, así que la primera transacción esperará a que la segunda libere su bloqueo compartido, y la segunda espera a que la primera libere su bloqueo compartido para obtener uno exclusivo. Este es un ejemplo típico de interbloqueo. Para evitar esta situación tenemos este tipo de bloqueos de actualización (U). Dos transacciones no pueden obtener simultáneamente un bloqueo de actualización (U) para un recurso, y si una transacción modifica un recurso, el bloqueo de actualización (U) se convierte en bloqueo exclusivo (X). En caso contrario, el bloqueo se convierte en bloqueo de modo compartido.
•
Bloqueos exclusivos Los bloqueos exclusivos (EXCLUSIVE, X) se utilizan para realizar modificaciones con sentencias INSERT, UPDATE y DELETE. La principal característica de este bloqueo es que otras transacciones no pueden leer ni modificar los registros bloqueados. Asimismo si hay un bloqueo compartido(S) sobre un recurso ninguna transacción puede obtener un bloqueo exclusivo sobre ese recurso.
•
Bloqueos de actualización masiva Los bloqueos de actualización masiva (BULK UPDATE, BU) se utilizan durante la inserción masiva de datos en una tabla. Este tipo de bloqueos permiten que se copien datos concurrentemente en la misma tabla mientras que se impide que otros procesos accedan a esa tabla.
4-63
Desarrollo de Sistemas
•
Bloqueos de Esquema Los bloqueos de esquema (SCHEMA) se usan cuando se realiza una operación que modifica el esquema de nuestra base de datos. Por ejemplo al ejecutar una sentencia DDL como ALTER TABLE se adquiere un bloqueo de modificación de esquema en la tabla para garantizar que ninguna otra conexión haga referencia ni siquiera a los metadatos de la tabla durante el cambio.
4-64
Tema 5 Diseño y programación orientada a objetos. Elementos y componentes software: objetos, clases, herencia, métodos, sobrecarga. Ventajas e inconvenientes. Patrones de diseño y lenguaje de modelado unificado (UML).
Desarrollo de Sistemas
Guión-resumen
1. Diseño y programación orientada a objetos 1.1. Diseño orientado a objetos 2. Elementos y componentes software: objetos, clases, herencia, métodos, sobrecarga 2.1. Evolución de los objetos 2.2. La programación orientada a objetos (POO-OOP) 2.3. La abstracción 2.4. El encapsulado 2.5. La herencia 2.6. El poliformismo 2.7. Clase 2.8. Objeto 2.9. Propiedades y métodos 2.10. Mensajes 2.11. Identidad 2.12. Reutilización o reusabilidad 2.13. Jerarquía 2.14. Concurrencia, persistencia y tipificado 2.15. Modularidad 2.16. Relaciones entre los conceptos asociados al modelo de objetos
5-2
3. Ventajas e inconvenientes 3.1. Ventajas de POO 3.2. Inconvenientes de la POO 4. Patrones de diseño y lenguaje de modelado unificado (UML) 4.1. UML 4.2. Diagramas
Diseño y programación orientada a objetos
1.
Diseño y programación orientada a objetos
La programación orientada a objetos y todos los lenguajes que la usan (entre los cuales destacaremos: C++, C # y JAVA) han cobrado gran renombre, no quizás tanto por su funcionalidad, sino por la revolución que causaron en la forma de pensar el programador y que con gran seguridad marcarán los lenguajes del futuro. La idea es tan sencilla como: si quiero hacer miles de bicicletas qué sería mejor: 1.
¿Ir haciendo “una a una” hasta alcanzar la cantidad prevista?
2.
¿Crear un molde (clase) para hacer bicicletas (objetos) y a partir de ese molde crear las bicicletas?
Está claro, la segunda opción es la ideal. Es más, si nos paramos a pensar: todas las bicicletas que salgan del molde serán exactamente iguales a no ser que modifiquemos el molde. A las bicicletas que quiera una vez creadas les pudo acoplar los atributos que se desee. Si le ponemos un atributo nuevo al molde todas las nuevas bicicletas tendrán ese nuevo atributo. Podríamos seguir citando ventajas durante folios y folios de este libro, pero lo que se debe es comprender la idea. El término objeto surgió a principios de los sesenta en varios campos de la informática, para referirse a nociones que eran diferentes en su apariencia, pero relacionados entre sí. Cada concepto que usamos los humanos es una idea particular o una comprensión de nuestro mundo, los conceptos adquiridos nos permiten sentir y razonar acerca de las cosas en el mundo. A estas cosas a las que se aplican nuestros conceptos se las llama objetos. Un objeto puede ser real (ejemplo: una piedra, un avión) o abstracto (ejemplo: organización). De manera formal decimos: un objeto es cualquier cosa, real o abstracta, a cerca de la cual almacenamos datos y los métodos que controlan dichos datos. Un objeto puede estar compuesto por otros objetos; estos últimos, a su vez, pueden estar compuestos de objetos, del mismo modo que una máquina está formada por partes y éstas, también, están formadas por otras partes. Esta estructura intrincada de los objetos permite definir objetos muy complejos. Las técnicas orientadas a objetos permiten que el software se construya a partir de objetos de comportamiento específico. El elemento fundamental de la OOP-POO (OOP en inglés, POO en español) es, como su nombre lo indica, el objeto. Podemos definir un objeto (en programación) como un conjunto complejo de datos y programas que poseen estructura y forman parte de una organización. Esta definición especifica varias propiedades importantes de los objetos. En primer lugar, un objeto no es un dato simple, sino que contiene en su interior cierto número de componentes bien estructurados. En segundo lugar, cada objeto no es un ente aislado, sino que forma parte de una organización jerárquica o de otro tipo. Un objeto puede considerarse como una especie de cápsula dividida en tres partes:
5-3
Desarrollo de Sistemas
—
Las relaciones permiten que el objeto se inserte en la organización y están formadas esencialmente por punteros a otros objetos.
—
Las propiedades distinguen un objeto determinado de los restantes que forman parte de la misma organización y tiene valores que dependen de la propiedad de que se trate. Las propiedades de un objeto pueden ser heredadas a sus descendientes en la organización.
—
Los métodos son las operaciones que pueden realizarse sobre el objeto, que normalmente estarán incorporados en código que el objeto es capaz de ejecutar y que también pone a disposición de sus descendientes a través de la herencia.
1.1.
Diseño orientado a objetos
1.1.1. Introducción El proceso de desarrollo de software es aquel en el que las necesidades del usuario son traducidas en requisitos de “software”, éstos transformados en diseño y el diseño implementado en código. Podemos definir el diseño en el software como el proceso de aplicar distintas técnicas y principios con el propósito de definir un producto con los suficientes detalles como para permitir su realización física. Con el diseño se pretende construir un sistema que satisfaga determinada especificación del sistema, se ajuste a las limitaciones impuestas por el medio de destino y respete requisitos sobre forma, rendimiento, utilización de recursos… A través del diseño producimos un modelo o representación técnica del “software” que se va a desarrollar. El diseño es uno de los procesos básicos sobre el que se asienta la calidad del “software”. Se trata de un proceso iterativo a través del cual se traducen los requisitos en una representación del “software”. Se representa a un alto nivel de abstracción, un nivel que se puede seguir hasta requisitos específicos de datos, funcionales y de comportamiento.
1.1.2. Metodologías de diseño —
Diseño de datos. Modelo de información a estructuras de datos.
—
Diseño arquitectónico. Define las relaciones entre los elementos estructurales del programa.
—
Diseño procedimental. Se transforman los elementos estructurales del programa en una descripción procedimental del software.
—
Diseño de interfaz. Describe cómo se comunica el software consigo mismo y con su entorno.
1.1.3. Directrices de diseño —
5-4
El diseño debe implementar todos los requisitos explícitos contenidos en el modelo de análisis y debe acomodar todos los requisitos implícitos que desee el cliente.
Diseño y programación orientada a objetos
—
El diseño debe ser una guía que puedan leer y entender los que construyan el código y los que prueban y mantienen el “software”.
—
El diseño debería proporcionar una completa idea de lo que es el “software”, enfocando los dominios de datos, funcional y de comportamiento desde la perspectiva de la implementación.
1.1.4. Principios básicos de diseño —
El diseñador debe considerar enfoques alternativos juzgando a cada uno en relación a los requisitos del problema, los resultados disponibles y los criterios de calidad interna.
—
Se deben seguir los pasos de diseño hasta el modelo de análisis.
—
El diseño no va a reinventar nada que ya esté inventado.
—
El diseño debería presentar uniformidad de integración.
—
Debe estructurarse para admitir cambios.
—
El diseño no es escribir código y escribir código no es diseñar.
—
Se debería valorar la calidad del diseño mientras se crea, no después de terminado.
1.1.5. Patrones de diseño Un patrón describe un problema que ocurre una y otra vez en nuestro entorno, para describir después el núcleo de la solución a ese problema, de tal manera que esa solución pueda ser usada varias veces sin hacerlo dos veces de la misma forma. Un patrón aborda un problema de diseño recurrente que aparece en situaciones específicas de diseño y presenta una solución para éste. Los patrones identifican y especifican abstracciones que están por encima del nivel de las clases e instancias, o de componentes. Un patrón tiene 4 elementos esenciales: —
El nombre del patrón se usa para describir un problema de diseño, sus soluciones y consecuencias en una o dos palabras.
—
El problema describe cuándo aplicar el patrón. Explica el problema y su contexto. Podría describir problemas específicos de diseño del tipo de cómo representar algoritmos como objetos. Podría describir estructuras de clases u objetos que son síntomas de un diseño inflexible.
—
La solución describe los elementos que construyen el diseño, sus relaciones, responsabilidades y colaboraciones. El patrón proporciona una descripción abstracta de un problema de diseño y cómo una disposición general de elementos lo resuelve.
5-5
Desarrollo de Sistemas
—
Las consecuencias son los resultados e inconvenientes de aplicar el patrón. Aunque las consecuencias se ignoran cuando describimos las decisiones de diseño, son críticas para evaluar las alternativas de diseño y para entender los costes y beneficios de aplicar el patrón.
1.1.6. Tipos de patrones A) Patrones de creación Los patrones de creación conciernen el proceso de creación de objetos. Los patrones de creación proporcionan ayuda a la hora de crear objetos, principalmente cuando esta creación requiere tomar decisiones. Esta toma de decisiones puede ser dinámica. Estos patrones ayudan a estructurar y encapsular estas decisiones. En algunas ocasiones existe más de un patrón que se puede aplicar a la misma situación. En otras ocasiones se pueden combinar múltiples patrones convenientemente. Un patrón de creación asociado a clases usa la herencia para variar la clase que se instancia, mientras que un patrón de creación asociado a objetos delegará la instanciación a otro objeto. Hay dos formas de clasificar los patrones de creación basándose en las clases de objetos que se crean. Una es clasificar las clases que crean los objetos (Factory Method), otra forma está relacionada con la composición de objetos; definir un objeto que es responsable de conocer las clases de los objetos producto, en esta característica se apoyan los patrones Abstract Factory, Builder o Prototype. —
Factory method proporciona una interfaz para crear un objeto, pero deja a las subclases decidir cuál clase instanciar. Permite a una clase delegar la instanciación a las subclases.
—
Abstract Factory proporciona una interfaz para crear familias de objetos relacionados o dependientes sin especificar su clase concreta.
—
Builder separa la construcción de un objeto complejo de su representación para que el mismo proceso de construcción pueda crear diferentes representaciones.
—
Prototype especifica el tipo de objetos a crear usando una instancia prototipo, y crea nuevos objetos copiando este prototipo.
—
Singleton asegura que una clase sólo tiene una instancia, y proporciona un punto de acceso global a ésta.
B) Patrones estructurales Tratan de la composición de clases y objetos. Los patrones estructurales están relacionados con cómo las clases y los objetos se combinan para dar lugar a estructuras más complejas. Puede hacerse aquí la misma distinción que hacíamos en los patrones de creación y hablar de patrones estructurales asociados a clases (Adapter) y asociados a objetos (Bridge, Composite, Decorator, Facade, Flyweight, Proxy), los primeros utilizarán la herencia, los segundos la composición. 5-6
Diseño y programación orientada a objetos
Los patrones estructurales asociados con objetos describen formas de componer los objetos para conseguir nueva funcionalidad. La flexibilidad de la composición de objetos viene de la posibilidad de cambiar la composición en tiempo de ejecución, lo que es imposible con la composición estática de clases. —
Adapter convierte la interfaz de una clase en otra interfaz que espera el cliente. Permite trabajar juntas a clases que de otra forma no podrían hacerlo por incompatibilidad de “interfaces”.
—
Bridge desacopla una abstracción de su implementación para que los dos puedan variar independientemente.
—
Composite compone objetos en estructuras de árbol para representar jerarquías parte-todo. Permite a los usuarios tratar objetos individuales y composiciones de manera uniforme.
—
Decorator agrega responsabilidades adicionales a un objeto dinámicamente. Proporcionan una alternativa flexible a las subclases para extender funcionalidad.
—
Facade proporciona una interfaz unificada a un conjunto de interfaces en un subsistema. Define una interfaz de alto nivel que hace el subsistema más fácil de usar.
—
Flyweight comparte para proporcionar un gran número de objetos pequeños eficientemente.
—
Proxy proporciona un sustituto para otro objeto, para controlar el acceso a él.
C) Patrones de comportamiento Los de comportamiento caracterizan las maneras en las que las clases u objetos interactúan y se distribuyen las responsabilidades. Estos patrones de diseño están relacionados con algoritmos y asignación de responsabilidades a los objetos. Los patrones de comportamiento describen no sólamente patrones de objetos o clases sino también patrones de comunicación entre ellos. Nuevamente se pueden clasificar en función de que trabajen con clases (Template Method, Interpreter) u objetos (Chain of Responsability, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Visitor). La variación de la encapsulación es la base de muchos patrones de comportamiento. Cuando un aspecto de un programa cambia frecuentemente, estos patrones definen un objeto que encapsula dicho aspecto. Los patrones definen una clase abstracta que describe la encapsulación del objeto. —
Observer define una dependencia uno a varios entre objetos de manera que cuando un objeto cambia su estado, todos los objetos dependientes son notificados y actualizados automáticamente.
—
Mediator define un objeto que encapsula cómo un conjunto de objetos interactúan. Promueve bajo acoplamiento evitando que los 5-7
Desarrollo de Sistemas
objetos se refieran entre ellos explícitamente, y permite variar su interacción independientemente. —
Chain of Responsibility evita el acoplamiento del emisor de una petición a su receptor dando a más de un objeto una oportunidad para manejar la petición. Encadena los objetos receptores y pasa la petición a lo largo de la cadena hasta que un objeto la maneja.
—
Template Method define un esqueleto de un algoritmo en una operación, aplazando algunos pasos a las subclases. Permite a las subclases redefinir ciertos pasos de un algoritmo sin cambiar la estructura del algoritmo.
—
Interpreter, dado un lenguaje, define una representación para su gramática y un intérprete que usa la representación para interpretar las sentencias en el lenguaje.
—
Strategy define una familia de algoritmos, encapsulados individualmente, y los hace intercambiables. Permite a un algoritmo cambiar independientemente de los usuarios que lo usen.
—
Visitor representa una operación que va a ejecutarse sobre elementos de una estructura de objetos. Permite definir una nueva operación sin cambiar las clases de los elementos con los que opera.
—
State permite a un objeto alterar su comportamiento cuando cambia su estado interno. El objeto aparenta cambiar su clase.
—
Command encapsula una petición como objeto, por lo tanto permite valorar o analizar los usuarios con diferentes peticiones, colas o peticiones de conexión, y permite operaciones que se pueden deshacer.
—
Iterator proporciona un medio para acceder a los elementos de un objeto agregado secuencialmente sin exponer su representación subyacente.
El ámbito especifica cuando los patrones se aplican principalmente a las clases o a los objetos. Los patrones de clase tratan con relaciones entre las clases y sus subclases. Estas relaciones se establecen a través de la herencia, así que son estáticas, fijas en tiempo de compilación. Los patrones de objetos tratan con las relaciones de objetos, que pueden cambiar en tiempo de ejecución y son más dinámicos. Casi todos los patrones utilizan la herencia en algún punto. Los patrones de clase de creación delegan alguna parte de la creación a subclases, mientras que los de objeto delegan en otro objeto. Los estructurales de clases usan la herencia para componer las clases, mientras que los de objeto describen maneras de reunir objetos. Los patrones de clase de comportamiento usan herencia para describir algoritmos y flujo de control, mientras que los de objeto describen cómo un grupo de objetos cooperan para realizar una tarea que no podría llevar a cabo un objeto sólo. Los métodos de diseño orientados a objetos favorecen varios enfoques. Se puede escribir el problema y extraer nombres y verbos como clases y ope-
5-8
Diseño y programación orientada a objetos
raciones. Se puede modelar desde el punto de vista de las colaboraciones y responsabilidades en el sistema. Se puede modelar el mundo real y trasladar los objetos encontrados en el análisis al diseño. Los patrones de diseño ayudan a identificar las abstracciones menos obvias y los objetos que pueden capturarlas. D) Interfaces Cada operación declarada por un objeto especifica el nombre de una operación, los objetos que toma como parámetros y lo que devuelve. A esto se le llama signatura. Al conjunto de todas las signaturas definidas por las operaciones de un objeto se le llama la interfaz del objeto. La interfaz de un objeto caracteriza el conjunto completo de peticiones que se le pueden mandar a un objeto. Cualquier petición que coincida con una signatura de la interfaz de un objeto puede ser mandada a ese objeto. Un tipo es el nombre que se suele usar para denotar una interfaz determinada. Un objeto puede tener varios tipos, y objetos muy diferentes pueden ser del mismo tipo. Dos objetos del mismo tipo sólo necesitan compartir parte de sus interfaces. Las interfaces pueden contener otras interfaces como subconjuntos. Se dice que un tipo es un subtipo de otro si su interfaz contiene la interfaz de su supertipo (herencia). Los “interfaces” son fundamentales en los sistemas orientados a objetos. Los objetos sólo son conocidos a través de sus interfaces. No hay manera de saber nada acerca de un objeto o de pedirle que haga algo sin pasar a través de su interfaz. La interfaz de un objeto no dice nada acerca de su implementación, pues diferentes objetos pueden implementar las peticiones de manera diferente. Cuando una petición se envía a un objeto, la operación que se realiza depende tanto de la petición como del objeto que la recibe. La asociación en tiempo de ejecución de una petición a un objeto y la operación se llama ligadura dinámica. La ligadura dinámica significa que hacer una petición no se asocia a una implementación particular hasta tiempo de ejecución. Por lo tanto, se pueden escribir programas que esperen un objeto con una interfaz particular, sabiendo que cualquier objeto que tenga la interfaz correcta aceptará la petición. Es más, la ligadura dinámica permite sustituir objetos que tienen interfaces idénticas entre sí en tiempo de ejecución (polimorfismo). El polimorfismo simplifica las definiciones de los usuarios, desacopla los objetos entre sí, y permite que varíen sus relaciones en tiempo de ejecución. Los patrones de diseño ayudan a definir interfaces identificando sus elementos principales y los tipo de datos que pueden ser enviados a través de una interfaz. Un patrón de diseño puede indicar también qué es lo que no se debe poner en una interfaz. El patrón memento describe cómo encapsular y salvar el estado interno de un objeto para que este pueda ser restaurado posteriormente. El patrón estipula que los objetos memento deben definir dos interfaces: una restringida para los usuarios, y otra privilegiada para que únicamente el objeto original pueda guardar y recuperar su estado. 5-9
Desarrollo de Sistemas
Los patrones de diseño también especifican las relaciones entre “interfaces”. En particular, muchas veces necesitan que algunas clases tengan “interfaces” similares, o añaden restricciones a las interfaces de algunas clases. Existen dos beneficios al manipular los objetos en términos de su interfaz definida por una clase abstracta: —
Los usuarios ignoran en tipo específico de objeto que están usando, mientras que respondan a la interfaz que el cliente espera.
—
Los usuarios ignoran las clases que implementan los objetos. Únicamente deben conocer la clase abstracta que definen el interfaz.
Esto reduce significativamente las dependencias de implementación entre los subsistemas. Los patrones de creación abstraen el proceso de creación, de manera que ofrecen diferentes maneras de asociar una interfaz con su implementación de manera transparente a la instanciación. Los patrones de creación aseguran que el sistema se escribe en términos de interfaces, no implementaciones.
1.1.7. Herencia y composición Las dos maneras de reutilizar la funcionalidad en los sistemas Orientados a Objetos son la herencia de clases (caja blanca) y la composición de objetos (caja negra). Cada una tiene sus ventajas e inconvenientes. —
Ventajas: La herencia de clases se define estáticamente en tiempo de compilación, y se usa directamente, ya que está proporcionada por el lenguaje de programación. La herencia permite modificar de manera más sencilla la implementación que se está reutilizando. Si se sobrescriben algunos, pero no todos los métodos, podría ocurrir que los no sobrescritos llamen a los sobrescritos.
—
Inconvenientes: No se puede cambiar la implementación heredada de las clases base en tiempo de ejecución, ya que se define en tiempo de compilación. La herencia rompe el encapsulamiento. Las superclases definen parte de la representación física de las subclases. Las dependencias de implementación pueden causar problemas cuando se intenta reutilizar una subclase... se podría solucionar heredando de “interfaces”.
La composición de objetos se define dinámicamente en tiempo de ejecución mediante objetos que toman referencias a otros objetos. La composición requiere que los objetos respeten sus “interfaces”, lo que requiere “interfaces” diseñados cuidadosamente. A cambio, no se rompe el encapsulamiento, y cualquier objeto puede ser reemplazado por otro en tiempo de ejecución siempre que tenga el mismo tipo. Existen menos dependencias de implementación. La composición de objetos tiene otro efecto sobre el diseño de sistemas. Cada clase se mantiene encapsulada y enfocada en una tarea. Las clases y jerarquías de clases permanecerán pequeñas en lugar de crecer hasta convertirse en 5-10
Diseño y programación orientada a objetos
monstruos inmanejables. Por otra parte un diseño basado en la composición de objetos tendrá más objetos (y menos clases). Idealmente, se debería poder conseguir toda la funcionalidad que se necesite ensamblando componentes existentes mediante composición de objetos. Pero este caso no se suele dar, porque el conjunto de componentes nunca es suficientemente rico. La reutilización mediante la herencia hace más fácil componer nuevos componentes con los antiguos. De esta manera la herencia y la composición trabajan juntos.
1.1.8. Delegación Sirve para simular la herencia mediante la composición. La principal ventaja de la delegación es que hace fácil componer comportamientos en tiempo de ejecución y cambiar la manera en la que están compuestos. La desventaja es que es más difícil de entender, e ineficiencias de tiempo de ejecución. Varios patrones usan delegación: state, strategy y visitor. La delegación es un ejemplo extremo de composición de objetos. Nos muestra que siempre se puede reemplazar herencia por composición de objetos como mecanismo de reutilización de código.
1.1.9. Diseño La llave para maximizar la reutilización está en anticiparse a los nuevos requisitos y cambios a los requisitos existentes, y diseñar el sistema para que pueda evolucionar como corresponde. Los cambios pueden provocar redefinición de las clases, reimplementación, modificación del cliente, y nuevos tests. El rediseño afecta a muchas partes del sistema software. Los patrones de diseño nos ayudan a evitarlo asegurando que el sistema puede cambiar en ciertos aspectos determinados. Cada patrón de diseño permite que algún aspecto de la estructura del sistema varíe independientemente de otros aspectos, por lo tanto hace el sistema más robusto para un tipo determinado de cambio. Se listan a continuación las causas de rediseño más comunes junto con los patrones de diseño que las abordan: 1.
Crear un objeto especificando una clase explícitamente. Especificar el nombre de una clase cuando se crea un objeto, nos compromete a una implementación particular en lugar de a una interfaz. Este compromiso puede complicar cambios futuros. Para evitarlo, los objetos se deben crear indirectamente. Patrones de diseño: Factoría abstracta, factory method, prototipo.
2.
Dependencias de operaciones específicas. Cuando se especifica una operación particular, nos comprometemos a una manera de satisfacer una petición. Evitando peticiones especificadas en tiempo de compilación, se hace más fácil de cambiar la manera en la que se satisface una petición. Patrones de diseño: Cadena de responsabilidad, comando. 5-11
Desarrollo de Sistemas
3.
Dependencia de la plataforma hardware y software. Las APIs de los sistemas operativos son diferentes para las distintas plataformas. El “software” que depende de una plataforma particular será más difícil de portar a otras plataformas. Podría incluso ser difícil de mantener actualizada en su plataforma nativa. Por lo tanto, es importante diseñar el “software” para limitar las dependencias de la plataforma. Patrones de diseño: factoría abstracta, puente.
4.
Dependencia de las representaciones o implementaciones de objetos. Los usuarios que saben como se representa, almacena o implementa podrían necesitar ser cambiados cuando cambien los objetos. Ocultar esta información de los usuarios evita cambios en cascada. Patrones de diseño: factoría abstracta, puente, memento, proxy.
5.
Dependencias algorítmicas. Los algoritmos se extienden, optimizan y se reemplazan habitualmente durante el desarrollo y la reutilización. Los objetos que dependen de un algoritmo tendrán que cambiar cuando el algoritmo cambie. Por lo tanto los algoritmos que tengan pinta de cambiar deben ser aislados. Patrones de diseño: buider, iterator, estrategia, template method, visitor.
6.
Acoplamiento fuerte. Las clases que están muy acopladas son difíciles de reutilizar individualmente, ya que dependen unas de otras. El acoplamiento fuerte lleva a sistemas monolíticos, donde no se puede cambiar o eliminar una clase sin entender y cambiar muchas otras clases. El sistema se convierte en una masa densa que es difícil de aprender, portar y mantener. El acoplamiento débil incrementa la probabilidad de que una clase pueda ser reutilizada por sí misma y que el sistema pueda ser aprendido, portado, modificado y extendido más fácilmente. Patrones de diseño: factoría abstracta, puente, cadena de responsabilidad, comando, fachada, mediador, observador.
7.
5-12
Extender la funcionalidad mediante la herencia. Adaptar un objeto mediante la herencia muchas veces no es fácil. Cada nueva clase tiene una implementación fija por encima (inicialización, terminación...). Definir una subclase además requiere un entendimiento en profundidad de la clase base. Por ejemplo, sobrescribir una operación puede requerir sobrescribir otra. Una operación sobrescrita podría ser necesitada para llamar a una operación heredada. Y la herencia puede llevar a una explosión de clases, porque podrían necesitarse muchas subclases nuevas para una simple extensión. La composición de objetos en general y la delegación en particular proporciona una alternativa flexible a la herencia para combinar comportamientos. La nueva funcionalidad puede añadirse a una aplicación componiendo objetos existentes de nuevas maneras en lugar de definir nuevas subclases de clases existentes. Por otra parte, demasiado uso de la composición puede hacer los diseños más difíciles de entender. Muchos patrones de diseño producen diseños en los que se pueden introducir nuevas funcionalidades simplemente definiendo una subclase y componiendo las instancias con otras existentes.
Diseño y programación orientada a objetos
Patrones de diseño: puente, cadena de responsabilidad, composite, decorador, observador, estrategia. 8.
Incapacidad para alterar las clases convenientemente. A veces hay que modificar una clase que no puede ser modificada convenientemente. Quizás se necesita el código fuente y no se tiene. O puede que cualquier cambio necesite modificar un montón de clases existentes. Los patrones nos ayudan a modificar las clases en estas circunstancias. Patrones de diseño: adaptador, decorador, visitor.
2. 2.1.
Elementos y componentes software: objetos, clases, herencia, métodos, sobrecarga Evolución de los objetos
Las ideas básicas sobre los objetos, nacen a principios de los años setenta en la universidad de Noruega donde un equipo dirigido por el Dr. Nygaard, se dedicaba a desarrollar sistemas informáticos para realizar simulaciones de sistemas físicos. Debido a que eran programas muy complejos y el mantenimiento era muy necesario (para que el software se adaptara a nuevas necesidades), se dieron cuenta de las limitaciones de la ingeniería de software tradicional, para solucionar este problema idearon una forma de diseñar el programa paralelamente al objeto físico, donde cada componente del objeto físico se correspondía con un componente de “software”, con lo que se simplificaba el programa y, por tanto, el mantenimiento exigía menor esfuerzo. Lo anterior trajo consigo otro beneficio como es la reutilización del código, hecho que por sí mismo repercute en una baja en el costo del “software” y en el tiempo requerido para el desarrollo de sistemas. El primer lenguaje que implementó estas ideas fue el lenguaje SIMULA-67. Luego, en la década de los 70, XEROX en sus laboratorios de Palo Alto desarrolla SMALL-TALK. En los años 80 tomando ideas de Simula y de Small-Talk, en los laboratorios Bell de ATT, Stroustrup crea el lenguaje C++ como sucesor del Lenguaje C, y a éste se debe la gran extensión de los conceptos de objetos. En el área de la inteligencia artificial, se desarrolla Clos, una variante de Lisp orientada a objetos. En sistemas operativos, el Next-Step de Sun es un Sistema Operativo Orientado a objetos. Microsoft trabaja en Cairo, IBM y Apple trabajan en Pink, como sistemas operativos que incluyen conceptos de objetos. En las bases de datos, tenemos al SNAP (Strategic Networed Applications Plataform), conocido en español como Sistemas distribuidos en línea orientados a objetos. Poco más tarde (1993-4) Sun crea JAVA, el cual arrasa hasta la actualidad en aplicaciones cliente y en programas ejecutables para Internet (applets).
5-13
Desarrollo de Sistemas
Los conceptos de objetos entran en profundidad poco a poco en todos los ámbitos de la computación, y de la misma manera que ha sido inevitable aprender programación estructurada o bases de datos relacionales, ahora se hace necesario aprender programación orientada a objetos. Los conceptos de análisis, diseño y programación con objetos son fáciles de dominar una vez que se tiene una base de programación en un lenguaje. Para poder construirnos nuestras propias librerías de clases o para llegar a ser un programador de objetos de alto rendimiento, se requiere un poco mas de práctica. En el mercado existen bibliotecas de clases, y también varios lenguajes traen bibliotecas de clases que le permiten al programador realizar ciertas tareas sin tener que programarlas. No obstante, no debemos confundir Programación Orientada a Objetos donde el programador puede usar clases precreadas o crearlas el mismo con Programación Basada en Objetos donde las clases ya están precreadas y el programador solo puede usarlas o modificarlas. Ejemplos de lenguajes de Programación Basada en Objetos son: Clipper que tiene objetos ya creados como el Tbrowse que permite el manejo de tablas y Visual Basic al igual que Delphi tienen objetos como botones o cuadros de diálogo con los que permite desarrollar interfaces de usuario con un mínimo de programación.
Nota. Hemos de destacar que Visual Basic .NET es también un lenguaje de última generación Orientado a Objetos.
2.2.
La programación orientada a objetos (POO-OOP)
La idea principal de POO es construir programas que utilizan objetos de software. Un objeto puede considerarse como una entidad independiente de cómputo con sus propios datos y programación. En computadoras modernas, las ventanas, los menús y las carpetas de archivos, por ejemplo, suelen representarse con objetos de “software”. Pero los objetos pueden aplicarse a muchos tipos de programas Se incluirían datos que describen los atributos físicos, y programación (métodos), que gobierna la manera en que funciona internamente y en que interactúa con otras partes relacionadas. En contraste, la programación tradicional trabajaba con “bytes”, variables, matrices, índices y otros utensilios de programación que resultaba difícil relacionar con el problema actual. Además, la programación tradicional se concentra en los procedimientos paso a paso, llamados algoritmos, para realizar las tareas deseadas. Por esta razón, a la programación tradicional también se le conoce como programación orientada a procedimientos. Todos los lenguajes de programación están formados por dos elementos: código y datos. Cuando los programas empezaron a hacerse complicados y su código es enorme y casi inmanejable, se pensó crear una nueva forma de pensar de la cual surgió la programación orientada a objetos. En ella un programa se organiza entorno a sus datos (objetos) y a un conjunto de interfaces bien definidas para esos datos. La POO es un modelo de programación que utiliza objetos ligados mediante mensajes para la resolución de problemas. La idea inicial siempre ha sido organizar los programas a imagen y semejanza de la organización de los objetos en el mundo real. 5-14
Diseño y programación orientada a objetos
Las técnicas orientadas a objetos se basan en organizar el software como una colección de objetos discretos que incorporan tanto estructuras de datos como comportamiento. Esto contrasta con la programación convencional, en la que las estructuras de datos y el comportamiento estaban escasamente relacionadas. Las características principales del enfoque orientado a objetos serán estudiadas con detenimiento en este tema y son: —
Abstracción.
—
Encapsulación.
—
Herencia.
—
Polimorfismo.
—
Clase.
—
Objeto.
—
Método.
—
Mensajes.
—
Identidad.
—
Reutilización.
—
Jerarquía.
—
Concurrencia.
—
Modularidad.
A partir de estos elementos fundamentales, trataremos de dar un enfoque tanto estructurado como también un enfoque orientado a objetos. Un objeto en el mundo real tiene una apariencia, peso, volumen y se puede definir por la función que realiza. Tiene por tanto un conjunto de características (atributos) que describen su naturaleza y funcionalidad. Un objeto es cualquier cosa, real o abstracta, en la cual almacenamos datos y los métodos que controlan dichos datos, pongamos la vista enfrente y estaremos rodeados de objetos. La teoría de los objetos puede ser aplicada a cualquier sistema, por que la organización de éstos (objetos) es lo que define al sistema, ya que éste posee atributos y características individuales que lo hacen organizacional, desde su denominación en sí, su clase, su poliformismo, el proceso de encapsulación, la herencia que sucede a otro objeto, los mensajes que de algún modo llevan a cabo que se realice una operación, el método como se hace, su identidad, que lo difiere de los demás, su reutilización, el orden jerarquizado, su abstracción, modularidad y, por ultimo, su concurrencia. 5-15
Desarrollo de Sistemas
La solución para tratar con la complejidad típica de los programas es “Divide y vencerás”: —
Descomposición Algorítmica (top-down o estructural): Rompe el sistema en partes, cada una representando un pequeño paso del proceso. Métodos de diseño estructurados conducen a descomposiciones algorítmicas, donde la atención se centra en el flujo del sistema.
—
Descomposición orientada a objetos: Trata de identificar semánticamente el dominio del problema. El entorno del problema se estudia como un conjunto de agentes autónomos (objetos) que colaboran para realizar un comportamiento complejo. Algorítmica.
Orientada a objetos.
Diagramas tipo árbol.
Varias posibilidades.
Desmenuza el problema.
Identifica semánticamente el problema.
Se programa en detalle.
Se programa a lo grande.
Lenguajes imperativos.
Lenguajes declarativos.
En cuanto a las características de las descomposiciones algorítmica y orientada a objetos hemos de diferenciar tres términos usados en la orientación a objetos: —
Análisis Orientado a Objetos: es un método de análisis que examina los requerimientos desde la perspectiva de clases y objetos encontrada en el vocabulario original del problema.
—
Diseño Orientado a Objetos: es un método de diseño que abarca el proceso de descomposición orientado a objetos y una notación para describir modelos lógicos y físicos, dinámicos y estáticos, del sistema bajo diseño.
—
Programación Orientada a Objetos: es el método de implementación en el cual los programas se organizan como colecciones cooperantes de objetos, cada uno de los cuales representa un ejemplo de alguna clase, y cuyas clases son todas miembros de una jerarquía de clases unidas por relaciones.
El concepto renovador de la tecnología de POO es la anexión de procedimientos de programas a elementos de datos. Esta idea cambia la separación tradicional entre datos y programas. A esta nueva unión se le llama encapsulamiento y el resultado es un objeto de software. En JAVA, por ejemplo, todos los procedimientos están encapsulados y se les llama métodos. Por ejemplo, un objeto de ventana en un sistema de interfaz gráfica del usuario contiene las dimensiones físicas de la ventana, la ubicación en la pantalla, los colores de primer plano y de fondo, los estilos de borde y otros datos relevantes. Encapsulados junto con estos datos, se encuentran los métodos
5-16
Diseño y programación orientada a objetos
para mover y modificar el tamaño de la propia ventana, para cambiar sus colores, para desplegar texto, para reducirlo a un icono, etc. Otras partes del programa de interfaz del usuario sólo llaman a un objeto de ventana para realizar estas tareas enviándole mensajes bien definidos. El trabajo de un objeto de ventana consiste en realizar las acciones apropiadas y mantener actualizados sus datos internos. Para programas fuera del objeto no importa mucho la manera exacta en que se realizan estas tareas ni las estructuras de los datos internos. La interfaz pública formada por los diferentes tipos de mensajes que envía un objeto, definen por completo la manera de usarlo. Ésta es la interfaz de programación de aplicaciones (API, Application Programming Interface) del objeto. El ocultamiento de detalles internos hace que un objeto sea abstracto. La separación entre la interfaz pública y el funcionamiento interno no es difícil de comprender. Por ejemplo, cuando montamos en un coche, pensamos en un medio de transporte que nos lleve de un lugar a otro. No pensamos en un masijo de hierros, plástico, etc. Además, si montamos en el coche del vecino sabremos conducirlo igualmente pues su forma de utilización es la misa. Su funcionamiento interno lo dejamos para cuando falla y tenemos que pagarle al taller. Cuando se ejecuta un programa, los objetos se crean, los mensajes se envían y los objetos se destruyen. Éstas son las únicas operaciones permisibles sobre ellos. Los datos o los métodos internos (privados) de un objeto están fuera de los límites del público. El desacoplamiento de los mecanismos privados de los objetos de las rutinas externas a ellos, reducen en gran medida la complejidad de un programa. En POO se define una clase para cada tipo diferente de objeto. Se utilizan una definición de clase y valores iniciales apropiados para crear una instancia (objeto) de la clase. A esta operación se le conoce como instanciación de objetos. La tecnología de POO necesita formas fáciles para construir objetos sobre otros, para eso existen dos métodos principales, composición y herencia. El primero permite que objetos existentes se utilicen como componentes para construir otros. Por ejemplo, un objeto de calculadora puede estar compuesto por otro de unidad aritmética y uno más de interfaz de usuario. La herencia es una función importante de POO que le permite ampliar y modificar clases existentes sin cambiar su código. Una subclase hereda código de su superclase y, también, agrega sus propios datos y métodos. La herencia permite la extracción de elementos comunes entre objetos similares o relacionados. También permite que se utilicen clases de bibliotecas de software para muchos propósitos diferentes o no previsibles. Heredar de una clase se conoce como herencia simple y heredar de varias clases se conoce como herencia múltiple. Además, POO permite el polimorfismo, que es la capacidad de un programa para trabajar con diferentes objetos. Se permite la creación de objetos compatibles que son transferibles. La modificación y el mejoramiento de un programa polimórfico puede ser tan sólo cuestión de enlazar objetos actualizados. Una clase es como el plano de los objetos. Describe las estructuras de datos del objeto y sus operaciones asociadas. Una vez que se ha definido una clase, es posible declarar los objetos que le pertenecen y utilizarlos en un pro5-17
Desarrollo de Sistemas
grama. Por lo general una clase contiene miembros que pueden ser campos y métodos. Los primeros son variables que almacenan datos y objetos. Los segundos son funciones que codifican operaciones. Es así que ellos reciben argumentos, realizan cálculos predefinidos y devuelven resultados. Un mensaje enviado a un objeto invoca un método de ese objeto, le pasa argumentos y obtiene el valor que devuelve. Los objetos interactúan al enviar y recibir mensajes. Una clase proporciona el nombre bajo el que se reúnen los miembros para formar una unidad de cálculo que puede operar con independencia de otras partes del programa. Con objetos, puede construirse un programa grande con muchas unidades pequeñas, independientes y que interactúan entre sí. La orientación a objetos puede reducir significativamente la complejidad del programa, aumentar su flexibilidad y mejorar las posibilidades de volver a usarlo. Un programa puede definir sus propias clases, utilizar las precreadas (generalmente guardadas en bibliotecas de clases) y emplear las que han sido creadas por otros programadores. Las clases pueden estar organizadas en paquetes con un nombre. Cada paquete puede contener uno o más archivos de código fuente. Veamos a continuación cada uno de los concepto clave de la programación orientada a objetos en profundidad.
2.3.
La abstracción
La abstracción es una de las bases de la POO. Desde siempre el programador ha intentado abstraerse y no ver el programa como un conjunto complejo de código. Se pretender ignorar los detalles y obtener una visión en su conjunto. Una forma de obtener una buena abstracción es utilizando clasificaciones jerárquicas. En las clasificaciones jerárquicas primero vemos el sistema desde su exterior. Luego, en un segundo nivel, nos vamos adentrando en él y viendo los subsistemas, en un tercer nivel nos adentramos en cada uno de los subsistemas, etc.
Ejemplo. Un camión: Si nos abstraemos lo vemos exteriormente como un objeto de grandes dimensiones y con gran capacidad de carga que puede transportar de un lugar a otro. El camión lo podemos dividir en cabina, remolque, usos, coste, etc. (subsistemas). Dentro de la cabina vemos que es un habitáculo donde el conductor se acomoda y controla el camión. A su vez dentro del subsistema cabina tenemos: los asientos, la radio, controles, etc. Cada uno de ellos con una función delimitada. Dentro de cada uno de ellos, … Esta abstracción jerárquica de sistemas en subsistemas se puede aplicar a los programas que el programador crea y de los datos tradicionales obtenemos su abstracción en objetos. Cada uno de estos objetos tiene un comportamiento y funcionalidad propio, que se pueden tratar como entidades inde5-18
Diseño y programación orientada a objetos
pendientes y que responden a mensajes (secuencia de pasos de un proceso) que les dicen lo que tienen que hacer y en qué orden. Una abstracción denota las características esenciales de un objeto que lo distinguen de todos los demás tipos de objetos, y proporciona así fronteras conceptuales definidas con nitidez, desde la perspectiva del observador. Todo objeto es único. Sin embargo, la abstracción elimina algunas distinciones para que podamos ver los aspectos comunes entre los objetos. La abstracción es una de las vías fundamentales por la que los humanos podamos combatir la complejidad. Una abstracción se centra en la visión externa de un objeto y, por lo tanto, sirve para separar el comportamiento esencial de un objeto de su implantación. Sin la abstracción sólo sabríamos que cada objeto es diferente de los demás, con ella se omiten de manera selectiva varias características distintivas de uno o más objetos, lo que permite concentrarnos en las características que comparten. Para hacerlo más entendible, diremos que la abstracción: es el acto o resultado de eliminar diferencias entre los objetos, de modo que podamos ver los aspectos más comunes. La abstracción denota las características esenciales que distinguen a un objeto de otros tipos de objetos, definiendo precisas fronteras conceptuales, relativas al observador. Las características de la abstracción son:
2.4.
—
Surge del reconocimiento de similitudes entre ciertos objetos, situaciones o procesos en el mundo real.
—
Decide concentrarse en estas similitudes e ignorar las diferencias.
—
Enfatiza detalles con significado para el usuario, suprimiendo aquellos detalles que, por el momento, son irrelevantes o distraen de lo esencial.
—
Deben seguir el “principio de mínimo compromiso”, que significa que la interface de un objeto provee su comportamiento esencial, y nada más que eso. Pero también el “principio de mínimo asombro”: capturar el comportamiento sin ofrecer sorpresas o efectos laterales.
El encapsulado
Se trata de un mecanismo que permite juntar el código y los datos y que mantiene a ambos alejados de posibles usos indebidos. Para ello el acceso al código y a los datos se realiza de forma controlada a través de una interfaz bien definida. El encapsulado permite que se realice la migración de las implementaciones tras el paso del tiempo, sin necesidad de reescribir de nuevo todo el código (reutilización o reusibilidad).
Ejemplo. Estás en tu coche. Si pisas el freno lo que debe de hacer el coche es frenar y no debe de activarse el parabrisas, ni acelerar, ni aumentar el volumen de la radio, etc. Eso sucede gracias a que el sistema de frenado está perfectamente definido y funciona como un sistema independiente y con una 5-19
Desarrollo de Sistemas
función muy definida. Puede comunicarse con otros sistemas como las luces para que se activen las luces de frenado, pero esta comunicación está perfectamente definida y aunque no funcionen las luces el coche ha de frenar igualmente. Igualmente, si el coche no frena el fallo es del sistema de frenado y no de otro sistema. Con el encapsulado lo único que debemos conocer del sistema es como acceder a él, sin preocuparnos de los detalles internos (abstracción) y estamos seguros de que no se van a producir efectos secundarios imprevistos. Por tanto, el encapsulado consiste en ocultar los detalles de instrumentación de un objeto, a la vez que se provee de una interfaz pública por medio de sus operaciones permitidas. En los modelos orientados a objetos, lo que realmente nos importa es el comportamiento de los objetos y no cómo está instrumentado ese comportamiento. Así, si la instrumentación de un objeto cambia pero su interfaz se mantiene igual, los objetos que interactúan con él no se verán afectados por esos cambios. Además, el encapsulamiento oculta la complejidad de la instrumentación. El encapsulamiento es el proceso de compartimentar los elementos de una abstracción que constituyen su estructura y su compartimiento. Dicho de otro modo, cada objeto es una estructura compleja en cuyo interior hay datos y código, todos ellos relacionados entre sí, como si estuvieran encerrados conjuntamente en una cápsula. Es, a esta propiedad, a lo que se denomina encapsulamiento. Se puede definir como el proceso de almacenar en un mismo compartimiento los elementos de una abstracción que constituyen su estructura y su comportamiento; sirve para separar la interfaz contractual de una abstracción y su implantación. El hecho de cada objeto sea una cápsula facilita enormemente que un objeto determinado pueda ser transportado a otro punto de la organización, o incluso a otra organización totalmente diferente que precise de él. Si el objeto ha sido bien construido, sus métodos seguirán funcionando en el nuevo entorno sin problemas. Esta cualidad hace que la POO sea muy apta para la reutilización de programas. El encapsulamiento es importante porque separa el comportamiento del objeto de su implantación. Esto permite la modificación de la implantación del objeto sin que se tengan que modificar las aplicaciones que lo utilizan. La encapsulación sirve para separar la interface de una abstracción y su implementación. —
Es un concepto complementario al de abstracción.
—
La encapsulación esconde la implementación del objeto que no contribuye a sus características esenciales.
—
La encapsulación da lugar a que las clases se dividan en dos partes:
—
5-20
1.
Interface: captura la visión externa de una clase, abarcando la abstracción del comportamiento común a los ejemplos de esa clase.
2.
Implementación: comprende la representación de la abstracción, así como los mecanismos que conducen al comportamiento deseado.
Se conoce también como ocultamiento o privacidad de la información.
Diseño y programación orientada a objetos
Como hemos visto, cada objeto es una estructura compleja en cuyo interior hay datos y programas, todos ellos relacionados entre sí, como si estuvieran encerrados conjuntamente en una cápsula. Esta propiedad (encapsulamiento), es una de las características fundamentales en la OOP. No obstante, los objetos son inaccesibles, e impiden que otros objetos, los usuarios, o incluso los programadores conozcan cómo está distribuida la información o qué información hay disponible. Esta propiedad de los objetos se denomina ocultación de la información. Esto no quiere decir, sin embargo, que sea imposible conocer lo necesario respecto a un objeto y a lo que contiene. Si así fuera no se podría hacer gran cosa con él. Lo que sucede es que las peticiones de información a un objeto deben realizarse a través de mensajes dirigidos a él, con la orden de realizar la operación pertinente. La respuesta a estas órdenes será la información requerida, siempre que el objeto considere que quien envía el mensaje está autorizado para obtenerla. El hecho de que cada objeto sea una cápsula facilita enormemente que un objeto determinado pueda ser transportado a otro punto de la organización, o incluso a otra organización totalmente diferente que precise de él. Si el objeto ha sido bien construido, sus métodos seguirán funcionando en el nuevo entorno sin problemas. Esta cualidad hace que la POO sea muy apta para la reutilización de programas. Otra forma de ver su definición: Una de las características centrales de POO es la partición del programa completo en pequeñas entidades autónomas, llamadas objetos, con interacciones bien definidas. Esta característica reduce significativamente la complejidad global y ayuda a la calidad del programa de diferentes maneras. Un objeto organiza datos y operaciones relacionadas en una caja negra, que oculta las estructuras de datos, las representaciones y el código interno de la vista exterior. Una estructura de datos es concreta cuando se conocen sus elementos exactos. Los métodos de programación tradicional dependen mucho de datos concretos. Una estructura de datos es abstracta, si sólo conocemos su comportamiento y no sus detalles de implantación. Por tanto, la abstracción de datos pone el énfasis en el ocultamiento de los detalles internos de los datos y sólo presenta su comportamiento externo. Por ejemplo, sin conocer los detalles de la construcción de un automóvil, podemos manejarlo con sólo conocer comportamientos como: “al girar el volante en el sentido de las manecillas del reloj se gira a la derecha”. Esto deja la implantación de la conducción a la caja negra, que puede utilizar una de las varias alternativas de dirección: normal, hidráulica, de engranes, etc. Además, un objeto también contiene mecanismos o códigos que son necesarios para operar las estructuras de datos que están adjuntos a los códigos para formar una unidad inseparable. A esta técnica se le llama encapsulación. En POO el comportamiento externo de un objeto es un contrato de interfaz entre el objeto y sus clientes (los programas que utilizan el objeto). Este convenio contiene datos/operaciones que quedan disponibles para los clientes externos a partir de un objeto y que documenta su significado preciso.
5-21
Desarrollo de Sistemas
2.5.
La herencia
La herencia es el proceso mediante el cual un objeto adquiere las propiedades de otro. Gracias a la herencia se consigue llevar a cabo la idea vista anteriormente de clasificación jerárquica. Si una clase dada encapsula algunos atributos entonces cualquier clase “hija” de la anterior (subclase) heredará los atributos de la anterior más los que se le quieran definir. La herencia nos da reusabilidad de código. El concepto de herencia se refiere a la comparición de atributos y operaciones basadas en una relación jerárquica entre varias clases. Una clase pude definirse de forma general y luego redefinirse en sucesivas subclases. Cada clase hereda todas las propiedades de sus superclases y añade sus propiedades particulares.
Ejemplo 1. Los seres vivos tienen una serie de propiedades. Los animales al ser seres vivos heredan esas propiedades a las cuales añaden otros. Los mamíferos al ser animales, heredan todos los atributos de los animales (algunas de las cuales heredaban de los seres vivos) a los cuales añaden otros. Los humanos, como animales, etc. Ejemplo 2. Imagínate que tienes un molde para hacer bicicletas sin marchas. Has hecho miles de bicicletas y todo va bien. De repente deseas hacer bicicletas con marchas. ¿Qué te parece mas apropiado: crear un nuevo molde (es decir, crear de nuevo con todo lo que ello conlleva), o coger el molde anterior y acoplarle las marchas? Ésta es la idea de la herencia, a partir de un sistema creado se crea uno nuevo acoplándole nuevos atributos. La herencia es el medio por el cual los objetos de una clase pueden acceder a variables y funciones miembro contenidas en una clase previamente definida, sin tener que volver a realizar esas definiciones. Existen dos tipos de herencia: A) Herencia simple La herencia simple es aquella en la que una clase puede heredar la estructura de datos y operaciones de una superclase. Es una relación entre clases en la que una clase comparte la estructura o el comportamiento definido en una. B) Herencia múltiple La herencia múltiple se da cuando una clase puede heredar la estructura de datos y operaciones de más de una superclase. Es la relación entre clases en la que una clase comparte la estructura demás de una clase base. La herencia múltiple presenta una gran dificultad y es el hecho que puede heredar dos operaciones con el mismo nombre. Esto hace que las colisiones pueden introducir ambigüedad en el comportamiento de la subclase que hereda en forma múltiple.
Nota. El lenguaje C++ permite la herencia múltiple, pero JAVA no lo permite.
5-22
Diseño y programación orientada a objetos
Es una de las propiedades más destacadas de la POO, se puede definir como un mecanismo que define nuevos objetos con base en los existentes. Lenguajes de programación como C++ y JAVA la soportan con extensión de clase, definiendo una nueva clase con base en otra ya existente sin modificarla. A la primera se le llama subclase o clase extendida, recibe a los miembros de una superclase y agrega otros de su propiedad. Aunque también es posible que la herede dado el surgimiento de jerarquías. JAVA soporta herencia sencilla a través de extensión de clase. En este tipo de herencia una clase se extiende a partir de una sola superclase. Por ejemplo, una subclase puede extenderse de la superclase. Algunos lenguajes POO, como C++, soportan herencia múltiple porque una subclase puede tener varias superclases. Al anular la herencia múltiple, JAVA evita las dificultades y complicaciones relacionadas con este tipo de herencia. Si una clase se designa final no puede extenderse. El uso experto de la herencia a través de subclases contribuye en gran medida a un programa bien diseñado. Varias son las ventajas de la herencia, entre las cuales citamos:
2.6.
—
Fácil modificación de código: evita la modificación del código existente y utiliza la herencia para agregar nuevas características o cambiar características existentes.
—
Reutilización de código existente: usando la herencia como base de código que funciona y está probado podemos crear nuevas clases fácilmente.
—
Adaptación de programas para trabajar en situaciones similares pero diferentes: evita que se vuelvan a escribir programas muy similares porque la aplicación, el sistema de computadora, el formato de datos o el modo de operación es un poco diferente.
—
Extracción de elementos comunes de clases diferentes: evita que se dupliquen códigos; y estructuras idénticas o similares de clases diferentes. Se extraen las partes comunes para formar otra clase y permita que las demás las hereden.
—
Organización de objetos en jerarquías: forma grupos de objetos que tienen una relación. Las agrupaciones le dan una mejor organización a un programa y permite que los objetos de la misma jerarquía se utilicen como tipos compatibles, en oposición a los que carecen totalmente de relación.
El poliformismo
El polimorfismo que en griego significa “muchas formas” es una característica que le permite a una interfaz ser utilizada por una clase general de acciones. La frase “una interfaz, muchas formas” resume la idea de polimorfismo. El polimorfismo permite que se cree un código limpio, sensible, legible y resistente.
Ejemplo. Para abrir un archivo en Windows se pincha dos veces en su icono. Para ver el contenido de una carpeta también se pincha dos veces sobre su icono. Es decir, en ambos casos hemos pinchado dos veces para ejecutar la acción correspondiente. La interfaz ha sido la misma aunque con dos formas diferentes. 5-23
Desarrollo de Sistemas
Este concepto reduce la complejidad permitiendo que la misma interfaz se utilice para especificar una clase general de acción. Es el compilador o el intérprete el que debe seleccionar la acción específica a ejecutar. El polimorfismo se define como la posibilidad de asumir varias formas. Permite que una misma operación pueda llevarse a cabo de varias formas, en clases diferentes. Desde este punto de vista, representa un concepto de teoría de tipos en el que un solo nombre puede denotar objetos de muchas clases diferentes que se relacionan por alguna superclase común. Cualquier objeto denotado por este nombre es, por lo tanto, capaz de responder a algún conjunto común de operaciones. Una operación es una acción o transformación que realiza o padece un objeto. La implementación específica de una operación determinada a una clase determinada se denomina método. Aunque los métodos sean distintos, llevan a cabo el mismo propósito operativo, y así estaríamos hablando también, de polimorfismo. Según lo dicho, una operación es una abstracción de un comportamiento similar (pero no idéntico) en diferentes clases de objetos. La semántica de la operación debe ser la misma para todas las clases. Sin embargo, cada método concreto seguirá unos pasos específicos. Existe el polimorfismo cuando interactúan las características de la herencia y el enlace dinámico. Ésta es quizás la característica más importante de los lenguajes orientados a objetos después de su capacidad para soportar la abstracción y es lo que distingue la programación orientada a objetos de otra programación más tradicional con tipos abstractos de datos. El polimorfismo es también un concepto central en el diseño orientado a objetos. Una de las ventajas del polimorfismo es que se puede hacer una solicitud de una operación sin conocer el método que debe ser llamado. No es otra cosa que la posibilidad de construir varios métodos con el mismo nombre, pero con relación a la clase a la que pertenece cada uno, con comportamientos diferentes. Esto conlleva la habilidad de enviar un mismo mensaje a objetos de clases diferentes. Estos objetos recibirían el mismo mensaje global pero responderían a él de formas diferentes. El polimorfismo, encapsulado y herencia son quizás los tres principios clave de la programación orientada a objetos. Cuando se aplican conjunta y adecuadamente producen un entorno de programación que admite el desarrollo de programas robustos y más fáciles de ampliar que los modelos tradicionales de diseño orientado al proceso. Permiten la reutilización de código y un menor coste de programación.
2.7.
Clase
La clase es en núcleo de la POO. Se trata de una construcción lógica sobre la que se construye la orientación a objetos. En la clase se define la forma y la naturaleza de un objeto. Una clase define un nuevo tipo de dato que se utiliza para crear objetos de ese tipo. Dicho de otra forma: una clase es el molde a partir del cual se fabrican objetos.
5-24
Diseño y programación orientada a objetos
Una clase está formada por miembros de datos que a su vez son variables de instancia (datos) y métodos. Una clase es, por tanto, una agrupación de datos (variables o campos) y de funciones (métodos) que operan sobre esos datos. A estos datos y funciones pertenecientes a una clase se les denomina variables y métodos o funciones miembro. La programación orientada a objetos se basa en la programación de clases. Un programa se construye a partir de un conjunto de clases. Una vez definida e implementada una clase, es posible declarar elementos de esta clase. Los elementos declarados de una clase se denominan objetos de la clase. De una clase se pueden declarar o crear numerosos objetos. La clase es lo genérico: es el patrón o modelo para crear objetos. El cuerpo de la clase, encerrado entre { }, es la lista de atributos (variables) y métodos (funciones) que constituyen la clase. No es obligatorio, pero en general se listan primero los atributos y luego los métodos. La definición de una clase se realiza en la siguiente forma:
[ ] class nombredelaclase [datos] { [lista_de_atributos] [lista_de_métodos] } El esqueleto de cualquier aplicación JAVA se basa en la definición de una clase. Todos los datos básicos, como los enteros, se deben declarar en las clases antes de hacer uso de ellos. En la práctica son pocas las sentencias que se pueden colocar fuera del bloque de una clase. La palabra clave import (equivalente al #include) puede colocarse al principio de un fichero, fuera del bloque de la clase. Sin embargo, el compilador reemplazará esa sentencia con el contenido del fichero que se indique, que consistirá, como es de suponer, en más clases. Los tipos de clases que podemos definir son: —
Abstract. Una clase abstract tiene al menos un método abstracto. Una clase abstracta no se instancia (no se puede crear ningún objeto de ella), sino que se utiliza como clase base para la herencia.
—
Final. Una clase final se declara como la clase que termina una cadena de herencia. No se puede heredar de una clase final. Por ejemplo, la clase Math es una clase final.
—
Public. Las clases public son accesibles desde otras clases, bien sea directamente o por herencia. Son accesibles dentro del mismo
5-25
Desarrollo de Sistemas
paquete en el que se han declarado. Para acceder desde otros paquetes, primero tienen que ser importadas. —
Synchronizable. Este modificador especifica que todos los métodos definidos en la clase son sincronizados, es decir, que no se puede acceder al mismo tiempo a ellos desde distintos threads; el sistema se encarga de colocar los flags necesarios para evitarlo. Este mecanismo hace que desde threads diferentes se puedan modificar las mismas variables sin que haya problemas de que se sobrescriban.
Citaremos ahora algunas de las normas de las clases en JAVA: 1.
Todas las variables y funciones de JAVA deben pertenecer a una clase. No hay variables y funciones globales.
2.
Si una clase deriva de otra (extends), hereda todas sus variables y métodos.
3.
JAVA tiene una jerarquía de clases estándar de la que pueden derivar las clases que crean los usuarios.
4.
Una clase sólo puede heredar de una única clase (en JAVA no hay herencia múltiple). Si al definir una clase no se especifica de qué clase deriva, por defecto la clase deriva de Object. La clase Object es la base de toda la jerarquía de clases de JAVA. En C++ sí se permite la herencia múltiple.
5.
En un fichero se pueden definir varias clases, pero en un fichero no puede haber más que una clase public. Este fichero se debe llamar como la clase public que contiene con extensión *.java. Con algunas excepciones, lo habitual es escribir una sola clase por fichero.
6.
Si una clase contenida en un fichero no es public, no es necesario que el fichero se llame como la clase.
7.
Los métodos de una clase pueden referirse de modo global al objeto de esa clase al que se aplican por medio de la referencia this.
8.
Las clases se pueden agrupar en “packages”, introduciendo una línea al comienzo del fichero (package packageName;).Esta agrupación en packages está relacionada con la jerarquía de directorios y ficheros en las que se guardan las clases.
El término clase se refiere a la implantación en software de un tipo de objeto. Especifica una estructura de datos y los métodos operativos permisibles que se aplican a cada uno de los objetos. Una clase puede tener sus propios métodos y estructura de datos, así como también heredarlos de su superclase. La superclase es la clase de la cual hereda otra clase, llamada esta última subclase inmediata. Una clase es una abstracción de un conjunto posiblemente infinito de objetos individuales. Cada uno de estos objetos se dice que es una instancia o ejemplar de dicha clase. Cada instancia de una clase posee sus propios valores para sus atributos, pero comparte el nombre de estos atributos y las opera5-26
Diseño y programación orientada a objetos
ciones con el resto de instancias de su clase. La elección de clases es arbitraria, y depende del dominio del problema.
Nota. La industria utiliza el término clase para hacer referencia a las implantaciones de los tipos de objetos. Se construyen clases a partir de otras clases, las cuales a su vez se integran mediante clases. Así, como los bienes manufacturados se fabrican a partir de una serie de materiales de partes y subpartes ya existentes, también el software se crea mediante una serie de materiales de clases ya existentes y probadas. Una clase es un conjunto de objetos que comparten una estructura y comportamiento comunes. —
Clase representa una abstracción, la esencia que comparten los objetos.
—
Un objeto es un ejemplo de una clase.
—
Un objeto no es una clase, y una clase no es un objeto (aunque puede serlo, p.e. en Smalltalk).
—
Las clases actúan como intermediarias entre una abstracción y los clientes que pretenden utilizar la abstracción. De esta forma, la clase muestra: 1.
Visión externa de comportamiento (interface), que enfatiza la abstracción escondiendo su estructura y secretos de comportamiento.
2.
Visión interna (implementación), que abarca el código que se ofrece en la interface de la clase.
2.7.1. Relaciones entre clases Representan tipos de compartición entre clases, o relaciones semánticas. 1.
Asociación. Indica relaciones de mandato bidireccionales (punteros ocultos en C++). Conlleva dependencia semántica y no establece una dirección de dependencia. Tienen cardinalidad.
2.
Herencia. Por esta relación una clase (subclase) comparte la estructura o comportamiento definidos en una (herencia simple) o más (herencia múltiple) clases, llamadas superclases: —
Representa una relación del tipo “es un” entre clases.
—
Una subclase aumenta o restringe el comportamiento o estructura de la superclase (o ambas cosas).
—
Una clase de la que no existen ejemplos se denomina {\it abstracta}.
5-27
Desarrollo de Sistemas
—
C++ declara como virtuales todas aquellas funciones que quiere modificar en sus subclases.
3.
Agregación. Representa una relación del tipo “tener un” entre clases. Cuando la clase contenida no existe independientemente de la clase que la contiene se denomina agregación por valor y además implica contenido físico, mientras que si existe independientemente y se accede a ella indirectamente, es agregación por referencia.
4.
Uso. Es un refinamiento de la asociación donde se especifica cuál es el cliente y cuál el servidor de ciertos servicios, permitiendo a los clientes acceder sólo a las interfaces públicas de los servidores, ofreciendo mayor encapsulación de la información.
5.
Ejemplificación. Se usa en lenguajes que soportan genericidad (declaración de clases parametrizadas y argumentos tipo “template”). Representa las relaciones entre las clases parametrizadas, que admiten parámetros formales, y las clases obtenidas cuando se concretan estos parámetros formales, ejemplificados o inicializados con un ejemplo.
6.
Metaclases. Son clases cuyos ejemplos son a su vez clases. No se admiten en C++.
2.7.2. Relaciones entre clases y objetos
0 0 más objetos.
—
Todo objeto es el ejemplo de una clase, y toda clase tiene
—
Mientras las clases son estáticas, con semántica, relaciones y existencia fijas previamente a la ejecución de un programa, los objetos se crean y destruyen rápidamente durante la actividad de una aplicación.
El diseño de clases y objetos es un proceso incremental e iterativo. Debe asegurar la optimización en los parámetros:
5-28
1.
Acoplamiento: Grado de acoplamiento entre módulos.
2.
Cohesión: Mide el grado de conectividad entre elementos de un módulo, y entre objetos de una clase.
3.
Suficiencia: Indica que las clases capturan suficientes características de la abstracción para conseguir un comportamiento e interacción eficiente y con sentido.
4.
Completitud: Indica que la interface de la clase captura todo el significado característico de una abstracción, escrito en el mínimo espacio.
5.
Primitividad: Las operaciones deben implementarse si dan acceso a una representación fundamental de la abstracción. Cuáles son operaciones primitivas y cuáles no (se pueden realizar a partir de otras) es un asunto subjetivo y afecta a la eficiencia en la implementación.
Diseño y programación orientada a objetos
2.8.
Objeto
La construcción clase (class) de JAVA soporta abstracción y encapsulación de datos. Una clase describe la construcción de un objeto y sirve como plano para construirlo; especifica su funcionamiento interno y su interfaz pública. Cada clase tiene un nombre y especifica a los miembros que pertenecen a ella; éstos pueden ser campos (datos) y métodos (funciones). Una vez que se define una clase, su nombre se vuelve un nuevo tipo de dato y se usa para declarar variables de ese tipo y crear objetos de ese tipo. Una vez que se ha declarado una clase pueden crearse sus objetos. La definición de una clase es el plano de construcción de los objetos, y éstos se conocen como instancias de la clase. El nombre de ésta se vuelve un nombre de tipo y puede utilizarse para declarar variables. Una variable de tipo de clase es una variable de referencia que puede contener la dirección de memoria (o referencia) de un objeto de la clase o null para una referencia no válida. La inicialización del objeto creado de una clase se hace por medio del constructor (un método especial de una clase). El operador new de JAVA asigna espacio dinámicamente (al momento de la ejecución) y se utiliza para crear objetos. En lenguajes como JAVA, todos los objetos se crean al momento de la ejecución con el operador new. Veamos un ejemplo: Tenemos una clase llamada cochazo y queremos crear un objeto llamado mi coche de esa clase: cochazo mi_coche = new cochazo();
o cochazo mi_coche; mi_coche = new cochazo();
El objeto es un concepto, una abstracción o una cosa con unos límites definidos y que es relevante para el tema en cuestión, podemos decir además que estos poseen identidad y son distinguibles, aunque dos objetos tengan los mismos valores para todos, sus atributos son diferentes. En la vida real se llama objeto a cualquier cosa real o abstracta, en la cual podemos almacenar datos y los métodos para controlar dichos datos. —
Un objeto es una cosa tangible, algo a que se puede aprehender intelectualmente o algo hacia lo que se puede dirigir una acción o pensamiento.
—
Un objeto representa un ítem individual e identificable, o una entidad real o abstracta, con un papel definido en el dominio del problema. 5-29
Desarrollo de Sistemas
—
Un objeto tiene: •
Estado.
•
Comportamiento.
•
Identidad.
La estructura y el comportamiento de objetos similares se definen en sus clases comunes. El término objeto e instancia de una clase son idénticas. Estado de un objeto. El estado de un objeto abarca todas las propiedades del objeto, y los valores actuales de cada una de esas propiedades. Las propiedades de los objetos suelen ser estáticas, mientras los valores que toman estas propiedades cambian con el tiempo. —
El hecho de que los objetos tengan estado implica que ocupan un espacio, ya en el mundo físico, ya en la memoria del ordenador.
—
El estado de un objeto está influido por la historia del objeto.
—
No deben confundirse los objetos, que existen en el tiempo, son mutables, tienen estado, pueden ser creados, destruidos y compartidos..., con los valores (los asignados a una variable, por ejemplo) que son cantidades con las propiedades de ser atemporales, inmutables.
—
El estado de un objeto representa el efecto acumulado de su comportamiento.
Identidad de un objeto. Identidad es la propiedad de un objeto que lo lleva a distinguirse de otros. Comportamiento de un objeto. Comportamiento es como un objeto actúa y reacciona, en términos de sus cambios de estado y de los mensajes que intercambia. El comportamiento de un objeto representa su actividad externamente visible y testable. Son las operaciones que una clase realiza (llamadas también mensajes) las que dan cuenta de cómo se comporta la clase. Por operación se denota el servicio que una clase ofrece a sus clientes. Un objeto puede realizar cinco tipos de operaciones sobre otro, con el propósito de provocar una reacción: 1.
Modificador: altera el estado de un objeto.
2.
Selector: accede al estado de un objeto, sin alterarlo.
3.
Iterador: permite a todas las partes de un objeto ser accedidas en un orden.
4.
Constructor: crea un objeto o inicializa su estado.
5.
Destructor: libera el estado de un objeto o destruye el objeto.
C++ soporta, además de las operaciones, subprogramas libres. En la terminología de C++ las operaciones que un cliente puede realizar sobre un objeto se declaran como funciones miembro. 5-30
Diseño y programación orientada a objetos
2.8.1. Relaciones entre objetos Las relaciones entre objetos abarcan las operaciones, resultados y suposiciones que unos hacen sobre los otros. 1.
Links: son conexiones físicas o conceptuales entre objetos. Denota la asociación específica por la que un objeto (cliente) usa o solicita el servicio de otro objeto (servidor). El paso de mensajes entre objetos los sincroniza.
2.
Agregaciones: denota relaciones todo/parte, con capacidad para gobernar desde el todo las partes. Es equivalente a la relación “tener un”. El todo puede contener a la parte.
Agregación es conveniente en las ocasiones en que el encapsulamiento de las partes es prioritario. Si se requiere que las relaciones entre objetos estén vagamente acopladas, se utilizan “links”.
2.9.
Propiedades y métodos
El método es la especificación de un proceso de una operación, es un proceso disciplinado para generar un conjunto de modelo que describen varios aspectos de un sistema de “software” en desarrollo, utilizando alguna notación bien definida. Los métodos especifican la forma en que se controlan los datos de un objeto. Los métodos en un tipo de objeto sólo hacen referencia a las estructuras de datos de ese tipo de objeto. No deben tener acceso directo a las estructuras de datos de otros de objeto. Para utilizar la estructura de datos de otro objeto, debe enviar un mensaje a este. El tipo de objeto empaca junto los tipos de datos y los métodos.
2.9.1. Propiedades Todo objeto puede tener cierto número de propiedades, cada una de las cuales tendrá, a su vez, uno o varios valores. En POO, las propiedades corresponden a las clásicas “variables” de la programación estructurada. Son, por lo tanto, datos encapsulados dentro del objeto, junto con los métodos (programas) y las relaciones (punteros a otros objetos). Las propiedades de un objeto pueden tener un valor único o pueden contener un conjunto de valores más o menos estructurados (matrices, vectores, listas, etc.). Además, los valores pueden ser de cualquier tipo (numérico, alfabético, etc.) si el sistema de programación lo permite. Pero existe una diferencia con las “variables”, y es que las propiedades se pueden heredar de unos objetos a otros. En consecuencia, un objeto puede tener una propiedad de maneras diferentes: —
Propiedades propias. Están formadas dentro de la cápsula del objeto.
5-31
Desarrollo de Sistemas
—
Propiedades heredadas. Están definidas en un objeto antepasado del actual. A veces estas propiedades se llaman propiedades miembro porque el objeto las posee por el mero hecho de ser miembro de una clase.
2.9.2. Métodos Podemos definir al método como un programa procedimental o procedural escrito en cualquier lenguaje, que está asociado a un objeto determinado y cuya ejecución sólo puede desencadenarse a través de un mensaje recibido por éste o por sus descendientes. Son sinónimos de “método” todos aquellos términos que se han aplicado tradicionalmente a los programas, como procedimiento, función, rutina, etc. Sin embargo, es conveniente utilizar el término “método” para que se distingan claramente las propiedades especiales que adquiere un programa en el entorno POO, que afectan fundamentalmente a la forma de invocarlo (únicamente a través de un mensaje) y a su campo de acción, limitado a un objeto y a sus descendientes (no tiene por qué a todos). Dado que los métodos son partes de los programas pueden tener argumentos o parámetros. Puesto que los métodos pueden heredarse de unos objetos a otros, un objeto puede disponer de un método de dos maneras diferentes (al igual que sucedía con las propiedades): —
Métodos propios. Están incluidos dentro de la cápsula del objeto.
—
Métodos heredados. Están definidos en un objeto en un objeto antepasado del actual. A veces estos métodos se llaman “métodos miembro” porque el objeto los posee por el mero hecho de ser miembro de una clase.
—
La sobrecarga de métodos consiste en poner varios métodos con el mismo nombre en la misma clase, pero siempre que su lista de argumentos sea distinta. El compilador sabría a cuál de todas las sobrecargas nos referimos por los argumentos que se le pasen en la llamada. Lo que diferencia las listas de argumentos de las diferentes sobrecargas no es el nombre de las variables, sino el tipo de cada una de ellas. Los métodos sobrecargados dan a los programadores la flexibilidad de llamar a un método similar para diferentes tipos de datos.
Un método es un procedimiento de cálculo definido en una clase. Cada método contiene instrucciones que especifican una secuencia de acciones de cálculo que habrán de realizarse, y variables que se utilizan para almacenar y producir los valores necesarios. Algunas de las variables pueden ser objetos y el proceso de cálculo incluye interacciones entre los mismos, generalmente. Un método toma argumentos como entrada, realiza una secuencia de pasos programados y devuelve un resultado del tipo declarado (existen métodos que no devuelven obligatoriamente un valor, pueden sencillamente cambiar el valor de una propiedad o inicializar una de esas propiedades). También puede llamar a otros métodos en el curso de sus cálculos. 5-32
Diseño y programación orientada a objetos
Una definición de método contiene un encabezado y un cuerpo. El encabezado define el nombre del método y el tipo del valor de regreso. El encabezado también especifica variables, conocidas como parámetros formales, que reciben los argumentos de entrada y se utilizan en el cuerpo del método para realizar cálculos. Mientras que el cuerpo incluye una secuencia de declaraciones e instrucciones encerradas entre llaves, { }, una declaración proporciona información al compilador y una instrucción especifica las acciones que habrán de ejecutarse. En general, a las propiedades y a los métodos de una clase se les suele llamar miembros de datos pues realmente son los únicos elementos que aparecen físicamente dentro de la clase. Antes de pretender crear un molde para hacer objetos (clase) nos debemos plantear muy a conciencia qué propiedades y qué métodos queremos que tenga esa clase. Hoy en día casi todos los lenguajes de programación se basan en propiedades y métodos, pudiendo el usuario del programa cambiar los valores de estas propiedades a su gusto (ejemplo: escritorio, propiedad: papel tapiz, podemos nosotros configurarlo a nuestro antojo).
2.10. Mensajes Para que el objeto haga algo, enviamos una solicitud. Ésta hace que se produzca una operación. La operación ejecuta el método apropiado y, de manera opcional, produce una respuesta. El mensaje que constituye la solicitud contiene el nombre del objeto, el nombre de una operación, a veces, un grupo de parámetros. Un mensaje es una solicitud para que se lleve a cabo la operación indicada y se produzca el resultado. Por tanto, los mensajes son solicitudes que invocan operaciones específicas, con uno o más objetos como parámetros. La respuesta a estas órdenes será la información requerida, siempre que el objeto considere que quien envía el mensaje está autorizado para obtenerla.
2.11. Identidad La identidad es aquella propiedad de un objeto que los distingue de todos los demás objetos. La identidad única (pero no necesariamente el nombre) de cada objeto se preserva durante el tiempo de vida del mismo, incluso cuando su estado cambia. La identidad es la naturaleza de un objeto que lo distingue de todos los demás.
2.12. Reutilización o reusabilidad Es volver a generar una clase, teniendo en cuenta que puede ser útil para varios sistemas, sin tener que volver a generarlos, ahorrando con esto tiempo para programación, etc. Las clases están definidas para que se reutilicen en muchos sistemas. Para que ésta sea efectiva, las clases se deben construir a partir de un modo que puedan ser adaptables y reutilizables indefinidamente. 5-33
Desarrollo de Sistemas
Un objetivo de las técnicas orientadas a objetos es lograr la reutilización masiva al construir un “software”. Los sistemas suelen ser construidos a través de objetos ya existentes, que se lleva a un alto grado de reutilización, esto conlleva a un ahorro de dinero, un menor tiempo de desarrollo y una mayor confiabilidad de sistemas. Por lo tanto, si ya hemos puesto a prueba una clase en un sistema, tendremos la garantía y la confiabilidad que podrá volver a ser reutilizada.
2.13. Jerarquía La jerarquía es una clasificación u ordenación de abstracciones. Las dos jerarquías más importantes en un sistema complejo son su estructura de clases y su estructura de objetos, jerarquía de clase y jerarquía de partes correspondientemente. Como ya habíamos mencionado anteriormente la herencia es el ejemplo más claro de una jerarquía de clases. Ésta define una relación entre clases, en la que una clase comparte la estructura de comportamiento definida en una o más clases (herencia simple o herencia múltiple, respectivamente).
2.14. Concurrencia, persistencia y tipificado 2.14.1.
Concurrencia
Es la propiedad que distingue un objeto activo de uno no activo. Concurrencia permite que diferentes objetos actúen al mismo tiempo, usando distintos threads (hilos) de control. Para cierto tipos de problemas, un sistema automatizado puede tener que manejar muchos eventos diferentes simultáneamente, en otro problema pueden implicar tantos cálculos que excedan la capacidad de cualquier procesador individual. En ambos casos es natural considerar el uso de un conjunto distribuido de computadores para la implantación que se persigue o utilizar procesadores capaces de realizar multitareas, a través de un hilo de control, mediante la cual se producen acciones dinámicas independientes dentro del sistema. La concurrencia permite a diferentes objetos actuar al mismo tiempo.
2.14.2. Persistencia Es la propiedad por la cual la existencia de un objeto trasciende en el tiempo (esto es, el objeto sigue existiendo después de que su creador deja de existir) o en el espacio (esto es, la localización del objeto cambia respecto a la dirección en la que fue creado).
2.14.3. Tipificado Tipificar es la imposición de una clase a un objeto, de tal modo que objetos de diferentes tipos no se puedan intercambiar, o se puedan intercambiar sólo de forma restringida. 5-34
Diseño y programación orientada a objetos
—
Tipo es una caracterización precisa de las propiedades estructurales y de comportamiento que comparten una colección de entidades.
—
Una clase define un nuevo tipo de objetos, por tanto, clase y tipo muchos programadores los consideran sinónimos.
—
Existen lenguajes fuertemente tipificados (Ada) y débilmente tipificados. Estos últimos soportan polimorfismo, mientras que los fuertemente tipificados no.
2.15. Modularidad La modularidad es la propiedad que posee un sistema que ha sido descompuesto en un conjunto de módulos cohesivos y débilmente acoplados. La modularización consiste en dividir un programa en módulos que pueden compilarse separadamente, pero que tiene conexiones con otros módulos. Así, los principios de abstracción, encapsulamiento y modularidad son sinérgicos (puede haber muchas más abstracciones diferentes de las que se pueden comprender simultáneamente, el encapsulamiento ayuda a manejar esta complejidad ocultando la visión interna de las abstracciones. La modularidad ayuda ofreciendo una vía para agrupar abstracciones relacionadas lógicamente).
2.16. Relaciones entre los conceptos asociados al modelo de objetos —
Los conceptos de abstracción y encapsulación son conceptos complementarios: abstracción hace referencia al comportamiento observable de un objeto, mientras encapsulación hace referencia a la implementación que la hace alcanzar este comportamiento.
—
Existe una tensión entre los conceptos de encapsulación de la información y el concepto de jerarquía de herencia, que requiere una apertura en el acceso a la información.
—
C++ ofrece mucha flexibilidad, pudiendo disponer de tres compartimentos en cada clase: 1.
Privado: declaraciones accesibles sólo a la clase (completamente encapsulado).
2.
Protegido: declaraciones accesibles a la clase y a sus subclases.
3.
Público: declaraciones accesibles a todos los clientes.
Además de estos tres tipos, C++ soporta la definición de clases cooperativas a las que se les permite acceder a la parte privada de la implementación. Estas clases se denominan friends.
5-35
Desarrollo de Sistemas
Vamos a realizar un esquema de cada uno de los términos vistos: OBJETO
Fin, intento, propósito. Materia y sujeto de una ciencia.
CLASE
Orden de cosas de una misma especie. Conjunto de órdenes.
POLIMORFISMO
Propiedad de los cuerpos que cambian de forma sin cambiar su naturaleza. Presencia de distintas formas individuales en una sola especie.
ENCAPSULACIÓN
Proceso de constitución de una cápsula.
HERENCIA
Derecho de suceder a otro la posesión de bienes o acciones.
MENSAJE
Información que se le envía a alguien.
MÉTODO
Modo de hacer en orden una cosa, modo habitual de procede.
IDENTIDAD
Cualidad de ser lo mismo que otra cosa con que se compara.
REUSABILIDAD
Acción de volver a utilizar. Que puede volver a ser utilizado.
Asistencia, reunión simultánea de personas o cosas.
MODULARIDAD
Acción de pasar de un término a otro.
3. 3.1.
Ventajas e inconvenientes Ventajas de POO
Un programa grande de computadora se encuentra entre las construcciones más complejas. El costo del diseño, la implantación, la verificación, el mantenimiento y la revisión de sistemas grandes de “software” es muy alto. Por tanto, es importante encontrar maneras de facilitar estas tareas. En este sentido, POO tiene un potencial enorme. Los sistemas orientados a objetos son también más resistentes al cambio y, por lo tanto, están mejor preparados para evolucionar en el tiempo, porque su diseño esta basado en formas intermedias estables. El modelo de objetos ha influido incluso en las fases iniciales del ciclo de vida del desarrollo del software. El análisis orientado a objetos (AOO) enfatiza la construcción de modelos del mundo real utilizando una visión del mundo orientado a objetos. El análisis orientado a objetos es un método de análisis que examina los requisitos desde la perspectiva de las clases y objetos que se encuentran en el vocabulario del dominio del problema. Básicamente los productos del análisis orientado a objetos sirven como modelos de los que se puede partir para un diseño orientado a objetos; los productos del diseño orientado a objetos pueden utilizarse entonces como anteproyectos para la implementación completa de unos sistemas utilizando métodos de programación orientado a objetos, de esta forma se relacionan AOO, DOO y POO.
5-36
Diseño y programación orientada a objetos
La Programación Orientada a Objetos ofrece las siguientes ventajas principales: —
Simplicidad: como los objetos de “software” son modelos de objetos reales en el dominio de la aplicación, la complejidad del programa se reduce y su estructura se vuelve clara y simple.
—
Modularidad: cada objeto forma una entidad separada cuyo funcionamiento interno está desacoplado de otras partes del sistema.
—
Facilidad para hacer modificaciones: es sencillo hacer cambios menores en la representación de los datos o los procedimientos utilizados en un programa Orientado a Objetos (O.O.). Las modificaciones hechas en el interior de un objeto no afectan ninguna otra parte del programa, siempre y cuando se preserve su comportamiento externo.
—
Posibilidad de extenderlo: la adición de nuevas funciones o la respuesta a ambientes operativos cambiantes puede lograrse con sólo introducir algunos objetos nuevos y variar algunos existentes.
—
Flexibilidad: un programa Orientado a Objetos (O.O.) puede ser muy manejable al adaptarse a diferentes situaciones, porque es posible cambiar los patrones de interacción entre los objetos sin alterarlos.
—
Facilidad para darle mantenimiento: los objetos pueden mantenerse por separado, lo que facilita la localización y el arreglo de problemas, así como la adición de otros elementos.
—
Reusabilidad: los objetos pueden emplearse en diferentes programas. Por ejemplo, si se tiene uno que construye tablas puede utilizarse en cualquier programa que requiera cierto tipo de tabla. Por tanto, es posible construir programas a partir de componentes prefabricados y preprobados en una fracción del tiempo requerido para elaborar nuevos programas desde el principio.
—
Dominio del problema: el paradigma O.O. es más que una forma de programar. Es una forma de pensar acerca de un problema desde el punto de vista del mundo real en vez de desde el punto de vista del ordenador. El AOO permite analizar mejor el dominio del problema, sin pensar en términos de implementar el sistema de un ordenador, permite, además, pasar directamente el dominio del problema al modelo del sistema.
—
Comunicación: el concepto O.O. es más simple y está menos relacionado con la informática que el concepto de flujo de datos. Esto permite una mejor comunicación entre el analista y el experto en el dominio del problema.
—
Consistencia: los objetos encapsulan tanto atributos como operaciones. Debido a esto, el AOO reduce la distancia entre el punto de vista de los datos y el punto de vista del proceso, dejando menos lugar a inconsistencias disparidades entre ambos modelos.
5-37
Desarrollo de Sistemas
—
Expresión de características comunes: el paradigma lo utiliza la herencia para expresar explícitamente las características comunes de una serie de objetos estas característica comunes quedan escondidas en otros enfoques y llevan a duplicar entidades en el análisis y código en los programas. Sin embargo, el paradigma O.O. pone especial énfasis en la reutilización y proporciona mecanismos efectivos que permiten reutilizar aquello que es común sin impedir por ello describir las diferencias.
—
Resistencia al cambio: los cambios en los requisitos afectan notablemente a la funcionalidad de un sistema por lo que afectan mucho al software desarrollando con métodos estructurados. Sin embargo, los cambios afectan en mucha menos medida a los objetos que componen o maneja el sistema, que son mucho más estables. Las modificaciones necesarias para adaptar una aplicación basada en objetos a un cambio de requisitos suelen estar mucho más localizadas.
—
Reutilización: aparte de la reutilización interna, basada en la expresión explícita de características comunes, el paradigma O.O. desarrolla modelos mucho más próximos al mundo real, con lo que aumentan las posibilidades de reutilización. Es probable que en futuras aplicaciones nos encontremos con objetos iguales o similares a los de la actual.
Todos los problemas aún no han sido solucionados en forma completa. Pero como los objetos son portables mientras que la herencia permite la reusabilidad del código orientado a objetos, es más sencillo modificar código existente porque los objetos no interaccionan excepto a través de mensajes; en consecuencia un cambio en la codificación de un objeto no afectará la operación con otro objeto siempre que los métodos respectivos permanezcan intactos. La introducción de tecnología de objetos como una herramienta conceptual para analizar, diseñar e implementar aplicaciones permite obtener aplicaciones más modificables, fácilmente extensibles y a partir de componentes reusables. Esta reusabilidad del código disminuye el tiempo que se utiliza en el desarrollo y hace que el desarrollo del software sea más intuitivo porque la gente piensa naturalmente en términos de objetos más que en términos de algoritmos de “software”.
3.2.
Inconvenientes de la POO
En un sistema orientado a objetos los problemas (si los hubiese) suelen surgir en la implementación de tal sistema. Muchas compañías oyen acerca de los beneficios de un sistema orientado a objetos e invierten gran cantidad de recursos luego comienzan a darse cuenta que han impuesto una nueva cultura que es ajena a los programadores actuales. A pesar de ser muy pocos los inconvenientes de la POO, citaremos los siguientes:
5-38
—
Curvas de aprendizaje largas. Al hacer la transición a un sistema orientado a objetos la mayoría de los programadores deben capacitarse nuevamente antes de poder usarlo.
—
Dependencia del lenguaje. La elección de un lenguaje orientado a objetos u otro tiene ramificaciones de diseño muy importantes.
Diseño y programación orientada a objetos
4.
—
Determinación de las clases. Una clase es un molde que se utiliza para crear nuevos objetos. Si bien hay muchas jerarquías de clase predefinidas usualmente se deben crear clases específicas para la aplicación que se esté desarrollando. En consecuencia, es importante crear el conjunto de clases adecuado para un proyecto.
—
Descomposición funcional. El análisis estructurado se basa en la descomposición funcional del sistema que queremos construir. El problema es que no existe un mecanismo para comprobar si la especificación del sistema expresa con exactitud los requisitos del sistema.
—
Flujos de datos. El análisis estructurado muestra cómo fluye la información a través del sistema. Aunque este enfoque se adapta bien al uso de sistemas informáticos para implementar al sistema, no es la forma habitual de pensar.
—
Modelo de datos. El análisis estructurado moderno incorpora modelos de datos, además de modelos de procesos y de comportamiento. Sin embargo, la relación entre los modelos es muy débil, y hay muy poca influencia de un modelo en otro.
Patrones de diseño y lenguaje de modelado unificado (UML)
4.1.
UML
UML, Lenguaje Unificado de Modelado (Unified Modeling Language) es el lenguaje de modelado de sistemas de software. Es un lenguaje gráfico para visualizar, especificar, construir y documentar un sistema de software. UML ofrece un estándar para describir un “plano” del sistema (modelo), incluyendo aspectos conceptuales tales como procesos de negocios y funciones del sistema, y aspectos concretos como expresiones de lenguajes de programación, esquemas de bases de datos y componentes de software reutilizables. Es importante remarcar que UML es un “lenguaje” para especificar y no un método o un proceso, se utiliza para definir un sistema de software, para detallar los artefactos en el sistema y para documentar y construir –es el lenguaje en el que está descrito el modelo–. Se puede aplicar en una gran variedad de formas para soportar una metodología de desarrollo de software pero no especifica en sí mismo qué metodología o proceso usar. UML es un lenguaje. Un lenguaje proporciona un vocabulario y unas reglas para permitir una comunicación. Este lenguaje nos indica cómo crear y leer los modelos, pero no dice cómo crearlos. Esto último es el objetivo de las metodologías de desarrollo. Los objetivos de UML son: —
Visualizar, expresa de forma gráfica.
—
Especificar, las características de un sistema.
—
Construir, a partir de los modelos especificados.
—
Documentar, los propios elementos gráficos sirven de documentación. 5-39
Desarrollo de Sistemas
4.1.1. Bloques de construcción de un modelo UML Un modelo UML está compuesto por tres clases de bloques de construcción: A) Elementos Los elementos son abstracciones de cosas reales o ficticias (objetos, acciones, etc.). —
Pueden ser estructurales, de comportamiento de agrupación o de anotación.
—
Elementos estructurales: actores, casos de uso, clases, objetos.
—
Elementos de comportamiento: mensajes.
—
Elementos de agrupación: paquetes.
CLASE ATRIBUTOS MÉTODOS ACTOR
CASOS DE USO
PAQUETE
MENSAJES
B) Relaciones Relacionan los elementos entre sí. Las relaciones pueden ser del tipo: • Dependencia Es una relación semántica entre dos elementos, en la cual un cambio en un elemento puede afectar a la semántica de otro elemento. Existen varios tipos de dependencia predefinidas que se indican mediante extend o include para casos de uso. • Asociación Es una relación estructural entre dos elementos, que describen las conexiones entre ellos (suele ser bidireccional). Puede presentarse como agregación o composición. • Generalización Es una relación entre un elemento más general (el padre) y un elemento más específico (el hijo). 5-40
Diseño y programación orientada a objetos
• Implementación Es una relación en la que un elemento (hijo) realiza las acciones indicadas por el padre.
4.2.
Diagramas
Son colecciones de elementos con sus relaciones. UML cuenta con varios tipos de diagramas, los cuales muestran diferentes aspectos de las entidades representadas. Los veremos en esta sección: DIAGRAMAS
Estructura
Comportamiento
Clases
Actividades
Componentes
Iteración
Secuencia
Objetos
Casos de Uso
Comunicación
Paquetes
Estados
Tiempos
Despliegue
Revisión Interacciones
4.2.1. Diagramas de estructura Enfatizan en los elementos que deben existir en el sistema modelado. Los diagramas estructurales representan elementos y así componen un sistema o una función. Estos diagramas pueden reflejar las relaciones estáticas de una estructura, como lo hacen los diagramas de clases o de paquetes, o arquitecturas en tiempo de ejecución, tales como diagramas de Objetos o de Estructura de Composición. • Diagrama de clases Los diagramas de clases son utilizados durante el proceso de análisis y diseño de los sistemas informáticos, donde se crea el diseño conceptual de la información que se manejara en el sistema, y los componentes que se encar5-41
Desarrollo de Sistemas
garan del funcionamiento y la relación entre uno y otro. Muestra una colección de elementos de modelado declarativo (estáticos), tales como clases, tipos y sus contenidos y relaciones. Se basan en: —
Propiedades: también llamados atributos o características, son valores que corresponden a un objeto, como color, material, cantidad, ubicación. Generalmente se conoce como la información detallada del objeto. Suponiendo que el objeto es una puerta, sus propiedades serían: la marca, tamaño, color y peso.
—
Operaciones: son aquellas actividades o verbos que se pueden realizar con/para este objeto, como por ejemplo abrir, cerrar, buscar, cancelar, acreditar, cargar. De la misma manera que el nombre de un atributo, el nombre de una operación se escribe con minúsculas si consta de una sola palabra. Si el nombre contiene más de una palabra, cada palabra será unida a la anterior y comenzará con una letra mayúscula, a excepción de la primera palabra que comenzará en minúscula. Por ejemplo: abrirPuerta, cerrarPuerta, buscarPuerta, etc.
—
Interfaz: es un conjunto de operaciones o propiedades que permiten a un objeto comportarse de cierta manera, por lo que define los requerimientos mínimos del objeto.
—
Herencia: se define como la reutilización de un objeto padre ya definido para poder extender la funcionalidad en un objeto hijo. Los objetos hijos heredan todas las operaciones o propiedades de un objeto padre. Por ejemplo: una persona puede subdividirse en Proveedores, Acreedores, Clientes, Accionistas, Empleados; todos comparten datos basicos como una persona, pero además tendrá información adicional que depende del tipo de persona, como saldo del cliente, total de inversión del accionista, salario del empleado, etc.
• Diagrama de componentes Un diagrama de componentes representa la separación de un sistema de “sofware” en componentes físicos (por ejemplo archivos, cabeceras, módulos, paquetes, etc.) y muestra las dependecias entre estos componentes. Representa los componentes que componen una aplicación, sistema o empresa. Los componentes, sus relaciones, interacciones y sus interfaces públicas. Los diagramas de componentes describen los elementos físicos del sistema y sus relaciones. Muestran las opciones de realización incluyendo código fuente, binario y ejecutable. Los componentes representan todos los tipos de elementos software que entran en la fabricación de aplicaciones informáticas. Pueden ser simples archivos, paquetes de Ada, bibliotecas cargadas dinámicamente, etc. Las relaciones de dependencia se utilizan en los diagramas de componentes para indicar que un componente utiliza los servicios ofrecidos por otro componente. Un diagrama de componentes representa las dependencias entre componentes “software”, incluyendo componentes de código fuente, componentes del código binario, y componentes ejecutables. Un módulo de “software” se 5-42
Diseño y programación orientada a objetos
puede representar como componente. Algunos componentes existen en tiempo de compilación, algunos en tiempo de enlace y algunos en tiempo de ejecución, otros en varias de éstas. Un componente de sólo compilación es aquel que es significativo únicamente en tiempo de compilación. Un componente ejecutable es un programa ejecutable. Un diagrama de componentes tiene sólo una versión con descriptores, no tiene versión con instancias. Para mostrar las instancias de los componentes se debe usar un diagrama de despliegue. Un diagrama de componentes muestra clasificadores de componentes, las clases definidas en ellos, y las relaciones entre ellas. Los clasificadores de componentes también se pueden anidar dentro de otros clasificadores de componentes para mostrar relaciones de definición. Un diagrama que contiene clasificadores de componentes y de nodo se puede utilizar para mostrar las dependencias del compilador, que se representa como flechas con líneas discontinuas (dependencias) de un componente cliente a un componente proveedor del que depende. Los tipos de dependencias son específicos del lenguaje y se pueden representar como estereotipos de las dependencias. El diagrama también puede usarse para mostrar interfaces y las dependencias de llamada entre componentes, usando flechas con líneas discontinuas desde los componentes a las interfaces de otros componentes. El diagrama de componente hace parte de la vista física de un sistema, la cual modela la estructura de implementación de la aplicación por sí misma, su organización en componentes y su despliegue en nodos de ejecución. Esta vista proporciona la oportunidad de establecer correspondecias entre las clases y los componentes de implementación y nodos. La vista de implementación se representa con los diagramas de componentes. • Diagrama de objetos Los diagramas de objetos son utilizados durante el proceso de Análisis y Diseño de los sistemas informáticos en la metodología UML. Se puede considerar un caso especial de un diagrama de clases en el que se muestran instancias específicas de clases (objetos) en un momento particular del sistema. Los diagramas de objetos utilizan un subconjunto de los elementos de un diagrama de clase. Los diagramas de objetos no muestran la multiplicidad ni los roles, aunque su notación es similar a los diagramas de clase. Una diferencia con los diagramas de clase es que el compartimiento de arriba va en la forma, Nombre de objeto: Nombre de clase. Por ejemplo, Miguel: Persona. Un diagrama que presenta los objetos y sus relaciones en un punto del tiempo. Un diagrama de objetos se puede considerar como un caso especial de un diagrama de clases o un diagrama de comunicaciones. 5-43
Desarrollo de Sistemas
• Diagrama de despliegue El Diagrama de Despliegue es un tipo de diagrama del Lenguaje Unificado de Modelado que se utiliza para modelar el hardware utilizado en la implementaciones de sistemas y las relaciones entre sus componentes. Los elementos usados por este tipo de diagrama son nodos (representados como un prisma), componentes (representados como una caja rectangular con dos protuberancias del lado izquierdo) y asociaciones. En el UML 2.0 los componentes ya no están dentro de nodos. En cambio, puede haber artefactos u otros nodos dentro de un nodo. Un artefacto puede ser algo como un archivo, un programa, una biblioteca, o una base de datos construida o modificada en un proyecto. Estos artefactos implementan colecciones de componentes. Los nodos internos indican ambientes, un concepto más amplio que el hardware propiamente dicho, ya que un ambiente puede incluir al lenguaje de programación, a un sistema operativo, un ordenador o un cluster de terminales. Un diagrama de despliegue físico muestra cómo y dónde se desplegará el sistema. Las máquinas físicas y los procesadores se representan como nodos y la construcción interna puede ser representada por nodos o artefactos embebidos. Como los artefactos se ubican en los nodos para modelar el despliegue del sistema, la ubicación es guiada por el uso de las especificaciones de despliegue. • Diagrama de paquetes En el Lenguaje Unificado de Modelado, un diagrama de paquetes muestra cómo un sistema está dividido en agrupaciones lógicas mostrando las dependencias entre esas agrupaciones. Dado que normalmente un paquete está pensado como un directorio, los diagramas de paquetes suministran una descomposición de la jerarquía lógica de un sistema. Los paquetes están normalmente organizados para maximizar la coherencia interna dentro de cada paquete y minimizar el acoplamiento externo entre los paquetes. Con estas líneas maestras sobre la mesa, los paquetes son buenos elementos de gestión. Cada paquete puede asignarse a un individuo o a un equipo, y las dependencias entre ellos pueden indicar el orden de desarrollo requerido. Un diagrama que presenta cómo se organizan los elementos de modelado en paquetes y las dependencias entre ellos, incluyendo importaciones y extensiones de paquetes. • Diagrama de Estructura de Composición Representa la estructura interna de un clasificador (tal como una clase, un componente o un caso de uso), incluyendo los puntos de interacción de clasificador con otras partes del sistema.
5-44
Diseño y programación orientada a objetos
Los diagramas de composición de estructuras fueron específicamente diseñados para la representación de patrones de diseño, y son una de las modificaciones de mayor impacto dentro de UML 2.0. Los diagramas de composición de estructuras permiten, potencialmente, documentar arquitecturas de software de manera un poco más clara que en versiones anteriores del UML 2.0.
4.2.2. Diagramas de comportamiento Enfatizan en lo que debe suceder en el sistema modelado. Los diagramas de comportamiento representan las características de comportamiento de un sistema o proceso de negocios y, a su vez, incluyen a los diagramas de: actividades, casos de uso, máquinas de estados, tiempos, secuencias, repaso de interacciones y comunicaciones. • Diagrama de actividades En el Lenguaje de Modelado Unificado, un diagrama de actividades representa los flujos de trabajo paso a paso de negocio y operacionales de los componentes en un sistema. Un Diagrama de Actividades muestra el flujo de control general. En SysML el diagrama de Actividades ha sido extendido para indicar flujos entre pasos que mueven elementos físicos (e.g., gasolina) o energía (e.g., presión). Los cambios adicionales permiten al diagrama soportar mejor flujos de comportamiento y datos continuos. Muchos cambios fueron realizados en los diagramas de actividad en la versión 2 de UML. Los cambios realizados son tendentes a: —
Dar soporte en la definición de procesos de negocio.
—
Brindar una semántica similar al de las redes de Petri.
—
Permitir una mayor y más flexible representación de paralelismo.
Representa los procesos de negocios de alto nivel, incluidos el flujo de datos. También puede utilizarse para modelar lógica compleja o paralela dentro de un sistema. • Diagrama de casos de uso En el Lenguaje de Modelado Unificado, un diagrama de casos de uso es una especie de diagrama de comportamiento. El Lenguaje de Modelado Unificado define una notación gráfica para representar casos de uso llamada modelo de casos de uso. UML no define estándares para que el formato escrito describa los casos de uso, y así mucha gente no entiende que esta notación gráfica define la naturaleza de un caso de uso; sin embargo una notación gráfica puede solo dar una vista general simple de un caso de uso o un conjunto de casos de uso. Los diagramas de casos de uso son a menudo confundidos con los casos de uso. Mientras los dos conceptos están relacionados, los casos de uso son mucho más detallados que los diagramas de casos de uso.
5-45
Desarrollo de Sistemas
El estándar de Lenguaje de Modelado Unificado de OMG define una notación gráfica para realizar diagramas de casos de uso, pero no el formato para describir casos de uso. Mucha gente sufre la equivocación pensando que un caso de uso es una notación gráfica (o es su descripción). Mientras la notación gráfica y las descripciones son importantes, ellas son documentación de un caso de uso: un propósito para el que el actor puede usar el sistema. los diagramas de casos de uso son diagramas que muestran las relaciones entre actores y el sistema. Un diagrama que muestra las relaciones entre los actores y el sujeto (sistema), y los casos de uso. El valor verdadero de un caso de uso reposa en dos áreas: —
La descripción escrita del comportamiento del sistema al afrontar una tarea de negocio o un requisito de negocio. Esta descripción se enfoca en el valor suministrado por el sistema a entidades externas tales como usuarios humanos u otros sistemas.
—
La posición o contexto del caso de uso entre otros casos de uso. Dado que es un mecanismo de organización, un conjunto de casos de uso coherentes, consistentes promueve una imágen fácil del comportamiento del sistema, un entendimiento común entre el cliente/propietario/usuario y el equipo de desarrollo.
Es práctica común crear especificaciones suplementarias para capturar detalles de requisitos que caen fuera del ámbito de las descripciones de los casos de uso. Ejemplos de esos temas incluyen rendimiento, temas de escalabilidad/gestión, o cumplimiento de estándares. Relaciones de Casos de Uso. Las tres relaciones principales entre los casos de uso son soportadas por el estándar UML, el cual describe notación gráfica para esas relaciones. Include. En una forma de interacción, un caso de uso dado puede “incluir” otro. El primer caso de uso a menudo depende del resultado del caso de uso incluido. Esto es útil para extraer comportamientos verdaderamente comunes desde múltiples casos de uso a una descripción individual. La notación es una flecha rayada desde el caso de uso que lo incluye hasta el caso de uso incluido, con la etiqueta “include”. Este uso se asemeja a una expansión de una macro donde el comportamiento del caso incluido es colocado dentro del comportamiento del caso de uso base. No hay parámetros o valores de retorno. Extend. En otra forma de interacción, un caso de uso dado (la extensión), puede extender a otro. Esta relación indica que el comportamiento del caso de uso extensión puede ser insertado en el caso de uso extendido bajo ciertas condiciones. La notación es una flecha rayada desde el caso de uso extensión al caso de uso extendido, con la etiqueta “extend”. Esto puede ser útil para lidiar con casos especiales, o para acomodar nuevos requisitos durante el mantenimiento del sistema y su extensión. Generalization. En la tercera forma de relación entre casos de uso, existe una relación generalización/especialización. Un caso de uso dado puede estar
5-46
Diseño y programación orientada a objetos
en una forma especializada de un caso de uso existente. La notación es una línea solida terminada en un triángulo dibujado desde el caso de uso especializado al caso de uso general. Esto se asemeja al concepto orientado a objetos de sub-clases, en la práctica puede ser útil factorizar comportamientos comunes, restricciones al caso de uso general, descríbelos una vez, y enfréntate a los detalles excepcionales en los casos de uso especializados. • Diagrama de estados El diagrama de estados de UML es un diagrama de estados con notación estandarizada que puede describir los elementos, desde un programa de computador a procesos de negocio. Un diagrama de Máquina de Estados ilustra cómo un elemento, muchas veces una clase, se puede mover entre estados que clasifican su comportamiento, de acuerdo con disparadores de transiciones, guardias de restricciones y otros aspectos de los diagramas de Máquinas de Estados, que representan y explican el movimiento y el comportamiento. Lo siguiente son los elementos básicos de notación que pueden usarse para componer un diagrama: —
Círculo lleno, apuntando a un estado inicial
—
Círculo hueco que contiene un círculo lleno más pequeño en el interior, indicando el estado final (si existiera)
—
Rectángulo redondeado, denotando un estado. En la parte superior del rectángulo está el nombre del estado. Puede contener una línea horizontal en la mitad, debajo de la cual se indican las actividades que se hacen en el estado
—
Flecha, denotando transición. El nombre del evento (si existiera) que causa esta transición etiqueta el cuerpo de la flecha. Se puede añadir una expresión de Guarda, encerrada en corchetes ([]) denotando que esta expresión debe ser cierta para que la transición tenga lugar. Si se realiza una acción durante la transición, se añade a la etiqueta después de “/”. NombreDeEvento[ExpresiónGuarda]/acción
—
Línea horizontal gruesa con x>1 líneas entrando y 1 línea saliendo o 1 línea entrando y x>1 líneas saliendo. Éstas denotan Unión/Separación, respectivamente.
Un diagrama de Máquina de Estados ilustra cómo un elemento, muchas veces una clase, se puede mover entre estados que clasifican su comportamiento, de acuerdo con disparadores de transiciones, guardias de restricciones y otros aspectos de los diagramas de Máquinas de Estados, que representan y explican el movimiento y el comportamiento. Al igual que los diagramas de secuencia, las Máquinas de Estados permiten un mejor rehúso, a través del agregado de Puntos de Entrada y Puntos de Salida (Entry/Exit Points). Las máquinas de estados son ahora generalizables y soportan una vista centrada en la transición. Las capacidades de generalización incluyen: agregar estados y transiciones, extender estados, reemplazar transiciones, reemplazar maquinas compuestas, etc. Lo que permite que, por ejemplo, dada una clase que hereda de otra, especificar ambas clases mediante máquinas de estados que heredan funcionalidad. 5-47
Desarrollo de Sistemas
4.2.3. Diagramas de interacción Son un subtipo de diagramas de comportamiento, que enfatiza sobre el flujo de control y de datos entre los elementos del sistema modelado. El UML 2.0 se encuentra diseñado de manera Orientada a Objetos, dentro de la nueva organización interna, y cuenta con los llamados “Diagramas de Interacciones”, que son una subcategoría de los diagramas de comportamiento. Estos diagramas muestran la interacción entre distintos clasificadores de un modelo desde distintos puntos de vista, es decir, haciendo foco en distintos aspectos de la interacción. Esto hace que todos los diagramas de interacción tengan ciertas características compartidas, como por ejemplo la capacidad de crear Diagramas de descripción de interacción y la utilización de fragmentos combinados. Dichos conceptos serán descriptos a continuación utilizando los diagramas de secuencias. • Diagrama de secuencia El diagrama de secuencia es uno de los diagramas más efectivos para modelar interacción entre objetos en un sistema. Un diagrama de secuencia muestra la interacción de un conjunto de objetos en una aplicación a través del tiempo y se modela para cada caso de uso. Mientras que el diagrama de casos de uso permite el modelado de una vista “business” del escenario, el diagrama de secuencia contiene detalles de implementación del escenario, incluyendo los objetos y clases que se usan para implementar el escenario, y mensajes pasados entre los objetos. Típicamente uno examina la descripción de un caso de uso para determinar qué objetos son necesarios para la implementación del escenario. Si tienes modelada la descripción de cada caso de uso como una secuencia de varios pasos, entonces puedes “caminar sobre” esos pasos para descubrir qué objetos son necesarios para que se puedan seguir los pasos. Un diagrama de secuencia muestra los objetos que intervienen en el escenario con líneas discontinuas verticales, y los mensajes pasados entre los objetos como vectores horizontales. Los mensajes se dibujan cronológicamente desde la parte superior del diagrama a la parte inferior; la distribución horizontal de los objetos es arbitraria. Durante el análisis inicial, el modelador típicamente coloca el nombre “business” de un mensaje en la línea del mensaje. Más tarde, durante el diseño, el nombre “business” es reemplazado con el nombre del método que está siendo llamado por un objeto en el otro. El método llamado, o invocado, pertenece a la definición de la case instanciada por el objeto en la recepción final del mensaje. Las modificaciones de los diagramas de secuencias tienden básicamente a permitir la reutilización de los diagramas, agregando los elementos de tipos Fragmento Combinado. Un diagrama que representa una interacción, poniendo el foco en la secuencia de los mensajes que se intercambian, junto con sus correspondientes ocurrencias de eventos en las Líneas de Vida. • Diagrama de comunicación (Antiguos diagramas de colaboración) Anteriormente tenían el nombre de “Diagramas de Colaboración”. Por ser las colaboraciones un diagrama de interacción, al igual que los diagramas 5-48
Diseño y programación orientada a objetos
de secuencias, heredan la misma capacidad de soportar fragmentos combinados. En UML 2.0, un diagrama de comunicación es una versión simplificada del diagrama de colaboración de la versión de UML 1.x. Un diagrama de Comunicación modela las interacciones entre objetos o partes en términos de mensajes en secuencia. Los diagramas de Comunicación representan una combinación de información tomada desde el diagrama de Clases, Secuencia, y Diagrama de casos de uso describiendo tanto la estructura estática como el comportamiento dinámico de un sistema. Los diagramas de comunicación y de secuencia describen información similar, y con ciertas transformaciones, pueden ser transformados unos en otros sin dificultad. Para mantener el orden de los mensajes en un diagrama de comunicación, los mensajes son etiquetados con un número cronológico y colocados cerca del enlace por el cual se desplaza el mensaje. Leer un diagrama de comunicación conlleva comenzar en el mensaje 1.0, y seguir los mensajes desde un objeto hasta el siguiente, sucesivamente. Es un diagrama que enfoca la interacción entre líneas de vida, donde es central la arquitectura de la estructura interna y cómo ella se corresponde con el pasaje de mensajes. La secuencia de los mensajes se da a través de un esquema de numerado de la secuencia. • Diagrama de tiempos Un diagrama de tiempos o cronograma es una gráfica de formas de onda digitales que muestra la relación temporal entre varias señales, y cómo varía cada señal en relación a las demás. Un cronograma puede contener cualquier número de señales relacionadas entre sí. Examinando un diagrama de tiempos, se puede determinar los estados, nivel alto o nivel bajo, de cada una de las señales en cualquier instante de tiempo especificado, y el instante exacto en que cualquiera de las señales cambia de estado con respecto a las restantes. El propósito primario de los diagramas de tiempos (o temporizados) es mostrar los cambios en el estado, o la condición, de una línea de vida de una instancia (de un Clasificador o un Rol de un clasificador), a lo largo del tiempo y de manera lineal. El uso más común es mostrar el cambio de estado de un objeto a lo largo del tiempo, en respuesta a los eventos o estímulos aceptados. El propósito primario del diagrama de tiempos es mostrar los cambios en el estado o la condición de una línea de vida (representando una Instancia de un Clasificador o un Rol de un clasificador) a lo largo del tiempo lineal. El uso más común es mostrar el cambio de estado de un objeto a lo largo del tiempo, en respuesta a los eventos o estímulos aceptados. Los eventos que se reciben se anotan, a medida que muestran cuándo se desea mostrar el evento que causa el cambio en la condición o en el estado.
5-49
Desarrollo de Sistemas
• Diagramas de Revisión de interacciones Es un diagrama que muestra cómo interactúan varios diagramas de interacciones. Este tipo de diagramas es muy útil para mostrar de qué manera distintos escenarios se combinan. Los Diagramas de Revisión de la Interacción enfocan la revisión del flujo de control, donde los nodos son Interacciones u Ocurrencias de Interacciones. Las Líneas de Vida los Mensajes no aparecen en este nivel de revisión.
4.2.4. Diagramas UML 2.0 La superestructura del UML es la definición formal de los elementos que componen el UML 2.0. Éste se encuentra organizado en paquetes, que definen los elementos internos del UML y de qué manera se relacionan. El diseño interno del UML 2.0 se encuentra orientado a objetos.
5-50
Tema 6 Lenguajes de programación. Características, elementos y funciones en entornos Java, C, C++ y .Net.
Desarrollo de Sistemas
Guión-resumen
1. Introducción 2. Conceptos básicos 3. Introducción a los lenguajes de programación 4. Generaciones en los lenguajes de programación 5. Tipos de programación 6. Procesos en la programación 7. Tipos de lenguajes de programación 8. Estilos de programación 9. Otros conceptos base en programación
10. Aplicaciones de los lenguajes de programación 11. Historia de los lenguajes de programación 12. Algunos lenguajes de programación 13. Otros lenguajes de programación 14. Características, elementos y funciones de JAVA, C y C++ 15. Entornos de programación visual 16. .NET Framework 17. Clasificación de los lenguajes de programación Anexos
6-2
Lenguajes de programación
1.
Introducción
Desde que surgieron las primeras computadoras ha sido necesario definir lenguajes que le permitan al hombre comunicarse con la computadora. Esta máquina (el ordenador) fue diseñada para realizar procesos internos en virtud de entradas y salidas de datos. En la actualidad hay equipos computacionales que son automáticos pero, aun así, necesitan comunicarse internamente de alguna forma. El diseño de soluciones a la medida de nuestros problemas requiere una metodología que nos enseñe de manera gradual la forma de llegar a las soluciones. A las soluciones creadas por computadora se les conoce como programas y no son más que una serie de operaciones que realiza la computadora para llegar a un resultado, con un grupo de datos específicos. Es decir, un programa nos sirve para solucionar un problema específico. Para poder realizar programas, además de conocer la metodología mencionada, también debemos de conocer, de manera específica, las funciones que pueden realizar las computadoras y las formas en que se pueden manejar los elementos que hay en la misma. Como ya sabemos, un ordenador es un conjunto de circuitos, cables, etc. por los cuales circula corriente eléctrica en forma de 0 y 1. Entonces, ¿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? Es este el papel que juegan los lenguajes de programación.
2.
Conceptos básicos
Un programa es una serie o secuencia de instrucciones que el ordenador debe ejecutar para realizar la tarea prevista por el programador. La CPU sólo ejecuta las instrucciones que componen el programa: operaciones aritméticas, operaciones lógicas, comparaciones y movimientos de datos. Cuando nos planteamos un problema complejo y queremos resolverlo con la utilización del ordenador, necesitamos descomponerlo en una serie de tareas simples que se irán repitiendo a lo largo de un proceso hasta la resolución del problema; el ordenador ha realizado una tarea compleja, a partir de instrucciones simples. Dicho conjunto de tareas simples sería el programa, y su elaboración es lo que entendemos por programación. La determinación de la calidad de los programas estará en función de las ventajas de su utilización; para ello existen unas características entre la cuales citamos: —
Legible. Todo programa debe ser de fácil comprensión no sólo por los futuros usuarios, sino por cualquier programador.
—
Flexible. Capaz de adaptarse con facilidad a los cambios que puedan producirse en el planteamiento inicial.
—
Portable. Facilidad para compilarse o interpretarse en distintas máquinas y sistemas operativos, también un factor a tener en cuenta sería su facilidad para ser traducido a otros lenguajes de programación. 6-3
Desarrollo de Sistemas
—
Fiable. El programa debe ser capaz de recuperar el control cuando su utilización no sea la adecuada.
—
Eficaz. El programa ha de utilizar eficazmente los recursos de que disponga, tanto del sistema operativo como del equipo en que trabaje.
Cuando nos disponemos a programar, la primera decisión que hemos de tomar es la elección del lenguaje a emplear, es decir, la forma en la que el programador tiene que escribir las operaciones a realizar por el ordenador. Cada computadora tiene un solo lenguaje que puede ejecutarse: el lenguaje de máquina. Hablamos de programar en lenguajes de alto nivel, pero estos lenguajes deben ser traducidos al lenguaje de máquina de la computadora con que estamos trabajando. Estos lenguajes de alto nivel son un medio de facilitar la labor del programador. Los programas en lenguaje máquina (primera generación) están escritos en el nivel más básico de operación de la computadora. Las instrucciones están dirigidas a ese nivel, el lenguaje máquina y los lenguajes programadores de segunda generación, que utilizan símbolos para las instrucciones reciben la designación de lenguaje de bajo nivel. Programar en estos lenguajes resulta ser arduo y tedioso. Casi toda la programación se hace en lenguajes de alto nivel (de la tercera generación y subsiguientes). Un lenguaje de programación es un conjunto de reglas, notaciones, símbolos y/o caracteres que permiten al programador poder expresar el pensamiento de datos y sus estructuras en la computadora, usando también una sintaxis y una gramática determinada. Análogamente, diremos que un programa es un conjunto de órdenes o instrucciones que resuelven un problema específico basado en un Lenguaje de Programación. Los Lenguajes de Programación se clasifican según su base de desarrollo y su uso en: —
Lenguajes basados en Cálculo Numérico: Fortran, Maple, Mathlab y Algol.
—
Lenguajes para Negocios: Cobol.
—
Lenguajes para la Inteligencia Artificial: Prolog, Adal, Lisp y Logo.
—
Lenguajes para sistemas: C y ensamblador.
También se pueden clasificar según la forma de ejecutar los programas:
6-4
—
Lenguajes imperativos. Son aquellos que son controlados por instrucciones imperativas. Pascal, Fortran y otros manejan este modelo.
—
Lenguajes aplicativos o funcionales. Son aquellos que manejan una preaplicación y dan una prerespuesta antes de aplicarlo realmente.
—
Lenguajes con base en reglas. Son los que ejecutan instrucciones en base al cumplimiento de ciertas condiciones.
—
Lenguajes orientados a objetos. Son los que manejan muchas instrucciones por medio de un objeto y que son controladas por pocas funciones.
Lenguajes de programación
Generalizando, definiremos los siguientes términos:
3.
—
Lenguaje. Un conjunto de símbolos, caracteres y reglas que permiten a los programadores comunicarse con las computadoras para que realicen algo.
—
Lenguaje de alto nivel. Lenguaje que se basa en instrucciones más globales y más poderosas, tal como los Visuales, C++ y otros más. El archivo resultado de un lenguaje de alto nivel es más grande que los de lenguaje de máquina.
—
Lenguaje de máquina. Lenguaje que usa instrucciones más directas hacia el procesador de la computadora, las cuales son más simples y más sencillas. Programa Fuente
Traductor
Programa Objeto
(Escrito por el Programador)
(compilador o intérprete)
(Lenguaje máquina)
Introducción a los lenguajes de programación
El lenguaje de programación es la forma en la que el programador escribe las operaciones que el ordenador debe realizar. La CPU esta preparada para manejar unas instrucciones escritas en un tipo de lenguaje muy simple llamado lenguaje máquina. Cada modelo de CPU posee su propio lenguaje máquina, y puede ejecutar un programa sólo si está escrito en ese lenguaje (para poder ejecutar programas escritos en otros lenguajes, es necesario primero trasladarlos a lenguaje máquina). El ordenador ejecuta mecánicamente los programas en lenguaje máquina esto es, sin entenderlos, o pensar sobre ellos simplemente porque es la única forma física de hacerlo. Las instrucciones del lenguaje máquina están expresadas con números binarios. Un número binario está compuesto únicamente por dos dígitos, cero y uno. Por tanto, las instrucciones del lenguaje máquina son una secuencia de ceros y unos. Cada secuencia concreta indica una instrucción determinada. Un interruptor ON, representa un uno, mientras que si está OFF, representa cero. Las instrucciones máquina están almacenadas en la memoria como conjuntos de interruptores en ON y en OFF. El ordenador realiza los cálculos por medio de estos interruptores que se van poniendo ON y OFF, siguiendo un patrón determinado al ejecutar cada una de las instrucciones del programa. En función de su parecido con el lenguaje natural, podemos hablar de lenguajes de bajo nivel y lenguajes de alto nivel. En los primeros la sintaxis está más próxima al lenguaje máquina que al lenguaje humano y en los de alto nivel es todo lo contrario. Cuando un programa es ejecutado directamente por el ordenador, es decir está en código máquina, decimos que es un lenguaje de bajo nivel. Casi todos los programas son escritos en lenguajes de alto nivel como Java, Pascal, o C++.
6-5
Desarrollo de Sistemas
4.
Generaciones en los lenguajes de programación
La clasificación de los lenguajes de programación viene estipulada por su aproximación al lenguaje utilizado por el ordenador, teniendo en cuenta que todo lenguaje tiene que acabar siendo traducido al propio lenguaje del ordenador. Según este criterio, los niveles de clasificación se pueden establecer por generaciones:
4.1.
Lenguajes de Primera Generación
En este primer bloque nos vamos a encontrar con un único lenguaje denominado lenguaje máquina difícil de diseñar, ya que está en relación directa con el hardware. Es el denominado lenguaje del ordenador, formado por tan sólo dos valores, el 1 y el 0, que representan los niveles alto y bajo de tensión. Así, cualquier expresión (instrucción) que se le quiera comunicar al procesador a través del programa deberá realizarse únicamente como expresión de 1 y 0, siguiendo una tabla de codificación interna de la que debe disponer el propio procesador. Con este lenguaje la tarea de programar se hace larga y tediosa, y la longitud de los programas resultantes es muy grande, por lo que su manejo pasa a ser complicado, así como las correcciones del mismo. Por el contrario, dispone de la ventaja de ser el lenguaje que más rápidamente se interpreta, al ser precisamente el propio lenguaje del procesador.
4.2.
Lenguajes de Segunda Generación
Con el paso del tiempo los programadores, cansados de escribir códigos máquina, llegaron a la conclusión de que todas aquellas expresiones que utilizaban en los programas, tarde o temprano volvían a repetirse, con lo que debían volver a rescribir el mismo código. Esto les llevó a idear una serie de abreviaturas denominadas nemotécnicos, consistentes en la representación de códigos binarios. Gracias a este invento cada programador tenía la facilidad de resumir todos sus códigos en muchas menos líneas, ya que las expresiones que necesitaban ser repetidas se hacían de forma abreviada gracias a estos nemotécnicos. Este lenguaje resultante recibió el nombre de Ensamblador. Y podemos destacar de él:
6-6
—
Su menor tamaño de programa con respecto al lenguaje máquina.
—
Su menor velocidad de ejecución, porque por supuesto todos los nemotécnicos deben ser traducidos por el procesador, invirtiendo un tiempo en ello que los hace más lentos que los lenguajes máquina.
—
Su dependencia todavía del hardware.
—
Su menor complejidad a la hora de ser diseñado, ya que cada programador conocía perfectamente sus propios nemotécnicos.
Lenguajes de programación
A los lenguajes de primera y segunda generación se les conoce con el nombre de Lenguajes de Bajo Nivel, ya que son próximos al lenguaje del ordenador.
4.3.
Lenguajes de Tercera Generación
Para reducir las deficiencias de los lenguajes de primera y segunda generación, surgieron nuevos lenguajes denominados de tercera generación, que se acercaban al lenguaje humano, y por tanto se alejaban del lenguaje máquina. Entre sus propiedades cabe destacar: —
Las instrucciones se obtienen mediante expresiones que tienen algún significado para el lenguaje humano: While, if, End, Then, etc. (Mientras, Si, Fin, Entonces, etc.).
—
Los programas sufren una reducción a la mínima expresión.
—
La construcción por parte del programador es realmente fácil.
—
Son independientes del hardware, aunque todavía se tiene problemas con respecto al sistema operativo en el que se trabaja.
—
La corrección de códigos es sencilla.
—
Su velocidad de ejecución disminuye con respecto a los lenguajes anteriores, pero con los avances tecnológicos, este dato parece carecer de importancia.
A los lenguajes de tercera generación se les dio el nombre de Lenguajes de Alto Nivel precisamente por su lejanía del lenguaje máquina. Estos lenguajes los podemos clasificar a su vez según un criterio de utilización:
4.4.
—
Lenguajes de Propósito general: son aquellos que son utilizados para la resolución de todo tipo de problemas, como por ejemplo C1, Cobol, Basic.
—
Lenguajes de Propósito específico: son aquellos que son utilizados para resolver problemas determinados según un área específica, como, por ejemplo, FORTRAN para resolver cálculos numéricos, COBOL para resolver problemas relacionados con la gestión de empresas, como contabilidad, nóminas, etc.
Lenguajes de Cuarta Generación
Con la aparición de estos lenguajes, el programador prácticamente se desentiende de “programar”, ya que sus expresiones son prácticamente similares al lenguaje humano.
1 “C” surgió inicialmente como lenguaje de propósito específico para la construcción de sistemas operativos, pero dada su versatilidad, se fue ampliando a otros campos y en la actualidad es considerado de propósito general.
6-7
Desarrollo de Sistemas
Los podemos clasificar en:
5.
•
Generadores de código. Genera de forma automática o semiautomática programas en lenguaje de alto nivel, empleando un número mucho menor de instrucciones que las que se necesitarían con los propios lenguajes de alto nivel.
•
Petición. Son la mayoría de ellos, y se basan en el manejo de bases de datos.
Tipos de programación
Para la obtención de programas que reúna los máximos requisitos de calidad, la programación ha ido evolucionando al mismo tiempo que los métodos y técnicas. Se han ido estableciendo diferentes criterios de programación: programación secuencial, programación modular, programación estructurada, programación orientada a objetos; son complementarias entre sí y por ello no excluyentes para la obtención de los programas requeridos.
5.1.
Programación secuencial
En los primeros pasos de los lenguajes de programación, éstos es escribían de arriba a bajo, todo ello en un solo archivo sin el uso de funciones ni modularidad. Si un trozo de código tenía un error había que revisarlo todo y volver a compilar el programa entero.
5.2.
Programación modular
En la programación modular, descomponemos el problema en una serie de operaciones simples cuya realización nos lleva a la resolución del problema complejo planteado; ahora bien, está descomposición en operaciones simples la agruparemos en módulos que funcionen independientes entre sí y que sean independientes del programa en el que actúen. En la programación modular encontraremos un programa que será el programa principal o módulo raíz. Dicho módulo estará compuesto principalmente de llamadas a otros módulos secundarios. La programación modular es una programación lineal, sencilla, donde aparece un punto de entrada o de comienzo y se va desarrollando de arriba abajo (top-down) con un punto final o de salida.
5.3.
Programación estructurada
La programación estructurada nos permite resolver problemas a partir de un solo punto de entrada (inicio) y otro de salida (final); su estructura con una composición lineal permite la utilización de estructuras más complejas: —
6-8
Estructura secuencial: operaciones consecutivas.
Lenguajes de programación
—
Estructura condicional: selección entre dos o más módulos si se cumple x.
—
Estructura repetitiva: se repiten una o varias veces dependiendo de una condición expresa.
5.4.
Programación orientada a objetos
Ante la dificultad para la optimización de los programas y la creciente complejidad de las aplicaciones, partiendo de la programación estructurada, a la que engloba, y dotando al programador de nuevos elementos para el análisis y desarrollo de software comienza la programación orientada a objetos, facilitando la producción de sistemas cada vez más complejos, que permiten modelar problemas no estructurados, incrementan la productividad gracias a la reutilización de objetos y facilitan su mantenimiento. En la programación orientada a objetos (POO) se utilizan conceptos y herramientas que modelan y representa el mundo real tan fielmente como sea posible. La POO proporciona ventajas sobre otros lenguajes de programación: —
Uniformidad. La representación de los objetos lleva implícita tanto el análisis como el diseño y codificación de los mismos.
—
Compresión. Los datos que componen los objetos y los procedimientos que los manipulan, están agrupados en clases, que se corresponden con las estructuras de información que el programa trata.
—
Flexibilidad. Al relacionar procedimientos y datos, cualquier cambio que se realice sobre ellos quedará reflejado automáticamente en cualquier lugar donde estos datos aparezcan.
—
Reutilización. La noción de objeto permite que programas que traten las mismas estructuras de datos reutilicen las definiciones de objetos empleadas en otros programas e incluso los procedimientos que los manipulan. De esta forma, el desarrollo de un programa puede llegar a ser una simple combinación de objetos ya definidos donde éstos están relacionados de una manera particular.
La POO no sustituye a ninguna metodología ni lenguaje de programación anterior. Todos los programas que se realizan según POO se pueden realizar igualmente mediante programación estructurada. Su uso en la actualidad se justifica porque el desarrollo de todas las nuevas herramientas basadas en una interface de usuario gráfico como Windows, X- Windows, etc. es mucho más sencillo. La POO fue estudiada en detalle en el Tema 5.
6-9
Desarrollo de Sistemas
6.
Primera generación
FORTRAN
Segunda generación
ALGOL-60
Tercera generación
Pascal
Cuarta generación
Ada
Programación Orientada a Objetos
Smalltalk y C++
Programación Funcional
LISP, Scheme y ML
Programación Orientada a la Lógica
PROLOG
Procesos en la programación Fases a seguir en todo proceso de programación: 1.
Análisis detallado del problema a resolver Plantearemos el problema que deseamos resolver, analizándolo detalladamente; para ello tendremos en cuenta una serie de factores como son: —
El equipo con el que contamos (hardware y software). Los datos iniciales o datos de entrada.
—
Tratamiento u operaciones que realizaremos con esos datos de entrada.
—
Los resultados o datos de salida que queramos obtener.
En este análisis estudiaremos las posibilidades de descomposición del problema en módulos más simples que faciliten la tarea de programación y de ejecución. 2.
Diseño del algoritmo Este diseño del algoritmo sería la descomposición de la ejecución del problema en tareas elementales. El algoritmo describirá la realización del problema complejo en operaciones básicas, sencillas y elementales a realizar por el ordenador. Estas operaciones son las instrucciones u órdenes que podemos dar al ordenador y se corresponden con las diferentes estructuras de control utilizadas en la programación estructurada; en función de dichas estructuras, se pueden agrupar en las siguientes instrucciones: —
6-10
Secuencias. Son instrucciones que se irán realizando ininterrumpidamente y en el orden exacto en el que estén transcritas.
Lenguajes de programación
3.
—
Iteraciones (bucles). Son conjuntos de instrucciones que se ejecutarán repetidamente mientras se cumpla unas condiciones determinadas.
—
Decisiones. El ordenador tomará ciertas decisiones, en función de los resultados que se produzcan en el transcurso del programa, escogiendo diferentes itinerarios u opciones diferentes.
—
Saltos. El ordenador desviará la secuencia lineal de instrucciones, saltando de unas a otras instrucciones en función de ciertas condiciones.
Programación Es la codificación del algoritmo o programación propiamente dicha en el lenguaje de programación elegido. El programador, en función de dicho lenguaje, utilizando la sintaxis y el vocabulario requerido, irá traduciendo el algoritmo al lenguaje de programación, utilizando las funciones que se corresponderán con las diferentes tareas a ejecutar, estructuras, etc.
4.
Obtención del programa Una vez codificado el algoritmo, necesitamos obtener el programa ejecutable, es decir el software que resolverá el problema de partida. Esta tarea se resolverá en tres fases: edición, compilación y montaje o linkado. La edición del programa es la escritura del mismo en el lenguaje de programación y dentro del sistema operativo utilizado; obtendremos el programa en un fichero de texto o código fuente. La compilación es la traducción del fichero de textos en lenguaje máquina, obteniéndose los ficheros código objeto o código máquina. Dicho proceso lo realizarán los programas compiladores o intérpretes. El montaje o linkado es el proceso de unión entre las distintas partes del lenguaje máquina para la obtención del fichero o programa ejecutable.
5.
Depuración del programa La depuración del programa es la comprobación de la bondad del mismo; estudiaremos el funcionamiento del programa en todas las situaciones posibles comprobando su correcto funcionamiento, para ello iremos introduciendo los diferentes valores posibles y comprobando el resultado; tendremos que volver al fichero de texto o código fuente cuantas veces sea necesario hasta que el definitivo programa ejecutable cumpla todas las condiciones requeridas, en ese momento el resultado es el óptimo y el programa está terminado.
6.
Documentación del programa Es la elaboración de la documentación técnica del programa. Se realizará en función del programa y de su futura utilización o comercialización. Dicha documentación se realizará a dos niveles: 6-11
Desarrollo de Sistemas
•
Documentación interna. Se elaborará una documentación a nivel de programación, especificándose cuantas aclaraciones y comentarios sean necesarios. Podrán ir dentro del propio programa y su función es simplificar la actualización del programa.
•
Documentación externa. La documentación será a nivel de usuario, en la que se especificará con toda claridad la instalación del programa, las condiciones técnicas o el sistema requerido para su funcionamiento, así como la descripción del funcionamiento del programa, datos de entrada, salida, etc.
1. Análisis del problema
Equipo: hardware y software. Datos iniciales, tratamiento, resultados. Descomposición del problema en módulo.
2. Diseño del algoritmo– descomposición solución en tareas elementales
Secuencias. Iteraciones. Decisiones. Saltos.
3. Codificación–programación del algoritmo
Traducción al lenguaje elegido de programación.
4. Obtención programa ejecutable
Edición: Código fuente. Compilación: Código objeto. Linkado: Programa ejecutable.
5. Prueba, verificación y depuración del programa
Estudio en todas las situaciones.
6. Documentación del programa–elaboración de manuales
Documentación interna. Documentación externa.
Los principales errores en la ejecución de un programa son:
7.
—
Datos de entrada incorrectos que producen una parada del sistema (por ejemplo, introducir un dividendo con valor cero en una operación de división).
—
Bucles mal definidos que producen un funcionamiento continuo del programa (por ejemplo, un bucle sin fin o bucle infinito).
—
Datos de salida incorrectos, producidos por un mal desarrollo del programa o ambigüedad en las especificaciones del usuario.
Tipos de lenguajes de programación
Dentro de los lenguajes de alto nivel podemos distinguir entre lenguajes compilados y lenguajes interpretados; dicha distinción se realiza en función de la forma en que generamos el programa ejecutable. Un lenguaje de alto nivel 6-12
Lenguajes de programación
no puede ser ejecutado directamente por ningún ordenador; es necesario trasladarlo a lenguaje máquina.
7.1.
Lenguajes compilados
Los lenguajes compilados son aquellos que, una vez escrito el código fuente, permite obtener un programa ejecutable y autónomo, susceptible de ser ejecutados en cualquier máquina bajo el sistema operativo para el que fue diseñado. En estos lenguajes de programación es imprescindible la existencia de un compilador. El compilador es un programa complejo que traduce todo el programa de golpe en código máquina. Es decir, el programa compilador traduce las instrucciones de un lenguaje de alto nivel en instrucciones de lenguaje máquina que la computadora puede interpretar y ejecutar. Un compilador traduce en instrucciones de lenguaje de máquina las instrucciones de lenguaje de alto nivel, llamadas programa fuente. El resultado de la compilación es el programa objeto. El código objeto también se suele denominar código binario o código máquina. Una vez que el programa está compilado, las líneas de código fuente dejan de tener sentido durante la ejecución del programa.
7.2.
Lenguajes interpretados
Los lenguajes interpretados son aquellos en los que el programa fuente necesita de un programa intérprete para ser ejecutado. El intérprete traduce instrucción por instrucción a medida que va siendo necesario. Un intérprete es un programa que trabaja de una forma muy semejante a la CPU, con una especie de ciclo de leer y ejecutar. Para poder ejecutar un programa, el intérprete ejecuta un bucle en el que va leyendo las instrucciones una a una, decide si se han de ejecutar y, si es así, las convierte en el código máquina apropiado. El intérprete puede permitir el uso de un programa en código máquina de un ordenador, en otro tipo de ordenador completamente diferente. Un intérprete cumple las mismas funciones que un compilador, aunque en diferente forma. En vez de traducir íntegramente el programa fuente en una sola pasada, traduce y ejecuta cada instrucción de él antes de traducir y ejecutar la siguiente. La ventaja de los intérpretes sobre los compiladores radica en que, si hay error en la sintaxis de instrucciones, éste se indica al programador de inmediato, con lo cual se le permite hacer las correcciones durante el desarrollo del programa. Las desventajas del intérprete consiste en que no utiliza los recursos de la compilación con la misma eficiencia con que un programa que ha sido compilado. Como el intérprete no produce programa objeto, debe hacer el proceso de traducción cada vez que un programa se corre, línea por línea. Con la compilación separada, el programa, por la extensión del código fuente, no puede ser compilado en un solo bloque, con lo que se compilará por partes, obteniéndose diferentes códigos objetos; una vez completado todo el programa, todos los códigos objetos que conforman un programa se agruparán obteniéndose el código o fichero ejecutable. Los lenguajes ensambladores y máquina son dependientes de la máquina. Cada tipo de máquina tiene su propio lenguaje máquina distinto y su lenguaje ensamblador asociado. El lenguaje ensamblador es simplemente una representación simbólica del lenguaje máquina asociado, lo cual permite una programación menos tediosa que con el anterior. 6-13
Desarrollo de Sistemas
La programación en un lenguaje de alto nivel o en un lenguaje ensamblador requiere algún tipo de interfaz con el lenguaje máquina para que el programa pueda ejecutarse. Las tres interfaces más comunes son: un ensamblador, un compilador y un intérprete. El ensamblador y el compilador traduce el programa a otro equivalente en el lenguaje de la máquina residente como un paso separado antes de la ejecución. Por otra parte, el intérprete ejecuta directamente las instrucciones en un lenguaje de alto nivel, sin un paso de procesamiento previo. La compilación es un proceso mas eficiente que la interpretación para la mayoría de los tipos de máquinas. Esto se debe principalmente a que las sentencias dentro de un bucle deben ser interpretadas cada vez que se ejecutan por un intérprete. Con un compilador, cada sentencia es interpretada y luego traducida a lenguaje máquina solo una vez. Algunos lenguajes son lenguajes principalmente interpretados, como APL, PROLOG y LISP, JAVA, etc. Ejemplos de lenguajes compilados son: PASCAL, FORTRAN, COBOL, PL/I, SNOBOL, C, ADA, etc. En algunos casos, un compilador estará utilizable alternativamente para un lenguaje interpretado y viceversa.
8.
Estilos de programación
De acuerdo con el estilo de programación, podemos clasificar los lenguajes en las siguientes categorías: •
Imperativos: son aquellos lenguajes que basan su funcionamiento en un conjunto de instrucciones secuenciales, las cuales, al ejecutarse, van alterando las regiones de memoria donde residen todos los valores de las variables involucradas en el problema que se plantea resolver. Es decir, se cambia progresivamente el estado del sistema, hasta alcanzar la solución del problema. Está basada en el modelo Von Neumann, en donde un conjunto de operaciones primitivas realizan una ejecución secuencial. Realiza una abstracción en el manejo de variables, expresiones e instrucciones y para programar es necesario declarar las variables necesarias y diseñar una secuencia adecuada de instrucciones (asignaciones). Algunos de los lenguajes de este tipo son Pascal, Ada y C.
•
Declarativos: en este paradigma, más que el cómo desarrollar paso a paso un proceso, nos interesa el qué deseamos obtener a través del programa. El ejemplo típico de lenguaje declarativo es SQL, el cual es utilizado para interactuar con la información de bases de datos, concentrándose sólo en los resultados que van a ser obtenidos, dejándole al traductor la tarea de cómo llegar a ellos y presentárnoslos. Dentro de este paradigma, se encuentran dos estilos distintos de programación, cada uno de los cuales posee su propia lógica: —
6-14
Funcionales: son lenguajes basados en funciones, las cuales se representan mediante expresiones que nos permiten obtener ciertos resultados a partir de una serie de argumentos. De
Lenguajes de programación
hecho, las expresiones están formadas por un conjunto de términos, que a su vez pueden encapsular otras expresiones, para con la evaluación de todas ellas, llegar a la solución deseada. El programa es una función (o un grupo de funciones). La relación entre las funciones son muy simples: una función puede llamar a otra función, o el resultado de una función puede ser usado como el argumento de otra función. Las variables, comandos y efectos laterales son exclusivos. Los programas son escritos enteramente dentro del lenguaje de expresiones, funciones y declaraciones. Dos de estos lenguajes son Scheme y ML. —
•
Lógicos: este tipo de lenguajes se basan en el cálculo de predicados, la cual es una teoría matemática que permite entre otras cosas, lograr que un ordenador basándose en un conjunto de hechos y de reglas lógicas, pueda derivar en soluciones inteligentes. La Programación Lógica está basada en la noción de relación, debido a que en la relación es un concepto más general de una aplicación. La Programación Lógica es potencialmente de alto nivel. Los lenguajes de Programación Lógica pueden explotar la Inteligencia Artificial. Un lenguaje de este tipo es Prolog.
Programación Orientada a Objetos: los programas de este tipo, se concentran en los objetos que van a manipular, y no en la lógica requerida para manipularlos. La programación orientada al objeto está basada en los objetos, clase, método, envío y recepción de mensajes, herencia y polimorfismo. Algunos de los lenguajes de este tipo son C++, JAVAy Smalltalk. La Orientación a Objetos está constituyendo una metodología de diseño y desarrollo de “software” de gran trascendencia para la producción de software eficiente y barato. Esta metodología de análisis, diseño y programación, que configura las fases fundamentales del ciclo de vida de un sistema informático, se está asentando como la estructura metodológica de los años 90, y se presenta educacionalmente como paradigmática en el desarrollo de aplicaciones. La Orientación a Objetos se puede definir como una disciplina de ingeniería de desarrollo y modelado de software que permite construir más fácilmente sistemas complejos a partir de componentes individuales. La Orientación a Objetos permite una representación más directa del modelo del mundo real, reduciendo fuertemente la transformación radical normal desde los requerimientos del sistema, definidos en términos del usuario, a las especificaciones del sistema, definidas en términos del computador. Actualmente la tendencia de la Ingeniería Informática es la producir componentes reutilizables para ensamblarlos unos a otros y obtener así el producto completo. Estos elementos reutilizables son denominados “Componentes Integrados de Software” (CIS) por su teórica similitud con los “componentes integrados de hardware” (chips), innovación que revolucionó la industria del computador en los años 70. El Paradigma Orientado a Objetos es, pues, una filosofía de desarrollo y empaquetamiento de 6-15
Desarrollo de Sistemas
“software” que permite crear unidades funcionales extensibles y genéricas, de forma que el usuario las pueda aplicar según sus necesidades y de acuerdo con las especificaciones del sistema a desarrollar. La Orientación a Objetos proporciona mejores herramientas para: —
Modelar el mundo real de un modo más cercano a la perspectiva del usuario.
—
Interactuar fácilmente con un entorno computacional, usando metáforas familiares.
—
Construir componentes reutilizables de software y bibliotecas específicas de estos componentes fácilmente extensibles.
—
Modificar y ampliar con facilidad la implementación de estos componentes sin afectar al resto de la estructura.
En cuanto a los elementos fundamentales que configuran el Paradigma Orientado a Objetos, se suelen significar siete: —
Estructura modular basada en objetos, dado que los sistemas en esta metodología son modularizados sobre la base de sus estructuras de datos.
—
Abstracción de datos, porque los objetos son descritos como implementaciones de tipos abstractos de datos.
—
Gestión automática de memoria, de forma que los objetos no utilizados sean desasignados por el propio sistema sin intervención del programador.
—
Clases, en las que cada tipo no simple sea un módulo, y cada módulo de alto nivel sea un tipo.
—
Herencia, que permita que una clase sea definida como una extensión o restricción de otra.
—
Polimorfismo y enlace dinámico, de forma que las entidades del programa puedan referenciar en tiempo de ejecución a objetos de diferentes clases.
—
Herencia múltiple y repetida para que se pueda declarar una clase como heredera de varias, e incluso de ella misma.
El Paradigma Orientado a Objetos se describe a menudo usando el concepto de Objeto/Mensaje, en el que cada objeto (elemento autónomo de información creado en tiempo de ejecución) es solicitado para realizar un determinado servicio mediante el envío a ese objeto del mensaje apropiado. El solicitante no precisa conocer cómo el objeto proporciona el servicio pedido; la implementación es interna al objeto y la gestiona el suministrador del objeto. El énfasis se produce en qué se puede obtener más que en cómo se obtiene.
6-16
Lenguajes de programación
Un programa orientado a objetos viene definido por la ecuación: OBJETOS + MENSAJES = PROGRAMA Aquí el objeto es una instancia de una clase, la cual implementa un tipo abstracto de dato (TAD). Y el mensaje es la información específica que se envía al objeto para que ejecute una determinada tarea. Un TAD define conjuntos encapsulados de objetos similares, con una colección asociada de operaciones; y especifica la estructura y el comportamiento de los objetos. Las especificaciones estructuradas del TAD describen las características de los objetos pertenecientes a ese TAD, y las especificaciones de comportamiento describen qué mensajes son aplicables a cada objeto. •
8.1.
Programación orientada al evento: Esta programación es el resultado de la programación orientada al objeto. En este tipo de programación permite trabajar con objetos y clases estándar previamente definidas por la aplicación, las cuales manejan los conceptos de encapsulasión. Las herramientas que trabajan de esta forma por lo general se trabajan con código original de lenguajes imperativos. Algunas herramientas de este tipo son Visual Basic (Basic), Delphi (Pascal) y Power Builder (C).
Programación estructurada
Existen en la actualidad dos formas o metodologías básicas de construcción de software: la programación estructurada u orientada al flujo de datos, y la programación orientada a objetos. Es evidente que un programa tiene como finalidad la resolución de un determinado problema, o la realización de determinada tarea, pero para ello no hay una forma única. Se pueden hacer diferentes programas, o algoritmos de resolución, que cumplan todos ellos un objetivo propuesto. Por esto debemos conocer las reglas o principios que nos permitan la elección del más adecuado, pues todos ellos incidirán directamente en el coste de su diseño y posterior mantenimiento, no olvidando nunca una premisa fundamental: se desarrolla para mantener. Con independencia de la metodología empleada en su construcción, los programas deben cumplir unas características generales como: ser legibles (fáciles de leer y comprender, por lo que hay que comentarlos ampliamente en sus partes complejas), portables (fáciles de codificar en otros lenguajes o en otros sistemas y configuraciones físicas), fácilmente modificables (para facilitar su mantenimiento), eficientes (para aprovechar bien los recursos), modularizables (descomponer el problema general de arriba abajo, top-down, en bloques o módulos a diferentes niveles), y estructurados (siguiendo un método y unas normas básicas). Todas estas características van dirigidas tanto a facilitar su implementación como su verificación y depuración, así como su posterior y seguro mantenimiento. Todo ello tiene como resultado final el que los costes y el esfuerzo personal de todo el proceso sean menores.
6-17
Desarrollo de Sistemas
Si consideramos cualquier centro de desarrollo de programas con proyectos en curso, veremos que, frecuentemente, los programadores que comenzaron el proyecto no siguen en el mismo centro o que han pasado a trabajar en otros proyectos. Por ello es de vital importancia que un programa desarrollado inicialmente por una persona sea fácilmente ampliado y modificado por otra distinta. Ésta es una de las ventajas de la programación estructurada. Los programas escritos sin un determinado método suelen tener problemas como los siguientes: —
Suelen ser demasiado rígidos, con problemas al adaptarlos a distintos entornos y configuraciones.
—
Los programadores pasan la mayoría del tiempo corrigiendo sus errores.
—
Los programadores rehúsan el uso de programas y módulos ya escritos y en funcionamiento, pues prefieren escribir los suyos. La comunicación entre ellos es difícil.
—
Un proyecto de varios programadores suele tener varios conjuntos diferentes de objetivos.
—
Cada programador tiene sus propios programas, y esta relación se hace inseparable.
—
Las modificaciones en aplicaciones y programas son muy difíciles de hacer, implican mucho tiempo y un elevado coste de mantenimiento. Ello conduce, bien a colocar “parches” que complican cada vez más el diseño inicial, o bien a que el programa caiga en desuso y que frente al elevado coste de actualización se opte por crear una nueva aplicación que sustituya a la existente.
—
Deficiencias en la documentación: incompleta o no actualizada.
Se hace preciso pues realizar programas siguiendo técnicas o métodos estandarizados que consiguen las características anteriormente descritas, rápida y eficazmente. Las técnicas de programación que permiten seguir una metodología de la programación más empleadas son la programación modular y la programación estructurada. Estas dos técnicas suelen ser complementarias, ya que en el análisis de un problema pueden utilizarse criterios de programación modular para dividirlo en partes independientes y utilizar métodos estructurados en la programación de cada módulo. Por ello no debe causarnos extrañeza que en la actualidad se difundan con gran fuerza las técnicas de programación estructurada, cuyo objetivo principal consiste en: —
Facilitar la comprensión del programa.
—
Permitir rápidamente el mantenimiento del mismo, a lo largo de su vida útil.
Una forma de simplificar el diseño de algoritmos es utilizar la técnica de diseño descendente de programas, Top-down (de arriba abajo), que consiste en descomponer un problema en una serie de niveles o pasos sucesivos de refinamiento (stepwise). La metodología descendente consiste en efectuar una relación entre las sucesivas etapas de estructuración de modo que se relacio6-18
Lenguajes de programación
nen unas con otras mediante entradas y salidas de información. Es decir, se descompone el problema en etapas o estructuras jerárquicas, de modo que se puede considerar cada estructura desde dos puntos de vista: lo que hace, y cómo lo hace. La programación estructurada sigue completamente las directrices top-down. En la programación tradicional se utilizan de un modo excesivo, indiscriminado, y a veces caprichoso, las instrucciones de bifurcación condicional e incondicional, lo que hace difícil el seguimiento de la lógica del programa y consecuentemente sus necesarias modificaciones futuras. La programación estructurada tiene como uno de sus fines la eliminación de los problemas descritos; por ello las instrucciones de bifurcación o saltos han sido eliminadas, o por lo menos seriamente restringidas en su utilización. Es por ello que un programa estructurado, en una secuencia normal de lectura, puede ser fácilmente leído en su totalidad, sin saltos ni búsquedas incontroladas. En cierto sentido, la programación estructurada ha sido precursora del diseño orientado a objetos, dado que los programadores en programación estructurada, dentro de la fase de diseño deben realizar sus diferentes tareas en forma de módulos, subrutinas, o bloques, los cuales son susceptibles de estandarizarse, por lo que se pueden incluir como elementos dentro de bibliotecas de programas para su futura utilización en otros programas e incluso para diversas aplicaciones. Así tenemos la reutilización del código. El concepto de objeto, si bien es más amplio, puede tener como origen estos elementos de biblioteca. Vemos pues que con la programación estructurada se consigue hoy en día producir buen código, dado que se utilizan estructuras estándar de control para mejorar la calidad y el mantenimiento de los programas. Las estructuras de control fomentan el desarrollo de programas de alto nivel por expansión ordenada de bloques de programa; dado además que las estructuras de control son limitadas, se minimiza la complejidad de los problemas y por consiguiente se reducen los errores. Los diseñadores especifican funciones de alto nivel con un bloque de programa, y este bloque es entonces expandido en más componentes detallados, basándose en que la programación estructurada se auxilia en los denominados Recursos Abstractos, en lugar de los recursos concretos de que se dispone en un determinado lenguaje de programación. Así, descomponer un programa en términos de recursos abstractos consiste en descomponer una determinada acción compleja en términos de un número de acciones más simples, que uno es capaz de ejecutarlas o que constituyen instrucciones disponibles de un computador. Con todo ello, la documentación que se produce es mucho más legible. Cada bloque debe desarrollar una función bien definida, siendo una buena práctica de programación el intercalar comentarios interactivos que mejoren aún más su legibilidad. Se debe pues definir cada bloque de la estructura de control y describir sus propósitos. Los enlaces con las descripciones de salida del código también deben ser incluidos en la documentación. Es relativamente fácil utilizar las especificaciones para crear código bien estructurado. Las especificaciones de proceso usan palabras clave muy similares a las construcciones utilizadas en programación estructurada. Las sentencias aritméticas o de transformación de las especificaciones del proceso se reemplazan por la gramática utilizada en el lenguaje de programación.
6-19
Desarrollo de Sistemas
Tenemos, en consecuencia, que un programa estructurado es: fácil de comprender en su lectura; fácil de codificar en diversos lenguajes; fácil de implantar en diferentes sistemas; fácil de documentar; fácil de mantener; eficiente; modularizable, pues es un valor añadido por la propia técnica de diseño. Como resumen podemos concluir que la programación estructurada es el conjunto de técnicas que incorporan: diseño descendente (top-down), recursos abstractos, y estructuras básicas. En 1966 Böhm y Jacopini demostraron que todo programa propio, sea cual sea el trabajo que tenga que realizar, se puede hacer utilizando tres únicas estructuras de control, que son la secuencial, la selectiva y la repetitiva. Un programa se define como propio si cumple las tres siguientes características: —
Posee sólo un punto de entrada y uno de salida o fin para control de programa.
—
Existen caminos desde la entrada hasta la salida que se pueden seguir y que pasan por todas las partes del programa.
—
Todas las instrucciones son ejecutables y no existen lazos o bucles infinitos.
Así pues podemos definir la programación estructurada como aquella que utiliza siempre una estructura con un único punto de entrada y un único punto de salida, y que utiliza solo tres estructuras de control: la secuencial, la selectiva o alternativa (simple, doble, múltiple), y la repetitiva: —
Estructura secuencial: se trata de una estructura con solo un punto de entrada y uno de salida, compuesta por una serie de tareas que también tienen un solo punto de entrada y de salida, y donde cada tarea sigue a otra en secuencia. Las tareas se suceden de modo que la salida de una es la entrada de la siguiente, y así sucesivamente hasta el final del proceso.
—
Estructura selectiva o alternativa: se utiliza para tomar decisiones lógicas. En ella se evalúa una condición, y en función del resultado de la misma se realiza una opción u otra. Las estructuras selectivas pueden ser simples, dobles o múltiples. La alternativa simple es la típica “si-entonces” (IF-THEN), donde se evalúa la condición, y si esta es verdadera se ejecuta una determinada acción, y si es falsa entonces no se hace nada. La alternativa doble ejecuta una acción diferente en cada caso posible de evaluación de la condición (verdadero o falso). Es el “si-entonces-sino” (IF-THEN-ELSE). La alternativa múltiple se podría realizar con las dos anteriores estructuras anidadas o en cascada, pero la legibilidad del programa podría verse comprometida. Por ello esta estructura múltiple se incluye también. Es la típica instrucción “según-sea, caso de” (CASE OF).
—
6-20
Estructura repetitiva: es el algoritmo necesario para repetir una o varias acciones un número determinado de veces. Estas estructuras
Lenguajes de programación
se denominan bucles. Para limitar el número de veces que debe repetirse el bucle hay que contar con una condición, para lo que se suele utilizar una variable que se incrementa con cada ejecución. Es la típica construcción “mientras” (WHILE).
9.
Otros conceptos base en programación Existen algunos términos añadidos que debemos de tener claros como son: —
Código fuente. Es el texto de un programa que un usuario puede leer, normalmente considerado como el programa en sí. El código fuente es la entrada al compilador o intérprete.
—
Código objeto. Es la traducción a través del compilador del código fuente a código máquina, que es el que el ordenador puede leer y ejecutar. El código objeto es la entrada al enlazador.
—
Enlazador. Es un programa que enlaza módulos compilados por separado para producir un solo programa. La salida del enlazador es un programa ejecutable.
—
Tiempo de compilación. Es el tiempo que tarda el compilador en traducir el código fuente a código objeto.
10. Aplicaciones de los lenguajes de programación Las aplicaciones de los lenguajes de programación vienen dadas por el programa que se crea con él. Los programas se pueden clasificar por diversos tipos: aquí realizaremos una pequeña clasificación funcional, como la siguiente: APLICACIÓN
LENGUAJE
NEGOCIOS
COBOL, C, 4GL, PL/I
CIENTÍFICA
FORTRAN, C, C++, BASIC, PASCAL, APL, ALGOL
SISTEMAS
JOVIAL C, C++, PASCAL, ADA, BASCI, MODULA
IA
LISP, PROLOG, SNOBOL
EDICIÓN
TEX, POSTSCRIPT
PROCESO
SHELL DE UNIX, TCL; PERL, MARVEL
NUEVOS PARADIGMAS
ML, SMALLTALK, EIFFEL
INTERNET (USUARIO)
HTML, DHTML, XML, SCRIPT
INTERNET (SERVIDOR)
PHP, ASP, JSP
6-21
Desarrollo de Sistemas
11. Historia de los lenguajes de programación La historia de los lenguajes de programación se remonta a la época anterior a la II Guerra Mundial; no obstante es a partir de los años setenta cuando tiene su mayor auge. A partir de los años sesenta, empiezan a surgir diferentes lenguajes de programación, atendiendo a diversos enfoques, características y propósitos. Actualmente existen alrededor de 2000 lenguajes de programación 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. A continuación detallamos algunas referencias interesantes. AÑO
LENGUAJE
INVENTOR
USO
1946
Plankalkul
Konrad Zuse
Jugar al ajedrez.
1949
Short Code
1950
ASM (ensamblador)
1951
A-0
Grace Hopper
1952
AUTOCODE
Alick E. Glennie
Compilador rudimentario.
1956
FORTRAN
IBM
Traducción de fórmulas matemáticas.
1956
COBOL
1958
ALGOL 58
1960
LISP
1961
FORTRAN IV
1961
COBOL 61 Extendido
1960
ALGOL 60 Revisado
1964 1964 1965
SNOBOL
Lenguaje traducido a mano. Lenguaje ensamblador. Primer compilador.
Compilador.
Interprete orientado a la Inteligencia Artificial. IBM
Traducción de fórmulas matemáticas.
PASCAL
Niklaus Wirth
Programación estructurada.
BASIC
Universidad de Dartmouth
1965
APL
1965
COBOL 65
1966
PL/I
1966
FORTRAN 66
1967
SIMULA 67
1968
ALGOL 68
1968
SNOBOL4
+1970
GW-BASIC
1970
APL/360
1972
SMALLTALK
Xerox
Pequeño y rápido.
1972
C
Laboratorios Bell
Lenguaje con tipos.
1974
COBOL 74
1975
PL /I
1977
FORTRAN 77
6-22
IBM
Antiguo BASIC.
Lenguaje sencillo. IBM
Lenguajes de programación
AÑO
LENGUAJE
INVENTOR
USO
+1980
SMALLTALK/V
Digitalk
Pequeño y rápido.
1981
PROLOG
Ministerio Japonés
Lenguaje para la Inteligencia Artificial.
1982
ADA
Ministerio Defensa EE.UU
Lenguaje muy seguro.
1984
C++
AT&T Bell Laboratories
PROG. ORIENTADA A OBJETOS.
1985
CLIPPER
1985
QuickBASIC 1.0
Microsoft®
Compilador para bases de datos. Vompilador de BASIC.
1986
QuickBASIC 2.0
Microsoft®
Soporte de tarjeta gráfica EGA.
1987
QuickBASIC 3.0
Microsoft®
43 líneas con la tarjeta EGA.
1987
QuickBASIC 4.0
Microsoft®
1987
CLIPPER '87
1988
QuickBASIC 4.5
Microsoft®
1989
QuickBASIC 7.1
Microsoft®
Tarjetas Hércules, VGA. Compilador para bases de datos. Tarjeta SVGA.
1989
ASIC V5.0
+1990
VISUAL C++
Interprete tipo QBASIC shareware. Entorno visual de C++.
+1990
JavaScript
Lenguaje de Script (GUIONES).
+1990
VBScript
1993 1993
Microsoft®
Lenguaje de Script (GUIONES).
HTML
Tim Berners-Lee
Surge para su uso en Internet.
XML
C. M. Sperberg-McQueen
Surge para su uso en Internet.
+1990
SGML
Charles F. Goldfarb
Surge para su uso en Internet.
+1990
WML
+1990
ASP
Microsoft®
Uso en Internet (SERVIDOR).
+1990
PHP
1995
JAVA
1995
DELPHI
Surge para su uso en Internet. Uso en Internet (SERVIDOR). Sun Microsystems
1995
CLIPPER 5.01
1995
GNAT ADA95
Ministerio Defensa EE.UU
1995
FORTRAN 95
IBM
1991
VISUAL BASIC 1.0
Microsoft®
1992
VISUAL BASIC 2.0
Microsoft®
1993
VISUAL BASIC 3.0
Microsoft®
1994
VISUAL BASIC 4.0
Microsoft®
1995
VISUAL BASIC 5.0
Microsoft®
1998
VISUAL BASIC 6.0
Microsoft®
1998
JAVA(JDK 1.2)
1998
JSP
Applets y aplicaciones. Compilador para bases de datos.
Sun Microsystems
+-1999
C#
1999
Delphi 5
2000
JAVA(JDK 1.3)
2001
.NET
Microsoft®
2002
JAVA(JDK 1.4)
Sun Microsystems
Lenguaje muy seguro. Entorno visual de BASIC.
Uso en Internet (SERVIDOR). PROG. ORIENTADA A OBJETOS.
Sun Microsystems PROG. ORIENTADA A OBJETOS.
6-23
Desarrollo de Sistemas
12. Algunos lenguajes de programación 12.1. ADA Nombrado en honor de la primera persona programador de computadoras del mundo, AUGUSTA ADA BYRON KING, Condesa de Lovelace, e hija del poeta inglés Lord Byron. Ada es un idioma de la programación de alto nivel pensado para las aplicaciones en vías de desarrollo donde la exactitud, seguridad, fiabilidad y manutención son primeras metas. Ada es un lenguaje del tipo orientado a Objeto. Se piensa que trabaja bien en un ambiente del multi-lenguaje y ha estandarizado los rasgos para apoyar la unión a otros idiomas. La Razón de Ada proporciona una descripción de los rasgos principales del idioma y sus bibliotecas y explicaciones hacen lo propio con las opciones hechas por los diseñadores del idioma.
12.2. COBOL El deseo de desarrollar un lenguaje de programación que fuera aceptado por cualquier marca de computadora reunió en Estados Unidos, en mayo de 1959, una comisión (denominada CODASYL: Conference On Data Systems Languages) integrada por fabricantes de computadoras, empresas privadas y representantes del Gobierno, dando lugar a la creación del lenguaje COBOL (COmmon Business Oriented Language) orientado a los negocios, llamándose ésta primera versión COBOL-60, por ser éste el año que vio la luz. COBOL estaba en constante evolución gracias a las sugerencias de los usuarios y expertos, dando lugar a las revisiones de 1961, 1963 y 1965. La primera versión standard nació en 1968, siendo revisada en 1974, llamadas COBOL ANSI o COBOL-74, muy extendidas todavía. En la actualidad es COBOL-85 la última versión revisada del lenguaje COBOL, estando pendiente la de 1997. ¿Por qué se hablaba de fabricantes de computadoras y no de Sistemas Operativos, como en la actualidad? Sí que es significativo, pero por aquellos años no existían Sistemas Operativos abiertos, sino que cada fabricante tenía su propio Sistema Operativo y por lo tanto cada Cobol debería valer para cada computadora. Ciertamente no había mucha diferencia entre ellos. Cobol es un lenguaje compilado, es decir, existe el código fuente escrito con cualquier editor de textos y el código objeto (compilado) dispuesto para su ejecución con su correspondiente runtime. Cuando se ve un programa escrito en Cobol saltan a la vista varios aspectos:
6-24
—
Existen unos márgenes establecidos que facilitan su comprensión.
—
Está estructurado en varias partes, cada una de ellas con un objetivo dentro del programa.
Lenguajes de programación
—
Nos recuerda mucho al idioma inglés, puesto que su gramática y su vocabulario están tomados de dicho idioma.
—
En contraste con otros lenguajes de programación, COBOL no se concibió para cálculos complejos matemáticos o científicos; de hecho sólo dispone de comandos para realizar los cálculos mas elementales: suma, resta, multiplicación y división; sino que su empleo es apropiado para el proceso de datos en aplicaciones comerciales, utilización de grandes cantidades de datos y obtención de resultados, ya sea por pantalla o impresos.
Con Cobol se pretendía un lenguaje universal, a pesar de lo cual los numerosos fabricantes existentes en la actualidad han ido incorporando retoques y mejoras, aunque las diferencias esenciales entre ellos es mínima. Con la llegada del Sistema Operativo Windows, son muchos los que intentan proveer al Cobol de esa interface gráfica: Objective Cobol, Visual Object Cobol de Microfocus, Fujitsu PowerCobol, Acucobol-GT, Vangui y Cobol-WOW de Liant (RM), etc… que están consiguiendo que este lenguaje siga estando presente en moda visual de ofrecer los programas. Sin embargo, son muchas las empresas que siguen dependiendo del Cobol-85 tradicional para sus proyectos debido principalmente a la estructura de su sistema informático.
12.3. FORTRAN FORTRAN que originalmente significa Sistema de Traducción de Fórmulas Matemáticas pero se ha abreviado a la FORmula TRANslation, es el más viejo de los establecidos lenguajes de “alto-nivel”, fue diseñado por un grupo en IBM durante los años 50. El idioma se hizo tan popular en los 60 que otros vendedores empezaron a producir sus propias versiones y esto llevó a una divergencia creciente de dialectos (a través de 1963 había 40 recopiladores diferentes). Así las cosas, fue reconocido que tal divergencia no estaba en los intereses de los usuarios de la computadora o los vendedores de la computadora, por lo que FORTRAN 66 fue el primer idioma en ser regularizado oficialmente en 1972. La publicación de la norma significó que ese FORTRAN se llevó a cabo más ampliamente que cualquier otro idioma. A mediados de los años setenta se proporcionó virtualmente a cada computadora, mini o mainframe, con un sistema FORTRAN 66 normal. Era, por tanto, posible escribir programas en FORTRAN en cualquier sistema y estar bastante seguro que éstos pudieran moverse para trabajar en cualquier otro sistema de forma bastante fácil. Esto hacía que pudieran procesarse programas de FORTRAN muy eficazmente. La definición normal de FORTRAN se puso al día en 1970 y una nueva norma, ANSI X3.9-1978, fue publicada por el Instituto de las Normas Nacional americana. Esta norma fue adoptada en 1980 por la Organización de Normas Internacionales (ISO) como una Norma Internacional (ES 1539: 1980). El idioma es normalmente conocido como FORTRAN 77 (desde que el proyecto final realmente se completó en 1977) y es ahora la versión del idioma en su uso extendido.
6-25
Desarrollo de Sistemas
El FORTRAN fue un lenguaje verdaderamente revolucionario, pues antes de él todos los programas de computadores eran lentos, tendenciosos y originaban muchos errores. En los primeros tiempos, un programador podía escribir el algoritmo deseado como una serie de ecuaciones algebraicas y el compilador FORTRAN podía convertir las declaraciones en lenguaje de máquina que el computador podía reconocer y ejecutar. El lenguaje FORTRAN original era muy pequeño en comparación con las versiones modernas. Contenía apenas un número limitado de declaraciones tipo, y sólo se podía trabajar con el tipo “integer” (entero) y “real” (real), y tampoco había subrutinas. Cuando se comenzó a usar este programa, se verificó la existencia de diversos errores, por lo que la IBM lanzó el FORTRAN II en 1958. El desarrollo continuó en 1962, con el lanzamiento del FORTRAN IV. Éste tenía muchas mejoras y por eso se convirtió en la versión más utilizada en los quince años siguientes. En 1977 el lenguaje recibe otra actualización muy importante, incluyendo muchas nuevas características que permitían escribir y guardar más fácilmente programas estructurados. El FORTRAN 77 introducía nuevas estructuras, como el bloque IF, y fue la primera versión del lenguaje en que las variables “character” (caracteres) eran realmente fáciles de manipular. Este lenguaje se volvió un poco limitado en términos de estructuras de información y también por sólo permitir la codificación de algunas figuras de programación estructurada. La siguiente mejora fue más importante y dio origen al FORTRAN 90. Éste incluía todo el FORTRAN 77 como base pero con cambios significativos, fundamentalmente en las operaciones sobre tablas (array) pero también sobre: una configuración en parámetros de las funciones intrínsecas, permitiendo así utilizar una secuencia de caracteres muy grande, como también usar más de dos tipos de precisión para variables del tipo Real y Complex; se perfeccionó la computación numérica con la inclusión de un conjunto de funciones numéricas, y mediante el desenvolvimiento de un conjunto de funciones y subrutinas que permiten acceder con mayor facilidad a bibliotecas de programas, función auxiliar en la definición de datos globales; se mejoró la capacidad de escribir procedimientos internos y recursivos, como también llamar los procedimientos a través de argumentos, siendo éstos opcionales u obligatorios; y se añadió una implementación del concepto de apuntadores. En conjunto, los nuevos aspectos contenidos en FORTRAN 90 hacen que éste sea considerado como el lenguaje más eficiente de la nueva generación de supercomputadores, y aseguran que el FORTRAN continuará siendo usado con éxito por mucho tiempo. FORTRAN 90 fue seguido de una pequeña mejora llamada FORTRAN 95, en 1997. Éste ofrece nuevas características del lenguaje, y clarifica algunas de las ambigüedades de la antigua versión. Así pues, el FORTRAN de hoy revela unas superioridades en relación con sus competidores que marcan la diferencia, fundamentalmente en las aplicaciones de ingeniería y científicas.
12.4. PASCAL El lenguaje de programación Pascal fue desarrollado originalmente por Niklaus Wirth, un miembro de la International Federation of Information Processing (IFIP). El Profesor Niklaus Wirth desarrolló Pascal para proporcionar rasgos que estaban faltando en otros idiomas en aquel entonces. 6-26
Lenguajes de programación
Los principales objetivos para Pascal eran ser eficiente para llevarse a cabo y poder ejecutarse los programas, permitir el desarrollo de estructuras y también organizar programas, y para servir como un vehículo para la enseñanza de los conceptos importantes de programación de la computadora. Pascal, que se nombró así gracias al matemático Blaise Pascal, es un descendiente directo de ALGOL 60, qué ayudó a su desarrollo. Pascal también tomó componentes de ALGOL 68 y ALGOL-W. El original idioma de Pascal aparecido en 1971 tuvo su última revisión publicada en 1973. Fue diseñado para enseñar las técnicas de programación y otros temas a los estudiantes de la universidad y era el idioma de opción de los años 60 a los 80.
12.5. BASIC BASIC es la abreviación de Beginners All Purpose Symbolic Instruction Code, sistema desarrollado en la Universidad de Dartmouth en 1964 bajo la dirección de J. Kemeny y T. Kurtz. Se llevó a cabo para los G.E.225. Esto significa ser un idioma muy simple para aprender y también que sería fácil de traducir. Además, los diseñadores desearon que fuera una piedra en la cual caminar para que los estudiantes aprendieran más adelante los idiomas más poderosos como FORTRAN o ALGOL. Bill Gates y Paul Allen tenían algo diferente en mente. En los 70 cuando la computadora personal Altair de M.I.T.S fue concebida, Allen convenció a Gates a ayudarle a desarrollar un Idioma Básico para él. El futuro de BASIC y la PC empezó. Gates estaba asistiendo a Harvard en ese momento y Allen era un empleado de Honeywell. Allen y Gates adoptaron su BASIC a M.I.T.S. para su Altair. Esta versión tomó un total de 4K de memoria incluido el código y los datos que se usaron para el código fuente. Gates y Allen pusieron a funcionar BASIC en otras plataformas y se mudaron a su lugar de origen en Seattle en donde ellos habían asistido a la escuela primaria juntos. En este momento la Corporación de Microsoft empezó su reinado en el mundo del PC. Más tarde en los 70, BASIC se había puesto ya en las plataformas como la Apple, Comodor y Atari y ahora era tiempo para el DOS de Bill Gates, que vino con un intérprete de BASIC. La versión distribuida con MS-DOS era GW-BASIC y se ajustaba en cualquier máquina que podía ejecutar DOS. No había ninguna diferencia entre BASIC-A y GW-BASIC, el A proporcionado por las computadoras de IBM.
12.6. C El lenguaje C reúne características de programación intermedia entre los lenguajes ensambladores y los lenguajes de alto nivel; con gran poderío basa6-27
Desarrollo de Sistemas
do en sus operaciones a nivel de bits (propias de ensambladores) y la mayoría de los elementos de la programación estructurada de los lenguajes de alto nivel, por lo que resulta ser el lenguaje preferido para el desarrollo de software de sistemas y aplicaciones profesionales de la programación de computadoras. En 1970, Ken Thompson de los laboratorios Bell se había propuesto desarrollar un compilador para el lenguaje Fortran que corría en la primera versión del sistema operativo UNIX tomando como referencia el lenguaje BCPL; el resultado fue el lenguaje B (orientado a palabras) que resultó adecuado para la programación de software de sistemas. Este lenguaje tuvo la desventaja de producir programas relativamente lentos. En 1971 Dennis Ritchie, con base en el lenguaje B, desarrolló NB que luego cambio su nombre por C; en un principio sirvió para mejorar el sistema UNIX por lo que se le considera su lenguaje nativo. Su diseño incluyo una sintaxis simplificada, la aritmética de direcciones de memoria (permite al programador manipular “bits”, “bytes” y direcciones de memoria) y el concepto de apuntador; además, al ser diseñado para mejorar el “software” de sistemas, se buscó que generase códigos eficientes y una portabilidad total, es decir el que pudiese ejecutarse en cualquier máquina. Logrados los objetivos anteriores, C se convirtió en el lenguaje preferido de los programadores profesionales. En 1980 Bjarne Stroustrup de los laboratorios Bell de Murray Hill, New Jersey, inspirado en el lenguaje Simula67 adiciona las características de la programación orientada a objetos (incluyendo la ventaja de una biblioteca de funciones orientada a objetos) y lo denomina C con clases. Para 1983 dicha denominación cambió a la de C++. Con este nuevo enfoque surge la nueva metodología que aumenta las posibilidades de la programación bajo nuevos conceptos.
13. Otros lenguajes de programación Cualquier notación para describir algoritmos y estructuras de datos se puede calificar como un lenguaje de programación, pero principalmente este término se refiere a los implementados para computadoras. Se han diseñado he implementado cientos de lenguajes de programación distintos, y actualmente todos ellos además cuentan con entornos gráficos. Existen otros conceptos tomados en cuenta para agrupar los lenguajes, que dan origen a diversas clasificaciones, entre los que destacan las siguientes: —
6-28
Lenguajes de cuarta generación 4GL: estos lenguajes se distinguen por formar parte de un entorno de desarrollo, que comprende el manejador de una base de datos, y todo lo que de esto se deriva, como la administración de un diccionario de datos, el control de accesos, el manejo de la consistencia de la información y otras características enfocadas a facilitar los programas de acceso y explotación de la información. Como ejemplos podemos citar a los 4 grandes: PROGRESS, SYSBASE, INFORMIX, y ORACLE.
Lenguajes de programación
—
Lenguajes Visuales: se les llama de esta manera a los lenguajes que forman parte de una aplicación dotada de una Interfase gráfica, la cual por medio de iconos y otras herramientas visuales y simbólicas, pretenden facilitar las tareas rutinarias de los programadores, como son el diseño y desarrollo de formularios e informes. Los ejemplos más comerciales de estos lenguajes son: VISUAL BASIC, VISUAL CAFE, VISUAL FOX, etc.
—
Metalenguajes: son lenguajes como XML, SGML y HTML que sirven para definir otros lenguajes, cuyo objetivo es llevar a cabo la estructuración de textos mediante un conjunto de etiquetas, de manera tal, que puedan ser entendidos por los humanos y también procesado por los ordenadores. Estos lenguajes están teniendo un gran auge sobre la plataforma de Internet, en la cual son usados para la creación de documentos, y el intercambio o transferencia de información.
—
Lenguajes de propósito específico: son aquellos lenguajes desarrollados con la finalidad de resolver problemas de una naturaleza muy determinada, tal como SPSS para problemas estadísticos, MATLAB para cálculos científicos y de ingeniería, CAD/CAM para el diseño de piezas y programación de máquinas de control numérico, como tornos y fresadoras, GPSS para simulación de sistemas, CORBA para el manejo de interfaces en ambientes cliente-servidor, etc.
—
Lenguajes Script: son lenguajes como JAVASCRIPT, VBSCRIPT, PERLSCRIPT, que se utilizan en ambientes clientes servidor, mediante la incrustación de código en las páginas HTML, y así permitir la programación del lado del cliente, buscando fundamentalmente, hacer más atractivos las interfaces gráficos de las páginas.
—
Lenguajes vinculados a Internet.
14. Características, elementos y funciones de JAVA, C y C++ A pesar de la evolución de los lenguajes de programación y su éxito de cada uno ellos, hay mucho que avanzar todavía. Pero para comprender mejor esto último, analicemos las características más esenciales de un buen lenguaje de programación: 1.
Una forma clara, sencilla y exacta de la sintaxis para que el programador pueda expresar sus ideas de sus algoritmos (integridad conceptual), haciendo mas fácil su comprensión del mismo para posibles mejorías o modificaciones.
2.
Tener menos restricciones en la forma de codificación de los valores y en la colocación de los mismos (ortogonalidad).
3.
Tener la facilidad de que al codificar nuestro algoritmo, podamos ver la parte esencial del mismo en nuestro programa, es decir, al tener codificado nuestro programa, podamos ver en el nuestro algoritmo en una forma sencilla para poder hacer modificaciones futuras (Naturalidad de la aplicación), pero también viendo como va quedando la estructura de nuestros datos. 6-29
Desarrollo de Sistemas
4.
Apoyo para la abstracción, es decir, permitir al usuario una fácil creación de sus estructuras de sus datos en forma breve y sencilla, sin caer en redundancias.
5.
Un gran problema para los lenguajes de programación es la facilidad de verificar los programas, pero sobre todo la confiabilidad de que cuando se hace un programa, el lenguaje verifique todos los posibles errores de semántica y a su vez que sus datos de salida o en casos de entrada sean reales y confiables.
6.
Otro punto es el entorno de programación. Sin una interfaz, una ayuda o herramientas para la programación, el lenguaje sería inútil, aburrido y desesperante para el programador.
7.
Un factor importante en la creación de los programas es la portabilidad de los mismos hacia otros sistemas. Para ello el lenguaje de programación debe permitirlo por medio de no basarse en una sola arquitectura de computadora en la ejecución de los programas, tal es el caso de C, FORTRAN, Ada y Pascal que manejan la implementación de los programas hacía otros sistemas.
8.
El costo según su uso. El costo de tiempo en la ejecución, de traducción, de creación, prueba y uso y coste de mantenimiento de los programas.
Otras características de los lenguajes de programación son los entornos de diseño. Éstos pueden ser: —
Entorno de procesamiento por lotes. Las instrucciones se ejecutan por secciones o estructuras o lotes.
—
Entorno interactivo. Posibilidad de uso de los periféricos.
—
Entornos incrustados. Son aquellos lenguajes grandes que tienen la posibilidad de llamar a otros más pequeños.
—
Entornos de programación.
—
Entornos de marcos de ambiente. Posibilidad de interactuar con la red.
Un punto importante en todas estas características es que se debe tener un estándar para evitar caer en incompatibilidades de equipo, ya que como sabemos existen cientos de marcas diferentes en equipos de cómputo y sus distintos accesorios.
14.1. Introducción histórica: C, C++ y JAVA En la década de los setenta Ken Thompson creó un lenguaje de programación denominado B que no tubo repercusiones espectaculares pero que sirvió de base para que Dennos Ritchie en esta misma década crease el lenguaje C que a finales de los setenta y durante los ochenta el fue el lenguaje de programación más utilizado por los programadores. Su invención de desarrollo usando UNIX 6-30
Lenguajes de programación
como sistema operativo. En 1983 se estableció un comité para crear el estándar ANSI que definiera el lenguaje C. Al cabo de seis años, 1989, este estándar fue adoptado comenzando su implementación en 1990. En este mismo año este estándar también fue adoptado por la ISO. En 1995 se adoptó la Enmienda 1 del estándar C. En 1989 se creó un nuevo estándar que, junto con la Enmienda 1, se convirtió en el documento base del estándar C++. A partir de este momento C quedó como relegado a un segundo plano, pero en un segundo plano totalmente operativo. De hecho, en 1999, se creó un nuevo estándar y en la actualidad sigue siendo un lenguaje de programación en pleno vigor (a este estándar se le suele denominar C99). Es, por tanto, la creciente complejidad de los sistemas lo que ha conducido a la necesidad de cambiar de C a C++. C++ fue inventado por Bjarne Stroustrup en 1979 en los Laboratorios Bell. La nueva forma de pensar (programación orientada a objetos) hizo que fuese una auténtica revolución en el mundo de los lenguajes de programación. C++ es una extensión de C en la que se añaden las características orientadas a objetos. C++ fue estandarizado en 1997 cumpliendo los estándares ANSI/ISO. C++ fue el lenguaje dominante a finales de los ochenta y principios de los noventa. No obstante, y aunque C++ combinaba de forma perfecta la programación orientada a objetos con un lenguaje ten completo como C, llegó un Internet y se hizo necesario adaptar este lenguaje a los nuevos mundos. Así fue como en 1995 vio la luz JAVA, aunque ya en 1991 fue desarrollado y comenzó a ser utilizado. Fue desarrollado durante año y medio por cinco programadores expertos que crearon este lenguaje de programación cuya sintaxis básica esta basada en C y que implementa la orientación a objetos de C++. El dominio de JAVA como lenguaje de programación orientado a objetos se ha extendido hasta nuestra época. Su utilidad tanto para aplicaciones independientes de la plataforma como para subprogramas ejecutables dentro de una pagina Web (applets) y muchas características más que veremos a continuación le han hecho digno de todo elogio.
14.2. El lenguaje C EL lenguaje C es el resultado de un proceso de desarrollo que inició con un lenguaje denominado BCPL. Éste influenció a otro llamado B (inventado por Ken Thompson). En los años 70, este lenguaje llevó a la aparición del C. Con la popularidad de las microcomputadoras muchas compañías comenzaron a implementar su propio C por lo cual surgieron discrepancias entre sí. Por esta razón ANSI estableció un comité en 1983 para crear una definición no ambigüa del lenguaje C e independiente de la máquina que pudiera utilizarse en todos los tipos de C. Algunos de las C existentes son: —
Quick C
—
C++
—
Turbo C 6-31
Desarrollo de Sistemas
—
Turbo C ++
—
Borland C
—
Borland C++
—
Microsoft C
Cuando se habla del lenguaje C se ha de tener en cuanta las dos grandes estandarizaciones existentes en su larga historia: por un lado tenemos C89 y C99. Para hacernos una idea de las diferencias, C89 contiene 32 palabras clave, C99 incluye cinco más. Hoy en día la mayoría de los compiladores se basan en esta segunda estandarización. El lenguaje C se engloba dentro de los lenguajes de nivel medio. Como lenguaje de nivel medio, C permite la manipulación de “bits”, “bytes” y direcciones que son los elementos básicos con los cuales funciona la computadora. El lenguaje C es muy portable, en el sentido de que funciona en distintos sistemas o diferentes tipos de computadoras; para que nos hagamos una idea: Windows en todas sus versiones, DOS, Linux, etc. C no lleva a cabo una comprobación de errores en tiempo de ejecución. Es el programador el único responsable de llevar a cabo estas comprobaciones. Dado que con C podemos manipular “bits”, “bytes” y direcciones, se hace ideal para la programación de sistemas. De hecho el sistema Linux nació con el intento logrado de reescribir el código de UNIX en C. C es un lenguaje estructurado pero no estructurado en bloques ya que no permite por ejemplo la creación de funciones dentro de funciones. Al ser por tanto un lenguaje estructurado su sintaxis es la que sirve de base para C++ y JAVA y será la que veamos en un apartado posterior en este mismo tema. El componente principal de C es la función que se define como una subrutina independiente. Cada una de estas funciones está formada por bloques en los que se desarrolla toda la actividad del programa. Cada bloque queda delimitado por “{“ y “}”. A estos bloques se les conoce como bloques de código que son un grupo de instrucciones de un programa conectados de forma lógica y que es tratado como una unidad. En un primer momento C fue usado sólo para la programación de sistemas. Definimos un programa de sistemas como una parte del sistema operativo del ordenador o de sus utilidades de soporte, tales como editores, los compiladores, los enlazadores y similares. No obstante y debido al gran éxito que tuvo desde un primer momento empezó a ser utilizado por los programadores para crear sus propios programas. El lenguaje C tiene una lista de palabras clave que el programador usa constantemente y que tienen un objetivo definido se enumeran en la tabla adjunta. Cada una de estas palabras clave no pueden ser utilizadas para otro fin diferente al que tienen asignado. En C, a pesar de no ser un lenguaje fuertemente tipado como ocurre con C++ y JAVA, sí que se diferencia entre mayúsculas y minúsculas.
6-32
Lenguajes de programación
Como hemos visto, en C un programa consiste en una o más funciones; no obstante siempre ha de haber una que sea invocada cuando se ejecuta el programa, es decir que sea la primera que se ejecute y que llame a las demás (una función principal), a esta función se le ha de denominar main(). Main no es una palabra reservada pero no puede usarse para otras cosas (trataremos main como si fuese una palabra reservada aunque no lo sea). La mayoría de los programas C incluyen llamadas a varias funciones contenidas en la biblioteca estándar de C. Todos los compiladores actuales de C incorporan esta biblioteca estándar en la cual se incluyen funciones que realizan las tareas más habituales (raíces, impresiones, lectura de ficheros, etc.). Independientemente de esta biblioteca estándar existen muchas otras las cuales podemos utilizar o incluso crear nuestra propia biblioteca de funciones. Para poder fusionar el código del programa con el código de las funciones existentes en las bibliotecas los compiladores incluyen enlazadores, a cuyo proceso se le denomina “enlace”. En C un programa se puede escribir en varios archivos y compilar cada uno de ellos por separado. De este modo la recompilacion se puede efectuar en aquel archivo que da el problema o en el cual queramos realizar alguna modificación sin tener que recompilar todo el programa. El código objeto completo lo forman todos los archivos del programa compilados y las rutinas de las bibliotecas utilizadas. Por tanto los pasos a seguir en la creación de un programa C son: —
Creación del programa.
—
Compilación del programa.
—
Enlace del programa con todas las funciones que se necesiten de la biblioteca.
14.2.1. Elementos generales de un programa en C Aunque cada uno de los programas son distintos, todos tienen características comunes. Los elementos de un programa en C son los siguientes: Comentarios
Inclusión de archivos main() { variables locales flujo de sentencias } Definición de funciones creadas por el programador utilizadas en main()
6-33
Desarrollo de Sistemas
Veamos en qué consiste cada uno: —
Comentarios: se identifican porque van entre diagonales y asterisco. Nos sirve para escribir información que nos referencie al programa pero que no forme parte de él. Por ejemplo, especificar qué hace el programa, quién lo elaboró, en qué fecha, qué versión es, etc.
—
Inclusión de archivos: consiste en mandar llamar a la o las bibliotecas donde se encuentran definidas las funciones de C (instrucciones) que estamos utilizando en el programa. En realidad, la inclusión de archivos no forma parte de la estructura propia de un programa sino que pertenece al desarrollo integrado de C. Se incluye aquí para que el alumno no olvide que debe mandar llamar a los archivos donde se encuentran definidas las funciones estándar que va a utilizar.
—
main(): en C todo está constituido a base de funciones. El programa principal no es la excepción. main() indica el comienzo de la función principal del programa, la cual se delimita con llaves.
—
Variables locales: antes de realizar alguna operación en el programa, se deben declarar la(s) variable(s) que se utilizarán en el programa.
—
Flujo de sentencias: es la declaración de todas las instrucciones que conforman el programa.
—
Definición de funciones creadas por el programador utilizadas en main(): finalmente, se procede a definir el contenido de las funciones utilizadas dentro de main(). Éstas contienen los mismos elementos que la función principal.
Un programa en C consta de tres secciones. La primera sección es donde van todos los “headers”. Estos “headers” son comúnmente los “#define” y los “#include”. Como segunda sección se tienen las “funciones”. Al igual que Pascal, en C todas las funciones que se van a ocupar en el programa deben ir antes que la función principal (main()). Declarando las funciones a ocupar al principio del programa, se logra que la función principal esté antes que el resto de las funciones. Ahora, solo se habla de funciones ya que en C no existen los procedimientos. Y como última sección se tiene a la función principal, llamada main. Cuando se ejecuta el programa, lo primero que se ejecuta es esta función, y de ahí sigue el resto del programa. Los símbolos { y } indican “begin” y “end” respectivamente. Si en una función o en un ciclo while, por ejemplo, su contenido es de solamente una línea, no es necesario usar “llaves” ({ }), en caso contrario es obligación usarlos. Ejemplo de un programa en C
/*Programa que imprime un saludo en pantalla*/ #include tomates () {
6-34
Lenguajes de programación
printf("Dedicado a Ismael”); } void main() { tomates(); } /* Fin programa */ Los primeros lenguajes ensambladores ofrecen una forma de trabajar directamente con un conjunto de instrucciones incorporadas en la computadora. Cada una de estas especificaciones se ha de especificar en términos de máquina (bits de los registros). Dado que esto se hacía muy pesado para el programador, surgieron los primeros lenguajes de alto nivel, como Fortran, que en un principio se desarrollaron como alternativa a los lenguajes ensambladores. En un principio estos lenguajes fueron usados para resolver problemas de matemáticas, ingeniería o científicos (lenguajes orientados al problema). Algunos desarrolladores de software quisieron desarrollar lenguajes para su área y crearon los lenguajes orientados a la máquina (B y C). El lenguaje C está ligado a la computadora y nos ofrece un importante control sobre los detalles de la implementación de una aplicación; ésta es la razón por la cual se le considera a la vez un lenguaje de bajo y de alto nivel (lenguaje de nivel medio). Entre las muchas ventajas que posee el lenguaje C citaremos: —
Tamaño óptimo de código. Dado que tiene pocas reglas de sintaxis.
—
Conjunto de palabras clave. Palabras reservadas que usa el lenguaje.
—
Ejecutables rápidos. Muchos programas C se ejecutan con una velocidad equivalente al lenguaje ensamblador.
—
Comprobación de tipos limitada. Se permite visualizar datos de distintas maneras.
—
Implementación de diseño descendente. El denominado diseño Top-Down gracias a la implementación de sentencias de control.
—
Estructura modular. Compilación y enlazado por separado.
—
Interfaz transparente para el lenguaje ensamblador. Se puede llamar a las rutinas del lenguaje ensamblador desde un compilador C.
—
Manipulación de bits. C permite manipular bits y bytes
—
Tipos de datos puntero. C permite manipular direcciones.
—
Estructuras extensibles. Los arrays son unidimensionales.
—
Memoria eficiente. Los programas C tienden a ser muy eficientes en memoria. 6-35
Desarrollo de Sistemas
—
Portabilidad entre plataformas. Un programa C se puede ejecutar en una computadora u otra con un sistema operativo u otro.
—
Rutinas de biblioteca. Hay una gran cantidad de bibliotecas con funciones precreadas.
(Para más información sobre el Lenguaje C, consulte el Anexo II de este mismo tema).
14.3. El lenguaje C++ C++ es un subconjunto de C que mantiene todas las características de C y su flexibilidad para el trabajo en el tratamiento de la interfaz hardware/software, su programación del sistema a bajo nivel, sus expresiones, etc., pero todo ello dentro de la programación orientada a objetos. Este lenguaje combina las construcciones del lenguaje procedimental estándar y el modelo orientado a objetos. Se trata pues de una nueva forma de pensar. C++ se desarrolló originariamente para resolver simulaciones conducidas por sucesos. Fue utilizado en 1983 y aún en 1987 se encontraba en fase de evolución. En su evolución siempre se ha procurado preservar la integridad de los programas escritos en otros lenguajes al intentar exportarlos a C++. Diferencias entre C y C++:
6-36
—
Trabajo con clases. Frente al trabajo con estructuras definido en C.
—
Constructores de clases y encapsulación de datos.
—
La clase struct. Esta clase puede contener tanto dado como funciones.
—
Constructores y destructores. Se usan para garantizar la inicialización y destrucción de los datos.
—
Mensajes. Los objetos se manipulan enviándoles mensajes.
—
Funciones afines. Se permite acceder a los métodos y datos de una clase privada.
—
Sobrecarga de operadores. Se puede hacer sobrecarga de operadores por número de argumentos o por su tipo.
—
Clases derivadas. Una subclase de una clase específica.
—
Polimorfismo. El objeto determina qué clase o subclase recibe un mensaje.
Lenguajes de programación
—
Biblioteca de flujos. Permitiendo que las operaciones de entrada y salida de datos desde un terminal o un archivo sean más accesibles.
Existen otras diferencias menos notorias como: —
Su sintaxis en algunos aspectos puntuales como son los comentarios y variables enumeradas
—
Conversiones de tipo explícitas.
—
Sobrecarga de funciones.
—
Argumentos por referencia.
—
Punteros de tipo void.
—
Funciones inline.
—
Etc.
Ejemplo de programa en C++:
/* Comentario creado para los estudiantes del TAImucha suerte a todos */ # include int main() { printf(“ C++ es guay”); return (0); } Este código fuente ha de ser guardado con extensión “*.c”, luego será compilado para poder ejecutarlo. Existen entornos gráficos para el desarrollo de programas en C y en C++ que incluyen los compiladores correspondientes y facilitan visualmente la labor del programador. En la imagen vemos el Visual C++ de Microsoft que permite escribir programas C o C++.
6-37
Desarrollo de Sistemas
(Para más información sobre el Lenguaje C++, consulte el Anexo III de este mismo tema).
14.4. El lenguaje JAVA JAVA es un lenguaje de programación desarrollado por un grupo de ingenieros de Sun Microsystems (1991); en principio está destinado a electrodomésticos, está basado en C++ y se diseñó para ser un lenguaje sencillo con códigos de tamaño muy reducido. Posteriormente (1995) se comienza a utilizar como lenguaje para computadores; Netscape Navigator incorpora un intérprete JAVA en su versión 2.0 siendo la base para JAVAScript. El rápido crecimiento de Internet y el uso de JAVA para dar dinamismo a las páginas de HTML, lo convierten en un medio popular de crear aplicaciones para Internet. Si bien su uso se destaca en el Web, y sirve para crear todo tipo de aplicaciones (locales, Intranet o Internet). En la actualidad es un lenguaje muy completo (la versión JAVA 1.0 tenía 12 paquetes (packages), JAVA 1.1 tenía 23 y JAVA 1.2 o JAVA 2 tiene 59). El haber sido diseñado en una época muy reciente y por un único equipo le confieren unas características que facilitan su aprendizaje y utilización a los usuarios; JAVA incorpora muchos aspectos que en cualquier otro lenguaje son extensiones propiedad de empresas de software o fabricantes de ordenadores (threads, ejecución remota, componentes, seguridad, acceso a bases de datos, etc.). La importancia de JAVA es su utilización como nexo de unión de los usuarios con la información, ya sea en el ordenador local, en un servidor de Web, en una base de datos o en cualquier otro lugar. JAVA es un lenguaje potente que resuelve los problemas que se plantean al acceder a una base de datos, en la programación de redes, la distribuida, etc.; tiene muchas posibilidades de utilización como aplicación independiente (Standalone Application), programación a través de los applets. Ejecución como servlet, etc. Un applet es un programa que corre bajo un navegador o browser (por ejemplo Netscape Navigator o Internet Explorer) y es descargado como parte de una 6-38
Lenguajes de programación
página HTML desde un servidor Web. El Applet se descarga desde el servidor y no requiere instalación en el ordenador donde se encuentra el “browser”. Un servlet es una aplicación sin interface gráfica que se ejecuta en un servidor de Internet. Es un lenguaje orientado a objetos, ha sido concebido como un lenguaje de programación orientado a objetos, a diferencia de otros lenguajes como C++ que son lenguajes modificados para poder trabajar con objetos.
14.4.1. ¿Qué entendemos por objeto? Podemos decir que todo puede verse como un objeto. Un objeto es una pieza de software que cumple con ciertas características: —
Encapsulamiento: el objeto es autocontenido, es la integración sus datos (atributos) y los procedimientos (métodos) que actúan sobre él. Al utilizar la programaci6n orientada a objetos, se definen clases (objetos genéricos) y la forma en que interactúan entre ellos, a través de mensajes. Dado que los programas no modifican al objeto, éste se mantiene independiente del resto de la aplicación; si necesitamos modificar un objeto lo hacemos sin tocar el resto de la aplicación.
—
Herencia: se pueden crear nuevas clases que comparten características (atributos) y comportamientos (métodos) de otras ya preexistentes, relacionadas por una relación jerárquica, simplificando la programación.
JAVA es independiente de la plataforma, puede hacerse funcionar con cualquier ordenador. Al compilar un programa JAVA, lo que se genera es un seudo-código definido por Sun, para una máquina genérica; el “software” de ejecución lava interpreta las instrucciones, emulando a dicha máquina. Por supuesto esto no es muy eficiente, por lo que tanto Netscape como Hot JAVA o Explorer, al ejecutar el código por primera vez, lo van compilando (mediante un JIT: Just In Time compiler), de modo que al crear por ejemplo la segunda instancia de un objeto el código ya está compilado específicamente para la máquina huésped.
14.4.2. Compilador de Java, JAVA Virtual Machine Existen distintos programas comerciales que permiten desarrollar código JAVA. Sun distribuye gratuitamente el JDK (JAVADevelopment Kit); el JDK es un conjunto de programas y librerías que permiten desarrollar, compilar y ejecutar programas en JAVA. El Compilador de JAVA realiza un análisis de sintaxis del código escrito en los ficheros fuente de JAVA (con extensión *.java). Si no encuentra errores en el código genera los ficheros compilados (con extensión *.class). En otro caso muestra la línea o líneas erróneas. Incorpora además la posibilidad de ejecutar parcialmente el programa, deteniendo la ejecución en el punto deseado y estudiando en cada momento el valor de cada una de las variables. Los IDEs (Integrated Development Environment-Entornos de Desarrollo Integrados), tal y como su nombre indica, son entornos de desarrollo inte6-39
Desarrollo de Sistemas
grados. En un mismo programa es posible escribir el código JAVA, compilarlo y ejecutarlo sin tener que cambiar de aplicación. Estos entornos integrados permiten desarrollar las aplicaciones de forma mucho más rápida, incorporando en muchos casos librerías con componentes ya desarrollados, los cuales se incorporan al proyecto o programa. Como inconvenientes se pueden señalar algunos fallos de compatibilidad entre plataformas, y ficheros resultantes de mayor tamaño que los basados en clases estándar. La existencia de distintos tipos de procesadores y ordenadores resalta la importancia de contar con un software que no dependa del tipo de procesador utilizado. Esto llevo al los ingenieros de Sun a desarrollar un código capaz de ejecutarse en cualquier tipo de máquina. Al ser compilado el código fuente no necesita ninguna modificación al cambiar de procesador o al ejecutarlo en otra máquina; esto se debe a que se ha desarrollado un código “neutro” el cual estuviera preparado para ser ejecutado sobre una “máquina hipotética o virtual”, denominada JAVA Virtual Machine). La JVM interpreta el código neutro y lo convierte en el código particular de la CPU utilizada, evitando tener que realizar un programa diferente para cada CPU o plataforma. Nota. JDK, por tanto, es necesario si queremos crear applets o aplicaciones en lenguaje JAVA y JVM es necesario si queremos visualizarlos. JDK, no obstante, es un Kit de desarrollo que lleva integrado JVM.
14.4.3. Estructura de un programa JAVA La estructura de un programa realizado en cualquier lenguaje orientado a objetos (Object Oriented Programming) (OOP-POO), y en particular en el lenguaje JAVA es una clase. En JAVA todo forma parte de una clase, es una clase o describe cómo funciona una clase. El conocimiento de las clases es fundamental para poder entender los programas Java. Todas las acciones de los programas JAVA se colocan dentro del bloque de una clase o un objeto. Todos los métodos se definen dentro del bloque de la clase, JAVA no soporta funciones o variables globales. En todo programa nos encontramos con una clase que contiene el programa principal y algunas clases de usuario (las específicas de la aplicación que se está desarrollando) que son utilizadas por el programa principal. Los ficheros fuente tienen la extensión *.java, mientras que los ficheros compilados tienen la extensión *.class. Un fichero fuente (*.java) puede contener más de una clase, pero sólo una puede ser publico El nombre del fichero fuente debe coincidir con el de la clase public (con la extensión *.java). Si, por ejemplo, en un fichero aparece la declaración (public class MiClase {...}) entonces el nombre del fichero deberá ser MiClase.java. Es importante que coincidan mayúsculas y minúsculas ya que MiClase.JAVAy miclase.JAVAserían clases diferentes para lava. Si la clase no es public, no es necesario que su nombre coincida con el del fichero. Una clase puede ser public o package (default), pero no private o protected.
6-40
Lenguajes de programación
De ordinario una aplicación está constituida por varios ficheros *.class. Cada clase realiza unas funciones particulares, permitiendo construir las aplicaciones con gran modularidad e independencia entre clases. Las clases de lava se agrupan en packages, que son librerías de clases. Si las clases no se definen como pertenecientes a un package, se utiliza un package por defecto (default) que es el directorio activo. Es necesario entender y dominar la sintaxis utilizada en la programación; observemos nuestro primer programa en lava y un breve comentario de las partes que lo componen, para posteriormente pasar a estudiar la nomenclatura empleada y los elementos que empleamos para desarrollar nuestro lenguaje:
/* lolo.JAVA Escribe en pantalla "¡Dedicado a Maria Luz!" */ class lolo { public static void main(String args [ ]) { System.out.println(" ¡Dedicado a Maria Luz!") ; } } Con JAVA se pueden crear dos tipos de programas: aplicaciones y applets. Una aplicación es un programa que se ejecuta en una computadora utilizando el sistema operativo de esa computadora. Se trata pues de un programa normal como podría haber sido en C o C++ pero en el lenguaje JAVA. En este aspecto la funcionalidad de JAVA no es diferente a la de cualquier otro lenguaje orientado a objetos. Una Applet es una aplicación diseñada para ser transmitida por Internet y ejecutada en un navegador Web compatible con JAVA. Un applet es realmente un pequeño programa que se transfiere dinámicamente a través de la red, como si fuese una imagen, un archivo de sonido o de vídeo. La diferencia principal es que una Applet es un programa que puede reaccionar ante las acciones del usuario y cambiar dinámicamente. Vamos a detallar algunos de los aspectos (cualidades) que han hecho de JAVA uno de los lenguajes más populares: • Simple JAVA es un lenguaje relativamente fácil de aprender una vez que se comprenden los conceptos básicos de la programación orientada a objetos que veremos en el tema correspondiente. Además, para la realización de una determinada acción existen siempre varios caminos por los cuales podamos programar.
6-41
Desarrollo de Sistemas
• Seguro Esta cualidad hace que los applets sean ideales para la transmisión por Internet sin violar la vulnerabilidad de los sistemas. • Portable El código ejecutable generado por JAVA es adaptable a cualquier tipo de plataforma con cualquier tipo de Sistema Operativo. • Orientado a objetos JAVA junto con C++ es el máximo exponente de la programación Orientada a Objetos. Esta cualidad le brinda de todas las ventajas de esta programación. • Robusto Ya que JAVA se ejecuta en multitud de plataformas, esta cualidad le permite satisfacer con éxito su deber en todas ellas. • Multihilo JAVA permite que se ejecuten varios hilos al mismo tiempo (cada hilo representa una tarea a desarrollar). Esta cualidad es la que le ha hecho ideal para trabajo en redes. Además permite la sincronización de cada una de esas tareas y su comunicación. • Arquitectura neutral Esta cualidad le permite que un programa JAVA se pueda ejecutar en un Sistema Operativo actual y en las actualizaciones que puedan surgir de ese sistema operativo. Lo único que necesitamos es la JVM (Máquina Virtual de JAVA). • Interpretado Una vez compilado un programa éste se convierte en código binario que es interpretado por un intérprete de Java. Este intérprete debe ser optimizado para que la conversión al código máquina sea de buen rendimiento. • Distribuido A través del protocolo TCP/IP podemos distribuir programas JAVA en Internet. JAVA dispone además de RMI (Invocación de Método Remoto) que permite ejecutar procedimientos de forma remota. Otra característica es la programación cliente/servidor.
6-42
Lenguajes de programación
Existen entornos gráficos para el desarrollo de programas en JAVA que incluyen los compiladores correspondientes y facilitan visualmente la labor del programador. (Para más información sobre el Lenguaje JAVA, consulte el Anexo I de este mismo tema).
15. Entornos de programación visual El desarrollo de interfaz de tipo gráfico en los sistemas operativos impulsó el desarrollo de aplicaciones que utilizaran dicho entorno. A partir de 1990, con la proliferación de Windows, esto se hizo especialmente importante. Visual Basic surgió a principios de los años 90. Basado en el popular lenguaje Basic supuso una auténtica revolución en el mundo de la programación, entre otras cosas por su facilidad de uso y su entorno eminentemente gráfico. Las primeras versiones del lenguaje no eran excesivamente potentes, pero las últimas (5.0 y 6.0) incorporan todo tipo de herramientas y controles que permiten la construcción de aplicaciones de desarrollo en cualquier entorno. Todas estas herramientas ofrecen una gran facilidad en la construcción de aplicaciones. Están orientadas tanto a eventos como a objetos, y proporcionan la posibilidad de desarrollar aplicaciones para Windows sin necesidad de utilizar primitivos lenguajes para programar directamente la interfaz gráfica. Por el contrario, el código generado no está completamente optimizado y suele tener un tamaño superior al de otros programas, por lo que su velocidad de ejecución es más lenta que en entornos no gráficos.
15.1. Visual Basic Cuando se inicia Visual Basic aparece una ventana denominada IDE (Entorno Integrado de Desarrollo) a través de la cual desarrollaremos la aplicación. La ventana del IDE contiene las restantes ventanas del entorno de desarrollo. Dicha ventana usa un entorno MDI, es decir, permite mantener múltiples ventanas abiertas contenidas en una ventana principal. En un proyecto visual Basic tenemos tres formas principales de trabajar, diseño, ejecución e interrupción: —
En la fase de diseño se incorporan todos los controles, seleccionados a través del cuadro de herramientas, que van a formar parte de la aplicación. El objeto que va a contener todos los controles se denomina formulario. Una vez que se han incorporado los controles se pueden establecer sus propiedades de dos formas: mediante la ventana de propiedades del control o mediante código.
—
En la fase de ejecución se escriben las sentencias de código necesarias, utilizando la sintaxis del lenguaje, para realizar acciones, modificar propiedades de un objeto o invocar a sus métodos. 6-43
Desarrollo de Sistemas
—
En la fase de interrupción se depuran los posibles errores que contenga el código.
En un proyecto puede haber más de un formulario. La combinación de formularios módulos de código, clases y otros recursos son parte integrante de un proyecto de visual basic. Normalmente los proyectos Visual Basic tienen extensión EXE. Clarifiquemos toda la exposición anterior observando el IDE de Visual Basic:
Visual Basic es un lenguaje estructurado aunque permite utilizar estructuras de tipo goto en ciertas rutinas, como las de tratamiento de errores. Otras características importantes del lenguaje VISUAL BASIC son: —
6-44
Contiene una biblioteca de clases que da soporte a los objetos Windows tales como: •
Ventanas.
•
Cuadros de texto.
•
Botones de pulsación.
•
Casillas de verificación.
•
Listas desplegables.
•
Listas combinadas.
•
Marcos.
•
Etiquetas.
Lenguajes de programación
—
—
Tiene un entorno de desarrollo integrado que incluye, entre otras: •
Editor de texto.
•
Interpret.
•
Depurador.
•
Examinador de objetos.
•
Explorador de proyectos.
•
Compilador.
Dispone de asistentes para: •
Aplicaciones.
•
Barras de herramientas.
•
Formularios de datos.
•
Empaquetado y distribución.
•
Crear la interfaz pública de controles ACTIVEX.
•
Objetos de datos.
•
Generador de clases.
•
Diseñador de complementos.
—
Galería de objetos vinculados e incrustados.
—
Creación de bibliotecas dinámicas.
—
Soporte para:
—
•
Aplicaciones de Internet.
•
Estandar com.
Acceso a base de datos utilizando: •
Controladores ODBC.
•
Motor de Access.
•
OLEDB.
•
Controles ADO y DATA. 6-45
Desarrollo de Sistemas
—
Biblioteca para SQL que permite la manipulación de base de datos relacionales.
—
Un administrador visual de datos para manipular base de datos.
—
Utilidad para crear ficheros de ayuda estilo Windows.
Mediante la implementación de todas estas características mencionadas es posible realizar cualquier tipo de desarrollo utilizando Visual Basic. Con herramientas como el Dataenvironment todo el proceso de acceso a base de datos es de muy sencilla utilización, ya que, al crear una nueva conexión, aparece automáticamente un asistente para poder seleccionar el proveedor de acceso a los datos, su localización y otros parámetros dependientes del tipo de controlador. También, como se ha mencionado, es posible utilizar herramientas que generan código SQL. Como contrapunto a todas estas funciones, Visual Basic no ofrece demasiadas herramientas relacionadas con Internet. Dispone solamente de dos controles: WINSOCK e INET. Con el primero se pueden desarrollar servicios de comunicaciones tanto servidor como cliente, tomando como base los servicios. El segundo control facilita la comunicación entre servidores de tipo FTP y http, lo que es adecuado para crear clientes que necesiten transferencia de archivos y descargas de documentos en la WEB.
15.2. Otros entornos visuales Existen otros entornos de programación visual aparte de los ya mencionados. Otras herramientas RAD (herramientas de desarrollo rápido) son: —
Borland C++ Builder.
—
Power++.
—
Sybase Powerbuilder.
Borland C++ Builder usa prácticamente el mismo entorno que Delphi. Al igual que Delphi, está abierta a otras tecnologías. La mayor ventaja que ofrece es que el lenguaje utilizado para programar es C++, lenguaje muy potente y versátil. El entorno POWER++, de Sybase, dispone de un compilador muy optimizado y de un entorno de trabajo muy eficiente. También tiene capacidades para crear aplicaciones cliente/servidor y aplicaciones WEB, aunque su uso no está demasiado extendido en nuestro país. POWERBUILDER, también de Sybse, está relacionado estrechamente con el mundo de las bases de datos. Es un lenguaje de cuarta generación que, al igual que los demás, se apoya en el uso de objetos. Su gran ventaja es su capacidad MULTIPLATAFORMA.
6-46
Lenguajes de programación
Pero sin duda, las herramientas que más desarrollo han tenido dentro del mundo RAD han sido todas aquellas relacionadas con JAVA. Casi la totalidad de los entornos de desarrollo de JAVA son RAD y disponen de avanzados conjuntos de componentes y generadores de código. Mencionaremos algunas como: JBUILDER, IBM VISUALAGE FOR JAVA, SUN JAVAESTUDIO y MICROSOFT VISUAL J++.
16. .NET Framework 16.1. Introducción Tal como entendíamos los entornos de desarrollo hasta “ayer”, éstos permitían desarrollar aplicaciones para DOS, para Linux, para Windows, etc. Si queríamos desarrollar una aplicación con C++, debíamos ejecutar Visual C++; si queríamos desarrollar una aplicación ASP, podíamos ejecutar Microsoft InterDev; si queríamos desarrollar una aplicación en Visual Basic, debíamos desarrollar en el entorno de desarrollo de Visual Basic, etc. Sin embargo, .NET Framework ha modificado esta idea por completo, y otras empresas de creación de software de desarrollo tienden hacia esta idea.
16.2. ¿Un mismo entorno para todos los lenguajes? En realidad el cambio conceptual no es tan complicado de comprender. .NET Framework comparte la misma estructura general para todos los lenguajes de desarrollo. Imaginemos una mano. Tiene cinco dedos. Cada dedo se llama de manera diferente y tiene unas características particulares, uno es más largo, el otro más gordo, otro el más pequeño... sin embargo, todos comparten el mismo corazón, el mismo cerebro y el mismo brazo. Es un ejemplo un poco simple, pero tiene todo el sentido como se verá en las siguientes explicaciones. Así por ejemplo, y hablando de .NET Framework en concreto, dentro de un entorno de desarrollo o trabajando dentro del Marco de Trabajo.NET, podemos trabajar con diferentes lenguajes de desarrollo, es decir, podemos trabajar con JScript .NET, Visual Basic .NET, Visual C#, ASP .NET, etc. Cada lenguaje posee sus propias características que permiten al desarrollador trabajar con el que más conozca, el que más le guste o con el que se sienta más identificado o cómodo. Utilizando un mismo entorno de desarrollo como Visual Studio .NET, podemos emplear en nuestros desarrollos el lenguaje o lenguajes que consideremos oportuno. En realidad, conviene antes de seguir, comprender la diferencia entre Visual Studio .NET y .NET Framework, ya que muchos desarrolladores confunden las diferencias existentes entre estos dos. .NET Framework es el marco de trabajo con el cual desarrollaremos nuestras aplicaciones. En él se incluyen las diferentes partes del lenguaje (clases, objetos, tipos, etc.) que nos permiten desarrollar nuestras soluciones informáticas. Visual Studio .NET es el entorno RAD (Rapid Application Development o Desarrollo Rápido de Aplicaciones), que nos permite utilizar .NET Frame6-47
Desarrollo de Sistemas
work para desarrollar nuestras aplicaciones de una forma rápida y visual, incluyendo características de desarrollo como el IntelliSense. Visual Studio .NET utiliza por tanto, .NET Framework.
Figura 1. Aspecto de Visual Studio .NET
Para desarrollar aplicaciones .NET, deberemos por tanto, utilizar necesariamente .NET Framework, ya sea utilizando Visual Studio .NET o no. Hablando del entorno .NET Framework, diremos que Microsoft ha añadido en él las capacidades y características necesarias para hacer de este modelo, un modelo POO o modelo de programación orientada a objetos. Todos los desarrollos que realicemos con .NET serán desarrollos orientados a objetos. Este cambio de «chip» es un cambio especialmente problemático para los desarrolladores que estaban acostumbrados a trabajar con Visual Basic sin tener conocimientos sobre la orientación a objetos, pero ¿quién ha dicho que es difícil o imposible? La problemática llega porque será necesario cambiar el esquema de trabajo que llevábamos a cabo cuando trabajábamos con Visual Basic. Ahora es necesario tener claro lo que se va a hacer, cómo se va a hacer y cuándo se va a hacer.
16.3. ¿Todos los lenguajes para un entorno? Dentro del .NET Framework conviven, como hemos comentado, diferentes lenguajes de desarrollo; sin embargo, todos comparten una serie de características que son idénticas para cada uno de ellos. La más importante es que comparten el mismo entorno de trabajo, el comentado .NET Framework, el cual contiene todo lo necesario para programar, compilar y ejecutar nuestras aplicaciones. 6-48
Lenguajes de programación
Algunas de las características de este entorno único es la posibilidad de compartir el mismo CLR. El CLR (Common Language Runtime, es decir, el Motor Común de Ejecución) es el centro neurálgico del .NET Framework encargado de gestionar la ejecución de nuestras aplicaciones, aplicar parámetros de seguridad y ejecutar el denominado recolector de basura entre otras cosas. La particularidad del CLR es que tendremos un CLR distinto por cada plataforma, ya sea una plataforma Windows, Linux, etc. Esto significa que una aplicación desarrollada bajo Microsoft Windows 2000 Advanced Server, por ejemplo, debería poder ejecutarse en un entorno que dispusiese de un CLR para Linux y un CLR para Windows. El CLR está formado, como hemos ya indicado, por diferentes partes que son igualmente importantes. De esta manera, nos encontramos con diferentes «cajas» que veremos a continuación. Cuando desarrollamos una solución en .NET Framework, la compilamos y la ejecutamos posteriormente, pero debemos tener en cuenta diferentes aspectos que suceden de manera transparente para el desarrollador. Todas las aplicaciones .NET son compiladas a un lenguaje neutral denominado IL (Intermediate Language, es decir, Lenguaje Intermedio). El CLR es capaz de compilar ese lenguaje intermedio a lenguaje máquina, específico para cada sistema en el cual se ejecuta. Ése es el «truco» que utiliza .NET Framework para poder ejecutar una misma aplicación en Windows o Linux por ejemplo.
Figura 2. Estructura general de Microsoft .NET Framework
Entre otras características, el CLR contiene un mismo CTS (Common Type Specification, es decir, Especificación de Tipos de Datos Común). El CTS para hablar de forma clara, constituye los diferentes tipos y definiciones de cada tipo de datos utilizable en una aplicación .NET. Un tipo de dato no encontrado en el CTS es devuelto como error por el CLR. Cada tipo de dato hereda su tipo del objeto o clase System.Object. Relacionado con el CTS, nos encontramos con la CLS (Common Language Specification, es decir, la Especificación Común de Lenguajes), la cual no es otra cosa que la especificación o reglas a seguir a la hora de trabajar con los tipos de datos. Pero no sólo los tipos son parte fundamental de .NET Framework; también el BCL (Base Class Library, es decir, la Biblioteca de Clases Base) es importante dentro de la estructura de desarrollo .NET. Dentro del BCL encon-
6-49
Desarrollo de Sistemas
traremos una extensa biblioteca formada por clases que nos proporcionarán la posibilidad de acceder a una gran cantidad de servicios. En el .NET Framework, referenciamos a las BCL mediante lo que se ha denominado Namespace (Espacios de Nombres) y que se engloban dentro del Namespace System. Además de todos los aspectos que acabamos de ver, debemos tener en cuenta otros no menos importantes, algunos de ellos ya comentados pero no explicados. Uno de ellos es el denominado Recolector de Basura o Garbage Collector que se incluye dentro del CLR. El Recolector de Basura hace las tareas «sucias» de .NET Framework. Es el gestor de limpieza de .NET. Su objetivo es el de eliminar de la memoria los objetos que no sean útiles para el programador. Si bien el Recolector de Basura se ejecuta sólo cada vez que detecta que no hay espacio suficiente para ejecutar un objeto, podemos personalizar la ejecución del Recolector de Memoria y lanzarla cuando consideremos oportuno. El programador no debe preocuparse por los objetos, su existencia, eliminación,... en otras palabras, no debe preocuparse por la gestión posterior de los objetos. Para más información, acceda a la video/entrevista en inglés con Jim Miller y Jim Hogg, quienes han trabajo durante mucho tiempo en el CLR (http://msdn.microsoft.com/theshow/ Episode020/default.asp) y a la introducción de Microsoft .NET Framework en inglés (http:// msdn.microsoft.com/netframework/prodinfo/overview.asp).
16.4. .NET Framework, un entorno de desarrollo de nuestro tiempo Si algo es el .NET Framework, es un entorno de desarrollo de nuestro tiempo, un entorno de desarrollo moderno. En él podemos conjugar los aspectos modernos y actuales para cubrir las necesidades de los desarrolladores. Es un entorno capaz de resolver las necesidades de los desarrolladores de hoy, capaz incluso de trabajar con los errores que se sucedan en los programas a modo de excepciones. Es decir, podemos trabajar con los errores de una aplicación que se producen en tiempo de ejecución en cualquier momento. El trabajo con excepciones nos ofrece capacidades de gestión de errores mucho mayores a las que estábamos acostumbrados con los antiguos entornos de desarrollo de Microsoft. Y hablando de errores, ¿quién no ha tenido alguna vez problemas con las DLL en una aplicación Windows? Bueno, es hora de irse olvidando de las «famosas» DLL que se utilizaban en los anticuados entornos de desarrollo de Microsoft. En un entorno .NET podemos utilizar una DLL sin problemas, pero el entorno .NET está diseñado fuera del planteamiento y uso de las DLL. Por fin ha llegado el final de la era de las DLL que tantos quebraderos de cabeza han dado a los desarrolladores y administradores de sistemas. Tal ha sido el número de quebraderos de cabeza de las DLL, que se las ha llegado a denominar «el infierno de las DLL». .NET Framework es un entorno orientado a objetos, por lo que a la hora de desarrollar aplicaciones, trabajaremos con objetos y clases en lugar de trabajar con DLLs, aunque si lo deseamos, podremos seguir utilizando las DLL en nuestros desarrollos. .NET Framework es, además, un entorno abierto. Cuando decimos abierto, queremos decir que es un entorno adaptable o receptivo a nuevos len6-50
Lenguajes de programación
guajes de programación y tecnologías. Dentro de .NET Framework, podemos hacer uso de un conjunto de lenguajes de desarrollo determinado, pero una empresa externa, puede desarrollar su propio lenguaje de desarrollo o compilador para la plataforma .NET, tan sólo se ha de seguir unas normas para adaptarse al entorno .NET. Algunas de estas normas constituidas dentro del CLR son las que se han expuesto con anterioridad. De hecho, existen ya lenguajes de desarrollo para la plataforma .NET como Fortran .NET, Cobol .NET, Phyton .NET, etc., pertenecientes a otras empresas externas. Otra de las características de .NET Framework como entorno abierto, es que ha sido desarrollado con la pretensión de cumplir con todos los estándares actuales, siguiendo patrones de estandarización ya aprobados como el ECMA (http://www.ecma.ch/). Todo en el .NET Framework, cumple con los patrones de la normalización y se apoya en estándares abiertos. Este cambio de rumbo de Microsoft es claramente una apuesta por el desarrollador, dándole libertad absoluta en sus decisiones y desarrollos. Pero si algo es especialmente interesante dentro de .NET Framework es todo lo relacionado con la seguridad. La seguridad dentro del entorno .NET, proporciona la posibilidad de ser sensible a tipos o roles de ejecución, es decir, se puede restringir la ejecución de una aplicación según diferentes parámetros. En relación con la seguridad, dentro de .NET Framework, podemos trabajar con el cifrado de información según los algoritmos SHA-1 y MD5. Para más información, las páginas centrales de Microsoft .NET Framework en inglés (http://msdn.microsoft.com/netframework/default.asp).
16.5. .NET es a XML lo que XML es a .NET Pero si algo ha revolucionado a los desarrolladores en los últimos años y especialmente en los últimos meses, es la aparición en escena de un nuevo lenguaje, XML (http://www.w3.org/XML/). XML (eXtensible Markup Language, es decir, Lenguaje de Marcadores Extensible) es un lenguaje de marcas cuya particularidad reside en que está aceptado por el W3C, es decir, es un lenguaje estándar lo cual significa que un sistema Windows puede entenderse con cualquier otro sistema mediante XML y al revés. Ésta es una noticia especialmente esperanzadora para los desarrolladores, ya que no importa la plataforma en la cual se desarrollen sus aplicaciones ni tampoco dónde se ejecuten. Lo más importante es que el lenguaje XML es un lenguaje universal, capaz de ser manipulado siguiendo un conjunto de reglas necesarias para que el lenguaje XML sea correctamente interpretable. Algunas personas definen XML como una tecnología, otros como un lenguaje y otros ni siquiera lo definen como lenguaje ni como tecnología. XML, sin embargo, forma una parte muy importante a tener en cuenta dentro de
6-51
Desarrollo de Sistemas
.NET, por lo que es conveniente tener algunos conocimientos básicos sobre XML para poder utilizarlos en .NET sin problemas, conociendo lo que se realiza en cada instante. Uno de los usos más importantes de XML, es el que tiene que ver con los Servicios Web, más conocidos como XML Web Services (Servicios Web XML). Otro de los usos de XML es el que se da con el trabajo de fuentes de datos junto a ADO .NET. Sin embargo, XML se utiliza dentro de muchos ficheros de configuración y aplicaciones dentro del propio entorno .NET. XML es una tecnología o lenguaje que se utiliza ya en otras plataformas de desarrollo no sólo en .NET. Obtendremos más información sobre XML y sobre los Servicios Web XML, en la dirección Web de Microsoft (http://msdn.microsoft.com/library/ default.asp?url=/nhp/Default.asp?contentid=28000438).
16.6. Conclusiones En este epígrafe hemos visto algunas de las características más destacables de la plataforma .NET Framework. El desarrollo de aplicaciones .NET puede ser muy versátil, por lo que conviene entender con claridad las partes fundamentales de .NET Framework. Como ejemplos de versatilidad en el desarrollo, destacaremos la posibilidad que nos ofrece .NET Framework, de poder desarrollar una aplicación que contenga una parte escrita en Visual Basic .NET y otra parte escrita en C# por ejemplo. Por otro lado, en este artículo hemos diferenciado el .NET Framework de Visual Studio .NET, el cual en algunas ocasiones, es confundido por el desarrollador que se sienta delante de .NET por primera vez. Esperemos que con estas explicaciones, quede claro lo que es la plataforma .NET Framework, qué partes lo componen, y cuáles son sus características más destacables. Visual Basic .NET usa una jerarquía de clases que están incluidas en el .NET Framework, por tanto conocer el .NET Framework nos ayudará a conocer al propio Visual Basic .NET, aunque también necesitarás conocer la forma de usar y de hacer del VB ya que, aunque en el fondo sea lo mismo, el aspecto sintáctico es diferente para cada uno de los lenguajes basados en .NET Framework; si no fuese así sólo, existiría un solo lenguaje.
«.NET Framework es un entorno para construir, instalar y ejecutar servicios Web y otras aplicaciones. Se compone de tres partes principales: el Common Language Runtime, las clases Framework y ASP.NET» Lo que dice la MSDN Library:
6-52
—
«El .NET Framework es un entorno multi-lenguaje para la construcción, distribución y ejecución de Servicios Webs y aplicaciones.»
—
«El .NET Framework es una nueva plataforma diseñada para simplificar el desarrollo de aplicaciones en el entorno distribuido de Internet.»
—
«El .NET Framework consta de dos componentes principales: el Common Language Runtime y la librería de clases .NET Framework.»
Lenguajes de programación
El Common Lenguage Runtime (CLR) es una serie de librerías dinámicas (DLLs), también llamadas assemblies, que hacen las veces de las DLLs del API de Windows así como las librerías runtime de Visual Basic o C++. Como sabrás, cualquier ejecutable depende de una forma u otra de una serie de librerías, ya sea en tiempo de ejecución como a la hora de la compilación. Pues el CLR es eso, una serie de librerías usadas en tiempo de ejecución para que nuestros ejecutables o cualquiera basado en .NET puedan funcionar. Por otro lado, la librería de clases de .NET Framework proporciona una jerarquía de clases orientadas a objeto disponibles para cualquiera de los lenguajes basados en .NET, incluido el Visual Basic. Tendrá a su disposición todas las clases disponibles para el resto de los lenguajes basados en .NET. VB.NET ahora es totalmente un lenguaje orientado a objetos.
17. Clasificación de los lenguajes de programación Los lenguajes de programación constan de: —
Léxico. Conjunto finito de símbolos, a partir del cual se define el vocabulario del lenguaje, la ortografía del lenguaje.
—
Sintaxis. Conjunto finito de reglas para la construcción de las sentencias correctas del lenguaje, la gramática del lenguaje.
—
Semántica. Asociar un significado a cada posible construcción del lenguaje.
Podemos decir que un lenguaje de programación consta de un conjunto de símbolos y un conjunto de reglas válidas para componerlos, de forma que formen un mensaje con significado para el ordenador. Distintos tipos de lenguajes de programación:
17.1. Lenguaje máquina Los ordenadores sólo entienden el código máquina. Este lenguaje utiliza un código binario (0 - 1). Las instrucciones en este lenguaje tienen dos partes diferenciadas: código de operación y código de operando/s: —
En el código de operación se codifica la operación que realiza la instrucción. Este código de operación siempre es único para cada instrucción.
—
En el código de operando se indica la dirección binaria absoluta de memoria en la que se encuentra el operando, con un máximo de tres, sobre el que se aplicará la operación.
6-53
Desarrollo de Sistemas
Como cada tipo de ordenador tiene su código máquina específico, para programar en este lenguaje el programador debe conocer la arquitectura física del ordenador. Ventajas: —
Los programas son directamente interpretables por el procesador central.
—
Los programas no se necesitan transformaciones previas para ser ejecutado.
—
Los programas se ejecutan muy eficientemente, ya que se crean específicamente para los circuitos que lo han de interpretar y ejecutar.
—
Los programas pueden utilizar la totalidad de los recursos de la máquina.
Inconvenientes: —
Las instrucciones son cadenas de ceros y unos (estas cadenas se pueden introducir en el ordenador mediante un código intermedio: octal o hexadecimal).
—
Los datos se utilizan por medio de las direcciones de memoria donde se encuentran (binarias absolutas).
—
El repertorio de instrucciones es muy reducido y las instrucciones realizan operaciones muy simples.
—
Existe poca elasticidad, flexibilidad y versatilidad para la redacción de instrucciones. Estas tienen un formato rígido en cuanto a posición de los distintos campos que configuran la instrucción (código de operación, dirección o direcciones de memoria, códigos de puertos, etc.).
—
El código de operación debe seleccionarse estrictamente entre los que figuran en una tabla o repertorio fijo.
—
Un programa máquina no permite el uso de sentencias declarativas, existiendo sólo las instrucciones.
—
No pueden incluirse comentarios.
—
Es muy difícil de reconocer o interpretar por el usuario.
—
La dependencia del lenguaje máquina de la configuración de la CPU hace que los programas redactados en este lenguaje de programación sean poco transferibles o transportables de un ordenador a otro (no portabilidad).
El lenguaje máquina depende íntimamente a la CPU del computador. Si dos ordenadores tienen CPU diferentes, tendrán distintos lenguajes máquina. Dos ordenadores con el mismo microprocesador e iguales circuitos de control, tienen igual lenguaje máquina. 6-54
Lenguajes de programación
17.2. Traductores Los lenguajes simbólicos permiten utilizar una simbología y terminología próximas a las tradicionalmente utilizadas en la descripción de problemas. Dado que un ordenador solo puede interpretar y ejecutar código máquina, existen programas traductores, que traducen el lenguaje simbólico al lenguaje máquina. El código inicial se denomina programa fuente y el programa obtenido tras el proceso de traducción programa objeto. Existen dos tipos de lenguajes que necesitan de un traductor:
17.2.1. Ensambladores Los lenguajes ensambladores permiten al programador: —
Escribir las instrucciones utilizando, en vez de códigos binarios o intermedios, con una notación simbólica o mnemotécnica para representar los códigos de operación.
—
Los códigos mnemotécnicos están constituidos por tres o cuatro letras que, en forma abreviada, indican la operación a realizar: SUB (sustracción), MOV (movimiento), CALL (llamada a un procedimiento), etc.
—
Utilizan direcciones simbólicas de memoria en lugar de direcciones binarias absolutas.
—
Existen sentencias declarativas (también denominadas seudo instrucciones o directivas) para indicar al traductor la correspondencia entre direcciones simbólicas y direcciones de memoria. Con estas seudo instrucciones, el traductor crea una tabla con cuya ayuda, al generar las instrucciones máquina, sustituye las direcciones simbólicas por las direcciones binarias correspondientes.
—
Las instrucciones escritas en este lenguaje, guardan una estrecha relación con las instrucciones del lenguaje máquina en que posteriormente serán traducidas.
—
Hace corresponder a cada instrucción en ensamblador una instrucción en código máquina (tener en cuenta los macroensambladores explicados mas a continuación).
—
Incluyen líneas de comentarios entre las líneas de instrucciones.
Un programa en ensamblador no puede ejecutarse directamente por el ordenador, siendo necesario ser traducido (ensamblado). El traductor de lenguaje ensamblador a lenguaje máquina se denomina ensamblador. El ensamblador mejora o resuelve algunos de los problemas de los lenguajes máquina pero siguen persistiendo otras limitaciones (repertorio de instrucciones reducido, poca elasticidad para la redacción de instrucciones, o que está íntimamente ligado a la CPU del ordenador).
6-55
Desarrollo de Sistemas
Hay unos lenguajes evolucionados de los ensambladores, que se denominan macroensambladores. Con ellos se solventa en cierta medida la limitación de tener un repertorio de instrucciones muy reducido. Los lenguajes macro ensambladores disponen de macroinstrucciones, como por ejemplo transferir un bloque de datos de memoria principal a disco, multiplicar, dividir, etc. La macroinstrucción es una llamada a un módulo o rutina, llamada macro, que el traductor inserta, antes de realizar el proceso de generación del código máquina definitivo, en el lugar de la llamada correspondiente. A cada macroinstrucción le corresponden varias instrucciones máquina y no sólo una.
17.2.2. Lenguajes de alto nivel Los lenguajes de alto nivel no obligan al usuario a conocer los detalles del ordenador que utiliza. Con estos lenguajes las operaciones se expresan con sentencias o frases muy parecidas al lenguaje matemático o al lenguaje natural, utilizados habitualmente por los humanos. Las características de los lenguajes de alto nivel son: —
Las instrucciones se expresan por caracteres alfabéticos, numéricos y caracteres especiales.
—
Se pueden definir las variables que desee.
—
La asignación de memoria para variables y constantes las hace directamente el compilador.
—
El repertorio de instrucciones es muy amplio, conteniendo operadores y funciones de una gran diversidad: aritméticas, lógicas, de tratamiento de caracteres,…
—
El programador puede definir sus instrucciones con una gran versatilidad, siendo las reglas gramáticas de los lenguajes muy abiertas.
—
Los lenguajes de alto nivel apenas dependen de la máquina.
—
Pueden incluirse comentarios en las líneas de instrucciones, o puede haber líneas específicas de comentarios. Esto facilita la legibilidad de los programas, tanto para el propio programador, como para otras personas.
—
Un programa escrito en un lenguaje de alto nivel no puede ser directamente interpretado por el ordenador, siendo necesario realizar previamente su traducción a lenguaje máquina.
Usualmente la traducción se hace en dos etapas: primero a ensamblador, y posteriormente a código máquina. Por lo general, una sentencia en un lenguaje de alto nivel da lugar, al ser traducida, a varias instrucciones en ensamblador o lenguaje máquina.
6-56
Lenguajes de programación
Entre sus actividades, el American National Standard Institute (ANSI) se encarga de realizar normalizaciones de lenguajes para garantizar la translabilidad de los programas. Existen dos tipos de traductores para los lenguajes de alto nivel: A) Compiladores Los compiladores traducen el código fuente a código objeto, para todo el programa a la vez. A su vez llevan a cabo optimizaciones del programa que permiten que el programa ocupe menos espacio o sea más rápido. Un compilador traduce un programa fuente, escrito en un lenguaje de alto nivel, a un programa objeto, escrito en lenguaje ensamblador o máquina. El programa fuente suele estar contenido en fichero y el programa objeto pasa a ocupar otros ficheros. El fichero objeto puede almacenarse en memoria masiva para ser procesado posteriormente. La traducción por un compilador consta de dos etapas fundamentales, que a veces no están claramente diferenciadas a lo largo del proceso: —
La etapa de análisis del programa fuente.
—
La etapa de síntesis del programa objeto.
A su vez, cada una de estas etapas conlleva la realización de varias fases, y en cada una de las cuales se recorre o analiza completamente el programa fuente. 1.
Análisis lexicográfico. Consiste en descomponer el programa fuente en sus elementos constituyentes, es decir, sus símbolos, que son caracteres o secuencias de caracteres con significado especial. El analizador léxico (también denominado escáner) aísla los símbolos, identifica su tipo y almacena en las tablas de símbolos la información del símbolo que pueda ser necesaria durante el proceso de traducción. La representación obtenida en esta fase contiene la misma información que el programa fuente, pero de forma más compacta.
2.
Análisis sintáctico. La sintaxis de los lenguajes de programación se especifica mediante un conjunto de reglas (la gramática del lenguaje). Esta fase deberá comprobar si un programa es sintácticamente correcto, es decir, si sus estructuras (expresiones, sentencias o asignaciones) están construidas de acuerdo con las reglas del lenguaje.
3.
Análisis semántico. La semántica de un lenguaje de programación define el significado dado a las distintas construcciones sintácticas. En los lenguajes de programación, el significado está ligado a la estructura sin6-57
Desarrollo de Sistemas
táctica de las sentencias. En el proceso de traducción, el significado de las sentencias se obtiene de la identificación sintáctica de las construcciones sintácticas y de la información almacenada en la tabla de símbolos. 4.
Generación de Código Intermedio. Si no se han producido errores en algunas de las etapas anteriores, este módulo realiza la traducción a un código interno propio del compilador, denominado Código Intermedio, a fin de permitir la transportabilidad del lenguaje a otros ordenadores.
5.
Optimizaciones. En la fase de optimización se mejora el código intermedio generado anteriormente, analizando el programa de forma global.
6.
Generación de Código Objeto. En esta etapa se genera el código objeto final. En algunos casos, este código es directamente ejecutable, y en otros necesita algunos pasos previos a la ejecución (ensamblado, encuadernación y carga). La compilación es un proceso complejo y que consume a veces un tiempo muy superior a la propia ejecución del programa. En cualquiera de las fases de análisis el compilador puede dar mensajes sobre los errores que detecta en el programa fuente, cancelando en ocasiones la compilación para que el usuario realice en el fichero las correcciones oportunas. Existen compiladores que permiten al usuario omitir o reducir las fases de optimización, disminuyéndose así el tiempo global de la compilación.
B) Intérpretes Los intérpretes traducen el código fuente línea por línea, sin generar programa objeto, y traduciendo las instrucciones en comandos para el hardware. Son más lentos que los compiladores, puesto que tienen que interpretar una línea cada vez que pasan por ella. Un intérprete hace que un programa fuente escrito en un lenguaje vaya, sentencia a sentencia, traduciéndose a código objeto y sea ejecutado directamente por el ordenador. El intérprete capta una sentencia fuente y la traduce, expandiéndola en una o varias instrucciones máquina, que ejecuta inmediatamente, no creándose, por tanto, un fichero o programa objeto almacenable en memoria masiva para posteriores ejecuciones. Características de los lenguajes interpretados: —
6-58
Las optimizaciones sólo se realizan dentro del contexto de cada sentencia.
Lenguajes de programación
—
Si una sentencia forma parte de un bucle, se traduce tantas veces como tenga que ejecutarse el bucle, y no una sola vez como ocurriría en un compilador.
—
Cada vez que utilicemos un programa tenemos que volver a traducirlo, ya que en la traducción no se genera un fichero objeto que poder guardar en memoria masiva y utilizarlo en cada ejecución.
Los intérpretes son preferibles a los compiladores cuando el número de veces que se va a ejecutar el programa es muy bajo. Es más fácil desarrollar programas. Los lenguajes intérpretes resultan más pedagógicos para aprender a programar, ya que el alumno puede detectar y corregir más fácilmente sus errores. Los traductores-intérpretes ocupan, por lo general, menos memoria que los compiladores. En la actualidad, para un lenguaje dado pueden existir tanto compiladores como intérpretes. C) Clasificación de los lenguajes de programación según el estilo de programación Antes de realizar esta clasificación hemos de pensar que un mismo lenguaje podría estar incluido en más de un paradigma. • Lenguajes imperativos o procedimentales o procedurales Se basan en la asignación de valores. Usan la instrucción o sentencia de asignación como construcción básica en la estructuras de los programas, son lenguajes orientados a instrucciones. Se fundamentan en la utilización de variables para almacenar valores y en la realización de operaciones con los datos almacenados. Se caracterizan por: —
Uso intensivo de variables.
—
Estructura de programas basada en instrucciones.
—
Manejo frecuente de las instrucciones de asignación.
—
Resolución de algoritmos por medio de estructuras de control secuenciales, alternativas (condicionales) y repetitivas (iterativas).
—
Incorporan mecanismos para el manejo de bloques.
—
Gestionan la memoria de modo dinámico (en tiempo de ejecución).
Ejemplos de este paradigma son: Fortran, Pascal, C,… a)
Lenguajes de alto nivel, caracterizados por estar enfocados a la resolución de problemas en campos de aplicación específicos y los programas escritos en ellos ser fácilmente trasladables de uno a otro ordenador. 6-59
Desarrollo de Sistemas
b)
Lenguajes ensambladores y máquina, totalmente adaptados y predeterminados por la CPU de la máquina.
• Lenguajes declarativos. Son lenguajes de muy alto nivel cuya notación es muy próxima al problema real del algoritmo que pretenten resolver. Están basados en la definición de funciones o relaciones. No utilizan instrucciones de asignación (sus variables no almacenan valores). Son más fáciles de utilizar pues están muy próximos al algoritmo. Se suelen denominar también lenguajes de órdenes, ya que los programas están formados por sentencias que ordenan “qué es lo que se quiere hacer”, no teniendo el programador que indicar al ordenador el proceso detallado (el algoritmo) de cómo hacerlo”. Hay dos tipos: a)
Lenguajes funcionales o aplicativos. Los lenguajes funcionales son un tipo de lenguajes declarativos, en los que los programas están formados por una serie de definiciones de funciones. No hay instrucciones, todo el programa es una función, todas las operaciones se realizan por composición de funciones más simples. Ejemplos de estos lenguajes son el LISP y el SCHEME.
b)
Lenguajes lógicos. Los lenguajes lógicos son el otro tipo de lenguajes declarativos, y en ellos los programas están formados por una serie de definiciones de predicados (relaciones entre objetos-datos). También se les denomina lenguajes de programación lógica, y el mayor exponente es el lenguaje PROLOG.
• Lenguajes concurrentes Permiten la ejecución simultánea de dos o más tareas. Podrían llamarse también lenguajes paralelos o simultáneos. Podría ser una característica del lenguaje o el resultado de ampliar las instrucciones de un lenguaje que en sus orígenes no sea concurrente. • Lenguajes Orientados a Objetos Se dice que un lenguaje es Orientado a Objetos si soporta tipos abstractos de datos (clases). Se basan en objetos (entes físicos de las clases), herencia, polimorfismo, abstracción y encapsulado. Ejemplos serian JAVA y C#.
6-60
Anexo I Lenguaje C.
Desarrollo de Sistemas
Guión-resumen
1. Introducción
9. Cadenas de caracteres
2. Definición de las variables
10. Estructuras, uniones, enumeraciones y typedef
2.1. Nombres de identificadores 2.2. Tipos de datos 2.3. Modificadores de tipos de datos 2.4. Modificadores de acceso 2.5. Clase de almacenamiento 3. Comentarios 4. Operadores 4.1. Clasificación de los operadores 4.2. Orden de prioridad de los operadores 5. Caracteres especiales (barra invertida) 6. Instrucciones 6.1. Las instrucciones de control 6.2. Bucles 6.3. Sentencias de salto 7. Arrays 7.1. Declaración de un array 7.2. Arrays multidimensionales 7.3. Cadenas de caracteres 7.4. Acceso a los miembros de un array 7.5. Arrays indeterminados 8. Punteros 8.1. Definición de un puntero 8.2. Arrays y punteros 8.3. Indirección múltiple punteros a punteros 8.4. Arrays de punteros
Anexo I 6-2
10.1. Estructuras 10.2. Uniones 10.3. Enumeraciones 10.4. Typedef 11. Funciones 12. Memoria dinámica 13. Entrada y Salida por consola (estándar) 13.1. Entrada y salida de caracteres 13.2. La función scanf() 13.3. Otras funciones para la entrada/salida estándar 14. Entrada y salida con ficheros 14.1. Apertura y cierre de un fichero 14.2. Lectura y escritura sobre un fichero 14.3. Lectura y escritura de datos binarios 14.4. Operaciones especiales con los ficheros 14.5. Posicionamiento del indicador de posición del fichero 14.6. Otras operaciones con ficheros 15 El preprocesador 15.1. Directivas de preprocesado
Lenguaje C
1.
Introducción
El lenguaje de programación C ha sido utilizado durante muchos años y hoy en día sigue siendo un lenguaje a tener en cuenta. Ha sido utilizado para infinidad de utilidades como compiladores, sistemas operativos, bases de datos, etc. Este detalle de su uso general es quizá una de las diferencias importantes con respecto a otros lenguajes de programación cuyo uso está más relegado a una tarea en concreto. Dicho de otra forma, estamos ante un lenguaje de propósito general. Muchas son sus ventajas: portabilidad, compatibilidad, flexibilidad, etc. El lenguaje C nació en 1972 en los Laboratorios Bell de la mano de Dennis Ritchie. Tuvo dos grandes revisiones en los años 1989 (C89) y 1999 (C99), este último conocido como el estándar ANSI C es objeto de esta oposición. Entre las características más generales de este lenguaje destacaremos a modo de primer paso las siguientes: —
Permite la manipulación de “bits”, “bytes” y direcciones.
—
No lleva a cabo comprobación de errores en tiempo de ejecución. Por supuesto sí lo lleva en tiempo de compilación y de “linkado”.
—
Existe un conjunto de palabras clave que luego cotaremos que en C89 son 32 y en C99 ha sido ampliado en 5 más. No obstante, los diferentes compiladores de este lenguaje pueden añadir más.
—
No se permite la creación de funciones dentro de funciones.
—
Los bloques de código van encerrados entre llaves.
—
Un programa puede estar formado por uno o varios archivos que se compilan cada uno de ellos por separado. Cada uno de esos archivos ha de llevar extensión “*.c”.
—
Existen bibliotecas de subrutinas precreadas que podemos incluir en nuestro programa. Estas bibliotecas se encuentran en archivos con extensión “*.h”.
—
Las instrucciones terminan en “;”.
—
El concepto de verdadero toma el valor 1 y el de falso el 0. Aunque diremos que generalmente cualquier valor diferente de 0 es tomado como verdadero.
El formato general de un programa en C viene dado por el siguiente esquema:
Pudiendo estar todo el código del programa en un solo archivo o en diferentes archivos que luego han de ser compilados y linkados. Los programas en C están formados por una serie de líneas de código que se ejecutan sucesivamente. Todos los programas se dividen en bloques, estos bloques vienen dados por la combinación de “{“ de apertura del bloque y “}” de finalización del bloque. La ejecución del programa siempre comienza en la función main(). Esta función es la encargada de llamar a las demás. Los bloques están formados por instrucciones. En C todas las instrucciones acaban con un “;”. Hay varios tipos de instrucciones que iremos estudiando a lo largo de este tema. • La función main Si el programa es pequeño es probable que la mayor parte del programa se halle dentro de la función main. Cuando el programa comienza a tener un tamaño mayor conviene dejar para la función main solo el cuerpo del programa. Al principio de la función main colocaremos todas las rutinas de inicialización. Se suele procesar en la función main las posibles contraseñas que queramos darle al programa y los mensajes de bienvenida. Si el programa admite parámetros en la línea de órdenes, la función main debe procesarlos, ya que la función main tiene normalmente acceso a la línea de argumentos con que fue ejecutado el programa. Es esta función la primera que se ejecuta y debe ser la que llame a las demás, aunque luego otra función llame sucesivamente a otras funciones. Al terminar la llamada a la función llamante, el control del programa vuelve a “main”. También debe ser la última en ejecutarse.
Anexo I 6-4
Lenguaje C
2.
Definición de las variables
Las variables permiten guardar información. Los principales tipos de datos son los datos numéricos, los caracteres y las cadenas de caracteres. Comencemos creando un programa con una variable numérica entera, “x”:
#include int x = 1; main() { printf(«x vale %d\n», x); } El valor de las variables es, como su propio nombre indica, variable. Podemos alterar su valor en cualquier punto del programa. La forma más sencilla de hacerlo es mediante una sentencia de asignación. Para asignar un nombre a una variable se escribe su identificador seguido de un = y el nuevo valor. Este tipo de sentencia es el segundo más importante. Así:
#include int i = 1; main() { printf(«el antiguo valor de i es %d\n», i); i = 2; printf(«el nuevo es %d\n», i); } • Palabras reservadas El lenguaje C está formado por un conjunto pequeño de palabras clave. En el estándar C89 hay 32 palabras claves: auto
break
float
for
goto
if
int
long
register
case
default do
short
signed
sizeof
static
struct
typedef
char
const
return
enum
extern
union
unsigned
void
volatile
while
double else
continue switch
A este conjunto de palabras se les denomina «palabras reservadas». En el estándar C99 se le han añadido cinco más. • Duración de las variables Las variables pueden ser de dos tipos: estáticas y dinámicas. Las estáticas se crean al principio del programa y duran mientras el programa se ejecute. Anexo I 6-5
Desarrollo de Sistemas
Las variables son dinámicas si son creadas dentro de una función. Su existencia está ligada a la existencia de la función. Se crean cuando la función es llamada y se destruyen cuando la función o subrutina devuelve el control a la rutina que la llamó. Las variables estáticas se utilizan para almacenar valores que se van a necesitar a lo largo de todo el programa. Las variables dinámicas se suelen utilizar para guardar resultados intermedios en los cálculos de las funciones. Como regla general, una variable es estática cuando se crea fuera de una función y es dinámica cuando se crea dentro de una función. • Definición de una variable Cuando se define una variable, lo que realmente estamos haciendo es reservando en memoria un espacio para poder almacenar los valores que pueda ir tomando dicha variable a lo largo de la ejecución del programa. La sintaxis que se debe de seguir en C para definir una variable viene dada por:
[Clase_almacenamiento] [modificadores] [tipo_dato] [identificador]; Todas las variables han de ser declaradas antes de ser usadas. Veamos en detalle cada uno de los elementos que forman esta sintaxis:
2.1.
Nombres de identificadores
Son los nombres usados para referirse a las variables, funciones, etiquetas y otros elementos del programa. Son nombres construidos por una secuencia de letras, dígitos y el carácter subrayado. Las reglas que han de seguir los identificadores son: —
Pueden contener letras, dígitos y el carácter subrayado “_”, pero obligatoriamente deben de comenzar por un carácter alfabético o el “_”. Es decir, el primer carácter debe ser una letra o un símbolo de subrayado, los caracteres siguientes pueden ser letras, números o símbolos de subrayado.
—
No está permitido el uso de espacios en blanco.
—
La longitud del identificador vienen dada por el compilador que se utilice (generalmente está en torno a los 32 caracteres).
—
Las letras mayúsculas y minúsculas son interpretadas como diferentes.
Existen cinco tipos de datos básicos; todos los demás tipos de datos se basan en alguno de estos tipos. El tamaño de estos tipos de datos pueden variar dependiendo del procesador y del compilador utilizado; por ejemplo, los datos de tipo “int” pueden ocupar 16 ó 32 bits dependiendo del procesador que tengamos: TIPO
BITS
RANGO
char
8
0 a 255
int
16
-32.768 a 32.767
float
32
3,4 E -38 a 3,4 E +38
double
64
1,7 E -308 a 1,7 E +308
void
0
sin valor
Nota: Void se usa para declarar funciones que no devuelven ningún valor o para declarar funciones sin parámetros.
2.3.
Modificadores de tipos de datos —
signed: con signo.
—
unsigned: sin signo.
—
long: usado en “int” hace que estos tengan un tamaño de 32 bits. Usado en “double” hace que éstos tengan un tamaño de 80.
—
short: usado en “int” hace que estos tengan un tamaño de 16 bits.
Los modificadores signed, unsigned, long y short se pueden aplicar a los tipos base entero y carácter. Sin embargo, long también se puede aplicar a double.
Anexo I 6-7
Desarrollo de Sistemas
2.4.
TIPO
BITS
RANGO
char
8
-128 a 127
unsigned char
8
0 a 255
signed char
8
-128 a 127
int
16
-32.768 a 32.767
unsigned int
16
0 a 65.535
signed int
16
-32.768 a 32.767
short int
16
-32.768 a 32.767
unsigned short int
16
0 a 65.535
signed short int
16
-32.768 a 32767
long int
32
-2147483648 a 2147483647
signed long int
32
-2147483648 a 2147483647
float
32
3,4 E -38 a 3,4 E +38
double
64
1,7 E -308 a 1,7 E +308
long double
80
1,7 E -308 a 1,7 E +308
Modificadores de acceso
Se utilizan para controlar la forma en que se modifican las variables. Existen dos modificadores de acceso:
2.5.
—
const. Las variables de tipo const no pueden ser cambiadas durante la ejecución del programa. Por ejemplo: const int x;
—
volatile. A través de este modificador se le indica al compilador que el contenido de la variable puede ser modificado por elementos ajenos al programa a lo largo de su ejecución. Por ejemplo: volatile float hora:
Clase de almacenamiento
• Alcance de las variables Otra característica de las variables es su alcance. El alcance se refiere a los lugares de un programa en los que podemos utilizar una determinada variable. Distinguiremos así dos tipos principales de variables: globales y locales. Una variable es global cuando es accesible desde todo el programa, y es local Anexo I 6-8
Lenguaje C
cuando sólo puede acceder a ella la función que la creó. También hay una norma general para el alcance de las variables: una variable es global cuando se define fuera de una función, y es local cuando se define dentro de una función o de un bloque. Dentro de las variables globales hay dos tipos: las que son accesibles por todos los ficheros que componen nuestro programa y las que son accesibles solo por todas las funciones que componen un fichero. Esto es debido a que normalmente los programas en C se fragmentan en módulos más pequeños, que son mas fáciles de manejar y depurar. Por ello, hay veces que interesa que una variable sea accesible desde todos los módulos, y otras solo que sea accesible por las funciones que componen un determinado módulo. Por defecto, todas las variables globales que se creen son accesibles por todos los ficheros que componen nuestro programa. Existen, por tanto, tres sitios donde se pueden declarar variables: dentro de las funciones (variables locales), en la definición de parámetros de funciones (parámetros formales) y fuera de todas las funciones (variables globales). Normalmente en todo programa en C hay una sección de variables globales. En las variables globales almacenaremos datos que deben ser accesibles a todo el programa. Cuando el programa es pequeño, por ejemplo si consta de un solo fichero, por comodidad se suelen definir todas las variables como globales. A través de la clase de almacenamiento se especifica la forma en la que se almacenara la variable. Existen cuatro especificadores de almacenamiento, que son:
3.
—
auto. Es la opción por defecto de tal forma que la variable se considera local siempre y cuando esté definida dentro de una función.
—
extern. Con este especificador indicamos que la variable esta declarada con un enlace externo en algún otro lugar del programa. Su empleo más típico es cuando el archivo consta de dos o más archivos y en alguno de esos archivos deseamos que enlace con la variable global definida en otro de los archivos.
—
static. En una variable local el especificador static hace que la variable mantenga su valor en las sucesivas llamadas a la función. Tienen memoria asignada durante toda la ejecución del programa. Su valor es recordado incluso si la función donde está definida acaba y se vuelve a llamar más tarde. En una variable global el especificador static hace que esa variable solo sea conocida en el archivo donde ha sido declarada.
—
register. El especificador register pide al compilador que mantenga el valor de una variable con ese especificador de forma que se permita el acceso más rápido a la misma. Esto significa colocarla en un registro de la CPU. Solo se puede aplicar a variables locales y a los parámetros formales de una función.
Comentarios
Un comentario es una línea que se incluye en el programa, cuya misión consiste en aclarar la función de una parte concreta del programa a otro lector,
Anexo I 6-9
Desarrollo de Sistemas
o incluso al mismo programador. En C hay dos formas de incluir estos comentarios. La primera es incluir el texto que sirve de comentario al principio de la sección, entre dos símbolos especiales: el /* o principio de comentario y el */ o fin de comentario. Todo el texto que se incluya entre ellos el compilador lo ignora, incluyendo los saltos de línea. Por ejemplo:
/* Hola, que tal, estoy dentro de un comentario */ El otro tipo de comentarios se suele usar para señalar una determinada línea del programa. Para ello escribimos el comentario a la derecha de la línea a comenta con “//”. Por ejemplo:
printf(“HOLA, HOLITA”); // imprime HOLA, HOLITA.
4.
Operadores Los operadores en C se pueden clasificar de la siguiente forma:
4.1.
Clasificación de los operadores
A) Aritméticos •
-
resta
•
+
suma
•
*
producto
•
/
división
•
%
módulo (resto de la división entera)
•
—
decrementar
•
++
incrementar
B) Relacionales •
>
mayor que
•
>=
mayor o igual que
•
<
menor que
•
<=
menor o igual que
•
==
igual
•
!=
distinto
Anexo I 6-10
Lenguaje C
C) Lógicos •
&&
y (Conjunción)
•
||
o (Disyunción)
•
!
no (Negación)
D) El operador ? Exp 1 ? Exp 2 : Exp 3 Se evalúa exp1 si es cierto se evalúa exp2 y toma ese valor para la expresión. Si exp1 es falso evalúa exp3 tomando su valor para la expresión. Ejemplo:
x=5; y=x>2 ? 100 : 200; Resultado: 100 E) Operadores a nivel de bits •
&
y
•
|
o
•
^
o exclusivo
•
~
complemento a 1
•
>>
desplazamiento a la derecha
•
<<
desplazamiento a la izquierda
F) Operador de molde Se utiliza para convertir el tipo de dato de un operando. Anteponiendo al operando el tipo entre paréntesis. Ejemplo (int) 3.2; G) Los operadores de punteros & y * •
& devuelve la dirección de memoria del operando. Ejemplo: m=&cont; coloca en m la dirección de memoria de la variable cont
Anexo I 6-11
Desarrollo de Sistemas
•
* devuelve el valor de la variable ubicada en la dirección que se especifica. Ejemplo: q=*m; coloca el valor de cont en q.
H) Operador de asignación •
= Asignación
Forma general: nombre_variable = expresion; I) Operador de tamaño •
sizeof devuelve el tamaño de una variable o de un tipo de dato
Es un operador monario que devuelve la longitud, en “bytes”, de la variable o del especificador de tipo al que precede. El nombre del tipo debe ir entre paréntesis.
Ejemplo: float f; printf («%f»,sizeof f); Mostrara 4 printf («%d», sizeof (int)); Mostrara 2 J) Otros operadores Existen otros operadores como son: •
El “.” Que permite acceder a elementos individuales en las estructuras y en las uniones.
•
El “->” que opera igual que el anterior pero trabajando a través de punteros.
•
El operador “,” que permite encadenar varias expresiones.
•
El operador “[]” cuyo uso fundamental es en el trabajo con “arrays”.
•
El operador “( )” para aumentar la precedencia de las operaciones a realizar.
K) Abreviaturas en C •
+= ; x=x+10 ———— x+=10
•
-=;
x=x-10 ————- x-=10
•
*=;
x=x*10 ————- x*=10
•
/=
x=x/10 ————- x/=10
Anexo I 6-12
Lenguaje C
4.2.
Orden de prioridad de los operadores ()
[]
->
. —
!
~
++
*
/
%
+
-
<<
>>
<
<=
==
!=
>
>=
-=
*=
(tipo) * (punteros)
&
sizeof
& ^ | && || ?: =
+=
/=
, Los operadores monarios y “?” tienen prioridad de derecha a izquierda. Los demás de de izquierda a derecha y de arriba abajo.
5.
Caracteres especiales (barra invertida)
6.
\n Nueva línea
\f Salto de página
\t Tabulación horizontal
\\ Barra invertida
\b Espacio atrás
\’ Comilla simple
\r Retorno de carro
\» Comilla doble
Instrucciones
En el lenguaje C, al igual que en la mayoría de los lenguajes de programación, existen los siguientes tipos de instrucciones: •
Selección, condicionales o de control. Anexo I 6-13
Desarrollo de Sistemas
•
Repetitivas o bucles.
•
Salto.
En las sentencias de control y repetitivas su ámbito incluye el bloque que se encierre entre “{ }”; no obstante, si éste se omite, su ámbito es única y exclusivamente la siguiente sentencia a ejecutar. Por tanto, cuando necesitamos ejecutar varias sentencias que dependen de una condición o queramos que éstas se repitan, utilizaremos la sentencia de tipo bloque de sentencias “ { }”.
6.1.
Las instrucciones de control
A) La sentencia “if” La primera sentencia de control es la sentencia “if”. Admite dos tipos de sintaxis:
if (expresion1) sentencia1; o: if (expresion1) sentencia1; else sentencia2; o: if (expresion) { ............ ............ } else { ........... ........... } o: if (expresion1) { ............ ............ } else if(expresion2){ ...........
Anexo I 6-14
Lenguaje C
........... } ... .... else if(expresionn){ ........... ........... } else { ........... ........... } o: if (expresion1) { ............ ............ } else if(expresion2){ if (expresion) { ............ ............ } else { ........... ........... } } else { ........... ........... } Sirve para bifurcar en un punto de programa. La sentencia “if” permite tomar decisiones al programa. En su primera forma, la sentencia1 solo se ejecuta si el resultado de evaluar la expresion1 es verdadero (distinto de cero). En la segunda forma tenemos dos posibilidades: si al evaluar la expresion1 el Anexo I 6-15
Desarrollo de Sistemas
resultado es verdadero se ejecuta la sentencia1, pero si el resultado es falso se ejecuta la sentencia2. En cualquier caso sólo una de las dos sentencias se ejecuta. Tras evaluarse la expresión “if” y ejecutarse la sentencia adecuada, el programa continua con la línea siguiente a la de la última sentencia del “if”. Para la sentencia “if” vale como expresión cualquier expresión valida en C, incluso las asignaciones y llamadas a funciones. Cuando hay dos if anidados y a continuación hay un else, este else pertenece al último “if”. Así, en el caso anterior, el primer else corresponde al segundo “if”. Si queremos que un else pertenezca al primer “if” de un “if” anidado deberemos encerrar al segundo entre paréntesis. Por ejemplo:
if (num > 0) { if (num == 1) puts(«num es igual a 1»); } else puts(«num es menor que 0»); B) La sentencia “switch”
switch (variable) { case cte1 : ........... ........... break; case cte2 : ........... ........... break; ............. ............. default : ........... ........... } “Switch” solo puede comprobar la igualdad. Esta sentencia sirve para agrupar varias sentencias “if” en una sola, en el caso particular en el que una variable es comparada a diferentes valores, todos ellos constantes. Primero se evalúa la expresión de control. Se compara con la expresión de la primera etiqueta case. Anexo I 6-16
Lenguaje C
Si son iguales se ejecuta la sentencia1. Luego se vuelve a comparar la expresión de control con la etiqueta del segundo case. De nuevo, si son iguales se ejecuta la sentencia2. Se repite el proceso hasta agotar todas las etiquetas case. Si al llegar a la etiqueta default no se ha ejecutado ninguna otra sentencia. Ésta es la acción por defecto. La etiqueta default es opcional.
6.2.
Bucles
Un bucle es un conjunto de sentencias que se ejecutan repetidamente hasta que se alcanza una condición de fin de bucle o condición de salida. A) El bucle “for” La sintaxis del bucle “for” es:
for (inicio; control; incremento o decremento) sentencia; Este bucle se utiliza para realizar una acción un número determinado de veces. Está compuesto de tres expresiones: la de inicio, la de control y la de incremento, y las sentencias que, si son varias, deben de ir dentro de un bloque (“{ }”). Primero se ejecuta la expresión de inicio. Normalmente‚ ésta es una expresión de asignación a una variable, que le da un valor inicial. Luego se comprueba la expresión de control. Si esta expresión es verdadera, se ejecuta la sentencia o el grupo de sentencias. Si la expresión es falsa, el bucle finaliza. Tras ejecutarse la sentencia se evalúa la expresión de incremento. A continuación se vuelve al segundo paso. El bucle finaliza cuando la expresión de control es falsa. B) El bucle “while” Su sintaxis es la siguiente:
while (expresion) sentencia; El bucle “while” comienza por evaluar la expresión. Si es cierta se ejecutan las sentencias que, si son varias, deben ir dentro de un bloque (“{ }”). Entonces se vuelve a evaluar la expresion1. De nuevo si es verdadera se vuelve a ejecutar la sentencia. Este proceso continua hasta que el resultado de evaluar la expresión es falso. C) El bucle “do / while” La sintaxis de este bucle es:
do sentencia; while (expresion); Anexo I 6-17
Desarrollo de Sistemas
o: do { ........... ........... } while (condicion); Su funcionamiento es análogo el del bucle “while”, salvo que la expresión de control se evalúa al final del bucle. Esto nos garantiza que el bucle “do-while” se ejecuta al menos una vez. Podemos incluir dentro del bucle un grupo de sentencias como siempre dentro de un bloque (“{ }”); no obstante, éste es el único bucle en el que no serían obligatorias llaves para encerrar el grupo de sentencias. Por ejemplo:
int i = 5; do printf(«numero %d\n», i); —i; while (i >= 0);
6.3.
Sentencias de salto
A) La sentencia “break” Tiene dos usos: •
Para finalizar un case en una sentencia “switch”.
•
Para forzar la terminación inmediata de un bucle.
Ejemplo:
#include int main() { int i; for(i=0;i<=10;i++){ if(i==3){ /*break acaba con el for*/ break; } printf(«Hola %d\n»,i); } return(0); } Anexo I 6-18
Lenguaje C
B) La sentencia “exit” Para salir de un programa anticipadamente. Da lugar a la terminación inmediata del programa, forzando la vuelta al Sistema Operativo Usa el archivo de cabecera stdlib.h. Ejemplo:
#include int main() { int num; printf(«introduce un numero\n»); scanf(«%d»,&num); if (num==2)
{
/*al introducir el 2, exit obliga a terminar el programa sin ejecutar las sentencias posteriores*/ exit(0); } printf(«Hola\n»); return(0); } C) La sentencia “continue” Hace comenzar la iteración siguiente del bucle, saltando así la secuencia de instrucciones comprendida entre el continue y el fin del bucle. Ejemplo:
D) La sentencia “goto” Esta sentencia está totalmente desaconsejada, y su función es la de permitir el salto de un punto del programa a otro a través de un marcador. • Diferencias entre las sentencias “break” y “continue” en un bucle Hay veces en que interesa romper un bucle en una determinada posición, para ejecutar una nueva pasada del bucle o para finalizar su ejecución. Para realizar estos dos tipos de salto disponemos de dos sentencias: la sentencia “break” y la sentencia “continue”. La sentencia “break” rompe la ejecución de un bucle o bloque de instrucciones y continúa en la instrucción que siga al bucle o bloque. Por ejemplo:
int a = 10; while (a—>=1) { if (a ==· 1) break; printf(«%d\n», a); } printf(“fuera del bucle”); La sentencia “continue” rompe la ejecución habitual del bucle y procede a evaluar de nuevo la expresión del bucle. Actúa como si se saltase al final del bloque de un bucle. Por ejemplo:
int a = 1; while (a++ < 10) { if (a==7) continue; printf(«%d\n», a); }
7.
Arrays
Podemos definir los arrays o tablas de C como una colección de datos del mismo tipo que se denominan o referencian por un mismo nombre común y que son almacenados en posiciones de memoria físicamente contiguas, donde la dirección de memoria más baja corresponde al primer elemento y la más alta al último elemento. Los arrays y los punteros que serán estudiados en la siguiente sección están íntimamente relacionados.
Anexo I 6-20
Lenguaje C
Dentro de los arrays tenemos los siguientes tipos: •
Arrays unidimensionales.
•
Arrays bidimensionales.
•
Arrays multidimensionales.
•
Arrays de caracteres.
•
Arrays indeterminados.
Un array tiene una dimensión y un tamaño. La dimensión viene dada por el número de pares de “[ ]” que éste tenga y el tamaño (no capacidad que ocupa) por la multiplicación de los enteros que se introduce dentro de los corchetes, de tal forma que int x[2][3] tendrá por dimensión dos y por tamaño 2*3. Todos los arrays tienen el 0 como índice de su primer elemento y como último uno menos de su tamaño y los demás tienen la numeración consecutiva. Ejemplo. char c [10]; “array” de caracteres de dimensión uno que tiene 10 elementos, desde c[0] hasta c[9].
7.1.
Declaración de un array
Para crear un array se sigue la misma sintaxis que para la creación de variables, algo normal si pensamos en que realmente son colecciones de variables, de tal forma que para declarar un array de n elementos de un cierto tipo se introduce la línea:
modificadores tipo identificador [n]; Donde n es una constante de tamaño fijo. Si el array es estático o global, el compilador crea el espacio para la matriz al principio del programa. Si es de tipo automático, reservar el espacio en la pila de datos. Como todos los tipos de datos, un array se puede inicializar. Si el array es estático, por defecto cada elemento se inicializa a 0. Si es dinámico, los valores de cada elemento no están definidos y antes de usarlos los debemos inicializar. Para inicializar un array en el momento de su creación añadiremos tras el identificador y los corchetes de tamaño un = y la serie de valores (tipo nombre_array [tamaño] = {lista de valores};). Cada valor debe ser una constante válida para el tipo de datos del array, y cada valor ir separado del valor precedente mediante una coma. Para abrir y cerrar la serie de valores usaremos las llaves. Por ejemplo:
int x [4]={0,1,2,3 }; char y[] = { ‘t’, ‘a’, ‘i’, ‘\0’}; Los “arrays” de carácteres que contienen cadenas permiten una inicialización de la forma:
char nombre_array [tamaño]=»cadena»; Se añade automáticamente el terminador nulo al final de la cadena. Anexo I 6-21
Desarrollo de Sistemas
No podemos dar un número de valores mayor al tamaño del “array”, pero si podemos dar menos de los necesarios. El compilador siempre rellenar los demás con ceros. El compilador siempre asignar el primer valor al primer elemento del array, y los demás los asignan consecutivamente. Como siempre, acabaremos la línea con un “;”.
7.2.
Arrays multidimensionales
En C se pueden construir arrays de arrays, es decir, tipos de “arrays” cuyos elementos son a su vez “arrays”. Dado que ahora necesitaremos un índice para situarnos dentro del “array” principal y otro más para movernos dentro de cada uno de los nuevos “arrays”, diremos que los “arrays” de “arrays” poseen dos dimensiones. A un “array” de dos dimensiones se le suele llamar matriz, y a un array de una dimensión, vector. Para crear una matriz de enteros, es decir, un “array” de “arrays” de enteros, se hace de modo análogo a cuando se crea un “array”, salvo que ahora se añade el nuevo índice entre corchetes. Por ejemplo:
int matriz[8][9]; Declara una matriz de 8 filas por 9 columnas, o 9 por 8 columnas, según queramos representar. La elección de cuál índice representa las filas y cuál las columnas es arbitrario. Podemos usar la norma habitual en matemáticas: el de la izquierda representa filas y el de la derecha columnas. Es decir, en el caso particular de los Arrays Bidimensionales se declaran utilizando la siguiente forma general:
tipo nombre_array [tamaño 2ª dim] [tamaño 1ª dim]; Ejemplo ——-> int x [10][20]; Los “arrays” multidimensionales siguen la misma sintaxis, pero en vez de ser de dos dimensiones, son de la dimensión que nosotros le indiquemos.
7.3.
Cadenas de caracteres Hay un tipo de “arrays” de especial importancia; las cadenas de caracteres.
Una cadena de caracteres es un array de caracteres que acaba con el carácter nulo. En C siempre las cadenas de caracteres acaban con este carácter. Esto se hace así por dos motivos: el tamaño de la cadena no tiene un límite prefijado: puede ser tan grande como lo permita la memoria. Las operaciones de manipulación de cadenas de caracteres se simplifican bastante. Para inicializar una cadena de caracteres basta crear un array de caracteres, en el que no necesitamos definir el tamaño e inicializarlo con la cadena de caracteres entrecomillada. Observar que el compilador siempre añade un carácter nulo al final, por lo que el tamaño del “array” es una unidad mayor del aparente. Por ejemplo:
char cadena[] = «hola, holita» Anexo I 6-22
Lenguaje C
Los caracteres especiales como el tabulador \t y el retorno de carro \r se almacenan como un único carácter. El carácter nulo está representado por un 0.
7.4.
Acceso a los miembros de un array
Para usar un elemento de un array se utiliza el identificador y el número de orden del elemento. Al primer elemento siempre le corresponde el número 0. Así
printf («%d», vector[0]) imprimiría el contenido del primer elemento del “array” que definimos antes, que lo habíamos inicializado a 0. En el lenguaje C no se hace ningún control acerca de si intentamos leer un número de elemento mayor que el último número del array. Esto es lo que llama sobrepasar el límite, y el compilador deja al programador la tarea de preocuparse por los límites del array. Si los sobrepasamos, pueden ocurrir resultados imprevisibles.
7.5.
Arrays indeterminados
El tamaño de los “arrays” es siempre constante y se especifica al crear el “array”. Hay dos formas de especificar el tipo índice: dándoselo explícitamente al compilador o haciéndolo implícitamente. Para dar un tamaño al “array” simplemente indicamos el número de elementos entre los corchetes. El otro modo consiste en hacer que sea el compilador el que decida el tamaño. Esto se hace cuando en la creación del “array” le damos una lista de valores iniciales. En este caso, si omitimos el tamaño del “array”, el compilador ajusta el tamaño del “array” según el número de elementos que le demos para inicializar el array. Por ejemplo:
int vector[] = { 1, 2, 3, 4, 5, 6 }; Ejemplo de programa:
#include int main(){ int datos[10][10],i,j; //se carga la matriz con la suma de filas y columnas, se visualiza for(i=0;i<10;i++) { for(j=0;j<10;j++) { datos[i][j]=i+j; printf(«%d\t»,datos[i][j]); } printf(«\n»); } return(0); } Anexo I 6-23
Desarrollo de Sistemas
8.
Punteros
Un puntero es una variable que contiene como valor una dirección de memoria de otra variable. Un puntero es un nuevo tipo de datos, que no contiene un dato en sí, sino que contiene la dirección donde podemos encontrar el dato. Decimos que un puntero «apunta» a un dato, pudiendo alterar dicho dato a través del puntero.
8.1.
Definición de un puntero
Para poder usar punteros y direcciones de datos vamos a introducir dos nuevos operadores. El primero es el operador puntero(“*”), que nos permite definir las variables como punteros y también acceder a los datos. El otro nuevo operador, el operador dirección (”&”), que nos permite obtener la dirección en la que se haya ubicada una variable en la memoria. El operador dirección es el complementario al operador puntero. Todo puntero tiene asociado un tipo de datos que es conveniente que coincida con el tipo de dato de la variable a la cual va a apuntar. Un puntero se define igual que una variable normal, salvo que delante del identificador colocaremos un asterisco. Ejemplo: char *c; /* puntero a entero */ Normalmente, al definir un puntero, lo solemos inicializar para que apunte a algún dato. Disponemos de tres formas de inicializar un puntero: —
inicializarlo con la dirección de una variable que ya existe en memoria. Ejemplo: char *x = &c;
—
asignarle el contenido de otro puntero que ya esta‚ inicializado: char *x = &c; char *y = x;
—
inicializarlo con cualquier expresión constante que devuelva un valor.
Siempre se debe inicializar el puntero antes de usarlo. Una vez que el puntero apunta a un objeto o dato en la memoria podemos emplear el puntero para acceder al dato. A este proceso se la llama desreferenciar el puntero, debido a que es una operación inversa a obtener la dirección de una variable. Para desreferenciar un puntero se utiliza el operador puntero. Para acceder al dato al que apunta el puntero basta colocar el asterisco * delante del identificador. Como norma de buena escritura no se deja ningún espacio entre el * y el identificador, aunque el compilador lo acepte. Un puntero desreferenciado se comporta como una variable normal. Por ejemplo:
int x = 100; int *punt = &entero; printf(«%d %d \n», *punt, x); Un uso habitual de los punteros es para recorrer los arrays. Para ello, basta con que el puntero apunte al primer elemento del array y luego, usando la aritmética de los punteros, podemos a acceder a todos los elementos del array a través del puntero. Ejemplo:
int x[] ={ 0,1, 2, 3, 4, 5}; int *punt = x;
Anexo I 6-24
Lenguaje C
En este momento el puntero apunta al primer miembro del array (no se usa el & para indicar el array). Ejemplo: printf(«%d\n», *(punt+2)); // imprimiría 2
8.2.
Arrays y punteros Un nombre de “array” sin índice es un puntero al primer elemento del array.
Ejemplo: //Estas sentencias son idénticas: char p[10]; - p - &p[0]; int *p, i[10]; p=i; // ambas sentencias ponen el valor 100 en el sexto elemento de i. i[5]=100; *(p+5)=100; . int a[10][10]; a=&a[0][0]; a[0][4]=*((*a)+4); char cad[80], *p1; p1 = cad cad[4] o *(p1+4) // Para acceder al quinto elemento de cada.
8.3.
Indirección múltiple punteros a punteros
Consiste en que un puntero contiene la dirección de otro puntero que a su vez apunta a una variable. Ejemplo: int x; int *y=&x; int **z=&y; Ejemplo:
main() { int x, *p, **q; x=10; p=&x; q=&p; Anexo I 6-25
Desarrollo de Sistemas
printf(«%d»,**q); /* imprime el valor de x */ return 0; } Cuando trabajamos con cadenas de caracteres podemos recurrir a la inialización del puntero a la cadena directamente. Ejemplo: char *cad=”hola, holita”;
8.4.
Arrays de punteros Se pueden crear “arrays” formados por punteros. Ejemplo: “Array” de punteros a enteros: int *x [10];
Para asignar la dirección de una variable entera llamada var al tercer elemento del “array” de punteros, se escribe: x[2]=&var; Para encontrar el valor de var: *x[2]
9.
Cadenas de caracteres
Como habíamos definido, una cadena de caracteres es un array de caracteres cuyo ultimo carácter es el carácter nulo ‘\0’. Para definir una cadena de caracteres basta definir un array de caracteres del tamaño conveniente, dejando espacio para el carácter nulo. Por ejemplo: char cad[] = «Hola»; La mayoría de las funciones de cadenas de la librería estándar comienzan con el prefijo str y se hayan definidas en el fichero de cabecera . Las funciones más importantes de esta librería son: •
strlen(cad). Devuelve el tamaño de una cadena de caracteres, sin incluir el carácter nulo de terminación.
•
strcpy(s1,s2). Copia la cadena s2 en la cadena s1, incluyendo el carácter de terminación y devuelve un puntero a s1. Los dos parámetros que necesita son punteros a caracteres, y devuelve un puntero a caracteres.
•
strcat(s1, s2). Copia la cadena s2 al final de la cadena s1. Para ello busca el carácter de terminación de s1 y a partir de allí va colocando sucesivamente los caracteres de s2, incluyendo el carácter de terminación.
•
strchr(cad, c). Busca el carácter c a lo largo de la cadena cad. Si lo encuentra devuelve un puntero a la primera posición del carácter. Si
Anexo I 6-26
Lenguaje C
falla la búsqueda devuelve un puntero nulo. La función tiene dos par metros, el puntero a la cadena en la que buscar el carácter y el carácter a buscar. •
strcmp(s1, s2). Devuelve 0 si s1 y s2 son iguales, menor que 0 si s1s2.
Aquí tenemos las declaraciones de las funciones anteriores:
10. Estructuras, uniones, enumeraciones y typedef 10.1. Estructuras Una estructura es un tipo de datos compuesto por un grupo de datos, cada uno de los cuales puede ser de un tipo distinto. A cada componente de la estructura se le llama campo. A) Definición de una estructura Para la definición de estructuras el C dispone de la palabra reservada “struct”. Para crear una estructura primero comenzamos por definir el tipo de estructura. Para ello se procede de manera parecida a la definición de una variable. La forma general de una definición de estructura es:
struct etiqueta { tipo nombre_variable; tipo nombre_variable; ....... ........ } variables _de_estructura; Ejemplo:
struct estru { int x; char cad[10];
Anexo I 6-27
Desarrollo de Sistemas
float sueldo ; double real; double imaginario; }; Una vez que hemos definido un tipo de estructura ya podemos definir variables estructuras de dicho tipo. Esto se hace de una forma análoga a la definición de variables normales, esto es, se pone la palabra reservada struct, el identificador del tipo de estructura y el identificador de la nueva estructura. Por ejemplo:
struct estru xxx; También se puede definir una variable estructura a la vez que se define el tipo de estructura. Ejemplo:
struct estru{ double x ; double y; } z1, z2; Podemos definir variables estructuras sin tipo específico. Ejemplo:
struct { int x; char y[14]; int z; } zorro; A los elementos individuales de la estructura se hace referencia utilizando . (punto). Forma general es: nombre_estructura.elemento Ejemplo:
zorro.z = 12345; B) Arrays de estructuras Se define primero la estructura y luego se declara una variable “array” de dicho tipo. Ejemplo:
struct dir info_dir [100]; Para acceder a una determinada estructura se indexa el nombre de la estructura:
info_dir [2].codigo = 12345; Anexo I 6-28
Lenguaje C
C) Punteros a estructuras Declaración: struct dir * pruntero_dir; Existen dos usos principales de los punteros a estructuras: •
Para pasar la dirección de una estructura a una función.
•
Para crear listas enlazadas y otras estructuras de datos dinámicas.
Para encontrar la dirección de una variable de estructura se coloca & antes del nombre de la estructura. Ejemplo:
struct bal { float balance; char nombre[80]; } persona; struct bal *p; p = &persona; //coloca la dirección de la estructura persona en el puntero No podemos usar el operador punto para acceder a un elemento de la estructura a través del puntero a la estructura. Debemos utilizar el operador flecha “->”. Ejemplo: p -> balance.
10.2. Uniones Una unión es un tipo de datos formado por un campo capaz de almacenar un solo dato pero de diferentes tipos. Dependiendo de las necesidades del programa el campo adoptar uno de los tipos admitidos para la unión. Para definir uniones el C utiliza la palabra reservada unión. La definición y el acceso al campo de la unión es análogo al de una estructura. Al definir una variable de tipo unión el compilador reserva espacio para el tipo que mayor espacio ocupe en la memoria. Siempre hay que tener en cuenta que sólo se puede tener almacenado un dato a la vez en la variable. En C es responsabilidad del programador el conocer qué tipo de dato se esta guardando en cada momento en la unión. Para definir una unión seguimos la misma sintaxis que para las estructuras. Ejemplo:
union un { int num1; float num2; } var_union; Define una unión en la que el campo puede ser de tipo entero o de tipo número con coma flotante.
Anexo I 6-29
Desarrollo de Sistemas
10.3. Enumeraciones Una enumeración es un conjunto de constantes enteras con nombre. Su sintaxis es: enum identificador {lista de constantes simbólicas} variables_enumeracion;
enum enum { x, y , z=10, t} var_enum;
Ejemplo:
en la enumeración anterior cuando var_enum tome el valor x valdra 0, cuando tome el valor y valdrá 1, cuando tome el valor z valdrá 10 y cuando tome el valor t valdrá 11.
10.4. Typedef Sirve para definir nuevos nombres para los tipos de datos. Ejemplo:
typedef float real; real x; //estaríamos declarando la x como tipo float
11. Funciones Una función es un módulo o parte de una aplicación informática que forma un bloque o unidad de código. Es el lugar en donde se produce la actividad del lenguaje C. Gracias al empleo de funciones se facilita el mantenimiento y el uso futuro de ellas. El lenguaje C contiene muchas funciones ya predefinidas en bibliotecas (librerías) tanto del sistema como también el programador puede insertar las suyas. Por tanto, una función es una rutina o conjunto de sentencias que realiza una determinada labor. Las funciones admiten argumentos, que son datos que le pasan a la función las sentencias que la llaman. • Definición de una función La sintaxis base de toda funcion es:
tipo_devuelto nombre_funcion (lista de parámetros) { ............ /* bloque de código */ ............ } —
tipo_devuelto especifica el tipo de valor que devuelve la sentencia return de la función.
—
identificador es el nombre de la función. Debe ser un identificador válido.
—
lista_de_argumentos es una lista de variables, separadas por comas, que conforman los datos que le pasamos a la función.
Anexo I 6-30
Lenguaje C
La lista de argumentos es opcional. Un ejemplo es la función main(), que en principio no tiene argumentos. Cuando el programa al ejecutarse alcanza la llave de cierre ‘}’ de la función, ésta finaliza y devuelve el control al punto del programa que la llamó. Para obligar a la función a retornar un determinado valor se utiliza la sentencia return, seguida del valor a retornar. Los tipos de datos escalares son los punteros, tipos numéricos y el tipo carácter. En C no se pueden devolver arrays ni estructuras. • Paso de parámetros a una función Utilizando la lista de argumentos podemos pasar parámetros a una función. En la lista de parámetros se suele colocar un conjunto de identificadores, separados por comas, que representan cada uno de ellos a uno de los parámetros de la función. El orden de los parámetros es importante. Para llamar a la función habrá que colocar los parámetros en el orden en que la función los espera. Cada parámetro puede tener un tipo diferente. Para declarar el tipo de los parámetros añadiremos entre el paréntesis ‘)’ y la llave ‘{‘ una lista de declaraciones, similar a una lista de declaraciones de variables. Ejemplo:
Archivo1.c: #include void imp_may(char *cadena) { int i; for(i=0;cadena[i];i++) cadena[i]=toupper(cadena[i]); } Archivo2.c: #include #include void imp_may(char *cadena); int main() { char x[80]; printf(«introduce una cadena de caracteres\n»); gets(x); imp_may(x);//llamada a la f por referencia printf(«\n\nla cadena nueva es\n»); printf(«%s\n\n»,x); return(0); } Anexo I 6-31
Desarrollo de Sistemas
• Paso de parámetros por valor y por referencia En los lenguajes de programación estructurada hay dos formas de pasar variables a una función: por referencia o por valor. Cuando la variable se pasa por referencia función puede acceder a la variable original. En C todos los parámetros se pasan por valor. La función recibe una copia de los parámetros y variables, y no puede acceder a las variables originales. Cualquier modificación que efectuemos sobre un parámetro no se refleja en la variable original. En determinadas ocasiones necesitaremos alterar el valor de la variable que le pasamos a una función. Para ello en el C se emplea el mecanismo de los punteros. • Llamada por valor Copia el valor de un argumento en el parámetro formal de la subrutina. Los cambios en los parámetros de la subrutina no afectan a las variables usadas en la llamada. int cuad (int x); main ( ) { int t=10; printf («%d %d»,cuad(t),t); return 0; }
cuad (int x) { x=x*x; return(x); }
Salida es “100 10” • Llamada por referencia Es posible causar una llamada por referencia pasando un puntero al argumento. Se pasa la dirección del argumento a la función, por tanto es posible cambiar el valor del argumento exterior de la función.
int x,y; inter (&x,&y);
inter (int *x,int *y) { int temp; temp=*x; *x=*y; *y=temp; }
• Declaración y comprobación de tipos (Prototipo) Al igual que para las variables, cuando una función se va a usar en un programa antes del lugar donde se define, o cuando una función se define en otro fichero (funciones externas), la función se debe declarar. Anexo I 6-32
Lenguaje C
La declaración de una función consiste en especificar el tipo de datos que va a retornar la función, el identificador de la función y el número de argumentos y su tipo. Una declaración típica de función es: tipo_devuelto identificador( lista_de_argumentos_con_tipo ); Esto avisa al compilador de que la función ya existe, o que la vamos a definir después. Ejemplo:
void hola(int *x);
• Funciones recursivas Una función se dice recursiva cuando se llama a sí misma. Ejemplo:
int fact (int x)
{
int factorial; if(x==1) return 1; factorial=x* fact(x-1); return factorial; }
12. Memoria dinámica •
malloc (n) reserva una porción de memoria libre de n “bytes” y devuelve un puntero sobre el comienzo de dicho espacio.
•
free (p) libera la memoria apuntada con el puntero p.
Ambas funciones utilizan el archivo de cabecera “stdlib.h” Si no hay suficiente memoria libre para satisfacer la petición, “malloc ( )” devuelve un nulo. Ejemplo:
char *p; p=malloc(1000000);
13. Entrada y Salida por consola (estándar) Un programa en C se comunica con el usuario y con el sistema a través de las funciones de entrada y salida. Con estas funciones se pueden solicitar y enviar datos al terminal del usuario y a otros programas. Además, podemos elegir entre enviar datos binarios o enviarlos como cadenas de texto. Las funAnexo I 6-33
Desarrollo de Sistemas
ciones de entrada y salida en C más habituales son las que forman parte de la llamada «librería estándar».
13.1. Entrada y salida de caracteres En la librería estándar se definen las dos principales vías de comunicación de un programa en C: la entrada estándar y la salida estándar. Generalmente ambas están asociadas a nuestro terminal de manera que cuando se imprimen datos en la salida estándar los caracteres aparecen en el terminal, y cuando leemos caracteres de la entrada estándar los leemos del teclado del terminal. La entrada y salida estándar trabaja con caracteres (en modo carácter), con datos o números binarios. Es decir, todos los datos que enviemos a la salida estándar deben ser cadenas de caracteres. Por ello, para imprimir cualquier dato en la salida estándar primero deberemos convertirlo en texto, es decir, en cadenas de caracteres. Sin embargo esto lo haremos mediante las funciones de librería, que se encargan de realizar esta tarea eficientemente. Comenzaremos con las dos funciones principales de salida de caracteres: putchar() y getchar(). La función putchar escribe un único carácter en la salida estándar. Su uso es sencillo y generalmente está implementada como una macro en la cabecera de la librería estándar. La función “getchar()” devuelve el carácter que se halle en la entrada estándar. Esta función tiene dos particularidades. La primera es que aunque se utiliza para obtener caracteres no devuelve un carácter, sino un entero. Esto se hace así ya que con un entero podemos representar tanto el conjunto de caracteres que cabe en el tipo carácter (normalmente el conjunto ASCII de caracteres) como el carácter EOF de fin de fichero. En UNIX es habitual representar los caracteres usando el código ASCII, tanto en su versión de 7 bits como en su versión ampliada a 8 bits. Estos caracteres se suelen representar como un entero que va del 0 al 127 o 256. El carácter EOF entonces es representado con un -1. Además, esto también lo aplicaremos cuando leamos los ficheros binarios “byte” a “byte”. Una tercera función de caracteres que no es muy frecuente es la función ungetchar(). Con ella devolvemos al sistema el ultimo carácter que hemos leído con “getchar()”. No se puede llamar dos veces seguidas a “ungetchar”. El porqué‚ queda más claro al explicar el uso de “ungetchar”. Habitualmente, cuando leemos un conjunto de caracteres de la entrada estándar le pediremos que sean de un determinado tipo. Si, por ejemplo, queremos leer un dato numérico bastará con hacer un bucle que lea números (caracteres numéricos). El bucle normalmente terminará cuando el carácter leído no sea un número. La mejor forma de saber si el siguiente carácter es un número es leerlo. Pero al leerlo, si no es un número ya no estará disponible para futuras lecturas. Aquí es donde se usa “ungetchar()”. Una vez que hemos comprobado que no es un número lo devolvemos, y así estará listo para la siguiente lectura. Visto esto podemos seguir con las funciones gets() y puts(). La función “puts()” simplemente se imprime una cadena de caracteres en la salida estándar. Le debemos proporcionar la dirección donde encontrar la cadena de caracteres. Como ejemplo vamos a dar una implementación sencilla de esta función:
Anexo I 6-34
Lenguaje C
void putchar(char *p) { while (*p) putchar(*p++); } realmente la función “puts” es más complicada, pues devuelve un EOF si ha ocurrido algún error. Para imprimir datos de un modo más general el C dispone de la función printf(), que se ocupa de la impresión formateada en la salida estándar. La función “printf()” imprime los datos en la salida estándar según una cadena de control. Está definida en la cabecera estándar stdio.h como: int printf(const char *formato, ...); La función “printf()” tiene varias características peculiares. La primera es que es una función común número variable de argumentos. Normalmente a estas funciones se las llama variadic, y se reconocen porque incluyen en su línea de argumentos el símbolo de elipsis (tres puntos ...). Sólo el primer parámetro es obligatorio, y es del tipo puntero constante a carácter. Esta cadena tiene dos funciones: imprimir un mensaje en la salida estándar y formatear los demás argumentos que se la pasan a la función para ser impresos como texto. • Funcionamiento de la función printf() Si llamamos a la función “printf()” simplemente con una cadena de caracteres la función fprintf la imprime de modo parecido a como lo hace la función “puts()”. El prototipo de “printf ( )” es:
int printf (const char *cad_fmt, ...); Por ejemplo:
printf(«Hola, holita\n»); Imprime la cadena «Hola, holita\n» en la salida estándar. Pero, además, la función printf es capaz de imprimir otros tipos de datos como variables numéricas en la salida estándar. Para ello debemos avisar a la función “printf()” de que le pasamos como argumento una variable, ya que la función no tiene modo alguno de saber si le hemos pasado algún parámetro. El modo de hacerlo es insertando códigos de control en la cadena de formato. Estos códigos normalmente van precedidos del carácter %. Por ejemplo, el código %d representa enteros en formato decimal. Así, la forma de imprimir una variable entera en la salida estándar es:
printf(«esto es un entero: %d\n», 10); Anexo I 6-35
Desarrollo de Sistemas
Cuando “printf()” se encuentra el código %d en la cadena de formato lee el siguiente argumento de la función, que debe ser un entero, y lo convierte en su representación decimal como cadena de caracteres. La cadena que representa al número sustituye al código %d de la cadena de formato y se imprime la cadena resultante. Hay una gran variedad de códigos de control para formatear los diferentes tipos de datos. Los más importantes son: La cadena de formato consiste en dos tipos de elementos: caracteres que se mostrarán en pantalla y órdenes de formato que empiezan con un signo de porcentaje y va seguido por el código del formato. •
%c un único carácter.
•
%d decimal.
•
%i decimal.
•
%e notación científica.
•
%f decimal en coma flotante.
•
%o octal.
•
%s cadena de caracteres.
•
%u decimales sin signo.
•
%x hexadecimales.
•
%% imprime un signo %.
•
%p muestra un puntero.
Las órdenes de formato pueden tener modificadores que especifiquen la longitud del campo, número de decimales y el ajuste a la izquierda. Un entero situado entre % y el código de formato actúa como un especificador de longitud mínima de campo. Si se quiere rellenar con ceros, se pone un 0 antes del especificador de longitud de campo. —
%05 rellena con ceros un número con menos de 5 dígitos.
—
%10.4f imprime un número de al menos diez caracteres con cuatro decimales.
Si se aplica a cadenas o enteros el número que sigue al punto especifica la longitud máxima del campo. —
%5.7s imprime una cadena de al menos cinco caracteres y no más de siete.
Entre el código de alineación y el código de control podemos insertar un valor de anchura de campo que controla el ancho de la conversión. Por ejemplo:
printf(«:%3d:», 4); /* imprime : 3: */ Anexo I 6-36
Lenguaje C
También podemos especificar un valor que controle el número de dígitos decimales en un valor real. Este valor se coloca tras la anchura de campo precedido de un punto. Por ejemplo:
printf(«%.3f», 3.99999); /* imprime 3.999 */ Para cadenas de caracteres también podemos insertar un valor que permite escoger cuantos caracteres se imprimen de la cadena. Para ello daremos este valor tras un punto, al igual que hacemos para el valor de precisión. Por ejemplo:
13.2. La función scanf() La función “scanf()” hace el trabajo inverso a la función “printf()”, es decir, examina la entrada estándar y carga valores en variables. Se define como: int scanf(const char *formato, ...); Esta función trabaja de un modo parecido a como lo hace “printf()”. Necesita una cadena que indica el formato de los datos que se deben leer. La cadena de formato no se imprime, sino que sólo sirve para que “scanf()” determine el tipo de datos a leer. El resto de los argumentos deben ser punteros a las variables donde se deben almacenar los datos leídos. Por ejemplo: scanf(«%d», &i); Lee un entero en formato decimal y lo almacena en la variable i. Hay que tener cuidado de pasar siempre punteros a “scanf()”, por lo que para guardar datos en variables normales deberemos emplear el operador dirección &. Los códigos de control son análogos a los de printf, es decir, %d., %e, %s, ... La función “scanf()” es bastante sensible a los errores. Si el usuario introduce los datos incorrectamente, la función “scanf()” simplemente falla. Si queremos realizar una función de lectura más robusta podemos realizar lo siguiente: —
Leemos la entrada en un array de caracteres. Para ello, simplemente usaremos la función “gets()”.
—
Exploramos el array de caracteres manualmente paso a paso. Para ello, podemos usar la función “sscanf()”.
La función sscanf se define como: int sscanf(const char *s, const char *formato, ...); Realiza una tarea parecida a “scanf()”, pero explorando la cadena apuntada por s en vez de la entrada estándar. De este modo podemos ir exploranAnexo I 6-37
Desarrollo de Sistemas
do la cadena leída previamente con gets() paso a paso e informando al usuario del lugar donde ha cometido un error al introducir los datos.
13.3. Otras funciones para la entrada/salida estándar •
getche ( ): lee un carácter del teclado, espera hasta que se pulse una tecla y entonces devuelve su valor. El eco de la tecla pulsada aparece automáticamente en la pantalla. Requiere el archivo de cabecera “conio.h”
•
putcahr ( ): imprime un carácter en la pantalla.
Los prototipos son: int getche (void); int putchar (int c); Hay dos variaciones de getche( ):
•
—
getchar ( ): función de entrada de caracteres definida por el ANSI C. El problema es que guarda en un “buffer” la entrada hasta que se pulsa la tecla INTRO.
—
getch ( ): trabaja igual que “getche( )” excepto que no muestra en la pantalla un eco del carácter introducido.
gets ( ) y puts ( ): permiten leer y escribir cadenas de caracteres en la consola. gets ( ): lee una cadena de caracteres introducida por el teclado y la sitúa en la dirección apuntada por su argumento de tipo puntero a carácter. Su prototipo es:
char * gets (char *cad); Ejemplo:
main ( ) { char cad[12]; gets (cad); return (0); } puts ( ): escribe su argumento de tipo cadena en la pantalla seguido de un carácter de salto de línea. Su prototipo es:
char * puts (const char *cad);
Anexo I 6-38
Lenguaje C
14. Entrada y salida con ficheros 14.1. Apertura y cierre de un fichero Para abrir un fichero, primero debemos crear una variable de tipo puntero a FILE. Este puntero permitirá realizar las operaciones necesarias sobre el fichero. Este puntero deberá apuntar a una estructura de tipo FILE. Estas estructuras son creadas por el sistema operativo al abrir un fichero. Para poder inicializar nuestro puntero a fichero bastará llamar a la función fopen(). Esta función intenta abrir un fichero. Si tiene éxito creará una estructura de tipo FILE y devuelve un puntero a FILE que apunta a la estructura creada. En caso de no poder abrir el fichero devuelve en puntero nulo. La función “fopen()” se define en la cabecera estándar “stdio.h” como:
FILE *fopen( const char * filename, const char *modo); Necesita dos argumentos del tipo puntero a carácter. Cada uno de ellos debe apuntar a una cadena de caracteres. El primero indica el nombre del fichero a abrir. En UNIX y otros sistemas se puede especificar con el nombre del fichero el directorio donde se abrirá el fichero. El segundo indica el modo en el que se abrirá el fichero. Hay que tener cuidado en pasar un puntero a cadena de caracteres y no un solo carácter. Es fácil cometer la equivocación de pasar como segundo argumento un carácter ‘r’ en vez de la cadena «r». Los modos mas frecuentes de abrir un fichero son: •
«r» Abre un fichero de texto que existía previamente para lectura.
•
«w» Crea un fichero de texto para escritura si no existe el fichero con el nombre especificado, o trunca (elimina el anterior y crea uno nuevo) un fichero anterior.
•
«a» Crea un fichero de texto si no existe previamente o abre un fichero de texto que ya existía para añadir datos al final del fichero. Al abrir el fichero el puntero del fichero queda posicionado al final del fichero.
•
«rb» Funciona igual que «r» pero abre o crea el fichero en modo binario.
•
«wb» Análogo a «w» pero escribe en un fichero binario.
•
«ab» Análogo a «a» pero añade datos a un fichero binario.
•
«r+» Abre un fichero de texto ya existente para lectura y escritura.
•
«w+» Abre un fichero de texto ya existente o crea uno nuevo para lectura y escritura.
•
«a+» Abre un fichero de texto ya existente o crea un fichero nuevo para lectura y escritura. El indicador de posición del fichero queda posicionado al final del fichero.
•
«r+b» ¢ «rb+» Funciona igual que «r+» pero lee y escribe en un fichero binario.
Anexo I 6-39
Desarrollo de Sistemas
•
«w+b» ¢ «wb+» Análogo a «w+» pero en modo binario.
•
«a+b» ¢ «ab+» Análogo a «a+» pero en modo binario.
Una llamada típica a la función fopen() es la siguiente:
FILE *fp; if (( fp = fopen( «datos», « r»)) = = NULL) perror( «No se puede abrir el fichero\n»); Para cerrar un fichero basta llamar a la función fclose que se define en stdio.h como:
int fclose(FILE *fichero); Su argumento es un puntero a una estructura FILE asociada a algún fichero abierto. Esta función devuelve 0 en caso de éxito y EOF en caso de error.
14.2. Lectura y escritura sobre un fichero Para leer y escribir en un fichero en modo texto se usan funciones análogas a las de lectura y escritura de la entrada y salida estándar. La diferencia estriba en que siempre se deberá dar un puntero a FILE para indicar sobre qué fichero efectuaremos la operación, ya que se pueden tener simultáneamente abiertos varios ficheros. Las funciones que trabajan con ficheros tienen nombres parecidos a las funciones de entrada y salida estándar, pero comienzan con la letra f. Las más habituales son: • int fprintf (FILE *fichero, const char *formato, ... );
/* trabaja igual que printf() sobre el fichero */
• int fscanf (FILE *fichero, const char *formato, ... );
/* trabaja igual que scanf() sobre el fichero */
• int fputs (const char *s, FILE *fichero );
/* escribe la cadena s en el fichero */
• int fputc (int c, FILE *fichero);
/* escribe el carácter c en el fichero */
• int fgetc (FILE *fichero);
/* lee un carácter del fichero */
• char *fgets( char *s, int n, FILE * fichero);
/* lee una línea del fichero */
Hay una equivalencia entre las funciones de lectura y escritura estándar y las funciones de lectura y escritura de ficheros. Normalmente las funciones de lectura y escritura estándar se definen en la cabecera estándar como macros. Así la línea:
printf(«hola\n»); es equivalente a la escritura en el fichero stdout:
fprintf(stdout, «hola\n»); A los ficheros “stdin” y “stdout” normalmente se accede con las funciones de lectura y escritura estándar. Estos ficheros son automáticamente abiertos y cerrados por el sistema. Para escribir en la salida de error estándar se deben usar las funciones de ficheros con el fichero “stderr”. Normalmente en UNIX se rediAnexo I 6-40
Lenguaje C
rige la salida de error estándar a la impresora. Esta salida de error es muy útil en los procesos por lotes y cuando se usan filtros. Un filtro es simplemente un programa que lee datos de la entrada estándar, los procesa y los envía a la salida estándar. Por ello, es conveniente que no se mezclen los mensajes de error con el resultado del proceso. Un ejemplo de filtro sería un programa que expande los caracteres de tabulación en espacios en blanco. Si el programa se llama convierte y se quiere procesar el fichero mifichero, se deberá escribir la línea:
cat mifichero | convierte > nuevofichero Se han usado los mecanismos del UNIX de redirección (> envía la salida estándar de un programa a un fichero), de tubería (| conecta la salida estándar de un programa con la entrada estándar de otro) y la utilidad cat, que envía un fichero a la salida estándar.
14.3. Lectura y escritura de datos binarios Para leer y escribir grupos de datos binarios, como por ejemplo “arrays” y estructuras, la librería estándar provee dos funciones: fread() y fwrite(). Se declaran en stdio.h como:
size_t fread(void *p, size_t longitud, size_t numelem, FILE *fichero); size_t fwrite(void *p, size_t longitud, size_t numelem, FILE *fichero); La función “fread()” lee del fichero pasado como último argumento un conjunto de datos y lo almacena en el array apuntado por p. Se debe especificar en longitud la longitud del tipo de datos a leer y en numelem el número de datos a leer. La función fwrite() se comporta igual que “fread()” pero escribe los datos desde la posición apuntada por p en el fichero dado. Como siempre, para usar estas funciones, se debe abrir el fichero y cerrarlo después de usarlas. Por ejemplo, para leer un array de 100 enteros:
int array[100]; FILE *fp; fp = fopen(«mifichero», «rb»); fread(array, sizeof(int), 100, fp); fclose(fp); Estas funciones devuelven el número de elementos leídos. Para comprobar si ha ocurrido un error en la lectura o escritura se usará la función ferror (FILE *fichero), que simplemente devuelve un valor distinto de 0 si ha ocurrido un error al leer o escribir el fichero pasado como argumento. Al escribir datos binarios en un fichero se deben tener en cuenta consideraciones de portabilidad. Esto es debido a que el orden en que se almacenan los bytes que componen cada tipo de datos en la memoria puede variar de unos sistemas a otros, y las funciones “fread()” y “fwrite()” los leen y escriben según estén en la memoria. Anexo I 6-41
Desarrollo de Sistemas
14.4. Operaciones especiales con los ficheros Para comprobar si se ha alcanzado el fin de fichero, por ejemplo cuando se lee un fichero binario con “fread()”, se puede emplear la función feof(), que se define en stdio.h como:
int feof (FILE *fichero); Esta función devuelve un 0 si no se ha alcanzado el fin de fichero y un valor distinto de 0 si se alcanzó el fin de fichero. Para comprobar si ha ocurrido un error en la lectura o escritura de datos en un fichero disponemos de la función ferror, que se declara en stdio.h como:
int ferror (FILE *fichero); Esta función devuelve un valor distinto de 0 si ha ocurrido algún error en las operaciones con el fichero y un 0 en caso contrario. Estas dos funciones trabajan leyendo los indicadores de fin de fichero y error de la estructura FILE asociada a cada fichero. Podemos limpiar ambos indicadores utilizando la función clearerr(), que se define en stdio.h como:
void clearerr (FILE *fichero);
14.5. Posicionamiento del indicador de posición del fichero Cuando se manejan ficheros de acceso aleatorio se necesita poder colocar el indicador de posición del fichero en algún punto determinado del fichero. Para mover el puntero del fichero la librería estándar proporciona la función “fseek()”, que se define en stdio.h como:
int fseek (FILE *fichero, long desplazamiento, int modo); La función devuelve un 0 si ha tenido éxito y un valor diferente en caso de error. El argumento desplazamiento señala el número de caracteres que hay que desplazar el indicador de posición. Puede ser positivo o negativo, o incluso 0, ya que hay tres modos diferentes de desplazar el indicador de posición. Estos modos se indican con el argumento modo. En stdio.h se definen tres macros que dan los posibles modos. La macro SEEK_SET desplaza al indicador de posición desde el comienzo del fichero. La macro SEK_CUR desplaza el indicador de posición desde la posición actual y, la macro SEEK_END desplaza al indicador de posición desde el final del fichero. Para este último modo se debe usar un valor de desplazamiento igual o menor que 0. Para ver en qué posición se halla el puntero del fichero se puede usar la función “ftell()”, que se define en stdio.h como:
long ftell (FILE *fichero);
Anexo I 6-42
Lenguaje C
Para un fichero binario “ftell()” devuelve el número de bytes que está desplazado el indicador de posición del fichero desde el comienzo del fichero. Además, para llevar el indicador de posición al comienzo del fichero, tenemos la función rewind(), que se define en stdio.h como:
void rewind( FILE * fichero); Esta función simplemente llama a “fseek” (fichero, 0L, SEEK_SET) y luego limpia el indicador de error.
14.6. Otras operaciones con ficheros La librería estándar proporciona algunas funciones adicionales para manejar ficheros. Por ejemplo, la función remove(), que se define en stdio.h como:
int remove(const char *nombrefichero); Esta función elimina el fichero de nombre nombrefichero. Conviene cerrar el fichero antes de eliminarlo. También disponemos de una función para renombrar el fichero, la función rename(), definida en stdio.h como:
int rename(const char *antiguo, const char *nuevo); intenta renombrar al fichero de nombre antiguo. Si tiene éxito devuelve un 0. Hay que asegurarse antes de que no existía un fichero de nombre nuevo. Otra función para abrir ficheros es freopen(), que se define en stdio.h como:
FILE *freopen( const char *nombre, const char *modo, FILE *fichero); Esta función cierra el fichero pasado como tercer argumento y lo abre con el nuevo nombre y modo especificado. Devuelve un puntero a FILE que apunta al nuevo fichero abierto, o un puntero nulo en caso de error, tal y como lo hace “fopen()”.
15. El preprocesador El preprocesador en el lenguaje C es una herramienta que convierte el programa fuente desarrollado con extensión *.c en un fichero compilable con extensión *.l siendo éste el primer paso que se efectúa en la fase de compilación de un programa C con el fin de obtener el programa objeto con extensión *.obj. El preprocesador utiliza sentencias llamadas directivas de compilación que son órdenes dirigidas al compilador. Estas sentencias utilizan el símbolo “#” como carácter específico antes de la sentencia. Es una parte de la compilación en la que se hacen algunas tareas sencillas. Las fundamentales son: •
Supresión de comentarios.
•
Expansión de macros. Anexo I 6-43
Desarrollo de Sistemas
•
Inclusión del código de las cabeceras.
•
Conversión de las secuencias de escape en caracteres dentro de cadenas de caracteres y de constantes de tipo carácter.
15.1. Directivas de preprocesado Para realizar las diferentes acciones que admite el preprocesado se dispone de una serie de directivas de preprocesado, que son como comandos que instruyen al Preprocesador para realizar las expansiones. Todas las directivas del Preprocesador comienzan con el carácter # seguida del nombre de comando. El signo # debe estar al comienzo de una línea, para que el Preprocesador lo pueda reconocer. • Directiva “include” (inclusión de ficheros) Una de esas directivas es #include. Esta directiva debe ir seguida de un nombre de fichero. El nombre debe ir entrecomillado o encerrado entre signos de mayor y menor. Lo que hace el preprocesador es sustituir la línea donde se halla la directiva por el fichero indicado. Por ejemplo:
#include #include «stdio.h» La diferencia entre encerrar el nombre del fichero entre comillas o entre signos de mayor y menor es que al buscar el fichero con las comillas la búsqueda se hace desde el directorio actual, mientras que entre signos de mayor y menor la búsqueda se hace en un directorio especial. Este directorio varía con la implementación, pero suele estar situado en el directorio del compilador. El Preprocesador y el compilador ya conocen dónde se ubica el directorio. Todas las cabeceras estándar se hallan en ese directorio. Se puede incluir cualquier tipo de fichero fuente, pero lo habitual es incluir solo ficheros de cabecera. Hay que tener en cuenta que el fichero incluido es preprocesado. Esto permite expandir algunos tipos de macros y ajustar la cabecera al sistema mediante las directivas de preprocesado. Para ello se suelen usar macros que actúan como banderas. • Directiva “define” (definición de macros) En C una macro es un identificador que el preprocesador sustituye por un conjunto de caracteres. Para definir una macro se dispone de la directiva #define. Su sintaxis es: #define identificador conjunto de caracteres
Anexo I 6-44
Lenguaje C
Se utiliza habitualmente en los ficheros de cabecera para definir valores y constantes. Por ejemplo: #define x 1 • Directivas condicionales Las directivas de compilación condicional son: #if, #endif, #else, #elif, #ifdef y #ifndef. • Otras directivas Existen otras directivas como son: #undef, #line, #pragma y #error.
Anexo I 6-45
Anexo II Lenguaje C++
Desarrollo de Sistemas
Guión-resumen
1. Introducción
9. Funciones
2. La función main ()
10. Programación eficiente
3. Tipos básicos de datos
11. El preprocesador
4. Variables
12. Entrada / Salida de datos
5. Operadores, caracteres especiales, instrucciones, arrays, cadenas de caracteres, punteros, estructuras, uniones, enumeraciones y typedef
13. Programación Orientada a Objetos
6. Tipos compuestos
16. Templates
7. Asignación dinámica de memoria
17. Manejo de excepciones
8. Sentencias de control en C++
Anexo II 6-2
14. Herencia en C++ 15. Sobrecarga en C++
Lenguaje C++
1.
Introducción
El lenguaje de programación C++ se comenzó a desarrollar en 1980 como una ampliación del lenguaje C; de ahí el nombre de C++ que proviene del operador incremento ++. También es conocido como C con clases. Entre las características iniciales que debemos tener en cuenta tenemos: •
Los ficheros fuente terminan con la extensión “*.cpp”
•
Los ficheros de encabezado figuran con la extensión “*.h” y se incluyen en el fichero fuente usando la directriz #include del preprocesador (como sucedía en el lenguaje C), Ejemplo:
#include “iostream.h” ó #include
Nota1. La diferencia entre usar las comillas o usar “<>” estriba en que si el nombre de la biblioteca está entre comillas se busca el archivo de la manera definida en la implementación (generalmente primero en el directorio actual de trabajo y luego como si se hubiese puesto “<>”). Y si se usa “<>” se busca según se haya establecido en la creación del compilador. Esto también es aplicable al lenguaje C. Nota2. Las líneas que empieza por # son una directiva. En este caso indica que se incluya el fichero «iostream.h», que contiene las definiciones para entrada/salida de datos en C++. •
Los identificadores válidos del C++ son los formados a partir de los caracteres del alfabeto (del inglés, no podemos usar ni la ñ ni palabras acentuadas), los dígitos “0 – 9” y el guión bajo “_”; la única restricción es que no podemos comenzar un identificador con un dígito. Hay que señalar que el C++ distingue entre mayúsculas y minúsculas, por lo que Lolo y lolo representan dos cosas diferentes. Hay que evitar el uso de identificadores que solo difieran en letras mayúsculas y minúsculas, porque inducen a error.
•
Las variables se pueden declarar en cualquier lugar del programa, siempre y cuando tengamos cuidado de usarlas una vez que hayan sido declaradas.
•
Los comentarios se pueden hacer de dos formas: —
Una sola línea (no afecta a la sentencia situada a la izquierda de las “//”):
Ejemplo: x=2+3; // este es un comentario de una línea —
Un bloque:
Ejemplo: x=2+3; /* comentario de un bloque*/
Anexo II 6-3
Desarrollo de Sistemas
Los comentarios entre /* y */ pueden tener la longitud que queramos, pero no se anidan, es decir, si escribimos /* hola /* amigos */ de TAI. Los comentarios que comienzan por // sólo son válidos hasta el final de la línea en la que aparecen. •
Los agrupadores de bloque o de funciones, igual que sucedía en C, son las llaves “{ }”.
•
Cada sentencia debe terminar en un punto y coma “;”. Es por eso que en una misma línea se pueden poner varias sentencias, separadas por el “;” o incluso podríamos poner todo el programa en una sola línea.
Ejemplo: int x=2; float y=8; float z; z=x+y; •
2.
El soporte a la programación modular en C++ se consigue mediante el empleo de algunas palabras clave y de las directivas de compilación. Lo más habitual es definir cada módulo mediante una cabecera (un archivo con la terminación .h) y un cuerpo del módulo (un archivo con la terminación .c, .cpp, o algo similar; depende del compilador). En el archivo cabecera (header) ponemos las declaraciones de funciones, tipos y variables que queremos que sean accesibles desde el exterior y en el cuerpo o código definimos las funciones publicas o visibles desde el exterior, además de declarar y definir variables, tipos o funciones internas a nuestro módulo.
La función main ()
La función main() es la función principal del programa, por donde empieza la ejecución del mismo. En caso de no recibir argumentos se declara de la siguiente forma:
void main(void) { …… …… } Puede recibir argumentos desde la consola en el momento de la ejecución del programa:
void main (int argc, char *argv[]) { …… …… } •
“argc” indica el número de argumentos que son pasados a la función “main”.
Anexo II 6-4
Lenguaje C++
•
“argv” es un “array” de caracteres a través de punteros que contiene el valor de los argumentos.
Todos los programas deben tener una función “main()” que es la que se ejecuta al comenzar el programa. Un programa será una secuencia de líneas que contendrán sentencias, directivas de compilación y comentarios. Las sentencias simples se separan por punto y coma, y las compuestas se agrupan en bloques mediante llaves. Las directivas serán instrucciones que le daremos al compilador para indicarle que realice alguna operación antes de compilar nuestro programa, las directivas comienzan con el símbolo # y no llevan punto y coma.
Nota. El mínimo programa de C++ es: main() { }
3.
Tipos básicos de datos (Ver tipos de datos en Lenguaje C.)
Los tipos elementales definidos en C++ son: char, short, int, long, que representan enteros de distintos tamaños (los caracteres son enteros de 8 bits) float, double y long double, que representan números reales (en coma flotante). Para declarar variables de un tipo determinado escribimos el nombre del tipo seguido del de la variable. Por ejemplo:
int i; double d; char c; Podemos declarar varias variables de un mismo tipo poniendo el nombre del tipo y las variables a declarar separadas por comas: int i, j,k. Además podemos inicializar una variable a un valor en el momento de su declaración: int i=100; Cada tipo definido en el lenguaje (o definido por el usuario) tiene un nombre sobre el que se pueden emplear dos operadores: sizeof, que nos indica la memoria necesaria para almacenar un objeto del tipo, y new, que reserva espacio para almacenar un valor del tipo en memoria. C++ solo define un nuevo tipo de dato al lenguaje C que es el tipo de dato “bool” que puede tomar solo dos valores: true o false. Para especificar si los valores a los que se refieren tienen o no signo, empleamos las palabras signed y unsigned delante del nombre del tipo (por ejemplo unsigned int para enteros sin signo). El tipo void es sintácticamente igual a los tipos elementales pero solo se emplea junto a los derivados y no hay objetos del tipo void. Se emplea para especificar que una función no devuelve nada o como base para punteros a objetos de tipo desconocido. Por ejemplo: void lolo (void); indica que la función lolo no tiene parámetros y no retorna nada.
Anexo II 6-5
Desarrollo de Sistemas
3.1.
Conversiones de tipos
• Conversiones implícitas Cuando trabajamos con tipos elementales podemos mezclarlos en operaciones sin realizar conversiones de tipos, ya que el compilador se encarga de aproximar al valor de mayor precisión. • Conversiones explícitas (casting)
Para indicar la conversión explícita de un tipo en otro usamos el nombre del tipo; por ejemplo, si tenemos “i” de tipo “int” y “j” de tipo “long”, podemos hacer i=(long)j (sintaxis del C, válida en C++) o i=long(j) (sintaxis del C++).
4.
Variables (Ver variables en Lenguaje C.)
4.1.
Visibilidad y duración de las variables
La visibilidad o ámbito de vida de una variable es la parte del programa en la que esa variable está definida y puede ser utilizada. Podemos clasificar las variables en locales y globales: •
Variables locales son aquellas que son declaradas dentro de un bloque y son visibles en ese bloque y en los bloques anidados dentro de él. Ocultan a las variables globales con el mismo nombre.
•
Variables globales son aquellas que son declaradas fuera de cualquier función. Son visibles durante toda la ejecución del programa y se pueden utilizar en todas las funciones del fichero que forma el programa.
La duración o tiempo de vida de una variable hace referencia al tiempo que transcurre entre la creación de una variable y el instante en que es destruida. Si son variables auto (locales), la variable se crea y se destruye cada vez que pasa por el bloque (esta es la opción por defecto). Si es una variable static, la duración de dicha variable es hasta que finalice el programa. Las variables existen sólo dentro del bloque en el que se definen, es decir, se crean cuando se entra en el bloque al que pertenecen y se destruyen al salir de él. Para acceder a variables que se definen en otros módulos la declaramos en nuestro módulo precedida de la palabra extern. Si queremos que una variable sea local a nuestro módulo la definimos static, de manera que es inaccesible desde el exterior de nuestro módulo y además permanece durante todo el tiempo que se ejecute el programa guardando su valor entre accesos al bloque. Anexo II 6-6
Lenguaje C++
Si queremos que una variable no pueda ser modificada la declaramos const; tenemos que inicializarla en su declaración y mantendrá su valor durante todo el programa. Estas variables se emplean para constantes que necesitan tener una dirección (para pasarlas por referencia). Si queremos que una variable sea comprobada cada vez que la utilicemos la declararemos precedida de la palabra volatile; esto es útil cuando definimos variables que almacenan valores que no sólo modifica nuestro programa. Podemos intentar hacer más eficientes nuestros programas indicándole al compilador qué variables usamos más a menudo para que las coloque en los registros. Esto se hace declarando la variable precedida de la palabra register. Podemos tener definida una variable local con el mismo nombre que una global; en este caso la local prevalece sobre la global. No obstante si queremos acceder a una variable global en un bloque donde exista una local del mismo nombre, utilizamos el operador “::”. Ejemplo:
int x=10; //variable global void main( ) { int x = 20; //variable local int y = ::x; // asigna a y el valor 10 int t = x; // asigna a t el valor 20 }
5.
Operadores, caracteres especiales, instrucciones, arrays, cadenas de caracteres, punteros, estructuras, uniones, enumeraciones y typedef (Ver Lenguaje C)
5.1.
Tipos derivados
De los tipos fundamentales podemos derivar otros mediante el uso de los siguientes operadores de declaración: * Puntero
& Referencia
[] Vector (Array)
() Función
Ejemplos:
int *punt; // puntero a un entero int v[20]; // vector de 20 enteros int *punt[20]; // vector de 20 punteros a entero void f(int j); // función con un parámetro entero Anexo II 6-7
Desarrollo de Sistemas
int i; // declaración de un entero i i = *n; // almacenamos en i el valor al que apunta n i = v[2] // almacenamos en i el valor de el tercer elemento de v i = *v[2] // almacenamos en i el valor al que apunta el tercer puntero de v f(i) // llamamos a la función f y le enviamos el parámetro i Para declarar un puntero a un vector necesitamos paréntesis:
int *v[20]; // vector de 20 punteros a entero int (*punt)[20] // puntero a vector de 20 enteros Al declarar variables de tipos derivados, el operador se asocia a la variable, no al nombre del tipo:
int x,y,z; // declaración de tres variables enteras int *i, j; // declaramos un puntero a entero (i) y un entero (j) int v[10], *p; // declaramos un vector de 10 enteros y un puntero a entero • Punteros Para cualquier tipo X, el puntero a ese tipo es X*. Una variable de tipo X* contendrá la dirección de un valor de tipo X. Los punteros a vectores y funciones necesitan el uso de paréntesis:
int *punt_i // Puntero a entero char **punt_c // Puntero a puntero a carácter int (*punt_v)[10] // Puntero a vector de 10 enteros int (*punt_f)(float) // Puntero a función que recibe un real y retorna un entero La operación fundamental sobre punteros es la de indirección (retornar el valor apuntado por él):
char c1 = ‘a’; // c1 contiene el carácter ‘a’ char *p = &c1; // asignamos a p la dirección de c1 (& es el operador referencia) char c2 = *p; // ahora c2 vale lo apuntado por p (‘a’) • Vectores Para un tipo X, X[n] indica un tipo vector con n elementos. Los índices del vector empiezan en 0 y llegan hasta n-1. Podemos definir vectores multidimensionales como vectores de vectores:
int v1[10]; // vector de 10 enteros int v2[20][10]; // vector de 20 vectores de 10 enteros (matriz de 20*10) Anexo II 6-8
Lenguaje C++
Accedemos a los elementos del vector a través de su índice (entre []):
v1[3] = 15; // el elemento con índice 3 vale 15 v2[8][3] = v1[3]; // el elemento 3 del vector 8 de v2 vale lo mismo que v[3] El compilador no comprueba los límites del vector, es responsabilidad del programador. Para inicializar un vector podemos enumerar sus elementos entre llaves. Los vectores de caracteres se consideran cadenas, por lo que el compilador permite inicializarlos con una constante cadena (pero les añade el carácter nulo). Si no ponemos el tamaño del vector al inicializarlo el compilador le dará el tamaño que necesite para los valores que hemos definido. Ejemplos:
int v1[5] = {0, 1, 2, 3, 4}; char v2[2][3] = {{‘t’, ‘a’, ‘i’}, {‘T’, ‘A’, ‘I’} }; // vect. multidimensional int v3[2] = {1, 2, 3, 4}; // error: sólo tenemos espacio para 2 enteros char c1[5] = {‘h’,’o’,’l’,’a’,’\0’}; // cadena «hola» char c2[5] = «holita»; // cadena holita char c3[] = «hola»; // el compilador le da tamaño 5 al vector char vs[3][] = {«hola», «aprobados», «tai»} // vector de 3 cadenas (3 punteros a carácter) • Referencias Una referencia es un nombre alternativo a un objeto; se emplea para el paso de argumentos y el retorno de funciones por referencia. X& significa referencia a tipo X. Las referencias tienen restricciones: 1.
Se deben inicializar cuando se declaran (excepto cuando son parámetros por referencia o referencias externas).
2.
Cuando se han inicializado no se pueden modificar.
3.
No se pueden crear referencias a referencias ni punteros a referencias.
Ejemplos:
int a; // variable entera int &r1 = a; // ref es sinónimo de a int &r2; // error, no está inicializada extern int &r3; // válido, la referencia es externa (estará inicializada en otro // módulo) int &&r4=r1; // error: referencia a referencia
Anexo II 6-9
Desarrollo de Sistemas
6.
Tipos compuestos Existen cuatro tipos compuestos en C++: Estructuras
Uniones
Campos de bits
Clases
• Estructuras Las estructuras son el tipo equivalente a los registros de otros lenguajes. Se definen poniendo la palabra struct delante del nombre del tipo y colocando entre llaves los tipos y nombres de sus campos. Si después de cerrar la llave ponemos una lista de variables, las declaramos a la vez que definimos la estructura. Si no, luego podemos declarar variables poniendo struct nombre_tipo (ANSI C, C++) o nombre_tipo (C++). Ejemplo:
struct persona { int edad; char nombre[50]; } empleado; struct persona alumno; // declaramos la variable alumno de tipo persona (ANSI C) persona profesor; // declaramos la variable profesor de tipo persona persona *p; // declaramos un puntero a una variable persona Podemos inicializar una estructura de la misma forma que un array:
persona lolo= {21, «Lolo Lolete»}; Para acceder a los campos de una estructura ponemos el nombre de la variable, un punto y el nombre del campo. Si trabajamos con punteros podemos poner -> en lugar de de referenciar el puntero y poner un punto (esto lo veremos en el punto de variables dinámicas):
alumno.edad = 20; // el campo edad de alumno vale 20 p->nombre = «Lolo»; // el nombre de la estructura apuntada por p vale «Lolo» (*p).nombre = «Lolo»; // igual que antes Empleando el operador sizeof a la estructura podemos saber cuántos bytes ocupa. • Uniones Las uniones son idénticas a las estructuras en su declaración (poniendo union en lugar de struct), con la particularidad de que todos sus campos comparten la misma memoria (el tamaño de la unión será el del campo con un tipo mayor). Es responsabilidad del programador saber qué está haciendo con Anexo II 6-10
Lenguaje C++
las uniones, es decir, podemos emplear el campo que queramos, pero si usamos dos campos a la vez uno machacará al otro. Ejemplo:
union codigo { int i; float f; } cod; cod.i = 10; // i vale 10 cod.f = 25e3f; // f vale 25 * 1000, i indefinida (ya no vale 10) Podemos declarar uniones o estructuras sin tipo siempre y cuando declaremos alguna variable en la definición. Si no declaramos variables la estructura sin nombre no tiene sentido, pero la unión permite que dos variables compartan memoria. Ejemplo:
struct { int i; char n[20] } reg; union { int i; float f; }; // i y f son variables, pero se almacenan en la misma memoria • Campos de bits Un campo de bits es una estructura en la que cada campo ocupa un numero determinado de bits, de forma que podemos tratar distintos bits como campos independientes, aunque estén juntos en una misma palabra de la máquina. Ejemplo:
struct fichero { :3 // nos saltamos 3 bits unsigned int lectura : 1; // reservamos un bit para lectura unsigned int escritura : 1; unsigned int ejecución : 1; :0 // pasamos a la siguiente palabra unsigned int directorio: 8; } flags; flags.lectura = 1; // ponemos a 1 el bit de lectura Los campos siempre son de tipo discreto (enteros), y no se puede tomar su dirección. Anexo II 6-11
Desarrollo de Sistemas
• Clases Las clases son estructuras con una serie de características especiales; las estudiaremos en profundidad más adelante.
7.
Asignación dinámica de memoria
Hay dos formas principales por medio de las cuales un programa puede almacenar información en la memoria del ordenador: •
Por medio del uso de variables, en cuyo caso la cantidad de memoria necesaria queda fijada en tiempo de compilación y no puede ser cambiada durante la ejecución del programa. Esto es, si se ha reservado memoria para un array de 10 elementos de tipo “int”, este array no puede cambiar durante la ejecución.
•
La segunda forma de almacenamiento de información es por medio del sistema de asignación dinámica de memoria de C++. Por medio de este método se va asignando memoria en tiempo de ejecución según se vaya necesitando.
C++ tiene 2 operadores para la asignación dinámica de memoria: new y delete. Sus formas generales son:
var_puntero = new tipo_variable; delete var_puntero; Usando new para un vector, el tamaño del vector se sitúa entre corchetes. Con delete el contenido al que apunta el puntero es borrado. Para asignar memoria dinámicamente a un vector de “n” elementos, se utiliza el siguiente formato: var_puntero = new tipo_variable[n]; delete [] var_puntero; int n=100;
o int *obj = new int[n]; //asigna delete [] obj; //libera
Ejemplo: int *obj = new int; *obj=7.2; delete obj;
Anexo II 6-12
Lenguaje C++
Hemos mencionado que en C se usaban las funciones “malloc()” y “free()” para el manejo de memoria dinámica, pero dijimos que en C++ se suelen emplear los operadores new y delete. El operador new se encarga de reservar memoria y delete de liberarla. Estos operadores se emplean con todos los tipos del C++, sobre todo con los tipos definidos por nosotros (las clases). La ventaja sobre las funciones de C de estos operadores está en que utilizan los tipos como operandos, por lo que reservan el número de bytes necesarios para cada tipo y cuando reservamos más de una posición no lo hacemos en virtud de un número de bytes, sino en función del número de elementos del tipo que deseemos. El resultado de un new es un puntero al tipo indicado como operando y el operando de un delete debe ser un puntero obtenido con new. Veamos con ejemplos cómo se usan estos operadores:
int * i = new int; // reservamos espacio para un entero, i apunta a él delete i; // liberamos el espacio reservado para i int * v = new int[10]; // reservamos espacio contiguo para 10 enteros, v apunta // al primero delete []v; // Liberamos el espacio reservado para v Hay que tener cuidado con el “delete”. Si ponemos: “delete v”; sólo liberamos la memoria ocupada por el primer elemento del vector, no la de los 10 elementos. Con el operador new también podemos inicializar la variable a la vez que reservamos la memoria:
int *i = new int (5); // reserva espacio para un entero y le asigna el valor Para asignar memoria dinámicamente a una estructura, hay que definir una variable puntero a dicha estructura y luego utilizar el operador new para la asignación. Para acceder a los elementos de dicha estructura se utiliza el operador “->” Para la asignación dinámica de memoria de arrays de estructuras, se utiliza la forma general de los arrays, es decir, hay que declarar un puntero a la estructura y luego asignar con el operador new el tipo y número de elementos. Para acceder a cada elemento del array se utiliza el índice y para acceder a cada miembro de la estructura se utiliza el operador “.”:
nombre[indice]. miembro; Ejemplo: struct empleado{ char nombre[20]; int hijos; float sueldo }; int n=10; empleado *obj2 = new empleado[n]; obj1[0].nombre=”Olga”;
Anexo II 6-13
Desarrollo de Sistemas
obj1[0].hijos=1; obj1[0].sueldo=1200; delete [] obj1; empleado *obj2 = new empleado[n]; obj2->nombre=”Yolanda”; obj2->hijos=1; obj1->sueldo=1200; delete obj2; a la hora de usar el operador -> lo único que hay que tener en cuenta es la precedencia de operadores. Ejemplo:
++p->i1; // preincremento del campo i1, es como poner ++ (p->i1) (++p)->i1; // preincremento de p, luego acceso a i1 del nuevo p. Por último diremos que la posibilidad de definir campos de una estructura como punteros a elementos de esa misma estructura es la que nos permite definir los tipos recursivos como los nodos de colas, listas, árboles, etc. • Punteros a punteros (indirección múltiple) Además de definir punteros a tipos de datos elementales o compuestos también podemos definir punteros a punteros. La forma de hacerlo es poner el tipo y luego tantos asteriscos como niveles de indirección:
int *p1;
// puntero a entero
int **p2;
// puntero a puntero a entero
char *c[];
// vector de punteros a carácter
Para usar las variables puntero a puntero hacemos lo mismo que en la declaración, es decir, poner tantos asteriscos como niveles queramos acceder:
int ***p3;
// puntero a puntero a puntero a entero
p3 = &p2;
// trabajamos a nivel de puntero a puntero a puntero a entero // no hay indirecciones, a p3 se le asigna un valor de su mismo tipo
*p3 = &p1;
// el contenido de p2 (puntero a puntero a entero) toma la dirección de // p1 (puntero a entero). Hay una indirección, accedemos a lo apuntado por p3
p1 = **p3;
Anexo II 6-14
// p1 pasa a valer lo apuntado por lo apuntado por p3 (es decir, lo apuntado por p2). En //nuestro caso, no cambia su valor, ya que p2 apuntaba a p1 desde la operación anterior
Lenguaje C++
***p3 = 5
8.
// El entero apuntado por p1 toma el valor 5 (ya que p3 apunta a p2 que apunta a p1)
Sentencias de control en C++ (Ver sentencias de control en Lenguaje C) A modo de resumen: Como estructuras de control el C++ incluye las siguientes construcciones: —
—
9.
condicionales: •
if → instrucción de selección simple.
•
switch → instrucción de selección múltiple.
bucles: •
do-while → instrucción de iteración con condición final.
•
while → instrucción de iteración con condición inicial.
•
for → instrucción de iteración especial (similar a las de repetición con contador).
—
de salto:
•
break → instrucción de ruptura de secuencia (sale del bloque de un bucle o instrucción condicional).
•
continue → instrucción de salto a la siguiente iteración (se emplea en bucles para saltar a la posición donde se comprueban las condiciones).
•
goto → instrucción de salto incondicional (salta a una etiqueta).
•
return → instrucción de retorno de un valor (se emplea en las funciones).
Funciones
Una función es una porción de código, un conjunto de sentencias, agrupadas por separado, generalmente enfocadas a realizar una tarea específica. También se suelen denominar subrutinas o subprogramas.
9.1.
Definición
La definición de una función consta de la cabecera de la función y del cuerpo. Su forma general es:
Anexo II 6-15
Desarrollo de Sistemas
tipo_valor_retorno nombre_funcion(tipo arg1, tipo arg2, ... ,tipo argn) { …… // CUERPO DE LA FUNCION …… } La lista de argumentos, también llamados argumentos formales, es una lista de declaraciones de variables, precedidas de su tipo correspondiente y separadas por comas (,). Los argumentos formales son la forma más natural y directa para que una función reciba valores desde el programa que le llama. El “tipo_valor_retorno” indica el tipo del valor devuelto al programa que le ha llamado. Si no se desea que devuelva nada, el tipo de retorno debe ser void. La sentencia return permite devolver el valor.
9.2.
Declaración
Toda función debe ser declarada antes de ser utilizada en el programa que realiza la llamada. Ésta se hace mediante el prototipo de la función. La forma general del prototipo coincide con la primera línea de la definición –el encabezamiento–, con tres pequeñas diferencias: •
En vez de la lista de argumentos formales o parámetros, basta incluir sólo los tipos de dichos argumentos.
•
El prototipo termina con un carácter “;”.
•
Los valores pueden ser inicializados si se desea.
Ejemplo: int func (int, char, float); La llamada a una función se hace incluyendo su nombre en una expresión o sentencia del programa principal o de otra función. Este nombre debe ir seguido de una lista de argumentos separados por comas y encerrados entre paréntesis. A los argumentos incluidos en la llamada se les llama argumentos actuales.
9.3.
Paso de argumentos por valor y por referencia
Por defecto los parámetros se pasan por valor, para pasarlos por referencia usaremos punteros y referencias. Sigue el mismo mecanismo visto en el lenguaje C.
9.4.
Funciones recursivas
Son funciones que pueden llamarse a sí mismas. Cuando una función es llamada por sí misma, se crea un nuevo juego de parámetros y variables locales, pero el código ejecutable es el mismo.
Anexo II 6-16
Lenguaje C++
9.5.
Funciones Inline
Las funciones inline son funciones que no son llamadas sino que son expandidas en línea, en el punto de cada llamada. Las ventajas de estas funciones es que no representan un retardo vinculado con la llamada a la función ni con los mecanismos de vuelta de ésta. Esto significa que las funciones inline son ejecutadas de forma mucho más rápida que las normales. Las desventajas de estas funciones es que si son demasiado grandes y son llamadas con demasiada frecuencia, el programa se hace más grande. Para declarar una función inline basta con anteponer el especificador inline a la definición de la función. Estas deben ser declaradas antes de ser usadas. Una función inline es igual que una función normal que no genera código de llamada a función, sino que sustituye las llamadas a la misma por el código de su definición. La principal ventaja frente a las macros es que estas funciones sí que comprueban el tipo de los parámetros. No se pueden definir funciones inline recursivas.
10. Programación eficiente Veremos en este módulo una serie de mecanismos del C++ útiles para hacer que nuestros programas sean más eficientes.
10.1. Estructura de los programas El código de los programas se almacena en ficheros, pero el papel de los ficheros no se limita al de mero almacén, también tienen un papel en el lenguaje: son un ámbito para determinadas funciones (estáticas y en línea) y variables (estáticas y constantes) siempre que se declaren en el fichero fuera de una función. Además de definir un ámbito, los ficheros nos permiten la compilación independiente de los archivos del programa, aunque para ello es necesario proporcionar declaraciones con la información necesaria para analizar el archivo de forma aislada. Una vez compilados los distintos ficheros fuente (que son los que terminan en .c, .cpp, etc.), es el “linker” el que se encarga de enlazarlos para generar un único fichero fuente ejecutable. En general, los nombres que no son locales a funciones o a clases se deben referir al mismo tipo, valor, función u objeto en cada parte de un programa. Si en un fichero queremos declarar una variable que está definida en otro fichero, podemos hacerlo declarándola en nuestro fichero precedida de la palabra extern.
Anexo II 6-17
Desarrollo de Sistemas
Si queremos que una variable o función sólo pertenezca a nuestro fichero la declaramos “static”. Si declaramos funciones o variables con los mismos nombres en distintos ficheros producimos un error (para las funciones el error sólo se produce cuando la declaración es igual, incluyendo los tipos de los parámetros). Las funciones y variables cuyo ámbito es el fichero tienen enlazado interno (es decir, el “linker” no las tiene en cuenta).
10.2. Los ficheros cabecera Una forma fácil y cómoda de que todas las declaraciones de un objeto sean consistentes es emplear los denominados ficheros cabecera, que contienen código ejecutable y/o definiciones de datos. Estas definiciones o código se corresponderán con la parte que queremos utilizar en distintos archivos. Para incluir la información de estos ficheros en nuestro fichero .C empleamos la directiva include, que le servirá al preprocesador para leer el fichero cabecera cuando compile nuestro código. Un fichero cabecera debe contener: •
Definición de tipos struct punto { int x, y; };
•
Templates template class V { … }
•
Declaración de funciones extern int strlen (const char *);
•
Definición de funciones inline inline char get { return *p++ ;}
•
Declaración de variables extern int a;
•
Definiciones constantes const float pi = 3.141593;
•
Enumeraciones enum bool { false, true };
•
Declaración de nombres class Matriz;
•
Directivas include #include
•
Definición de macros #define Case break;case
•
Comentarios /* cabecera de mi_prog.c */
Y no debe contener: •
Definición de funciones ordinarias char get () { return *p++}
•
Definición de variables int a;
•
Definición de agregados constantes const tabla[] = { … }
Anexo II 6-18
Lenguaje C++
Si nuestro programa es corto, lo más usual es crear un solo fichero cabecera que contenga los tipos que necesitan los diferentes ficheros para comunicarse y poner en estos ficheros sólo las funciones y definiciones de datos que necesiten e incluir la cabecera global. Si el programa es largo o usamos ficheros que pueden ser reutilizados, lo más lógico es crear varios ficheros cabecera e incluirlos cuando sean necesarios. Por último, indicaremos que las funciones de biblioteca suelen estar declaradas en ficheros cabecera que incluimos en nuestro programa para que luego el linker las enlace con nuestro programa. Las bibliotecas estándar son: Bibliotecas de C: assert.h
Define la macro assert()
ctype.h
Manejo de caracteres
errno.h
Tratamiento de errores
float.h
Define valores en coma flotante dependientes de la implementación
limits.h
Define los límites de los tipos dependientes de la implementación
locale.h
Define la función setlocale()
math.h
Definiciones y funciones matemáticas
setjmp.h
Permite saltos no locales
signal.h
Manejo de señales
stdarg.h
Manejo de listas de argumentos de longitud variable
stddef.h
Algunas constantes de uso común
stdio.h
Soporte de E/S
sdlib.h
Algunas declaraciones estándar
string.h
Funciones de manipulación de cadenas
time.h
Funciones de tiempo del sistema
Anexo II 6-19
Desarrollo de Sistemas
Bibliotecas de C++: fstream.h
Streams fichero
iostream.h
Soporte de E/S orientada a objetos (streams)
new.h
Definición de _new_handler
strstream.h
Definición de streams cadena
11. El preprocesador El preprocesador es un programa que se aplica a los ficheros fuente del C++ antes de compilarlos. Realiza diversas tareas, algunas de las cuales se pueden controlar mediante el uso de directivas de preprocesado. Como veremos, estas directivas permiten definir macros como las de los lenguajes ensambladores (en realidad no se trata más que de una sustitución). A continuación veremos las fases de preprocesado y las directivas, así como una serie de macros predefinidas. Por último explicaremos lo que son las secuencias trigrafo.
11.1. Fases de preprocesado 1.
Traduce los caracteres de fin de línea del fichero fuente a un formato que reconozca el compilador.
2.
Concatena cada línea terminada con la barra invertida (\) con la siguiente.
3.
Elimina los comentarios. Divide cada línea lógica en símbolos de preprocesado y espacios en blanco.
4.
Ejecuta las directivas de preprocesado y expande los macros.
5.
Reemplaza las secuencias de escape dentro de constantes de caracteres y cadenas de literales por sus caracteres individuales equivalentes.
6.
Concatena cadenas de literales adyacentes.
7.
Convierte los símbolos de preprocesado en símbolos de C++ para formar una unidad de compilación.
Estas fases se ejecutan exactamente en este orden. Directivas del preprocesador: #define ID VAL
Define la macro ID con valor VAL.
#include «fichero»
Incluye un fichero del directorio actual.
#include
Incluye un fichero del directorio por defecto.
Anexo II 6-20
Lenguaje C++
#defined id
Devuelve 1 si id está definido.
#defined (id)
Lo mismo que el anterior.
#if expr
Si la expresión se cumple se compila todo lo que sigue. Si no, se pasa hasta un #else o un #endif.
#ifdef id
Si el macro id ha sido definido con un #define, la condición se cumple y ocurre lo del caso anterior. Es equivalente a if defined id.
#ifndef id
Si el macro id no ha sido definido con un #define, la condición se cumple. es equivalente a if !defined id.
#else
Si el #if, #ifdef o #ifndef más reciente se ha cumplido todo lo que haya después del #else hasta #endif no se compila. Si no se ha cumplido si se compila.
#elif exp.
Contracción de #else if expr.
#endif
Termina una condición.
#line CONST ID
Cambia el número de línea según la constante CONST y el nombre del fichero de salida de error a ID. Modifica el valor de los macros predefinidos __LINE__ y __FILE__
#pragma OPCION
Especifica al compilador opciones específicas de la implementación.
#error CADENA
Causa la generación de un mensaje de error con la cadena dada.
Ejemplo de macros:
#define MIN(a,b) (((a) < (b)) ? (a) : (b) ) main () { int i, j=6, k=8; i = MIN(j*3, k-1); } Después del preprocesado tendremos: main () { int i, j=6, k=8; i = (((j*3) < (k-1)) ? (j*3) : (k-1)); } Si no hubiéramos puesto paréntesis en la definición de la macro, al sustituir a y b podíamos haber introducido operadores con mayor precedencia que ?: y haber obtenido un resultado erróneo al ejecutar la macro. Notar que la macro no hace ninguna comprobación en los parámetros simplemente sustituye, por lo que a veces puede producir resultados erróneos.
Anexo II 6-21
Desarrollo de Sistemas
12. Entrada / Salida de datos 12.1. Entrada / Salida a través de Consola en C++ C++ incorpora su propio archivo de cabecera, denominado “iostream.h” que implementa su propio conjunto de funciones de entrada/salida. La E/S es un flujo de C++ se describe como un conjunto de clases en “iostream.h”. Estas clases sobrecargan los operadores “put to” y “get from”, “<<” y “>>”. En C++, la clase proporciona soluciones modulares a las necesidades de manipulación de datos. La biblioteca estándar de C++ ofrece tres clases de E/S como alternativa a las funciones de E/S de propósito general de C. Estas clases contienen definiciones para el mismo par de operadores (“>>” y “<<”) que se optimizan para todos los tipos de datos. cin,cout y cerr. El equivalente para los flujos en C++ de stdin, stdout y stderr, descritos en STDIO.H, son cin, cout y cerr, que se describen en IOSTREAM.H. Estos tres flujos se abren automáticamente cuando comienza la ejecución del programa y pasan a ser la interfaz entre el programa y el usuario. El flujo “cin” se asocia con el teclado del terminal. Los flujos cout y cerr se asocian con la pantalla de visualización. Los operadores “>>” para extracción y “<<” para inserción. Las entradas y salidas en C++ han mejorado de forma significativa y se han simplificado debido a los operadores de la biblioteca de flujo “>>” (get from o extracción) y “<<” (put to o inserción). Una de las principales mejoras que presenta C++ respecto a C es la sobrecarga de operadores. La sobrecarga de operadores permite al compilador determinar qué función u operador va a ser ejecutado basándose en los tipos de datos de las variables asociadas.
Ejemplo: int ivalor=10; float fvalor=7.2; count << “Valor entero: “ << ivalor << “, Valor en coma flotante: “ << fvalor; cin >> ivalor >> fvalor >> c; Ya no es necesario preceder a las variables de entrada con el operador de dirección &. Cuando se quiere introducir información, se extrae “>>” del flujo de entrada, cin, y se coloca la información en una variable, por ejemplo ivalor. Para extraer información, se coge una copia de la información de la variable fvalor y se inserta “<<” en el flujo de salida, cout. El operador de extracción “>>” lee hasta el carácter nueva línea pero no lo ignora. Cuando se compara el código fuente en C++ con la salida del programa, una de las cosas que se observa inmediatamente es que el operador de inserción “<<” no genera automáticamente una nueva línea. Puede controlar cuándo ocurre esto incluyendo el símbolo de nueva línea “\n” o “endl” cuando sea necesario. Ende es muy útil para la salida de datos en un programa interactivo porque no sólo inserta una nueva línea en un flujo, sino que además vuelca el “buffer” de salida. También puede utilizar flush; sin embargo, éste no inserta una nueva línea. Anexo II 6-22
Lenguaje C++
La función cin.get() leerá todo, incluyendo el espacio en blanco, hasta el número máximo de caracteres especificado que se hayan leído o hasta el siguiente carácter de nueva línea. El tercer parámetro opcional, que no se ha mostrado, identifica un símbolo de terminación. Por ejemplo, la siguiente línea leería N caracteres en nombre o todos los caracteres escritos antes de un símbolo “*” o un carácter de nueva línea:
cin.get (nombre, N, *); La clase istream mantiene las operaciones de entrada básicas, mientras que la salida básica la mantiene la clase ostream. La E/S bidireccional es mantenida por la clase iostream, que se deriva de istream y ostream. Hay cuatro objetos de flujo predefinidos para el usuario: •
cin → Objeto de la clase istream asociado a la entrada estándar.
•
cout → Objeto de la clase ostream asociado a la salida estándar.
•
cerr → Objeto de la clase ostream sin buffer de salida asociado al error estándar.
•
clog → Objeto de la clase ostream con buffer de salida asociado al error estándar.
12.2. Entrada / Salida de datos a través de ficheros en C++ Los ficheros se utilizan para la lectura y/o escritura de datos en unidades de almacenamiento permanente como los disquetes, discos duros, etc. En los ficheros de acceso secuencial se lee o escribe desde el inicio del fichero o se escribe a partir del final. Para trabajar con este tipo de ficheros las clases necesarias son ifstream, ofstream y fstream, que derivan de istream y ostream, que a su vez derivan de la clase ios. Para utilizarlas se debe incluir el archivo de cabecera “fstream.h”. Antes de abrir un fichero hay que crear un flujo o stream, es decir un objeto de las clases ifstream, ofstream o fstream e indicar el modo de apertura (lectura, escritura, etc.) • Clase ofstream Es una clase derivada de ostream, especializada en manipular ficheros en el disco abiertos para escribir. Al construir un objeto de esta clase, el constructor lo conecta automáticamente con un objeto filebuf (un buffer). La funcionalidad de esta clase está soportada por las siguientes funciones miembro: ofstream (const char *nombre_fichero, int modo=ios::out, int proteccion=filebuf:: openprot); void open (const char *nombre_fichero, int modo=ios::out, int proteccion= filebuf::openprot); void close(); //esta función cierra el fichero int is_open(); //verifica si el fichero está abierto(=1). Si no lo está devuelve un 0. Anexo II 6-23
Desarrollo de Sistemas
Para escribir en el fichero se utiliza el operador de inserción “<<” sobrecargado. Para leer del fichero se usa el operador de extracción “>>”. Esta forma de escritura es sólo en formato texto. • Clase ifstream Es una clase derivada de istream, especializada en manipular ficheros en el disco abiertos para leer. Al construir un objeto de esta clase, el constructor lo conecta automáticamente con un objeto filebuf (un buffer). La funcionalidad de esta clase está soportada por las siguientes funciones miembro: ifstream (const char *nombre_fichero, int modo=ios::in, int proteccion=filebuf:: openprot); void open (const char *nombre_fichero, int modo=ios::in, int proteccion=filebuf:: openprot); void close(); //esta función cierra el fichero int is_open(); //verifica si el fichero está abierto(=1). Si no lo está devuelve un 0. • Clase fstream Es una clase derivada de iostream, especializada en manipular ficheros en el disco abiertos para leer y/o escribir. Al construir un objeto de esta clase, el constructor lo conecta automáticamente con un objeto filebuf (un buffer). La funcionalidad de esta clase está soportada por las siguientes funciones miembro: fstream (const char *nombre_fichero, int modo, int proteccion=filebuf::openprot); void open (const char *nombre_fichero, int modo, int proteccion=filebuf::openprot); void close(); //esta función cierra el fichero int is_open(); //verifica si el fichero está abierto(=1). Si no lo está devuelve un 0. Si el fichero se abre con el modo: ios::app entonces, todo lo que se escriba se agregará a partir del final del fichero. Otras posibilidades de leer y escribir en un fichero son:
—
getline(): lee de fichero un número de caracteres especificado en la variable nCount o hasta que encuentre el carácter fin de línea “\n”. Devuelve un NULL cuando encuentra el final del fichero. Su prototipo es: istream& getline(unsigned char* puch, int nCount, char dlm = ‘\n’);
—
read() y write(): leen y escriben, respectivamente, bloques de bytes o datos binarios. Sus prototipos son: istream& read( unsigned char* bif, int num); ostream& write( unsigned char* bif, int num);
Anexo II 6-24
Lenguaje C++
En el acceso aleatorio de ficheros permite leer o escribir a partir de una determinada posición del fichero. Esto tiene una gran ventaja, ya que se pueden modificar algunos de los valores contenidos en el fichero. C++ nos da unas funciones para el acceso aleatorio: —
streampos es un typedef de long. desp es la nueva posición, desplazada. desp bytes, desde la posición dada por pos, el cual puede ser: ios::beg Principio del fichero. ios::cur Posición actual del puntero del stream. ios::end Final del stream. seekg se usa para desplazarse en un fichero para lectura. seekp se usa para desplazarse en un fichero para escritura. tellg, tellp dan la posición actual del puntero de lectura y escritura, respectivamente. Para escribir en un fichero de acceso aleatorio, éste debe ser abierto de modo lectura/escritura, usando para ello: ios::in | ios::out Leer y escribir objetos. Para leer y escribir objetos en formato binario, se deben sobrecargar los operadores de extracción “>>” e inserción “<<”, en los cuales pondremos el código necesario para usar las funciones read y write de las clases ofstream, ifstream o fstream.
13. Programación Orientada a Objetos 13.1. Concepto de “clase” en C++ Una clase es un tipo de datos definido por el usuario. Es una agrupación de datos (variables) y de funciones (métodos) que operan sobre esos datos. La definición de una clase consta de dos partes:
Anexo II 6-25
Desarrollo de Sistemas
•
La primera está formada por el nombre de la clase precedido por la palabra reservada class.
•
La segunda parte es el cuerpo de la clase, encerrado entre llaves y seguido por “;”:
class nombre { //cuerpo }; El cuerpo de la clase consta de: •
Especificadores de acceso: public, protected y private.
•
Atributos: datos miembro de la clase (variables).
•
Métodos: definiciones de funciones miembro de la clase.
Tanto las variables como los métodos pueden ser declarados public, protected y private, controlando de esta forma el acceso y evitando un uso inadecuado. La idea de clase junto con la sobrecarga de operadores, que estudiaremos más adelante, permite al usuario diseñar tipos de datos que se comporten de manera similar a los tipos estándar del lenguaje. Esto significa que debemos poder declararlos y usarlos en diversas operaciones como si fueran tipos elementales, siempre y cuando esto sea necesario. La idea central es que los tipos del usuario sólo se diferencian de los elementales en la forma de crearlos, no en la de usarlos. Pero las clases no sólo nos permiten crear tipos de datos, sino que nos dan la posibilidad de definir tipos de datos abstractos y definir sobre estos nuevas operaciones sin relación con los operadores estándar del lenguaje. Introducimos un nuevo nivel de abstracción y comenzamos a ver los tipos como representaciones de ideas o conceptos que no necesariamente tienen que tener una contrapartida a nivel matemático, como sucedía hasta ahora, sino que pueden ser conceptos relacionados con el mundo de nuestro programa. Así, podremos definir un tipo coche y un tipo motor en un programa de mecánica o un tipo unidad funcional en un programa de diseño de arquitectura de computadores, etcétera. Sobre estos tipos definiremos una serie de operaciones que definirán la interacción de las variables (objetos) de estos tipos con el resto de entidades de nuestro programa. Otra idea fundamental a la hora de trabajar con clases es la distinción clara entre la definición o interface de nuestros tipos de datos y su implementación, esto es, la distinción entre qué hace y cómo lo hace. Una buena definición debe permitir el cambio de la estructura interna de nuestras clases sin que esto afecte al resto de nuestro programa. Deberemos definir los tipos de manera que el acceso a la información y operaciones que manejan sea sencillo, pero el acceso a las estructuras y algoritmos utilizados en la implementación sea restringido, de manera que una modificación en estos últimos solo sea percibido en la parte de nuestro programa que implementa la clase. Por último, es interesante tener presentes las ideas ya mencionadas en el bloque anterior de objeto y paso de mensajes entre objetos. Estas ideas pue-
Anexo II 6-26
Lenguaje C++
den resultar de gran ayuda a la hora del diseño e implementación de programas, ya que podemos basarnos en el comportamiento de los objetos de interés en nuestro programa para abstraer clases y métodos (mensajes).
Nota. Por omisión, los datos miembro de la clase son private.
13.2. Miembros de una clase Los miembros de una clase pueden ser variables de cualquier tipo y funciones. En C++ a las variables se las denomina datos miembro, y a las funciones, funciones miembro de la clase. A) Datos miembro de una clase Para declarar un dato miembro se procede de la misma forma que para declarar cualquier variable. Por ejemplo:
class Lolo { public: float real; }; •
Los datos miembro no pueden ser inicializados durante la declaración.
•
Si no se pone un especificador de acceso (como es nuestro ejemplo public), por defecto los datos miembro serán private.
•
En una clase cada dato miembro debe tener un nombre único.
•
También podemos declarar como datos miembro de una clase, objetos de otra clase, siendo necesario que ésta haya sido previamente definida.
class datos { Lolo obj_lolo; //Declaración de un objeto de la clase dolo }; •
Para acceder a un dato miembro (siempre y cuando sea declarado como público) de una clase desde un objeto, se utilizará el operador Punto “.”. Por ejemplo: obj_lolo.real = 5.4;
13.3. Funciones miembro de una clase Las funciones miembro de una clase definen las operaciones que se pueden realizar con sus datos miembro. Las funciones miembro también pueden ser públicas o privadas, lo cual se hace con los respectivos especificadores. Al Anexo II 6-27
Desarrollo de Sistemas
igual que los datos miembro, si no se especifica serán privadas (private) por defecto. Para declarar una función miembro de una clase se procede de la misma forma que para declarar una función cualquiera. Por ejemplo en el fichero lolo.h:
class Lolo { private: float real,imaginario; public: //funcione miembro públicas void Asignar (float x, float y); }; El cuerpo de una clase solo contiene los prototipos de las funciones miembro (las declaraciones). La definición de la función se hace en los ficheros fuente (*.cpp). Para definir la función miembro en el fichero fuente se utiliza el nombre de la clase seguido por el operador de resolución de ámbito “::”. Por ejemplo, en el fichero lolo.cpp:
void Lolo::Asignar(int x, float y) { real = x; imaginario = y; //acceso a los datos miembro …… …… } Las funciones miembro que tengan poco código, o las que se desee, también pueden ser definidas en el cuerpo de la clase. Dentro del cuerpo de la clase no hay necesidad de anteponer el nombre de la clase con el operador “::”, como se hace generalmente en el fichero fuente, puesto que el nombre de la clase es conocido. Para acceder a una función miembro (siempre y cuando sea declarada como pública) de una clase desde un objeto, se utilizará el operador Punto “.”. Por ejemplo: obj_lolo.Asignar (10, 7.2); A) Control de acceso a los miembros de la clase El concepto de clase incluye la idea de ocultación de datos, que, básicamente, consiste en que no se puedan acceder a los datos miembro directamente, sino que hay que hacerlo a través de las funciones miembro públicas de la clase. Para controlar el acceso a los miembros (datos y funciones) de una clase, C++ provee de tres especificadores: •
Public. Un miembro declarado público es accesible en cualquier parte del programa donde el objeto de la clase en cuestión es accesible.
•
Private. Un miembro declarado privado puede ser accedido solamente por las funciones miembro de su propia clase o por funciones amigas (friend) de su clase. En cambio, no puede ser accedido por funciones globales o por las funciones propias de una clase derivada (herencia).
Anexo II 6-28
Lenguaje C++
•
Protected. Un miembro declarado protegido se comporta exactamente igual que uno privado para las funciones globales, pero actúa como un miembro público para las funciones miembro de una clase derivada.
Una clase es un tipo de datos que se define mediante una serie de miembros que representan atributos y operaciones sobre los objetos de ese tipo. Hasta ahora conocemos la forma de definir un tipo de datos por los atributos que posee, lo hacemos mediante el uso de las estructuras. Pensemos, por ejemplo, en cómo definimos un tipo de datos empleado en un programa de gestión de personal:
struct empleado { char * nombre; long DNI; float sueldo; ... }; Con esta definición conseguimos que todas las características que nos interesan del empleado se puedan almacenar conjuntamente, pero nos vemos obligados a definir funciones que tomen como parámetro variables de tipo empleado para trabajar con estos datos:
void modificar_sueldo (empleado *e, float nuevo_sueldo); ... Pero el C++ nos da una nueva posibilidad, incluir esas funciones como miembros del tipo empleado:
struct empleado { char * nombre; long DNI; float sueldo; ... void modificar_sueldo (float nuevo_sueldo); ... }; A estas funciones se les denomina miembros función o métodos, y tienen la peculiaridad de que sólo se pueden utilizar junto con variables del tipo definido. Es interesante señalar, aunque sea anticipar acontecimientos, que la función miembro no necesita que se le pase la estructura como parámetro, ya que al estar definida dentro de ella tiene acceso a los datos que contiene.
Anexo II 6-29
Desarrollo de Sistemas
Como distintas clases pueden emplear el mismo nombre para los miembros, a la hora de definir las funciones miembro debemos especificar el nombre de la estructura a la que pertenecen:
void empleado::modificar_sueldo (float nuevo_sueldo) { sueldo = nuevo_sueldo; }; Si definimos la función dentro de la estructura esto último no es necesario, ya que no hay lugar para la confusión. B) Acceso a miembros: la palabra class Hasta ahora hemos empleado la palabra struct para definir las clases; este uso es correcto, pero tiene una connotación específica: todos los miembros del tipo son accesibles desde el exterior del tipo, es decir, podemos modificar los datos o invocar a las funciones del mismo desde el exterior de la definición:
empleado lolo; // declaramos un objeto de tipo empleado lolo.sueldo = 500; // asignamos el valor 500 al campo sueldo lolo.modificar_sueldo(600) // le decimos a lolo que cambie su sueldo a 600 En el caso del ejemplo puede parecer poco importante que se pueda acceder a los datos del tipo, pero hemos dicho que lo que nos interesa es que la forma de representar los datos o de implementar los algoritmos sólo debe ser vista en la definición de la clase. Para que lo que contiene la clase solo sea accesible desde la definición empleamos la palabra class en lugar de struct para definir el tipo:
class empleado { ... } C) Acceso a miembros: etiquetas de control de acceso Con public delante de los miembros de la clase éstos sí deben ser vistos desde fuera:
class empleado { char * nombre; long DNI; float sueldo; ... public: void modificar_sueldo (float nuevo_sueldo); ... Anexo II 6-30
Lenguaje C++
} lolo; lolo.sueldo = 500; // ERROR, sueldo es un miembro privado lolo.modificar_sueldo (600); // CORRECTO, modificar_sueldo() es un método público Además de public también podemos emplear las etiquetas protected y private dentro de la declaración de la clase. Todo lo que aparezca después de una etiqueta será accesible (o inaccesible) hasta que encontremos otra etiqueta que cambie la accesibilidad o inaccesibilidad. La etiqueta protected tiene una utilidad especial que veremos cuando hablemos de herencia; de momento la usaremos de la misma forma que private, es decir, los miembros declarados después de la etiqueta serán inaccesibles desde fuera de la clase. Utilizando las etiquetas podemos emplear indistintamente la palabra struct o class para definir clases, la única diferencia es que si no ponemos nada con struct se asume acceso público y con class se asume acceso privado (con el sentido de la etiqueta private, no protected). Es mejor usar siempre la palabra class y especificar siempre las etiquetas de permiso de acceso, aunque podamos tener en cuenta el hecho de que por defecto el acceso es privado es más claro especificarlo. Hemos de indicar que también se puede definir una clase como union, que implica acceso público pero solo permite el acceso a un miembro cada vez (es lo mismo que sucedía con las uniones como tipo de datos compuesto). D) Operadores de acceso a miembros El acceso a los miembros de una clase tiene la misma sintaxis que para estructuras (el operador . y el operador ->), aunque también se emplea muy a menudo el operador de campo (::) para acceder a los miembros de la clase. Por ejemplo se emplea el operador de campo para distinguir entre variables de un método y miembros de la clase:
class empleado { ... float sueldo; ... public: void modificar_sueldo (float sueldo) { empleado::sueldo = sueldo; } ... }; E) El puntero this En uno de los puntos anteriores comentábamos que un método perteneciente a una clase tenía acceso a los miembros de su propia clase sin necesidad de pasar como parámetro el objeto con el que se estaba trabajando. Esto no es tan sencillo, puesto que es lógico pensar que los atributos (datos) conAnexo II 6-31
Desarrollo de Sistemas
tenidos en la clase son diferentes para cada objeto de la clase, es decir, se reserva memoria para los miembros de datos, pero no es lógico que cada objeto ocupe memoria con una copia de los métodos, ya que replicaríamos mucho código. Los objetos de una clase tienen un atributo específico asociado: su dirección. La dirección del objeto nos permitirá saber qué variables debemos modificar cuando accedemos a un miembro de datos. Esta dirección se pasa como parámetro (implícito) a todas las funciones miembro de la clase y se llama this. Si en alguna función miembro queremos utilizar nuestra propia dirección podemos utilizar el puntero como si lo hubiéramos recibido como parámetro. Por ejemplo, para retornar el valor de un atributo escribimos:
float empleado::cuanto_cobra (void) { return sueldo; } Pero también podríamos haber hecho lo siguiente:
float empleado::cuanto_cobra (void) { return this->sueldo; } Utilizar el puntero dentro de una clase suele ser redundante, aunque a veces es útil cuando trabajamos con punteros directamente. F) Funciones miembro constantes Un método de una clase se puede declarar de forma que nos impida modificar el contenido del objeto (es decir, como si para la función el parámetro this fuera constante). Para hacer esto basta escribir la palabra después de la declaración de la función:
class empleado { ... float cuanto_cobra (void) const; ... }; float empleado::cuanto_cobra (void) const { return sueldo; } Las funciones miembro constantes se pueden utilizar con objetos constantes, mientras que las que no lo son no pueden ser utilizadas (ya que podrían modificar el objeto). De cualquier forma, existen maneras de modificar un objeto desde un método constante: el empleo de cast sobre el parámetro this o el uso de miembros puntero a datos no constantes. Veamos un ejemplo para el primer caso:
Anexo II 6-32
Lenguaje C++
class empleado { private: ... long num_accesos_a_empleado; ... public: ... float cuanto_cobra (void) const ... }; float empleado::cuanto_cobra (void) const { ((empleado *)this)->num_accesos_a_empleado += 1; // hemos accedido una vez más a // la clase empleado return sueldo; } Otro ejemplo: struct contabilidad { long num_accesos_a_clase; }; class empleado { private: ... contabilidad *conta; ... public: ... float cuanto_cobra (void) const ... }; float empleado::cuanto_cobra (void) const { conta->num_accesos_a_clase += 1; // hemos accedido una vez más a // la clase empleado return sueldo; } Anexo II 6-33
Desarrollo de Sistemas
Esta posibilidad de modificar objetos desde métodos constantes se permite en el lenguaje por una cuestión conceptual: un método constante no debe modificar los objetos desde el punto de vista del usuario, y declarándolo como tal el usuario lo sabe, pero, por otro lado, puede ser interesante que algo que, para el que llama a una función miembro, no modifica al objeto si lo haga realmente con variables internas (no visibles para el usuario) para llevar contabilidades o modificar estados. Esto es especialmente útil cuando declaramos objetos constantes de una clase, ya que podemos modificar variables mediante funciones constantes. G) Funciones miembro inline Al igual que se podían declarar funciones de tipo “inline” generales, también se pueden definir funciones miembro “inline”. La idea es la misma; que no se genere llamada a función. Para hacer esto en C++ existen dos posibilidades: definir la función en la declaración de la clase (por defecto implica que la función miembro es inline), o definir la función fuera de la clase precedida de la palabra “inline”:
inline float empleado::cuanto_cobra { return sueldo; } Lo único que hay que indicar es que no podemos definir la misma función inline dos veces (en dos ficheros diferentes). H) Atributos estáticos Cuando en la declaración de una clase ponemos atributos (datos) estáticos, queremos indicar que ese atributo es compartido por todos los objetos de la clase. Para declararlo estático solo hay que escribir la palabra static antes de su declaración:
class empleado { ... static long num_total_empleados; ... }; Con esto conseguimos que el atributo tenga características de variable global para los miembros de la clase, pero que permanezca en el ámbito de la misma. Hay que tener presente que los atributos estáticos ocupan memoria aunque no declaremos ningún objeto. Si un atributo se declara público para acceder a él desde el exterior de la clase debemos identificarlo con el operador de campo: empleado::num_total_empleados = 1000;
Anexo II 6-34
Lenguaje C++
El acceso desde los miembros de la clase es igual que siempre. Los atributos estáticos se deben definir fuera del ámbito de la clase, aunque al hacerlo no se debe poner la palabra static (podrían producirse conflictos con el empleo de static para variables y funciones globales). Si no se inicializan en su definición toman valor 0:
long empleado::num_total_empleados; // definición, toma valor 0 El uso de atributos estáticos es más recomendable que el de las variables globales. I) Tipos anidados Dentro de las clases podemos definir nuevos tipos (enumerados, estructuras, clases…), pero para utilizarlos tendremos las mismas restricciones que para usar los miembros, es decir, serán accesibles según el tipo de acceso en el que se encuentren y para declarar variables de esos tipos tendremos que emplear la clase y el operador de campo:
class lista { private: struct nodo { int val; nodo *sig; }; nodo *primero; public: enum tipo_lista { FIFO, LIFO }; void inserta (int); int siguiente (); ... }; nodo n1;
// ERROR, nodo no es un tipo definido, está en otro ámbito
tipo_lista tl1;
// ERROR, tipo_lista no definido
lista::nodo n2;
// ERROR, tipo nodo privado
lista::tipo_lista tl2;
// CORRECTO
J) Punteros a miembros Cuando dimos la lista de operadores de indirección mencionamos dos de ellos que aún no se han visto: el operador de puntero selector de puntero a miembro (->*) y el operador selector de puntero a miembro (.*). Estos operadores están directamente relacionados con los punteros a miembros de una clase (como sus nombres indican). Suele ser especialmente interesante tomar la dirección de Anexo II 6-35
Desarrollo de Sistemas
los métodos por la misma razón que era interesante tomar la dirección de funciones, aunque en clases se utiliza más a menudo (de hecho las llamadas a métodos de una clase hacen uso de punteros a funciones, aunque sea implícitamente). Para tomar la dirección de un miembro de la clase X escribimos &X::miembro. Una variable del tipo puntero a miembro de la clase X se obtiene declarándolo de la forma X::*. Por ejemplo si tenemos la clase:
class empleado { ... void imprime_sueldo (void); ... }; podemos definir una variable que apunte a un método de la clase que retorna “void” y no tiene parámetros:
void (empleado::*ptr_a_metodo) (void); o usando typedef:
typedef void (empleado::*PMVV) (void); PMVV ptr_a_metodo; Para usar la variable podemos hacer varias cosas:
// acceso a miembro apuntado por puntero a través de un objeto
(pe->*ptr_a_metodo)();
// acceso a miembro apuntado por puntero a través de un puntero a objeto
En el ejemplo se usan paréntesis porque .* y ->* tienen menos precedencia que el operador de función. En realidad el uso de estos punteros es poco usual, ya que se puede evitar usando funciones virtuales. K) Objetos de una clase Un objeto es un ejemplar concreto de una clase. Las clases son como tipos de variables, mientras que los objetos son como variables concretas de un tipo determinado. Los objetos constan de una estructura interna (los datos) y de una interfaz que permite manipular tal estructura (las funciones). Un objeto se crea de la misma forma que como se crea una variable. Anexo II 6-36
Lenguaje C++
Ejemplo.- Lolo milolo; Un objeto no se inicializa como las variables:
Ejemplo: Lolo milolo=5;
//Error
13.4. Constructores y destructores Hasta ahora hemos hablado de la declaración y definición de clases, pero hemos utilizado los objetos sin saber cómo se crean o se destruyen. En este punto veremos cómo las clases se crean y destruyen de distintas maneras y qué cosas se hacen al crear o destruir un objeto. • Creación de objetos Podemos clasificar los objetos en cuatro tipos diferentes según la forma en que se crean: 1.
Objetos automáticos: son los que se crean al encontrar la declaración del objeto y se destruyen al salir del ámbito en que se declaran.
2.
Objetos estáticos: se crean al empezar la ejecución del programa y se destruyen al terminar la ejecución.
3.
Objetos dinámicos: son los que se crean empleando el operador new y se destruyen con el operador delete.
4.
Objetos miembro: se crean como miembros de otra clase o como un elemento de un array.
Los objetos también se pueden crear con el uso explícito del constructor (lo vemos en seguida) o como objetos temporales. En ambos casos son objetos automáticos. Hay que notar que estos modelos de creación de objetos también es aplicable a las variables de los tipos estándar del C++, aunque no tenemos tanto control sobre ellos. • Inicialización y limpieza de objetos Con lo que sabemos hasta ahora sería lógico pensar que si deseamos inicializar un objeto de una clase debemos definir una función que tome como parámetros los valores que nos interesan para la inicialización y llamar a esa función nada más declarar la función. De igual forma, nos interesará tener una función de limpieza de memoria si nuestro objeto utiliza memoria dinámica, que deberíamos llamar antes de la destrucción del objeto. Bien, esto se puede hacer así, explícitamente, con funciones definidas por nosotros, pero las llamadas a esos métodos de inicialización y limpieza pueden resultar pesadas y hasta difíciles de localizar en el caso de la limpieza de memoria.
Anexo II 6-37
Desarrollo de Sistemas
Para evitar el tener que llamar a nuestras funciones, el C++ define dos funciones especiales para todas las clases: los métodos constructor y destructor. La función constructor se invoca automáticamente cuando creamos un objeto y la destructor cuando lo destruimos. Nosotros podemos implementar o no estas funciones, pero es importante saber que si no lo hacemos el C++ utiliza un constructor y destructor por defecto. Estos métodos tienen una serie de características comunes muy importantes: •
No retornan ningún valor, ni siquiera de tipo “void”. Por lo tanto, cuando las declaramos, no debemos poner ningún tipo de retorno.
•
Como ya hemos dicho, si no se definen se utilizan los métodos por defecto.
•
No pueden ser declarados constantes, volátiles ni estáticos.
•
No se puede tomar su dirección.
•
Un objeto con constructores o destructores no puede ser miembro de una unión.
•
El orden de ejecución de constructores y destructores es inverso, es decir, los objetos que se construyen primero se destruyen los últimos. Ya veremos que esto es especialmente importante al trabajar con la herencia.
A) Constructor En C++, una forma de asegurar que los objetos siempre contengan valores válidos y que puedan ser inicializados al momento de la declaración es escribiendo un constructor. Un constructor es una función miembro especial de una clase que es llamada de forma automática siempre que se declara un objeto de esa clase. Su función es crear e inicializar un objeto de su clase. Dado que un constructor es una función miembro, admite argumentos al igual que éstas. El constructor se puede distinguir claramente, con respecto a las demás funciones miembro de la clase, porque tiene el mismo nombre que el de la clase. Un constructor no retorna ningún valor ni se hereda. Si el usuario no ha creado uno, el compilador crea uno por omisión, sin argumentos. Pueden existir varios constructores, siempre y cuando difieran en los argumentos. Se puede crear un objeto de cualquiera de las formas siguientes: •
Declarando un objeto local o temporal: Lolo obj_lolo;
•
Invocando al operador new: Lolo *obj_lolo = new Lolo;
Ejemplo: Lolo *obj_lolo = new Lolo(3,4); De esta forma, se debe llamar al operador delete al finalizar; •
Llamando explícitamente al constructor: Lolo obj_lolo(4 , 5.0);
•
Cuando se utiliza el operador new para crear el objeto, el acceso a las variables y funciones miembro de la clase se hace a través del operador (->), siempre y cuando sean públicas. Por ejemplo:
Anexo II 6-38
Lenguaje C++
obj_lolo->real = 4.5; //llamada a una variable perteneciente a la clase obj_lolo->funcion(); //llamada a una funcion perteneciente a la clase Los constructores se pueden considerar como funciones de inicialización y, como tales, pueden tomar cualquier tipo de parámetros, incluso por defecto. Los constructores se pueden sobrecargar, por lo que podemos tener muchos constructores para una misma clase (como ya sabemos, cada constructor debe tomar parámetros distintos). Existe un constructor especial (que podemos definir) o no definir que tiene una función muy específica: copiar atributos entre objetos de una misma clase. Si no lo definimos se usa uno por defecto que copia todos los atributos de los objetos, pero si lo definimos se usa el nuestro. Este constructor se usa cuando inicializamos un objeto por asignación de otro objeto. Para declarar un constructor lo único que hay que hacer es declarar una función miembro sin tipo de retorno y con el mismo nombre que la clase, como ya hemos dicho los parámetros pueden ser los que queramos:
// constructor con dos parámetros por defecto // Lo podremos usar con 0, 1, o 2 parámetros.
Complejo (&Complejo);
// constructor copia
... }; // Definición de los constructores // Inicialización Complejo::Complejo (float pr, float pi) { re = pr; im = pi; } // Constructor copia Complejo::Complejo (Complejo& c) { re= c.re; im= c.im; } Los constructores se suelen declarar públicos, pero si todos los constructores de una clase son privados solo podremos crear objetos de esa clase utilizando funciones amigas. A las clases que solo tienen constructores privados se las suele denominar privadas. Los constructores se pueden declarar virtuales. B) Destructor De la misma forma que existe una función para construir cada unos de los objetos que declaramos, también existe una función para destruir cada objeto construido, liberando así la memoria que ocupa. Esta función recibe el Anexo II 6-39
Desarrollo de Sistemas
nombre de destructor. Un objeto es destruido automáticamente al salir del ámbito en el que ha sido definido. Sin embargo, si se ha creado con el operador new, se debe utilizar el operador delete para destruirlo. El destructor es una función miembro especial de una clase que se utiliza para eliminar un objeto de esa clase. El destructor se distingue fácilmente del resto de funciones porque tiene el mismo nombre que la clase precedido del operador tilde “~”. El destructor no es heredado, no tiene argumentos ni puede retornar ningún valor. No hace falta llamar al destructor, ya que éste es invocado de forma automática cuando se destruye el objeto por los medios mencionados anteriormente. Si hiciera falta, el destructor también puede ser invocado de forma explícita:
Objeto.nombre_clase::~nombre_clase;
//llamada al destructor
Objeto->nombre_clase::~nombre_clase; //cuando el objeto ha sido creado con new class Lolo{
real = imaginario = 0; } Lolo::~Lolo(){ //codigo necesario para liberar memoria. cout << «Se ha llamado al destructor\n»; } Para cada clase sólo se puede definir un destructor, ya que el destructor no puede recibir parámetros y por tanto no se puede sobrecargar. Ya hemos dicho que los destructores no pueden ser constantes, volátiles ni estáticos, pero si pueden ser declarados virtuales (ya veremos más adelante que quiere decir esto). Para declarar un destructor escribimos dentro de la declaración de la clase el símbolo ~ seguido del nombre de la clase. Se emplea el símbolo ~ para indicar que el destructor es el complemento del constructor. Veamos un ejemplo:
class X { private: int *ptr; public: Anexo II 6-40
Lenguaje C++
X(int =1);
// constructor
~X();
// destructor
}; // declaración del constructor X::X(int i){ ptr = new int [i]; } // declaración del destructor X::~X() { delete []ptr; }
13.5. Miembros static de una clase Cada objeto tiene una copia de los datos miembro de la clase. Un dato miembro de una clase declarado como static implica que solo existirá una copia de ese dato miembro para todos los objetos y existe aunque no existan objetos de esa clase, con lo cual se concluye que es un dato asociado con la clase y no con el objeto. Un dato miembro static: •
Puede ser declarado “static”, private o public.
•
Tiene que ser inicializado a nivel global (ámbito de fichero, no de clase).
•
Cuando sea accedido a través de una función miembro, dicha función también tiene que ser declarada “static”.
•
Existe aunque no haya ningún objeto de la clase declarado.
Al igual que los atributos estáticos mencionados en un punto anterior, las funciones miembro estáticas son globales para los miembros de la clase y deben ser definidas fuera del ámbito de la declaración de la clase. Estos métodos son siempre públicos, se declaren donde se declaren. Al no tener parámetro this no pueden acceder a los miembros no estáticos de la clase (al menos directamente, ya que se le podría pasar un puntero al objeto para que modificara lo que fuera).
13.6. Funciones amigas (friend) Son funciones que tienen acceso a los miembros privados de una clase sin ser miembros de la misma. Se emplean para evitar la ineficiencia que supone el tener que acceder a los miembros privados de una clase a través de métodos. Como son funciones independientes de la clase no tienen parámetro this, por lo que el acceso a objetos de una clase se consigue pasándoles como parámetro una Anexo II 6-41
Desarrollo de Sistemas
referencia al objeto (una referencia como tipo implica pasar el objeto sin copiar, aunque se trata como si fuera el objeto y no un puntero), un puntero o el mismo objeto. Por la misma razón, no tienen limitación de acceso, ya que se definen fuera de la clase. Para hacer amiga de una clase a una función debemos declararla dentro de la declaración de la clase precedida de la palabra friend:
class X { private: int i; ... friend int f(X&, int);
// función amiga que toma como parámetros una referencia // a un objeto del tipo X y un entero y retorna un entero
} En la definición de la función (que se hace fuera de la clase como las funciones normales) podremos usar y modificar los miembros privados de la clase amiga sin ningún problema: int f(X& objeto, int i) { int j = objeto.i; objeto.i = i; return j; } Es importante ver que aunque las funciones amigas no pertenecen a la clase se declaran explícitamente en la misma, por lo que forman parte de la interface de la clase. Una función miembro de una clase puede ser amiga de otra: class X { ... void f(); ... }; class Y { ... friend void X::f(); }; Si queremos que todas las funciones de una clase sean amigas de una clase podemos poner: class X { friend class Y; ... }; Anexo II 6-42
Lenguaje C++
En el ejemplo todas las funciones de la clase Y son amigas de la clase X, es decir, todos los métodos de Y tienen acceso a los miembros privados de X. Resumiendo, los miembros de una clase declarados como privados solamente pueden ser accedidos mediante las funciones miembro de la clase, garantizando así la integridad de los datos. Una función declarada friend (amiga) de una clase puede acceder a los miembros privados y protegidos de la clase C++. Para declarar una función amiga, basta con escribir la palabra clave friend delante del nombre de la función.
13.7. Variables locales El constructor de una variable local se ejecuta cada vez que encontramos la declaración de la variable local y su destructor se ejecuta cuando salimos del ámbito de la variable. Para ejecutar un constructor distinto del constructor por defecto al declarar una variable hacemos:
Complejo c (1, -1); // Crea el complejo c llamando al constructor // Complejo (float, float) y para emplear el constructor copia para inicializar un objeto hacemos:
Complejo d = c; // crea el objeto d usando el constructor copia // Complejo(Complejo&) Si definimos c y luego d, al salir del bloque de la variable primero llamaremos al destructor de d, y luego al de c.
13.8. Almacenamiento estático Cuando declaramos objetos de tipo estático su constructor se invoca al arrancar el programa y su destructor al terminar. Un ejemplo de esto está en los objetos “cin”, “cout” y “cerr”. Estos objetos se crean al arrancar el programa y se destruyen al acabar. Como siempre, constructores y destructores se ejecutan en orden inverso. El único problema con los objetos estáticos está en el uso de la función “exit()”. Cuando llamamos “a exit()” se ejecutan los destructores de los objetos estáticos, luego usar “exit()” en uno de ellos provocaría una recursión infinita. Si terminamos un programa con la función “abort()” los destructores no se llaman.
13.9. Almacenamiento dinámico Cuando creamos objetos dinámicos con “new” ejecutamos el constructor utilizado para el objeto. Para liberar la memoria ocupada debemos emplear el operador “delete”, que se encargará de llamar al destructor. Si no usamos “delete” no tenemos ninguna garantía de que se llame al destructor del objeto.
Anexo II 6-43
Desarrollo de Sistemas
Para crear un objeto con new ponemos: Complejo *c= new Complejo (1); y para destruirlo: delete c; El usuario puede redefinir los operadores “new” y “delete” y puede modificar la forma de interacción de los constructores y destructores con estos operadores. Veremos todo esto al hablar de sobrecarga de operadores. La creación de arrays de objetos se discute más adelante.
13.10. Objetos como miembros Cuando definimos una clase podemos emplear objetos como miembros, pero lo que no sabemos es como se construyen estos objetos miembro. Si no hacemos nada los objetos se construyen llamando a su constructor por defecto (aquel que no toma parámetros). Esto no es ningún problema, pero puede ser interesante construir los objetos miembro con parámetros del constructor del objeto de la clase que los define. Para hacer esto lo único que tenemos que hacer es poner en la definición del constructor los constructores de objetos miembro que queramos invocar. La sintaxis es poner dos puntos después del prototipo de la función constructora (en la definición, es decir, cuando implementamos la función) seguidos de una lista de constructores (invocados con el nombre del objeto, no el de la clase) separados por comas. El cuerpo de la definición de la función se pone después. Estos constructores se llamaran en el orden en el que los pongamos y antes de ejecutar el constructor de la clase que los invoca. Veamos un ejemplo:
class cjto_de_tablas { private: tabla elementos;
// objeto de clase tabla
tabla componentes;
// objeto de clase tabla
int tam_tablas; ... public: cjto_de_tablas (int tam);
// constructor
~cjto_de_tablas ();
// destructor
... }; cjto_de_tablas::cjto_de_tablas (int tam) :elementos (tam), componentes(tam), tam_tablas(tam) { ... // Cuerpo del constructor } Anexo II 6-44
Lenguaje C++
Como se ve en el ejemplo podemos invocar incluso a los constructores de los objetos de tipos estándar. Si en el ejemplo no inicializáramos componentes el objeto se crearía invocando al constructor por defecto (el que no tiene parámetros, que puede ser un constructor nuestro con parámetros por defecto). Este método es mejor que emplear punteros a objetos y construirlos con “new” en el constructor y liberarlos con “delete” en el destructor, ya que el uso de objetos dinámicos consume más memoria que los objetos estáticos (ya que usan un puntero y precisan llamadas al sistema para reservar y liberar memoria). Si dentro de una clase necesitamos miembros objeto pero no necesitamos que sean dinámicos emplearemos objetos miembro con la inicialización en el constructor de la clase. • Arrays de objetos Al igual que con los tipos estándar, también es posible crear array’s de objetos: •
Forma estática, utilizando los corchetes:
Nombre_clase Nombre_array[num_objetos]; •
Forma dinámica, usando new:
Nombre_clase *Nom_Puntero = new Nombre_clase[num]; Notas. •
Para crear un puntero a objeto se utiliza new.
•
Para acceder a los miembros se usa el operador (->).
•
Hemos de acordarnos de llamar a “delete” cuando ya no necesitemos el “array”.
Para declarar un array de objetos de una clase determinada es imprescindible que la clase tenga un constructor por defecto (que como ya hemos dicho es uno que no recibe parámetros pero puede tener parámetros por defecto). Al declarar el “array” se crearán tantos objetos como indiquen los índices llamando al constructor por defecto. La destrucción de los elementos para “arrays” estáticos se realiza por defecto al salir del bloque de la declaración (igual que con cualquier tipo de objetos estáticos), pero cuando creamos un “array” dinámicamente se siguen las mismas reglas explicadas al hablar de “new” y “delete”, es decir, debemos llamar a delete indicándole que queremos liberar un array. Veamos varios ejemplos:
tabla at[20];
// array de 20 tablas, se llama a los constructores por defecto
void f(int tam) { tabla *t1 = new tabla;
// puntero a un elemento de tipo tabla
tabla *t2 = new tabla [tam];
// puntero a un array de ‘tam’ tablas
... delete t1;
// destrucción de un objeto
detele []t2;
// destrucción de un array
} Anexo II 6-45
Desarrollo de Sistemas
14. Herencia en C++ Una clase clase derivada puede definirse a partir de otra clase ya existente (clase base), de la que hereda sus variables y funciones miembro. La clase derivada puede añadir y/o redefinir nuevas variables y/o funciones miembro. La clase base suele ser más general que la clase derivada. Ésta añade nuevas determinaciones o especificaciones (nuevas variables y/o funciones miembro). A su vez, la clase derivada puede ser clase base de una nueva clase derivada, que hereda sus variables y funciones miembro. Se puede constituir una jerarquía de clases. Además de public y private, C++ permite también definir miembros protected. Los miembros protected, al igual que los private, no son accesibles desde fuera de la clase. En una clase base, los miembros protected se diferencian de los private en que sí pueden ser accesibles para las clases derivadas de dicha clase base. Para la clase derivada, la clase base se puede heredar como pública o como privada: •
La clase derivada no tiene acceso a los miembros private de la clase base. Sí tiene acceso a los miembros public y protected.
•
Si la clase base se hereda como public, la clase derivada hereda los miembros public y protected de la clase base como miembros public y protected, respectivamente.
•
Si la clase base se hereda como private, la clase derivada hereda todos los miembros de la clase base como private.
14.1. Constructores de clases derivadas Un objeto de una clase derivada contiene todos los miembros de la clase base. El constructor de la clase derivada debe llamar al de la clase base. Cuando se define un constructor para una clase derivada, se debe especificar un inicializador base (llamada al constructor de la clase base). El inicializador base se especifica poniendo, a continuación de los argumentos del constructor, el carácter “:” y un constructor de la clase base seguido de una lista de argumentos entre paréntesis. Al declarar un objeto de la clase derivada, se ejecuta primero el constructor de la clase base y luego el de la clase derivada. El inicializador base puede ser omitido si la clase base tiene un constructor por defecto. El constructor de una clase derivada debe disponer de valores para sus propias variables y para el constructor de la clase base.
popo::popo(const char *nombre) : lolo (nombre) { …… …… }; Definimos las clases como antes, pero intentamos dar unas clases base o clases padre para representar las características comunes de las clases y luego
Anexo II 6-46
Lenguaje C++
definimos unas clases derivadas o subclases que definen tan solo las características diferenciadoras de los objetos de esa clase. Por ejemplo, si queremos representar empleados y clientes podemos definir una clase base persona que contenga las características comunes de ambas clases (nombre, DNI, etc.) y después declararemos las clases empleado y cliente como derivadas de persona, y solo definiremos los miembros que son nuevos respecto a las personas o los que tienen características diferentes en la clase derivada, por ejemplo un empleado puede ser despedido, tiene un sueldo, puede firmar un contrato, etc., mientras que un cliente puede tener una cuenta, una lista de pedidos, puede firmar un contrato, etc. Como se ha mencionado ambos tipos pueden firmar contratos, pero los métodos serán diferentes, ya que la acción es la misma pero tiene significados distintos. En definitiva, introducimos los mecanismos de la herencia y polimorfismo para implementar las relaciones entre las clases. La herencia consiste en la definición de clases a partir de otras clases, de tal forma que la clase derivada hereda las características de la clase base, mientras que el polimorfismo nos permite que métodos declarados de la misma manera en una clase base y una derivada se comporten de forma distinta en función de la clase del objeto que la invoque, el método es polimórfico, tiene varias formas.
14.2. Clases derivadas o subclases Una clase derivada es una clase que se define en función de otra clase. La sintaxis es muy simple: declaramos la clase como siempre, pero después de nombrar la clase escribimos dos puntos y el nombre de su clase base. Esto le indica al compilador que todos los miembros de la clase base se heredan en la nueva clase. Por ejemplo, si tenemos la clase empleado (derivada de persona) y queremos definir la clase directivo podemos declarar esta última como derivada de la primera. Así, un directivo tendrá las características de persona y de empleado, pero definirá además unos nuevos atributos y métodos propios de su clase:
class directivo : empleado { private: long num_empleados; long num_acciones; ... public: ... void despide_a (empleado *e); void reunion_con (directivo *d); ... }; Como un objeto de tipo directivo es un empleado, se podrá usar en los lugares en los que se trate a los empleados, pero no al revés (un empleado no puede usarse cuando necesitamos un directivo). Esto es cierto cuando trabajamos con punteros a objetos, no con objetos: Anexo II 6-47
Desarrollo de Sistemas
directivo d1, d2; empleado e1; lista_empleados *le; le= &d1; // inserta un directivo en la lista de empleados d1.next = &e1; // el siguiente empleado es e1 e1.next = &d2; // el siguiente empleado es el directivo 2 d1.despide_a (&e1); // el directivo puede despedir a un empleado d1.despide_a (&d2); // o a otro directivo, ya que también es un empleado e1.despide_a (&d1); // ERROR, un empleado no tiene definido el método despide a d1.reunion_con (&d2); // Un directivo se reúne con otro d1.reunion_con (&e); // ERROR, un empleado no se reúne con un directivo empleado *e2 = &d2; // CORRECTO, un directivo es un empleado directivo *d3 = &e; // ERROR, no todos los empleados son directivos d3->num_empleados =3; // Puede provocar un error, ya que e1 no tiene espacio // reservado para num_empleados d3 = (directivo *)e2. // CORRECTO, e2 apunta a un directivo d3->num_empleados =3; // CORRECTO, d3 apunta a un directivo En definitiva, un objeto de una clase derivada se puede usar como objeto de la clase base si se maneja con punteros, pero hay que tener cuidado ya que el C++ no realiza chequeo de tipo dinámico (no tiene forma de saber que un puntero a un tipo base realmente apunta a un objeto de la clase derivada). A) Funciones miembro en clases derivadas En el ejemplo del punto anterior hemos definido nuevos miembros (podemos definir nuevos atributos y métodos, e incluso atributos de la clase derivada con los mismos nombres que atributos de la clase base de igual o distinto tipo) para la clase derivada, pero, ¿cómo accedemos a los miembros de la clase base desde la derivada? Si no se redefinen, podemos acceder a los atributos de la forma habitual y llamar a los métodos como si estuvieran definidos en la clase derivada, pero si se redefinen para acceder al miembro de la clase base debemos emplear el operador de campo aplicado al nombre de la clase base (en caso contrario accedemos al miembro de la clase derivada):
class empleado { ... void imprime_sueldo(); void imprime_ficha (); ...
Anexo II 6-48
Lenguaje C++
} class directivo : empleado { ... void imprime_ficha () { imprime_sueldo(); empleado::imprime_ficha(); ... } ... }; directivo d; d.imprime_sueldo (); // se llama al método implementado para empleado, ya // que la clase directivo no define el método d.imprime_ficha (); // se llama al método definido en directivo d.empleado::imprime_ficha (); // llamamos al método de la clase base empleado
14.3. Constructores y destructores Algunas clases derivadas necesitan constructores, y si la clase base de una clase derivada tiene un constructor este debe ser llamado proporcionándole los parámetros que necesite. En realidad, la gestión de las llamadas a los constructores de una clase base se gestionan igual que cuando definimos objetos miembro, es decir, se llaman en el constructor de la clase derivada de forma implícita si no ponemos nada (cuando la clase base tiene un constructor por defecto) o de forma explícita siempre que queramos llamar a un constructor con parámetros (o cuando esto es necesario). La única diferencia con la llamada al constructor respecto al caso de los objetos miembro es que en este caso llamamos al constructor con el nombre de la clase y no del objeto, ya que aquí no existe. Veamos un ejemplo:
class X { ... X();
// constructor sin param
X (int); // constructor que recibe un entero ~X();
// destructor
}; class Y : X { ... Y();
// constructor sin param
Anexo II 6-49
Desarrollo de Sistemas
Y(int); // constructor con un parámetro entero Y (int, int)
; // constructor con dos parámetros enteros
... }; // constructor sin param, invoca al constructor por defecto de X Y::Y() { ... } // constructor con un parámetro entero, invoca al constructor que recibe un entero de la clase X Y::Y(int i) : X(i) { ... } // constructor con dos parámetros enteros, invoca al constructor por defecto de X Y::Y (int i , int j) { ... }
14.4. Las jerarquías de clases Como ya hemos visto las clases derivadas pueden a su vez ser clases base de otras clases, por lo que es lógico pensar que las aplicaciones en las que definamos varias clases acabemos teniendo una estructura en árbol de clases y subclases. En realidad esto es lo habitual, construir una jerarquía de clases en las que la clase base es el tipo objeto y a partir de él cuelgan todas las clases. Esta estructura tiene la ventaja de que podemos aplicar determinadas operaciones sobre todos los objetos de la clase, como por ejemplo, mantener una estructura de punteros a objeto de todos los objetos dinámicos de nuestro programa o declarar una serie de variables globales en la clase raíz de nuestra jerarquía que sean accesibles para todas las clases pero no para funciones definidas fuera de las clases. Aparte del diseño en árbol se utiliza también la estructura de bosque: definimos una serie de clases sin descendencia común, pero que crean sus propios árboles de clases. Generalmente, se utiliza un árbol principal y luego una serie de clases contenedor que no están en la jerarquía principal y, por tanto, pueden almacenar objetos de cualquier tipo sin pertenecer realmente a la jerarquía (si están junto con el árbol principal podemos llegar a hacer programas muy complejos de forma innecesaria, ya que una pila podría almacenarse a sí misma, causando problemas a la hora de destruir objetos). No siempre la estructura es un árbol, ya que la idea de herencia múltiple provoca la posibilidad de interdependencia entre nodos de ramas distintas, por lo que sería más correcto hablar de grafos en vez de árboles. Anexo II 6-50
Lenguaje C++
14.5. Los métodos virtuales El C++ permite el empleo de funciones polimórficas, que son aquellas que se declaran de la misma manera en distintas clases y se definen de forma diferente. En función del objeto que invoque a una función polimórfica se utilizará una función u otra. En definitiva, una función polimórfica será aquella que tendrá formas distintas según el objeto que la emplee. Los métodos virtuales son un mecanismo proporcionado por el C++ que nos permiten declarar funciones polimórficas. Cuando definimos un objeto de una clase e invocamos a una función virtual, el compilador llamará a la función correspondiente a la de la clase del objeto. Para declarar una función como virtual basta poner la palabra virtual antes de la declaración de la función en la declaración de la clase. Una función declarada como virtual debe ser definida en la clase base que la declara (excepto si la función es virtual pura), y podrá ser empleada aunque no haya ninguna clase derivada. Las funciones virtuales solo se redefinen cuando una clase derivada necesita modificar la de su clase base. Una vez se declara un método como virtual esa función sigue siéndolo en todas las clases derivadas que lo definen, aunque no lo indiquemos. Es recomendable poner siempre que la función es virtual, ya que si tenemos una jerarquía grande se nos puede olvidar que la función fue declarada como virtual. Para gestionar las funciones virtuales el compilador crea una tabla de punteros a función para las funciones virtuales de la clase, y luego cada objeto de esa clase contendrá un puntero a dicha tabla. De esta manera tenemos dos niveles de indirección, pero el acceso es rápido y el incremento de memoria escaso. Al emplear el puntero a la tabla el compilador utiliza la función asociada al objeto, no la función de la clase que tenga el objeto en el momento de invocarla. Empleando funciones virtuales nos aseguramos que los objetos de una clase usarán sus propias funciones virtuales aunque se estén accediendo a través de punteros a objetos de un tipo base. Ejemplo:
// llamamos a la función de empleado, ya que aunque es // virtual, la clase directivo no la redefine
empleado *pe = &d; pe->imprime_sueldo();
// pe apunta a un directivo, llamamos a la función de la // clase directivo, que es la asociada al objeto d
La tabla se crea al construir el objeto por lo que los constructores no podrán ser virtuales, ya que no disponemos del puntero a la tabla hasta terminar con el constructor. Por esa misma razón hay que tener cuidado al llamar a funciones virtuales desde un constructor: llamaremos a la función de la clase base, no a la que redefina nuestra clase. Los destructores sí pueden ser declarados virtuales. Las funciones virtuales necesitan el parámetro this para saber qué objeto las utiliza y, por tanto, no pueden ser declaradas “static” ni friend. Una función friend no es un método de la clase que la declara como amiga, por lo que tampoco tendría sentido definirla como virtual. De cualquier forma, dijimos que una clase puede tener como amigos métodos de otras clases. Pues bien, estos métodos amigos pueden ser virtuales; si nos fijamos un poco, la clase que declara una función como amiga no tiene por qué saber si ésta es virtual o no.
14.6. Clases abstractas Ya hemos mencionado lo que son las jerarquías de clases, pero hemos dicho que se pueden declarar objetos de cualquiera de las clases de la jerarquía. Esto tienen un problema importante: al definir una jerarquía es habitual definir clases que no queremos que se puedan instanciar, es decir, clases que solo sirven para definir el tipo de atributos y mensajes comunes para sus clases derivadas: son las denominadas clases abstractas. En estas clases es típico definir métodos virtuales sin implementar, es decir, métodos que dicen cómo debe ser el mensaje pero no qué se debe hacer cuando se emplean con objetos del tipo base. Este mecanismo nos obliga a implementar estos métodos en todas las clases derivadas, haciendo más fácil la consistencia de las clases. Pues bien, el C++ define un mecanismo para hacer esto (ya que si no lo hiciera deberíamos definir esos métodos virtuales con un código vacío, lo que no impediría que declaráramos subclases que no definieran el método y además permitiría que definiéramos objetos del tipo base abstracto). La idea es que podemos definir uno o varios métodos como virtuales puros o abstractos (sin implementación), y esto nos obliga a redeclararlos en todas las clases derivadas (siempre que queramos definir objetos de estas subclases). Además, una clase con métodos abstractos se considera una clase abstracta y por tanto no podemos definir objetos de esa clase. Anexo II 6-52
Lenguaje C++
Para declarar un método como abstracto solo tenemos que igualarlo a cero en la declaración de la clase (escribimos un igual a cero después del prototipo del método, justo antes del punto y coma, como cuando inicializamos variables):
class X { private: ... public: X(); ~X(); virtual void f(int) = 0;
// método abstracto, no debemos definir la función para esta clase
... } class Y : public X { ... virtual void f(int);
// volvemos a declarar f, deberemos definir el método para la clase Y
... } Lo único que resta por mencionar de las funciones virtuales puras es que no tenemos por qué definirlas en una subclase de una clase abstracta si no queremos instanciar objetos de esa subclase. Esto se puede producir cuando de una clase abstracta derivan subclases para las que nos interesa definir objetos y también subclases que van a servir de clases base abstractas para nuevas clases derivadas. Una subclase de una clase abstracta será abstracta siempre que no redefinamos todas las funciones virtuales puras de la clase padre. Si redefinimos algunas de ellas, las clases que deriven de la subclase abstracta sólo necesitarán implementar las funciones virtuales puras que su clase padre (la derivada de la abstracta original) no haya definido.
14.7. Herencia múltiple La idea de la herencia múltiple es bastante simple, aunque tiene algunos problemas a nivel de uso. Igual que decíamos que una clase podía heredar características de otra, se nos puede ocurrir que una clase podría heredar características de más de una clase. El ejemplo típico es la definición de la clase de vehículos anfibios; como sabemos los anfibios son vehículos que pueden circular por tierra o por mar. Por tanto, podríamos definir los anfibios como elementos que heredan características de los vehículos terrestres y los vehículos marinos. La sintaxis para expresar que una clase deriva de más de una clase base es simple, ponemos el nombre de la nueva clase, dos puntos y la lista de clases padre: Anexo II 6-53
Desarrollo de Sistemas
class anfibio : terrestre, marino { ... }; Los objetos de la clase derivada podrán usar métodos de sus clases padre y se podrán asignar a punteros a objetos de esas clases. Las funciones virtuales se tratan igual, etc. Todo lo que hemos comentado hasta ahora es que la herencia múltiple es como la simple, excepto por el hecho de que tomamos (heredamos) características de dos clases. Pero no todo es tan sencillo, existen una serie de problemas que comentaremos en los puntos siguientes.
14.8. Ocurrencias múltiples de una base Con la posibilidad de que una clase derive de varias clases es fácil que se presente el caso de que una clase tenga una clase como clase más de una vez. Por ejemplo, en el caso del anfibio tenemos como base las clases terrestre y marino, pero ambas clases podrían derivar de una misma clase base vehículo. Esto no tiene por qué crear problemas, ya que podemos considerar que los objetos de la clase anfibio contienen objetos de las clases terrestre y marino, que a su vez contienen objetos diferentes de la clase vehículo. De todas formas, si intentamos acceder a miembros de la clase vehículo, aparecerán ambigüedades. A continuación veremos cómo podemos resolverlas.
14.9. Resolución de ambigüedades Evidentemente, dos clases pueden tener miembros con el mismo nombre, pero cuando trabajamos con herencia múltiple esto puede crear ambigüedades que deben ser resueltas. El método para acceder a miembros con el mismo nombre en dos clases base desde una clase derivada es emplear el operador de campo, indicando cuál es la clase del miembro al que accedemos:
virtual void imprime_tipo_motor(); { cout << Tipo_Motor; } ... }; class anfibio : terrestre, marino { ... virtual void imprime_tipo_motor(); ... }; void anfibio::imprime_tipo_motor () { cout << «Motor terrestre : «; terrestre::imprime_tipo_motor (); cout << «Motor acuático : «; marino::imprime_tipo_motor (); } Lo habitual es que la ambigüedad se produzca al usar métodos (ya que los atributos suelen ser privados y, por tanto, no accesibles para la clase derivada), y la mejor solución es hacer lo que se ve en el ejemplo: redefinir la función conflictiva para que utilice las de las clases base. De esta forma los problemas de ambigüedad se resuelven en la clase y no tenemos que emplear el operador de campo desde fuera de ésta (al llamar al método desde un objeto de la clase derivada). Si intentamos acceder a miembros ambiguos el compilador no generará código hasta que resolvamos la ambigüedad.
14.10. Clases base virtuales Las clases base que hemos empleado hasta ahora con herencia múltiple tienen la suficiente entidad como para que se declararen objetos de esas clases, es decir, heredábamos de dos o más clases porque en realidad los objetos de la nueva clase se componían o formaban a partir de otros objetos. Esto está muy bien, y suele ser lo habitual, pero existe otra forma de emplear la herencia múltiple: el hermanado de clases. El mecanismo de hermanado se basa en lo siguiente: para definir clases que toman varias características de clases derivadas de una misma clase. Es decir, definimos una clase base y derivamos clases que le añaden características y luego queremos usar objetos que tengan varias de las características que nos han originado clases derivadas. En lugar de derivar una clase de la base que reúna las características, podemos derivar una clase de las subclases que las incorporen. Por ejemplo, si definimos una clase ventana y derivamos las clases ventana_con_borde y ventana_con_menu, en lugar de derivar de la clase ventana una clase ventana_con_menu_y_borde, la derivamos de las dos subclases. En realidad lo que queremos es emplear un mismo objeto de la clase base ventana, por lo que nos interesa que las dos subclases generen sus objetos a partir de un mismo objeto ventana. Esto se consigue declarando la herencia de la clase Anexo II 6-55
Desarrollo de Sistemas
base como virtual en todas las subclases que quieran compartir su padre con otras subclases al ser empleadas como clase base, y también en las subclases que la hereden desde varias clases distintas:
class ventana { }; class ventana_con_borde : public virtual ventana { }; class ventana_con_menu : public virtual ventana { }; class ventana_con_menu_y_borde : public virtual ventana, public ventana_con_borde, public ventana_con_menu { }; El problema que surge en estas clases es que los métodos de la clase base común pueden ser invocados por dos métodos de las clases derivadas y que, al agruparse en la nueva clase, generen dos llamadas al mismo método de la clase base inicial. Por ejemplo, en el caso de la clase ventana, supongamos que definimos un método dibujar, que es invocado por los métodos dibujar de las clases ventana_con_borde y ventana_con_menu. Para definir el método dibujar de la nueva clase ventana_con_menu_y_ borde lo lógico sería llamar a los métodos de sus funciones padre, pero esto provocaría que llamáramos dos veces al método dibujar de la clase ventana, provocando no sólo ineficiencia, sino incluso errores (ya que el redibujado de la ventana puede borrar algo que no debe borrar, por ejemplo el menú). La solución pasaría por definir dos funciones de dibujo, una virtual y otra no virtual: usaremos la virtual para dibujar objetos de la clase (por ejemplo, ventanas con marco) y la no virtual para dibujar sólo lo característico de nuestra clase. Al definir la clase que agrupa características llamaremos a las funciones no virtuales de las clases padre, evitando que se repitan llamadas. Otro problema con estas clases es que si dos funciones hermanas redefinen un método de la clase padre (como el método dibujar anterior), la clase que herede de ambas deberá redefinirla para evitar ambigüedades (¿a qué función se llama si la subclase no redefine el método?).
14.11. Control de acceso Como ya comentamos en puntos anteriores, los miembros de una clase pueden ser privados, protegidos o públicos (private, protected, public). El acceso a los miembros privados está limitado a funciones miembro y amigas de la clase; el acceso protegido es igual que el privado, pero también permite que accedan a ellos las clases derivadas; y los miembros públicos son accesibles desde cualquier sitio en el que la clase sea accesible. El único modelo de acceso que no hemos estudiado es el protegido. Cuando implementamos una clase base podemos querer definir funciones que puedan utilizar las clases derivadas pero que no se puedan usar desde fuera de la clase. Si definimos miembros como privados tenemos el problema de que la clase derivada tampoco puede acceder a ellos. La solución es definir esos métodos como “protected”. Anexo II 6-56
Lenguaje C++
Estos niveles de acceso reflejan los tipos de funciones que acceden a las clases: las funciones que la implementan, las que implementan clases derivadas y el resto. Ya se ha mencionado que dentro de la clase podemos definir prácticamente cualquier cosa (tipos, variables, funciones, constantes, etc.). El nivel de acceso se aplica a los nombres, por lo que lo que podemos definir como privados, públicos o protegidos no sólo los atributos, sino todo lo que puede formar parte de la clase. Aunque los miembros de una clase tienen definido un nivel de acceso, también podemos especificar un nivel de acceso a las clases base desde clases derivadas. El nivel de acceso a clases base se emplea para saber quién puede convertir punteros a la clase derivada en punteros a la clase base (de forma implícita, ya que con casts siempre se puede) y acceder a miembros de la clase base heredados en la derivada. Es decir, una clase con acceso private a su clase base puede acceder a su clase base, pero ni sus clases derivadas ni otras funciones tienen acceso a la misma, es como si definiéramos todos los miembros de la clase base como “private” en la clase derivada. Si el acceso a la clase base es “protected”, solo los miembros de la clase derivada y los de las clases derivadas de esta última tienen acceso a la clase base. Y si el acceso es público, el acceso a los miembros de la clase base es el especificado en ella. Para especificar el nivel de acceso a la clase base ponemos la etiqueta de nivel de acceso antes de escribir su nombre en la definición de una clase derivada. Si la clase tiene herencia múltiple, debemos especificar el acceso de todas las clases base. Si no ponemos nada, el acceso a las clases base se asume public. Ejemplo:
class anfibio : public terrestre, protected marino { ... }; • Gestión de memoria Cuando creamos objetos de una clase derivada se llama a los constructores de sus clases base antes de ejecutar el de la clase, y luego se ejecuta el suyo. El orden de llamada a los destructores es el inverso, primero el de la clase derivada y luego el de sus padres. Comentamos al hablar de métodos virtuales que los destructores podían ser declarados como tales; la utilidad de esto es clara: si queremos destruir un objeto de una clase derivada usando un puntero a una clase base y el destructor no es virtual la destrucción será errónea, con los problemas que esto puede traer. De hecho, casi todos los compiladores definen un “flag” para que los destructores sean virtuales por defecto. Lo más típico es declarar los destructores como virtuales siempre que en una clase se defina un método virtual, ya que es muy posible que se manejen punteros a objetos de esa clase. Además de comentar la forma de llamar a constructores y destructores, en este punto se podría hablar de las posibilidades de sobrecarga de los operadores “new” y “delete” para clases, ya que esta sobrecarga nos permite modificar el modo en que se gestiona la memoria al crear objetos. Como el siguiente punto es la sobrecarga de operadores estudiaremos esta posibilidad en ella. Sólo decir que la sobrecarga de la gestión de memoria es especialmente interesante en las clases base, ya que si ahorramos memoria al trabajar con objetos de la clase base es evidente que la ahorraremos siempre que creemos objetos de clases derivadas. Anexo II 6-57
Desarrollo de Sistemas
15. Sobrecarga en C++ • Sobrecarga de funciones La sobrecarga de funciones consiste en definir varias funciones con el mismo nombre diferenciándolas por los argumentos que son de distinto tipo y será el detalle que permite al compilador llamar a una u otra función.
} void funcion(int a) { cout << «Valor entero: « << a << endl; } void funcion(double a) { cout << «Valor real: « << a << endl; } • Sobrecarga de operadores Los operadores, al igual que las funciones, pueden ser sobrecargados: •
La mayor parte de los operadores de C++ pueden ser redefinidos para actuar sobre objetos de una clase.
•
Se puede cambiar la definición de un operador, pero no su gramática: número de operandos, precedencia y asociatividad.
•
El tipo de los operandos determina qué definición del operador se va a utilizar.
•
Al menos uno de los operandos debe ser un objeto de la clase.
La sintaxis para declarar un operador sobrecargado es la siguiente:
Tipo_operator operador([argumentos]); Donde:
tipo indica el tipo del valor retornado por la función operador es unos de los siguientes: +,-,*,/,%,&,!,>,<,=,[],new, delete, ...
Anexo II 6-58
Lenguaje C++
Sobrecarga del operador de indexación “[ ]“: •
El operador de indexación, operator[], permite manipular los objetos de clases igual que si fuesen arrays.
•
La llamada a la función operator[] de una clase se hace escribiendo el nombre de un objeto de la clase para el cual se quiere invocar dicha función, seguido de un valor encerrado entre corchetes.
•
La forma de sobrecargar el operador es:
int& operator[]( int i); Esta capacidad se traduce en poder definir un significado para los operadores cuando los aplicamos a objetos de una clase específica. Además de los operadores aritméticos, lógicos y relacionales, también la llamada (), el subíndice [] y la de referencia -> se pueden definir, e incluso la asignación y la inicialización pueden redefinirse. También es posible definir la conversión implícita y explícita de tipos entre clases de usuario y tipos del lenguaje.
15.1. Funciones operador Se pueden declarar funciones para definir significados para los siguientes operadores: +-*/%^&|~! = < > += -= *= /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ — ->* , -> [] () new delete No podemos modificar ni las precedencias ni la sintaxis de las expresiones para los operadores, ya que podríamos provocar ambigüedades. Tampoco podemos definir nuevos operadores. El nombre de una función operador es la palabra clave operator seguida del operador, por ejemplo operator+. Al emplear un operador podemos llamar a su función o poner el operador, el uso del operador sólo es para simplificar la escritura. Por ejemplo:
int c = a + b; es lo mismo que:
int c = operator+ (a, b);
15.2. Asignación e inicialización La asignación entre objetos de un mismo tipo definido por el usuario puede crear problemas; por ejemplo, si tenemos la clase cadena:
Anexo II 6-59
Desarrollo de Sistemas
class cadena { private: char *p; // puntero a cadena int tam; // tamaño de la cadena apuntada por p public: cadena (int t) { p = new char [tam =t] } ~cadena () { delete []p; } } la operación: cadena c1(10); cadena c2(20); c2 = c1; asignará a c2 el puntero de c1, por lo que al destruir los objetos dejaremos la cadena c2 original sin tocar y llamaremos dos veces al destructor de c1. Esto se puede resolver redefiniendo el operador de asignación:
class cadena { ... cadena& operator= (const cadena&); // operador de asignación } cadena& cadena::operator= (const cadena& a) { if (this != &a) { // si no igualamos una cadena a si misma delete []p; p = new char[tam = a.tam]; strncpy (p, a.p); } return *this; // nos retornamos a nosotros mismos } Con esta definición resolvemos el problema anterior, pero aparece un nuevo problema: hemos dado por supuesto que la asignación se hace para objetos inicializados pero, ¿qué pasa si en lugar de una asignación estamos haciendo una inicialización? Por ejemplo:
cadena c1(10); cadena c2 = c1; En esta situación solo construimos un objeto, pero destruimos dos. El operador de asignación definido por el usuario no se aplica a un objeto sin iniAnexo II 6-60
Lenguaje C++
cializar; en realidad debemos definir un constructor copia para objetos de un mismo tipo. Este constructor es el que se llama en la inicialización:
class cadena { ... cadena (const cadena&); // constructor copia } cadena::cadena (const cadena& a) { p = new char[tam = a.tam]; strncpy (p, a.p); }
15.3. Sobrecarga de new y delete Al igual que el resto de operadores, los operadores “operator new” y “operator delete” se pueden sobrecargar. Esto se emplea para crear y destruir objetos de formas distintas a las habituales: reservando el espacio de forma diferente o en posiciones de memoria que no están libres en el “heap”, inicializando la memoria a un valor concreto, etc. El “operador new” tiene un parámetro obligatorio de tipo size_t y luego podemos poner todo tipo y número de parámetros. Su retorno debe ser un puntero “void”. El parámetro size_t es el tamaño en “bytes” de la memoria a reservar, si la llamada a “new” es para crear un vector size_t debe ser el número de elementos por el tamaño de la clase de los objetos del “array”. Es muy importante tener claro lo que hacemos cuando redefinimos la gestión de memoria, y siempre que sobrecarguemos el “new” o el “delete” tener presente que ambos operadores están relacionados y ambos deben ser sobrecargados a la vez para reservar y liberar memoria de formas extrañas.
16. Templates C++ es un lenguaje muy potente tal y como lo hemos definido hasta ahora, pero al ir incorporándole características se ha tendido a que no se perdiera eficiencia (dentro de unos márgenes) a cambio de una mayor comodidad y potencia a la hora de programar. El C introdujo en su momento un mecanismo sencillo para facilitar la escritura de código: las macros. Una macro es una forma de representar expresiones; se trata en realidad de evitar la repetición de la escritura de código mediante el empleo de abreviaturas, sustituyendo una expresión por un nombre o un nombre con aspecto de función que luego se expande y sustituye las abreviaturas por código. El mecanismo de las macros no estaba mal, pero tenía un grave defecto: el uso y la definición de macros se hace a ciegas en lo que al compilador se refiere. El mecanismo de sustitución que nos permite definir pseudo-funcioAnexo II 6-61
Desarrollo de Sistemas
nes no realiza ningún tipo de chequeos y es por tanto poco seguro. Además, la potencia de las macros es muy limitada. Para evitar que cada vez que definamos una función o una clase tengamos que replicar código en función de los tipos que manejemos (como parámetros en funciones o como miembros y retornos y parámetros de funciones miembro en clases), el C++ introduce el concepto de funciones y clases genéricas. Una función genérica es realmente como una plantilla de una función: lo que representa es lo que tenemos que hacer con unos datos sin especificar el tipo de algunos de ellos. Por ejemplo una función máximo se puede implementar igual para enteros, para reales o para complejos, siempre y cuando esté definido el operador de relación <. Pues bien, la idea de las funciones genéricas es definir la operación de forma general, sin indicar los tipos de las variables que intervienen en la operación. Una vez dada una definición general, para usar la función con diferentes tipos de datos, la llamaremos indicando el tipo (o los tipos de datos) que intervienen en ella. En realidad es como si le pasáramos a la función los tipos junto con los datos. Al igual que sucede con las funciones, las clases contenedor son estructuras que almacenan información de un tipo determinado, lo que implica que cada clase contenedor debe ser reescrita para contener objetos de un tipo concreto. Si definimos la clase de forma general, sin considerar el tipo que tiene lo que vamos a almacenar y luego le pasamos a la clase el tipo o los tipos que le faltan para definir la estructura, ahorraremos tiempo y código al escribir nuestros programas.
16.1. Funciones genéricas Para definir una función genérica solo tenemos que poner delante de la función la palabra template seguida de una lista de nombres de tipos (precedidos de la palabra “class”) y separados por comas, entre los signos de menor y mayor. Los nombres de los tipos no se deben referir a tipos existentes, sino que deben ser como los nombres de las variables, identificadores. Los tipos definidos entre mayor y menor se utilizan dentro de la clase como si de tipos de datos normales se tratara. Al llamar a la función el compilador sustituirá los tipos parametrizados en función de los parámetros actuales (por eso, todos los tipos parametrizados deben aparecer al menos una vez en la lista de parámetros de la función). Ejemplo:
template // sólo un tipo parámetro T max (T a, T b) { return (a>b) ? a : b } // función genérica máximo Los tipos parámetro no solo se pueden usar para especificar tipos de variables o de retornos, también podemos usarlos dentro de la función para lo que queramos (definir variables, punteros, asignar memoria dinámica, etc.). En definitiva, los podemos usar para lo mismo que los tipos normales. Todos lo modificadores de una función (inline, static, etc.) van después de template < ... >.
Anexo II 6-62
Lenguaje C++
Las funciones genéricas se pueden sobrecargar y también especializar. Para sobrecargar una función genérica lo único que debemos hacer es redefinirla con distinto tipo de parámetros (haremos que emplee más tipos o que tome distinto número o en distinto orden los parámetros), y para especializar una función debemos implementarla con los tipos parámetro especificados (algunos de ellos al menos):
template T max (T a, T b) { ... }
// función máximo para dos parámetros de tipo T, sobrecarga de la función
template T max (int *p, T a) { ... }
// función máximo para punteros a entero y valores de tipo T // sobrecarga de la función
template T max (T a[]) { ... }
// función genérica máximo para vectores de tipo T, especialización // función máximo para cadenas como punteros a carácter
const char* max(const char *c1, const char *c2) { return (strncmp(c1, c2) >=1) ? c1 : c2; } // ejemplos de uso int i1 = 9, i2 = 12; cout << max (i1, i2);
// se llama a máximo con dos enteros, T=int
int *p = &i2; cout << max (p, i1); (T=entero)
//llamamos a la función que recibe puntero y tipo T
cout << max («HOLA», «ADIOS»);
// se llama a la función especializada para trabajar con cadenas.
Con las funciones especializadas lo que sucede es muy simple: si llamamos a la función y existe una versión que especifica los tipos, usamos esa. Si no encuentra la función, busca una función template de la que se pueda instanciar una función con los tipos de la llamada. Si las funciones están sobrecargadas resuelve como siempre, si no encuentra ninguna función aceptable, da un error.
Anexo II 6-63
Desarrollo de Sistemas
16.2. Clases genéricas También podemos definir clases genéricas de una forma muy similar a las funciones. Esto es especialmente útil para definir las clases contenedor, ya que los tipos que contienen sólo nos interesan para almacenarlos y podemos definir las estructuras de una forma más o menos genérica sin ningún problema. Hay que indicar que si las clases necesitan comparar u operar de alguna forma con los objetos de la clase parámetro, las clases que usemos como parámetros actuales de la clase deberán tener sobrecargados los operadores que necesitemos. Para declarar una clase paramétrica hacemos lo mismo de antes:
template
// podríamos poner más de un tipo
class vector { T* v;
// puntero a tipo T
int tam; public: vector (int); T& operator[] (int);
// el operador devuelve objetos de tipo T
... } Pero para declarar objetos de la clase debemos especificar los tipos (no hay otra forma de saber por que debemos sustituirlos hasta no usar el objeto):
vector v(100); // vector de 100 elementos de tipo T = int Una vez declarados los objetos se usan como los de una clase normal. Para definir los métodos de la clase solo debemos poner la palabra “template” con la lista de tipos y al poner el nombre de la clase adjuntarle su lista de identificadores de tipo (igual que lo que ponemos en template pero sin poner “class”):
Al igual que las funciones genéricas, las clases genéricas se pueden especializar, es decir, podemos definir una clase especifica para unos tipos determinados e incluso especializar solo métodos de una clase. Lo único a tener en cuenta es que debemos poner la lista de tipos parámetro especificando los tipos al especificar una clase o un método:
// especializamos la clase para char *, podemos modificar totalmente la def. de la clase class vector { char *feo; public: vector (); void hola (); } // Si sólo queremos especializar un método vector::vector (int i) { ... // constructor especial para float } Además de lo visto el C++ permite que las clases genéricas admitan constantes en la lista de tipos parámetro:
template class pila { T bloque[SZ]; // vector de SZ elementos de tipo T ... }; La única limitación para estas constantes es que deben ser conocidas en tiempo de compilación. Otra facilidad es la de poder emplear la herencia con clases parametrizadas, tanto para definir nuevas clases genéricas como para definir clases no genéricas. En ambos casos debemos indicar los tipos de la clase base, aunque para clases genéricas derivadas de clases genéricas podemos emplear tipos de nuestra lista de parámetros. Ejemplo: template
class pila { ... } // clase template derivada template class pilita : public pila { // la clase base usa el tipo T y SZ vale 20 Anexo II 6-65
Desarrollo de Sistemas
... }; // clase no template derivada class pilita_chars : public pila { // heredamos de la clase pila con T=char y SZ=50 ... };
17. Manejo de excepciones Existen varios tipos de errores a la hora de programar: los errores sintácticos y los errores de uso de funciones o clase y los errores del usuario del programa. Los primeros los debe detectar el compilador, pero el resto se deben detectar en tiempo de ejecución, es decir, debemos tener código para detectarlos y tomar las acciones oportunas. Ejemplos típicos de errores son el salirse del rango de un vector, divisiones por cero, desbordamiento de la pila, etc. Para facilitarnos el manejo de estos errores el C++ incorpora un mecanismo de tratamiento de errores más potente que el simple uso de códigos de error y funciones para tratarlos.
17.1. Tratamiento de excepciones en C++ (throw - catch - try) La idea es la siguiente: en una cadena de llamadas a funciones los errores no se suelen tratar donde se producen, por lo que la idea es lanzar un mensaje de error desde el sitio donde se produce uno y ir pasándolo hasta que alguien se encargue de él. Si una función llama a otra y la función llamada detecta un error lo lanza y termina. La función llamante recibirá el error, si no lo trata, lo pasará a la función que la ha llamado a ella. Si la función recoge la excepción ejecuta una función de tratamiento del error. Además de poder lanzar y recibir errores, debemos definir un bloque como aceptor de errores. La idea es que probamos a ejecutar un bloque y si se producen errores los recogemos. En el resto de bloques del programa no se podrán recoger errores.
17.2. Lanzamiento de excepciones: throw Si dentro de una función detectamos un error lanzamos una excepción poniendo la palabra “throw” y un parámetro de un tipo determinado, es como si ejecutáramos un return de un objeto (una cadena, un entero o una clase definida por nosotros). Por ejemplo:
f() { ... int *i; if ((i= new int) == NULL) Anexo II 6-66
Lenguaje C++
throw «Error al reservar la memoria para i»; // no hacen falta paréntesis, // es como en return ... } Si la función f() fue invocada desde g() y ésta, a su vez, desde h(), el error se irá pasando entre ellas hasta que se recoja.
17.3. Recogida: catch Para recoger un error empleamos la pseudofunción “catch”; esta instrucción se pone como si fuera una función, con “catch” y un parámetro de un tipo determinado entre paréntesis, después abrimos llave, escribimos el código de gestión del error y cerramos la llave. Por ejemplo si la función h() trataba el error anterior:
h() { ... catch (char *ce) { cout << «He recibido un error que dice : « << ce; } ... } Podemos poner varios bloques “catch” seguidos, cada uno recogerá un error de un tipo distinto. El orden de los bloques es el orden en el que se recogen las excepciones:
h() { ... catch (char *ce) { ... // tratamos errores que lanzan cadenas } catch (int ee) { ... // tratamos errores que lanzan enteros } ... } Si queremos que un catch trate más de un tipo de errores, podemos poner tres puntos (parámetros indefinidos):
h() { Anexo II 6-67
Desarrollo de Sistemas
... catch (char *ce) { ... // tratamos errores que lanzan cadenas } catch (...) { ... // tratamos el resto de errores } ... }
17.4. El bloque de prueba: try El tratamiento de errores visto hasta ahora es muy limitado, ya que no tenemos forma de especificar dónde se pueden producir errores (en qué bloques del programa). La forma de especificar dónde se pueden producir errores que queremos recoger es emplear bloques “try”, que son bloques delimitados poniendo la palabra try y luego poniendo entre llaves el código que queremos probar. Después del bloque “try” se ponen los bloques “catch” para tratar los errores que se hayan podido producir:
h() { ... g();
// si produce un error, se le pasa al que llamo a h()
try { g();
// si produce un error lo tratamos nosotros
} catch (int i){ ... } catch (...){ ... } z(); } Solo podemos recoger errores después de un bloque “try”, por lo que los “catch” siempre van asociados a los “try”. Si una función que no está dentro de un bloque de prueba recibe un error la pasa a su nivel superior hasta que llegue a una llamada producida dentro de un bloque de prueba que trate el error o salga del programa principal. Si en un bloque “try” se produce un error que Anexo II 6-68
Lenguaje C++
no es tratado por sus “catch”, también pasamos el error hacia arriba. Cuando se recoge un error con un “catch” no se retorna al sitio que lo originó, sino que se sigue con el código que hay después del último “catch” asociado al “try” donde se aceptó el error. En el ejemplo se ejecutaría la función z().
17.5. La lista throw Podemos especificar los tipos de excepciones que puede lanzar una función, poniendo después del prototipo de la función la lista “throw”, que no es más que la palabra “throw” seguida de una lista de tipos separada por comas y entre paréntesis:
void f () throw (char*, int); // f sólo lanza cadenas y enteros Si una función lanza una excepción que no esté en su lista de tipos se produce un error de ejecución. Si ponemos una lista vacía la función no puede lanzar excepciones.
7. Sentencias o expresiones 8. Arrays y Cadenas 9. Clases en JAVA 10. Entrada / Salida 11. Herencia 12. Clases abstractas
Anexo III 6-2
19. Paseando por la Red 20. Los Sockets 21. El JAVA Development Kit
Lenguaje JAVA
1.
Introducción a JAVA
JAVA es un lenguaje de programación desarrollado por un grupo de ingenieros de Sun Microsystems (1991-1995). Es un lenguaje orientado a objetos, a diferencia de otros lenguajes que son lenguajes modificados para poder trabajar con objetos. En un principio fue denominado “Oak”, se le puso el nombre de JAVA en 1995. La estructura de un programa realizado en cualquier lenguaje orientado a objetos (Object Oriented Programming) (OOP), y en particular en el lenguaje JAVA es una clase. En JAVA todo forma parte de una clase, es una clase o describe cómo funciona una clase. El conocimiento de las clases es fundamental para poder entender los programas JAVA. Todas las acciones de los programas JAVA se colocan dentro del bloque de una clase. Todos los métodos se definen dentro del bloque de la clase, JAVA no soporta funciones o variables globales. En todo programa nos encontramos con una clase que contiene el programa principal y algunas clases de usuario (las específicas de la aplicación que se está desarrollando) que son utilizadas por el programa principal. Los ficheros fuente tienen la extensión *.java, mientras que los ficheros compilados tienen la extensión *.class. Un fichero fuente (*.java) puede contener más de una clase, pero sólo una puede ser public. El nombre del fichero fuente debe coincidir con el de la clase public (con la extensión *.java), es decir, si por ejemplo en un fichero aparece la declaración (public class MiClase {...}) entonces el nombre del fichero deberá ser MiClase.java. Es importante que coincidan mayúsculas y minúsculas ya que MiClase.java y miclase.java serían clases diferentes para JAVA. Si la clase no es public, no es necesario que su nombre coincida con el del fichero. Una clase puede ser public o package (default), pero no “private” o “protected”. De ordinario una aplicación está constituida por varios ficheros *.class. Cada clase realiza unas funciones particulares, permitiendo construir las aplicaciones con gran modularidad e independencia entre clases. Las clases de JAVA se agrupan en packages, que son librerías de clases. Si las clases no se definen como pertenecientes a un package, se utiliza un package por defecto (default) que es el directorio activo. Es necesario entender y dominar la sintaxis utilizada en la programación; observemos nuestro primer programa en JAVA y un breve comentario de las partes que lo componen, para posteriormente pasar a estudiar la nomenclatura empleada y los elementos que empleamos para desarrollar nuestro lenguaje:
/*
lolo.java Autor NombreyApellido 2005 Escribe en pantalla «¡Hola, mundo!»
*/ class lolo { public static void main(String args[]) { System.out.println(«¡Hola, mundo!»); } } Anexo III 6-3
Desarrollo de Sistemas
Analicemos el programa anterior: Comentario:
/* Lolo.java Autor NombreyApellido 2005 Escribe en pantalla «¡Hola, mundo!» */
Definición de clase Lolo: class Lolo Define método main: public static void main(String args[]) Accesible para todos: public Método de clase: static Tipo devuelto. (No devuelve nada): void Punto de entrada de los programas JAVA: main Argumentos en línea de comandos: (String args[]) Clase: System Objeto estático de System (salida estándar): out (flujo) Método de “out” (escribe en línea): println JAVA es un lenguaje que ha sido diseñado para producir “software”: —
Confiable: Minimiza los errores que se escapan a la fase de prueba.
—
Multiplataforma: Los mismos binarios funcionan correctamente en Windows, Unix y Power/Mac.
—
Seguro: Applets recuperados por medio de la red no pueden causar daño a los usuarios.
—
Orientado a objetos: Beneficioso tanto para el proveedor de bibliotecas de clases como para el programador de aplicaciones.
—
Robusto: Los errores se detectan en el momento de producirse, lo que facilita la depuración.
Entre las características que nombramos nos referimos a la robustez. JAVA no permite el manejo directo del “hardware” ni de la memoria. El intérprete siempre tiene el control. El compilador es suficientemente inteligente como para no permitir un montón de cosas que podrían traer problemas, como usar variables sin inicializarlas, modificar valores de punteros directamente, acceder a métodos o variables en forma incorrecta, utilizar herencia múltiple, etc. Además, JAVA Anexo III 6-4
Lenguaje JAVA
implementa mecanismos de seguridad que limitan el acceso a recursos de las máquinas donde se ejecuta, especialmente en el caso de los Applets (que son aplicaciones que se cargan desde un servidor y se ejecutan en el cliente). También está diseñado específicamente para trabajar sobre una red, de modo que incorpora objetos que permiten acceder a archivos en forma remota (via URL por ejemplo). Además, con el JDK (JAVA Development Kit) vienen incorporadas muchas herramientas, entre ellas un generador automático de documentación que, al poner los comentarios adecuados en las clases, crea inclusive toda la documentación de las mismas en formato HTML.
2.
Comentarios
Los comentarios son aquellas aclaraciones que el programador introduce en el lenguaje dirigido al propio programador como recordatorio o a futuros programadores que deseen variar el programa, con el fin de facilitar la comprensión del mismo; el ordenador ignorará dichos comentarios. // Comentarios para una sola línea /* Comentarios de una o más líneas */ /** Comentario que va a ir en ejecutable (Comentario de documentación) */
3.
Identificadores
Los identificadores se utilizan para nombrar variables, funciones, clases y objetos, o cualquier elemento que se necesite identificar o utilizar. Los identificadores utilizados en JAVA comienzan con una letra, un subrayado (_) o un símbolo de dólar ($), nunca por un dígito u otro carácter gráfico (@, #, …). El resto de los caracteres que componen los identificadores son o bien letras o bien dígitos o una combinación de ambos y no existe una longitud máxima. Serían identificadores válidos: nombre nombre_y_apellidos Nombre_Usuarios _variable_interna $moneda JAVA tiene una serie de palabras clave. Estas palabras no se pueden utilizar como identificadores. La siguiente lista de palabras incluye todas aquellas que son consideradas palabras clave por JAVA: Anexo III 6-5
Desarrollo de Sistemas
abstract
continue
for
new
switch
boolean
default
goto
null
synchronized
break
do
private
package
threadsafe
byte
double
This
if
implements
byvalue
else
import
protected
throw
case
extends
instanceof
public
transient
catch
false
int
return
true
char
final
“interface”
short
try
class
finally
long
static
void
const
float
native
super
while
Poco a poco las irá conociendo todas. Además de estas palabras clave, JAVA se ha reservado otras ocho palabras que no pueden ser utilizadas como identificadores; se presentan a continuación y por el momento no tienen ningún cometido específico.
4.
cast
future
generic
inner
operator
outer
rest
var
Variables
Son variables aquellos identificadores que a lo largo del programa pueden variar su valor, bien por el usuario, bien por el propio programa. JAVA utiliza cinco tipos de Variables: enteros, reales en coma flotante, booleanos, caracteres y cadenas. Las variables se pueden definir y utilizar en cualquier parte del código. Cada variable define un tipo de elementos con un rango perfectamente definido, siempre dentro de la clase.
4.1.
Declaración de variables locales Las variables locales se declaran igual que los atributos de la clase: Tipo NombreVariable [= Valor]; Ej:
int suma; float precio; Contador obj; Sólo que aquí no se declaran private, public, etc., sino que las variables definidas dentro del método sólo son accesibles por él. Las variables pueden inicializarse al crearse: Anexo III 6-6
Lenguaje JAVA
Ej: int suma = 0; float precio = 12.3; Contador obj = new Contador ( );
4.2.
Asignaciones a variables Se asigna un valor a una variable mediante el signo =: Variable = Constante | Expresión;
Ej: suma = suma + 1; precio = 1.05 * precio; obj.cnt = 0; Todas las variables en el lenguaje JAVA deben tener un tipo de dato. El tipo de la variable determina los valores que la variable puede contener y las operaciones que se pueden realizar con ella. Existen dos categorías de datos principales en el lenguaje JAVA: los tipos primitivos y los tipos referenciados. Los tipos primitivos contienen un solo valor e incluyen los tipos como los enteros, coma flotante, los caracteres, etc... La tabla siguiente muestra todos los tipos primitivos soportados por el lenguaje JAVA, su formato, su tamaño y una breve descripción de cada uno: TIPO
TAMAÑO/FORMATO
DESCRIPCIÓN
byte
8-bit complemento a 2
Entero de un Byte
short
16-bit complemento a 2
Entero corto
int
32-bit complemento a 2
Entero
long
64-bit complemento a 2
Entero largo
float
32-bit IEEE 754
Coma flotante de precisión simple
double
64-bit IEEE 754
Coma flotante de precisión doble
char
16-bit Carácter
Un solo carácter
boolean
true o false
Un valor booleano (verdadero o falso)
Los tipos referenciados se llaman así porque el valor de una variable de referencia es una referencia (un puntero) hacia el valor real. En JAVA tenemos los arrays, las clases y los “interfaces” como tipos de datos referenciados.
Anexo III 6-7
Desarrollo de Sistemas
Por convención, como ya hemos comentado, en JAVA los nombres de las variables empiezan con una letra minúscula (los nombres de las clases empiezan con una letra mayúscula).
5.
Operadores
Los operadores utilizados por JAVA son parecidos a los utilizados en operaciones algebraicas y lógicas. A continuación se muestran los operadores que maneja JAVA por orden de precedencia: .
[]
()
++
—
!
~
instanceof
*
/
%
+
-
<<
>>
>>>
<
>
<=
&
^
|
&&
||
?
:
=
op=
(*=
>=
==
!=
/=
%=
+=
-=
Los operadores con una precedencia más alta se evalúan primero. Por ejemplo, el operador división tiene una precedencia mayor que el operador suma, por eso, en la expresión anterior x + y / 100, el compilador evaluará primero y / 100. El operador = hace copias de objetos, marcando los antiguos para borrarlos, y el garbage collector se encargará de devolver al sistema la memoria ocupada por el objeto eliminado. Los operadores realizan algunas funciones en uno o dos operandos. Los operadores que requieren un operador se llaman operadores unarios. Por ejemplo, ++ es un operador unario que incrementa el valor operando en uno. Los operadores que requieren dos operandos se llaman operadores binarios. El operador = es un operador binario que asigna un valor del operando derecho al operando izquierdo. Los operadores unarios en JAVA pueden utilizar la notación de prefijo o de sufijo. La notación de prefijo significa que el operador aparece antes de su operando: operador operando. La notación de sufijo significa que el operador aparece después de su operando: operando operador . Los operadores binarios de JAVA tienen la misma notación, es decir, aparecen entre los dos operandos: op1 operator op2. Además de realizar una operación también devuelve un valor. El valor y su tipo dependen del tipo del operador y del tipo de sus operandos. Por ejemplo, Anexo III 6-8
Lenguaje JAVA
los operadores aritméticos (realizan las operaciones de aritmética básica como la suma o la resta) devuelven números, el resultado típico de las operaciones aritméticas. El tipo de datos devuelto por los operadores aritméticos depende del tipo de sus operandos: si sumas dos enteros, obtendrás un entero. Se dice que una operación evalúa su resultado. Los operadores JAVA se dividen en las siguientes categorías: aritméticos, relacionales y condicionales, lógicos y de desplazamiento y de asignación. A) Operadores aritméticos OPERADOR
USO
DESCRIPCIÓN
+
op1 + op2
Suma op1 y op2
-
op1 - op2
Resta op2 de op1
*
op1 * op2
Multiplica op1 y op2
/
op1 / op2
Divide op1 por op2
%
op1 % op2
Obtiene el resto de dividir op1 por op2
Nota: El lenguaje JAVA extiende la definición del operador + para incluir la concatenación de cadenas. Los operadores + y - tienen versiones unarias que seleccionan el signo del operando: OPERADOR
USO
DESCRIPCIÓN
+
+ op
Indica un valor positivo
-
- op
Niega el operando
Además, existen dos operadores de atajos aritméticos: ++ que incrementa en uno su operando, y — que decrementa en uno el valor de su operando. OPERADOR
USO
DESCRIPCIÓN
++
op ++
Incrementa op en 1; evalúa el valor antes de incrementar
++
++ op
Incrementa op en 1; evalúa el valor después de incrementar
—
op —
Decrementa op en 1; evalúa el valor antes de decrementar
—
— op
Decrementa op en 1; evalúa el valor después de decrementar
B) Operadores relacionales y condicionales Los valores relacionales comparan dos valores y determinan la relación entre ellos. Por ejemplo, != devuelve true si los dos operandos son distintos.
Anexo III 6-9
Desarrollo de Sistemas
OPERADOR
USO
DEVUELVE
TRUE SI
>
op1 > op2
op1 es mayor que op2
>=
op1 >= op2
op1 es mayor o igual que op2
<
op1 < op2
op1 es menor que op2
<=
op1 <= op2
op1 es menor o igual que op2
==
op1 == op2
op1 y op2 son iguales
!=
op1 != op2
op1 y op2 son distintos
Aquí tiene tres operadores condicionales: OPERADOR
USO
DEVUELVE
TRUE SI
&&
op1 && op2
op1 y op2 son verdaderos
||
op1 || op2
uno de los dos es verdadero
!
! op
op es falso
El operador & se puede utilizar como un sinónimo de && si ambos operadores son booleanos. Similarmente, | es un sinónimo de || si ambos operandos son booleanos. C) Operadores de desplazamiento OPERADOR
USO
DESCRIPCIÓN
>>
op1 >> op2
Desplaza a la derecha op2 bits de op1
<<
op1 << op2
Desplaza a la izquierda op2 bits de op1
>>>
op1 >>> op2
Desplaza a la derecha op2 bits de op1(sin signo)
&
op1 & op2
Bitwise and
|
op1 | op2
Bitwise or
^
op1 ^ op2
Bitwise xor
~
~ op
Bitwise complemento
D) Operadores de asignación Puede utilizar el operador de asignación =, para asignar un valor a otro. Además del operador de asignación básico, JAVA proporciona varios operadores de asignación que permiten realizar operaciones aritméticas, lógicas o de bits y una operación de asignación al mismo tiempo.
Anexo III 6-10
Lenguaje JAVA
6.
Separadores
En la gramática de JAVA también hay definidos una serie de separadores simples, que definen la forma y función del código. Estos separadores son:
()paréntesis
Se utilizan para identificar listas de parámetros utilizados en la definición y llamada a métodos. También se utilizan para definir el orden de precedencia en expresiones, para delimitar expresiones en situaciones de control de flujo y rodear las conversiones de tipo.
{}llaves
Contienen los valores de las matrices inicializadas automáticamente. También se utiliza para definir un bloque de código, para clases, métodos y ámbitos locales.
[] corchetes
Para declarar tipos matriz. También se utiliza cuando se referencian valores de matriz.
;punto y coma Separa sentencias. ,coma
Separa identificadores consecutivos en una declaración de variables. También se utiliza para encadenar sentencias dentro de una sentencia “for”.
.punto
Para separar nombres de paquete de subpaquetes y clases. También se utiliza para separar una variable o método de una variable de referencia.
7.
Sentencias o expresiones
Las expresiones realizan el trabajo de un programa JAVA. Entre otras cosas, las expresiones se utilizan para calcular y asignar valores a las variables y para controlar el flujo de un programa JAVA. El trabajo de una expresión se divide en dos partes: realizar los cálculos indicados por los elementos de la expresión y devolver algún valor. Una expresión es, por tanto, una serie de variables, operadores y llamadas a métodos (construida de acuerdo a la sintaxis del lenguaje) que evalúa a un valor sencillo. Si no se le indica explícitamente al compilador el orden en el que se quiere que se realicen las operaciones, él decide basándose en la precedencia asignada a los operadores y otros elementos que se utilizan dentro de una expresión. Una expresión es un conjunto variables unidos por operadores. Son órdenes que se le dan al equipo para que realice una tarea determinada. Se permite incluir varias sentencias en una línea, aunque lo habitual es utilizar una línea para cada sentencia. Por ejemplo: i = 0; j = 5; x = i + j; // Tres sentencias.
7.1.
Bifurcaciones
Las bifurcaciones permiten ejecutar una de entre varias acciones en función del valor de una expresión lógica o relacional. Se trata de estructuras muy importantes ya que son las encargadas de controlar el flujo de ejecución de un programa. Existen dos bifurcaciones diferentes: “if” y “switch”. Anexo III 6-11
Desarrollo de Sistemas
A) Bifurcación if Esta estructura permite ejecutar un conjunto de sentencias en función del valor que tenga la expresión de comparación (se ejecuta si la expresión de comparación tiene valor true). Tiene la forma siguiente: if (expresión_booleana) { SENTENCIAS; } Las llaves {} sirven para agrupar en un bloque las sentencias que se han de ejecutar, y no son necesarias si solo hay una sentencia dentro del “if”. B) Bifurcación if else Análoga a la anterior, de la cual es una ampliación. Las sentencias incluidas en el “else” se ejecutan en el caso de no cumplirse la expresión de comparación (false). if (expresión_booleana) { SENTENCIAS 1; } else { SENTENCIAS 2; } C) Bifurcación if elseif else Permite introducir más de una expresión de comparación. Si la primera condición no se cumple, se compara la segunda y así sucesivamente. En el caso de que no se cumpla ninguna de las comparaciones se ejecutan las sentencias correspondientes al else.
, if (expresión_booleana) instrucción_si_true; [else instrucción_si_false;] if (expresión_booleana) { instrucciones_si_true; } else { instrucciones_si_false; }
Anexo III 6-12
if (expresión_booleana1) { SENTENCIAS 1; } else if (expresión_booleana2) { SENTENCIAS 2; } else if (expresión_booleana3) { SENTENCIAS 3; } else { SENTENCIAS 4; }
Lenguaje JAVA
D) Sentencia switch Se trata de una alternativa a la bifurcación “if else” cuando se compara la misma expresión con distintos valores. Permite ejecutar una serie de operaciones para el caso de que una variable tenga un valor entero dado. La ejecución saltea todos los case hasta que encuentra uno con el valor de la variable, y ejecuta desde allí hasta el final del case o hasta que encuentre un break, en cuyo caso salta al final del case. El default permite poner una serie de instrucciones que se ejecutan en caso de que la igualdad no se dé para ninguno de los case. switch (expresión) { case (valor1): instrucciones_1; [break;] case (valor2): instrucciones_2; [break;] ….. case (valorN): instrucciones_N; [break;] default: instrucciones_por_defecto; }
switch (expression) { case value1: SENTENCIAS; case value2: SENTENCIAS; case value3: SENTENCIAS; case value4: SENTENCIAS; case value5: SENTENCIAS; case value6: SENTENCIAS; [default: statements7;] }
break; break; break; break; break; break;
Las características más relevantes de “switch” son las siguientes:
7.2.
1.
Cada sentencia case se corresponde con un único valor de expresión. No se pueden establecer rangos o condiciones, sino que se debe comparar con valores concretos.
2.
Los valores no comprendidos en ninguna sentencia case se pueden gestionar en default, que es opcional.
3.
En ausencia de “break”, cuando se ejecuta una sentencia case se ejecutan también todas las case que van a continuación, hasta que se llega a un “break” o hasta que se termina el “switch”.
Bucles
Un bucle se utiliza para realizar un proceso repetidas veces. Se denomina también lazo o “loop”. El código incluido entre las llaves {} (opcionales si el proceso repetitivo consta de una sola línea), se ejecutará mientras se cumpla unas determinadas condiciones. Hay que prestar especial atención a los bucles infinitos, hecho que ocurre cuando la condición de finalizar el bucle (Boolean expression) no se llega a cumplir nunca. Se trata de un fallo muy típico, habitual sobre todo entre programadores poco experimentados.
Anexo III 6-13
Desarrollo de Sistemas
A) Bucle while Permite ejecutar un grupo de instrucciones mientras se cumpla una condición dada: while (expresión_booleana) { instrucciones… } Por ejemplo: while ( linea != null) { linea = archivo.LeerLinea(); System.out.println(linea); } B) Bucle for La forma general del bucle for es la siguiente: for (initialization; booleanExpression; increment) { SENTENCIAS; } que es equivalente a utilizar “while” en la siguiente forma, initialization; while (booleanExpression) { SENTENCIAS; incremento; } La sentencia o sentencias “initialization” se ejecuta al comienzo del “for”, e incrementa después de “statements”. La Boolean expression se evalúa al comienzo de cada iteración; el bucle termina cuando la expresión de comparación toma el valor “false”. Cualquiera de las tres partes puede estar vacía. La initialization y el increment pueden tener varias expresiones separadas por comas. C) Bucle do while Es similar al bucle “while” pero con la particularidad de que el control está al final del bucle (lo que hace que el bucle se ejecute al menos una vez, independientemente de que la condición se cumpla o no). Una vez ejecutados las sentencias, se evalúa la condición: si resulta true se vuelven a ejecutar las
Anexo III 6-14
Lenguaje JAVA
sentencias incluidas en el bucle, mientras que si la condición se evalúa a “false” finaliza el bucle. Este tipo de bucles se utiliza con frecuencia para controlar la satisfacción de una determinada condición de error o de convergencia. do { instrucciones… } while (expresión_booleana); Por ejemplo: do { linea = archivo.LeerLinea(); if (linea != null) System.out.println(linea); } while (linea != null);
7.3.
Sentencias break, continue y return
La sentencia “break” es válida tanto para las bifurcaciones como para los bucles. Hace que se salga inmediatamente del bucle o bloque que se está ejecutando, sin realizar la ejecución del resto de las sentencias. La sentencia “continue” se utiliza en los bucles (no en bifurcaciones). Finaliza la iteración “i” que en ese momento se está ejecutando (no ejecuta el resto de sentencias que hubiera hasta el final del bucle). Vuelve al comienzo del bucle y comienza la siguiente iteración (i+1). A) Sentencias break y continue con etiquetas Las etiquetas permiten indicar un lugar donde continuar la ejecución de un programa después de un “break” o “continue”. El único lugar donde se pueden incluir etiquetas es justo delante de un bloque de código entre llaves {} (if, switch, do...while, while, for) y sólo se deben utilizar cuando se tiene uno o más bucles (o bloques) dentro de otro bucle y se desea salir (break) o continuar con la siguiente iteración (continue) de un bucle que no es el actual. lolo: for ( int i = 0, j = 0; i < 100; i++){ while ( true ) { if( (++j) > 5) { break lolo; } else { break; } } } Anexo III 6-15
Desarrollo de Sistemas
B) Sentencia return Otra forma de salir de un bucle (y de un método) es utilizar la sentencia “return”. A diferencia de “continue” o “break”, la sentencia “return” sale también del método o función. En el caso de que la función devuelva alguna variable, este valor se deberá poner a continuación del “return” (return value;). C) Sentencias de manejo de excepciones Cuando ocurre un error dentro de un método JAVA, el método puede lanzar una excepción para indicar a su llamador que ha ocurrido un error y que el error está utilizando la sentencia throw. El método llamador puede utilizar las sentencias try, catch, y finally para capturar y manejar la excepción. Puedes ver Manejar Errores Utilizando Excepciones para obtener más información sobre el lanzamiento y manejo de excepciones.
8.
Arrays y Cadenas
Al igual que otros lenguajes de programación, JAVA permite juntar y manejar múltiples valores a través de un objeto “array” (matriz). También se pueden manejar datos compuestos de múltiples caracteres utilizando el objeto String (cadena).
8.1.
Arrays
Como otras variables, antes de poder utilizar un array primero se debe declarar. De nuevo, al igual que otras variables, la declaración de un array tiene dos componentes primarios: el tipo del “array” y su nombre. Un tipo de array incluye el tipo de dato de los elementos que va contener el “array”. Por ejemplo, el tipo de dato para un “array” que sólo va a contener elementos enteros es un “array” de enteros. No puede existir un “array” de tipo de datos genérico en el que el tipo de sus elementos esté indefinido cuando se declara el “array”. Aquí tienes la declaración de un “array” de enteros: int[] arrayDeEnteros; La parte int[] de la declaración indica que arrayDeEnteros es un “array” de enteros. La declaración no asigna ninguna memoria para contener los elementos del “array”. Si se intenta asignar un valor o acceder a cualquier elemento de arrayDeEnteros antes de haber asignado la memoria para él, el compilador dará un error. Para asignar memoria a los elementos de un “array”, primero se debe ejemplarizar el “array”. Se puede hacer esto utilizando el operador new de JAVA (realmente, los pasos que se deben seguir para crear un array son similares a los se deben seguir para crear un objeto de una clase: declaración, ejemplarización e inicialización). La siguiente sentencia asigna la suficiente memoria para que arrayDeEnteros pueda contener diez enteros.
int[] arraydeenteros = new int[10]
Anexo III 6-16
Lenguaje JAVA
En general, cuando se crea un “array”, se utiliza el operador new, más el tipo de dato de los elementos del array, más el número de elementos deseados encerrado entre corchetes cuadrados (‘[‘ y ‘]’).
TipodeElemento[] NombredeArray = new TipodeElementos[tamanoArray] Los arrays pueden contener cualquier tipo de dato legal en JAVA incluyendo los tipos de referencia como son los objetos u otros “array”. Por ejemplo, el siguiente ejemplo declara un array que puede contener diez objetos String. String[] arrayDeStrings = new String[10]; Los elementos en este “array” son del tipo referencia, esto es, cada elemento contiene una referencia a un objeto String. En este punto, se ha asignado suficiente memoria para contener las referencias a los Strings, pero no se ha asignado memoria para los propios Strings. Si se intenta acceder a uno de los elementos de arraydeStrings obtendrá una excepción ‘NullPointerException’ porque el array está vacío y no contiene ni cadenas ni objetos String.
8.2.
Strings
Una secuencia de datos del tipo carácter se llama un string (cadena) y en el entorno JAVA está implementada por la clase String (un miembro del paquete java.lang).
String[] args; Este código declara explícitamente un “array”, llamado args, que contiene objetos del tipo String. Los corchetes vacíos indican que la longitud del “array” no se conoce en el momento de la compilación, porque el array se pasa en el momento de la ejecución. El segundo uso de String es el uso de cadenas literales (una cadena de caracteres entre comillas “y”):
«Hola mundo!» El compilador asigna implicitamente espacio para un objeto String cuando encuentra una cadena literal. Los objetos String son inmutables —es decir, no se pueden modificar una vez que han sido creados—. El paquete java.lang proporciona una clase diferente, StringBuffer, que se podrá utilizar para crear y manipular caracteres. JAVA concatena cadenas fácilmente utilizando el operador +.
9.
Clases en JAVA
Una clase es una agrupación de datos (variables o campos) y de funciones (métodos) que operan sobre esos datos. A estos datos y funciones pertenecientes a una clase se les denomina variables y métodos o funciones miembro. La programación orientada a objetos se basa en la programación de clases. Un programa se construye a partir de un conjunto de clases. Anexo III 6-17
Desarrollo de Sistemas
Una vez definida e implementada una clase, es posible declarar elementos de esta clase. Los elementos declarados de una clase se denominan objetos de la clase. De una clase se pueden declarar o crear numerosos objetos. La clase es lo genérico: es el patrón o modelo para crear objetos.
9.1.
El cuerpo de la clase
El cuerpo de la clase, encerrado entre { }, es la lista de atributos (variables) y métodos (funciones) que constituyen la clase. No es obligatorio, pero en general se listan primero los atributos y luego los métodos. La definición en plan muy general de una clase se realiza en la siguiente forma: [elementos] class nombre_clase [elementos] { [lista_de_atributos] [lista_de_métodos] } El esqueleto de cualquier aplicación JAVA se basa en la definición de una clase. Todos los datos básicos, como los enteros, se deben declarar en las clases antes de hacer uso de ellos. En la práctica son pocas las sentencias que se pueden colocar fuera del bloque de una clase. La palabra clave import (equivalente al #include) puede colocarse al principio de un fichero, fuera del bloque de la clase. Sin embargo, el compilador reemplazará esa sentencia con el contenido del fichero que se indique, que consistirá, como es de suponer, en más clases.
9.2.
Tipos de Clases Los tipos de clases que podemos definir son: •
abstract. Una clase abstract tiene al menos un método abstracto. Una clase abstracta no se instancia, sino que se utiliza como clase base para la herencia.
•
final. Una clase final se declara como la clase que termina una cadena de herencia. No se puede heredar de una clase final. Por ejemplo, la clase Math es una clase final.
•
public. Las clases public son accesibles desde otras clases, bien sea directamente o por herencia. Son accesibles dentro del mismo paquete en el que se han declarado. Para acceder desde otros paquetes, primero tienen que ser importadas.
•
synchronizable. Este modificador especifica que todos los métodos definidos en la clase son sincronizados, es decir, que no se puede acceder al mismo tiempo a ellos desde distintos ““threads””; el sistema se encarga de colocar los “flags” necesarios para evitarlo. Este mecanismo hace que desde ““threads”” diferentes se puedan modificar las mismas variables sin que haya problemas de que se sobreescriban.
Anexo III 6-18
Lenguaje JAVA
9.3.
Características de las clases 1.
Todas las variables y funciones de JAVA deben pertenecer a una clase. No hay variables y funciones globales.
2.
Si una clase deriva de otra (extends) hereda todas sus variables y métodos.
3.
JAVA tiene una jerarquía de clases estándar de la que pueden derivar las clases que crean los usuarios.
4.
Una clase sólo puede heredar de una única clase (en JAVA no hay herencia múltiple). Si al definir una clase no se especifica de qué clase deriva, por defecto la clase deriva de Object. La clase Object es la base de toda la jerarquía de clases de JAVA.
5.
En un fichero se pueden definir varias clases, pero en un fichero no puede haber más que una clase public. Este fichero se debe llamar como la clase public que contiene con extensión *.java.
6.
Si una clase contenida en un fichero no es public, no es necesario que el fichero se llame como la clase.
7.
Los métodos de una clase pueden referirse de modo global al objeto de esa clase al que se aplican por medio de la referencia “this”.
8.
Las clases se pueden agrupar en packages, introduciendo una línea al comienzo del fichero (package packageName;). Esta agrupación en packages está relacionada con la jerarquía de directorios y ficheros en la que se guardan las clases.
9.4.
Declaración de la clase
La clase se declara mediante la línea public class Contador. En el caso más general, la declaración de una clase puede contener los siguientes elementos: [public] [ final | abstract] class Clase [extends ClaseMadre] [implements Interfase1 [, Interfase2 ]…] O bien, para “interfaces”: [public] “interface” Interfase [extends InterfaseMadre1 [,InterfaseMadre2 ]…] Como se ve, lo único obligatorio es class y el nombre de la clase. Las interfases son un caso de clase particular que veremos más adelante. —
Extends. La instrucción extends indica de qué clase desciende la nuestra. Si se omite, JAVA asume que desciende de la superclase Object. Cuando una clase desciende de otra, esto significa que hereda sus atributos y sus métodos (es decir que, a menos que los redefinamos, sus métodos son los mismos que los de la clase madre y pueden utiliAnexo III 6-19
Desarrollo de Sistemas
zarse en forma transparente, a menos que sean privados en la clase madre o, para subclases de otros paquetes, protegidos o propios del paquete). —
Implements. Una interfase (“interface”) es una clase que declara sus métodos pero no los implementa; cuando una clase implementa (implements) una o más interfases, debe contener la implementación de todos los métodos (con las mismas listas de parámetros) de dichas interfases. Esto sirve para dar un ascendiente común a varias clases, obligándolas a implementar los mismos métodos y, por lo tanto, a comportarse de forma similar en cuanto a su interfase con otras clases y subclases.
—
Interface. Una interfase (“interface”), como se dijo, es una clase que no implementa sus métodos sino que deja a cargo la implementación a otras clases. Las interfases pueden, asimismo, descender de otras interfases pero no de otras clases. Todos sus métodos son por definición abstractos y sus atributos son finales (aunque esto no se indica en el cuerpo de la interfase). Son útiles para generar relaciones entre clases que de otro modo no están relacionadas (haciendo que implementen los mismos métodos), o para distribuir paquetes de clases indicando la estructura de la interfase pero no las clases individuales (objetos anónimos). Si bien diferentes clases pueden implementar las mismas interfases, y a la vez descender de otras clases, esto no es en realidad herencia múltiple ya que una clase no puede heredar atributos ni métodos de una “interface”; y las clases que implementan una interfase pueden no estar ni siquiera relacionadas entre sí.
En JAVA hay un montón de clases ya definidas y utilizables. Éstas vienen en las bibliotecas estándar: •
java.lang; clases esenciales, números, “strings”, objetos, compilador, “runtime”, seguridad y “threads” (es el único paquete que se incluye automáticamente en todo programa JAVA).
•
java.io; clases que manejan entradas y salidas.
•
java.util; clases útiles, como estructuras genéricas, manejo de fecha, hora y strings, números aleatorios, etc.
•
java.net; clases para soportar redes: URL, TCP, UDP, IP, etc.
•
java.awt; clases para manejo de “interface” gráfica, ventanas, etc.
•
java.awt.image; clases para manejo de imágenes.
•
java.awt.peer; clases que conectan la “interface” gráfica a implementaciones dependientes de la plataforma (motif, windows).
•
java.applet; clases para la creación de Applets y recursos para reproducción de audio.
Anexo III 6-20
Lenguaje JAVA
9.5.
Variables Miembro
Las variables declaradas en el cuerpo de la clase se dice que son miembros de la clase y son accesibles por todos los métodos de la clase; las Variables Miembro en la programación JAVA, son los datos o atributos que definen los objetos; cada objeto, es decir, cada ejemplar concreto de la clase, tiene su propia copia de las variables miembro. Un aspecto muy importante del correcto funcionamiento de los programas es que no haya datos sin inicializar. Por eso las variables miembro de tipos primitivos se inicializan siempre de modo automático, incluso antes de llamar al constructor (“false” para Boolean, el carácter nulo para char y cero para los tipos numéricos). De todas formas, lo más adecuado es inicializarlas también en el constructor. Las variables miembro pueden también inicializarse explícitamente en la declaración, como las variables locales, por medio de constantes o llamadas a métodos. Por Ejemplo: long nDatos = 100; Las variables miembro se inicializan en el mismo orden en que aparecen en el código de la clase. Esto es importante porque unas variables pueden apoyarse en otras previamente definidas. Cada objeto que se crea de una clase tiene su propia copia de las variables miembro. Por ejemplo, cada objeto de la clase Figura tiene sus propias coordenadas del centro “x” e “y”, y su propio valor del radio “r”. Los métodos de objeto se aplican a un objeto concreto poniendo el nombre del objeto y luego el nombre del método, separados por un punto. A este objeto se le llama argumento implícito. Por ejemplo, para calcular el área de un objeto de la clase Figura llamado obj1 se escribirá: obj1.area();. Las variables miembro del argumento implícito se acceden directamente o precedidas por la palabra this y el operador punto. Las variables miembro pueden ir precedidas en su declaración por uno de los modificadores de acceso: “public”, “private”, “protected” y “package (que es el valor por defecto y puede omitirse). Junto con los modificadores de acceso de la clase (“public” y “package”), determinan qué clases y métodos van a tener permiso para utilizar la clase y sus métodos y variables miembro. Existen otros dos modificadores (no de acceso) para las variables miembro: •
Transient: indica que esta variable miembro no forma parte de la persistencia (capacidad de los objetos de mantener su valor cuando termina la ejecución de un programa) de un objeto y por tanto no debe ser serializada (convertida en flujo de caracteres para poder ser almacenada en disco o en una base de datos) con el resto del objeto.
•
Volatile: indica que esta variable puede ser utilizada por distintas “threads” sincronizadas y que el compilador no debe realizar optimizaciones con esta variable. Anexo III 6-21
Desarrollo de Sistemas
9.6.
Variables miembro de clase (static)
Una clase puede tener variables propias de la clase y no de cada objeto. A estas variables se les llama variables de clase o variables static. Las variables static se suelen utilizar para definir constantes comunes para todos los objetos de la clase o variables que sólo tienen sentido para toda la clase. Las variables de clase son lo más parecido que JAVA tiene a las variables globales de C/C++. Las variables de clase se crean anteponiendo la palabra “static” a su declaración. Para llamarlas se suele utilizar el nombre de la clase (no es imprescindible, pues se puede utilizar también el nombre de cualquier objeto), porque de esta forma su sentido queda más claro. Si no se les da valor en la declaración, las variables miembro “static” se inicializan con los valores por defecto para los tipos primitivos (false para Boolean, el carácter nulo para “char” y cero para los tipos numéricos), y con “null” si es una referencia. Las variables miembro “static” se crean en el momento en que pueden ser necesarias: cuando se va a crear el primer objeto de la clase, en cuanto se llama a un método static o en cuanto se utiliza una variable “static” de dicha clase. Lo importante es que las variables miembro “static” se inicializan siempre antes que cualquier objeto de la clase.
9.7.
Variables Finales
Una variable de un tipo primitivo declarada como final no puede cambiar su valor a lo largo de la ejecución del programa. Puede ser considerada como una constante, y equivale a la palabra “const” de C/C++. JAVA permite separar la definición de la inicialización de una variable final. La inicialización puede hacerse más tarde, en tiempo de ejecución, llamando a métodos o en función de otros datos. La variable final así definida es constante (no puede cambiar), pero no tiene por qué tener el mismo valor en todas las ejecuciones del programa, pues depende de cómo haya sido inicializada. Además de las variables miembro, también las variables locales y los propios argumentos de un método pueden ser declarados final. Declarar como final un objeto miembro de una clase hace constante la referencia, pero no el propio objeto, que puede ser modificado a través de otra referencia. En JAVA no es posible hacer que un objeto sea constante.
9.8.
Métodos o funciones miembros
Los métodos son funciones definidas dentro de una clase. Salvo los métodos “static” o de clase, se aplican siempre a un objeto de la clase por medio del operador punto (.). Dicho objeto es su argumento implícito. Los métodos pueden además tener otros argumentos explícitos que van entre paréntesis, a continuación del nombre del método.
Anexo III 6-22
Lenguaje JAVA
La primera línea de la definición de un método se llama declaración o header; el código comprendido entre las llaves {…} es el cuerpo o “body” del método. Considérese el siguiente método tomado de la clase Figura: public Figura elMayor(Figura c) { if (this.r>=c.r) return this; else return c; } Los métodos tienen visibilidad directa de las variables miembro del objeto que es su argumento implícito, es decir, pueden acceder a ellas sin cualificarlas con un nombre de objeto y el operador punto (.). De todas formas, también se puede acceder a ellas mediante la referencia “this”, de modo discrecional (como en el ejemplo anterior con “this.r”) o si alguna variable local o argumento las oculta. El valor de retorno puede ser un valor de un tipo primitivo o una referencia. En cualquier caso no puede haber más que un único valor de retorno (que puede ser un objeto o un “array”). Se puede devolver también una referencia a un objeto por medio de un nombre de ““interface””. El objeto devuelto debe pertenecer a una clase que implemente esa “interface”. Los métodos pueden definir variables locales. Su visibilidad llega desde la definición al final del bloque en el que han sido definidas. No hace falta inicializar las variables locales en el punto en que se definen, pero el compilador no permite utilizarlas sin haberles dado un valor. A diferencia de las variables miembro, las variables locales no se inicializan por defecto.
9.9.
Paso de argumentos a métodos
En JAVA los argumentos de los tipos primitivos se pasan siempre por valor. El método recibe una copia del argumento actual; si se modifica esta copia, el argumento original que se incluyó en la llamada no queda modificado. La forma de modificar dentro de un método una variable de un tipo primitivo es incluirla como variable miembro en una clase y pasar como argumento una referencia a un objeto de dicha clase. Las referencias se pasan también por valor, pero a través de ellas se pueden modificar los objetos referenciados. En JAVA no se pueden pasar métodos como argumentos a otros métodos (en C/C++ se pueden pasar punteros a función como argumentos). Lo que se puede hacer en JAVA es pasar una referencia a un objeto y dentro de la función utilizar los métodos de ese objeto. Dentro de un método se pueden crear variables locales de los tipos primitivos o referencias. Estas variables locales dejan de existir al terminar la ejecución del método. Los argumentos formales de un método (las variables que aparecen en el header del método para recibir el valor de los argumentos actuales) tienen categoría de variables locales del método. Anexo III 6-23
Desarrollo de Sistemas
Si un método devuelve “this” (es decir, un objeto de la clase) o una referencia a otro objeto, ese objeto puede encadenarse con otra llamada a otro método de la misma o de diferente clase y así sucesivamente. En este caso aparecerán varios métodos en la misma sentencia unidos por el operador punto (.), por ejemplo:
String numeroComoString =”8.978”; float p = Float.valueOf(numeroComoString).floatValue(); Donde el método valueOf(String) de la clase java.lang.Float devuelve un objeto de la clase Flota sobre el que se aplica el método floatValue(), que finalmente devuelve una variable primitiva de tipo “float”. El ejemplo anterior se podía desdoblar en las siguientes sentencias:
String numeroComoString = ”8.978”; Float f = Float.valueOf(numeroComoString); float p = f.floatValue(); Obsérvese que se pueden encadenar varias llamadas a métodos por medio del operador punto (.) que, como todos los operadores de JAVA excepto los de asignación, se ejecuta de izquierda a derecha.
9.10. Métodos de clase (static) Análogamente, puede también haber métodos que no actúen sobre objetos concretos a través del operador punto. A estos métodos se les llama métodos de clase o “static”. Los métodos de clase pueden recibir objetos de su clase como argumentos explícitos, pero no tienen argumento implícito ni pueden utilizar la referencia this. Un ejemplo típico de métodos static son los métodos matemáticos de la clase java.lang.Math (sin(), cos(), exp(), pow(), etc.). De ordinario el argumento de estos métodos será de un tipo primitivo y se le pasará como argumento explícito. Estos métodos no tienen sentido como métodos de objeto. Los métodos y variables de clase se crean anteponiendo la palabra “static”. Para llamarlos se suele utilizar el nombre de la clase, en vez del nombre de un objeto de la clase (por ejemplo, Math.sin(ang), para calcular el seno de un ángulo). Los métodos y las variables de clase son lo más parecido que JAVA tiene a las funciones y variables globales de C/C++ o Visual Basic.
9.11. Llamadas a métodos public final class MiClase extends Number { // atributos: private float x; private float y; Anexo III 6-24
Lenguaje JAVA
// constructor: public MiClase(float rx, float iy) {x = rx;y = iy;} // métodos: public float Norma() {return (float)Math.sqrt(x*x+y*y);} public double doubleValue() {return (double)Norma( );} public float floatValue() {return Norma();} public int intValue() {return (int)Norma();} public long longValue() {return (long)Norma();} public String toString() {return «(«+x+»)+i(«+y+»)»;} }
Nombre_del_Objeto< .>Nombre_del_Método(parámetros) import java.io.*; public class Lolo { public static void main(String args[]) { MiClase obj = new MiClase(4,-3); System.out.println(obj.toString()); System.out.println(obj.Norma()); } } En la clase MiClase tenemos también un ejemplo de un llamado a un método de clase, o sea “static”:
return (float)Math.sqrt(x*x+y*y); Como el método es de clase, no hace falta llamarlo para un objeto en particular. En ese caso, en lugar del nombre de un objeto existente se puede utilizar directamente el nombre de la clase:
Nombre_de_la_ClaseNombre_del_Método(parámetros)
9.12. Constructores en JAVA Un punto clave de la Programación Orientada Objetos es el evitar información incorrecta por no haber sido correctamente inicializadas las variables. JAVA no permite que haya variables miembro que no estén inicializadas. JAVA inicializa siempre con valores por defecto las variables miembro de clases y objetos. El segundo paso en la inicialización correcta de objetos es el uso de constructores. Un constructor es un método que se llama automáticamente cada vez que se crea un objeto de una clase. La principal misión del constructor es reservar memoria e inicializar las variables miembro de la clase. Anexo III 6-25
Desarrollo de Sistemas
Los constructores no tienen valor de retorno (ni siquiera “void”) y su nombre es el mismo que el de la clase. Su argumento implícito es el objeto que se está creando. De ordinario una clase tiene varios constructores, que se diferencian por el tipo y número de sus argumentos. Se llama constructor por defecto al constructor que no tiene argumentos. El programador debe proporcionar en el código valores iniciales adecuados para todas las variables miembro. Un constructor de una clase puede llamar a otro constructor previamente definido en la misma clase por medio de la palabra “this”. En este contexto, la palabra “this” sólo puede aparecer en la primera sentencia de un constructor. El constructor de una sub-clase puede llamar al constructor de su súper-clase por medio de la palabra súper, seguida de los argumentos apropiados entre paréntesis. De esta forma, un constructor sólo tiene que inicializar por sí mismo las variables no heredadas. El constructor es tan importante que, si el programador no prepara ningún constructor para una clase, el compilador crea un constructor por defecto, inicializando las variables de los tipos primitivos a su valor por defecto, y los Strings y las demás referencias a objetos a “null”. Si hace falta, se llama al constructor de la súper-clase para que inicialice las variables heredadas. Al igual que los demás métodos de una clase, los constructores pueden tener también los modificadores de acceso “public”, “private”, “protected” y “package”. Si un constructor es private, ninguna otra clase puede crear un objeto de esa clase. En este caso, puede haber métodos “public” y “static” (factory methods) que llamen al constructor y devuelvan un objeto de esa clase. Dentro de una clase, los constructores sólo pueden ser llamados por otros constructores o por métodos “static”. No pueden ser llamados por los métodos de objeto de la clase.
9.13. Destrucción de Objetos En JAVA no hay destructores como en C++. El sistema se ocupa automáticamente de liberar la memoria de los objetos que ya han perdido la referencia, esto es, objetos que ya no tienen ningún nombre que permita acceder a ellos, por ejemplo por haber llegado al final del bloque en el que habían sido definidos, porque a la referencia se le ha asignado el valor “null” o porque a la referencia se le ha asignado la dirección de otro objeto. A esta característica de JAVA se le llama garbage collection (recogida de basura). En JAVA es normal que varias variables de tipo referencia apunten al mismo objeto. JAVA lleva internamente un contador de cuantas referencias hay sobre cada objeto. El objeto podrá ser borrado cuando el número de referencias sea cero. Como ya se ha dicho, una forma de hacer que un objeto quede sin referencia es cambiar ésta a “null”, haciendo por ejemplo: ObjetoRef = null; En JAVA no se sabe exactamente cuándo se va a activar el “garbage collector”. Si no falta memoria es posible que no se llegue a activar en ningún momento. No es pues conveniente confiar en él para la realización de otras tareas más críticas. Se puede llamar explícitamente al garbage collector con el método System.gc(), aunque esto es considerado por el sistema sólo como una “sugerencia” a la JVM.
Anexo III 6-26
Lenguaje JAVA
public class Contador { // Se define la clase Contador // Atributos int cnt; // Constructor (un metodo igual que otro cualquiera) public Contador() { cnt = 0; } // Métodos public int incCuenta() { cnt++; return cnt; } public int getCuenta() { return cnt; } } Cuando, desde una aplicación u otro objeto, se crea una instancia de la clase Contador mediante la instrucción new Contador() el compilador busca un método con el mismo nombre de la clase y que se corresponda con la llamada en cuanto al tipo y número de parámetros. Dicho método se llama Constructor, y una clase puede tener más de un constructor (no así un objeto o instancia, ya que una vez que fue creado no puede recrearse sobre sí mismo). En tiempo de ejecución, al encontrar dicha instrucción, el intérprete reserva espacio para el objeto/instancia, crea su estructura y llama al constructor. O sea que el efecto de “new Contador()” es, precisamente, reservar espacio para el contador e inicializarlo en cero. En cuanto a los otros métodos, se pueden llamar desde otros objetos (lo que incluye a las aplicaciones) del mismo modo que se llama una función desde C. Por ejemplo, usemos nuestro contador en un programa bien sencillo que nos muestre cómo evoluciona:
Anexo III 6-27
Desarrollo de Sistemas
import java.io.*; public class Lolo1 { static int n; static Contador obj; public static void main ( String args[] ) { System.out.println («Cuenta… «); obj = new Contador(); System.out.println (obj.getCuenta()); n = obj.incCuenta(); System.out.println (n); obj.incCuenta(); System.out.println (obj.getCuenta()); System.out.println (obj.incCuenta()); } }
import java.applet.*; import java.awt.*; public class Lolo2 extends Applet { static int n; static Contador obj; public Lolo2 () { obj = new Contador(); } public void paint (Graphics g) { g.drawString («Cuenta...», 20, 20); g.drawString (String.valueOf(obj.getCuenta()), 20, 35 ); n = obj.incCuenta(); g.drawString (String.valueOf(n), 20, 50 ); obj.incCuenta(); g.drawString (String.valueOf(obj.getCuenta()), 20, 65 ); g.drawString (String.valueOf(obj.incCuenta()), 20, 80 ); } }
Ahora es necesario crear una página HTML para poder visualizarlo. Para esto, crear y luego cargar el archivo Lolo2.htm con un “browser” que soporte JAVA (o bien ejecutar en la ventana DOS: «appletviewerLolo2.htm»): Lolo 2 - Applet Contador Observemos las diferencias entre la aplicación “standalone” y el Applet: —
La aplicación usa un método “main”, desde donde arranca.
—
El Applet, en cambio, se arranca desde un constructor (método con el mismo nombre que la clase).
—
En la aplicación utilizamos System.out.println para imprimir en la salida estándar.
—
En el applet necesitamos «dibujar» el texto sobre un fondo gráfico, por lo que usamos el método g.drawString dentro del método
Anexo III 6-28
Lenguaje JAVA
“paint” (que es llamado cada vez que es necesario redibujar el Applet). Con poco trabajo se pueden combinar ambos casos en un solo objeto, de modo que la misma clase sirva para utilizarla de las dos maneras: import java.applet.*; import java.awt.*; import java.io.*; public class Lolo3 extends Applet { static int n; static Contador obj; public Lolo3 () { obj = new Contador(); } public static void main(String args[]) { obj = new Contador(); paint(); } public static void paint () { System.out.println («Cuenta...»); System.out.println (obj.getCuenta()); n = obj.incCuenta(); System.out.println (n); obj.incCuenta(); System.out.println (obj.getCuenta()); System.out.println (obj.incCuenta()); } public void paint (Graphics g) { g.drawString («Cuenta...», 20, 20); g.drawString (String.valueOf(obj.getCuenta()), 20, 35 ); n = obj.incCuenta(); g.drawString (String.valueOf(n), 20, 50 ); obj.incCuenta(); g.drawString (String.valueOf(obj.getCuenta()), 20, 65 ); g.drawString (String.valueOf(obj.incCuenta()), 20, 80 ); } } Anexo III 6-29
Desarrollo de Sistemas
Esta clase puede ejecutarse tanto con «java Lolo3» en una ventana de MS-DOS, como cargarse desde una página HTML con:
Notar que conviene probar el Applet con el “appletviewer” («appletviewer Lolo3.htm»), ya que éste indica en la ventana DOS si hay algún error durante la ejecución. Los “browsers” dejan pasar muchos errores, simplemente suprimiendo la salida a pantalla del código erróneo. Fíjate que en todo este desarrollo de las clases Lolo1, Lolo2 y Lolo3, en ningún momento volvimos a tocar la clase Contador. Los métodos, como las clases, tienen una declaración y un cuerpo. La declaración es del tipo:
[private |protected|public] [static] [abstract] [final] [native ] [synchronized] TipoDevuelto NombreMétodo ([tipo1 nombre1[, tipo2 nombre2]…]) [throws excepción1 [,excepción2]…] Básicamente, los métodos son como las funciones de C: implementan, a través de funciones, operaciones y estructuras de control, el cálculo de algún parámetro que es el que devuelven al objeto que los llama. Sólo pueden devolver un valor (del tipo TipoDevuelto), aunque pueden no devolver ninguno (en ese caso TipoDevuelto es void). Como ya veremos, el valor de retorno se especifica con la instrucción return, dentro del método. Los métodos pueden utilizar valores que les pasa el objeto que los llama (parámetros), indicados con tipo1 nombre1, tipo2 nombre2… en el esquema de la declaración. Estos parámetros pueden ser de cualquiera de los tipos ya vistos. Si son tipos básicos, el método recibe el valor del parámetro; si son arrays, clases o interfases, recibe un puntero a los datos (referencia). Veamos un pequeño ejemplo: public int AumentarCuenta(int cantidad) { cnt = cnt + cantidad; return cnt; } Este método, si lo agregamos a la clase Contador, le suma cantidad al acumulador cnt. En detalle: —
el método recibe un valor entero (cantidad),
—
lo suma a la variable de instancia cnt,
—
devuelve la suma (return cnt).
Supongamos que queremos hacer un método dentro de una clase que devuelva la posición del mouse. Anexo III 6-30
Lenguaje JAVA
Lo siguiente no sirve: void GetMousePos(int x, int y) { x = ….; // esto no sirve! y = ….; // esto tampoco! } Porque el método no puede modificar los parámetros x e y (que han sido pasados por valor, o sea que el método recibe el valor numérico pero no sabe adónde están las variables en memoria). La solución es utilizar, en lugar de tipos básicos, una clase:
class MousePos { public int x, y; } Luego utilizar esa clase en nuestro método:
9.14. La clase MiClase public final class MiClase extends Number { private float x; private float y; public MiClase() {x = 0;y = 0;} public MiClase(float rx, float iy) {x = rx;y = iy;} public final float Norma() {return (float)Math.sqrt(x*x+y*y);} public final float Norma(MiClase c) {return (float)Math.sqrt(c.x*c.x+c.y*c.y);} public final MiClase Conjugado() {MiClase r = new MiClase(x,-y);return r;} public final MiClase Conjugado(MiClase c) {MiClase r = new MiClase(c.x,c.y);return r;} public final double doubleValue() {return (double)Norma();} public final float floatValue() {return Norma();} public final int intValue() {return (int)Norma();} public final long longValue() {return (long)Norma();} public final String toString() {
Anexo III 6-31
Desarrollo de Sistemas
f (y<0) return x+»-i»+(-y); else return x+»+i»+y; } public static final MiClase Suma(MiClase obj1, MiClase c2) { return new MiClase(obj1.x+c2.x,obj1.y+c2.y); } public static final MiClase Resta(MiClase obj1, MiClase c2) { return new MiClase(obj1.x-c2.x,obj1.y-c2.y); } public static final MiClase Producto(MiClase obj1, MiClase c2) { return new MiClase(obj1.x*c2.x-obj1.y*c2.y,obj1.x*c2.y+obj1.y*c2.x);} public static final MiClase DivEscalar(MiClase c, float f) {return new MiClase(c.x/ f,c.y/f);} public static final MiClase Cociente(MiClase obj1, MiClase c2) { float x = obj1.x*c2.x+obj1.y*c2.y; float y = -obj1.x*c2.y+obj1.y*c2.x; float n = c2.x*c2.x+c2.y*c2.y; MiClase r = new MiClase(x,y); return DivEscalar(r,n);} } Podemos hacer algunos comentarios: 1.
No hay “include” aquí, ya que la única biblioteca que usamos es java.lang y se incluye automáticamente.
2.
La clase es “public final”, lo que implica que cualquier clase en éste u otros paquetes puede utilizarla, pero ninguna clase puede heredarla (o sea que es una clase estéril...).
Hagamos un resumen de los atributos y métodos de la clase: // atributos: private float x; private float y; Siendo privados, no podemos acceder a ellos desde el exterior. Como además la clase es final, no hay forma de acceder “a” “x” e “y”. Además, al no ser “static”, cada instancia de la clase tendrá su propio “x” e “y”. // constructores: public MiClase() public MiClase(float rx, float iy) La clase tiene dos constructores, que se diferencian por su «firma» (signature), o sea por la cantidad y tipo de parámetros. El primero nos sirve para crear un objeto de tipo MiClase y valor indefinido (aunque en realidad el método lo inicializa en cero); con el segundo, podemos definir el valor al crearlo.
Anexo III 6-32
Lenguaje JAVA
// métodos: public final float Norma() public final float Norma(MiClase c) public final MiClase Conjugado() public final MiClase Conjugado(MiClase c) Estos métodos también son duales; cuando los usamos sin parámetros devuelven la norma o el conjugado del objeto individual (instancia): v = miMiClase.Norma(); // por ejemplo otroMiClase = miMiClase.Conjugado(); Con parámetros, en cambio, devuelven la norma o el conjugado del parámetro: v = unMiClase.Norma(miMiClase); otroMiClase = unMiClase.Conjugado(miMiClase); Notar que lo siguiente es inválido: otroMiClase = MiClase.Norma(miMiClase); // NO SE PUEDE! Porque el método no es static, por lo tanto debe llamarse para una instancia en particular (en este caso, unMiClase). // obligatorios (pues son abstractos en Number): public final double doubleValue() public final float floatValue() public final int intValue() public final long longValue() Estos métodos es obligatorio definirlos, ya que en la clase madre Number son métodos abstractos, o sea que debemos implementarlos aquí. Como todos los métodos de esta clase son final, o sea que no puede ser redefinido. No es importante en realidad puesto que la clase no puede tener descendientes... public final String toString() Este método nos sirve para representar el MiClase como una cadena de caracteres, de la forma x+iy. // Operaciones matemáticas public static final MiClase Suma(MiClase obj1, MiClase c2) public static final MiClase Resta(MiClase obj1, MiClase c2) public static final MiClase Producto(MiClase obj1, MiClase c2) public static final MiClase DivEscalar(MiClase c, float f) public static final MiClase Cociente(MiClase obj1, MiClase c2) Anexo III 6-33
Desarrollo de Sistemas
Aquí definimos varias operaciones matemáticas. Notar que se han definido como static, o sea que los métodos son únicos independientemente de las instancias. Esto permite que los podamos ejecutar sobre una instancia o directamente sobre la clase: miMiClase = unMiClase.Suma(comp1,comp2); // vale miMiClase = MiClase.Suma(comp1,comp2); // tambien seria correcto Por ejemplo, la siguiente aplicación nos muestra cómo podemos usar algunos de estos métodos: import java.io.*; public class Lolo5 { public static void main(String args[]) { MiClase obj1 = new MiClase(4,-3); System.out.println(obj1+»\tNorma=»+obj1.Norma()); MiClase c2 = new MiClase(-2,5); System.out.println(c2+»\tNorma=»+c2.Norma()+»\n»); System.out.println(«(«+obj1+»)/4 :»+MiClase.DivEscalar(obj1,4)); System.out.println(«Suma : «+MiClase.Suma(obj1,c2)); System.out.println(«Resta : «+MiClase.Resta(obj1,c2).toString()); System.out.println(«Multip: «+MiClase.Producto(obj1,c2).toString()); System.out.println(«Divis : «+MiClase.Cociente(obj1,c2).toString()); } } MiClase obj1 = new MiClase(4,-3); obj1 es un objetos (instancias) de la clase MiClase. El operador “new” ejemplariza una clase mediante la asignación de memoria para el objeto nuevo de ese “tipo. new” necesita un sólo argumento: una llamada al método constructor. Los métodos constructores son métodos especiales proporcionados por cada clase JAVA que son reponsables de la inicialización de los nuevos objetos de ese tipo. El operador new crea el objeto, el constructor lo inicializa.
new Rectangle(0, 0, 100, 200); En el ejemplo, Rectangle(0, 0, 100, 200) es una llamada al constructor de la clase Rectangle. El operador “new” devuelve una referencia al objeto recién creado. Esta referencia puede ser asignada a una variable del tipo apropiado. Anexo III 6-34
Lenguaje JAVA
Rectangle rect = new Rectangle(0, 0, 100, 200); (Recuerde que una clase escencialmente define un tipo de dato de referencia. Por eso, Rectangle puede utilizarse como un tipo de dato en los programas JAVA. El valor de cualquier variable cuyo tipo sea un tipo de referencia, es una referencia —un puntero— al valor real o conjunto de valores representado por la variable.) Como se dijo anteriormente, las clases porporcionan métodos constructores para incializar los nuevos objetos de ese tipo. Una clase podría proporcionar múltiples constructores para realizar diferentes tipos de inicialización en los nuevos objetos. Cuando vea la implementación de una clase, reconocerá los constructores porque tienen el mismo nombre que la clase y no tienen tipo de retorno. Recuerde la creacción del objeto Date en el sección inicial. El constructor utilizado no tenía ningún argumento: Date(). Un constructor que no tiene ningún argumento, como el mostrado arriba, es conocido como constructor por defecto. Al igual que Date, la mayoría de las clases tienen al menos un constructor, el constructor por defecto. Si una clase tiene varios constructores, todos ellos tienen el mismo nombre pero se deben diferenciar en el número o el tipo de sus argmentos. Cada constructor inicializa el nuevo objeto de una forma diferente. Junto al constructor por defecto, la clase Date proporciona otro constructor que inicializa el nuevo objeto con un nuevo año, mes y día: Date cumpleaños = new Date(1963, 8, 30); El compilador puede diferenciar los constructores a través del tipo y del número de sus argumentos. Para acceder a las variables de un objeto, sólo se tiene que añadir el nombre de la variable al del objeto referenciado introduciendo un punto en el medio (‘.’).
objetoReferenciado.variable Recuerde que el operador new devuelve una referencia a un objeto. Por eso, se puede utilizar el valor devuelto por new para acceder a las variables del nuevo objeto:
height = new Rectangle().height; Llamar a un método de un objeto es similar a obtener una variable del objeto. Para llamar a un método del objeto, simplemente se añade al nombre del objeto referenciado el nombre del método, separados por un punto (‘.’), y se proporcionan los argumentos del método entre paréntesis. Si el método no necesita argumentos, se utilizan los paréntesis vacios.
objetoReferenciado.nombreMétodo(listaArgumentos); o objetoReferenciado.nombreMétodo(); Recuerde que una llamada a un método es un mensaje al objeto nombrado. El objeto Referenciado en la llamada al método objetoReferenciado. Anexo III 6-35
Desarrollo de Sistemas
método() debe ser una referencia a un objeto. Como se puede utilizar un nombre de variable aquí, también se puede utilizar en cualquier expresión que devuelva una referencia a un objeto. Recuerda que el operador “new” devuelve una referencia a un objeto. Por eso, se puede utilizar el valor devuelto por “new” para acceder a las variables del nuevo objeto:
new Rectangle(0, 0, 100, 50).equals(anotherRect) La expresión new Rectangle(0, 0, 100, 50) evalúa a una referencia a un objeto que se refiere a un objeto Rectangle. Entonces, como verás, se puede utilizar la notación, de punto (‘.’) para llamar al método equals() del nuevo objeto Rectangle para determinar si el rectangúlo nuevo es igual al especificado en la lista de argumentos de equals().
9.15. Eliminar objetos no utilizados Muchos otros lenguajes orientados a objetos necesitan que se siga la pista de los objetos que se han creado y luego se destruyan cuando no se necesiten. Escribir código para manejar la memoria de esta es forma es aburrido y propenso a errores. JAVA permite ahorrarse esto, permitiendo crear tantos objetos como se quiera (solo limitados por los que el sistema pueda manejar) pero nunca tienen que ser destruidos. El entorno de ejecución JAVA borra los objetos cuando determina que no se van autilizar más. Este proceso es conocido como recolección de basura. Un objeto es elegible para la recolección de basura cuando no existen más referencias a ese objeto. Las referencias que se mantienen en una variable desaparecen de forma natural cuando la variable sale de su ámbito. O cuando se borra explícitamente un objeto referencia mediante la selección de un valor cuyo tipo de dato es una referencia a “null”.
9.16. Recolector de Basura El entorno de ejecución de JAVA tiene un recolector de basura que periódicamente libera la memoria ocupada por los objetos que no se van a necesitar más. El recolector de basura de JAVA es un barredor de marcas que escanea dinámicamente la memoria de JAVA buscando objetos, marcando aquellos que han sido referenciados. Después de investigar todos los posibles paths de los objetos, los que no están marcados (esto es, no han sido referenciados) se les conoce como basura y son eliminados. El colector de basura funciona en un “thread” (hilo) de baja prioridad y funciona tanto síncrona como asíncronamente dependiendo de la situación y del sistema en el que se esté ejecutando el entorno JAVA. El recolector de basura se ejecuta síncronamente cuando el sistema funciona fuera de memoria o en respuesta a una petición de un programa JAVA. Un programa JAVA le puede pedir al recolector de basura que se ejecute en cualquier momento mediante una llamada a System.gc().
Nota: que se ejecute el recolector de basura no garantiza que los objetos sean recolectados.
Anexo III 6-36
Lenguaje JAVA
En sistemas que permiten que el entorno de ejecución JAVA note cuando un “thread” a empezado a interrumpir a otro “thread” (como Windows 95/NT), el recolector de basura de JAVA funciona asíncromamente cuando el sistema está ocupado. Tan pronto como otro “thread” se vuelva activo, se pedirá al recolector de basura que obtenga un estado consistente y termine.
9.17. Finalización Antes de que un objeto sea recolectado, el recolector de basura le da una oportunidad para limpiarse él mismo mediante la llamada al método finalize() del propio objeto. Este proceso es conocido como finalización. Durante la finalización un objeto se podrían liberar los recursos del sistema como son los ficheros, etc., y liberar referencias en otros objetos para hacerse elegible por la recolección de basura. El método “finalize()” es un miembro de la clase java.lang.Object. Una clase debe sobreescribir el método “finalize()” para realizar cualquier finalización necesaria para los objetos de ese tipo.
9.18. This Normalmente, dentro del cuerpo de un método de un objeto se puede referir directamente a las variables miembros del objeto. Sin embargo, algunas veces no se querrá tener ambigüedad sobre el nombre de la variable miembro y uno de los argumentos del método que tengan el mismo nombre. Por ejemplo, el siguiente constructor de la clase HSBColor inicializa alguna variable miembro de un objeto de acuerdo a los argumentos pasados al constructor. Cada argumento del constructor tiene el mismo nombre que la variable del objeto cuyo valor contiene el argumento. class HSBColor { int luminosidad, saturacion, brillo; HSBColor (int luminosidad, int saturacion, int brillo) { this.luminosidad = luminosidad; this.saturacion = saturacion; this.brillo = brillo; } Se debe utilizar this en este constructor para evitar la ambigüedad entre el argumento luminosidad y la variable miembro luminosidad (y así con el resto de los argumentos). Escribir luminosidad = luminosidad no tendría sentido. Los nombres de argumentos tienen mayor precedencia y ocultan a los nombres de las variables miembro con el mismo nombre. Para referirise a la variable miembro se debe hacer explicitamente a través del objeto actual—this. También se puede utilizar this para llamar a uno de los métodos del objeto actual. Esto sólo es necesario si existe alguna ambigüedad con el nombre del método y se utiliza para intentar hacer el código más claro.
Anexo III 6-37
Desarrollo de Sistemas
9.19. SUPER Si el método oculta una de las variables miembro de la superclase, se puede referir a la variable oculta utilizando super. De igual forma, si el método sobreescribe uno de los métodos de la superclase, se puede llamar al método sobreescrito a través de super. Consideremos esta clase:
class MiClase { boolean unaVariable; void unMetodo() { unaVariable = true; } } y una subclase que oculta unaVariable y sobreescribe unMetodo():
class OtraClase extends MiClase { boolean unaVariable; void unMetodo() { unaVariable = false; super.unMetodo(); System.out.println(unaVariable); System.out.println(super.unaVariable); } } Primero unMetodo() selecciona unaVariable (una declarada en OtraClase que oculta a la declarada en MiClase) a false. Luego unMetodo() llama a su método sobreescrito con esta sentencia:
super.unMetodo(); Esto selecciona la versión oculta de unaVariable (la declarada en MiClase) a true. Luego unMetodo muestra las dos versiones de unaVariable con diferentes valores: false/true.
9.20. Miembros de la Clase y del Ejemplar Las variables de ejemplar están en contraste con las variables de clase (que se declaran utilizando el modificador static). El sistema asigna espacio para las variables de clase una vez por clase, sin importar el número de ejemplares creados de la clase. Todos los objetos creados de esta clase comparten la misma copia de las variables de clase de la clase, se puede acceder a las variables de clase a Anexo III 6-38
Lenguaje JAVA
través de un ejemplar o través de la propia clase. Los métodos son similares: una clase puede tener métodos de ejemplar y métodos de clase. Los métodos de ejemplar operan sobre las variables de ejemplar del objeto actual pero también pueden acceder a las variables de clase. Por otro lado, los métodos de clase no pueden acceder a las variables del ejemplar declarados dentro de la clase (a menos que se cree un objeto nuevo y acceda a ellos através del objeto). Los métodos de clase también pueden ser invocados desde la clase, no se necesita un ejemplar para llamar a los métodos de la clase. Para especificar que una variable miembro es una variable de clase, se utiliza la palabra clave static. Por ejemplo, cambiemos la clase UnEnteroLlamadoX para que su variable x sea ahora una variable de clase:
class UnEnteroLlamadoX { static int x; public int x() {
return x
}
public void setX(int newX) { x = newX;
}
} Ahora veamos el mismo código mostrado anteriormente que crea dos ejemplares de UnEnteroLlamadoX, selecciona sus valores de x, y muestra esta salida diferente: miX.x = 2 otroX.x = 2 La salida es diferente porque x ahora es una variable de clase por lo que solo hay una copia de la variable y es compartida por todos los ejemplares de UnEnteroLlamadoX incluyendo miX y otroX. Cuando se llama a setX() en cualquier ejemplar, cambia el valor de x para todos los ejemplares de UnEnteroLlamadoX. Las variables de clase se utilizan para aquellos puntos en lo que se necesite una sola copia que debe estar accesible para todos los objetos heredados por la clase en la que la variable fue declarada. Por ejemplo, las variables de clase se utilizan frecuentemente con final para definir constantes (esto es más eficiente en el consumo de memoria, ya que las constantes no pueden cambiar y sólo se necesita una copia). Similarmente, cuando se declare un método, se puede especificar que el método es un método de clase en vez de un método de ejemplar. Los métodos de clase sólo pueden operar con variables de clase y no pueden acceder a las variables de ejemplar definidas en la clase. Para especificar que un método es un método de clase, se utiliza la palabra clave static en la declaración de método. Cambiemos la clase UnEnteroLlamadoX para que su variable miembro x sea de nuevo una variable de ejemplar, y sus dos métodos sean ahora métodos de clase:
class UnEnteroLlamadoX { private int x; static public int x() { Anexo III 6-39
Desarrollo de Sistemas
return x; } static public void setX(int newX) { x = newX; } } Cuando se intente compilar esta versión de UnEnteroLlamadoX, se obtendrán errores de compilación:
UnEnteroLlamadoX.java:4: Can’t make a static reference to nonstatic variable x in class UnEnteroLlamadoX. return x; ^ UnEnteroLlamadoX.java:7: Can’t make a static reference to nonstatic variable x in class UnEnteroLlamadoX. x = newX; ^ 2 errors Esto es porque los métodos de la clase no pueden acceder a variables de ejemplar a menos que el método haya creado un ejemplar de UnEnteroLlamadoX primero y luego acceda a la variable a través de él. Construyamos de nuevo UnEnteroLlamadoX para hacer que su variable x sea una variable de clase:
class UnEnteroLlamadoX { static private int x; static public int x() { return x; static public void setX(int newX) {
} x = newX; }
} Ahora la clase se compilará y el código anterior que crea dos ejemplares de UnEnteroLlamadoX, selecciona sus valores x, y muestra en su salida los valores de x: miX.x = 2 otroX.x = 2 De nuevo, cambiar x a través de miX también lo cambia para los otros ejemplares de UnEnteroLlamadoX. Otra diferencia entre miembros del ejemplar y de la clase es que los miembros de la clase son accesibles desde la propia clase. No se necesita ejemplarizar la clase para acceder a los miembros de clase. Reescribamos el código anterior para acceder a x() y setX() directamente desde la clase UnEnteroLlamadoX:
... Anexo III 6-40
Lenguaje JAVA
UnEnteroLlamadoX.setX(1); System.out.println(«UnEnteroLlamadoX.x = « + UnEnteroLlamadoX.x()); ... Observe que ya no se tendrá que crear miX u otroX. Se puede seleccionar x y recuperarlo directamente desde la clase UnEnteroLlamadoX. No se puede hacer esto con miembros del ejemplar. Solo se puede invocar métodos de ejemplar a través de un objeto y solo puede acceder a las variables de ejemplar desde un objeto. Se puede acceder a las variables y métodos de clase desde un ejemplar de la clase o desde la clase misma.
9.21. Permisos de Acceso Cuando se crea una nueva clase en JAVA, se puede especificar el nivel de acceso que se quiere para las variables de instancia y los métodos definidos en la clase. Ciertas informaciones y peticiones contenidas en la clase, las soportadas por los métodos y variables accesibles públicamente en su objeto son correctas para el consumo de cualquier otro objeto del sistema. Otras peticiones contenidas en la clase son sólo para el uso personal de la clase. Estas otras soportadas por la operación de la clase no deberían ser utilizadas por objetos de otros tipos. Se querría proteger esas variables y métodos personales a nivel del lenguaje y prohibir el acceso desde objetos de otros tipos. En JAVA se puede utilizar los especificadores de acceso para proteger tanto las variables como los métodos de la clase cuando se declaran. El lenguaje JAVA soporta cuatro niveles de acceso para las variables y métodos miembros: “private”, “protected”, “public”, y, todavía no especificado, acceso de paquete. —
Public. Cualquier clase desde cualquier lugar puede acceder a las variables y métodos de instacia públicos.
public void CualquieraPuedeAcceder(){} —
Protected. Solo las subclases de la clase y nadie más puede acceder a las variables y métodos de instancia protegidos.
protected void SoloSubClases(){} —
Private. Las variables y métodos de instancia privados sólo pueden ser accedidos desde dentro de la clase. No son accesibles desde las subclases.
private String NumeroDelCarnetDeIdentidad; —
Friendly (sin declaración específica). Por defecto, si no se especifica el control de acceso, las variables y métodos de instancia se declaran friendly (amigas), lo que significa que son accesibles por todos los objetos dentro del mismo paquete, pero no por los externos al paquete. Es lo mismo que “protected”.
void MetodoDeMiPaquete(){} Anexo III 6-41
Desarrollo de Sistemas
Los métodos protegidos (protected) pueden ser vistos por las clases derivadas, como en C++, y también en JAVA, por los paquetes (packages). Todas las clases de un paquete pueden ver los métodos protegidos de ese paquete. Para evitarlo, se deben declarar como private protected, lo que hace que ya funcione como en C++ en donde solo se puede acceder a las variables y métodos protegidos de las clases derivadas. La siguiente tabla le muestra los niveles de acceso pemitidos por cada especificador: ESPECIFICADOR
CLASE
SUBCLASE
PAQUETE
private
X
protected
X
X*
X
public
X
X
X
package
X
MUNDO
X
X
La primera columna indica si la propia clase tiene acceso al miembro definido por el especificador de acceso. La segunda columna indica si las subclases de la clase (sin importar dentro de que paquete se encuentren éstas) tienen acceso a los miembros. La tercera columna indica si las clases del mismo paquete que la clase (sin importar su parentesco) tienen acceso a los miembros. La cuarta columna indica si todas las clases tienen acceso a los miembros. Observa que la intersección entre protected y subclase tiene un ‘*’ –este caso de acceso particular tiene una explicación en más detalle más adelante. Echemos un vistazo a cada uno de los niveles de acceso más detalladamente: A) Private El nivel de acceso más restringido es “private”. Un miembro privado es accesible sólo para la clase en la que está definido. Se utiliza este acceso para declarar miembros que sólo deben ser utilizados por la clase. Esto incluye las variables que contienen información que si se accede a ella desde el exterior podría colocar al objeto en un estado de inconsistencia, o los métodos que llamados desde el exterior pueden poner en peligro el estado del objeto o del programa donde se está ejecutando. Los miembros privados son como secretos, nunca deben contársele a nadie. B) Protected El siguiente especificador de nivel de acceso es ‘protected’ que permite a la propia clase, las subclases (con la excepción a la que nos referimos anteriormente), y todas las clases dentro del mismo paquete que accedan a los miembros. Este nivel de acceso se utiliza cuando es apropiado para una subclase da la clase tener acceso a los miembros, pero no las clases no relacionadas. Los miembros protegidos son como secretos familiares –no importa que toda la familia lo sepa, incluso algunos amigos allegados pero no se quiere que los extraños lo sepan. Anexo III 6-42
Lenguaje JAVA
C) Public El especificador de acceso más sencillo es “public”. Todas las clases, en todos los paquetes tienen acceso a los miembros públicos de la clase. Los miembros públicos se declaran sólo si su acceso no produce resultados indeseados si un extraño los utiliza. Aquí no importa que lo sepa todo el mundo. D) Acceso de Paquete Y finalmente, el último nivel de acceso es el que se obtiene si no se especifica ningún otro nivel de acceso a los miembros. Este nivel de acceso permite que las clases del mismo paquete que la clase tengan acceso a los miembros. Este nivel de acceso asume que las clases del mismo paquete son “amigas de confianza”.
10. Entrada / Salida En JAVA hay muchas clases para leer y escribir archivos (u otros dispositivos de E/S). Están reunidos en la biblioteca java.io. Vamos a empezar como siempre con un pequeño ejemplo funcional y en seguida nos meteremos en el necesario camino de las excepciones...
import java.io.*; public class Lolo9 { public static void main(String args[]) throws FileNotFoundException,IOException { FileInputStream fptr; DataInputStream f; String linea = null; fptr = new FileInputStream(«Lolo9.java»); f = new DataInputStream(fptr); do { linea = f.readLine(); f (linea!=null) System.out.println(linea); } while (linea != null); fptr.close(); } } El programa de ejemplo simplemente lee un archivo de texto y lo muestra en pantalla, algo así como el type del DOS o el cat de Unix. Dejemos por ahora el throws FileNotFoundException,IOException y vamos al código. Anexo III 6-43
Desarrollo de Sistemas
fptr = new FileInputStream(«Lolo9.java»); La clase FileInputStream (descendiente de InputStream) nos sirve para referirnos a archivos o conexiones (sockets) de una máquina. Podemos accederlos pasando un String como aquí, un objeto de tipo File o uno de tipo FileDescriptor, pero en esencia es lo mismo. Al crear un objeto de este tipo estamos «abriendo» un archivo, clásicamente hablando. Si el archivo no existe (por ejemplo reemplacen «Lolo9.java» por alguna otra cosa, como «noexiste.txt»), al ejecutarlo nos aparece un error:
C:\java\curso>java Lolo9 java.io.FileNotFoundException: noexiste.txt at java.io.FileInputStream.(FileInputStream.java:51) at Lolo9.main(Lolo9.java:9) La clase DataInputStream nos permite leer, en forma independiente del “hardware”, tipos de datos de una «corriente» (stream) que, en este caso, es un archivo. Es descendiente de FilterInputStream e implementa DataInput, una “interface”. Al crear un objeto de tipo DataInputStream lo referimos al archivo, que le pasamos como parámetro (fptr); esta clase tiene toda una serie de métodos para leer datos en distintos formatos. En nuestro programa usamos uno para leer líneas, que devuelve null cuando se llega al final del archivo o un String con el contenido de la línea: do { linea = f.readLine(); System.out.println(linea); } while (linea != null); Enseguida de leer la línea la imprimimos, y repetimos esto mientras no nos devuelva “null”. Al final, cerramos el archivo: fptr.close(); Tanto readLine como close pueden lanzar la excepción IOException, en caso de error de lectura o cierre de archivo. En realidad, podríamos no haber usado un DataInputStream y trabajar en forma más directa: import java.io.*; public class Lolo10 { public static void main(String args[]) throws FileNotFoundException,IOException { Anexo III 6-44
Lenguaje JAVA
FileInputStream fptr; int n; fptr = new FileInputStream(«Lolo9.java»); do { n = fptr.read(); if (n!=-1) System.out.print((char)n); } while (n!=-1); fptr.close(); } } Ya que la clase FileInputStream también dispone de métodos para leer el archivo. Sólo que son unos pocos métodos que nos permiten leer un entero por vez o un arreglo de “bytes”. DataInputStream tiene métodos para leer los datos de muchas formas distintas, y en general resulta más cómodo.
11. Herencia Herencia es el mecanismo por el que se crean nuevos objetos definidos en términos de objetos ya existentes. Por ejemplo, si se tiene la clase Mamífero, se puede crear la subclase Felino, que es una especialización de Mamífero. class Felino extends Mamífero { int numero_de_patas; } La palabra cl Mamífero extends se usa para generar una subclase (especialización) de un objeto. Una Felino es una subclase de Mamífero. Cualquier cosa que contenga la definición de Mamífero será copiada a la clase Felino; además, en Felino se pueden definir sus propios métodos y variables de instancia. Se dice que Felino deriva o hereda de Mamífero. Además, se pueden sustituir los métodos proporcionados por la clase base. Utilizando el ejemplo de MiClase, aquí hay un ejemplo de una clase derivada sustituyendo a la función Suma_a_i(): import MiClase; public class MiNuevaClase extends MiClase { public void Suma_a_i( int j ) { i = i + ( j/2 ); } } Anexo III 6-45
Desarrollo de Sistemas
Ahora, cuando se crea una instancia de MiNuevaClase, el valor de “i” también se inicializa a 10, pero la llamada al método Suma_a_i() produce un resultado diferente: MiNuevaClase mnc; mnc = new MiNuevaClase(); mnc.Suma_a_i( 10 ); En JAVA no se puede hacer herencia múltiple. Por ejemplo, de la clase aparato con motor y de la clase animal no se puede derivar nada, sería como obtener el objeto toro mecánico a partir de una máquina motorizada (aparato con motor) y un toro (animal). En realidad, lo que se pretende es copiar los métodos, es decir, pasar la funcionalidad del toro de verdad al toro mecánico, con lo cual no sería necesaria la herencia múltiple sino simplemente la compartición de funcionalidad que se encuentra implementada en JAVA a través de “interfaces”. En JAVA, como en otros lenguajes de programación orientados a objetos, las clases pueden derivar desde otras clases. La clase derivada (la clase que proviene de otra clase) se llama subclase. La clase de la que está derivada se denomina superclase. De hecho, en JAVA, todas las clases deben derivar de alguna clase. Lo que nos lleva a la cuestión ¿dónde empieza todo esto? La clase más alta, la clase de la que todas las demás descienden, es la clase Object, definida en java.lang. Object es la raíz de la herencia de todas las clases. Las subclases heredan el estado y el comportamiento en forma de las variables y los métodos de su superclase. La subclase puede utilizar los ítems heredados de su superclase tal y como son, o puede modificarlos o sobreescribirlos. Por eso, según se va bajando por el árbol de la herencia, las clases se convierten en más y más especializadas: Una subclase es una clase que desciende de otra clase. Una subclase hereda el estado y el comportamiento de todos sus ancestros. El término superclase se refiere a la clase que es el ancestro más directo, así como a todas las clases ascendentes.
11.1. Crear Subclases Se declara que un clase es una subclase de otra clase dentro de la declaración de Clase. Por ejemplo, supongamos que queremos crear una subclase llamada SubClase de otra clase llamada SuperClase. Se escribiría esto: class SubClass extends SuperClass { ... } Esto declara que SubClase es una subclase de SuperClase. Y también declara implícitamene que SuperClase es la superclase de SubClase. Una subclase también hereda variables y miembros de las superclases de su superclase, y así a lo largo del árbol de la herencia. Para hacer esta explicación un poco más sencilla, cuando este tutorial se refiere a la superclase de una clase significa el ancestro más directo de la clase así como a todas sus clases ascendentes.
Anexo III 6-46
Lenguaje JAVA
Una clase JAVA sólo puede tener una superclase directa. JAVA no soporta la herencia múltiple. Crear una subclase puede ser tan sencillo como incluir la clausula extends en la declaración de la clase. Sin embargo, normalmente se deberá realizar alguna cosa más cuando se crea una subclase, como sobreescribir métodos, etc...
11.2. ¿Qué variables miembro hereda una subclase? Una subclase hereda todas las variables miembros de su superclase que puedan ser accesibles desde la subclase (a menos que la variable miembro esté oculta en la subclase). Esto es, las subclases: •
Heredan aquellas variables miembros declaradas como public o protected.
•
Heredan aquellas variables miembros declaradas sin especificador de acceso (normalmente conocidas como «Amigas») siempre que la subclases esté en el mismo paquete que la clase.
•
No heredan las variables miembros de la superclase si la subclase declara una variable miembro que utiliza el mismo nombre. La variable miembro de la subclase se dice que oculta a la variable miembro de la superclase.
•
No heredan las variables miembro private.
11.3. Ocultar Variables Miembro Como se mencionó en la sección anterior, las variables miembros definidas en la subclase ocultan las variables miembro que tienen el mismo nombre en la superclase. Como esta característica del lenguaje JAVA es poderosa y conveniente, puede ser una fuente de errores: ocultar una variable miembro puede hacerse deliberadamente o por accidente. Entonces, cuando se nombren variables miembro se ha de ser cuidadoso y ocultar solo las variables miembro que realmente se desean ocultar. Una característica interesante de las variables miembro en JAVA es que una clase puede acceder a una variable miembro oculta a través de su superclase. Considere este pareja de superclase y subclase:
class Super { Number unNumero; } class Sub extends Super { Float unNumero; } La variable unNumero de Sub oculta a la variable unNumero de Super. Pero se puede acceder a la variable de la superclase utilizando: super.unNumero Anexo III 6-47
Desarrollo de Sistemas
Super es una palabra clave del lenguaje JAVA que permite a un método referirse a las variables ocultas y métodos sobreescritos de una superclase (ya la hemos estudiado en un apartado anterior en profundidad).
11.4. ¿Qué métodos hereda una Subclase? La regla que especifica los métodos heredados por una subclase es similar a la de las variables miembro. Una subclase hereda todos los métodos de sus superclase que son accesibles para la subclase (a menos que el método sea sobreescrito por la subclase). Esto es, una Subclase: •
Hereda aquellos métodos declarados como public o protected.
•
Hereda aquellos métodos sin especificador de acceso, siempre que la subclase esté en el mismo paquete que la clase.
•
No hereda un método de la superclase si la subclase declara un método que utiliza el mismo nombre. Se dice que el método de la subclase sobreescribe al método de la superclase.
•
No hereda los métodos private.
11.5. Sobreescribir Métodos La habilidad de una subclase para sobreescribir un método de su superclase permite a una clase heredar de su superclase aquellos comportamientos «más cercanos» y luego suplementar o modificar el comportamiento de la superclase. Una subclase puede sobreescribir completamente la implementación de un método heredado o puede mejorar el método añadiendole funcionalidad.
11.6. Redefinir el Método de una Superclase Algunas veces, una subclase querría reemplazar completamente la implementación de un método de su superclase. De hecho, muchas superclases proporcionan implementaciones de métodos vacías con la esperanza de que la mayoría, si no todas, de sus subclases reemplacen completamente la implementación de ese método. Un ejemplo de esto es el método run() de la clase Thread. La clase Thread proporciona una implementación vacía (el método no hace nada) para el método run(), porque por definición este método depende de la subclase. La clase Thread posiblemente no puede proporcionar una implementación medianamente razonable del método run(). Para reemplazar completamente la implementación de un método de la superclase, simplemente se llama a un método con el mismo nombre Anexo III 6-48
Lenguaje JAVA
que el del método de la superclase y se sobreescribe el método con la misma firma que la del método sobreescrito:
class ThreadSegundoPlano extends Thread { void run() { ... } } La clase ThreadSegundoPlano sobreescribe completamente el método run() de su superclase y reemplaza completamente su implementación.
11.7. Añadir Implementación a un método de la Superclase Otras veces una subclase querrá mantener la implememtación del método de su superclase y posteriormente ampliar algún comportamiento específico de la subclase. Por ejemplo, los métodos constructores de una subclase lo hacen normalmente —la subclase quiere preservar la inicialización realizada por la superclase, pero proporciona inicialización adicional específica de la subclase. Supongamos que queremos crear una subclase de la clase Windows del paquete java.awt. La clase Windows tiene un constructor que requiere un argumento del tipo Frame que es el padre de la ventana:
public Window(Frame parent) Este constructor realiza alguna inicialización en la ventana para que trabaje dentro del sistema de ventanas. Para asegurarnos de que una subclase de Window también trabaja dentro del sistema de ventanas, deberemos proporcionar un constructor que realice la misma inicialización. Mucho mejor que intentar recrear el proceso de inicialización que ocurre dentro del constructor de Windows, se podría utilizar lo que la clase Windows ya hace. Se puede utilizar el código del constructor de Windows llamándolo desde dentro del constructor de la subclase Window:
class Ventana extends Window { public Ventana(Frame parent) { super(parent); ... // Ventana especifica su inicialización aquí ... } }
Anexo III 6-49
Desarrollo de Sistemas
El constructor de Ventana llama primero al constructor de su superclase, y no hace nada más. Típicamente, éste es el comportamiento deseado de los constructores —las superclases deben tener la oportunidad de realizar sus tareas de inicialización antes que las de su subclase—. Otros tipos de métodos podrían llamar al constructor de la supeclase al final del método o en el medio.
11.8. Métodos que una subclase no puede sobrescibir Una subclase no puede sobrescribir métodos que hayan sido declarados como final en la superclase (por definición, los métodos finales no pueden ser sobrescritos). Si intentamos sobrescribir un método final, el compilador mostrará un mensaje similar a éste y no compilará el programa:
FinalTest.java:7: Final methods can’t be overriden. Method void iamfinal() is final in class ClassWithFinalMethod. void iamfinal() { ^ 1 error Para una explicación sobre los métodos finales, puede ver: escribir clases y métodos finales (epig. 11.10). Una subclase tampoco pude sobrescribir métodos que se hayan declarado como static en la superclase. En otras palabras, una subclase no puede sobrescribir un método de clase. Puede ver Miembros de la clase y del ejemplar (epig. 9.18) para obtener una explicación sobre los métodos de clase.
11.9. Métodos que una subclase debe sobrescribir Las subclases deben sobreescribir aquellos métodos que hayan sido declarados como abstract en la superclase, o la propia subclase debe ser abstracta. Escribir Clases y Métodos Abstractos explica con más detalle los métodos y clases abstractos.
11.10. Escribir clases y métodos finales A) Clases finales Se puede declarar que una clase sea final; esto es, que la clase no pueda tener subclases. Existen (al menos) dos razones por las que se querría hacer esto: razones de seguridad y de diseño. Un mecanismo que los hackers utilizan para atacar sistemas es crear subclases de una clase y luego sustituirla por el original. Las subclases parecen y sienten como la clase original pero hacen cosas bastante diferentes, probablemente causando daños u obteniendo información privada. Para prevenir esta Anexo III 6-50
Lenguaje JAVA
clase de subversión, se puede declarar que la clase sea final y así prevenir que se cree cualquier subclase. La clase String del paquete java.lang es una clase final sólo por esta razón. La clase String es tan vital para la operación del compilador y del intérprete que el sistema JAVA debe garantizar que siempre que un método o un objeto utilicen un String, obtenga un objeto java.lang.String y no algún otro string. Esto asegura que ningún string tendrá propiedades extrañas, inconsistentes o indeseables. Si se intenta compilar una subclase de una clase final, el compilador mostrará un mensaje de error y no compilará el programa. Además, los “bytescodes” verifican que no está teniendo lugar una subversión al nivel de “byte”, comprobando que una clase no es una subclase de una clase final. Otra razón por la que se podría querer declarar una clase final son razones de diseño orientado a objetos. Se podría pensar que una clase es «perfecta» o que, conceptualmente hablando, la clase no debería tener subclases. Para especificar que una clase es una clase final, se utiliza la palabra clave final antes de la palabra clave class en la declaración de la clase. Por ejemplo, si quisieramos declarar AlgoritmodeAjedrez como una clase final (perfecta), la declaración se parecería a esto:
final class AlgoritmodeAjedrez { ... } Cualquier intento posterior de crear una subclase de AlgoritmodeAjedrez resultará en el siguiente error del compilador:
Chess.java:6: Can’t subclass final classes: class AlgoritmodeAjedrez class MejorAlgoritmodeAjedrez extends AlgoritmodeAjedrez { ^ 1 error B) Métodos Finales Si la creacción de clases finales parece algo dura para nuestras necesidades, y realmente lo que se quiere es proteger algunos métodos de una clase para que no sean sobrescritos, se puede utilizar la palabra clave final en la declaración de método para indicar al compilador que este método no puede ser sobrescrito por las subclases. Se podría desear hacer que un método fuera final si el método tiene una implementación que no debe ser cambiada y que es crítica para el estado consistente del objeto. Por ejemplo, en lugar de hacer AlgoritmodeAjedrez como una clase final, podríamos hacer siguienteMovimiento() como un método final:
Anexo III 6-51
Desarrollo de Sistemas
class AlgoritmodeAjedrez { ... final void siguienteMovimiento(Pieza piezaMovida, PosicionenTablero nuevaPosicion) { } ... }
12. Clases abstractas Algunas veces, una clase que se ha definido representa un concepto abstracto y, como tal, no debe ser ejemplarizado. Por ejemplo, la comida en la vida real. ¿Ha visto algún ejemplar de comida? No. Lo que ha visto son ejemplares de manzanas, pan y chocolate. Comida representa un concepto abstracto de cosas que son comestibles. No tiene sentido que exista un ejemplar de comida. Similarmente, en la programación orientada a objetos se podría modelar conceptos abstractos pero no querer que se creen ejemplares de ellos. Por ejemplo, la clase Number del paquete java.lang representa el concepto abstracto de número. Tiene sentido modelar números en un programa, pero no tiene sentido crear un objeto genérico de números. En su lugar, la clase Number solo tiene sentido como superclase de otras clases como Integer y Float que implementan números de tipos específicos. Las clases como Number, que implementan conceptos abstractos y no deben ser ejemplarizadas, son llamadas clases abstractas. Una clase abstracta es una clase que solo puede tener subclases —no puede ser ejemplarizada—. Para declarar que una clase es un clase abstracta, se utiliza la palabra clave abstract en la declaración de la clase.
abstract class Number { ... } Si se intenta ejemplarizar una clase abstracta, el compilador mostrará un error similar a éste y no compilará el programa:
AbstractTest.java:6: class AbstractTest is an abstract class. It can’t be instantiated. new AbstractTest(); ^ 1 error
12.1. Métodos abstractos Una clase abstracta puede contener métodos abstractos, esto es, métodos que no tienen implementación. De esta forma, una clase abstracta puede definir un “interface” de programación completo, incluso porporciona a sus subclases Anexo III 6-52
Lenguaje JAVA
la declaración de todos los métodos necesarios para implementar el “interface” de programación. Sin embargo, las clases abstractas pueden dejar algunos detalles o toda la implementación de aquellos métodos a sus subclases. Veamos un ejemplo de cuándo sería necesario crear una clase abstracta con métodos abstractos: en una aplicación de dibujo orientada a objetos, se pueden dibujar círculos, rectángulos, líneas, etc. Cada uno de esos objetos gráficos comparten ciertos estados (posición, caja de dibujo) y comportamiento (movimiento, redimensionado, dibujo). Podemos aprovecharnos de esas similitudes y declararlos todos a partir de un mismo objeto padreObjetoGrafico. Sin embargo, los objetos gráficos también tienen diferencias sustanciales: dibujar un círculo es bastante diferente a dibujar un rectángulo. Los objetos gráficos no pueden compartir estos tipos de estados o comportamientos. Por otro lado, todos los ObjetosGraficos deben saber cómo dibujarse a sí mismos; se diferencian en cómo se dibujan unos y otros. Ésta es la situación perfecta para una clase abstracta. Primero se debe declarar una clase abstracta, ObjetoGrafico, para proporcionar las variables miembro y los métodos que van a ser compartidos por todas las subclases, como la posición actual y el método moverA(). También se deberían declarar métodos abstractos como dibujar(), que necesita ser implementado por todas las subclases, pero de manera completamente diferente (no tiene sentido crear una implementación por defecto en la superclase). La clase ObjetoGrafico se parecería a esto:
abstract class ObjetoGrafico { int x, y; ... void moverA(int nuevaX, int nuevaY) { ... } abstract void dibujar(); } Todas las subclases no abstractas de ObjetoGrafico como son Circulo o Rectangulo deberán proprocionar una implementación para el método dibujar(). class Circulo extends ObjetoGrafico { void dibujar() { ... } } class Rectangulo extends ObjetoGrafico { void dibujar() { ... } } Anexo III 6-53
Desarrollo de Sistemas
Una clase abstracta no necesita contener un método abstracto. Pero todas las clases que contengan un método abstracto o no proporcionen implemenación para cualquier método abstracto declarado en sus superclases debe ser declarada como una clase abstracta.
13. La clase Object La clase Object está situada en la parte más alta del árbol de la herencia en el entorno de desarrollo de JAVA. Todas las clases del sistema JAVA son descendentes (directos o indirectos) de la clase Object. Esta clase define los estados y comportamientos básicos que todos los objetos deben tener, como la posibilidad de compararse unos con otros, de convertirse a cadenas, de esperar una condición variable, de notificar a otros objetos que la condición variable ha cambiado y devolver la clase del objeto.
13.1. El método equals() Equals() se utiliza para comparar si dos objetos son iguales. Este método devuelve true si los objetos son iguales, o false si no lo son. Observe que la igualdad no significa que los objetos sean el mismo objeto. Consideremos este código que compara dos enteros:
Integer uno = new Integer(1), otroUno = new Integer(1); if (uno.equals(otroUno)) System.out.println(«Los objetos son Iguales»); Este código mostrará Los objetos son Iguales aunque uno y otroUno referencian a dos objetos distintos. Se les considera iguales porque su contenido es el mismo valor entero. Las clases deberían sobrescribir este método proporcionando la comprobación de igualdad apropiada. Un método equals() debería comparar el contenido de los objetos para ver si son funcionalmente iguales y devolver true si es así.
13.2. El método getClass() El método getClass() es un método final (no puede sobrescribirse) que devuelve una representación en tiempo de ejecución de la clase del objeto. Este método devuelve un objeto Class al que se le puede pedir varia información sobre la clase, como su nombre, el nombre de su superclase y los nombres de los “interfaces” que implementa. El siguiente método obtiene y muestra el nombre de la clase de un objeto:
void PrintClassName(Object obj) { System.out.println(«La clase del Objeto es « + obj.getClass().getName()); }
Anexo III 6-54
Lenguaje JAVA
Un uso muy manejado del método getClass() es crear un ejemplar de una clase sin conocer la clase en el momento de la compilación. Este método de ejemplo, crea un nuevo ejemplar de la misma clase que obj que puede ser cualquier clase heredada desde Object (lo que significa que podría ser cualquier clase):
13.3. El método toString() Este método devuelve una cadena de texto que representa al objeto. Se puede utilizar toString para mostrar un objeto. Por ejemplo, se podría mostrar una representación del Thread actual de la siguiente forma:
System.out.println(Thread.currentThread().toString()); System.out.println(new Integer(44).toString()); La representación de un objeto depende enteramente del objeto. El String de un objeto entero es el valor del entero mostrado como texto. El String de un objeto Thread contiene varios atributos sobre el “thread”, como su nombre y prioridad. Por ejemplo, las dos líneas anteriores darían la siguiente salida:
Thread[main,5,main] 4 El método toString() es muy útil para depuración y también puede sobreescribir este método en todas las clases.
14. Interface Un “interface” es una colección de definiciones de métodos (sin implementaciones) y de valores constantes. Los “interfaces” se utilizan para definir un protocolo de comportamiento que puede ser implementado por cualquier clase del árbol de clases. Los “interfaces” son útiles para: •
Capturar similitudes entre clases no relacionadas sin forzar una relación entre ellas.
•
Declarar métodos que una o varias clases necesitan implementar.
•
Revelar el “interface” de programación de un objeto sin recelar sus clases (los objetos de este tipo son llamados objetos anónimos y
Anexo III 6-55
Desarrollo de Sistemas
pueden ser útiles cuando compartas un paquete de clases con otros desarrolladores). En JAVA, un “interface” es un tipo de dato de referencia y, por tanto, puede utilizarse en muchos de los sitios donde se pueda utilizar cualquier tipo (como en un argumento de métodos y una declaración de variables). Podrá ver todo esto en Utilizar un Interface como un Tipo.
14.1. Los Interfaces no proporcionan herencia múltiple Algunas veces se tratra a los “interfaces” como una alternativa a la herencia múltiple en las clases. A pesar de que los “interfaces” podrían resolver algunos problemas de la herencia múltiple, son animales bastantes diferentes. En particular: •
No se pueden heredar variables desde un “interface”.
•
No se pueden heredar implementaciones de métodos desde un “interface”.
•
La herencia de un “interface” es independiente de la herencia de la clase —las clases que implementan el mismo “interface” pueden o no estar relacionadas a través del árbol de clases—.
14.2. Definir un Interface Para crear un “Interface”, se debe escribir tanto la declaración como el cuerpo del “interface”:
declaraciondeInterface { cuerpodeInterface } La Declaración de Interface declara varios atributos del “interface”, como su nombre o si se extiende desde otro “interface”. El Cuerpo de Interface contiene las constantes y las declaraciones de métodos del Interface.
14.3. La declaración de “interface” Como mínimo, una declaración de “interface” contiene la palabra clave “interface” y el nombre del “interface” que se va a crear:
“interface” Contable { ... }
Anexo III 6-56
Lenguaje JAVA
Por convención, los nombres de “interfaces” empiezan con una letra mayúscula al igual que las clases. Frecuentemente los nombres de “interfaces” terminan en «able» o «ible». Una declaración de “interface” puede tener otros dos componentes: el especificador de acceso public y una lista de «superinterfaces». Un “interface” puede extender otros “interfaces” como una clase puede extender o subclasificar otra case. Sin embargo, mientras que una clase sólo puede extender una superclase, los “interfaces” pueden extender de cualquier número de ““interfaces””. Así, una declaración completa de “interface” se parecería a esto:
[public] “interface” Nombredenterface [extends listadeSuperInterfaces] { ... } El especificador de acceso public indica que el “interface” puede ser utilizado por todas las clases en cualquier paquete. Si el “interface” no se especifica como público, solo será accesible para las clases definidas en el mismo paquete que el “interface”. La clausula extends es similar a la utilizada en la declaración de una clase; sin embargo, un “interface” puede extender varios “interfaces” (mientras una clase sólo puede extender una), y un “interface” no puede extender clases. Esta lista de superinterfaces es un lista delimitada por comas de todos los “interfaces” extendidos por el nuevo “interface”. Un “interface” hereda todas las constantes y métodos de sus superinterfaces a menos que el “interface” oculte una constante con el mismo nombre o redeclare un método con una nueva declaración.
14.4. El cuerpo del Interface El cuerpo del “interface” contiene las declaraciones de métodos para los métodos definidos en el “interface”. Implementar Métodos muestra cómo escribir una declaración de método. Además de las declaraciones del métodos, un “interface” puede contener declaraciones de constantes. En Declarar Variables Miembros existe más información sobre cómo construir una declaración de una variable miembro. Las declaraciones de miembros en un “interface” no permiten el uso de algunos modificadores y desaconsejan el uso de otros. No se podrán utilizar “transient”, “volatile”, o “synchronized” en una declaración de miembro en un “interface”. Tampoco se podrá utilizar los especificadores “private” y “protected” cuando se declaren miembros de un “interface”. Todos los valores constantes definidos en un “interfaces” son implicitamente públicos, estáticos y finales. El uso de estos modificadores en una declaración de constante en un “interface” está desaconsejado por falta de estilo. Similarmente, todos los métodos declarados en un “interface” son implícitamente públicos y abstractos. Este código define un nuevo “interface” llamado Coleccion que contiene un valor constante y tres declaraciones de métodos:
Anexo III 6-57
Desarrollo de Sistemas
“interface” Coleccion { int MAXIMO = 500; void añadir(Object obj); void borrar(Object obj); Object buscar(Object obj); int contadorActual(); } El “interface” anterior puede ser implementado por cualquier clase que represente una colección de objetos como pueden ser pilas, vectores, enlaces, etc... Observa que cada declaración de método está seguida por un punto y coma (;) porque un “interface” no proporciona implementación para los métodos declarados dentro de él.
14.5. Implementar un Interface Para utilizar un “interface” se debe escribir una clase que lo implemente. Una clase declara todos los “interfaces” que implementa en su declaración de clase. Para declarar que una clase implementa uno o más “interfaces”, se utiliza la palabra clave implements seguida por una lista delimitada por comas con los “interfaces” implementados por la clase. Por ejemplo, consideremos el “interface” Coleccion presentado en la página anterior. Ahora, supongamos que queremos escribir una clase que implemente un pila FIFO (primero en entrar, primero en salir). Como una pila FIFO contiene otros objetos tiene sentido que implemente el “interface” Coleccion. La clase PilaFIFO declara que implementa el “interface” Coleccion de esta forma:
int contadorActual() { ... } } Así se garantiza que proporciona implementación para los métodos añadir(), borrar(), buscar() y contadorActual(). Por convención, la cláusula implements sigue a la cláusula extends si es que ésta existe. Observe que las firmas de los métodos del “interface” Coleccion implementados en la clase PilaFIFO debe corresponder exactamente con las firmas de los métodos declarados en la “interface” Coleccion.
14.6. Utilizar un Interface como un Tipo Como se mencionó anteriormente, cuando se define un nuevo “interface”, en esencia se está definiendo un tipo de referencia. Se pueden utilizar los nombres de “interface” en cualquier lugar donde se usaría un nombre de dato de tipos primitivos o un nombre de datos del tipo de referencia. Por ejemplo, supongamos que se ha escrito un programa de hoja de cálculo que contiene un conjunto tabular de celdas y cada una contiene un valor. Querríamos poder poner cadenas, fechas, enteros, ecuaciones, en cada una de las celdas de la hoja. Para hacer esto, las cadenas, las fechas, los enteros y las ecuaciones tienen que implementar el mismo conjunto de métodos. Una forma de conseguir esto es encontrar el ancestro común de las clases e implementar ahí los métodos necesarios. Sin embargo, esto no es una solución práctica porque el ancestro común más frecuente es Object. De hecho, los objetos que puede poner en las celdas de su hoja de cálculo no están relacionadas entre sí, solo por la clase Object. Pero no puede modificar Object. Una aproximación podría ser escribir una clase llamada ValordeCelda que representara los valores que pudiera contener una celda de la hoja de cálculo. Entonces se podrían crear distintas subclases de ValordeCelda para las cadenas, los enteros o las ecuaciones. Además de ser mucho trabajo, esta aproximación arbitraria fuerza una relación entre esas clases que de otra forma no sería necesaria, y debería duplicar e implementar de nuevo clases que ya existen. Se podría definir un “interface” llamado CellAble que se parecería a esto:
Ahora, supongamos que existen objetos Línea y Columna que contienen un conjunto de objetos que implementan el “interface” CellAble. El método setObjectAt() de la clase Línea se podría parecer a esto:
class Línea { private CellAble[] contents; ... void setObjectAt(CellAble ca, int index) { ... } ... } Observe el uso del nombre del “interface” en la declaración de la variable miembro contents y en la declaración del argumento ca del método. Cualquier objeto que implemente el “interface CellAble”, sin importar que exista o no en el árbol de clases, puede estar contenido en el array contents y podría ser pasado al método setObjectAt().
15. Paquetes Los paquetes son grupos relacionados de clases e “interfaces” y proporcionan un mecanismo conveniente para manejar un gran juego de clases e “interfaces” y evitar los conflictos de nombres. Además de los paquetes de JAVA, se pueden crear paquetes propios y poner en ellos definiciones de clases y de “interfaces” utilizando la sentencia package. Supongamos que se está implementando un grupo de clases que representan una colección de objetos gráficos como círculos, rectángulos, líneas y puntos. Además de estas clases se debería escribir un “interface” Draggable para que las clases que lo implementen puedan moverse con el ratón. Si se quiere que estas clases estén disponibles para otros programadores, puedes empaquetarlas en un paquete, digamos, graphics y entregar el paquete a los programadores (junto con alguna documentación de referencia como qué hacen las clases y los “interfaces” y qué “interfaces” de programación son públicos). De esta forma, otros programadores pueden determinar fácilmente para qué es tu grupo de clases, cómo utilizarlos y cómo relacionarlos unos con otros, y con otras clases y paquetes. Los nombres de clases no tienen conflictos con los nombres de las clases de otros paquetes porque las clases y los “interfaces” dentro de un paquete son referenciados en términos de su paquete. Se declara un paquete utilizando la sentencia package:
package graphics; “interface” Draggable { } Anexo III 6-60
...
Lenguaje JAVA
class Circle {
...
} class Rectangle {
...
} La primera línea del código anterior crea un paquete llamado graphics. Todas las clases e “interfaces” definidas en el fichero que contiene esta sentencia son miembros del paquete. Por lo tanto, Draggable, Circle,y Rectangle son miembros del paquete graphics. Los ficheros .class generados por el compilador cuando se compila el fichero que contiene el fuente para Draggable, Circle y Rectangle debe situarse en un directorio llamado graphics en algún lugar se el path CLASSPATH. CLASSPATH es una lista de directorios que indica al sistema donde ha instalado varias clases e “interfaces” compiladas JAVA. Cuando busque una clase, el intérprete JAVA busca un directorio en su CLASSPATH cuyo nombre coincida con el nombre del paquete del que la clase es miembro. Los ficheros .class para todas las clases e “interfaces” definidas en un paquete deben estar en ese directorio de paquete. Los nombres de paquetes pueden contener varios componentes (separados por puntos). De hecho, los nombres de los paquetes de JAVA tienen varios componentes: java.util, java.lang, etc... Cada componente del nombre del paquete representa un directorio en el sistema de ficheros. Así, los ficheros .class de java.util están en un directorio llamado util en otro directorio llamado JAVA en algún lugar del CLASSPATH. Todas las clases e “interfaces” pertenecen a un paquete. Incluso si no especifica uno con la sentencia package. Si no se especifican las clases e “interfaces” se convierten en miembros del paquete por defecto, que no tiene nombre y que siempre es importado.
15.1. Utilizar Clases e Interfaces desde un Paquete Para importar una clase específica o un “interface” al fichero actual (como la clase Circle desde el paquete graphics creado en la sección anterior) se utiliza la sentencia de import:
import graphics.Circle; Esta sentencia debe estar al principio del fichero antes de cualquier definición de clase o de “interface” y hace que la clase o el “interface” esté disponible para su uso por las clases y los “interfaces” definidos en el fichero. Si se quiere importar todas las clases e “interfaces” de un paquete, por ejemplo, el paquete graphics completo, se utiliza la sentencia import con un caracter comodín, un asterisco ‘*’.
import graphics.*; Si intenta utilizar una clase o un “interface” desde un paquete que no ha sido importado, el compilador mostrará este error: Anexo III 6-61
Desarrollo de Sistemas
testing.java:4: Class Date not found in type declaration. Date date; ^ Observe que solo las clases e “intefaces” declarados como públicos pueden ser utilizados en clases fuera del paquete en el fueron definidos. El paquete por defecto (un paquete sin nombre) siempre es importado. El sistema de ejecución también importa automáticamente el paquete java.lang. Si, por suerte, el nombre de una clase de un paquete es el mismo que el nombre de una clase en otro paquete, se debe evitar la ambigüedad de nombres precediendo el nombre de la clase con el nombre del paquete. Por ejemplo, previamente se ha definido una clase llamada Rectangle en el paquete graphics. El paquete java.awt también contiene una clase Rectangle. Si estos dos paquetes son importados en la misma clase, el siguiente código sería ambigüo:
Rectangle rect; En esta situación se tiene que ser más específico e indicar exactamente qué clase Rectangle se quiere:
graphics.Rectangle rect; Se puede hacer esto anteponiendo el nombre del paquete al nombre de la clase y separando los dos con un punto.
15.2. Los paquetes de JAVA A) El paquete de lenguaje JAVA El paquete java.lang, contiene las clases principales de JAVA, y se importa automáticamente: —
Object. El “abuelo” de todas las clases —la clase de la que parten todas las demás. Esta clase se cubrió anteriormente en la lección La Clase Object.
—
Tipos de Datos Encubiertos. Una colección de clases utilizadas para encubrir variables de tipos primitivos: Boolean, Character, Double, Float, Integer y Long. Cada una de estas clases es una subclase de la clase abstracta Number.
—
Strings. Dos clases que implementan los datos de caracteres. Las Clases String y StringBuffer es una lección donde conocerá el uso de estos dos tipos de Strings.
—
System y Runtime. Estas dos clases permiten a los programas utilizar los recursos del sistema. System proporciona un “interface” de
Anexo III 6-62
Lenguaje JAVA
programación independiente del sistema para recursos del sistema y Runtime da acceso directo al entorno de ejecución específico de un sistema. Utilizar Recursos del Sistema describe las clases System y Runtime y sus métodos. —
Thread. Las clases Thread, ThreadDeath y ThreadGroup implementan las capacidades multitareas tan importantes en el lenguaje JAVA. El paquete java.lang también define el “interface” Runnable. Este “interface” es conveniente para activar la clase JAVA sin subclasificar la clase Thread. A través de un ejemplo de aproximación Threads de Control enseñará los Threads JAVA.
—
Class. La clase Class proporciona una descripción en tiempo de ejecución de una clase y la clase ClassLoader permite cargar clases en los programas durante la ejecución.
—
Math. Una librería de rutinas y valores matemáticos como pi.
—
Exceptions, Errors y Throwable. Cuando ocurre un error en un programa JAVA, el programa lanza un objeto que indica qué problema era y el estado del intérprete cuando ocurrió el error. Solo los objetos derivados de la clase Throwable puden ser lanzados. Existen dos subclasses principales de Throwable: Exception y Error. Exception es la forma que deben intentar capturar los programas normales. Error se utiliza para los errores catastróficos —los programas normales no capturan Errores—. El paquete java.lang contiene las clases Throwable, Exception y Error, y numerosas subclases de Exception y Error que representan problemas específicos. Manejo de Errores Utilizando Excepciones muestra cómo utilizar las excepciones para manejar errores en sus programas JAVA.
—
Process. Los objetos Process representa el proceso del sistema que se crea cuando se utiliza el sistema en tiempo de ejecución para ejecutar comandos del sistema. El paquete java.lang define e implementa la clase genérica Process.
B) El Paquete I/O de JAVA El paquete I/O de JAVA (java.io) proporciona un juego de canales de entrada y salida utilizados para leer y escribir ficheros de datos y otras fuentes de entrada y salida. Las clases e “interfaces” definidos en java.io se cubren completamente en Canales de Entrada y Salida. C) El paquete de utilidades de JAVA Este paquete, java.util, contiene una colección de clases útiles. Entre ellas se encuentan muchas estructuras de datos genéricas (Dictionary, Stack, Vector, Hashtable), un objeto muy útil para dividir cadenas y otro para la manipualción de calendarios. El paquete java.util también contiene el “interface” Observer y la clase Observable que permiten a los objetos notificarse unos a otros cuando han cambiado.
Anexo III 6-63
Desarrollo de Sistemas
D) El paquete de red de JAVA El paquete java.net contiene definiciones de clases e “interfaces” que implementan varias capacidades de red. Las clases de este paquete incluyen una clase que implementa una conexión URL. Se puede utilizar estas clases para implementar aplicaciones cliente-servidor y otras aplicaciones de comunicaciones. Conectividad y Seguridad del Cliente tiene varios ejemplos de utilización de estas clases, incluyendo un ejemplo cliente-servidor que utiliza datagramas. E) El paquete Applet Este paquete contiene la clase Applet —la clase que se debe subclasificar si se quiere escribir un applet—. En este paquete se incluye el “interface” AudioClip que proporciona una abstración de alto nivel para audio.Escribir Applets. F) Paquetes de herramientas para ventanas abstractas Tres paquetes componen las herramientas para Ventanas Abstractas: java.awt, java.awt.image, y java.awt.peer. —
El paquete AWT. El paquete java.awt proporciona elementos GUI utilizados para obtener información y mostrarla en la pantalla como ventanas, botones, barras de desplazamiento, etc.
—
El paquete AWT Image. El paquete java.awt.image contiene clases e “interfaces” para manejar imágenes de datos, como la selección de un modelo de color, el cortado y pegado, el filtrado de colores, la selección del valor de un píxel y la grabación de partes de la pantalla.
—
El paquete AWT Peer. El paquete java.awt.peer contiene clases e “interfaces” que conectan los componentes AWT independientes de la plataforma a su implementación dependiente de la plataforma (como son los controles de Microsoft Windows).
16. Excepciones En el lenguaje JAVA, una Exception es un cierto tipo de error o una condición anormal que se ha producido durante la ejecución de un programa. JAVA implementa excepciones dándole al usuario la oportunidad de corregir el error de forma que se pueden capturar y recuperar. JAVA incorpora en el propio lenguaje la gestión de errores. El mejor momento para detectar los errores es durante la compilación. Sin embargo, prácticamente solo los errores de sintaxis son detectados en esta operación. El resto de problemas surgen durante la ejecución de los programas. Algunas excepciones son fatales y provocan que se deba finalizar la ejecución del programa. En este caso conviene terminar ordenadamente y dar un Anexo III 6-64
Lenguaje JAVA
mensaje explicando el tipo de error que se ha producido. Otras excepciones, como por ejemplo no encontrar un fichero en el que hay que leer o escribir algo, pueden ser recuperables. En este caso el programa debe dar al usuario la oportunidad de corregir el error (definiendo por ejemplo un nuevo path del fichero no encontrado). Los errores se representan mediante clases derivadas de la clase Throwable, pero los que tiene que chequear un programador derivan de Exception (java.lang.Exception que a su vez deriva de Throwable). Existen algunos tipos de excepciones que JAVA obliga a tener en cuenta. Esto se hace mediante el uso de bloques “try”, “catch” y “finally”. • Bloque try {...} catch {...} finally {...} El código dentro del bloque “try” está “vigilado”. Si se produce una situación anormal y se lanza como consecuencia una excepción, el control pasa al bloque “catch”, que se hace cargo de la situación y decide lo que hay que hacer. Se pueden incluir tantos bloques “catch” como se desee, cada uno de los cuales tratará un tipo de excepción. Finalmente, si está presente, se ejecuta el bloque “finally”, que es opcional, pero que en caso de existir se ejecuta siempre, sea cual sea el tipo de error. En el caso en que el código de un método pueda generar una Exception y no se desee incluir en dicho método la gestión del error (es decir los bucles try/catch correspondientes), es necesario que el método pase la Exception al método desde el que ha sido llamado. Esto se consigue mediante la adición de la palabra “throws” seguida del nombre de la Exception concreta, después de la lista de argumentos del método. A su vez el método superior deberá incluir los bloques “try/catch” o volver a pasar la Exception. De esta forma se puede ir pasando la Exception de un método a otro hasta llegar al último método del programa, el método main(). import java.io.*; public class Lolo { public static void main(String args[]) { FileInputStream fptr; DataInputStream f; String linea = null; try { fptr = new FileInputStream(args[0]); f = new DataInputStream(fptr); do { linea = f.readLine(); if (linea!=null) System.out.println(linea); } while (linea != null);
Anexo III 6-65
Desarrollo de Sistemas
fptr.close(); } catch (FileNotFoundException e) { System.out.println(«Hey, ese archivo no existe!\n»); } catch (IOException e) { System.out.println(«Error de E/S!\n»); } } } También hicimos un cambio para elegir el archivo a imprimir desde la línea de comandos, en lugar de entrarlo fijo, utilizando para eso el argumento del método main(arg[]), que consiste en una lista de Strings con los parámetros que se pasan en la línea a continuación de java nombre_programa. Por ejemplo, si llamamos a este programa con:
java Lolo archi.txt otro.xxx arg[0] contendrá «archi.txt», arg[1] contendrá «otro.xxx», y así sucesivamente. Por supuesto, si llamamos a Lolo sin parámetros se lanzará otra excepción al intentar accederlo:
C:\java\curso>java Lolo java.lang.ArrayIndexOutOfBoundsException: 0 at Lolo.main(Lolo.java:10) La cláusula try engloba una parte del programa donde se pueden lanzar excepciones. Si una excepción se produce, JAVA busca una instrucción catch (nombre_de_la_excepción variable), y, si la encuentra, ejecuta lo que ésta engloba. Si no encuentra un catch para esa excepción, para el programa y muestra el error que se produjo. Por ejemplo, para evitar este último error bastaría con agregar:
catch (ArrayIndexOutOfBoundsException e) { System.out.println(«Debe ingresar un nombre de archivo!»); System.out.println(«Ej.: java Lolo pepe.txt»); } Hay que notar que cuando se lanza una excepción el programa igual se detiene, porque el código que sigue al lanzamiento de la excepción no se ejecuta. Más adelante se verá cómo se comporta esto en un objeto que fue creAnexo III 6-66
Lenguaje JAVA
ado por otro, y cómo usar la instrucción “finally” para poner una parte de código que se ejecute pase lo que pase.
16.1. Manejo de errores utilizando excepciones Existe una regla de oro en el mundo de la programación: en los programas ocurren errores. Esto es sabido. Pero, ¿qué sucede realmente después de que ha ocurrido el error? ¿Cómo se maneja el error? ¿Quién lo maneja? ¿Puede recuperarlo el programa? El lenguaje JAVA utiliza excepciones para proporcionar capacidades de manejo de errores. En este apartado se explicará qué es una excepción, cómo lanzar y capturar excepciones, qué hacer con una excepción una vez capturada, y cómo hacer un mejor uso de las excepciones heredadas de las clases proporcionadas por el entorno de desarrollo de JAVA. Una excepción es un evento que ocurre durante la ejecución del programa que interrumpe el flujo normal de las sentencias. Muchas clases de errores pueden utilizar excepciones —desde serios problemas de “hardware”, como la avería de un disco duro, a los simples errores de programación, como tratar de acceder a un elemento de un array fuera de sus límites—. Cuando dicho error ocurre dentro de un método JAVA, el método crea un objeto ‘exception’ y lo maneja fuera, en el sistema de ejecución. Este objeto contiene información sobre la excepción, incluyendo su tipo y el estado del programa cuando ocurrió el error. El sistema de ejecución es el responsable de buscar algún código para manejar el error. En terminología JAVA, crear una objeto exception y manejarlo por el sistema de ejecución se llama lanzar una excepción. Después de que un método lance una excepción, el sistema de ejecución entra en acción para buscar el manejador de la excepción. El conjunto de «algunos» métodos posibles para manejar la excepción es el conjunto de métodos de la pila de llamadas del método donde ocurrió el error. El sistema de ejecución busca hacia atrás en la pila de llamadas, empezando por el método en el que ocurrió el error, hasta que encuentra un método que contiene el «manejador de excepción» adecuado. Un manejador de excepción es considerado adecuado si el tipo de la excepción lanzada es el mismo que el de la excepción manejada por el manejador. Así, la excepción sube sobre la pila de llamadas hasta que encuentra el manejador apropiado y una de las llamadas a métodos maneja la excepción, se dice que el manejador de excepción elegido captura la excepción. Si el sistema de ejecución busca exhaustivamente por todos los métodos de la pila de llamadas sin encontrar el manejador de excepción adecuado, el sistema de ejecución finaliza (y, consecuentemente, el programa JAVA también). Mediante el uso de excepciones para manejar errores, los programas JAVA tienen las siguientes ventajas frente a las técnicas de manejo de errores tradicionales: —
Ventaja 1: Separar el Manejo de Errores del Código «Normal».
—
Ventaja 2: Propagar los Errores sobre la Pila de Llamadas. Anexo III 6-67
Desarrollo de Sistemas
—
Ventaja 3: Agrupar los Tipos de Errores y la Diferenciación de éstos.
16.2. La sentencia throw Todos los métodos JAVA utilizan la sentencia throw para lanzar una excepción. Esta sentencia requiere un solo argumento: un objeto Throwable. En el sistema JAVA, los objetos lanzables son ejemplares de la clase Throwable definida en el paquete java.lang. Aquí tiene un ejemplo de la sentencia throw:
throw algunObjetoThrowable;
16.3. La cláusula throws Habrá observado que la declaración del método pop() contiene esta cláusula:
throws EmptyStackException La cláusula throws especifica que el método puede lanzar una excepción EmptyStackException. Como ya sabe, el lenguaje JAVA requiere que los métodos capturen o especifiquen todas las excepciones chequeadas que puedan ser lanzadas dentro de su ámbito. Se puede hacer esto con la cláusula throws de la declaración del método.
16.4. La clase Throwable y sus subclases Como se aprendió en la página anterior, sólo se pueden lanzar objetos que estén derivados de la clase Throwable. Esto incluye descendientes directos (esto es, objetos de la clase Throwable) y descendiente indirectos (objetos derivados de hijos o nietos de la clase Throwable). Este diagrama ilustra el árbol de herencia de la clase Throwable y sus subclases más importantes:
Anexo III 6-68
Lenguaje JAVA
Object
Throwable
Exception Error
Runtime Exception
... ...
...
Como se puede ver en el diagrama, la clase Throwable tiene dos descendientes directos: Error y Exception. A) Error Cuando falla un enlace dinámico, y hay algún fallo «hardware» en la máquina virtual, ésta lanza un error. Típicamente los programas JAVA no capturan los errores. Pero siempre lanzarán errores. B) Exception La mayoría de los programas lanzan y capturan objetos derivados de la clase Exception. Una excepción indica que ha ocurrido un problema, pero que el problema no es demasiado serio. La mayoría de los programas que se escribirán lanzarán y capturarán excepciones. La clase Exception tiene muchos descendiente definidos en los paquetes JAVA. Estos descendientes indican varios tipos de excepciones que pueden ocurrir. Por ejemplo, IllegalAccessException señala que no se puede encontrar un método particular, y NegativeArraySizeException indica que un programa intenta crear un array con tamaño negativo. Una subclase de Exception tiene un significado especial en el lenguaje JAVA: RuntimeException. • Excepciones en Tiempo de Ejecución La clase RuntimeException representa las excepciones que ocurren dentro de la máquina virtual JAVA (durante el tiempo de ejecución). Un ejemplo de Anexo III 6-69
Desarrollo de Sistemas
estas excepciones es NullPointerException, que ocurre cuando un método intenta acceder a un miembro de un objeto a través de una referencia nula. Esta excepción puede ocurrir en cualquier lugar en que un programa intente desreferenciar una referencia a un objeto. Frecuentemente el coste de chequear estas excepciones sobrepasa los beneficios de capturarlas. Como las excepciones en tiempo de ejecución están omnipresentes e intentar capturar o especificarlas todas en todo momento podrían ser un ejercicio infructuoso (y un código infructuoso, imposible de leer y de mantener), el compilador permite que estas excepciones no se capturen ni se especifiquen. Los paquetes JAVA definen varias clases RuntimeException. Se pueden capturar estas excepciones al igual que las otras. Sin embargo, no se requiere que un método especifique que lanza excepciones en tiempo de ejecución. Además puedes crear sus propias subclases de RuntimeException.
17. HILOs - Threads 17.1. ¿Qué es un Thread? Un “thread”, por sí mismo, no es un programa. No puede ejecutarse por sí mismo, pero sí con un programa. Un “thread” es un flujo secuencial de control dentro de un programa. No hay nada nuevo en el concepto de un solo “thread”. Pero el juego real alrededor de los “threads” no está sobre los “threads” secuenciales solitarios, sino sobre la posibilidad de que un solo programa ejecute varios “threads” a la vez y que realicen diferentes tareas. El navegador HotJAVA es un ejemplo de una aplicación multi-“thread”. Dentro del navegador HotJAVA puede moverse por la página mientras baja un applet o una imagen, se ejecuta una animación o escucha un sonido, imprime la página en segundo plano mientras descarga una nueva página, o ve cómo los tres algoritmos de ordenación alcanzan la meta. Algunos textos utilizan el nombre proceso de poco peso en lugar de “thread”. Un “thread” es similar a un proceso real en el que un “thread” y un programa en ejecución son un solo flujo secuencial de control. Sin embargo, un “thread” se considera un proceso de poco peso porque se ejecuta dentro del contexto de un programa completo y se aprovecha de los recursos asignados por ese programa y del entorno de éste. Como un flujo secuencial de control, un “thread” debe conseguir algunos de sus propios recursos dentro de un programa en ejecución (debe tener su propia pila de ejecución y contador de programa, por ejemplo). El código que se ejecuta dentro de un Thread trabaja solo en este contexto. Así, algunos textos utilizan el término contexto de ejecución como un sinónimo para los “threads”. Este ejemplo define dos clases: SimpleThread y TwoThreadsTest. Empecemos nuestra exploración de la aplicación con la clase SimpleThread — una subclase de la clase Thread, que es proporcionada por el paquete java.lang—: Anexo III 6-70
Lenguaje JAVA
class SimpleThread extends Thread { public SimpleThread(String str) { super(str); } public void run() { for (int i = 0; i < 10; i++) { System.out.println(i + « « + getName()); try { sleep((int)(Math.random() * 1000)); } catch (InterruptedException e) {} } System.out.println(«HECHO! « + getName()); } } El primer método de esta clase es un constructor que toma una cadena como su único argumento. Este constructor está implementado mediante una llamada al constructor de la superclase y es interesante para nosotros solo porque selecciona el nombre del Thread, que se usará más adelante en el programa. El siguiente método es el método run(). Este método es el corazón de cualquier Thread y donde tiene lugar la acción del Thread. El método run() de la clase SimpleThread contiene un bucle for que itera diez veces. En cada iteración el método muestra el número de iteración y el nombre del Thread, luego espera durante un intervalo aleatorio de hasta 1 segundo. Después de haber terminado el bucle, el método run() imprime «HECHO!» con el nombre del Thread. La clase TwoThreads proporciona un método main() que crea dos “threads” SimpleThread: uno llamado «Jamaica» y otro llamado «Fiji» (si no quiere decidir, dónde ir de vacaciones, puede utilizar este programa para ayudarte a elegir –ve a la isla cuyo “threads” imprima «HECHO!» primero–.
class TwoThreadsTest { public static void main (String[] args) { new SimpleThread(«Jamaica»).start(); new SimpleThread(«Fiji»).start(); } } El método main() también arranca cada uno de los “threads” inmediatamente después siguiendo su construcción con una llamada al método start(). Anexo III 6-71
Desarrollo de Sistemas
El programa daría una salida parecida a esta: 0 2 4 6 8 9
Observe cómo la salida de cada uno de los “threads” se mezcla con la salida del otro. Esto es porque los dos “threads” SimpleThread se están ejecutando de forma concurrente. Así, los dos métodos run() se están ejecutando al mismo tiempo y cada “thread” está mostrando su salida al mismo tiempo que el otro. 0 Jamaica
0 Fiji
1 Fiji
1 Jamaica
2 Jamaica
2 Fiji
3 Fiji
3 Jamaica
4 Jamaica
4 Fiji
5 Jamaica
5 Fiji
6 Fiji
6 Jamaica
7 Jamaica
7 Fiji
8 Fiji
9 Fiji
8 Jamaica
HECHO! Fiji
9 Jamaica
HECHO! Jamaica
17.2. Atributos de un Thread Esta página presenta varias características específicas de los “threads” JAVA y proporciona enlaces a las páginas que explican cada característica con más detalle. Los “threads” java están implementados por la clase Thread, que es una parte del paquete java.lang. Esta clase implementa una definición de “threads” independiente del sistema. Pero, bajo la campana, la implementación real de la operación concurrente la proporciona una implementación específica del sistema. Para la mayoría de las aplicaciones, la implementación básica no importa. Se puede ignorar la implementación básica y programar el API de los “threads” descrito en estas lecciones y en otra documentación proporcionada con el sistema JAVA. • Cuerpo del Thread Toda la acción tiene lugar en el cuerpo del “thread” — el método run()— . Se puede proporcionar el cuerpo de un Thread de una de estas dos formas: subclasificando la clase Thread y sobreescribiendo su método run(), o creando un “thread” con un objeto de la clase Runnable y su target. Anexo III 6-72
Lenguaje JAVA
• Estado de un Thread A lo largo de su vida, un “thread” tiene uno o varios estados. El estado de un “thread” indica qué está haciendo el Thread y lo que es capaz de hacer durante su tiempo de vida: ¿se está ejecutando?, ¿está esperando? ¿o está muerto? • La prioridad de un Thread Una prioridad del Thread le dice al temporizador de “threads” de JAVA cuando se debe ejecutar este “thread” en relación con los otros. • Threads Daemon Estos “threads” son aquellos que proporcionan un servicio para otros “threads” del sistema. Cualquier “thread” JAVA puede ser un “thread” daemon. • Grupos de Threads Todos los “threads” pertenecen a un grupo. La clase ThreadGrpup, perteneciente al paquete java.lang define e implementa las capacidades de un grupo de “thread” relacionados. —
La mayoría de los ordenadores solo tienen una CPU, los “threads” deben compartir la CPU con otros “threads”. La ejecución de varios “threads” en un solo CPU, en cualquier orden, se llama programación. El sistema de ejecución JAVA soporta un algoritmo de programación determinístico que es conocido como programación de prioridad fija.
—
A cada “thread” JAVA se le da una prioridad numérica entre MIN_PRIORITY y MAX_PRIORITY (constantes definidas en la clase Thread). En un momento dato, cuando varios “threads” están listos para ejecutarse, el “thread” con prioridad superior será el elegido para su ejecución. Sólo cuando el “thread” para o se suspende por alguna razón, se empezará a ejecutar un “thread” con prioridad inferior.
—
La programación de la CPU es totalmente preventiva. Si un “thread” con prioridad superior que el que se está ejecutando actualmente necesita ejecutarse, toma inmediatamente posesión del control sobre la CPU.
—
El sistema de ejecución de JAVA no hace abandonar a un “thread” el control de la CPU por otro “thread” con la misma prioridad. En otras palabras, el sistema de ejecución de JAVA no comparte el tiempo. Sin embargo, algunos sistemas sí lo soportan, por lo que no se debe escribir código que esté relacionado con el tiempo compartido.
—
Además, un “thread” cualquiera, en cualquier momento, puede ceder el control de la CPU llamando al método yield(). Los “threAnexo III 6-73
Desarrollo de Sistemas
ads” sólo pueden ‘prestar’ la CPU a otros “threads” con la misma prioridad que él –intentar cederle la CPU a un “thread” con prioridad inferior no tendrá ningún efecto–. —
Cuando todos los “threads” «ejecutables» del sistema tienen la misma prioridad, el programador elige a uno de ellos en una especie de orden de competición.
17.3. Programas con varios Threads A) Sincronización de Threads Frecuentemente, los “threads” necesitan compartir datos. Por ejemplo, supongamos que existe un “thread” que escribe datos en un fichero mientras, al mismo tiempo, otro “thread” está leyendo el mismo fichero. Cuando los “threads” comparten información necesitan sincronizarse para obtener los resultados deseados. Existen muchas situaciones interesantes donde ejecutar “threads” concurrentes que compartan datos y deban considerar el estado y actividad de otros “threads”. Este conjunto de situaciones de programación son conocidos como escenarios ‘productor/consumidor’, donde el productor genera un canal de datos que es consumido por el consumidor. Por ejemplo, se puede imaginar una aplicación JAVA donde un “thread” (el productor) escribe datos en un fichero mientras que un segundo “thread” (el consumidor) lee los datos del mismo fichero. O si se teclean caracteres en el teclado, el “thread” productor sitúa las pulsaciones en una pila de eventos y el “thread” consumidor lee los eventos de la misma pila. Estos dos ejemplos utilizan “threads” concurrentes que comparten un recurso común: el primero comparte un fichero y el segundo una pila de eventos. Como los “threads” comparten un recurso común, deben sincronizarse de alguna forma. B) Imparcialidad, Hambre y Punto Muerto Si se escribe un programa en el que varios “threads” concurrentes deben competir por los recursos, se deben tomar las precauciones necesarias para asegurarse la justicia. Un sistema es justo cuando cada “thread” obtiene suficiente acceso a los recursos limitados como para tener un progreso razonable. Un sistema justo previene el hambre y el punto muerto. El hambre ocurre cuando uno o más “threads” de un programa están bloqueados por ganar el acceso a un recurso y así no pueden progresar. El punto muerto es la última forma de hambre; ocurre cuando dos o más “threads” están esperando una condición que no puede ser satisfecha. El punto muerto ocurre muy frecuentemente cuando dos (o más) “threads” están esperando a que el otro u otros hagan algo. C) Volatile Los programas pueden modificar variables miembros fuera de la protección de un método o un bloque sincronizados y puede declarar que la variable miembro es volatile. Anexo III 6-74
Lenguaje JAVA
Si una variable miembro es declarada como “volatile”, el sistema de ejecución JAVA utiliza esta información para asegurarse que la variable sea cargada desde la memoria antes de cada uso, y almacenada en la memoria después de utilizarla. Esto asegura que el valor de la variable es consistente y coherente a lo largo del programa. D) Monitores Los objetos, como el CubbyHole que son compartidos entre dos “threads” y cuyo acceso debe ser sincronizado son llamados condiciones variables. El lenguaje JAVA permite sincronizar “threads” alrededor de una condición variable mediante el uso de monitores. Los monitores previenen que dos “threads” accedan simultáneamente a la misma variable. E) Los métodos notify() y wait()
En un nivel superior, el ejemplo Productor/Consumidor utiliza los métodos notify() y wait() del objeto para coordinar la actividad de los dos “threads”. El objeto CubyHole utiliza notify() y wait() para asegurarse de que cada valor situado en él por el Productor es recuperado una vez y sólo una por el Consumidor.
18. Interfaz gráfico AWT (Abstract Window Toolkit) 18.1. Introducción El AWT (Abstract Windows Toolkit) es la parte de JAVA que se ocupa de construir “interfaces” gráficas de usuario. JAVA incluye una librería llamada «Abstract Window Toolkit» (AWT) que define los principales elementos para el desarrollo de una interfaz gráfica (GUI): Button, Canvas, TextField, TextArea, Checkbox, Choice, Label, List, etc… Todos estos elementos gráficos son objetos derivadas de una superclase común: Component. Object
Component
Button
Label
TextField
TextArea
Container
Anexo III 6-75
Desarrollo de Sistemas
Un objeto de la clase Container agrupa a los elementos gráficos. Para poder mostrar un elemento gráfico es necesario añadirlo a un objeto Container:
Container
Un objeto Container es a su vez un Component, por lo que puede estar anidado en otro objeto Container:
Existen diferentes objetos de la clase Container, por lo general se utilizan objetos de la clase Panel.
18.2. Creación de una Interface Gráfica de Usuario Para construir una “interface” gráfica de usuario hace falta: —
Un “contenedor” o “container”, que es la ventana o parte de la ventana donde se situarán los componentes (botones, barras de desplazamiento, etc.) y donde se realizarán los dibujos. Se correspondería con un formulario o una “picture box” de Visual Basic.
—
Los componentes: menús, botones de comando, barras de desplazamiento, cajas y áreas de texto, botones de opción y selección, etc. Se corresponderían con los controles de Visual Basic.
—
El modelo de eventos. El usuario controla la aplicación actuando sobre los componentes, de ordinario con el ratón o con el teclado. Cada vez que el usuario realiza una determinada acción, se produce el evento correspondiente, que el sistema operativo transmite al AWT. El AWT crea un objeto de una determinada clase de evento, derivada de AWTEvent. Este evento es transmitido a un determinado método para que lo gestione.
Anexo III 6-76
Lenguaje JAVA
En los siguientes apartados se verán con un cierto detalle estos tres aspectos del AWT. Hay que considerar que el AWT es una parte muy extensa y complicada de JAVA, sobre la que existen libros con muchos cientos de páginas.
18.3. Objetos “event source” y objetos “event listener” El modelo de eventos de JAVA está basado en que los objetos sobre los que se producen los eventos (event sources) “registran” los objetos que habrán de gestionarlos (event listeners), para lo cual los event listeners habrán de disponer de los métodos adecuados. Estos métodos se llamarán automáticamente cuando se produzca el evento. La forma de garantizar que los event listeners disponen de los métodos apropiados para gestionar los eventos es obligarles a implementar una determinada “interface” Listener. Las “interfaces” listener se corresponden con los tipos de eventos que se pueden producir. En los apartados siguientes se verán con más detalle los componentes que pueden recibir eventos, los distintos tipos de eventos y los métodos de las “interfaces” listener que hay que definir para gestionarlos. En este punto es muy importante ser capaz de buscar la información correspondiente en la documentación de JAVA. Las capacidades gráficas del AWT resultan pobres y complicadas en comparación con lo que se puede conseguir con Visual Basic, pero tienen la ventaja de poder ser ejecutadas casi en cualquier ordenador y con cualquier sistema operativo.
18.4. Proceso a seguir para crear una aplicación interactiva (orientada a eventos) Para avanzar un paso más, se resumen a continuación los pasos que se pueden seguir para construir una aplicación sencilla orientada a eventos, con “interface” gráfica de usuario: —
Determinar los componentes que van a constituir la “interface” de usuario (botones, cajas de texto, menús, etc.).
—
Crear una clase para la aplicación que contenga la función “main()”.
—
Crear una clase Ventana, sub-clase de Frame, que responda al evento WindowClosing(). La función “main()” deberá crear un objeto de la clase Ventana (en el que se van a introducir las componentes seleccionadas) y mostrarla por pantalla con el tamaño y posición adecuados.
—
Añadir al objeto Ventana todos los componentes y menús que deba contener. Se puede hacer en el constructor de la ventana o en el propio método “main()”.
—
Definir los objetos listener (objetos que se ocuparán de responder a los eventos, cuyas clases implementan las distintas “interfaces” listener) para cada uno de los eventos que deban estar soportados. En aplicaciones pequeñas, el propio objeto Ventana se puede ocupar de res-
Anexo III 6-77
Desarrollo de Sistemas
ponder a los eventos de sus componentes. En programas más grandes se puede crear uno o más objetos de clases especiales para ocuparse de los eventos. —
Finalmente, se deben implementar los métodos de las “interfaces” Listener que se vayan a hacer cargo de la gestión de los eventos.
18.5. Relación entre componentes y eventos En la siguiente tabla se relacionan los componentes del AWT y los eventos específicos de cada uno; hay que tener en cuenta que los eventos propios de una superclase de componentes pueden afectar a los componentes de una subclase. COMPONENT
EVENTOS
SIGNIFICADO
GENERADOS
Button
ActionEvent
Clicar en el botón
Checkbox
ItemEvent
Seleccionar o deseleccionar un ítem
CheckboxMenuItem
ItemEvent
Seleccionar o deseleccionar un ítem
Choice
ItemEvent
Seleccionar o deseleccionar un ítem
ComponentEvent
Mover, cambiar tamaño, mostrar u ocultar un componente
FocusEvent
Obtener o perder el focus
KeyEvent
Pulsar o soltar una tecla
MouseEvent
Pulsar o soltar un botón del ratón; entrar o salir de un componente; mover o arrastrar el ratón (tener en cuenta que este evento tiene dos Listener)
ContainerEvent
Añadir o eliminar un componente de un “container”
ActionEvent
Hacer doble clic sobre un ítem de la lista
ItemEvent
Seleccionar o deseleccionar un ítem de la lista
MunuItem
ActionEvent
Seleccionar un ítem de un menú
Scrollbar
AdjustementEvent
Cambiar el valor de la scrollbar
TextComponent
TextEvent
Cambiar el texto
TextField
ActionEvent
Terminar de editar un texto pulsando Intro
Window
WindowEvent
Acciones sobre una ventana: abrir, cerrar, iconizar, restablecer e iniciar el cierre
Component
Container List
Anexo III 6-78
Lenguaje JAVA
En la siguiente tabla se especifican también los eventos específicos de sus superclases:
Eventos que se pueden generar AWT Components
Ation Adjust. Compn. Contai. Focus Event Event Even Even Even
Key Mouse Mousm. Text Window Even Even Even Even Even
*
*
*
*
*
Canvas
*
*
*
*
*
Checkbox
*
*
*
*
*
*
*
*
Button
*
Item Even
* *
Checkbox-MenuItem Choice
*
*
Component
*
*
*
*
*
Container
*
*
*
*
*
*
Dialog
*
*
*
*
*
*
*
Frame
*
*
*
*
*
*
*
Label
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
List
*
MenuItem
* *
Panel *
Scrollbar TextArea TextField
*
Window
*
*
*
*
*
*
18.6. Interfaces listener Una vez vistos los distintos eventos que se pueden producir, conviene ver cómo se deben gestionar estos eventos. A continuación se detalla cómo se gestionan los eventos según el modelo de JAVA:
Anexo III 6-79
Desarrollo de Sistemas
Cada objeto que puede recibir un evento (event source), “registra” uno o más objetos para que los gestionen (event listener). Esto se hace con un método que tiene la forma:
eventSourceObject.addEventListener(eventListenerObject); Donde eventSourceObject es el objeto en el que se produce el evento, y eventListenerObject es el objeto que deberá gestionar los eventos. La relación entre ambos se establece a través de una “interface” Listener que la clase del eventListenerObject debe implementar. Esta “interface” proporciona la declaración de los métodos que serán llamados cuando se produzca el evento. La “interface” a implementar depende del tipo de evento. EVENTO
En la tabla están relacionados los distintos tipos de eventos, con la “interface” que se debe implementar para gestionarlos y los métodos declarados en cada “interface”. Obsérvese que el nombre de la “interface” coincide con el nombre del evento, sustituyendo la palabra Event por Listener. Una vez registrado el objeto que gestionará el evento, perteneciente a una clase que implemente la correspondiente “interface” Listener, se deben
Anexo III 6-80
Lenguaje JAVA
definir los métodos de dicha “interface”. Siempre hay que definir todos los métodos de la “interface”, aunque algunos de dichos métodos puedan estar “vacíos”.
18.7. Clases Adapter JAVA proporciona ayudas para definir los métodos declarados en las “interfaces” Listener. Una de estas ayudas son las clases Adapter, que existen para cada una de las “interfaces” Listener que tienen más de un método. Su nombre se construye a partir del nombre de la “interface”, sustituyendo la palabra “Listener” por “Adapter”. Hay siete clases Adapter: —
ComponentAdapter
—
ContainerAdapter
—
FocusAdapter
—
KeyAdapter
—
MouseAdapter
—
MouseMotionAdapter
—
WindowAdapter
Las clases Adapter derivan de Object, y son clases predefinidas que contienen definiciones vacías para todos los métodos de la “interface”. Para crear un objeto que responda al evento, en vez de crear una clase que implemente la “interface” listener, basta crear una clase que derive de la clase Adapter correspondiente, y redefina sólo los métodos de interés.
19. Paseando por la Red Es muy sencillo acceder a archivos en la red utilizando JAVA. El paquete java.net dispone de varias clases e “interfaces” a tal efecto. En primer lugar, la clase URL nos permite definir un recurso en la red de varias maneras, por ejemplo:
URL url1 = new URL («http://www.rockar.com.ar/index.html»); URL url2 = new URL («http», «www.rockar.com.ar», «sbits.htm»); Por otra parte, podemos establecer una conexión a un URL dado mediante openConnection:
URLConnection conexion = url.openConnection(); Una vez lograda la conexión, podemos leer y escribir datos utilizando streams (corrientes de datos), como en el caso de manejo de archivos comunes (ver capítulo X). Un DataInputStream nos permite leer datos que llegan a través de la red, y un DataOutputStream nos permite enviar datos al host. Anexo III 6-81
Desarrollo de Sistemas
Por ejemplo:
DataInputStream datos = new DataInputStream (corrienteEntrada); En nuestro caso, la corriente de entrada de datos proviene de la conexión al URL. El método getInputStream() del objeto URLConnection nos provee tal corriente:
DataInputStream datos = new DataInputStream(conex.getInputStream()) De este modo podemos escribir un pequeño programa para, por ejemplo, leer una página HTML desde una dirección arbitraria de internet. El programa, luego de compilarse mediante javac Lolo25.java, se ejecuta con java Lolo25 ; por ejemplo: java Lolo25 http://www.rockar.com.ar/index.html. import java.io.*;
import java.net.*;
public class Lolo25 { public static void main(String argv[]) { String s; try { URL url = new URL (argv[0]); URLConnection conex = url.openConnection(); System.out.println(«Cargando «+argv[0]); DataInputStream datos = new DataInputStream(conex.getInput am()); do { s = datos.readLine(); if (s != null) System.out.println(s); } while (s != null); } catch (ArrayIndexOutOfBoundsException e) { System.out.println(«Sintaxis: java Lolo25 »); } catch (UnknownHostException e) { System.out.println(«El host no existe o no responde»); } catch (Exception e) {e.printStackTrace();} } }