Programación orientada a objetos Unidad Temas 1 Arreglos unidimensionales y multidimensionales. 1.1 1.2 1.3 2 Métodos y mensajes. 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 Subtemas Arreglo Unidimensionales listas (vectores). 1.1.1 Conceptos básicos. 1.1.2 Operaciones. 1.1.3 Aplicaciones. Arreglo bidimensional. 1.2.1 Conceptos básicos. 1.2.2 Operaciones. 1.2.3 Aplicaciones. Arreglo Multidimensional. 1.3.1 Conceptos básicos. 1.3.2 Operaciones. 1.3.3 Aplicaciones. Atributos const y static. Concepto de método. Declaración de métodos. Llamadas a métodos (mensajes). Tipos de métodos. 2.5.1 Métodos const, static. 2.5.2 Métodos normales y volátiles. Referencia this. Forma de pasar argumentos. Devolver un valor desde un método. Estructura del código. 3 Constructor, destructor. 3.1 Conceptos de métodos constructor y destructor. 3.2 Declaración de métodos constructor y destructor. 3.3 Aplicaciones de constructores y destructores. 3.4 Tipos de constructores y destructores. 4 Sobrecarga. 4.1 Conversión de tipos. 4.2 Sobrecarga de métodos. 4.3 Sobrecarga de operadores. 5 Herencia. 5.1 Introducción a la herencia. 5.2 Herencia simple. 5.3 Herencia múltiple. 5.4 Clase base y clase derivada. 5.4.1 Definición. 5.4.2 Declaración. 5.5 Parte protegida. 5.5.1 Propósito de la parte protegida. 5.6 Redefinición de los miembros de las clases derivadas. 5.7 Clases virtuales y visibilidad. 5.8 Constructores y destructores en clases derivadas. 5.9 Aplicaciones. 6 Polimorfismo y reutilización 6.1 Concepto del polimorfismo. 6.2 Clases abstractas. 6.2.1 Definición. 6.2.2 Redefinición. 6.3 Definición de una interfaz. 6.4 Implementación de la definición de una interfaz. 6.5 Reutilización de la definición de una interfaz. 6.6 Definición y creación de paquetes / librería. 6.7 Reutilización de las clases de un paquete / librería. 6.8 Clases genéricas (Plantillas). 7 Excepciones. 7.1 Definición. 7.1.1 Que son las excepciones. 7.1.2 Clases de excepciones, excepciones predefinidas por el lenguaje. 7.1.3 Propagación. 7.2 Gestión de excepciones. 7.2.1 Manejo de excepciones. 7.2.2 Lanzamiento de excepciones. 7.3 Excepciones definidas por el usuarios. 7.3.1 Clase base de las excepciones. 7.3.2 Creación de un clase derivada del tipo excepción. 7.3.3 Manejo de una excepción definida por el usuario. 8 Flujos y archivos. 8.1 Definición de Archivos de texto y archivos binarios. 8.2 Operaciones básicas en archivos texto y binario. 8.2.1 Crear. 8.2.2 Abrir. 8.2.3 Cerrar. 8.2.4 Lectura y escritura. 8.2.5 Recorrer. 8.3 Aplicaciones. Unidad 1. Arreglos unidimensionales y multidimensionales. 1.1 Arreglo Unidimensionales listas (vectores). 1.1.1 Conceptos básicos. Un arreglo unidimensional es un tipo de datos estructurado que está formado de una colección finita y ordenada de datos del mismo tipo. Es la estructura natural para modelar listas de elementos iguales. El tipo de acceso a los arreglos unidimensionales es el acceso directo, es decir, podemos acceder a cualquier elemento del arreglo sin tener que consultar a elementos anteriores o posteriores, esto mediante el uso de un índice para cada elemento del arreglo que nos da su posición relativa. Para implementar arreglos unidimensionales se debe reservar espacio en memoria, y se debe proporcionar la dirección base del arreglo, la cota superior y la inferior. REPRESENTACION EN MEMORIA Los arreglos se representan en memoria de la forma siguiente: x : array[1..5] of integer Para establecer el rango del arreglo (número total de elementos) que componen el arreglo se utiliza la siguiente formula: RANGO = Ls - (Li+1) donde: ls = Límite superior del arreglo li = Límite inferior del arreglo Para calcular la dirección de memoria de un elemento dentro de un arreglo se usa la siguiente formula: A[i] = base(A) + [(i-li) * w] donde : A = Identificador único del arreglo i = Indice del elemento li = Límite inferior w = Número de bytes tipo componente Si el arreglo en el cual estamos trabajando tiene un índice numerativo utilizaremos las siguientes fórmulas: RANGO = ord (ls) - (ord (li)+1) A[i] = base (A) + [ord (i) - ord (li) * w] Arreglos Unidimensionales La declaración del arreglo unidimensional es un tipo seguido de un identificador con una expresión constante INT entre corchetes. El valor de la expresión, que debe ser positivo,es el tamaño del arreglo y especifica el numero de elementos que contiene. La forma de declarar un arreglo es la siguiente: < tipo > < variable > [N] Declara un arreglo de nombre < variable > con N elementos de tipo < tipo >, en donde N es una constante) Los corchetes [ ], sirven para encerrar los subíndices. Por ejemplo, para declarar un arreglo de enteros llamado arreglo con 5 elementos se hace de la siguiente forma: int arreglo[5]; Para accesar a algún elemento del arreglo, puede hacerse de las siguientes formas: a) int arreglo[] = {0,1,2,3,4,5} /*se valida cuando se inicializan todos los elementos del array */ b) int arreglo[4] = {4, 2, 3, 6} /*se declara y asigna valores iniciales al arreglo */ c) arreglo[3] = 10; num = arreglo[2]; /* Asigna 10 al 4er elemento del arreglo lista*/ /* Asigna el contenido del 3er elemento a la variable num */ Una Estructura de Datos, es una colección de datos que se caracterizan por su organización las operaciones que se definen en ella. Las Estructuras de Datos pueden ser de dos tipos: Estáticas y Dinámicas. Las Estructuras de Datos estáticas son aquellas en las que el espacio ocupado en la memoria se define en tiempo de compilación no puede ser modificado durante la ejecución del programa; por el contrario, en las Estructuras de Datos Dinámicas el espacio ocupado en memoria puede ser modificado en tiempo de ejecución> Un ARRAY (arreglo), es una colección de datos del mismo tipo, que se almacena en posiciones consecutivas de memoria y recibe un nombre com?os componentes individuales de un arreglo se llaman Elementos y se distinguen entre ellos por el nombre del arreglo seguido uno o varios �ices. El �ice es un n? que indica la posici�ue ocupa el elemento dentro del arreglo. Los elementos del arreglo se almacenan en la memoria de la computadora en posiciones adyacentes (un elemento por posici� Los arreglos se dividen en: - UNIDIMENSIONALES (Vectores o Listas) - BIDIMENSIONALES (Tablas o matrices) - MULTIDIMENSIONALES Arreglos Unidimensionales o Vectores. Un Vector es una secuencia de elementos del mismo tipo y en los que el orden es significativo. El orden estᠤado por el �ice del vector; por ejemplo: Vector[10] 0 21 -1 9 8 5 -9 15 7 6 -4 33 Declaraci�e Vectores: Es la reservaci�e un espacio en memoria, para almacenar un conjunto de datos. Formato: NOMBRE: ARRAY [N] DE TIPODEDATO NOMBRE: Identificador del arreglo. N: N? de elementos del arreglo. TIPODEDATO: Entero, Real, Cadena, L�o. El �ice del primer elemento del vector es 0 (cero) y el ?o es N-1. Ejemplo: Declarar un Vector llamado TEMP que almacene las tempraturas de cada hora durante un d� TEMP : ARRAY [24] DE REAL Arreglos A diferencia de los elementos estudiados hasta ahora, los arreglos pertenecen al dominio de los tipos de datos y no al de las instrucciones de Pascal. A grandes rasgos, son conjuntos de variables que comparten un mismo nombre, pudiendo ser referenciadas de manera individual las variables del conjunto con ayuda de uno o más índices. Los arreglos guardan estrecha similitud con elementos de datos de la vida cotidiana, como los vectores y las tablas, y en cierto modo trazan una línea que separa la programación básica de la avanzada. Como la sintaxis formal de los arreglos es muy general, se comenzará definiendo los arreglos unidimensionales o vectores. Arreglos unidimensionales: vectores Supóngase que se tienen cinco variables: a, b, c, d y e. Si se desea hacer referencia a las cinco, deben usarse sus nombres, y eso implicaría, a nivel de programación, poner sus nombres en cada sentencia que se requiera (ReadLn, fórmulas, etc.) y repetir las mismas cinco veces. Un enfoque más general es poner a las variables un mismo nombre, y distinguirlas por un número, parecido a como hacen los libros de matemáticas para referirse de manera general a las componentes de los vectores o de las tuplas. Entonces, se hablaría de a(1), a(2), a(3), a(4) y a(5), en vez de las letras usadas anteriormente; como ya se sabe cómo generar los índices (los números entre paréntesis que permiten distinguir las variables), se puede usar un ciclo para manipular las variables y usar menos código. La sintaxis para declarar un arreglo de una sola dimensión es: Var <nomb> : Array [<li>..<ls>] Of <tipo>; donde <nomb> es el nombre del arreglo, <li> es el valor inferior que puede tomar el índice y <ls> es el valor superior del índice. Un ejemplo real de declaración de arreglos se muestra a continuación: Var a : Array[1..5] Of Integer; que produce un arreglo llamado a de cinco variables enteras. La primera es a[1], la segunda es a[2], y así sucesivamente. ¿Cuál es la utilidad de este esquema? Si se tienen que leer cinco variables declaradas por separado, con nombres distintos, se usaría algo como esto: ReadLn(a); ReadLn(b); ReadLn(c); ReadLn(d); ReadLn(e); mientras que con arreglos, basta escribir: For i := 1 To 5 Do ReadLn(a[i]); que es evidentemente más cómodo. A lo largo del curso se han visto programas que suman una cantidad de números usando dos variables, una para leer cada número y otra para acumular la suma. Este enfoque tiene la desventaja de que se pierden los valores de los sumandos. El uso de arreglos permite calcular la suma de los números con una cantidad mínima de código y a la vez conservar cada valor, como muestra el siguiente programa completo: Program SumaN; Uses WinCrt; Const n = 5; {Cant. de #s} Var nums: Array[1..n] Of Integer; s, i: Integer; Begin For i:=1 To n Do Begin Write('Número: '); ReadLn(nums[i]); s := s + nums[i]; End; WriteLn('Suma: ', s); End. Nótese el uso de una constante para marcar el tamaño del arreglo; dicha constante, naturalmente, también sirve para controlar el For. De este modo, sólo se hace necesario cambiar un número para adecuar el programa a la escala apropiada. Arreglos Unidimensionales (Vectores o Listas) (Variables con subíndices) Nota: Un arreglo de variables es una colección de variables simples todas del mismo tipo con un nombre común, llamado el nombre del arreglo. Ejemplo: Considere las siguientes edades: 20, 19, 18, 26, 23, 18, 23. Estos siete valores se pueden guardar usando las variables Edad1 = 20, Edad2 = 19, Edad3 = 18, Edad4 = 26, Edad5 = 23, Edad6 = 18 y Edad7 = 23. Otra forma de guardarlos es como siete elementos del arreglo con nombre Edad. Se puede hacer referencia a elementos en el arreglo haciendo uso del nombre Edad y el número del elemento (índice de la variable) entre paréntesis. Referencia de la variable: Edad(1), Edad(2), Edad(3), ... Valor de la variable: 20, 19, 18, ... Ejemplos: 1) (Edad(4) + Edad(2))/2 2) Suma = 0 For i = 1 TO 6 Suma = Suma + Edad(i) Next i picOutput.Print "El promedio de las edades es:";Suma/6 http://www.cayey.upr.edu/crivera/sici3007/ArreglosUnidimensionales.htm Un arreglo (array, disposición, vector o lista, tabla o matriz) es una estructura de datos utilizada para almacenar un conjunto de datos del mismo tipo, en posiciones consecutivas de memoria. Un arreglo se identifica por medio de un nombre. Los componentes individuales del arreglo se denominan elementos y se distinguen entre ellos por el nombre del arreglo seguido de uno o varios índices o subíndices, entre paréntesis. El identificador o índice, determina la posición de memoria de un elemento del arreglo. 1.4.1.1 Clasificación de los Arreglos 1.4.1.1.1 Arreglos Unidimensionales. Un arreglo unidimensional es un conjunto finito (número especifico de elementos), ordenado, de elementos homogéneos (del mismo tipo). Figura 1.4 Arreglo unidimensional. Para tener acceso directo a la posición de memoria que contiene el valor de cada elemento del arreglo, se utiliza la siguiente fórmula: Dir E[I]=Dir E[Ii]+NumPos*(I-Ii) Donde: Dir E[I]: Dirección de memoria del elemento cuyo índice es I. Dir E[Ii]: Dirección de memoria del elemento inicial del arreglo. NumPos: Número de posiciones de memoria de que consta la celda. Ii: Índice del elemento inicial. Las operaciones con vectores se pueden realizar con elementos individuales o sobre los vectores completos mediante las instrucciones básicas y estructuras de control. Las operaciones que se pueden realizar sobre elementos individuales son: asignación y lectura. Entre las operaciones sobre el vector completo están: o o o o Recorrido: Es la manera de acceder de manera sucesiva y consecutiva a los contenidos de cada elemento del vector. Inserción: Consiste en introducir en el arreglo el valor del elemento. Aunque el sistema reserva memoria para cada elemento del arreglo, puede suceder que alguna posición de memoria se encuentre vacía. Surgen dos operaciones distintas: añadir una celda de memoria vacía o añadir en una celda de memoria ocupada, para no dañar el contenido de memoria donde se inserta el nuevo dato, se desplaza este contenido y todos los siguientes a una posición superior de la misma memoria. Búsqueda: Consiste en realizar un recorrido del vector, empezando desde su posición de memoria más baja con el fin de localizar un dato determinado. Eliminación: Borrar el dato contenido en una de las posiciones del vector, si es una posición diferente de la última, todos los elementos con posiciones posteriores retroceden una posición. Ordenación: Consiste en reorganizar el contenido de cada uno de los elementos del vector según una secuencia determinada (ascendente o descendente). 1.1.2 Operaciones. Las operaciones con vectores se pueden realizar con elementos individuales o sobre los vectores completos, mediante las instrucciones b�cas y las estructuras de control; por ejemplo: Operaciones sobre los elementos de un vector Las operaciones sobre los elementos de un vector son: ASIGNACIӎ. LECTURA. ESCRITURA. ASIGNACIӎ: Es la acci�e introducir un elemento a una posici�spec�ca del vector. As�ues, si queremos que la posici� del vector tenga un valor de -8, la operaci� realizar es la siguiente: A[6]=-8 LECTURA: Es la operaci� acci�e obtener un valor desde el teclado, para ser introducido en una posici�el vector: LEE A[4] ESCRITURA: Es la operaci�e sacar un valor de una posici�el vector: ESCRIBE A[6] Operaciones sobre vectores completos: Las operaciones que se pueden realizar sobre vectores completos son: - RECORRIDO. - BړQUEDA. - INSERCIӎ. - ELIMINACIӎ. - ORDENAMIENTO. RECORRIDO: Es la operaci�e escribir, asignar, leer o llenar de datos el vector completo y se realiza mediante las siguientes estructuras: FOR Ind=0 TO 9 DO FOR Ind=0 TO 9 DO LEE A[Ind] ESCRIBE A[Ind] Ind=1 Ind=1 While Ind<=10 DO While Ind<=10 DO LEE A[Ind] ESCRIBE A[Ind] Ejemplo: Dise�n Pseudoc�o que almacene en la memoria de la computadora un vector llamado FIBO de 100 posiciones el cual contendrᠤos primeros 100 n?sdel Fibonacci: Soluci�/big> Pseudoc�o Fibonacci Variables FIBO : ARRAY [100] DE Entero A,B,C,I : Entero INICIO A_1 B=0 FIBO[0]=0 FIBO[1]=1 FOR I=2 TO 99 DO INICIO C=A+B FIBO[I]=C B=A A=C FIN FIN BړQUEDA: Consiste en determinar si un valor espec�co se encuentra dentro del vector. Se deben examinar uno a uno todos los elementos, comenzando con el primer elemento del vector y comparando con el elemento buscado. El pseudoc�o de b?da es el siguiente: Pseudoc�o B?da Variables HALLADO, X, A : Entero INICIO HALLADO=0 LEE X A=0 WHILE HALLADO=0 AND A<>(N? de Elementos del Vector) DO INICIO IF VECTOR[A]=X THEN HALLADO=1 A=A+1 FIN IF HALLADO=1 THEN ESCRIBE "El N? fu頥ncontrado" ELSE ESCRIBE "El n? no estᠤn el vector" FIN ORDENAMIENTO: La ordenaci� clasificaci�e datos es el proceso de organizar datos en alg?den o secuencia espec�ca, tal como creciente o decrecente, para datos num鲩cos, o alfab鴩camente para datos alfanum鲩cos. Los m鴯dos de ordenaci�e dividen en dos categor�: Ordenaci�e Arrays. Ordenaci�e Archivos. La ordenaci�e Arrays se denomina tambi鮠ordenaci�nterna, ya que se almacena en la memoria interna de la camputadora a gran velocidad y acceso aleatorio. La ordenaci�e archivos se suele hacer casi siempre sobre soportes de almacenamiento externo: Discos, Cintas, etc. y por ello, se denomina ordenaci�xterna. Para este curso s�emplearemos la ordenaci�nterna, es decir, la ordenaci�e Arrays. ORDENACIӎ POR BURBUJA O INTERCAMBIO: El m鴯do de la burbuja es uno de los m�conocidos por su sencillez y facilidad de implementaci�la idea b�ca del m鴯do es comparar elementos consecutivos en cada paso a lo largo del vector. Cada vez que se realiza una comparaci�e los elementos se intercambian entre s�n caso de no estar en orden. Es decir, se examina dos elementos adyacentes X[i] y X[i+1]; en caso de no estar ordenados X[i] < X[i+1] o bien X[i] > X[i+1] se intercambian los valores de dichos elementos. Algoritmo de Ordenamiento por Burbuja: Pseudoc�o BURBUJA Variables A:ARRAY[N] DE Entero X,J,AUX:Entero INICIO FOR X=0 TO (N-2) DO INICIO FOR J=0 TO ((N-1)-(X+1)) DO INICIO IF A[J]>A[J+1] THEN INICIO AUX=A[J] A[J]=A[J+1] A[J+1]=AUX FIN FIN FIN FIN 1.1.3 Aplicaciones. Mezcla de Vectores: El proceso de mezcla, fusi� intercalaci�onsiste en tomar 2 vectores ordenados y obtener un nuevo vector tambi鮠ordenado. El algoritmo m�sencillo para resolver el problema es el siguiente: 1.- Situar todos los elementos del primer vector en el nuevo vector. 2.- Situar todos los elementos del segundo vector en el nuevo vector. 3.- Ordenar el nuevo vector. Esta soluci�iene un inconveniente, no se toma en cuenta que el vector 1 y 2 ya est鮠ordenados, lo cual genera p鲤ida de tiempo en el proceso. El algoritmo que toma en cuenta la ordenaci�s el siguiente: 1.- Seleccionar el elemento de valor m�peque� en cualquiera de los dos vectores y situarlo en el nuevo vector. 2.- Comparar el vector 1 con �ice I y el vector 2 con �ice J y colocar el elemento m�peque�n el vector 3 con �ice K. 3.- Seguir esta secuencia de comparaciones hasta que los elementos de un vector se hayan agotado en cuyo momento se copia el resto del otro vector en el nuevo vector. http://www.itver.edu.mx/comunidad/material/algoritmos/U4-41.htm 1.2 Arreglo bidimensional. 1.2.1 Conceptos básicos. Este tipo de arreglos al igual que los anteriores es un tipo de dato estructurado, finito ordenado y homogéneo. El acceso a ellos también es en forma directa por medio de un par de índices. Los arreglos bidimensionales se usan para representar datos que pueden verse como una tabla con filas y columnas. La primera dimensión del arreglo representa las columnas, cada elemento contiene un valor y cada dimensión representa una relación La representación en memoria se realiza de dos formas : almacenamiento por columnas o por renglones. Para determinar el número total de elementos en un arreglo bidimensional usaremos las siguientes fórmulas: RANGO DE RENGLONES (R1) = Ls1 - (Li1+1) RANGO DE COLUMNAS (R2) = Ls2 - (Li2+1) No. TOTAL DE COMPONENTES = R1 * R2 REPRESENTACION EN MEMORIA POR COLUMNAS x : array [1..5,1..7] of integer Para calcular la dirección de memoria de un elemento se usan la siguiente formula: A[i,j] = base (A) + [((j - li2) R1 + (i + li1))*w] REPRESENTACION EN MEMORIA POR RENGLONES x : array [1..5,1..7] of integer Para calcular la dirección de memoria de un elemento se usan la siguiente formula: A[i,j] = base (A) + [((i - li1) R2 + (j + li2))*w] donde: i = Indice del renglón a calcular j = Indice de la columna a calcular li1 = Límite inferior de renglones li2 = Límite inferior de columnas w = Número de bytes tipo componente Una MATRIZ o TABLA, es un arreglo de dos dimensiones, por lo cual se manejan dos índices; el primer índice se refiere a la fila o renglón y el segundo a la columna; gráficamente lo podemos entender así: Col 1 Col 2 Col 3 Col 4 Fila 1 Fila 2 Fila 3 Fila 4 Para hacer referencia a un elemento de la matriz se tiene que indicar con dos índices: Matriz[Fila][Columna] Ejemplo: M[3][4]=7, M[1][2]=4, M[5][3]=ERROR, M[2][1]=8 Col 1 Col 2 Col 3 Col 4 0 4 1 1 8 3 0 0 2 6 1 7 3 7 8 4 Declaración de Matrices La declaración de una matriz es similar a la de un Areglo Unidimensional (Vector), con la diferencia de que hay que agregar un índice para referenciar la nueva dimensión de la matriz, la sintaxis es la siguiente: NombreDelArreglo: ARRAY [# de Filas][# de Columnas] DE TipoDeDatos Ejemplo: La declaración de la matriz mostrada en el ejemplo de arriba sería de la siguiente forma: M: ARRAY [4][4] DE Enteros Ejemplo con Matrices Diseñe un Pseucocódigo que contenga en la memoria de la computadora una matriz de 3*3 con números enteros leídos del teclado. Pseudocódigo MATRIZ Variables MAT: ARRAY [3][3] DE Enteros Fila, COlumna: Enteros INICIO FOR Fila=1 TO 3 DO INICIO FOR Columna=1 TO 3 DO INICIO ESCRIBE 'Teclee un número: ' LEE MAT[Fila][Columna] FIN FIN FIN Arreglos bidimensionales: tablas Siguiendo la misma línea, se pueden relacionar grupos de vectores para formar tablas. Por ejemplo, supóngase que se quieren almacenar nombres de personas y sus respectivos números de teléfonos; se puede tener un vector de nombres y uno de números, de modo que el i-ésimo número de teléfono (en un vector) sea de la i-ésima persona (en el otro vector). Pues bien: existe un modo de fundir ambos vectores en un solo tipo de variable, una tabla de dos columnas y varias filas (o viceversa) en la que la posición [1, i] indique el nombre de una persona y la posición [2, i] indique el número de teléfono de esa persona. Para declarar esta estructura, se escribe: Var Tabla: Array[1..2, 1..n] Of String;. y de manera general, se puede declarar un arreglo bidimensional así: Var <nomb>: Array[<li1>..<ls1>, <li2>..<ls2>] Of <tipo>; De hecho, aunque no están contemplados en el programa del Laboratorio de Elementos de Computación, se pueden definir más dimensiones; no hay un límite definido a la cantidad de dimensiones de un arreglo. Volviendo al ejemplo de la lista de personas y números de teléfono, se puede escribir un programa completo que use una tabla para leer y guardar esta información: Program Telefonos; Uses WinCrt; Const n = 5; Var Tabla: Array[1..2, 1..n] Of String; i: Integer; Begin For i:=1 To n Do Begin WriteLn('Persona: ', i); Write('Nombre: '); ReadLn(Tabla[1, i]); Write('Teléfono: '); ReadLn(Tabla[2,i]); End; End. En este momento, los datos están en la memoria, y el esfuerzo que se requirió para leerlos fue menor que usando 10 variables independientes (cinco para los nombres y cinco para los números). Además, con sólo cambiar un valor, el de n, el programa almacena más pares de nombre y teléfono. Y aunque no se verá aquí, manipular esos datos una vez leídos es igual de fácil. Unas consideraciones importantes sobre los arreglos: son racimos o conjuntos de variables, pero no pueden manejarse como tales. Siempre deberá hacerse referencia a una posición específica del arreglo; si se desea acceder a una parte del mismo o a su totalidad, deberá hacerse de manera iterativa, visitando los elementos que lo componen uno a uno. http://www.intec.edu.do/~rjimenez/guia6.html Arreglos Bidimensionales. Un arreglo bidimensional es un conjunto de datos del mismo tipo, estructurado de tal forma que se precisa de dos índices para referenciar cada uno de sus elementos, el primer índice se refiere a la fila y el segundo se refiere a la columna. En cuanto al almacenamiento, el sistema reserva memoria para cada uno de sus elementos destinando en conjunto, un bloque de la misma, este se halla estructurado así: almacenamiento consecutivo y secuencial de una fila dentro de otra, sin solución de continuidad; almacenamiento consecutivo y secuencial de una columna dentro de otra, sin solución de continuidad. Para acceder a la posición de cada elemento del arreglo se tienen las siguientes fórmulas: o Almacenamiento por filas: Dir E[I,J]=Dir E[Ii,Ji]+NumPos*(Nc*(I-1)+(J-1)) o Almacenamiento por columnas: Dir E[I,J]=Dir E[Ii,Ji]+NumPos*(Nf*(J-1)+(I-1)) Donde: Dir E[I,J]: Dirección de memoria del elemento cuyos índices son I,J. Dir E[Ii,Ji]: Dirección de memoria del elemento inicial del arreglo. Nc: Número total de columnas. Nf: Número total de filas. Ii y Ji: Índices del elemento inicial. Los arreglos bidimensionales tienen las mismas operaciones que los vectores. los algoritmos cambian porque es necesario tener el orden según los dos índices. 1.2.2 Operaciones. 1.2.3 Aplicaciones. 1.3 Arreglo Multidimensional. 1.3.1 Conceptos básicos. Este también es un tipo de dato estructurado, que está compuesto por n dimensiones. Para hacer referencia a cada componente del arreglo es necesario utilizar n índices, uno para cada dimensión Para determinar el número de elementos en este tipo de arreglos se usan las siguientes fórmulas: RANGO (Ri) = lsi - (lii + 1) No. TOTAL DE ELEMENTOS = R1 * R2* R3 * ...* Rn donde: i = 1 ... n n = No. total de dimensiones Para determinar la dirección de memoria se usa la siguiente formula: LOC A[i1,i2,i3,...,in] = base(A) + [(i1-li1)*R3*R4*Rn + (i2li2)*R3*R2*... (in - lin)*Rn]*w Arreglos multidimensionales tanto C/C++ como Java permite arreglos con más de una dimensión, aunque los arreglos de tres o mas dimensiones no son muy utilizados debido a la cantidad de memoria que se necesita para su almacenamiento. El formato general es: tipo nombre_arr [ tam1 ][ tam2 ] ... [ tamN]; Por ejemplo para definir un arreglo bidimensional lo hariamos de la siguiente forma: int tabla[10][20]; Donde se define una matriz llamada tabla con 10 filas y 20 columnas. Para acceder a los elementos se procede de forma similar al ejemplo del arreglo unidimensional, esto es: arreglo_bi[6][2] = 35; /* Asigna 35 al elemento de la 7ª fila y la 3ª columna*/ num = arreglo_bi[25][16]; Ejemplo 14-1. Arreglo de enteros bidimensionales A continuación se muestra un ejemplo que asigna al primer elemento de un arreglo bidimensional cero, al siguiente 1, y así sucesivamente. main() { int t,i,num[3][4]; for(t=0; t<3; ++t) for(i=0; i<4; ++i) num[t][i]=(t*4)+i*1; for(t=0; t<3; ++t) { for(i=0; i<4; ++i) printf("num[%d][%d]=%d ", t,i,num[t][i]); printf("\n"); } } Arreglos Multidimensionales. Conjunto de datos del mismo tipo, estructurados de tal forma que se necesitan tres o más índices para referenciar cada elemento. Las reglas de índices, número de elementos y dimensión del arreglo son las mismas que para los anteriores. Por ejemplo, el arreglo tridimensional se almacena en la memoria como un vector compuesto por vectores que representan las filas o columnas de cada una de las páginas Arreglo Paralelos. En muchas ocasiones se requiere el proceso simultáneo de más de un arreglo, que teniendo igual número de elementos, el tipo de datos de los mismo es distinto. Estos vectores o matrices se denominan arreglos paralelos. http://www.virtual.unal.edu.co/cursos/ingenieria/2001412/capitulos/cap1/141.html 1.3.2 1.3.3 Operaciones. Aplicaciones. Las operaciones en arreglos pueden clasificarse de la siguiente forma: Lectura Escritura Asignación Actualización Ordenación Búsqueda a) LECTURA Este proceso consiste en leer un dato de un arreglo y asignar un valor a cada uno de sus componentes. La lectura se realiza de la siguiente manera: para i desde 1 hasta N haz x<--arreglo[i] b) ESCRITURA Consiste en asignarle un valor a cada elemento del arreglo. La escritura se realiza de la siguiente manera: para i desde 1 hasta N haz arreglo[i]<--x c) ASIGNACION No es posible asignar directamente un valor a todo el arreglo, por lo que se realiza de la manera siguiente: para i desde 1 hasta N haz arreglo[i]<--algún_valor d) ACTUALIZACION Dentro de esta operación se encuentran las operaciones de eliminar, insertar y modificar datos. Para realizar este tipo de operaciones se debe tomar en cuenta si el arreglo está o no ordenado. Para arreglos ordenados los algoritmos de inserción, borrado y modificación son los siguientes: 1.- Insertar. Si i< mensaje(arreglo contrario caso En arreglo[i]<--valor i<--i+1 entonces> 2.- Borrar. Si N>=1 entonces inicio i<--1 encontrado<--falso mientras i<=n y encontrado=falso inicio si arreglo[i]=valor_a_borrar entonces inicio encontrado<--verdadero N<--N-1 para k desde i hasta N haz arreglo[k]<--arreglo[k-1] fin en caso contrario i<--i+1 fin fin Si encontrado=falso entonces mensaje (valor no encontrado) 3.- Modificar. Si N>=1 entonces inicio i<--1 encontrado<--falso mientras i<=N y encontrado=false haz inicio Si arreglo[i]=valor entonces arreglo[i]<--valor_nuevo encontrado<--verdadero En caso contrario i<--i+1 fin fin ANEXOS: ARREGLOS. Estructura de datos: es una colección de datos organizados de un modo particular. Arreglo (ARRAY): es una estructura de datos en la que se almacena una colección de datos del mismo tipo. Ejemplo notas de los estudiantes. • Tienen un único nombre de variable, que representa todos los elementos, los cuales se diferencian por un índice o subíndice. Ejemplo: NOTAS nombre del arreglo NOTAS[1] nombre del primer elemento del arreglo NOTAS NOTAS[n] nombre del elemento n del arreglo NOTAS 1, 2, 3, ... n índices o subíndices del arreglo (pueden ser enteros, no negativos, variables o expresiones enteras) Clasificación de los arreglos: los arreglos se clasifican en UNIDIMENSIONALES (vectores o listas) y MULTIDIMENSIONALES (Ejemplo, los bidimensionales son las tablas o matrices). Arreglos unidimensionales o vectores: son una lista o columna de datos del mismo tipo, a los que colectivamente nos referimos mediante un nombre. Deben cumplir lo siguiente: • Compuesto por un número de elementos finito. • Tamaño fijo: el tamaño del arreglo debe ser conocido en tiempo de compilación. • Homogéneo: todos los elementos son del mismo tipo • Son almacenados en posiciones contiguas de memoria, cada uno de los cuales se les puede acceder directamente. • Cada elemento se puede procesar como si fuese una variable simple ocupando una posición de memoria. Ejemplo: Dado un vector denominado Z cada uno de sus elementos se designará por ese mismo nombre diferenciándose únicamente por su correspondiente subíndice. En TURBO PASCAL los arreglos unidimensionales se declaran de la siguiente manera: TYPE identificador = ARRAY [tipo-subíndice] OF tipo; Donde: identificador : es el nombre del arreglo tipo-subíndice: puede ser tipo ordinal (boolean o char), tipo enumerado o tipo subrango. No pueden ser usados los tipo estandar (real o integer) tipo: se refiere al tipo de los elementos y puede ser de cualquiera de los tipos estándar o definido por el usuario. Otros ejemplos de declaración de arreglos: TYPE X = ARRAY [TRUE .. FALSE] OF REAL; CODIGO = ARRAY [1 .. 10] OF CHAR; ALBA = ARRAY [0 .. 100] OF 1 .. 999; NOMBRE = ARRAY [1 .. 60] OF STRING[20]; ETIC = ARRAY [‘A’ ..`F’] OF REAL; NOT1 = ARRAY [1..6] OF REAL; http://www.ceidis.ula.ve/cursos/ingenieria/pd_10/clases/Apunt_7 .pdf Vectores Un "agregado" es una colección de entidades almacenadas en una unidad. El "vector" es el mecanismo básico para almacenar una colección de entidades del mismo tipo. En Java el vector no es un tipo primitivo, por lo que se comporta de forma muy similar al resto de los objetos. Así, muchas de las reglas de los objetos pueden aplicarse también a los vectores (ver objeto). Cada entidad en el vector puede ser accedida mediante el "operador de indexación de vectores [ ]". Decimos que el operador [ ] indexa el vector en el sentido de que especifica qué objeto debe ser accedido. A diferencia de C y C++, el chequeo de los límites se realiza automáticamente. En Java, los vectores se indexan comenzando siempre en cero. Así, un vector "x" de tres elementos almacena x[0], x[1], y a x[2]. El número de elementos que puede ser almacenado en un vector "x" se obtiene con "x.length". Observe que en esta ocasión "no hay paréntesis" en ".length". Un bucle típico para recorrer un vector se basaría en: for ( i = 0; i < x.length; i++) instrucción1; Declaración de un vector y Asignación en un vector Un vector es un objeto, así que dada una declaración de vector : int [ ] vector1; Aún no se ha asignado memoria para guardar el vector. "vector1" es simplemente una referencia de un vector, por lo que en este momento es "null" (vacio). Para generar 100 valores de tipo int, por ejemplo, aplicaríamos la instrucción "new" como sigue: vector1 = new int [100]; existen otras formas de declarar vectores. Por ejemplo, en algunos contextos lo siguiente es aceptable: int [ ] vector2 = new int [100]; También pueden emplearse listas de inicialización, como en C o C++ para especificar valores iniciales. En el siguiente ejemplo, un vector de cuatro enteros (int) es almacenado y referenciado por "vector3"; es decir, el vectore3 es declarado e inicializado : int [ ] vector3 = {3, 4, 10, 6}; Los corchetes pueden colocarse antes o después del nombre del vector. Como puede apreciarse en, int [ ] vector1; equivale a escribir: int vector1 [ ] ; O bién en: int [ ] vector3 = {3, 4, 10, 6}; equivale a escribir int vector3 [ ] = {3, 4, 10, 6}; Situándolos antes es más sencillo ver que el nombre corresponde a un objeto de tipo vector, por lo que éste puede ser un buen estilo de programación. Declaración de un vector de objetos La declaración de un vector de objetos ( en lugar de tipos primitivos) requiere la misma sintaxis. Obsérvese, sin embargo, que cuando se guarda un vector de objetos, cada objeto alamacena inicialmente una referencia "null". Además cada una de ellas debe redefinirse para que pase a referenciar a un objeto creado. Por ejemplo, un vector de cinco botones se construye como sigue: Button [ ] vectorDeBotones; vectorDeBotones = new Button [5]; for ( i = 0; i < vectorDeBotones.length; i++) vectorDeBotones[ i ] = new Button(); Dirección y contenido de un arreglo unidimensional o Vector Un arreglo es un grupo de posiciones de memorias contiguas, todas las cuales tienen el mismo nombre y el mismo tipo. Para referirnos a una posición o elemento en particular del arreglo, especificamos el nombre del arreglo y el número de ese elemento en el arreglo. Por los que detectamos conceptos importante, como conceptos importantes en un vector en el momento de definirlo y que son: nombre del vector, el tipo de dato que contendrá cada casilla (condición todos del mismo tipo), cuantos elementos tendrá. Desde el punto de vista operativo los conceptos importantes son : su dirección y el contenido. La dirección es la posición en que se encuentran las localidades de memoria, mientras que el contenido es el valor que se asigna a esa casilla de memoria. Esto se ejemplifica en la siguiente figura: En general se puede describir al i-ésimo elemento del vector x con x [i-1] dado que por ejemplo el elemento 2 se encuentra en la dirección x [1]. Operaciones con vectores En Java pueden realizarse todas las operaciones permitidas por el tipo de datos que contiene el vector. Ejemplo 1: Tomando el vector de la figura anterior, este está definido como de tipo int, así que se pueden sumar, restar, multiplicar, dividir (recordar la división entre enteros), etc. : a = 1; b = 2; x[a + b] = x[a + b] + 2; que equivale a escribir: a = 4; b = -2; x[a + b] += 2; dando en ambos casos como resultado x[3] = 26. x[0] = (int) (Math.pow(x[0],2)) ; dando x[0] = 1. x[3] = x[3] + x[0] + x[1]; dando x[3] = 31. x[2] = x[3] / x[4]; dando x[2] = 2. Es importante recalcar que en este caso ni x[3], ni x[4] alteran su contenido. Su contenido se tomo para realizar la operación y dejarla en x[2]. Ejemplo 2: Se tiene un vector que se declara de tipo String : String [ ] nom = {"Pedro", "William", "Rafael", "Jesús"}; El vector nom contiene 4 elementos que se encuentran en memoria como sigue: nom nom nom nom [0] [1] [2] [3] Pedro William Rafael Jesús las cadenas pueden concatenarse (sumarse) como sigue: x[2] = x[3] + x[2]; dando la cadena x[2] = "Jesús Rafael". Esto último también puede escribirse como: x[2] += x[3]; Pueden asignarse valores nuevos como: x[2] = "Jorge"; x[3] = "Luga"; Si esto ocurre los valores anteriores de x[2] y x[3] ya no existen fueron substituidos por los valores recien ingresados "Jorge" y "Luga". http://www.udlap.mx/~ccastane/Syllabus_Progra_Basica/Notas_P rog_Basica/3_POO/3_1_arreglos_vectores.html ¿Qué es un Array? Un array (arreglo) es una estructura de datos en la que se almacena una colección de datos del mismo tipo. Dicho de otro modo, un array es una lista de un número finito de N elementos del mismo tipo que se caracteriza por: Almacenar los elementos en posiciones de memoria continua Tener un único nombre de variable que representa a todos los elementos y éstos a su vez se diferencian por un indice o subíndice Acceso directo o aleatorio a los elementos individuales del array. Salario nombre del array Salario [1] elemento del array Salario [2] 1,2,..., n subíndice del array : Salario [n] Los componentes individuales de un array se llaman elementos y se distingue entre ellos por el nombre de array seguido de uno o varios índices o subíndices. Para poder utilizar arrays en un programa es necesario declararlos antes de hacer cualquier operación con ellos, indicando él número y el tipo de elementos que contendrá. Ventas 1 2 4856.20 6253.21 3 5230.50 4 5 6 7 6251.40 7562.00 5423.12 6352.12 Un array se identifica por su nombre y se le asocia con un nombre de variable válida. Los arrays se clasifican en: Unidimensionales ( vectores o listas) Bidimensionales (tablas o matrices) Multidimensionales 4.2.1 Array Unidimensionales ( vectores). Un arreglo unidimensional es un tipo de datos estructurado que está formado de una colección finita y ordenada de datos del mismo tipo. Es la estructura natural para modelar listas de elementos iguales. Un array de una dimensión (unidimensional) - vector o lista - es un tipo de datos estructurado compuesto de un número de elemento finito, tamaño fijo y elementos homogéneos. Finito:indica que hay un último elemento. Tamaño fijo: significa que el tamaño del array debe ser conocido en tiempo de compilación. Homogéneo: significa que todos los elementos son del mismo tipo. Los elementos del array se almacenan en posiciones contiguas de memoria, a cada una de las cuales se puede acceder directamente y los cuales son representados en forma de una fila o columna como se muestra a continuación: Arreglo que almacena las Ventas de 5 empleados: 1 2 3 4 5 Ventas 4856.20 6253.21 5230.50 6251.40 7562.00 Ventas 1 2 4856.20 6253.21 3 5230.50 4 6251.40 5 7562.00 DECLARACION. La declaración del número y tipo de elementos se realiza de diversas maneras según cada lenguaje. En Pascal la forma de declarar un arreglo puede ser de dos formas: 1. Como una variable Var NombreArreglo: Array [1.. tamaño del vector] of tipo_de_dato; Consideremos el ejemplo de Ventas anterior: Var Ventas : Array [1..5] of real; 2. Declarando primero un nuevo tipo de dato y luego declarar un arreglo de ese tipo Type NuevoTipo = Array [1.. tamaño del vector] of tipo_de_dato; Var NombreArreglo:NuevoTipo; Consideremos el ejemplo de Ventas anterior: Type Arreglo= array [1..5] of real; Var Ventas:Arreglo; Ejemplos típicos de índices son: 1..10 ---------- enteros 'C'.. ´N´ -------- caracteres true..false ---- lógicos azul..marron- enumerados Nota: tipo_de_dato: puede ser cualquier tipo de datos (INTEGER, REAL, CHAR, etc). Operadores con arrays ( vectores) Los vectores (arrays) no se pueden leer/escribir en una sola operación o sentencia. La lectura o escritura de un array se debe hacer elemento a elemento, y para realizar estas operaciones se deben leer o visualizar los componentes de un array mediante estructuras repetitivas. Supongamos por ejemplo, que el vector Notas contiene las 30 calificaciones obtenidas en un examen por los alumnos en la asignatura de Programación. Type ListadeNotas = array [ 1..30 ] of integer; Var Notas : ListadeNotas; LECTURA DE UN VECTOR. La lectura de un vector se puede realizar con bucles implementados con las estructuras for, while y repeat. Bucle for a) for I:= 1 to 30 do readln (Notas[1]); Bucle while a) I:=1; While I<=30 do Begin Read (Notas[I]; I:=I+1; end; Bucle repeat a) I:=1; Repeat Read(Notas[I]); I:=I+1; Until I>30; Ejemplos: Type Nombres = ARRAY [1..7] Vector = ARRAY [1..10] Temperat = ARRAY [1..5] OF STRING; OF INTEGER; OF REAL; Define el tipo Nombre que serán arreglos unidimensionales de 7 elementos cadena Define el tipo Vector que serán arreglos unidimensionales de 10 elementos enteros Define el tipo Temperat que serán arreglos unidimensionales de 5 elementos reales Una vez definidos los nuevos tipos de datos podrán ser utilizados para definir las variables del nuevo tipo. Una vez declarado el arreglo (Variable de tipo arreglo), podemos realizar operaciones con ese vector, dichas operaciones se pueden realizar con un elemento individual o sobre el vector completo mediante instrucciones básicas o estructuras de control. ASIGNACIÓN. Si se desea, puede darle un valor definido a alguno de los elementos mediante la instrucción siguiente: Pseudocódigo Pascal Ventas [ 4 ] --> 1850.26 Ventas [ 4 ] : = 1850.26 Asigna el valor '1850.26' al elemento 4 del vector VENTAS LECTURA Y ESCRITURA. Si se desea leer un solo elemento del arreglo Ventas, por ejemplo, la posición 3, bastaría la siguiente sentencia: Pseudocódigo Leer ( Ventas [ 3 ] ) Escribir ( Ventas [ 3 ] ) Pascal ReadLn ( Ventas [ 3 ] ) ; WriteLn ( Ventas [ 3 ] ) ; Siguiendo con nuestro ejemplo de Ventas, el cual tiene 5 posiciones, pudiéramos pensar que si agrego las siguientes instrucciones, se habrá leído todo el arreglo: Pseudocódigo Pascal Leer ( Ventas [ 1 ] ) Leer ( Ventas [ 2 ] ) Leer ( Ventas [ 3 ] ) Leer ( Ventas [ 4 ] ) Leer ( Ventas [ 5 ] ) ReadLn ( Ventas [ 1 ] ) ; ReadLn ( Ventas [ 2 ] ) ; ReadLn ( Ventas [ 3 ] ) ; ReadLn ( Ventas [ 4 ] ) ; ReadLn ( Ventas [ 5 ] ) ; Sin embargo resultaría poco práctico tener que hacer esto para un arreglo que contiene 50 posiciones (o quizá más). Por tanto si observamos las líneas anteriores, encontramos que se repiten las mismas instrucciones, solo cambia el subíndice. Si empleamos estructuras iterativas, donde las variables de control (por ejemplo X) se utilizan como subíndices del vector (por ejemplo, Ventas[x]). El incremento del contador del bucle producirá el tratamiento sucesivo de los elementos del vector y entonces lograremos leer un arreglo de 50 (o más posiciones) con solo unas cuantas líneas, sin tener que escribir una para cada posición. Lectura, llenado o carga del vector Ventas de 5 posiciones: x <-- 1 Mientras x <= 5 hacer Leer ( ventas [x] ) Mientras x <-- x + 1 Fin mientras x <-- 0 Repetir Repetir x <-- x + 1 Leer ( ventas [x] ) Hasta que x = 5 Desde x <-- 1 hasta 5 hacer Leer ( ventas [x] ) Desde Fin desde La escritura o despliegue es similar a la tabla anterior, sustituyendo las líneas de Leer( Ventas [ x ] ) por la de Escribir (Ventas [ x ]) x <-- 1 Mientras x <= 5 hacer Escribir ( ventas [x] ) Mientras x <-- x + 1 Fin mientras x <-- 0 Repetir x <-- x + 1 Repetir Escribir ( ventas [x] ) Hasta que x = 5 Desde x <-- 1 hasta 5 hacer Escribir ( ventas [x] ) Desde Fin desde COPIA DE VECTORES. Una operación que se suele dar en ocasiones es la copia de los elementos de un vector en otro vector. Supongamos, por ejemplo, que los vectores alfa y beta se declaran con: Type Listareal= array[1..5] of real; Var Alfa,beta:Listareal; Si el vector alfa tiene asignados valores, éstos se pueden copiar en el vector beta por la sentencia. For I:= 1 to 5 do Beta[I]:= alfa [I]; Nota: En general, un array puede ser asignado a otro array solo cuando ambos tienen el mismo tipo y el mismo tamaño. Esto significa que deben ser declarados por el mismo identificador o identificadores equivalentes. VALORES MINIMO Y MAXIMO DE UN VECTOR. Supongamos que se dispone de un vector números enteros que definen en la declaración. Type ListaNúmeros = array [1..100] of integer; Var Calificaciones, Números : ListaNúmeros; I,,Maximo, N: Integer; Begin { Este programa obtiene el elemento mayor del vector Números de N elementos} N:=10; Maximo:=0; For I := 1 to N do Begin Write ('dame el valor del elemento', I); Readln (Números [ I ]); If Números [ I ] > Maximo then Maximo := Números [ I ]; End; Write('El numero mayor del vector Números es: ', Maximo); End. Arrays paralelos. Dos o más arrays que utilizan el mismo subíndice para referirse a términos homólogos se llaman arrays paralelos. Estos arrays se pueden procesar simultáneamente. Considere el caso de tener que representar dos vectores, uno para nombres de estudiantes - Nombres - y otro la nota media de un curso - promedia . Un método lógico es utilizar dos vectores en los que el primer elemento del arreglo Nombres sea el nombre, sea el nombre del alumno cuya promedio es el primer elemento de Media, y así sucesivamente. Nombres[1] Nombres[2] Nombres[3] Nombres[4] Nombres[5] Mortimer Juan Claudia Alejandra Gerardo . : . : Nombres[30]Noemi Var I : integer; Nombres : array [1..30] of string [20]; Media : array [1..30] of real: Begin For I:=1 to 30 do Begin Writeln( 'dame el nombre del alumno' ); Readln( Nombre[i] ); Writeln( 'dame el valor de la media' ); Readln( Media[I] ); Writeln( Nombre[I], Media[I]: 8:3 ); End; End. Media[1] Media[2] Media[3] Media[4] Media[5] . : Media[30] 4.234 5.634 8.734 5.734 4.754 . : 9.224 4.2.2 Array Bidimensionales (Tablas). Se puede considerar como un vector de vectores o un arreglo de arreglos unidimensionales y constituyen la forma más simple de los arreglos multidimensionales. Es, por consiguiente, un conjunto de elementos, todos del mismo tipo (homogéneo), en el cual el orden de los componentes es significativo y el acceso a ellos también es en forma directa por medio de un par de índices para poder identificar a cada elemento del arreglo. También se les llama Matriz o Tabla. Los elementos se referencian con el formato: T [3,4] elemento de la fila 3 y columna 4. Los arreglos bidimensionales se usan para representar datos que pueden verse como una tabla con filas y columnas. Matriz 1 2 3 4 5 1 2 3 15.2 4 DECLARACIÓN. Al igual que en los arrays de una dimensión (los vectores), los arrays multidimensionales (tablas) se crean con declaraciones Type y Var cuando un programa se codifica en Pascal. Podríamos definir una matriz de 4 renglones por 5 columnas que almacene datos reales de la siguiente manera: · Nombre del array · Tipo del array (recuerde que todos los elementos de un array deben ser del mismo tipo) · El rango permitido(es decir, el primero y último valor posible) por cada subíndice. 1. Como una variable Var Matriz : Array [ 1..4 , 1..5 ] of real; 2. Declarando primero un Nuevo tipo de dato y luego declarar un arreglo de ese tipo Type Mat= Array[1..4,1..5] of real; Var Matriz:Mat; Nota: Mat y Matriz, son nombres arbitráreos de identificadores ASIGNACIÓN. Se considera que este arreglo tiene dos dimensiones (un subíndice para cada dimensión) y necesita un valor para cada subíndice, y poder identificar un elemento individual. En notación estándar, normalmente el primer subíndice se refiere a la fila del arreglo, mientras que el segundo subíndice se refiere a la columna del arreglo. Es decir, Matriz(I,J), es el elemento de Matriz que ocupa la I-ésima fila y la J-ésima columna. Para tener acceso a un elemento de la matriz se tiene que especificar primero el renglón después una coma y por último la columna a la que se quiere tener acceso. Ejemplo: Matriz [ 3, 2] : = 15.2; LECTURA Y ESCRITURA. Si se deseara leer un solo elemento de un arreglo bidimensional debe especificarse el renglón y la columna a que se refiere, por ejemplo, la posición 3,2: Pseudocódigo Leer ( Ventas [ 3, 2] ) Escribir ( Ventas [ 3, 2] ) Pascal ReadLn ( Matriz [ 3, 2] ) ; WriteLn ( Matriz [ 3, 2] ) ; Pero si el objetivo es, leer o escribir la matriz completa entonces al igual que con los arreglos unidimensionales se deben usar estructuras iterativas: Ejemplo: Supóngase que una empresa tiene 6 sucursales y en cada una de ellas han contratado 10 empleados. Se desea registrar los salarios y mostrarlos posteriormente. Lectura con estructura DESDE Para x <-- 1 hasta 6 hacer inicio escribir ( " sucursal ", x ) para y <-- 1 hasta 10 hacer Desde inicio escribir ( " sueldo del empleado ", y ) leer ( salarios [x, y] ) Fin para y fin para x Escritura con estructura DESDE Para x <-- 1 hasta 6 hacer inicio escribir ( " sucursal ", x ) para y <-- 1 hasta 10 hacer Desde inicio escribir ( " sueldo del empleado ", y, " es:" ) escribir ( salarios [x, y] ) Fin para y fin para x Nota: Es importante mencionar que aunque se haya utilizado la estructura Desde, para la lectura y escritura, bien podría utilizarse cualquiera de las estructuras iterativas (Repetir o Mientras). 4.2.3 Array Multidimensionales. Hasta ahora toda la información procesada se manipula con una sola columna o lista de entrada, el llamado vector o array de una dimensión. Sin embargo, en numerosas ocasiones es necesario trabajar con tablas que tengan diferentes columnas. Por ejemplo, la siguiente tabla de temperaturas de un mapa del tiempo: Ciudad Día Madrid Jaén Granada Sevilla 1 35 40 38 41 2 34 41 37 40 3 30 38 35 42 4 36 39 37 40 5 29 42 40 39 ... ... Un sistema de estructurar los datos podría ser con cuatro arrays paralelos. Sin embargo, puede utilizarse un array de dos dimensiones que suele ser un método más eficiente generalmente. En este caso se puede emplear un array de dos dimensiones (dos subíndices), una dimensión(subíndice) correspondiente a los nombres de la fila y otro para los nombres de la columna. Así el ejemplo anterior se puede representar con: Type Ciudad=(Madrid,Jaen,Granada,Sevilla); Var Temperatura: :array [1..5,ciudad] of integer {los dos arreglos son del mismo tipo} Los datos se pueden almacenar en el array mediante sentencias de asignación de la forma: Temperatura [1, Madrid]:=35; Temperatura [1, Jaen]:=38; Los arrays se clasifican de acuerdo al número de índices o dimensiones en bidimensionales o multidimensionales propiamente dichos(más de una dimensión). Los arrays multidimensionales se declaran de igual modo que los arrays de una dimensión. http://148.202.148.5/cursos/cc102/int_programacion/MODULOIV/ tema4_2.htm ARREGLOS: Un arreglo es una colección de datos del mismo tipo, que se almacenan en posiciones consecutivas de memoria y reciben un nombre común. Para referirse a un determinado elemento de un arreglo se deberá utilizar el nombre del arreglo acompañado de un índice el cual especifica la posición relativa en que se encuentra el elemento. Los arreglos pueden ser: unidimensionales (vectores). Bidimensionales (matrices, tablas). Multidimensionales(tres dimensiones o más). ARRAY UNIDIMENSIONALES O VECTORES Los pasos para la utilización de un arreglo son; 1 Declarar el arreglo: consiste en establecer el nombre, el tamaño y el tipo de los datos que se van a almacenar en el arreglo ejemplo: hay que diferenciar dos términos : tamaño del vector (T): es el numero máximo de elementos que puede contener el arreglo. Numero de elementos(N): que indica cuantos elementos hay almacenados en el arreglo en determinado momento. Nota N<=T. T=10; Real:notas[T] 2 Llenar el arreglo con los datos: Se puede hacer en el momento de la declaración asignando al arreglo los valores que necesitamos almacenar. Ejemplo. float notas[10] = {2.3 , 3.5 , 4.2 , 3.3 , 3.0 , 4.9 , 4.2 , 3.0 , 2.0 , 1.5 }; ó recorriendo el arreglo así: para i = 1 hasta N .......lea notas[i]; fin del para 3 manipular la información guardada en el vector. Para esto es necesario recorrer dicha estructura y se puede hacer de la siguiente manera. para i = 0 hasta N ......mostrar notas[i]; fin del para las operaciones que se pueden realizar con los arreglos son las siguientes: - lectura (llenar el vector) - escritura (mostrar el vector) - asignación (dar valor a una posición específica) - actualización (inserción , eliminación, modificación ) - ordenación . (burbuja, inserción directa, selección directa, selle y quicksort). - búsqueda. (secuencial , binaria, hash( por claves) ). http://ayura.udea.edu.co/~jlsanche/vectores/vectores.htm ARREGLOS DE UNA DIMENSION Los arreglos es una herramienta maravillosa, le permite asociar un solo nombre de variable a una colección completa de datos puede mover el arreglo completo en memoria, copiarlo y además solo haciendo referencia a un solo nombre de variable un arreglo es un conjunto finito ordenado de elementos homogéneos, la propiedad de ordenación significa que es posible identificar el primero, segundo, tercero... y el enésimo elemento del arreglo, un arreglo puede ser un conjunto de elementos de tipo cadena en tanto que otro puede ser de tipo entero. Sintaxis en pascal: Type nombre_arreglo = array [rango_inicial...rango_final] of tipo_arreglo; Var Identificador: nombre_arreglo; ARREGLOS MULTIDIMENSIONALES Existen grupos de datos que se representan mejor en forma de tabla o matriz cada dos o mas subíndices a esos les llamamos arreglos multidimensionales se les llama así porque a diferencia de un arreglo bidimensional estos constan de dos o mas dimensiones. ARREGLOS BIDIMENSIONALES Un array bidimensional se puede considerar como un vector de vectores. Es decir un conjunto de elementos todos del mismo tipo, en el cual el orden de los componentes es significativo y en el que se necesitan especificar dos subíndices para poder identificar cada elemento del arreglo: Una forma importante de representar datos en un array bidimensional puede verse de forma lógica como una tabla de filas y columnas ORDENACIONES En los vectores con frecuencia es necesario clasificar u ordenar sus elementos en un orden particular La clasificación es una operación tan frecuente en programas de computación que una gran cantidad de algoritmos se han diseñado para clasificar listas de elementos con eficacia y rapidez. La ordenación o clasificación depende del tamaño del vector o array a clasificar, el tipo de datos y la cantidad de memoria disponible(Esta puede ser de forma creciente o decreciente). La ordenación puede ser de forma ascendente o descendente para datos numéricos alfabéticos o para datos de caracteres. METODO DE LA BURBUJA El algoritmo de clasificación de la burbuja se basa en el proceso de comparar pares de elementos adyacentes e intercambiarlos entre si hasta que estén todos ordenados, los pasos a dar son: Comparar el elemento ad1y ad2 si están en orden se mantienen en caso contrario se intercambia entre si. A continuación se comparan los elementos 2 y 3 de nuevo se intercambia si es necesario. El proceso continúa hasta que cada elemento del vector ha sido comparado con sus elementos adyacentes y se han realizado los intercambios necesarios. 01 01 01 01 01 01 36 36 36 36 06 06 24 24 24 06 36 36 10 10 06 24 24 24 06 06 10 10 10 10 12 12 12 12 12 12 y se sigue con 6 iteraciones hasta que este ordenado . CLASIFICACION POR SHELL Esta clasificación fue desarrollada por Daniel Shell para evitar la ineficiencia de la clasificación por burbujas para grandes matrices. La clasificación de Shell se diferencia de la clasificación por Burbuja en que se compara elementos mas separados antes de comparar los elementos adyacentes. Esto elimina gran parte del desorden de la matriz en las primeras iteraciones. La clasificación de Shell utiliza una variable llamada intervalo que inicialmente recibe un valor igual a la mitad del numero de elementos que hay en la matriz. El valor del intervalo especifica la distancia entre cada par de elementos comparables de la matriz. .44 .11 .11 .11 .11 .88 .88 .66 .66 .66 .22 .22 .22 .22 .22 .77 .77 .77 .77 .55 .33 .44 .44 .44 .44 .66 .66 .88 .88 .88 .44 .44 .44 .44 .44 .55 .55 .55 .55 .77 Los elementos separados serán inicialmente de un intervalo de 4 para este ejemplo, la primera iteración de la matriz comparara todos los elementos separados por esa distancia el proceso se repite hasta que no hay intercambios, con un intervalo de 4 y se vuelve a iniciar calculando el nuevo intervalo hasta que este llegue a 1. CLASIFICACION POR QUICK SORT Aunque la eficiencia de la clasificación Shell aumenta a medida que crece el numero de elementos también tiene limitaciones. La clasificación rápida que es un algoritmo recursivo de clasificación aumenta la velocidad a medida que el numero de elementos se aproxima a 150 o 200 , de hecho la clasificación rápida es uno de los algoritmos de clasificación que mas se utilizan en la actualidad. Si la matriz se pasa a la rutina de clasificación rápida el algoritmo seleccionara el valor contenido en [Principiolista+ Finallista]div 2 ; cualquier valor de la lista será colocado en una lista y los valores que sean mayores o iguales que el separador de lista se colocaran en una segunda lista como se muestra a continuación: 60 20 10 30 40 50 80 70 0 El orden que debe llevar las iteraciones es desde el punto medio hacia abajo y después de arriba hacia el centro después se sigue haciendo ciclos con los demás bloques. BÚSQUEDA SECUENCIAL Es la técnica de búsqueda mas simple comenzando por la cabeza de la lista se busca un determinado registro examinando cada registro secuencialmente hasta que se encuentra o la lista es agotada. Este algoritmo es adecuado tanto para listas secuenciales como para listas enlazadas. La lista no tiene que estar ordenada aunque la eficiencia de la búsqueda puede mejorarse si lo esta. ALGORITMO: Comenzar por el primer elemento de la lista mientras mas elementos en la lista y valor clave no encontrado. Obtener siguiente elemento de la lista: Si valor clave = encontrado Entonces devolver registro encontrado Si no devolver registro no encontrado BÚSQUEDA BINARIA Es uno de los algoritmos mas rápidos que usan los programadores .A diferencia de la búsqueda secuencial que examina elementos sucesivos de la matriz , la búsqueda binaria reduce el numero de elementos que deben ser examinados. Con un factor de dos en cada iteración hasta que se encuentra el registro deseado .La disminución de los tiempos de ejecución se hace mas importante al aumentar el tamaño de la matriz . La primera iteración de la búsqueda examina toda la matriz. Supongamos que las variables bajo y alto en el siguiente ejemplo reciben los valores de 0 y 9. La variable índice medio es el elemento medio del intervalo de búsqueda. Valores de [Indice_Medio] contiene el valor que se va a comparar con el valor deseado. Valor a buscar =jelipe Indice_medio =(Alto + Bajo) div 2 (9+0) div 2=4 (9+4) div2= 6 (6+4) div2 =5 Si el valor determinado en valores (índice medio) es igual al valor deseado la búsqueda se completa dando un valor verdadero a la variable encontrada. Si el valor contenido en valores (Indice Medio) es mayor que el valor deseado el algoritmo de búsqueda se modificó, ya que el valor indica que no hay razón para seguir buscando en ese punto la lista. BÚSQUEDA MEDIANTE TRANSFORMACIÓN DE CLAVES HASH L a búsqueda binaria proporciona un medio para reducir el tiempo requerido de buscar en una lista, sin embargo este método exige que los datos estén ordenados. Existen otros métodos que pueden aumentar la velocidad de búsqueda en los datos y no necesitan estar ordenados. Este otro método se denomina Hash o transformación de claves. El método Hash consiste en convertir la clave dada numérica o alfanumérica en una dirección (índice) dentro del array .La correspondencia entre las claves y la dirección en el medio de almacenamiento o el array se establece por una función de conversión (Función Hash). Existen numerosos métodos de transformación de claves todos ellos tienen en común la necesidad de convertir en direcciones .En esencia la función de conversión equivale a un calculador de direcciones .Cuando se desea localizar un elemento de clave x el indicador de direcciones indicara en que posición del array estará posicionado el elemento. TRUNCAMIENTO Ignora parte de la clave y utiliza la parte restante directamente como índice (Considerando campos no numéricos y sus códigos numéricos) .Si las claves por ejemplo, son enteros de 8 dígitos y la tabla de transformación tiene 1000 posiciones entonces el primero, segundo y quinto dígitos desde la derecha pueden formar la función de conversión. 0 Clave=72588495 : El truncamiento es un método muy rápido pero falla para distribuir las claves de modo uniforme. PLEGAMIENTO Consiste en la partición de la clave en diferentes partes y la combinación de las partes en un modo conveniente (a menudo utilizando suma o multiplicación) para obtener el índice .La clave x se divide en varias partes donde cada parte tienen el mismo numero de dígitos que la dirección especificada. 1000 000 a 999 Fx=x1+x2+x3...+xn 625 381 94 625+381+94=11100 100 ARITMÉTICA MODULAR Convertir la clave a un entero , dividir por el tamaño del rango del índice y tomar el resto como resultado. La función de conversión utilizada es MOD (Modulo o resta de división entera). Donde el mes el tamaño del arreglo con índices de 0 hasta n-1. Los valores de la función y direcciones van de 0 a n-1 ligeramente menor al tamaño del array. La mejor elección de los módulos son los números primos. M=100 F(x)= x mod 100 X=234661234 F(x)=234661234 mod 100 =34 La clave de búsqueda en una cadena de caracteres tal como el nombre para obtener direcciones de conversión el método mas simple es asignar a cada caracter de la cadena un valor entero (ejemplo A=1, B=2, C=3,etc) y sumar los valores de los caracteres en la cadena al resultado se le aplica entonces el modulo. MITAD DEL CUADRADO Este método consiste en calcular el cuadrado de la clave x. La función de conversión se define como F(x)=C donde C se obtiene eliminado dígitos de ambos extremos de x2; para todas las claves se deben usar las mismas posiciones de x2. Ejemplo: Una empresa tiene 80 empleados y cada uno de ellos tiene un numero de identificación de 4 dígitos y el conjunto de direcciones de memoria varia en un rango de 0 a 100 calcular las direcciones que se obtendrán al aplicar mitad del cuadrado. Clave x x2 4 y 5 4205 176 82 025 82 7148 510 93 904 93 3350 112 22 500 22 COLISIONES La función de operación Hash no siempre proporciona valores distintos, puede ser que para dos claves diferentes x1 y x2 se obtenga la misma dirección. Esta situación se denomina colisión y se debe de encontrar métodos para su correcta resolución. F (123445678) mod 100= 44 colisión F (123445880) mod 100= 44 RESOLUCION DE COLISIONES Si la clave transformada nos da una dirección que ya esta ocupada incrementamos el índice y examinamos el espacio siguiente. Para buscar un registro usando esta técnica de manejo de colisiones efectuamos la función Hash sobre la clave y luego comparamos la clave deseada con la real en la posición asignada. Si las claves no coinciden iniciamos una búsqueda secuencial, comenzando con la siguiente posición del array. REHASHING Otra técnica común para la resolución de colisiones se llama rehashing. Si el primer calculo de la función Hash produce una colisión se utiliza la dirección transformada como entrada para la función rehashing como entrada para la función rehashing y se calcula la nueva dirección. Clave mod 100 -> Dirección ocupada (Dirección +2) mod 100 ->Dirección 55667003 mod 100 =03 + 2= 5 mod 100 ->05 CUBOS Y ENCADENAMIENTO Otra alternativa para las técnicas de colisiones es permitir transformar múltiples claves de registros. Una solución es dejar que cada dirección transformada contenga espacios para múltiples registros en vez de un único registro. vacío vacío Vacío 453614001 302472107 Vacío vacío vacío Vacío 556677003 123450003 Vacío 123456004 445500124 222230504 234056399 vacío Vacío Otra forma para evitar este problema es usar la dirección transformada no como posición real del registro sino como un índice en un array de punteros. Cada puntero accede a una cadena de registros que participan de la misma dirección transformada en vez de buscar en el array o hacer una retransformación. MEZCLAS El proceso de mezclas es muy sencillo, el programa compara y lee los 2 registros y escribe en un archivo recién creado. A continuación se lee otro registro de datos no entradas este proceso continúa hasta que todos los registros de uno o ambos archivos hayan sido procesados. Normalmente uno de los archivos de entrada acaban con sus registros antes que otros. Cuando ocurre esto el proceso continúa leyendo registros del archivo restante y los escribe en el archivo recién creado. CLASIFICACION POR MEZCLAS DIRECTAS Por desgracia algunos algoritmos de clasificación son inaplicables si la continuidad De datos por ordenar no cabe en la memoria principal de la computadora. Algunos algoritmos de clasificación son inaplicables si la cantidad de datos por ordenar no cabe en la memoria principal de la computadora pero se le presenta por ejemplo en un dispositivo de almacenamiento periférico y secuencia como una cinta de disco. En este caso describimos los datos como un archivo secuencial cuya característica es que en cada momento un componente es accesible directamente. Esta es una restricción severa si se compara con las posibilidades que ofrecen los arreglos, por lo tanto hay que aplicar otras técnicas de clasificación la mas importante es la mezcla, mezclar significa compilar dos o mas secuencias ordenadas en una sola secuencia ordenada, es una operación mucho mas sencilla que clasificar y sirve como una operación auxiliar en el proceso mas complejo de la clasificación secuencial. Una manera de clasificar cada base en la mezcla, llamada mezcla directa es la siguiente: 1. - Dividir la secuencia A en dos mitades denominadas B y C. 2. - Mezclar B y C combinando cada elemento en pares ordenados. 3. -Llamar A a la secuencia mezclada y repetir los pasos 1 y 2 esta vez combinando los pares en cuádruplos ordenados. 4. - Repetir los pasos anteriores combinando los cuádruplos en octetos y seguir haciendo esto (Cada vez duplicando las longitudes de las subsecuencias combinadas hasta que quede ordenado a la secuencia total). A 44 55 12 42 / 94 18 06 67 B 44 55 12 42 C 94 18 06 67 A 44 94 18 55 /06 12 42 67 B 44 94 18 55 C 06 12 42 67 A 06 12 44 94 / 18 42 55 67 B 06 12 44 94 C 18 42 55 67 A 06 12 18 42 44 55 67 94 INTERCALACION CUADRATICA (SECUENCIAS EQUILIBRADAS) Este método utiliza la memoria de la computadora para realizar clasificaciones internas y cuatro archivos secuenciales temporales para trabajar. Supongamos que un archivo de entrada F que se desea ordenar por orden creciente de las claves de sus elementos se dispone de cuatro archivos secuenciales de trabajo F1, F2, F3 y F4 y que se pueden colocar n elementos en memoria central en un momento dado en una tabla T de n elementos el proceso es el siguiente: 1. - Lectura del archivo de entrada por lotes de n elementos. 2. -Ordenación de cada uno de estos bloques y escritura alternativa sobre F1 y F2. 3. - Fusión de F1 y F2 en bloques de 2 elementos que se escriben alternativamente sobre F3 y F4. 4. -Fusión de F3 y F4 y escritura alternativa en F1 y F2 en bloques de 4n elementos ordenados. 5. - El proceso consiste en doblar cada vez mas el tamaño de los bloques utilizando las parejas (F1 como F2) y (F3 como F4). F= 46 66 4 12 7 5 34 32 68 8 99 16 13 14 12 10 F1 = 4 12 46 66 8 16 68 99 F2 = 5 7 32 34 10 12 13 14 F3= 4 5 7 12 32 34 46 66 8 10 12 13 14 16 68 99 F1= 4 5 7 8 10 12 12 13 14 16 32 34 46 66 68 99 F2=Vacio MEZCLA NATURAL Primero se pone el apuntador en el primer elemento y se sigue hasta encontrar un elemento menor en el elemento en que se encuentra un elemento menor se corta el segmento y empieza otro. Estos se acomodan intercambiando los segmentos en F1 y F2. F= 32 66 | 34 72 84 96 | 48 | 31 | 24 39 | 14 F1= 32 66 48 24 39 F2=34 72 84 96 31 14 F= 32 34 6 | 48 | 24 39 72 84 96 | 31 | 14 F1= 32 34 66 24 39 72 84 96 14 F2= 48 31 http://html.rincondelvago.com/estructura-de-datos_8.html Unidad 2. Métodos y mensajes. 2.1 Atributos const y static. Clases Esta sección muesta la forma en la que se puede usar el especficador const con las clases. Puede ser interesante crear una constante local a una clase para usarla en expresiones constantes que serán evaluadas en tiempo de compilación. Sin embargo, el significado del especificador const es diferente para las clases [8], de modo que debe comprender la opciones adecuadas para crear miembros constantes en una clase. También se puede hacer que un objeto completo sea constante (y como se ha visto, el compilador siempre hace constantes los objetos temporarios). Pero preservar la consistencia de un objeto constante es más complicado. El compilador puede asegurar la consistencia de las variables de los tipos del lenguaje pero no puede vigilar la complejidad de una clase. Para garantizar dicha consistencia se emplean las funciones miembro constantes; que son las únicas que un objeto constante puede invocar. [PAG:373] const en las clases Uno de los lugares donde interesa usar const es para expresiones constantes dentro de las clases. El ejemplo típico es cuando se define un vector en una clase y se quiere usar const en lugar de #define para establecer el tamaño del vector y para usarlo al calcular datos concernientes al vector. El tamaño del vector es algo que desea mantener oculto en la clase, así que si usa un nombre como size, por ejemplo, se podría usar el mismo nombre en otra clase sin que ocurra un conflicto. El preprocesador trata todos los #define de forma global a partir del punto donde se definen, algo que const permite corregir de forma adecuada consiguiendo el efecto deseado. Se podría pensar que la elección lógica es colocar una constante dentro de la clase. Esto no produce el resultado esperado. Dentro de una clase const recupera un poco su significado en C. Asigna espacio de almacenamiento para cada variable y representa un valor que es inicializado y ya no se puede cambiar. El uso de una constante dentro de una clase significa “Esto es constante durante la vida del objeto”. Por otra parte, en cada objeto la constante puede contener un valor diferente. Por eso, cuando crea una constante ordinaria (no estática) dentro de una clase, no puede darle un valor inicial. Esta inicialización debe ocurrir en el constructor. Como la constante se debe inicializar en el punto en que se crea, en el cuerpo del constructor la constante debe estar ya inicializada. De otro modo, le quedaría la opción de esperar hasta algún punto posterior en el constructor, lo que significaria que la constante no tendría valor por un momento. Y nada impediría cambiar el valor de la constante en varios sitios del constructor. La lista de inicialización del constructor. Un punto especial de inicialización se llama “lista de inicialización del constructor” y fue pensada en un principio para su uso en herencia (tratada en el [FIXMEcapítulo 14]). La lista de inicialización del constructor (que como su nombre indica, sólo aparece en la definición del constructor) es una lista de llamadas a constructores que aparece después de la lista de argumentos del constructor y antes de abrir la llave del cuerpo del constructor. [PAG:375] Se hace así para recordarle que las inicialización de la lista sucede antes de ejecutarse el constructor. Ese es el lugar donde poner las inicializaciones de todas las constantes de la clase. El modo apropiado para colocar las constantes en una clase se muestra a continuación: <xi:include></xi:include> El aspecto de la lista de inicialización del constructor mostrada arriba puede crear confución al principio porque no es usual tratar los tipos del lenguaje como si tuvieran constructores. Constructores para los tipos del lenguaje Durante el desarrollo del lenguaje se puso más esfuerzo en hacer que los tipos definidos por el programador se pareciesen a los tipos del lenguaje, pero a veces, cuando se vió útil se hizo que los tipos empotrados (built-in se pareciesen a los definidos por el programador. En la lista de inicialización del constructor, puede tratar a los tipos del lenguaje como si tuvieran un constructor, como aquí: <xi:include></xi:include> [PAG:376] Esto es especialmente crítico cuando se inicializan atributos constantes porque se deben inicializar antes de entrar en el cuerpo de la función. Tiene sentido extender este “constructor” para los tipos del lenguaje (que simplemente significan asignación) al caso general que es por lo que la definición float funciona en el código anterior. A menudo es útil encapsular un tipo del lenguaje en una clase para garantizar la inicialización con el constructor. Por ejemplo, aquí hay una clase entero: <xi:include></xi:include> El vector de enteros declarado en main() se inicializa automaticamente a cero. Esta inicialización no es necesariamente más costosa que un bucle for o memset(). Muchos compiladores lo optimizan fácilmente como un proceso muy rápido. Las constantes en tiempo de compilación dentro de las clases. El uso anterior de const es interesante y probablemente útil en muchos casos, pero no resuelve el programa original de “como hacer una constante en tiempo de compilación dentro de una clase”. La respuesta requiere del uso de un especificador adicional que se explicará completamente en el [FIXME:capítulo 10]: static. El especificador static, en esta situación significa “hay sólo una instancia a pesar de que se creen varios objetos de la clase” que es precisamente lo que se necesita: un atributo de clase que es constante, y que no cambia de un objeto a otro de la misma clase. Por eso, una static const de un tipo del lenguaje se puede tratar como una constante en tiempo de compilación. Hay un aspecto de static const cuando se usa dentro de clases que es un tanto inusual: se debe indicar el valor inicial en el punto de la definición de la static const. Esto sólo ocurre con static const y no funciona en otras situaciones porque todos lo otros atributos deben inicializarse en el constructor o en otras funciones miembro. A continuación aparece un ejemplo que muestra la creación y uso de una static const llamada size en una clase que representa una pila de punteros a cadenas.[9] <xi:include></xi:include> [PAG:379] Como size se usa para determinar el tamaño del vector stack, es adecuado usar una constante en tiempo de compilación, pero que queda oculta dentro de la clase. Conste que push() toma un const string* como argumento, pop() retorna un const string* y StringStack contiene const string*. Si no fuera así, no podría usar una StringStack para contener los punteros de icecream. En cualquier caso, también impide hacer algo que cambie los objetos contenidos en StringStack. Por supuesto, todos los contenedores no están diseñados con esta restricción. El enumerado en codigo antiguo En versiones antiguas de C++ el tipo static const no se permitía dentro de las clases. Esto hacía que const no pudiese usarse para expresiones constantes dentro de clases. Pero muchos programadores lo conseguian con una solución típica (normalmente conocida como “enum hack”) que consiste en usar el tipo enum sin etiqueta y sin instancias. Una enumeración debe tener establecidos sus valores en tiempo de compilación, es local a una clase y sus valores están disponibles para expresiones constantes. Por eso, es común ver código como: <xi:include></xi:include> Este uso de enum garantiza que no se ocupa almacenamiento en el objeto, y que todos los símbolos definidos en la enumeración se evaluan en tiempo de compilación. Además se puede establecer explícitamente el valor de los símbolos: enum { one = 1, two = 2, three }; utilizando el tipo enum, el compilador continuará contando a partir del último valor, así que el símbolo three tendrá un valor 3. En el ejemplo StringStack anterior, la línea: static const int size = 100; podriá sustituirse por: enum { size = 100 }; Aunque es fácil ver esta tícnica en código correcto, el uso de static const fue añadido al lenguaje precisamente para resolver este problema. En todo caso, no existe ninguna razón abrumadora por la que deba usar static const en lugar de enum, y en este libro se utiliza enum porque hay más compiladores que le dan soporte en el momento en el momento en que se escribió este libro. Objetos y métodos constantes Las funciones miembro (métodos) se pueden hacer constantes. ¿Qué significa esto?. Para entenderlo, primero debe comprender el concepto de objeto constante. Un objeto constante se define del mismo modo para un tipo definido por el usuario que para un tipo del lenguaje. Por ejemplo: const int i = 1; const blob b(2); Aquí, b es un objeto constante de tipo blob, su constructor se llama con un 2 como argumento. Para que el compilador imponga que el objeto sea constante, debe asegurar que el objeto no tiene atributos que vayan a cambiar durante el tiempo de vida del objeto. Puede asegurar fácilmente que los atributos no públicos no sean modificables, pero. ¿Cómo puede saber que métodos cambiarán los atributos y cuales son seguros para un objeto constante? Si declara un método como constante, le está diciendo que la función puede ser invocada por un objeto constante. Un método que no se declara constante se trata como uno que puede modificar los atributos del objeto, y el compilador no permitirá que un objeto constante lo utilice. Pero la cosa no acaba ahí. Sólo porque una función afirme ser const no garantiza que actuará del modo correcto, de modo que el compilador fuerza que en la definición del método se reitere el especificador const (la palabra const se convierte en parte del nombre de la función, así que tanto el compilador como el enlazador comprobarán que no se viole la [FIXME:constancia]). De este modo, si durante la definición de la función se modifica algún miembro o se llama algún método no constante, el compilador emitirá un mensaje de error. Por eso, está garantizado que los miembros que declare const se comportarán del modo esperado. Para comprender la sintaxis para declarar métodos constantes, primero debe recordar que colocar const delante de la declaración del método indica que el valor de retorno es constante, así que no produce el efecto deseado. Lo que hay que hacer es colocar el especificador const después de la lista de argumentos. Por ejemplo: <xi:include></xi:include> [PAG:382] La palabra const debe incluirse tando en la declaración como en la definición del método o de otro modo el compilador asumirá que es un método diferente. Como f() es un método constante, si intenta modificar i de alguna forma o llamar a otro método que no sea constante, el compilador informará de un error. Puede ver que un miembro constante puede llamarse tanto desde objetos constantes como desde no constantes de forma segura. Por ello, debe saber que esa es la forma más general para un método (a causa de esto, el hecho de que los métodos no sean const por defecto resulta desafortunado). Un método que no modifica ningún atributo se debería escribir como constante y así se podría usar desde objetos constantes. Aquí se muestra un ejemplo que compara métodos const y métodos ordinarios: <xi:include></xi:include> Ni los constructores ni los destructores pueden ser métodos constantes porque prácticamente siempre realizarn alguna modificación en el objeto durante la inicialización o la terminación. El miembro quote() tampoco puede ser constante porque modifica el atributo lastquote (ver la sentencia de retorno). Por otra parte lastQuote() no hace modificaciones y por eso puede ser const y puede ser llamado de forma segura por el objeto constante cq. FIXME mutable: bitwise vs. logical const ¿Qué ocurre si quiere crear un método constante, pero necesita cambiar algún atributo del objeto? Esto se aplica a veces a la diferencia entre constante bitwise y constante lógica (llamado también constante memberwise ). Constante bitwise significa que todos los bits del objeto son permanentes, así que la imagen de bits del objeto nunca cambia. Constante lógica significa que, aunque el objeto completo es conceptualmente constante puede haber cambios en un member-bymember basis. Si se informa al compilador que un objeto es constante, cuidará celosamente el objeto para asegurar constancia bitwise. Para conseguir constancia lógica, hay dos formas de cambiar los atributos con un método constante. La primera solución es la tradicional y se llama constacia casting away. Esto se hace de un modo bastante raro. Se toma this (la palabra que inidica la dirección del objeto actual) y se moldea el puntero a un puntero a objeto de la clase actual. Parece que this ya es un puntero válido. Sin embargo, dentro de un método const, this es en realidad un puntero constante, así que moldeándolo a un puntero ordinario se elimina la constancia del objeto para esta operación. Aquí hay un ejemplo: <xi:include></xi:include> Esta aproximación funciona y puede verse en código correcto, pero no es la técnica ideal. El problema es que esta falta de constancia está oculta en la definición de un método y no hay ningún indicio en la interface de la clase que haga sospechar que ese dato se modifica a menos que puede accederse al código fuente (buscando el molde). Para poner todo al descubierto se debe usar la palabra mutable en la declaración de la clase para indicar que un atributo determinado se puede cambiar aún perteneciendo a un objeto constante. <xi:include></xi:include> De este modo el usuario de la clase puede ver en la declaración que miembros tienen posibilidad de ser modificados por un método. [PAG:386] Si un objeto se define como constante es un candidato para ser almacenado en memoriar de sólo lectura (ROM), que a menudo es una consideración importante en programación de sistemas empotrados. Para conseguirlo no es suficiente con que el objeto sea constante, los requisitos son mucha más estrictos. Por supuesto, el objeto debe ser constante bitwise. Eso es fácil de comprobar si la constancia lógica se implementa mediante el uso de mutable, pero probablemente el compilador no podrá detectarlo si se utiliza la técnica del moldeado dentro de un método constante. En resumen: La clase o estructura no puede tener constructores o destructor definidos por el usuario. No pueden ser clases base (tratado en el capitulo 14) u objetos miembro con constructores o destructor definidos por el usuario. El efecto de una operación de escritura en una parte del objeto constante de un tipo ROMable no está definido. Aunque un objeto pueda ser colocado en ROM de forma conveniente, no todos lo requieren. [8] Nota del traductor: Esto se conoce como polisemia del lenguaje [9] Al termino de este libro, no todos los compiladores permiten esta característica. http://es.tldp.org/Manuales-LuCAS/doc-pensarenc++/html/ch07s04.html 2.2 Concepto de método. Un método es un conjunto de instrucciones a las que se les da un determinado nombre de tal manera que sea posible ejecutarlas en cualquier momento sin tenerlas que reescribir sino usando sólo su nombre. A estas instrucciones se les denomina cuerpo del método, y a su ejecución a través de su nombre se le denomina llamada al método. La ejecución de las instrucciones de un método puede producir como resultado un objeto de cualquier tipo. A este objeto se le llama valor de retorno del método y es completamente opcional, pudiéndose escribir métodos que no devuelvan ninguno. La ejecución de las instrucciones de un método puede depender del valor de unas variables especiales denominadas parámetros del método, de manera que en función del valor que se dé a estas variables en cada llamada la ejecución del método se pueda realizar de una u otra forma y podrá producir uno u otro valor de retorno. Al conjunto formado por el nombre de un método y el número y tipo de sus parámetros se le conoce como signatura del método. La signatura de un método es lo que verdaderamente lo identifica, de modo que es posible definir en un mismo tipo varios métodos con idéntico nombre siempre y cuando tengan distintos parámetros. Cuando esto ocurre se dice que el método que tiene ese nombre está sobrecargado. http://www.clikear.com/manuales/csharp/c64.asp Definición de métodos Para definir un método hay que indicar tanto cuáles son las instrucciones que forman su cuerpo como cuál es el nombre que se le dará, cuál es el tipo de objeto que puede devolver y cuáles son los parámetros que puede tomar. Esto se indican definiéndolo así: <tipoRetorno> <nombreMétodo>(<parámetros>) { <cuerpo> } En <tipoRetorno> se indica cuál es el tipo de dato del objeto que el método devuelve, y si no devuelve ninguno se ha de escribir void en su lugar. Como nombre del método se puede poner en <nombreMétodo> cualquier identificador válido. Como se verá más adelante en el Tema 15: Interfaces, también es posible incluir en <nombreMétodo> información de explicitación de implementación de interfaz, pero por ahora podemos considerar que siempre será un identificador. Aunque es posible escribir métodos que no tomen parámetros, si un método los toma se ha de indicar en <parámetros> cuál es el nombre y tipo de cada uno de ellos, separándolos con comas si son más de uno y siguiendo la sintaxis que más adelante en este mismo tema es explica. El <cuerpo> del método también es opcional, pero si el método retorna algún tipo de objeto entonces ha de incluir al menos una instrucción return que indique cuál es el objeto a devolver. La sintaxis anteriormente vista no es la que se usa para definir métodos abstractos. Como ya se vio en el Tema 5: Clases, en esos casos lo que se hace es sustituir el cuerpo del método y las llaves que lo encierran por un simple punto y coma (;) Más adelante en este tema veremos que eso es también lo que se hace para definir métodos externos. A continuación se muestra un ejemplo de cómo definir un método de nombre Saluda cuyo cuerpo consista en escribir en la consola el mensaje "Hola Mundo" y que devuelva un objeto int de valor 1: int Saluda() { Console.WriteLine("Hola Mundo"); return 1; } 2.3 Declaración de métodos. 2.4 Llamadas a métodos (mensajes). Llamada a métodos La forma en que se puede llamar a un método depende del tipo de método del que se trate. Si es un método de objeto (método no estático) se ha de usar la notación: <objeto>.<nombreMétodo>(<valoresParámetros>) El <objeto> indicado puede ser directamente una variable del tipo de datos al que pertenezca el método o puede ser una expresión que produzca como resultado una variable de ese tipo (recordemos que, debido a la herencia, el tipo del <objeto> puede ser un subtipo del tipo donde realmente se haya definido el método); pero si desde código de algún método de un objeto se desea llamar a otro método de ese mismo objeto, entonces se ha de dar el valor this a <objeto>. En caso de que sea un método de tipo (método estático), entones se ha de usar: <tipo>.<nombreMétodo>(<valoresParámetros>) Ahora en <tipo> ha de indicarse el tipo donde se haya definido el método o algún subtipo suyo. Sin embargo, si el método pertenece al mismo tipo que el código que lo llama entonces se puede usar la notación abreviada: <nombreMétodo>(<valoresParámetros>) El formato en que se pasen los valores a cada parámetro en <valoresParámetros> a aquellos métodos que tomen parámetros depende del tipo de parámetro que sea. Esto se explica en el siguiente apartado. Tipos de parámetros. Sintaxis de definición La forma en que se define cada parámetro de un método depende del tipo de parámetro del que se trate. En C# se admiten cuatro tipos de parámetros: parámetros de entrada, parámetros de salida, parámetros por referencia y parámetros de número indefinido. Parámetros de Un parámetro de entrada recibe una copia del valor que almacenaría una variable del tipo del objeto que se le pase. Por tanto, si el objeto es de un tipo valor se le pasará una copia del objeto y cualquier modificación que se haga al parámetro dentro del cuerpo del método no afectará al objeto original sino a su copia; mientras que si el objeto es de un tipo referencia entonces se le pasará una copia de la referencia al mismo y cualquier modificación que se haga al parámetro dentro del método también afectará al objeto original ya que en realidad el parámetro referencia a ese mismo objeto original. Para definir un parámetro de entrada basta indicar cuál el nombre que se le desea dar y el cuál es tipo de dato que podrá almacenar. Para ello se sigue la siguiente sintaxis: <tipoParámetro> <nombreParámetro> Por ejemplo, el siguiente código define un método llamado Suma que toma dos parámetros de entrada de tipo int llamados par1 y par2 y devuelve un int con su suma: int Suma(int par1, int par2) { return par1+par2; } Como se ve, se usa la instrucción return para indicar cuál es el valor que ha de devolver el método. Este valor es el resultado de ejecutar la expresión par1+par2; es decir, es la suma de los valores pasados a sus parámetros par1 y par2 al llamarlo. En las llamadas a métodos se expresan los valores que se deseen dar a este tipo de parámetros indicando simplemente el valor deseado. Por ejemplo, para llamar al método anterior con los valores 2 y 5 se haría <objeto>.Suma(2,5), lo que devolvería el valor 7. Todo esto se resume con el siguiente ejemplo: using System; class ParámetrosEntrada { public int a = 1; public static void F(ParémetrosEntrada p) { p.a++; } public static void G(int p) { p++; } public static void Main() { int obj1 = 0; ParámetrosEntrada obj2 = new ParámetrosEntrada(); G(obj1); F(obj2); Console.WriteLine("{0}, {1}", obj1, obj2.a); } } Este programa muestra la siguiente salida por pantalla: 0, 2 Como se ve, la llamada al método G() no modifica el valor que tenía obj1 antes de llamarlo ya que obj1 es de un tipo valor (int) Sin embargo, como obj2 es de un tipo referencia (ParámetrosLlamadas) los cambios que se le hacen dentro de F() al pasárselo como parámetro sí que le afectan. Parámetros de salida Un parámetro de salida se diferencia de uno de entrada en que todo cambio que se le realice en el código del método al que pertenece afectará al objeto que se le pase al llamar dicho método tanto si éste es de un tipo por como si es de un tipo referencia. Esto se debe a que lo que a estos parámetros se les pasa es siempre una referencia al valor que almacenaría una variable del tipo del objeto que se les pase. Cualquier parámetro de salida de un método siempre ha de modificarse dentro del cuerpo del método y además dicha modificación ha de hacerse antes que cualquier lectura de su valor. Si esto no se hiciese así el compilador lo detectaría e informaría de ello con un error. Por esta razón es posible pasar parámetros de salida que sean variables no inicializadas, pues se garantiza que en el método se inicializarán antes de leerlas. Además, tras la llamada a un método se considera que las variables que se le pasaron como parámetros de salida ya estarán inicializadas, pues dentro del método seguro que se las inicializació. Nótese que este tipo de parámetros permiten diseñar métodos que devuelvan múltiples objetos: un objeto se devolvería como valor de retorno y los demás se devolverían escribiendos en los parámetros de salida. Los parámetros de salida se definen de forma parecida a los parámetros de entrada pero se les ha de añadir la palabra reservada out. O sea, se definen así: out <tipoParámetro> <nombreParámetro> Al llamar a un método que tome parámetros de este tipo también se ha preceder el valor especificado para estos parámetros del modificador out. Una utilidad de esto es facilitar la legibilidad de las llamadas a métodos. Por ejemplo, dada una llamada de la forma: a.f(x, out z) Es fácil determinar que lo que se hace es llamar al método f() del objeto a pasándole x como parámetro de entrada y z como parámetro de salida. Además, también se puede deducir que el valor de z cambiará tras la llamada. Sin embargo, la verdadera utilidad de forzar a explicitar en las llamadas el tipo de paso de cada parámetro es que permite evitar errores derivados de que un programador pase una variable a un método y no sepa que el método la puede modificar. Teniéndola que explicitar se asegura que el programador sea consciente de lo que hace. Parámetros por referencia Un parámetro por referencia es similar a un parámetro de salida sólo que no es obligatorio modificarlo dentro del método al que pertenece, por lo que será obligatorio pasarle una variable inicializada ya que no se garantiza su inicialización en el método. Los parámetros por referencia se definen igual que los parámetros de salida pero sustituyendo el modificador out por el modificador ref. Del mismo modo, al pasar valores a parámetros por referencia también hay que precederlos del ref. Parámetros de número indefinido C# permite diseñar métodos que puedan tomar cualquier número de parámetros. Para ello hay que indicar como último parámetro del método un parámetro de algún tipo de tabla unidimensional o dentada precedido de la palabra reservada params. Por ejemplo: static void F(int x, params object[] extras) {} Todos los parámetros de número indefinido que se pasan al método al llamarlo han de ser del mismo tipo que la tabla. Nótese que en el ejemplo ese tipo es la clase primigenia object, con lo que se consigue que gracias al polimorfismo el método pueda tomar cualquier número de parámetros de cualquier tipo. Ejemplos de llamadas válidas serían: F(4); // Pueden pasarse 0 parámetros indefinidos F(3,2); F(1, 2, "Hola", 3.0, new Persona()); F(1, new object[] {2,"Hola", 3.0, new Persona}); El primer ejemplo demuestra que el número de parámetros indefinidos que se pasen también puede ser 0. Por su parte, los dos últimos ejemplos son totalmente equivalentes, pues precisamente la utilidad de palabra reservada params es indicar que se desea que la creación de la tabla object[] se haga implícitamente. Es importante señalar que la prioridad de un método que incluya el params es inferior a la de cualquier otra sobrecarga del mismo. Es decir, si se hubiese definido una sobrecarga del método anterior como la siguiente: static void F(int x, int y) {} Cuando se hiciese una llamada como F(3,2) se llamaría a esta última versión del método, ya que aunque la del params es también aplicable, se considera que es menos prioritaria. Sobrecarga de tipos de parámetros En realidad los modificadores ref y out de los parámetros de un método también forman parte de lo que se conoce como signatura del método, por lo que esta clase es válida: class Sobrecarga { public void f(int x) {} public void f(out int x) {} } Nótese que esta clase es correcta porque cada uno de sus métodos tiene una signatura distinta: el parámetro es de entrada en el primero y de salida en el segundo. Sin embargo, hay una restricción: no puede ocurrir que la única diferencia entre la signatura de dos métodos sea que en uno un determinado parámetro lleve el modificador ref y en el otro lleve el modificador out. Por ejemplo, no es válido: class SobrecargaInválida { public void f(ref int x) {} public void f(out int x) {} } 2.5 Tipos de métodos. Métodos externos Un método externo es aquél cuya implementación no se da en el fichero fuente en que es declarado. Estos métodos se declaran precediendo su declaración del modificador extern. Como su código se da externamente, en el fuente se sustituyen las llaves donde debería escribirse su cuerpo por un punto y coma (;), quedando una sintaxis de la forma: extern <nombreMétodo>(<parámetros>); La forma en que se asocie el código externo al método no está definida en la especificación de C# sino que depende de la implementación que se haga del lenguaje. El único requisito es que no pueda definirse un método como abstracto y externo a la vez, pero por todo lo demás puede combinarse con los demás modificadores, incluso pudiéndose definir métodos virtuales externos. La forma más habitual de asociar código externo consiste en preceder la declaración del método de un atributo de tipo System.Runtime.InteropServices.DllImport que indique en cuál librería de enlace dinámico (DLL) se ha implementado. Este atributo requiere que el método externo que le siga sea estático, y un ejemplo de su uso es: using System.Runtime.InteropServices; DllImport public class Externo { [DllImport("kernel32")] // Aquí está definido public static extern void CopyFile(string fuente, string destino); public static void Main() { CopyFile("fuente.dat", "destino.dat"); } } El concepto de atributo se explica detalladamente en el Tema 14:Atributos. Por ahora basta saber que los atributos se usan de forman similar a los métodos sólo que no están asociados a ningún objeto ni tipo y se indican entre corchetes ([]) antes de declaraciones de elementos del lenguaje. En el caso concreto de DllImport lo que indica el parámetro que se le pasa es cuál es el fichero (por defecto se considera que su extensión es .dll) donde se encuentra la implementación del método externo a continuación definido. Lo que el código del ejemplo anterior hace es simplemente definir un método de nombre CopyFile() cuyo código se corresponda con el de la función CopyFile() del fichero kernel32.dll del API Win32. Este método es llamado en Main() para copiar el fichero de nombre fuente.dat en otro de nombre destino.dat. Nótese que dado que CopyFile() se ha declarado como static y se le llama desde la misma clase donde se ha declarado, no es necesario precederlo de la notación <nombreClase>. para llamarlo. Como se ve, la utilidad principal de los métodos externos es permitir hacer llamadas a código nativo desde código gestionado, lo que puede ser útil por razones de eficiencia o para reutilizar código antiguamente escrito pero reduce la portabilidad de la aplicación. http://www.programacion.com/tutorial/csharp/9/ 2.5.1 Métodos const, static. El modificador "static" indica que un atributo solo se instancia una vez. Es decir, todos los objetos de esa clase comparten ese mismo valor o método, que se crea con la primera instancia de la clase. Un método "static" puede ser ejecutado tambien desde el nombre de la clase, sin tener que hacer una instancia de la clase manualmente. De esta forma trabajan los métodos: System.out.println (); Integer.parseInt (); etc. Ahora bien, hay que tener cuidado, porque no se puede llamar a métodos no estaticos (métodos de instancia) desde métodos estaticos (métodos de clase). http://www.javahispano.org/faq.thread.action?forum=102&thread =214848691&id=214848691 Ver archivo: metodoconst.pdf 2.5.2 Métodos normales y volátiles. 2.6 Referencia this. Constructores y referencia this Un constructor, básicamente inicilializa los atributos de nuestro objeto. Cuando no se declara explícitamente un constructor , los atributos se inicializan por defecto. En la clase siguiente se crearán tres constructores diferentes, según qué atributos nos interesa inicializar con un valor específico. class Libro { public String titulo; public String autor; public int paginas; public int precio; } El primer constructor recibe como parámetro el título del libro. public Libro(String aux_titulo) { titulo = aux_titulo; } El segundo constructor, recibe como parámetros el título y el autor del libro. Cuando los parámetros se llaman igual que los atributos del objeto, se usa la referencia this que hace referencia sobre el objeto actual, no permitiendo la confusión de nombres. public Libro(String titulo , String autor) { this.titulo = titulo; this.autor = autor; } Por último, el tercer constructor permite inicializar todos los atributos. Para ahorrar código, se hace una llamada al segundo constructor mediante la referencia this , para inicilizar los atributos título y autor. public Libro(String titulo , String autor, int paginas , int precio) { this(titulo , autor); this.paginas = paginas; this.precio = precio; } Implementación Se crearán tres objetos de tipo Libro usando los tres constructores , y se va a imprimir los atributos de los tres objetos. class Libro { public String titulo; public String autor; public int paginas; public int precio; /* -- constructores -- */ public Libro(String aux_titulo) { titulo = aux_titulo; } public Libro(String titulo , String autor) { this.titulo = titulo; this.autor = autor; } public Libro(String titulo , String autor, int paginas , int precio) { this(titulo , autor); this.paginas = paginas; this.precio = precio; } /* -- metodo que muestra la informacion en forma tabulada -- */ public static void impLibro(Libro aux) { System.out.print("\n " + aux.titulo); System.out.print("\t\t"); if (aux.autor != null) System.out.print(aux.autor); System.out.print("\t\t"); if (aux.paginas != 0) System.out.print(aux.paginas); System.out.print("\t\t"); if (aux.precio != 0) System.out.print(aux.precio); } public static void main(String arg[ ]) { Libro a = new Libro("Los miserables"); Libro b = new Libro("El contrato social","Rousseau"); Libro c = new Libro("La divina comedia","Dante",320,8900); System.out.println("\n\n Titulo\t\t\tAutor\t\tPaginas\t\tPrecio"); System.out.println(" ------\t\t\t-----\t\t-------\t\t------"); impLibro(a); impLibro(b); impLibro(c); System.out.println("\n"); } } http://pjsml.50megs.com/java/objbasic.html La referencia this En ocasiones es conveniente disponer de una referencia que apunte al propio objeto que se está manipulando. Esto se consigue con la palabra reservada this. this es una referencia implicita que tienen todos los objetos y que apunta a si mismo. Por ejemplo: class Circulo { Punto centro; int radio; . . . Circulo elMayor(Circulo c) { if (radio > c.radio) return this; else return c; } } El método elMayor devuelve una referencia al círculo que tiene mayor radio, comparando los radios del Circulo c que se recibe como argumento y el propio. En caso de que el propio resulte mayor el método debe devolver una referencia a si mismo. Esto se consigue con la expresión return this. http://www.arrakis.es/~abelp/ApuntesJava/ClasesIV.htm#La%20r eferencia%20this La referencia this Java incluye un valor de referencia especial llamado this, que se utiliza dentro de cualquier método para referirse al objeto actual. El valor this se refiere al objeto sobre el que ha sido llamado el método actual. Se puede utilizar this siempre que se requiera una referencia a un objeto del tipo de una clase actual. Si hay dos objetos que utilicen el mismo código, seleccionados a través de otras instancias, cada uno tiene su propio valor único de this. Un refinamiento habitual es que un constructor llame a otro para construir la instancia correctamente. El siguiente constructor llama al constructor parametrizado MiPunto(x,y) para terminar de iniciar la instancia: MiPunto() { this( -1, -1 ); // Llama al constructor parametrizado } En Java se permite declarar variables locales, incluyendo parámetros formales de métodos, que se solapen con los nombres de las variables de instancia. No se utilizan x e y como nombres de parámetro para el método inicia, porque ocultarían las variables de instancia x e y reales del ámbito del método. Si lo hubiésemos hecho, entonces x se hubiera referido al parámetro formal, ocultando la variable de instancia x: void inicia2( int x, int y ) { x = x; // Ojo, no modificamos la variable de instancia!!! this.y = y; // Modificamos la variable de instancia!!! } http://pisuerga.inf.ubu.es/lsi/Invest/Java/Tuto/II_5.htm 2.7 Forma de pasar argumentos. Parámetros y argumentos §1 Sinopsis Las palabras parámetro y argumento, aunque de significado similar, tiene distintas connotaciones semánticas: Se denominan parámetros los objetos declarados en el prototipo 4.4.1 (que deben corresponder con la definición 4.4.2). Cuando se realiza una llamada a la función, los "valores" pasados se denominan argumentos. A veces se utilizan también las expresiones argumentos formales, para los parámetros y argumentos actuales para los valores pasados. Parámetros (en prototipo o definición) argumentos formales Valores pasados (en tiempo de ejecución) argumentos actuales §2 La sintaxis utilizada para la declaración de la lista de parámetros formales es similar a la utilizada en la declaración de cualquier identificador. A continuación se exponen varios ejemplos: int func(void) {...} // sin parámetros inf func() {...} // ídem. int func(T1 t1, T2 t2, T3 t3=1) {...} // tres parámetros simples, // uno con argumento por defecto int func(T1* ptr1, T2& tref) {...} // los argumentos son un puntero y // una referencia. // Petición de uso int func(register int i) {...} de registro para // argumento (entero) int func(char *str,...) {...} /* Una cadena y cierto número de otros argumentos, o un número fijo de argumentos de tipos variables */ §3 Los argumentos son siempre objetos. Sus tipos pueden ser: escalares; estructuras; uniones, o enumeraciones; clases definidas por el usuario; punteros o referencias a estructuras y uniones, o punteros a funciones, a clases o a matrices. El tipo void está permitido como único parámetro formal. Significa que la función no recibe ningún argumento. Nota: Recuerde que cuando coloquialmente se dice que se pasa una matriz como argumento de una función, en realidad se está pasando un puntero a su primer elemento ( 4.3.8). §3.1 Es un error realizar una declaración en la lista de parámetros [2]: int func (int x, class C{...} c) { ... } // Error!! §4 Todos los parámetros de una función tienen ámbito del bloque de la propia función y la misma duración automática que la función ( 4.1.5). §5 El único especificador de almacenamiento que se permite es register ( 4.1.8b). En la declaración de parámetros también pueden utilizarse los modificadores volatile ( 4.1.9) y const ( 3.2.1c). Este último se utiliza cuando se pasan argumentos por referencia y queremos garantizar que la función no modificará el valor recibido. Ejemplo: int dimension(X x1, const X& x2) modificar!! // x2 NO se puede §6 Argumentos por defecto C++ permite tener valores por defecto para los parámetros. Esto supone que, si no se pasa el parámetro correspondiente, se asume un valor predefinido [1]. La forma de indicarlo es declararlo en el prototipo de la función, como se muestra en el ejemplo (ambas expresiones son equivalentes). float mod (float x, float y = 0); float mod (float, float = 0); Más tarde no es necesario, ni posible (§6.1 ejemplo, es válido: ), indicarlo de nuevo en la definición. Por float mod (float, float = 0); // prototipo ... float mod (float x, float y = 0) { return x + y; } // Error!! float mod (float x, float y) { return x + y; } // definición Ok Si declaración y definición están en una sola sentencia entonces si es necesario indicar los valores por defecto. Ejemplo, en la sentencia: float mod (float x, float y = 0) { return pow(x*x + y*y, 0.5); } la ausencia de un segundo argumento en la invocación hace que se adopte para él un valor 0 (podría haber sido cualquier otro valor). En este contexto la función mod aceptaría estas dos llamadas como correctas y de resultados equivalentes: m1 = mod (2.0 , 0); m2 = mod (2.0); §6.1 Un argumento por defecto no puede ser repetido o cambiado en una siguiente declaración dentro del mismo ámbito. Por ejemplo: void func (int x = 5); ... void func (int x = 5); por defecto { void func (x = 7); oculta a la anterior } visible void func (x = 7); defecto // Error: repetición de argumento // nuevo ámbito // L.4 Correcto: esta función // el ámbito anterior vuelve a ser // Error: cambiar argumento por Nota: A pesar de que la expresión de la línea 4 es correcta, tenga en cuenta que las declaraciones en ámbitos anidados que ocultan declaraciones del mismo nombre en ámbitos externos suele ser fuente de errores y confusiones. Es muy de tener en cuenta esta regla en la definición de clases, ya que en ellas es frecuente que la declaración de métodos se realice en un punto (el cuerpo de la clase), y la definición se realice en otro ("off-line", fuera del cuerpo de la clase). En caso que el método tenga argumentos por defecto recuerde no repetirlos más tarde en la definición. §6.2 La gramática de C++ exige que los parámetros con valores por defecto deben ser los últimos en la lista de parámetros, y que si en una ocasión falta algún argumento, los que le siguen también deben faltar (adoptar también los valores por defecto). Nota: como puede verse, C++ no admite la posibilidad de otros lenguajes de saltarse parámetros por defecto incluyendo una coma sin ningún valor en la posición correspondiente, por ejemplo: int somefunc .... x = somefunc aceptable en x = somefunc (int, int = 1, char, long); // Incorrecto!! ( 33, , 'c', 3.14); C++ (int, char*, char* = 0); // Error! No Observe que en este último caso, el espacio de separación entre char* y = es importante, ya que *= es un operador de asignación ( 4.9.2). Así pues: x = somefunc (int, char*, char*= 0); // Error! §6.3 Los argumentos por defecto de métodos (funciones-miembro de clases 4.11) no pueden ser otros miembros a no ser que sean estáticos ( 4.11.7). Ejemplo: class C { int v1; void foo(char, int = v1); }; class B { static int v1; void foo(char, int = v1); } // Error!! // Ok. Un posible diseño de la clase C podría ser: class C { int v1; void foo(char, int = -1); }; // Ok más tarde, en la definición del método hacer: void C::foo(char a, int x) { if (x == -1) x = v1; ... } §6.4 Los argumentos por defecto no pueden ser otros argumentos: x = somefunc (int x, int y = x); // Error! §6.5 Los argumentos pasados por referencia (§7.3 ) solo pueden adoptar valores por defecto estáticos, globales, o de un subespacio cualificado. Ejemplo: namespace ALPHA { long n = 20.10L; } long n = 10.10L; void f1 (long& lg = ALPHA::n); void f2 (long& lg = n); void f3 (long lg = 10.2L); void f4 (long& lg = 10.2L ); // // // // Ok. Ok. Ok. Error!! §7 Argumentos: por valor y por referencia Existen dos formas de pasar argumentos a las funciones: por valor y por referencia. El primero es utilizado por defecto con la declaración usual de parámetros. En el paso "por valor", se crean copias de los argumentos pasados a la función, los cuales, junto a las variables locales (incluyendo el posible valor devuelto) y la dirección de vuelta a la rutina que efectúa la invocación, son pasados a la pila en la secuencia de llamada. Más tarde, cuando termina su ejecución definitivamente, es decir, cuando el control vuelve a la función que la invocó, toda esta información es sacada de la pila mediante la secuencia de retorno (y se pierde). Estos procesos suponen un consumo de tiempo y espacio (memoria), a veces considerable. §7.1 Paso por valor Hemos visto que el paso de parámetros por valor significa que existen copias de los argumentos formales (estas copias son variables locales de la función llamada), y que una función no puede alterar ninguna variable de la función que la invocó. La única excepción es el caso de las matrices. Cuando se utiliza una matriz como argumento en la llamada a una función, el valor pasado es un puntero a la dirección de memoria del principio de la matriz ( 4.3.2). §7.1.1 Cuando los argumentos pasan por valor pero no hay concordancia entre el tipo de los argumentos actuales y los argumentos formales utilizados en la declaración de la función, entonces se produce un modelado de tipo antes de la asignación. Supongamos el ejemplo: void func(int x) { entero x = x * 2; } float f = 3.14; func(f); asignación a 'x' // definición de func. Acepta un // f es promovido a int antes de // x == 6 Lo que sucede en estos casos es que la copia local f en func (x) es modificada para hacerla coincidir con el tipo esperado por la función, mientras que el valor original (f) permanece inalterado. §7.2 Pasar un puntero En C clásico, cuando se desea que la función llamada pueda alterar el valor de variables de la función que la invoca, o ahorrar el espacio que supone la copia local de los argumentos (que pueden ser estructuras de datos muy grandes), la solución consistía en utilizar punteros a las variables respectivas como argumentos para la función (en vez de pasar las variables en sí mismas). A su vez, la función llamada debía declarar el parámetro como puntero, y acceder a la variable indirectamente a través de él. En otras palabras: Cuando en C se desea que un valor X pase a una función F y que esta pueda alterar el valor de X en la función que la invocó, el argumento utilizado es &X (la dirección de X). De esta forma, aunque F recibe una copia de &X, puede alterar el valor original a través de esta dirección. Esta técnica puede tener sus ventajas. Por ejemplo, si X es una estructura muy grande, pero puede tener efectos colaterales peligrosísimos y ser una fuente de errores difíciles de detectar. §7.3 Paso por referencia Por supuesto, C++ permite utilizar la técnica del C clásico descrita arriba, pero también utilizar el paso de argumentos por referencia (en realidad es una variante semántica del proceso anteriormente descrito). Para ello se utiliza el declarador de referencia & ( 4.2.3). Las referencias presentan las ventajas de los punteros, en el sentido que permiten modificar los valores de los objetos pasados como argumento, y de que permiten ahorrar espacio si hay que pasar objetos muy grandes, pero no presentan los peligros potenciales de aquellos. En caso necesario las referencias pueden declararse constantes, indicando así que la función invocada no modificará estos valores. En estos casos, la utilización de referencias obedece casi exclusivamente a razones de eficacia en el mecanismo de llamada ( 4.2.3). Nota: En ocasiones el paso por referencia tiene una justificación de tipo físico. Es el caso en que los objetos utilizados como argumento representan dispositivos físicos. Por ejemplo, ficheros externos o dispositivos de comunicación. En estas circunstancias, el objeto no puede ser copiado alegremente por el mecanismo de invocación de funciones (que utilizaría el constructor-copia de la clase) si se utilizaran pasos por valor y es necesario recurrir al paso por referencia. Compare las tres implementaciones de la función pru: Implementación-1: Sistema clásico, paso "por valor" int pru1(int n) { return 3 * n; } ... int x, i = 4; x = pru1(i); int& ry = i; x = pru (ry); // n entero; pasa "por valor" // ahora: x = 12, i = 4 // ahora: x = 12, i = 4 Observe que la última sentencia no es un paso por referencia, sino por valor (a pesar de que el argumento actual sea una referencia). Implementación-2: Sistema clásico, paso de "punteros por valor" (seudo-referencia) void pru2(int* np) { valor" *np = (*np) * 3; } . . . int x = 4; pru2(&x); // np puntero-a-entero; pasa "por // ahora x = 12 Observe que en este caso, pasar el valor &x (dirección de x) como argumento, es equivalente a pasar un puntero a dicha variable (que es lo exigido en la definición de pru2). Es decir, la última línea se puede sustituir por las siguientes: int* ptr = &x pru2(ptr); Implementación-3: // define puntero-a-x // pasa el puntero como argumento Sistema C++, paso "por referencia" void pru3(int& n) { // n tipo "referencia-a-int"; pasa "por referencia" n = 3 * n; } . . . int x = 4; pru3(x); // ahora x = 12 Atención a la sintaxis: Aquí la invocación a pru3 tiene la forma: pru3(x) no pru3(&x) como en el caso anterior. Es decir, la notificación al compilador de que el argumento pasa por referencia hay que hacerla en la definición de la función, y no es necesario indicarlo en el momento de la invocación. En este último caso, la declaración int& n como parámetro de la función pru3, establece que este n sea declarado como "referencia-a-entero", de forma que cuando se pasa el argumento x, la función crea un valor n que es una especie de alias o espejo de x, de forma que la expresión n = 3*n tiene el mismo efecto que x = 3*x. §7.3.1 Ya hemos visto (Referencias 4.2.3) que cuando, en la declaración de una referencia, el iniciador es una constante, o un objeto de tipo diferente que el referenciado, se crea un objeto temporal para el que la referencia actúa como un alias. Esta creación de objetos temporales es lo que permite la conversión de tipos referenciaa-tipoX cuando se utilizan como parámetros de funciones y no hay concordancia entre el valor recibido y el esperado (suponiendo que exista posibilidad de conversión). Este sería el mecanismo utilizado en el siguiente caso: void pru(int& n) { // n tipo "referencia-a-int" (pasa "por referencia") n = 3 * n; } . . . float f = 4.1; pru(f); // ahora f = 12 §7.3.2 Ejemplo En el programa que sigue se muestra un caso de paso por referencia y acceso a subespacios. #include <iostream.h> namespace ALPHA { class CAlpha { int x; public: int getx(void) { return x; } void putx(int i) { x = i; } }; CAlpha CA1; // instancia de CAlpha (en ALPHA) } int func (ALPHA::CAlpha& ca, int i); /* prototipo: ca es la referencia a un objeto de la clase CAlpha en ALPHA */ int main (void) { // ======================== int x = 0; cout << "x = " << x << endl; ALPHA::CA1.putx(10); // invocación al método putx de CA1 x = func(ALPHA::CA1, 3); cout << "x = " << x << endl; } int func (ALPHA::CAlpha& ca, int i) { return (i + ca.getx()); } // definición Salida: x = 0 x = 13 http://www.zator.com/Cpp/E4_4_5.htm Cómo se pasan los argumentos? Una de las propiedades importantes de las funciones y de las subrutinas es que la información puede ser pasada a ellas cuando son llamadas y devolverla, al lugar donde fueron llamadas, cuando finalice su ejecución. El paso de esta información será llevado a cabo por los llamados argumentos. Existe una correspondencia uno a uno entre los argumentos del programa, llamados argumentos actuales, y los argumentos de las subrutinas o funciones, llamados falsos argumentos. Estos últimos, no tienen que tener el mismo nombre que los otros y la correspondencia es temporal, es decir, sólo es válida durante la llamada al procedimiento. Además, el número de ambos argumentos tiene que coincidir, salvo que declaremos a algún falso argumento como opcional, es decir, que pongamos, por ejemplo: Real, opcional :: x De esta forma el argumento x, tanto puede ser pasado como argumento, como no. Por otro lado, también es posible identificar a los argumentos con una letra, de esta forma podremos cambiar la correspondencia por defecto entre los argumentos, y realizarla de una forma más clara. Por ejemplo, podríamos llamar a una función Func (a, b, c) de las siguientes formas: Func (5, a = 1, c = 0) Func (5, 0, a = 1) o Func (1, 5, 0) Como vemos es posible identificar a algunos argumentos y a otros no. En estos casos, los argumentos que no estén identificados tendrán que ir ordenados entre sí. En cualquiera de los casos, a = 1, b = 5 y c = 0. Por otra parte, el tipo y el parámetro de clase de cada argumento actual deben coincidir con el de su correspondiente falso argumento. En resumen, podemos decir que existen dos formas típicas de pasar argumentos, por referencia y por valor. A continuación pasaremos a detallarlas. Argumentos por referencia Un argumento actual que es una variable, sólo podrá ser pasado por referencia. La referencia al correspondiente falso argumento de la subrutina provoca que el ordenador lo considere como el argumento actual correspondiente. Si en la subrutina se cambia el falso argumento estos cambios también afectarán al argumento actual. Por ello es una mala práctica en la programación, realizar cambios en los argumentos de entrada, que tengan lugar en una subrutina o en una función. Argumentos por valor Un argumento actual que sea una constante o una expresión más complicada que una variable, sólo podrá ser pasado por valor al correspondiente falso argumento. Así, el falso argumento no podrá cambiar su valor durante la ejecución del procedimiento. http://www.cesga.es/telecursos/F90/sec2/cap2/Tema2_Cap2_1.html ¿Qué son los argumentos de una función? Top Una función es una porción de código independiente del exterior (es decir de otros programas) y que se puede re-utilizar (es decir llamar muchas veces con datos diferentes). Aunque el concepto de función es general y existe en todos los lenguajes de programación, aquí se hará referencia a las funciones de C/C++. Se distingue entre argumentos formales y argumentos actuales. Los argumentos formales son los que aparecen en la definición de la función, mientras que los actuales son los que aparecen en la llamada a la función. Se podría también decir que los argumentos formales son los argumentos vistos desde dentro de la función, y los argumentos actuales son los argumentos vistos desde el programa que llama a la función, esto es desde fuera de la función. Los argumentos formales son siempre variables de la función que recogen los valores que se les pasan desde el exterior; los argumentos actuales pueden ser variables o expresiones cuyos valores se pasan a los argumentos formales. Cuando una función tiene valor de retorno puede ser utilizada en una sentencia aritmética (por ejemplo, la función seno en la expresión: y=sin(x)*x-1.0;). En este caso el valor de retorno se sustituye en el lugar ocupado por la llamada a la función en dicha sentencia aritmética. Cuendo una función no tiene valor de retorno, se llama simplemente colocando la llamada en una línea del programa seguida de punto y coma (por ejemplo: permutar(&x, &y);). ¿Qué es pasar un argumento por valor? Top En C/C++ el paso de argumentos a una función se hace de la siguiente manera. En el programa que llama a la función, se evalúan los argumentos actuales y sus valores se pasan a las variables de la función que constituyen los argumentos formales. En realidad siempre se pasan copias de dichos valores. Los argumentos formales (los argumentos vistos desde dentro de la función son variables nuevas, que se crean cuando la función es llamada, y que toman los valores que les pasan los argumentos actuales. Cuando de dice que en C los argumentos de las funciones se pasan siempre por valor, lo que se quiere decir es que las variables argumentos formales reciben copias de los valores de los argumentos actuales, pero son siempre variables diferentes, propias de la función. Por eso, si se modifica un argumento formal dentro de la función, se está modificando una copia del argumento actual y si el argumento actual es una variable, dicha variable no queda modificada. Esto es un mecanismo de seguridad de C, de modo que una función no pueda modificar facilmente las variables externas a ella. ¿Qué es pasar un argumento por referencia? Top En muchas ocasiones, las funciones tienen que modificar las variables pasadas como argumentos actuales. Como el valor de retorno es único, ésta es la única forma por ejemplo de que una función trasmita al exterior varios resultados en una sola llamada. Cuando se desea que la función modifique los argumentos actuales (las variables externas a la función que aparecen en la llamada), hay que pasárselos por referencia. ¿Qué quiere decir esto: que a veces se pasan copias de los argumentos actuales y otras veces se pasan los originales? Ya se verá en otro lugar que en C++ es así (a través de un tipo de variables que no tiene C: las variables reference), pero en C a las funciones siempre se les pasan copias de los argumentos actuales. ¿Como puede una función modificar una variable externa si las funciones sólo pueden recibir copias de las variables del programa que llama a la función? Esto se puede conseguir por medio de los punteros. La forma de modificar una variable externa es disponer de su dirección en memoria. Las funciones de C son capaces de recibir copias de las direcciones en memoria de las variables externas, pero para modificar el contenido de una dirección de memoria la dirección o una copia de esa dirección son igual de válidas. http://www1.ceit.es/Asignaturas/Informat2/C/FAQs-C/Faqs_c.htm#argsFunc 2.8 Devolver un valor desde un método. Los métodos son miembros de las clases y pueden realizar una serie de acciones, o devolver un valor ya sea que tengan que calcularlo o no. Pueden recibir o no parametros y pueden devolver o no devolver parametros. + Declaración: tipoRetorno + NombreDelMétodo + [Parámetros] + { cuerpo } + Ejemplo: int Suma(int numero1, int numero 2) { return numero1 + numero2; } + Uso: [valor de retorno] miMétodo([Parametros]); + Ejemplo: resultado = Suma(numero1,numero2); Los métodos pueden recibir diversos valores desde el exterior, de hecho pueden recibir tantos valores como sea necesario, los valores que se le pasan a un método pueden ser por valor o por referencia, los valores que un método recibe por valor, los recibe como una copia del dato que puede modificar a su gusto sin que se vean afectadas las demás copias de dicho dato, en cambio los valores que un método recibe por referencia son las direcciones a donde se encuentra el dato, al cambiar el dato que ahí donde una de estas referencias apunta cambia el valor para todos aquellos que lo estan utilizando, un ejemplo muy sencillo de esto es, en el mundo real un paso de valor por referencia se da si tenemos apuntada la dirección de un amigo, dicha dirección es una referencia al lugar donde vive, es por eso que si vamos al lugar donde apunta la referencia nos encontraremos en la casa de nuestro amigo, una vez allí, si rompemos un vidrio cualquier persona que llegue a ese lugar encontrara la ventana rota, en cambio con los pasos de parámetro por valor se tiene una copia del objeto en si, es decir, en el ejemplo anterior en vez de haber tenido una dirección, habría tenido una casa exactamente igual a la de mi amigo, por lo que si hubiera roto un vidrio, solo mi casa se habría visto afectada. En C# si pasamos un valor a un método se pasa por valor a menos que se le indique lo contrario. Debemos recordar que no ahi limite al número de parámetros que puede recibir o devolver un método pero si no se va a recibir o a devolver ningún valor se puede usar la palabra void para indicarlo. En C# los métodos son sumamente flexibles, es por esto que podemos devolver múltiples valores y nos permite sobrecargarlos, las palabras claves ref y out son las que nos permiten que un método retorne más de un valor al método que lo invoco. La sobrecarga de métodos nos permite que un método se comporte de manera diferente en función al tipo de parametros que recibe y/o del número de argumentos que le acompañan. Ejemplos de sobrecarga de métodos: int i1=2,i2=3; float f1=2.14,f2=5.25; int suma( int n1, int n2 ) //Método 1 { return n1+n2; } float suma( float n1, float n2 ) //Método 2 { return n1+n2; } float suma( int n1, float n2 ) //Método 3 { return n1+n2; } resultado = suma(i1+i2); //Llamada 1 resultado = suma(f1+f2); //Llamada 2 resultado = suma(i1+f1); //Llamada 3 En este ejemplo en ninguno de los casos ahi perdida de precisión debido a que el método haciendo uso de la sobrecarga se adapta a los parametros que recibe y actúa en consecuencia a ello, es decir si el método suma recibe dos enteros como en la llamada 1 devuelve un número entero empleando para ello el método 1, si recibe dos flotantes devuelve un flotante utilizando para ello el método 2 y si recibe primero un flotante y luego un entero responde utilizando el método 3. http://www.elguille.info/colabora/NET2005/cursoCS_vecrado/Vecrado_CursoCSh arp003.htm Métodos Los métodos son funciones que pueden ser llamadas dentro de la clase o por otras clases. La implementación de un método consta de dos partes, una declaración y un cuerpo. La declaración en Java de un método se puede expresar esquemáticamente como: tipoRetorno nombreMetodo( [lista_de_argumentos] ) { cuerpoMetodo } En C++, el método puede declararse dentro de la definición de la clase, aunque también puede colocarse la definición completa del método fuera de la clase, convirtiéndose en una función inline. En Java, la definición completa del método debe estar dentro de la definición de la clase y no se permite la posibilidad de métodos inline, por lo tanto, Java no proporciona al programador distinciones entre métodos normales y métodos inline. Los métodos pueden tener numerosos atributos a la hora de declararlos, incluyendo el control de acceso, si es estático o no estático, etc. La sintaxis utilizada para hacer que un método sea estático y su interpretación, es semejante en Java y en C++. Sin embargo, la sintaxis utilizada para establecer el control de acceso y su interpretación, es muy diferente en Java y en C++. La lista de argumentos es opcional, tanto en Java como en C++, y en los dos casos puede limitarse a su mínima expresión consistente en dos paréntesis, sin parámetro alguno en su interior. Opcionalmente, C++ permite utilizar la palabra void para indicar que la lista de argumentos está vacía, en Java no se usa. Los parámetros, o argumentos, se utilizan para pasar información al cuerpo del método. La sintaxis de la declaración completa de un método es la que se muestra a continuación con los items opcionales en itálica y los items requeridos en negrilla: especificadorAcceso static abstract final native synchronized tipoRetorno nombreMetodo( lista_de_argumentos ) throws listaEscepciones especificadorAcceso, determina si otros objetos pueden acceder al método y cómo pueden hacerlo. Está soportado en Java y en C++, pero la sintaxis e interpretación es considerablemente diferente. static, indica que los métodos pueden ser accedidos sin necesidad de instanciar un objeto del tipo que determina la clase. C++ y Java son similares en el soporte de esta característica. abstract, indica que el método no está definido en la clase, sino que se encuentra declarado ahí para ser definido en una subclase (sobreescrito). C++ también soporta esta capacidad con una sintaxis diferente a Java, pero con similar interpretación. final, evita que un método pueda ser sobreescrito. native, son métodos escritos es otro lenguaje. Java soporta actualmente C y C++. synchronized, se usa en el soporte de multithreading, que se verá también en este Tutorial. lista_de_argumentos, es la lista opcional de parámentros que se pueden pasar al método throws listaExcepciones, indica las excepciones que puede generar y manipular el método. También se verán en este Tutorial a fondo las excepciones en Java. Valor de Retorno de un Método En Java es imprescindible que a la hora de la declaración de un método, se indique el tipo de dato que ha de devolver. Si no devuelve ningún valor, se indicará el tipo void como retorno. Los métodos y funciones en C++ pueden devolver una variable u objeto, bien sea por valor (se devuelve una copia), por puntero o por referencia. Java no soporta punteros, así que no puede devolver nada por puntero. Todos los tipos primitivos en Java se devuelven por valor y todos los objetos se devuelven por referencia. El retorno de la referencia a un objeto en Java es similar a devolver un puntero a un objeto situado en memoria dinámica en C++, excepto que la sintaxis es mucho más simple en Java, en donde el item que se devuelve es la dirección de la posición en memoria dinámica donde se encuentra almacenado el objeto. Para devolver un valor se utiliza la palabra clave return. La palabra clave return va seguida de una expresión que será evaluada para saber el valor de retorno. Esta expresión puede ser compleja o puede ser simplemente el nombre de un objeto, una variable de tipo primitivo o una constante. El ejemplo java506.java ilustra el retorno por valor y por referencia. // Un objeto de esta clase sera devuelto por referencia class miClase { int varInstancia = 10; } Si un programa Java devuelve una referencia a un objeto y esa referencia no es asignada a ninguna variable, o utilizada en una expresión, el objeto se marca inmediatamente para que el reciclador de memoria en su siguiente ejecución devuelve la memoria ocupada por el objeto al sistema, asumiendo que la dirección no se encuentra ya almacenada en ninguna otra variable. En C++, si un programa devuelve un puntero a un objeto situado en memoria dinámica y el valor de ese puntero no se asigna a una variable, la posibilidad de devolver la memoria al sistema se pierde y se producirá un memory leak, asumiendo que la dirección no está ya disponible para almacenar ninguna otra variable. Tanto en Java como en C++ el tipo del valor de retorno debe coincidir con el tipo de retorno que se ha indicado en la declaración del método; aunque en Java, el tipo actual de retorno puede ser una subclase del tipo que se ha indicado en la declaración del método, lo cual no se permite en C++. En Java esto es posible porque todas las clases heredan desde un objeto raíz común a todos ellos: Object. En general, se permite almacenar una referencia a un objeto en una variable de referencia que sea una superclase de ese objeto. También se puede utilizar un interfaz como tipo de retorno, en cuyo caso, el objeto retornado debe implementar dicho interfaz. Nombre del Método El nombre del método puede ser cualquier identificador legal en Java. Java soporta el concepto de sobrecarga de métodos, es decir, permite que dos métodos compartan el mismo nombre pero con diferente lista de argumentos, de forma que el compilador pueda diferenciar claramente cuando se invoca a uno o a otro, en función de los parámetros que se utilicen en la llamada al método. El siguiente fragmento de código muestra una clase Java con cuatro métodos sobrecargados, el último no es legal porque tiene el mismo nombre y lista de argumentos que otro previamente declarado: class MiClase { . . . void miMetodo( int x,int y ) { . . . } void miMetodo( int x ) { . . . } void miMetodo( int x,float y ) { . . . } // void miMetodo( int a,float b ) { . . . } // no válido } Todo lenguaje de programación orientado a objetos debe soportar las características de encapsulación, herencia y polimorfismo. La sobrecarga de métodos es considerada por algunos autores como polimorfismo en tiempo de compilación. En C++, dos versiones sobrecargadas de una misma función pueden devolver tipos diferentes. En Java, los métodos sobrecargados siempre deben devolver el mismo tipo. Métodos de Instancia Cuando se incluye un método en una definición de una clase Java sin utilizar la palabra clave static, estamos generando un método de instancia. Aunque cada objeto de la clase no contiene su propia copia de un método de instancia (no existen múltiples copias del método en memoria), el resultado final es como si fuese así, como si cada objeto dispusiese de su propia copia del método. Cuando se invoca un método de instancia a través de un objeto determinado, si este método referencia a variables de instancia de la clase, en realidad se están referenciando variables de instancia específicas del objeto específico que se está invocando. La llamada a los métodos de instancia en Java se realiza utilizando el nombre del objeto, el operador punto y el nombre del método. miObjeto.miMetodoDeInstancia(); En C++, se puede acceder de este mismo modo o utilizando una variable puntero que apunte al objeto miPunteroAlObjeto->miMetodoDeInstancia(); Los métodos de instancia tienen acceso tanto a las variables de instancia como a las variables de clase, tanto en Java como en C++. Métodos Estáticos Cuando una función es incluida en una definición de clase C++, o un método e incluso en una definición de una clase Java, y se utiliza la palabra static, se obtiene un método estático o método de clase. Lo más significativo de los métodos de clase es que pueden ser invocados sin necesidad de que haya que instanciar ningún objeto de la clase. En Java se puede invocar un método de clase utilizando el nombre de la clase, el operador punto y el nombre del método. MiClase.miMetodoDeClase(); En C++, hay que utilizar el operador de resolución de ámbito para poder invocar a un método de clase: MiClase::miMetodoDeClase(); En Java, los métodos de clase operan solamente como variables de clase; no tienen acceso a variables de instancia de la clase, a no ser que se cree un nuevo objeto y se acceda a las variables de instancia a través de ese objeto. Si se observa el siguiente trozo de código de ejemplo: class Documento extends Pagina { static int version = 10; int numero_de_capitulos; static void annade_un_capitulo() { numero_de_capitulos++; // esto no funciona } static void modifica_version( int i ) { version++; // esto si funciona } } la modificación de la variable numero_de_capitulos no funciona porque se está violando una de las reglas de acceso al intentar acceder desde un método estático a una variable no estática. Todas las clases que se derivan, cuando se declaran estáticas, comparten la misma página de variables; es decir, todos los objetos que se generen comparten la misma zona de memoria. Los métodos estáticos se usan para acceder solamente a variables estáticas. class UnaClase { int var; UnaClase() { var = 5; } unMetodo() { var += 5; } } En el código anterior, si se llama al método unMetodo() a través de un puntero a función, no se podría acceder a var, porque al utilizar un puntero a función no se pasa implícitamente el puntero al propio objeto (this). Sin embargo, sí se podría acceder a var si fuese estática, porque siempre estaría en la misma posición de memoria para todos los objetos que se creasen de la clase UnaClase. Paso de parámetros En C++, se puede declarar un método en una clase y definirlo luego dentro de la clase (bajo ciertas condiciones) o definirlo fuera de la clase. A la hora de declararlo, es necesario indicar el tipo de argumentos que necesita, pero no se requiere indicar sus nombres (aunque pueda hacerse). A la hora de definir el método sí tiene que indicarse el nombre de los argumentos que necesita el método. En Java, todos los métodos deben estar declarados y definidos dentro de la clase, y hay que indicar el tipo y nombre de los argumentos o parámetros que acepta. Los argumentos son como variables locales declaradas en el cuerpo del método que están inicializadas al valor que se pasa como parámetro en la invocación del método. En Java, todos los argumentos de tipos primitivos deben pasarse por valor, mientras que los objetos deben pasarse por referencia. Cuando se pasa un objeto por referencia, se está pasando la dirección de memoria en la que se encuentra almacenado el objeto. Si se modifica una variable que haya sido pasada por valor, no se modificará la variable original que se haya utilizado para invocar al método, mientras que si se modifica una variable pasada por referencia, la variable original del método de llamada se verá afectada de los cambios que se produzcan en el método al que se le ha pasado como argumento. El ejemplo java515.java se ilustra el paso de parámetros de tipo primitivo y también el paso de objetos, por valor y por referencia, respectivamente. // Esta clase se usa para instanciar un objeto referencia class MiClase { int varInstancia = 100; } // Clase principal class java515 { // Función para ilustrar el paso de parámetros void pasoVariables( int varPrim,MiClase varRef ) { System.out.println( "--> Entrada en la funcion pasoVariables" ); System.out.println( "Valor de la variable primitiva: "+varPrim ); System.out.println( "Valor contenido en el objeto: "+ varRef.varInstancia ); System.out.println( "-> Modificamos los valores" ); varRef.varInstancia = 101; varPrim = 201; System.out.println( "--> Todavia en la funcion pasoVariables" ); System.out.println( "Valor de la variable primitiva: "+varPrim ); System.out.println( "Valor contenido en el objeto: "+ varRef.varInstancia ); } public static void main( String args[] ) { // Instanciamos un objeto para acceder a sus métodos java515 aObj = new java515(); // Instanciamos un objeto normal MiClase obj = new MiClase(); // Instanciamos una variable de tipo primitivo int varPrim = 200; System.out.println( "> Estamos en main()" ); System.out.println( "Valor de la variable primitiva: "+varPrim ); System.out.println( "Valor contenido en el objeto: "+ obj.varInstancia ); // Llamamos al método del objeto aObj.pasoVariables( varPrim,obj ); System.out.println( "> Volvemos a main()" ); System.out.println( "Valor de la variable primitiva, todavia : "+ varPrim ); System.out.println( "Valor contenido ahora en el objeto: "+ obj.varInstancia ); } } En C++, se puede pasar como parámetro un puntero que apunte a una función dentro de otra función, y utilizar este puntero en la segunda función para llamar a la primera. Esta capacidad no está directamente soportada en Java. Sin embargo, en algunos casos, se puede conseguir casi lo mismo encapsulando la primero función como un método de instancia de un objeto y luego pasar el objeto a otro método, donde el primer método se puede ejecutar a través del objeto. Tanto en Java como en C++, los métodos tienen acceso directo a las variables miembro de la clase. El nombre de un argumento puede tener el mismo nombre que una variable miembro de la clase. En este caso, la variable local que resulta del argumento del método, oculta a la variable miembro de la clase. Cuando se instancia un método se pasa siempre una referencia al propio objeto que ha llamado al método, es la referencia this. http://www.itapizaco.edu.mx/paginas/JavaTut/froufe/parte5/cap5 -5.html 2.9 Estructura del código. Ver archivo: art_tecn.pdf Ver archivo: estructuradelcodigo.pdf Unidad 3. Constructor, destructor. 3.5 Conceptos de métodos constructor y destructor. Podemos imaginar que la construcción de objetos tiene tres fases: (I) instanciación, que aquí representa el proceso de asignación de espacio al objeto, de forma que este tenga existencia real en memoria. (II) Asignación de recursos. Por ejemplo, un miembro puede ser un puntero señalando a una zona de memoria que debe ser reservada; un "handle" a un fichero; el bloqueo de un recurso compartido o el establecimiento de una línea de comunicación. (III) Iniciación, que garantiza que los valores iniciales de todas sus propiedades sean correctos (no contengan basura). La correcta realización de estas fases es importante, por lo que los diseñadores del lenguaje decidieron asignar esta tarea a un tipo especial de funciones (métodos) denominadas constructores. En realidad, la consideraron tan importante que, como veremos a continuación, si el programador no declara ninguno explícitamente, el compilador se encarga de definir un constructores de oficio , encargándose de utilizarlo cada vez que es necesario. Aparte de las invocaciones explícitas que pueda realizar el programador, los constructores son frecuentemente invocados de forma implícita por el compilador. Es significativo señalar que las fases anteriores se realizan en un orden, aunque todas deben ser felizmente completadas cuando finaliza la labor del constructor. §2 Descripción Para empezar a entender como funciona el asunto, observe este sencillo ejemplo en el que se definen sendas clases para representar complejos; en una de ellas definimos explícitamente un constructor; en otra dejamos que el compilador defina un constructor de oficio: #include <iostream> using namespace std; class CompleX { // Una clase para representar complejos public: float r; float i; // Partes real e imaginaria CompleX(float r = 0, float i = 0) { // L.7: construtor explícito this->r = r; this->i = i; cout << "c1: (" << this->r << "," << this->i << ")" << endl; } }; class CompX { // Otra clase análoga public: float r; float i; // Partes real e imaginaria }; void main() { CompleX c1; CompleX c2(1,2); CompX c3; cout << "c3: (" << c3.r << } // ====================== // L.18: // L.19: // L.20: "," << c3.i << ")" << endl; Salida: c1: (0,0) c2: (1,2) c3: (6.06626e-39,1.4013e-45) Comentario: En la clase CompleX definimos explícitamente un constructor que tiene argumentos por defecto ( ), no así en la clase CompX en la que es el propio compilador el que define un constructor de oficio. Es de destacar la utilización explícita del puntero this ( 4.11.6) en la definición del constructor (Ls.8-9). Ha sido necesario hacerlo así para distinguir las propiedades i, j de las variables locales en la función-constructor (hemos utilizado deliberadamente los mismos nombres en los argumentos, pero desde luego, podríamos haber utilizado otros :-) En la función main se instancian tres objetos; en todos los casos el compilador realiza una invocación implícita al constructor correspondiente. En la declaración de c1, se utilizan los argumentos por defecto para inicializar adecuadamente sus miembros; los valores se comprueban en la primera salida. La declaración de c2 en L.19 implica una invocación del constructor por defecto pasándole los valores 1 y 2 como argumentos. Es decir, esta sentencia equivaldría a: c2 = CompleX::CompleX(1, 2); // Hipotética invocación explícita al constructor Nota: En realidad esta última sentencia es sintácticamente incorrecta; se trata solo de un recurso pedagógico, ya que no es posible invocar de esta forma al constructor de una clase ( 4.11.2d). Una alternativa correcta a la declaración de L.19 sería: CompleX c2 = CompleX(1,2); El resultado de L.19 puede verse en la segunda salida. Finalmente, en L.20 la declaración de c3 provoca la invocación del constructor de oficio construido por el propio compilador. Aunque la iniciación del objeto con todos sus miembros es correcta, no lo es su inicialización ( 4.1.2). En la tercera salida vemos como sus miembros adoptan valores arbitrarios. En realidad se trata de basura existente en las zonas de memoria que les han sido adjudicadas. El corolario inmediato es deducir lo que ya señalamos en la página anterior: adunque el constructor de oficio inicia adecuadamente los miembros abstractos ( 4.11.2d), no hace lo mismo con los escalares. Además, por una u otra causa, en la mayoría de los casos de aplicaciones reales es imprescindible la definición explícita de uno o varios de estos constructores ( ). §3 Técnias de buena construcción Recordar que un objeto no se considera totalmente construido hasta que su constructor ha concluido satisfactoriamente. En los casos que la clase contenga sub-objetos o derive de otras, el proceso de creación incluye la invocación de los constructores de las subclases o de las super-clases en una secuencia ordenada que se detalla más adelante . Los constructores deben ser diseñados de forma que no puedan (ni áun en caso de error) dejar un objeto a medio construir. En cas que no sea posible alistar todos los recursos exigidos por el objeto, antes de terminar su ejecucón debe preverse un mecanismo de destrucción y liberación de los recursos que hubiesen sido asignados. Para esto es posible utilizar el mecanismo de excepciones. §4 Invocación de constructores Al margen de la particularidad que representan sus invocaciones implícitas, en general su invocación sigue las pautas del resto de los métodos. Ejemplos: X x1; constructor X::X(); [4] X x2 = X::X() X x3 = X(); constructor [5] X x4(); anterior [6] // L.1: Ok. Invocación implícita del // Error: invocación ilegal del constructor // Error: invocación ilegal del constructor // L.4: Ok. Invocación legal del // L.5: Ok. Variación sintáctica del Nota: Observe como la única sentencia válida con invocación explícita al constructor (L.4) es un caso de invocación de función miembro muy especial desde el punto de vista sintáctico (esta sintaxis no está permitida con ningún otro tipo de función-miembro, ni siquiera con funciones estáticas o destructores). La razón es que los constructores se diferencian de todos los demás métodos no estáticos de la clase en que no se invocan sobre un objeto (aunque tienen puntero this 4.11.6). En realidad se asemejan a los dispositivos de asignación de memoria, en el sentido que son invocados desde un trozo de memoria amorfa y la convierten en una instancia de la clase [7]. Como ocurre con los tipos básicos (preconstruidos en el lenguaje), si deseamos crear objetos persistentes de tipo abstracto (definidos por el usuario), debe utilizarse el operador new ( 4.9.20). Este operador está íntimamente relacionado con los constructores. De hecho, para invocar la creación de un objeto a traves de él, debe existir un constructor por defecto ( ). Si nos referimos a la clase CompleX definida en el ejemplo ( ), las sentencias: { CompleX* pt1 = new(CompleX); CompleX* pt2 = new(CompleX)(1,2); } provocan la creación de dos objetos automáticos, los punteros pt1 y pt2, así como la creación de sendos objetos (anónimos) en el montón. Observe que ambas sentencias suponen un invocación implícita al constructor. La primera al constructor por defecto sin argumentos, la segunda con los argumentos indicados. En consecuencia producirán las siguientes salidas: c1: (0,0) c1: (1,2) Observe también, y esto es importante, que los objetos pt1 y pt2 son destruidos automáticamente al salir de ámbito el bloque. No así los objetos señalados por estos punteros (ver comentario al respecto 4.11.2d2). §5 Propiedades de los constructores Aunque los constructores comparten muchas propiedades de los métodos normales, tienen algunas características que las hace ser un tanto especiales. En concreto, se trata de funciones que utilizan rutinas de manejo de memoria en formas que las funciones normales no suelen utilizar. §5.1 Los constructores se distinguen del resto de las funciones de una clase porque tienen el mismo nombre que esta. Ejemplo: class X { public: X(); }; // definición de la clase X // constructor de la clase X §5.2 No se puede obtener su dirección, por lo que no pueden declararse punteros a este tipo de métodos. §5.3 No pueden declararse virtuales ( class C { ... virtual C(); }; 4.11.8a). Ejemplo: // Error !! La razón está en la propia idiosincrasia de este tipo de funciones. En efecto, veremos que declarar que un método es virtual ( 4.11.8a) supone indicar al compilador que el modo concreto de operar la función será definido más tarde, en una clase derivada; sin embargo, un constructor debe conocer el tipo exacto de objeto que debe crear, por lo que no puede ser virtual. §5.4 Otras peculiaridades de los constructores es que se declaran sin devolver nada, ni siquiera void, lo que no es óbice para que el resultado de su actuación (un objeto) si pueda ser utilizado como valor devuelto por una función: class C { ... }; ... C foo() { return C(); } §5.5 No pueden ser heredados, aunque una clase derivada puede llamar a los constructores y destructores de la superclase siempre que hayan sido declarados public o protected ( 4.11.2a). Como el resto de las funciones (excepto main), los constructores también pueden ser sobrecargados; es decir: Una clase puede tener varios constructores. En estos casos, la invocación (incluso implícita) del constructor adecuado se efectuará según los argumentos involucrados. Es de destacar que en ocasiones, esta multiplicidad de constructores puede conducir a situaciones realmente curiosas; incluso se ha definido una palabra clave, explicit, para evitar los posibles efectos colaterales ( ). §5.5 Un constructor no puede ser friend ( 4.11.2a1) de ninguna otra clase. §5.6 Una peculiaridad sintáctica de este tipo de funciones es la posibilidad de incluir iniciadores ( 4.11.2d3), una forma de expresar la inicialización de variables fuera del cuerpo del constructor. Ejemplo: class X { const int i; char c; public: X(int entero, char caracter): i(entero), c(caracter) { }; }; §5.7 Como en el resto de las funciones, los constructores pueden tener argumentos por defecto. Por ejemplo, el constructor: X::X(int, int = 0) puede aceptar uno o dos argumentos. Cuando se utiliza con uno, el segundo se supone que es un cero int. De forma análoga, el constructor X::X(int = 5, int = 6) puede aceptar dos, uno o ningún argumento, con sus correspondientes valores por defecto para cuando faltan. Observe que un constructor sin argumentos, como X::X(), no debe ser confundido con X::X(int=0), que puede ser llamado sin argumentos o con uno; aunque en realidad siempre tendrá un argumento. En otras palabras: Que una función pueda ser invocada sin argumentos no implica necesariamente que no los acepte. §5.8 Cuando se definen constructores deben evitarse ambigüedades. Es el caso de los constructores por defecto del ejemplo siguiente: class X { public: X(); X(int i = 0); }; int main() { X uno(10); X dos; X::X(int = 0) return 0; } // Ok; usa el constructor X::X(int) // Error: ambigüedad cual usar? X::X() o §5.9 Los constructores de las variables globales son invocados por el módulo inicial antes de que sea llamada la función main y las posibles funciones que se hubiesen instalado mediante la directiva #pragma startup ( 1.5). §5.10 Los objetos locales se crean tan pronto como se inicia su ámbito. También se invoca implícitamente un constructor cuando se crea o copia un objeto de la clase (incluso temporal). El hecho de que al crear un objeto se invoque implícitamente un constructor por defecto si no se invoca ninguno de forma explícita, garantiza que siempre que se instancie un objeto será inicializado adecuadamente. En el ejemplo que sigue se muestra claramente como se invoca el constructor tan pronto como se crea un objeto. #include <iostream> using namespace std; class A { // definición de una clase public: int x; A(int i = 1) { // constructor por defecto x = i; cout << "Se ha creado un objeto" << endl; } }; int main() { // ========================= A a; // se instancia un objeto cout << "Valor de a.x: " << a.x << endl; return 0; } Salida: Se ha creado un objeto Valor de a.x: 1 §5.11 El constructor de una clase no puede admitir la propia clase como argumento (se daría lugar a una definición circular). Ejemplo: class X { public: X(X); }; // Error: ilegal §5.12 Los parámetros del constructor pueden ser de cualquier tipo, y aunque no puede aceptar su propia clase como argumento. En cambio si pueden aceptar una referencia a objetos de su propia clase, en cuyo caso se denomina constructor-copia (su sentido y justificación lo exponemos con más detalle en el apartado correspondiente 4.11.2d4). Ejemplo: class X { public: X(X&); }; // Ok. correcto Aparte del referido constructor-copia, existe otro tipo de constructores de nombre específico: el constructor oficial y el constructor por defecto ( ). §6 Constructor oficial Si el programador no define explícitamente ningún constructor, el compilador proporciona uno por defecto al que llamaremos oficial o de oficio. Es público, "inline" ( 4.11.2a), y definido de forma que no acepta argumentos. Es el responsable de que funcionen sin peligro secuencias como esta: class A { int x; }; ... A a; // C++ ha creado un constructor "de oficio" // invocación implícita al constructor de oficio Recordemos que el constructor de oficio invoca implícitamente los constructores de oficio de todos los miembros. Si algunos miembros son a su vez objetos abstractos, se invocan sus constructores. Así sucesivamente con cualquier nivel de complejidad hasta llegar a los tipos básicos (preconstruidos en el lenguaje 2.2) cuyos constructores son también invocados. Recordar que los constructores de los tipos básicos inician (reservan memoria) para estos objetos, pero no los inicializan con ningún valor concreto. Por lo que en principio su contenido es impredecible (basura) [1]. Dicho en otras palabras: el constructor de oficio se encarga de preparar el ambiente para que el objeto de la clase pueda operar, pero no garantiza que los datos contenidos sean correctos. Esto último es responsabilidad del programador y de las condiciones de "runtime". Por ejemplo: struct Nombre { char* nomb; }; struct Equipo { Nombre nm; size_t sz; }; struct Liga { int year; char categoria; Nombre nLiga; Equipo equipos[10]; }; ... Liga primDiv; En este caso la última sentencia inicia primDiv mediante una invocación al constructor por defecto de Liga, que a su vez invoca a los constructores por defecto de Nombre y Equipo. para crear los miembros nLiga y equipos (el constructor de Equipo es invocado diez veces, una por cada miembro de la matriz). A su vez, cada invocación a Equipo() produce a su vez una invocación al constructor por defecto de Nombre (size_t es un tipo básico y no es invocado su constructor 4.9.13). Los miembros nLiga y equipos son iniciados de esta forma, pero los miembros year y categoria no son inicializados ya que son tipos simples, por lo que pueden contener basura. Si el programador define explícitamente cualquier constructor, el constructor oficial deja de existir. Pero si omite en él la inicialización de algún tipo abstracto, el compilador añadirá por su cuenta las invocaciones correspondientes a los constructores por defecto de los miembros omitidos ( Ejemplo). §6.1 Constructor trivial Un constructor de oficio se denomina trivial si cumple las siguientes condiciones: La clase correspondiente no tiene funciones virtuales ( 4.11.8a) y no deriva de ninguna superclase virtual. Todos los constructores de las superclases de su jerarquía son triviales Los constructores de sus miembros no estáticos que sean clases son también triviales §7 Constructor por defecto Constructor por defecto de la clase X es aquel que "puede" ser invocado sin argumentos, bien porque no los acepte, bien porque disponga de argumentos por defecto ( 4.4.5). Como hemos visto en el epígrafe anterior, el constructor oficial creado por el compilador si no hemos definido ningún constructor es también un constructor por defecto, ya que no acepta argumentos. Tenga en cuenta que diversas posibilidades funcionales y sintácticas de C++ precisan de la existencia de un constructor por defecto (explícito u oficial). Por ejemplo, es el responsable de la creación del objeto x en una declaración del tipo X x;. §8 Un constructor explícito puede ser imprescindible En el primer ejemplo ( ), el programa ha funcionado aceptablemente bien utilizando el constructor de oficio en una de sus clases, pero existen ocasiones en que es imprescindible que el programador defina uno explícitamente, ya que el suministrado automáticamente por el compilador no es adecuado. Consideremos una variación del citado ejemplo en la que definimos una clase para contener las coordenadas de puntos de un plano en forma de matrices de dos dimensiones: #include <iostream> using namespace std; class Punto { public: int coord[2]; }; int main() { // ================== Punto p1(10, 20); // L.8: cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl; } Este programa produce un error de compilación en L.8. La razón es que si necesitamos este tipo de inicialización del objeto p1, utilizando una lista de argumentos, es imprescindible la existencia de un constructor explícito ( 4.11.2d3). Es decir, la versión correcta del programa seria: #include <iostream> using namespace std; class Punto { public: int coord[2]; Punto(int x = 0, int y = 0) { coord[0] = x; coord[1] = y; } }; // construtor explícito // inicializa int main() { // ================== Punto p1(10, 20); // L.8: Ok. cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl; } §8.1 La anterior no es por supuesto la única causa que hace necesaria la existencia de constructores explícitos. Más frecuente es el caso de que algunas de las variables de la clase deban ser persistentes. Por ejemplo: supongamos que en el caso anterior necesitamos que la matriz que almacena las coordenadas necesite este tipo de almacenamiento. En este caso, puesto que la utilización del especificador static aplicado a miembros de clase puede tener efectos colaterales indeseados ( 4.11.7), el único recurso es situar el almacenamiento en el montón ( 1.3.2), para lo que utilizamos el operador new ( 4.9.20) en un constructor definido al efecto. La definición de la clase tendría el siguiente aspecto: class Punto { public: int* coord; Punto(int x = 0, int y = defecto coord = new int[2]; coord[0] = x; coord[1] cout << "Creado punto; << coord[0] << "; } }; 0) { // construtor por // asigna espacio // inicializa = y; X == " Y == " << coord[1] << endl; Posteriormente se podrían instanciar objetos de la clase Punto mediante expresiones como: Punto p1; Punto p2(3, 4); argumentos Punto p3 = Punto(5, 6); argumentos Punto* ptr1 = new(Punto) argumentos Punto* ptr2 = new(Punto)(7, 8) argumentos // invocación implícita // invocación implícita con // invocación explícita con // invocación implícita sin // invocación implícita con §9 Orden de construcción Dentro de una clase los constructores de sus miembros son invocados antes que el constructor existente dentro del cuerpo de la propia clase. Esta invocación se realiza en el mismo orden en que se hayan declarado los elementos. A su vez, cuando una clase tiene más de una clase base (herencia múltiple 4.11.2c), los constructores de las clases base son invocados antes que el de la clase derivada y en el mismo orden que fueron declaradas. Por ejemplo en la inicialización: class Y {...} class X : public Y {...} X one; los constructores son llamados en el siguiente orden: Y(); X(); // constructor de la clase base // constructor de la clase derivada En caso de herencia múltiple: class X : public Y, public Z X one; los constructores de las clase-base son llamados primero y en el orden de declaración: Y(); Z(); X(); // constructor de la primer clase base // constructor de la segunda clase base // constructor de la clase derivada Nota: Al tratar de la destrucción de objetos ( 4.11.2d2), veremos que los destructores son invocados exactamente en orden inverso al de los constructores. §9.1 Los constructores de clases base virtuales ( 4.11.8a) son invocados antes que los de cualquier clase base no virtual. Si la jerarquía contiene múltiples clases base virtuales, sus constructores son invocados en el orden de sus declaraciones. A continuación de invocan los constructores del resto de las clase base, y por último el constructor de la clase derivada. §9.2 Si una clase virtual deriva de otra no virtual, primero se invoca el constructor de la clase base (no virtual), de forma que la virtual (derivada) pueda ser construida correctamente. Por ejemplo, el código: class X : public Y, virtual public Z X one; origina el siguiente orden de llamada en los constructores: Z(); Y(); X(); // constructor de la clase base virtual // constructor de la clase base no virtual // constructor de la clase derivada Un ejemplo más complicado: class class class class base; base2; level1 : public base2, virtual public base; level2 : public base2, virtual public base; class toplevel : public level1, virtual public level2; toplevel view; El orden de invocación de los constructores es el siguiente: base(); base2(); level2(); base2(); level1(); // // // // // // // clase virtual de jerarquía más alta base es construida solo una vez base no virtual de la base virtual level2 debe invocarse para construir level2 clase base virtual base no virtual de level1 otra base no virtual toplevel(); §9.3 Si una jerarquía de clases contiene múltiples instancias de una clase base virtual, dicha base virtual es construida solo una vez. Aunque si existen dos instancias de la clase base: virtual y no virtual, el constructor de la clase es invocado solo una vez para todas las instancias virtuales y después una vez para cada una de las instancias no virtuales. §9.4 En el caso de matrices de clases, los constructores son invocados en orden creciente de subíndices. §10 Los constructores y las funciones virtuales Debido a que los constructores de las clases-base son invocados antes que los de las clases derivadas, y a la propia naturaleza del mecanismo de invocación de funciones virtuales ( 4.11.8a), el mecanismo virtual está deshabilitado en los constructores, por lo que es peligroso incluir invocaciones a tales funciones en ellos, ya que podrían obtenerse resultados no esperados a primera vista. Considere los resultados del ejemplo siguiente, donde se observa que la versión de la función fun invocada no es la que cabría esperar en un funcionamiento normal del mecanismo virtual. #include <string> #include <iostream> using namespace std; class B { // superclase public: virtual void fun(const string& ss) { cout << "Funcion-base: " << ss << endl; } B(const string& ss) { // constructor de superclase cout << "Constructor-base\n"; fun(ss); } }; class D : public B { // clase derivada string s; // private por defecto public: void fun(const string& ss) { cout << "Funcion-derivada\n"; s = ss; } D(const string& ss) :B(ss) { // constructor de subclase cout << "Constructor-derivado\n"; } }; int main() { D d("Hola mundo"); constructor D } // ============= // invocación implícita a Salida: Constructor-base Funcion-base: Hola mundo Constructor-derivado Nota: La invocación de destructores ( 4.11.2d2) se realiza en orden inverso a los constructores: Las clases derivadas se destruyen antes que las clases-base [2]. Por esta razón el mecanismo virtual también está deshabilitado en los destructores (lo que no tiene nada que ver con que los destructores puedan ser en sí mismos funciones virtutales 4.11.2d2). Así pues, en la ejecución de un destructor solo se invocan las definiciones locales de las funciones implicadas. De lo contrario se correría el riesgo de referenciar la parte derivada del objeto que ya estaría destruida. §11 Constructores de conversión Normalmente a una clase con constructor de un solo parámetro puede asignársele un valor que concuerde con el tipo del parámetro. Este valor es automáticamente convertido de forma implícita en un objeto del tipo de la clase a la que se ha asignado. Por ejemplo: la definición: class X { public: X(); X(int); X(const char*, int = 0); }; // constructor C-1 // constructor C-2 // constructor C-3 en la que se han definido dos constructores que pueden ser utilizados con un solo argumento, permite que las siguientes asignaciones sean legales: void f() { X a; X b = X(); X c = X(1); X d(1); X e = X("Mexico"); X f("Mexico"); X g = 1; X h = "Madrid"; a = 2; } // // // // // // // // // Ok invocado C-1 Ok idem. Ok invocado C-2 Ok igual que el anterior Ok invocado C-3 Ok igual que el anterior L.1 Ok. L.2 Ok. L.3 Ok. La explicación de las tres últimas sentencias es la siguiente: En L.1, el compilador intenta convertir el Rvalue (que aquí es una constante numérica entera de valor 1) en el tipo del Lvalue, que aquí es la declaración de un nuevo objeto (una instancia de la clase). Como necesita crear un nuevo objeto, utilizará un constructor, de forma que busca si hay uno adecuado en X que acepte como argumento el tipo situado a la derecha. El resultado es que el compilador supone un constructor implícito a la derecha de L.1: X a = X(1); // interpretación del compilador para L.1 El proceso se repite en la sentencia L.2 que es equivalentes a: X B = X("Madrid"); // L.2bis La situación en L.3 es completamente distinta, ya que en este caso ambos operandos son objetos ya construidos. Para poder realizar la asignación, el compilador intenta convertir el tipo del Rvalue al tipo del Lvalue, para lo cual, el mecanismo de conversión de tipos busca si existe un constructor adecuado en X que acepte el operando derecho. Caso de existir se creará un objeto temporal tipoX que será utilizado como Rvalue de la asignación. La asignación propiamente dicha es realizada por el operador correspondiente (explícito o implícito) de X. La página adjunta incluye un ejemplo que muestra gráficamente el proceso seguido ( Ejemplo) Este tipo de conversión automática se realiza solo con constructores que aceptan un argumento o que son asimilables (como C-2), y suponen una conversión del tipo utilizado como argumento al tipo de la clase. Por esta razón son denominadas conversiones mediante constructor, y a este tipo de constructores constructores de conversión ("Converting constructor"). Su sola presencia habilita no solo la conversión implícita, también la explícita. Ejemplo: class X { public: X(int); }; // constructor C-2 la mera existencia del constructor C-2 en la clase X, permite las siguientes asignaciones: void f() { X a = X(1) constructor X a = 1; a = 2; a = (X) 2; tradicional) a = static_cast<X>(2); C++) } // L1: Ok. invocación explícita al // Ok. invocación implícita X(1) // Ok. invocación implícita X(2) // Ok. casting explícito (estlo // Ok. casting explícito (estilo Si eliminamos el constructor C-2 de la declaración de la clase, todas estas sentencias serían erróneas. Observe que en L1 cabría hacerse una pregunta: ¿Se trata de la invocación del constructor o un modelado explícito al estilo tradicional?. La respuesta es que se trata de una invocación al constructor, y que precisamente el modelado (explícito o implícito) se apoya en la existencia de este tipo de constructores para realizar su trabajo. [1] Las razones de este trato desigual entre ambos tipos de miembros se debe a la herencia de C y a consideraciones de eficiencia. http://www.zator.com/Cpp/E4_11_2d1.htm Los destructores son un tipo especial de función miembro, estrechamente relacionados con los constructores. Son también funciones que no devuelven nada (ni siquiera void). Tampoco aceptan ningún parámetro, ya que la destrucción de un objeto no acepta ningún tipo de opción o especificación particular y es idéntica para todos los objetos de la clase. Los destructores no pueden ser heredados, aunque una clase derivada puede llamar a los destructores de su superclase si no han sido declarados privados (son públicos o protegidos). Lo mismo que ocurre con los constructores, tampoco puede obtenerse su dirección, por lo que no es posible establecer punteros a este tipo de funciones. La misión más común de los destructores es liberar la memoria asignada por los constructores, aunque también puede ser desasignar y/o liberar determinados recursos asignados por estos. Por ejemplo, cerrar un fichero o desbloquear un recurso compartido previamente bloqueado por el constructor. Se ha señalado que si el programador no define uno explícitamente, el compilador C++ proporciona un destructor de oficio, que es declarado público y puede ser invocado sin argumentos. Por lo general en la mayoría de los casos este destructor de oficio es suficiente, por lo que el programador no necesita definir uno por sí mismo, a no ser que la clase incluya la inicialización de objetos persistentes ( 1.3.2). Por ejemplo matrices que necesiten del operador new en el constructor para su inicialización, en cuyo caso es responsabilidad del programador definir un destructor adecuado (ver ejemplo ). Los destructores son invocados automáticamente (de forma implícita) por el programa en multitud de ocasiones; de hecho es muy raro que sea necesario invocarlos explícitamente. Su misión es limpiar los miembros del objeto antes que el propio objeto se auto-destruya. §2 Declaración Los destructores se distinguen porque tienen el mismo nombre que la clase a que pertenecen precedido por la tilde ~ para simbolizar su estrecha relación con los constructores que utilizan el mismo nombre (son el "complemento" de aquellos). Ejemplo: class X { public: ~X(); }; ... X::~X() { ... } // destructor de la clase X // definición (off-line) del destructor §2.1 Ejemplo: La clase Punto definida en el epígrafe anterior ( 4.11.2d1) sería un buen exponente del caso en que es necesario definir un destructor explícito que se encargue de las correcta destrucción de los miembros. En efecto, manteniendo aquella definición, una sentencia del tipo: { ... Punto p1(2,3); ... } provoca la creación de un objeto en memoria dinámica. El miembro coord es un puntero-a-int que señala un área en el montón capaz para albergar dos enteros. Cuando la ejecución sale del ámbito del bloque en que se ha creado el objeto, es invocado el destructor de oficio y el objeto es destruido, incluyendo su único componente, el puntero coord; sin embargo el área señalada por este permanece reservada en el montón, y por tanto irremediablemente perdida. La forma sensata de utilizar tales objetos sería modificando la definición de la clase para añadirle un destructor adecuado. La versión correcta tendría el siguiente aspecto: class Punto { public: int* coord; Punto(int x = 0, int y = 0) { coord = new int[2]; coord[0] = x; coord[1] = y; } ~Punto() { delete [] coord; // L.8: }; // construtor // destructor En este caso, la sentencia de la línea 8 provoca que al ser invocado el destructor del objeto, se desasigne el área del montón señalada por el puntero (recuerde que, al igual que el resto de las funciones miembro, los destructores también tienen un argumento oculto this, por lo que la función sabe sobre que objeto tiene que operar en cada caso). §3 Invocación Como hemos señalado, los destructores son invocados automáticamente por el compilador, y es muy raro que sea necesario invocarlos explícitamente. §3.1 Invocación explícita de destructores En caso necesario los destructores pueden ser invocados explícitamente de dos formas: Indirectamente, mediante una llamada a delete ( 4.9.21) o directamente utilizando el nombre cualificado completo. Ejemplo: class X {...}; ... { X obj1; X* ptr = new(X) X* pt2 = &obj1; clase X ... pt2–>X::~X(); destructor // pt2->~X(); anterior // obj1.~X(); X::~X(); destructor [1] // X es una clase // L.4: objeto automático // L.5: objeto persistente // Ok: pt2 es puntero a obj1 de la // L.8: Ok: llamada legal del L.9: Ok: variación sintáctica de la L.10: Ok otra posibilidad análoga // L.11: Error: llamada ilegal al delete ptr; destructor } // L.12: Ok. invocación implícita al Comentario: L.4 crea el objeto obj1 en la pila ( 1.3.2), se trata de un objeto automático, y en cuanto el bloque salga de ámbito, se producirá automáticamente una llamada a su destructor que provocará su eliminación. Sin embargo, el objeto anónimo señalado por ptr es creado en el montón. Observe que mientras ptr es también un objeto automático, que será eliminado al salir del bloque, el objeto al que señala es persistente y su destructor no será automáticamente invocado al salir de ámbito el bloque. Como se ve en el punto siguiente, en estos casos es imprescindible una invocación explícita al destructor mediante el operador delete (cosa que hacemos en L.12), en caso contrario, el espacio ocupado por el objeto será perdido. Es muy importante advertir que la invocación explícita al destructor de obj1 en L.8 (o su versiones equivalentes L.9 y L.10) son correctas, aunque muy peligrosas [2]. En efecto, en L.8 se produce la destrucción del objeto, pero en el estado actual de los compiladores C++, que no son suficientemente "inteligentes" en este sentido [3], al salir el bloque de ámbito vuelven a invocar los destructores de los objetos automáticos creados en su interior, por lo que se producirá un error de ejecución irrecuperable (volcado de memoria si corremos bajo Linux). §3.1.1 Los objetos que han sido creados con el operador new ( 4.9.20) deben destruirse obligatoriamente con una llamada explícita al destructor. Ejemplo: #include <stdlib.h> class X { // clase public: ... ~X(){}; // destructor de la clase }; void* operator new(size_t size, void *ptr) { return ptr; } char buffer[sizeof(X)]; // matriz de caracteres, del tamaño de X void main() { X* ptr1 = new X; new X* ptr2; ptr2 = new(&buffer) X; buffer // ======================== // puntero a objeto X creado con // puntero a objeto X // se inicia con la dirección de ... delete ptr1; ptr2–>X::~X(); espacio de buffer } // delete destruye el puntero // llamada directa, desasignar el §3.2 Invocación implícita de destructores Además de las posibles invocaciones explícitas, cuando una variable sale del ámbito para el que ha sido declarada, su destructor es invocado de forma implícita. Los destructores de las variables locales son invocados cuando el bloque en el que han sido declarados deja de estar activo. Por su parte, los destructores de las variables globales son invocados como parte del procedimiento de salida ( 1.5) después de la función main ( 4.4.4). §3.2.1 En el siguiente ejemplo se muestra claramente como se invoca el destructor cuando un objeto sale de ámbito al terminar el bloque en que ha sido declarado. #include <iostream> using namespace std; class A { public: int x; A(int i = 1) { x = i; } // constructor por defecto ~A() { // destructor cout << "El destructor ha sido invocado" << endl; } }; int main() { // { A a; // cout << "Valor de a.x: " } // destructor de a return 0; } ========================= se instancia un objeto << a.x << endl; punto de invocación del Salida: Valor de a.x: 1 El destructor ha sido invocado §3.2.2 En el ejemplo que sigue se ha modificado ligeramente el código anterior para demostrar como el destructor es invocado incluso cuando la salida de ámbito se realiza mediante una sentencia de salto (omitimos la salida, que es idéntica a la anterior): #include <iostream> using namespace std; class A { public: int x; A(int i = 1) { x = i; } // constructor por defecto ~A() { // destructor cout << "El destructor ha sido invocado" << endl; } }; int main() { // ========================= { A a; // se instancia un objeto cout << "Valor de a.x: " << a.x << endl; goto FIN; } FIN: return 0; } §3.2.3 Una tercera versión, algo más sofisticada, nos muestra como la invocación del destructor se realiza incluso cuando la salida de ámbito se realiza mediante el mecanismo de salto del manejador de excepciones, y como la invocación se realiza para cualquier objeto, incluso temporal, que deba ser destruido ( Ejemplo). Recordar que que cuando los punteros a objetos salen de ámbito, no se invoca implícitamente ningún destructor para el objeto, por lo que se hace necesaria una invocación explícita al operador delete ( 4.9.21) para destruir el objeto (§3.1.1 ). §4 Propiedades de los destructores Cuando se tiene un destructor explícito, las sentencias del cuerpo se ejecutan antes que la destrucción de los miembros. A su vez la invocación de los destructores de los miembros se realiza exactamente en orden inverso en que se realizó la invocación de los constructores correspondientes ( 4.11.2d1). La destrucción de los miembros estáticos se ejecuta después que la destrucción de los miembros no estáticos. Los destructores no pueden ser declarados const ( 3.2.1c) o volatile ( 3.2.1d), aunque pueden ser invocados desde estos objetos. Tampoco pueden ser declarados static ( 4.11.7), lo que supondría poder invocarlos sin la existencia de un objeto que destruir. §5 Destructores virtuales Como cualquier otra función miembro, los destructores pueden ser declarados virtual ( 4.11.8a). El destructor de una clase derivada de otra cuyo destructor es virtual, también es virtual ( 4.11.8a). La existencia de un destructor virtual permite que un objeto de una subclase pueda ser correctamente destruido por un puntero a su clase-base [4]. Ejemplo: class B { ... virtual ~B(); }; // Superclase (polimórfica) class D : public B { ... ~D(); }; void func() { B* ptr = new D; subclase delete ptr; } // Destructor virtual // Subclase (deriva de B) // destructor también virtual // puntero a superclase asignado a objeto de // Ok: delete es necesario siempre que se usa new Comentario Aquí el mecanismo de llamada de las funciones virtuales permite que el operador delete invoque al destructor correcto, es decir, al destructor ~D de la subclase aunque se invoque mediante el puntero ptr a la superclase B*. Si el destructor no hubiese sido virtual no se hubiese invocado el destructor derivado ~D, sino el de la superclase ~B, dando lugar a que los miembros privativos de la subclase no hubiesen sido desasignados. Tendríamos aquí un caso típico de "misteriosas" pérdidas de memoria, tan frecuentes en los programas C++ como difíciles de depurar. A pesar de todo, el mecanismo de funciones virtuales puede ser anulado utilizando un operador de resolución adecuado ( 4.11.8a). En el ejemplo anterior podría haberse puesto: void func() { D d1, d2; foo(d1, d2); } void foo(B& b1, B& b2) { // referencias a la superclase!! b1.~B(); // invocación virtual a ~D() b2.B::~B(); // invocacion estática a B::~B() } §5.1 En el siguiente ejemplo se refiere a un caso concreto de la hipótesis anterior. Muestra como virtual afecta el orden de llamada a los destructores. Sin un destructor virtual en la clase base, no se produciría una invocación al destructor de la clase derivada. #include <iostream> class color { // clase base public: virtual ~color() { // destructor virtual std::cout << "Destructor de color\n"; } }; class rojo : public color { // clase derivada (hija) public: ~rojo() { // también destructor virtual std::cout << "Destructor de rojo\n"; } }; class rojobrillante : public rojo { // clase derivada (nieta) public: ~rojobrillante() { // también destructor virtual std::cout << "Destructor de rojobrillante\n"; } }; int main() { color *palette[3]; // matriz de tres punteros a tipo color palette[0] = new rojo; // punteros a tres objetos en algún sitio palette[1] = new rojobrillante; palette[2] = new color; // llamada a los destructores de rojo y color (padre). delete palette[0]; std::cout << std::endl; // llamada a destructores de rojobrillante, rojo (padre) y color (abuelo) delete palette[1]; std::cout << std::endl; // llamada al destructor de la clase raíz delete palette[2]; return 0; } Salida del programa: Destructor de rojo Destructor de color Destructor de rojobrillante Destructor de rojo Destructor de color Destructor de color Comentario Si los destructores no se hubiesen declarado virtuales las sentencias delete palette[0], delete palette[1], y delete palette[2] solamente hubiesen invocado el destructor de la clase color. Lo que no hubiese destruido correctamente los dos primeros elementos, que son del tipo rojo y rojobrillante. §6 Los destructores y las funciones virtuales El mecanismo de llamada de funciones virtuales está deshabilitado en los destructores por las razones ya expuestas al tratar de los constructores ( 4.11.2d1) §7 Los destructores y exit Cuando se invoca exit ( &1.5.1) desde un programa, no son invocados los destructores de ninguna variable local del ámbito actual. Las globales son destruidas en su orden normal. §8 Los destructores y abort Cuando se invoca la función abort ( &1.5.1) en cualquier punto de un programa no se invoca ningún destructor, ni aún para las variables de ámbito global. http://www.zator.com/Cpp/E4_11_2d2.htm Constructores Un constructor es una función especial que sirve para construir o inicializar objetos. En C++ la inicialización de objetos no se puede realizar en el momento en que son declarados; sin embargo, tiene una característica muy importante y es disponer de una función llamada constructor que permite inicializar objetos en el momento en que se crean. Un constructor es una función que sirve para construir un nuevo objeto y asignar valores a sus miembros dato. Se caracteriza por: Tener el mismo nombre de la clase que inicializa - Puede definirse inline o fuera de la declaración de la clase No devuelve Puede admitir parámetros como cualquier - Puede existir más de un constructor, e incluso no existir otra valores función Si no se define ningún constructor de una clase, el compilador generará un constructor por defecto. El constructor por defecto no tiene argumentos y simplemente sitúa ceros en cada byte de las variables instancia de un objeto. Si se definen constructores para una clase, el constructor por defecto no se genera. Un constructor del objeto se llama cuando se crea el objeto implícitamente: nunca se llama explícitamente a las funciones constructoras. Esto significa que se llama cuando se ejecuta la declaración del objeto. También, para objetos locales, el constructor se llama cada vez que la declaración del objeto se encuentra. En objetos globales, el constructor se llama cuando se arranca el programa. El constructor por defecto es un constructor que no acepta argumentos. Se llama cuando se define una instancia pero no se especifica un valor inicial. Se pueden declarar en una clase constructores múltiples, mientras tomen parte diferentes tipos o número de argumentos. El compilador es entonces capaz de determinar automáticamente a qué constructor llamar en cada caso, examinando los argumentos. Los argumentos por defecto se pueden especificar en la declaración del constructor. Los miembros dato se inicializarán a esos valores por defecto, si ningún otro se especifica. C++ ofrece un mecanismo alternativo para pasar valores de parámetros a miembros dato. Este mecanismo consiste en inicializar miembros dato con parámetros. class prueba { tipo1 d1; tipo2 d2; tipo3 d3; public: prueba(tipo1 p1, tipo2 p2, tipo3 p3):d1(p1),d2(p2),d3(p3) { } }; Un constructor que crea un nuevo objeto a partir de uno existente se llama constructor copiador o de copias. El constructor de copias tiene sólo un argumento: una referencia constante a un objeto de la misma clase. Un constructor copiador de una clase complejo es: complejo::complejo(const complejo &fuente) { real=fuente.real; imag=fuente.imag; } Si no se incluye un constructor de copia, el compilador creará un constructor de copia por defecto. Este sistema funciona de un modo perfectamente satisfactorio en la mayoría de los casos, aunque en ocasiones puede producir dificultades. El constructor de copia por defecto inicializa cada elemento de datos del objeto a la izquierda del operador = al valor del elmento dato equivalente del objeto de la derecha del operador =. Cuando no hay punteros invicados, eso funciona bien. Sin embargo, cuando se utilizan punteros, el constructor de copia por defecto inicializará el valor de un elemento puntero de la izquierda del operador = al del elemento equivalente de la derecha del operador; es decir que los dos punteros apuntan en la misma dirección. Si ésta no es la situación que se desea, hay que escribir un constructor de copia. 5.14 Destructores Un destructor es una función miembro con igual nombre que la clase, pero precedido por el carácter ~. Una clase sólo tiene una función destructor que, no tiene argumentos y no devuelve ningún tipo. Un destructor realiza la operación opuesta de un constructor, limpiando el almacenamiento asignado a los objetos cuando se crean. C++ permite sólo un destructor por clase. El compilador llama automáticamente a un destructor del objeto cuando el objeto sale fuera del ámbito. Si un destructor no se define en una clase, se creará por defecto un destructor que no hace nada. Normalmente los destructores se declaran public. 5.15 Creación y supresión dinámica de objetos Los operadores new y delete se pueden utilizar para crear y destruir objetos de una clase, así como dentro de funciones constructoras y destructoras. Un objetro de una determinada clase se crea cuando la ejecución del programa entra en el ámbito en que está definida y se destruye cuando se llega al final del ámbito. Esto es válido tanto para objetos globales como locales, ya que los objetos globales se crean al comenzar el programa y se destruyen al salir de él. Sin embargo, se puede crear un objeto también mediante el operador new, que asigna la memoria necesaria para alojar el objeto y devuelve su dirección, en forma de puntero, al objeto en cuestión. Los constructores normalmente implican la aplicación de new. p =new int(9) //p es un puntero a int inicializado a 9 cadena cad1("hola"); cadena *cad2=new cadena; Un objeto creado con new no tiene ámbito, es decir, no se destruye automáticamente al salir fuera del ámbito, sino que existe hasta que se destruye explícitamente mediante el operador delete. class cadena { char *datos; public: cadena(int); ~cadena(); }; cadena::cadena(int lon) { datos=new char[lon]; } cadena::~cadena() { delete datos; } http://programarenc.webcindario.com/Cplus/capitulo5.htm Constructores y Destructores Constructor Código PHP: void __construct ( [varios args [, ...]]) PHP 5 permite a los desarrolladores (programadores) declarar metodos constructores para las clases. Las clases que tienen un método constructor llaman a dicho método cada vez que se crea una instancia de ellas. Por eso, es conveniente para cualquier inicialización que el objeto pueda necesitar antes de ser usado.PHP 5 allows developers to declare constructor methods for classes. Classes which have a constructor method call this method on each newly-created object, so it is suitable for any initialization that the object may need before it is used. Nota: Los constructores de las superclases no son llamados por defecto si sus subclases definen un constructor. Se require una llamada a parent::__construct() dentro del constructor de la subclase para que ordene la ejecucion del constructor de su clase madre. Note: Parent constructors are not called implicitly if the child class defines a constructor. In order to run a parent constructor, a call to parent::__construct() within the child constructor is required. Ejemplo 19-6. Usando nuevo constructor unificado Código PHP: <?php class ClaseBase { function __construct() { print "Constrcutor en la clase Base\n"; } } class SubClase extends ClaseBase { function __construct() { parent::__construct(); print "Constructor en SubClase\n"; } } $obj = new ClaseBase(); $obj = new SubClase(); ?> Por compatibilidad con versiones anteriores, si PHP5 no puede encontrar una funcion __construct() para una clase determinada, buscara un constructor al viejo estilo, mediante un metodo con el mismo nombre de la clase. En efecto, esto quiere decir que el unico caso en el que habría problemas de compatibilidad es si la clase tuvo un metodo llamado __construct () que ha sido usado con un sentido diferente. For backwards compatibility, if PHP 5 cannot find a __construct() function for a given class, it will search for the old-style constructor function, by the name of the class. Effectively, it means that the only case that would have compatibility issues is if the class had a method named __construct() which was used for different semantics. Aca necesito correccion Destructor Código PHP: void __destruct ( void ) PHP 5 introduce el conpecto destructor similar al de otros lenguajes orientados a objetos, como C++. El metodo destructor sera llamado tan pronto como todas las referencias a un objeto en particular sean removidas o cuando el objeto es explicitamente destruido. PHP 5 introduces a destructor concept similar to that of other object-oriented languages, such as C++. The destructor method will be called as soon as all references to a particular object are removed or when the object is explicitly destroyed. Ejemplo 19-7. Ejemplo de Destructor Código PHP: <?php class MiClaseDestruible { function __construct() { print "En el constructor\n"; $this->name = "MiClaseDestruible"; } function __destruct() { print "Destruyendo ... " . $this->name . "\n"; } } $obj = new MiClaseDestruible(); ?> Asi como los constructores, los destructores de las superclases no seran llamados implicitamente por el motor interprete. Para ordenar la ejecucion del destructor de una superclase, uno tendria que tener en forma explicita la llamada parent::__destruct () en el destructor de la subclase. Like constructors, parent destructors will not be called implicitly by the engine. In order to run a parent destructor, one would have to explicitly call parent::__destruct() in the destructor body. http://www.forosdelweb.com/showpost.php?p=914250&postcount =13 3.6 Declaración de métodos constructor y destructor. 3.7 Aplicaciones de constructores y destructores. 3.8 Tipos de constructores y destructores. Unidad 4. Sobrecarga. 4.4 Conversión de tipos. Conversión automática de tipos En C y C++, si el compilador encuentra una expresión o una llamada a función que usa un tipo que no es el que requiere, puede usualmente realizar una conversión automática de tipos desde el tipo que tiene hasta el tipo que necesita. En C++, puede conseguir este mismo efecto para los tipos definidos por el usuario creando funciones de conversión de tipos automática. Estas funciones se pueden ver en dos versiones:un tipo particular de constructores y un operador sobrecargado. 6.1. Conversión por constructor Si define un constructor que toma como su ónico argumento un objeto(o referencia) de otro tipo, ese constructor permite al compilador realizar una conversión automática de tipos. Por ejemplo: //: C12:AutomaticTypeConversion.cpp // Type conversion constructor class One { public: One() {} }; class Two { public: Two(const One&) {} }; void f(Two) {} int main() { One one; f(one); // Wants a Two, has a One } ///:~ Cuando el compilador ve f() llamada con un objeto One, mira en la declaración de f() y nota que requiere un Two. Entonces busca si hay alguna manera de conseguir un Two de un One, y encuentra el constructor Two::Two(One) al cual llama. El objeto resultante Two es pasado a f(). En este caso, la conversión automática de tipos le ha salvado del problema de definir dos versiones sobrecargadas de f(). Sin embargo el coste es la llamada oculta al constructor de Two lo cual puede importar si está preocupado por la eficiencia de las llamadas a f(), 6.1.1. Prevenir la conversión por constructor Hay veces que la conversión automática de tipos via constructor puede ocasionar problemas. Para desactivarlo, modifique el constructor anteponiéndole la palabra reservada explicit(que sólo funciona con constructores). Así se ha hecho para modificar el constructor de la clase Two en el ejemplo anterior: //: C12:ExplicitKeyword.cpp // Using the "explicit" keyword class One { public: One() {} }; class Two { public: explicit Two(const One&) {} }; void f(Two) {} int main() { One one; //! f(one); // No auto conversion allowed f(Two(one)); // OK -- user performs conversion } ///:~ Haciendo el constructor de Two explicito, se le dice al compilador que no realice ninguna conversión automática de tipos usando ese constructor en particular(otros constructores no explicitos en esa clase pueden todavia realizar conversiones automáticas). Si el usuario quiere que ocurra esa conversión, debe escribir el codigo necesario. En el codigo de arriba, f(Two(one)) crea un objeto temporal de tipo Two desde one, justo como el compilador hizo en la versión previa. 6.2. Conversión por operador La segunda manera de producir conversiones automáticas de tipos es a través de la sobrecarga de operadores. Puede crear una función miembro que tome el tipo actual y lo convierta en el tipo deseado usando la palabras reservada operator seguida del tipo al que quiere convertir. Esta forma de sobrecarga de operadores es ónica porque parece que no se especifica un tipo de retorno - el tipo de retorno es el nombre del operador que está sobrecargando. He aquí un ejemplo: //: C12:OperatorOverloadingConversion.cpp class Three { int i; public: Three(int ii = 0, int = 0) : i(ii) {} }; class Four { int x; public: Four(int xx) : x(xx) {} operator Three() const { return Three(x); } }; void g(Three) {} int main() { Four four(1); g(four); g(1); // Calls Three(1,0) } ///:~ Con la técnica del constructor, la clase destino realiza la conversión, pero con los operadores, la realiza la clase origen. Lo valioso dela técnica del constructor es que puede añadir una nueva ruta de conversión a un sistema existente mientras está creando una nueva clase. Sin embargo, creando un constructor con un ónico argumento siempre define una conversión automática de tipos(incluso si requiere más de un argumento si el resto de los argumentos tiene un valor por defecto), que puede no ser lo que desea(en cuyo caso puede desactivarlo usando explicit). Además, no hay ninguna manera de usar una conversión por constructor desde un tipo definido por el usuario a un tipo incorporado;esto es posible solo con la sobrecarga de operadores. 6.2.1. Reflexividad Una de las razones mas normales para usar operadores sobrecargados globales en lugar de operadores miembros es que en la versión global, la conversión automática de tipos puede aplicarse a cualquiera de los operandos, mientras que con objetos miembro, el operando de la parte izquierda debe ser del tipo apropiado. Si quiere que ambos operandos sean convertidos, la versión global puede ahorrarle un montón de código. He aquí un pequeño ejemplo: //: C12:ReflexivityInOverloading.cpp class Number { int i; public: Number(int ii = 0) : i(ii) {} const Number operator+(const Number& n) const { return Number(i + n.i); } friend const Number operator-(const Number&, const Number&); }; const Number operator-(const Number& n1, const Number& n2) { return Number(n1.i - n2.i); } int main() { Number a(47), b(11); a + b; // OK a + 1; // 2nd arg converted to //! 1 + a; // Wrong! 1st arg not a - b; // OK a - 1; // 2nd arg converted to 1 - a; // 1st arg converted to } ///:~ Number of type Number Number Number La clase Number tiene tanto un miembro operator+ como un firiend operator-. Dado que hay un constructor que acepta un argumento int simple, un int puede ser convertido automáticamente a un Number, pero sólo bajo las condiciones adecuadas. En main(), puede ver que añadir un Number a otro Number funciona bien dado que tiene una correspondencia exacta con el operador sobrecargado. Además, cuando el compilador ve un Number seguido de un + y de un int, puede emparejarlo a la función miembro Number::operator+ y convertir el argumentoint a un Number usando el constructor. Pero cuando ve un int, un + y un Number, no sabe que hacer porque todo lo que tiene es Number::operator+ el cual requiere que el operando de la izquierda sea ya un objeto Number. Así que, el compilador emite un error. Con friend operator- las cosas son diferentes. El compilador necesita rellenar ambos argumentos como quiera que pueda; no está restringido a tener un Number como argumento de la parte izquierda. Asi que si ve: 1 - a puede convertir el primer argumento a un Number usando el constructor. A veces querrá ser capaz de restringir el uso de sus operadores haciéndolos miembros. Por ejemplo, cuando multiplique una matriz por un vector, el vector debe ir en la derecha. Pero si quiere que sus operadores sean capaces de convertir cualquier argumento, haga el operador una función friend. Afortunadamente, el compilador cogerá la expresión 1-1 y convertirá ambos argumentos a objetos Number y despues llamará a operator-. Eso significaría que el código C existente pudiera empezar a funcionar de forma diferente. El compilador encaja la posibilidad mas simple primero, la cual es el operador incorporado para la expresión 1-1 . 6.3. Ejemplo de conversión de tipos Un ejemplo en el que la conversión automática de tipos es extremadamente útil es con cualquier clase que encapsule una cadena de caracteres(en este caso, simplemente implementaremos la clase usando la clase estándar de C++ string dado que es simple). Sin la conversión automática de tipos, si quiere usar todas las funciones existentes de string de la librería estándar de C, tiene que crear una función miembro para cada una, así: //: C12:Strings1.cpp // No auto type conversion #include "../require.h" #include <cstring> #include <cstdlib> #include <string> using namespace std; class Stringc { string s; public: Stringc(const string& str = "") : s(str) {} int strcmp(const Stringc& S) const { return ::strcmp(s.c_str(), S.s.c_str()); } // ... etc., for every function in string.h }; int main() { Stringc s1("hello"), s2("there"); s1.strcmp(s2); } ///:~ Aquí, sólo se crea la función strcmp(), pero tendría que crear las correspondientes funciones para cada una de <cstring> que necesitará. Afortunadamente, puede proporcionar una conversión automática de tipos permitiendo el acceso a todas las funciones de cstring. //: C12:Strings2.cpp // With auto type conversion #include "../require.h" #include <cstring> #include <cstdlib> #include <string> using namespace std; class Stringc { string s; public: Stringc(const string& str = "") : s(str) {} operator const char*() const { return s.c_str(); } }; int main() { Stringc s1("hello"), s2("there"); strcmp(s1, s2); // Standard C function strspn(s1, s2); // Any string function! } ///:~ Ahora cualquier función que tome un argumento char* puede tomar también un argumento Stringc porque el compilador sabe como crear un char* de un Stringc. 6.4. Las trampas de la conversión automática de tipos Dado que el compilador debe elegir como realizar una conversión de tipos, puede meterse en problemas si no usted no diseña las conversiones correctamente. Una situación obvia y simple sucede cuando una clase X que puede convertirse a sí misma en una clase Y con un operator Y(). Si la clase Y tiene un constructor que toma un argumento simple de tipo X, esto representa la conversión de tipos por identidad. El compilador ahora tiene dos formas de ir de X a Y, asi que se generará una error de ambigüedad cuando esa conversión ocurra: //: C12:TypeConversionAmbiguity.cpp class Orange; // Class declaration class Apple { public: operator Orange() const; // Convert Apple to Orange }; class Orange { public: Orange(Apple); // Convert Apple to Orange }; void f(Orange) {} int main() { Apple a; //! f(a); // Error: ambiguous conversion } ///:~ La solución obvia a este problema es no hacerla. Simplemente proporcione una ruta ónica para la conversión automática de un tipo a otro. Un problema más difícil de eliminar sucede cuando proporciona conversiones automáticas a más de un tipo. Esto se llama a veces acomodamiento: //: C12:TypeConversionFanout.cpp class Orange {}; class Pear {}; class Apple { public: operator Orange() const; operator Pear() const; }; // Overloaded eat(): void eat(Orange); void eat(Pear); int main() { Apple c; //! eat(c); // Error: Apple -> Orange or Apple -> Pear ??? } ///:~ La clase Apple tiene conversiones automáticas a Orange y a Pear. El elemento capcioso sobre esto es que no hay problema hasta que alguien inocentemente crea dos versiones sobrecargadas de eat(). (Con sólo una versión el codigo en main() funciona correctamente). De nuevo la solución - y el lema general de la conversión automática de tipos- es proveer solo una ónica conversión automática de un tipo a otro. Puede tener conversiones a otros tipos, sólo que no deberían ser automaticas. Puede crear llamadas a funciones explicitas con nombres como makeA() y makeB(). 6.4.1. Actividades ocultas La conversión automática de tipos puede producir mas actividad subyacente de la que podría esperar. Mire esta modificación de CopyingVsInitialization.cpp como un juego de inteligencia: //: C12:CopyingVsInitialization2.cpp class Fi {}; class Fee { public: Fee(int) {} Fee(const Fi&) {} }; class Fo { int i; public: Fo(int x = 0) : i(x) {} operator Fee() const { return Fee(i); } }; int main() { Fo fo; Fee fee = fo; } ///:~ No hay un constructor para crear Fee fee de un objeto Fo. Sin embargo, Fo tiene una conversión automática de tipos a Fee. No hay un constructor de copia para crear un Fee de un Fee, pero esta es una de las funciones especiales que el compilador puede crear por usted. (El constructor por defecto, el constructor de copia y operator=) y el destructor puede sintetizarse automáticamente por el compilador. Asi que para la relativamente inocua expresión: Fee fee = fo; el operador de conversión automática es llamado, y se crea un constructor de copia. Use la conversión automática de tipos con precaución. Como con toda la sobrecarga de operadores, es excelente cuando reduce la tarea de codificación significativamente, pero no vale la pena usarla de forma gratuita. http://arco.inf-cr.uclm.es/~dvilla/pensar_en_C++/ch12s06.html http://www.geocities.com/usmindustrial/Economica.htm 4.5 Sobrecarga de métodos. Sobrecarga de métodos §1 Sinopsis Lo mismo que ocurre con las funciones normales ( 4.4.1), en la definición de funciones-miembro puede ocurrir que varias funciones compartan el mismo nombre, pero difieran en el tipo y/o número de argumentos. Ejemplo: class hotel { char * nombre; int capacidad; public: void put(char*); void put(long); void put(int); } La función put, definida tres veces con el mismo nombre está sobrecargada; el compilador sabe en cada caso que definición usar por la naturaleza de los argumentos (puntero-a-char, long o int). Nota: No confundir estos casos de sobrecarga en métodos con el polimorfismo; este último caso se refiere a jerarquías de clases en las que pueden existir diversas definiciones de métodos con los mismos argumentos, son las denominadas funciones virtuales ( 4.11.8a). §2 Adecuación automática de argumentos Una característica interesante y a nuestro entender más desafortunada del lenguaje (por la posibilidad de errores difíciles de detectar), es que en la invocación de funciones, sean estas miembros de clase o no, el compilador intenta automáticamente adecuar el tipo del argumento actual con el formal ( 4.4.5) es decir: adecuar el argumento utilizado con el que espera la función según su definición. Este comportamiento puede esquematizarse como sigue: int funcion (int i) { /* ... */ .... otraFuncion (){ int x = funcion(5); argumentos int y = funcion(int('a')); explícita int z = funcion('a'); automática!! ... } } // L.1: // L.4: Ok. concordancia de // L.5: Ok. promoción // L.6: Ok. promoción Es evidente que en L.4 y L.5 los argumentos formales y los actuales concuerdan, bien porque se trata del mismo tipo, bien porque se ha realizado una promoción explícita ( 4.9.9). Lo que no resulta ya tan evidente, ni deseable a veces, es que en L.6 el compilador autorice la invocación a funcion() pasando un tipo distinto del esperado. Esta promoción del argumento actual realizada de forma automática por el compilador (sin que medie ningún error o aviso -"Warning"- de compilación) puede ser origen de errores, y de que el programa use inadvertidamente una función distinta de la deseada. En este sentido podemos afirmar que el lenguaje C++ es débilmente tipado. Posiblemente esta característica sea una de las desafortunadas herencias del C clásico [1], y es origen incluso de ambigüedades que no deberían existir. Por ejemplo, no es posible definir las siguientes versiones de un constructor: class Entero { public: int x; Entero(float fl = 1.0) { x = int(fl); } Entero(char c) { x = int(c); } } A pesar de su evidente disparidad, el compilador interpreta que existe ambigüedad en los tipos, ya que char puede ser provomido a float. §2.1 Para complicar la cosa Pero la conformación de argumentos no acaba con lo expuesto. Si en la invocación de una función f1 no existe correspondencia entre el tipo de un argumento actual x, y el formal y, el compilador puede buscar automáticamente en el ámbito si existe una función f2 que acepte el argumento discordante x y devuelva el tipo y deseado. En cuyo caso realizará una invocación implícita a dicha función f2(x) y aplicará el resultado y como argumento a la función primitiva. El mecanismo involucrado puede ser sintetizado en el siguiente esquema: class C { // L.1 public: int x; ... }; int getx (C c1, C c2) { return c1.x + c2.x; } C fun (float f= 1.0) { // L.6: C ct = { f }; return ct; } ... unaFuncion () { int x = 10; C c1; ... int z = getx(x, c1); // L.15 ... } // L.5 La invocación a getx en L.15 es correcta, a pesar de no existir una definición concordante para esta función cuyos argumentos formales son dos objetos tipo C, y en este caso el primer argumento x es un int. El mecanismo utilizado por el compilador es el siguiente: Dentro del ámbito de visibilidad existe una función fun (definida en L.6) que acepta un float y devuelve un objeto del tipo necesitado por getx. Aunque el argumento x disponible no es precisamente un float, puede ser promovido fácilmente a dicho tipo. En consecuencia el compilador utiliza en L.15 la siguiente invocación: int z = getx( fun( float(x) ), c1); Este tipo de adecuaciones automáticas son realizadas por el compilador tanto con funciones-miembro [2] como con funciones normales. Considere cuidadosamente el ejemplo que sigue, e intente justificar el proceso seguido para obtener cada uno de sus resultados. #include <iostream> using namespace std; int x = 10; long lg = 10.0; class Entero { public: int x; int getx (int i) { return x * i; } int getx () { return x; } int getx (Entero e1, Entero e2) { return e1.x + e2.x; } Entero(float f= 1.0) { x = f; } }; int getx (int i) { return x * i; } int getx () { return x; } int getx (Entero e1, Entero e2) { return e1.getx() + e2.getx(); } Entero fun (float f= 1.0) { Entero e1 = { f }; return e1; } void main () { // ========================== Entero c1 = Entero(x); cout << "E1 = " << c1.getx(0) << endl; cout << "E2 = " << c1.getx() << endl; cout << "E3 = " << c1.getx('a') << endl; cout << "E4 = " << c1.getx(lg) << endl; cout << "E5 = " << c1.getx(x, c1) << endl; cout << "E6 = " << c1.getx('a', c1) << endl; cout << "F1 = " << getx(0) << endl; cout << "F2 = " << getx() << endl; cout << "F3 = " << getx('a') << endl; cout << "F4 = " << getx(lg) << endl; cout << "F5 = " << getx(x, c1) << endl; cout << "F6 = " << getx('a', c1) << endl; } Salida (reformateada en dos columnas): E1 E2 E3 E4 = = = = 0 10 970 100 F1 F2 F3 F4 = = = = 0 10 970 100 E5 = 20 F5 = 20 E6 = 107 F6 = 107 http://www.zator.com/Cpp/E4_11_2a2.htm 4.6 Sobrecarga de operadores. SOBRECARGA DE OPERADORES EN C++ Aunque la sobrecarga de operadores en C++ es un tema bastante básico y aparece en cualquier libro, hay algunos tipos de sobrecarga (la de operadores globales y la del cast) que descubrí bastante tarde y que me parecieron bastante útiles. Por este motivo escribo este tutorial, aunque posiblemente no encuentres en él nada que no puedas encontrar en un buen libro de C++ (como por ejemplo, "Programación Orientada a Objetos con C++", de Francisco Javier Ceballos). Los temas que voy a tratar son: 1. 2. 3. 4. 5. 6. Una sobrecarga normalita: el operator + operadores globales: nuevamente el operator + El operador de cast Algunos detalles sobre el operator = Los operadores new y delete El ejemplo Para nuestro ejemplo vamos a hacer parte de una clase ComplejoC que representa un número complejo. Únicamente redefiniremos los operadores de suma, de cast a double y para sacarlo por pantalla con cout . ¿Qué es la sobrecarga de operadores? En cualquier lenguaje de programación, y C++ no es menos, se puden hacer operaciones con los tipos básicos del lenguaje: se pueden sumar enteros, compararlos, etc. Lo siguiente es perfectamente válido en C++ int a=3; int b=4; int c; c = a + b; Si estos tipos no son los básicos del lenguaje, no se puede hacer sumas con ellos. Por ejemplo, si ClaseC es una clase que tengo definida en C++, el siguiente código dará error. ClaseC a; ClaseC b; ClaseC c; c = a + b; C++ permite que hagamos estas cosas si definimos en algún sitio cómo se suman esas clases. Definiendo cómo se hacen las operaciones, podremos escribir las operaciones con nuestras clases de la misma forma que si se trataran de tipos básicos del lenguaje. De hecho podemos definir cualquier operador que nos dé C++, desde algunos muy normales como suma, resta, multiplicación, mayor qué, etc, a otros un poco más raros como el operador () o el [], de forma que ClaseC[i] o ClaseC(i,j) pueden devolver lo que nosostros queramos. Sobrecarga del operador suma Si tenemos una clase ComplejoC que representa un complejo, para poder sumar dos de estas clases simplemente poniendo un +, como con cualquier tipo básico de C++, debemos sobrecargar el operador +, darle una nueva funcionalidad. La sobrecarga de un operador suma se hace de la siguiente manera class ComplejoC { public: ComplejoC operator + (double sumando); double ComplejoC operator + (const double sumando[]); array ComplejoC operator + (const ComplejoC &sumando); sí. }; // Permite sumar un ComplejoC con un // Permite sumar un ComplejoC con un // Permite sumar dos ComplejoC entre Aquí estamos redefiniendo tres operadores suma para que nos permita sumar a nuestra clase ComplejoC cosas como un double, un array de double (que supondremos contiene dos double, aunque no tenemos forma de comprobarlo) y otra clase ComplejoC . En estas funciones hemos puesto const en los parámetros para no poder cambiarlos dentro del código. En la tercera función pasamos ComplejoC por referencia (el &), para evitar que se hagan copias innecesarias. En la primera función no es necesario nada de esto, puesto que es un simple double. En la segunda, ponemos const para no poder modificar el array, pero no es necesaria la referencia, puesto que los arrays son punteros. A la hora de implementar debemos tener cuidado con los const que hemos puesto. Por ejemplo, en el tercer operador +, recibimos un sumando que es const. El código que implementemos dentro no puede modificar dicho sumando, ni puede llamar a ningún método de ese sumando que no esté declarado como const. Si lo intentamos, el compilador dará error. Supongamos que ComplejoC tiene un atributo double x (parte real) y un método dameX() para obtener dicho atributo, este método debe estar declarado const para poder llamarlo desde nuestro operator +. Más o menos esto: class ComplejoC { public: double dameX() const; sumando.dameX(). }; // Atención al const del final. Sin el no podemos llamar a de forma que en el operator + podemos llamar a sumando.dameX(). Una vez implementados estos métodos, podemos hacer operaciones como ComplejoC a; ComplejoC b; b = a + 1.0; // Aprovechando la primera sobrecarga double c[] = {1.0, 2.0};< BR > b = a + c; b = a + b; // Aprovechando la segunda sobrecarga // Aprovechando la tercera sobrecarga Cuando el compilador lee a + 1.0, lo interpreta como a.operator + (1.0), es decir, la llamada al operador suma al que se le pasa como parámetro un double . De la misma forma sucede con los otros dos operadores suma. Sin embargo, esto nos da un pequeño problema. ¿Qué pasa si ponemos 1.0 + a?. Debería ser lo mismo, pero al compilador le da un pequeño problema. Intenta llamar al método operator + de 1.0, que no existe, puesto que 1.0 ni es una clase ni tiene métodos. Para solucionar este problema tenemos la sobrecarga de operadores globales. Sobrecarga de operadores suma globales Un operador global es una función global que no pertenece a ninguna clase y que nos indica cómo operar con uno o dos parámetros (depende del tipo de operador). En nuestro ejemplo de las sumas, para poder poner los sumandos al revés, deberíamos definir las siguientes funciones globales: ComplejoC operator + (double sumando1, const ComplejoC &sumando2); ComplejoC operator + (double sumando1[], const ComplejoC &sumando2); Estas funciones le indican al compilador cómo debe sumar los dos parámetros y qué devuelve. Con ellas definidas e implementadas, ya podemos hacer b = 1.0 + a; b = c + a; // c era un array de double Esta sobrecarga es especialmente útil cuando tratamos con una clase ya hecha y que no podemos modificar. Por ejemplo, cout es de la clase ostream y no podemos modificarla, sin embargo nos sería de utilidad sobrecargar el operador << de ostream de forma que pueda escribir nuestros números complejos. La siguiente llamada nos dará error mientras no redefinamos el operator << de ostream . cout << a << endl; // a es un ComplejoC Con la sobrecarga de operadores globales podemos definir la función ostream &operator << (ostream &salida, const ComplejoC &valor); Con esta función definida, el complejo se escribirá en pantalla como indique dicha función. Esta función deberá escribir la parte real e imaginaria del complejo en algún formato, utilizando algo como salida << valor.dameX() << " + " << valor.dameY() << "j"; El operador devuelve un ostream, que será un return cout. De esta forma se pueden encadenar las llamadas a cout de la forma habitual. cout << a << " + " << b << endl; Primero se evalúa operator << (cout, a), que escribe a en pantalla y devuelve un cout, con lo que la expresión anterior quedaría, después de evaluar esto cout << " + " << b << endl; y así consecutivamente. Hay que tener en cuenta que estos operadores globales no son de la clase, así que sólo pueden acceder a métodos y atributos públicos de la misma. El operador cast Un operador interesante es el operador "cast". En C++, si tenemos dos tipos básicos distintos, podemos pasar de uno a otro haciendo un cast (siempre que sean compatibles de alguna forma). Por ejemplo int a; double b; a = (int)b; El cast consiste en poner delante, entre paréntesis, el tipo que queramos que sea. En algunos casos, como en el de este ejemplo, el cast se hace automáticamente y no es necesario ponerlo. Puede que de un "warning" en el compilador avisando de que perderemos los decimales. En principio, con las clases no se puede hacer cast a otros tipos, pero es posible declarar operadores que lo hagan. La sintaxis sería: class ComplejoC { public: operator double (); } // Permite hacer un cast de ComplejoC a double Con este podemos hacer cast de nuestra clase a un double . Es nuestro problema decidir cómo se hace ese cast. En el código de ejemplo que hay más abajo se ha definido como la operación módulo del número complejo. double a; ComplejoC b; a = (double)b; En el operator cast se pone operator seguido del tipo al que se quiere hacer el cast. No se pone el tipo del valor devuelto, puesto que ya está claro. Si ponemos operator double, hay que devolver un double . En el operator cast no se pone parámetro, puesto que el parámetro recibido será una instancia de la clase. Conviene tener cuidado con definir muchos operadores cast, puesto que el compilador los tendrá todos presentes y será capaz, encadenando unos con otros, de hacer cast entre tipos que no tienen nada que ver. Por ejemplo, si sumamos un ComplejoC con un DibujoC (que no tienen nada que ver) y ambos tienen cast e int, es posible que el compilador los transforme ambos en int y luego los sume como enteros. ¿Cómo hacemos un cast al revés?. Es decir, ¿Cómo podemos convertir un double a ComplejoC?. El asunto es sencillo, basta hacer un constructor que admite un double como parámetro. class ComplejoC { public: ComplejoC (double valor); }; Este constructor sirve para lo que ya sabemos ComplejoC a(3.0); o podemos usarlo para hacer un cast de double aComplejoC ComplejoC a; a = (ComplejoC)3.0; El operador igual El operator = es como los demás. Simplemente un pequeño detalle. C++ por defecto tiene el operador igual definido para clases del mismo tipo. Por ejemplo, sin necesidad de redefinir nada, podemos hacer ComplejoC a; ComplejoC b; a = b; Este igual por defecto lo único que hace es copiar el contenido del uno sobre el otro, como si fueran bytes, sin saber qué atributos está copiando ni qué significan. Para clases sencillas, que solo tienen atributos que no son punteros, esto es más que suficiente. Si embargo, si algún atributo es un puntero, tenemos que tener mucho cuidado con lo que hacemos. Supongamos que ClaseC tiene un atributo Atributo que es un puntero. Supongamos también que en el constructor de la clase, se hace new del puntero para que tenga algo y en el destructor se hace el delete correspondiente. ClaseC { public: ClaseC () {Atributo = new int[3]; } ~ClaseC () {delete [] Atributo; } protected: int *Atributo; }; // Se crea un array de tres enteros // Se libera el array. Si ahora hacemos esto ClaseC *a = new ClaseC(); ClaseC *b = new ClaseC(); // a tiene ahora un array de 3 enteros en su interior // b tiena ahora otro array de 3 enteros en su interior *a = *b; sitio // Se copia el Atributo de b sobre el de a, es decir, ahora a->Atributo apunta al mismo delete b; // Ahora si que la hemos liado. // que b->Atributo Cuando hacemos a=b, con el operador igual por defecto de C++, se hace que el puntero Atributo de a apunte al mismo sitio que el de b. El array original de a->Atributo lo hemos perdido, sigue ocupando memoria y no tenemos ningún puntero a él para liberarlo. Cuando hacemos delete b, el destructor de b se encarga de liberar su array. Sin embargo, al puntero a->Atributo nadie le avisa de esta liberación, se queda apuntando a una zona de memoria que ya no es válida. Cuando intentemos usar a->Atributo más adelante, puede pasar cualquier cosa (cambios aleatorios de variables, caidas del programa, etc). En el tema de punteros tienes un poco más detallado todo esto. Allí se habla de estructuras, pero también se aplica a clases. La forma de solucionar esto, es definiendo nosotros un operator = que haga una copia real del array, liberando previamente el nuestro o copiando encima los datos. ClaseC { public: ClaseC &operator = (const ClaseC &original) { int i; // Damos por supuesto que ambos arrays existen y son de tamaño 3 for (i=0;i<3;i++) Atributo[i] = original.Atributo[i]; } }; Sobrecarga del operador new y delete Otro operador interesante de sobrecargar es el new y el delete. Si los sobrecargamos dentro de la clase, cada vez que hagamos un new a nuestra clase se llamará a nuestro operador. Más interesante es la sobrecarga de los operadores new y delete globales. Sobrecargando estos operadores se llamará a nuestras funciones cada vez que hagamos un new o un delete de cualquier cosa (clases o variables). Esta característica nos permite hacer contabilidad de punteros, para ver si liberamos todo lo que reservamos o liberamos lo mismo más veces de la cuenta. El código de ejemplo Con esto, aunque no he entrado mucho en detalles, está todo más o menos dicho: Se pueden sobrecargar operadores en una clase, operadores globales fuera de las clases y algunos operadores interesantes/curiosos como los operadores de cast, new o delete . En el siguiente código de ejemplo se implementa la clase ComplejoC, pero sólo los operadores de suma indicados, algunos constructores, operador para cout y el módulo. En Complejo.h y Complejo.cc tienes la clase. En Prueba.cc tienes el programa principal que hace varias cosas con la clase. Para compilarlo todo, tienes el Makefile. Puedes descargar los cuatro ficheros, quitarles la extensión .txt y compilar con make . http://www.geocities.com/chuidiang/sobrecarga/sobrecarga.html Sobrecarga de operadores 12.1. Sobrecarga de operadores 12.1.1. ¿ Qué es la sobrecarga de operadores ? La sobrecarga de operadores es la capacidad para transformar los operadores de un lenguaje como por ejemplo el +, -, etc, cuando se dice transformar se refiere a que los operandos que entran en juego no tienen que ser los que admite el lenguaje por defecto. Mediante esta tecnica podemos sumar dos objetos creados por nosotros o un objeto y un entero, en vez de limitarnos a sumar numeros enteros o reales, por ejemplo. La sobrecarga de operadores ya era posible en c++ y en otros lenguajes, pero sorprendentemente java no lo incorpora asi que podemos decir que esta caracteristica es una ventaja de c# respesto a java, aunque mucha gente esta posibilidad no lo considera una ventaja por que complica el codigo. A la hora de hablar de operadores vamos a distinguir entre dos tipos, los unarios y los binarios. Los unarios son aquellos en que solo se requiere un operando, por ejemplo a++, en este caso el operando es 'a' y el operador '++'. Los operadores binarios son aquellos que necesitan dos operadores, por ejemplo a+c , ahora el operador es '+' y los operandos 'a' y 'c'. Es importante esta distincion ya que la programacion se hara de forma diferente Los operadores que podemos sobrecargar son los unarios, +, -, !, ~, ++, --; y los binarios +, -, *, /, %, &, |, ^, <<, >>. Es importante decir que los operadores de comparacion, ==, !=, <, >, <=, >=, se pueden sobrecargar pero con la condicion que siempre se sobrecargue el complementario, es decir si sobrecargamos el == debemos sobrecargar el !=. 12.1.2. Sobrecargando operadores en la practica Para mostrar la sobrecarga vamos a usar el repetido ejemplo de los numeros complejos, ( aunque tambien valdria el de las coordenadas cartesianas ). Como se sabe los numeros complejos tienen dos partes, la real y la imaginaria, cuando se suma dos numeros complejos su resultado es la suma de las dos partes, para ello se va a crear una clase llamada ComplexNum que contendra ambas partes. Sin esta técnica no se podria sumar dos objetos de este tipo con este practico método, ya que esta clase no es válida como operando de los operadores de c#. Empecemos con el código de la clase de numeros imaginarios. public class ComplexNum{ private float _img; private float _real; public ComplexNum(float real, float img) { _img = img; _real = real; } public ComplexNum() { _img = 0; _real = 0; } public float getReal(){ return this._real; } public float getImg() { return this._img; } public String toString() { if (_img >= 0 ) return _real + "+" + _img +"i"; else return _real + "" + _img + "i"; } } En el ejemplo hemos puesto la clase, con un par de constructores , dos getters para obtener los datos privados de la clase y un metodo que nos transfoma el numero complejo a cadena para que se pueda visualizarlo facilmente, a esta clase la iremos añadiendo métodos para que tenga capacidad de usar operadores sobrecargados. 12.1.2.1. Operadores binarios Para empezar vamos a sobrecargar el operador suma('+') para que al sumar dos objetos de la clase ComplexNum, es decir dos numeros complejos obtengamos otro numero complejo que sera la suma de ambas partes. Cabe destacar que los prototipos para sobrecargar operadores seran: public static Operando operator+(Operando a, Operando b) Este es el prototipo para el operador +, el resto de operadores binarios van a seguir el mismo patron. Por tanto el código del método de sobrecarga será: public static ComplexNum operator+(ComplexNum a, ComplexNum b) { return new ComplexNum(a.getReal() + b.getReal(), a.getImg() + b.getImg()); } Este método sobrecarga el operador suma para que podamos sumar dos numeros complejos. Un dato a tener en cuenta es que los métodos que sobrecargan operadores deben ser static. Como se ve en el código los operandos son 'a' y 'b', que se reciben como parametro y el resultado de la operacion es otro numero complejo que es el que retorna el método. Por tanto se limita a crear un nuevo numero complejo con ambas partes operadas. De la misma forma podemos crear la sobrecarga del operador resta('-') para que lleve a cabo la misma función public static ComplexNum operator-(ComplexNum a, ComplexNum b) { return new ComplexNum(a.getReal() - b.getReal(), a.getImg() b.getImg()); } Como vemos el metodo es identico solo q sustituyendo los + por -. En este caso el trabajo que hacemos dentro del metodo es trivial pero podria ser tan complejo como se quiera. 12.1.2.2. Operadores Unarios En esta sección se vera como sobrecargar los operadores unarios, es decir aquellos que toman un solo operando, como por ejemplo a++. El prototipo de los métodos que van a sobrecargar operadores unarios será: public static Operando operator++(Operando a) Como antes sustituyendo el ++ por cualquier operador unario. El ejemplo dentro de nuestra clase de numeros complejos sería: public static ComplexNum operator++(ComplexNum a) { float auximg = a.getImg(); float auxreal = a.getReal(); return new ComplexNum(++auxreal, ++auximg); } A primera vista puede quedar la duda si estamos sobrecargando la operacion ++a o a++. Este aspecto se encarga el compilador de resolverlo, es decir, se sobrecarga la operacion ++ y el compilador se encarga de sumar y asignar o asignar y sumar. Este problema no ocurria en C++, cosa que teniamos que manejar nosotros Como hemos dicho antes la operacion que hagamos dentro del metodo que sobrecarga el operador es totalmente libre, se puede poder el ejemplo de multiplicar dos matrices lo que es mas complejo que sumar dos numeros complejos http://www.monohispano.org/tutoriales/csharp/c831.html Unidad 5. Herencia. 5.10 Introducción a la herencia. Introducción Una de las propiedades más importantes de la POO es la abstracción de datos. Esta propiedad se manifiesta esencialmente en la encapsulación, que es la responsable de gestionar la complejidad de grandes programas al permitir la definición de nuevos tipos de datos: las clases. Sin embargo, la clase no es suficiente por sí sola para soportar diseño y programación orientada a objetos. Se necesita un medio para relacionar unas clases con otras. Un medio son las clases anidadas, pero sólo se resuelve parcialmente el problema, ya que las clases anidadas no tienen las características requeridas para relacionar totalmente una clase con otra. Se necesita un mecanismo para crear jerarquías de clases en las que la clase A sea afín a la clase B, pero con la posibilidad de poder añadirle también características propias. Este mecanismo es la herencia. La herencia es, seguramente, la característica más potente de la POO, después de las clases. Por herencia conocemos el proceso de crear nuevas clases, llamadas clases derivadas, a partir de una clase base. En C++ la herencia se manifiesta con la creación de un tipo definido por el usuario (clase), que puede heredar las características de otra clase ya existente o derivar las suyas a otra nueva clase. Cuando se hereda, las clases derivadas reciben las características (estructuras de datos y funciones) de la clase original, a las que se pueden añadir nuevas características o modificar las características heredadas. El compilador hace realmente una copia de la clase base en la nueva clase derivada y permite al programador añadir o modificar miembros sin alterar el código de la clase base. La derivación de clases consigue la reutilización efectiva del código de la clase base para sus necesidades. Si se tiene una clase base totalmente depurada, la herencia ayuda a reutilizar ese código en una nueva clase. No es necesario comprender el código fuente de la clase original, sino sólo lo que hace. 7.2 Clases derivadas En C++, la herencia simple se realiza tomando una clase existente y derivando nuevas clases de ella. La clase derivada hereda las estructuras de datos y funciones de la clase original. Además, se pueden añadir nuevos miembros a las clases derivadas y los miembros heredados pueden ser modificados. Una clase utilizada para derivar nuevas clases se denomina clase base, clase padre, superclase o ascendiente. Una clase creada de otra clase se denomina clase derivada o subclase. Se pueden construir jerarquías de clases, en las que cada clase sirve como padre o raíz de una nueva clase. 7.3 Conceptos fundamentales de derivación C++ utiliza un sistema de herencia jerárquica. Es decir, se hereda una clase de otra, creando nuevas clases a partir de las clases ya existentes. Sólo se pueden heredar clases, no funciones ordinarias n variables, en C++. Una clase derivada hereda todos los miembros dato excepto, miembros dato estáticos, de cada una de sus clases base. Una clase derivada hereda las funciones miembro de su clase base. Esto significa que se hereda la capacidad para llamar a funciones miembro de la clase base en los objetos de la clase derivada. http://programarenc.webcindario.com/Cplus/capitulo7.htm .Los siguientes elementos de la clase no se heredan: - Constructores - Destructores - Funciones amigas - Funciones estáticas de la clase - Datos estáticos de la clase - Operador de asignación sobrecargado Las clases base diseñadas con el objetivo principal de ser heredadas por otras se denominan clases abstractas. Normalmente, no se crean instancias a partir de clases abstractas, aunque sea posible. 7.4 La herencia en C++ En C++ existen dos tipos de herencia: simple y múltiple. La herencia simple es aquella en la que cada clase derivada hereda de una única clase. En herencia simple, cada clase tiene un solo ascendiente. Cada clase puede tener, sin embargo, muchos descendientes. La herencia múltiple es aquella en la cual una clase derivada tiene más de una clase base. Aunque el concepto de herencia múltiple es muy útil, el diseño de clases suele ser más complejo. 7.5 Creación de una clase derivada Cada clase derivada se debe referir a una clase base declarada anteriormente. La declaración de una clase derivada tiene la siguiente sintaxis: class clase_derivada:<especificadores_de_acceso> clase_base {...}; Los especificadores de acceso pueden ser: public, protected o private. 7.6 Clases de derivación Los especificadores de acceso a las clases base definen los posibles tipos de derivación: public, protected y private. El tipo de acceso a la clase base especifica cómo recibirá la clase derivada a los miembros de la clase base. Si no se especifica un acceso a la clase base, C++ supone que su tipo de herencia es privado. - Derivación pública (public). Todos los miembros public y protected de la clase base son accesibles en la clase derivada, mientras que los miembros private de la clase base son siempre inaccesibles en la clase derivada. - Derivación privada (private). Todos los miembros de la clase base se comportan como miembros privados de la clase derivada. Esto significa que los miembros public y protected de la clase base no son accesibles más que por las funciones miembro de la clase derivada. Los miembros privados de la clase siguen siendo inaccesibles desde la clase derivada. - Derivación protegida (protected). Todos los miembros public y protected de la clase base se comportan como miembros protected de la clase derivada. Estos miembros no son, pues, accesibles al programa exterior, pero las clases que se deriven a continuación podrán acceder normalmente a estos miembros (datos o funciones). 7.7 Constructores y destructores en herencia Una clase derivada puede tener tanto constructores como destructores, aunque tiene el problema adicional de que la clase base puede tomar ambas funciones miembro especiales. Un constructor o destructor definido en la clase base debe estar coordinado con los encontrados en una clase derivada. Igualmente importante es el movimiento de valores de los miembros de la clase derivada a los miembros que se encuentran en la base. En particular, se debe considerar cómo el constructor de la clase base recibe valores de la clase derivada para crear el objeto completo. Si un constructor se define tanto para la clase base como para la clase derivada, C++ llama primero al constructor base. Después que el constructor de la base termina sus tareas, C++ ejecuta el constructor derivado. Cuando una clase base define un constructor, éste se debe llamar durante la creación de cada instancia de una clase derivada, para garantizar la buena inicialización de los datos miembro que la clase derivada hereda de la clase base. En este caso la clase derivada debe definir a su vez un constructor que llama al constructor de la clase base proporcionándole los argumentos requeridos. Un constructor de una clase derivada debe utilizar un mecanismo de pasar aquellos argumentos requeridos por el correspondiente constructor de la clase base. derivada::derivada(tipo1 x, tipo2 y):base (x,y) {...} Otro aspecto importante que es necesario considerar es el orden en el que el compilador C++ inicializa las clases base de una clase derivada. Cuando El compilador C++ inicializa una instancia de una clase derivada, tiene que inicializar todas las clases base primero. Por definición, un destructor de clases no toma parámetros. Al contrario que los constructores, una función destructor de una clase derivada se ejecuta antes que el destructor de la clase base. 7.8 Redefinición de funciones miembro heredadas Se pueden utilizar funciones miembro en una clase derivada que tengan el mismo nombre que otra de una clase base. La redefinición de funciones se realiza mediante funciones miembro sobrecargadas en la clase derivada. Una función miembro redefinida oculta todas las funciones miembro heredadas del mismo nombre. Por tanto cuando en la clase base y en la clase derivada hay una función con el mismo nombre en las dos, se ejecuta la función de la clase derivada. 7.9 Herencia múltiple Es la propiedad con la cual una clase derivada puede tener más de una clase base. Es más adecuada para definir objetos que son compuestos, por naturaleza, tales como un registro personal, un objeto gráfico. Sólo es una extensión de la sintaxis de la clase derivada. Introduce una cierta complejidad en el lenguaje y el compilador, pero proporciona grandes beneficios. class ejemplo: private base1, private base2 {...}; La aplicación de clases base múltiples introduce un conjunto de ambigüedades a los programas C++. Una de las más comunes se da cuando dos clases base tienen funciones con el mismo nombre, y sin embargo, una clase derivada de ambas no tiene ninguna función con ese nombre. ¿ Cómo acceden los objetos de la clase derivada a la función correcta de la clase base ? El nombre de la función no es suficiente, ya que el compilador no puede deducir cuál de las dos funciones es la invocada. Si se tiene una clase C que se deriva de dos clases base A y B, ambas con una función mostrar() y se define un objeto de la clase C: C objetoC, la manera de resolver la ambigüedad es utilizando el operador de resolución de ámbito: objetoC.A::mostrar(); objetoC.B::mostrar(); //se refiere a la versión de //mostrar() de la clase A //se refiere a la versión de //mostrar() de la clase B 7.10 Constructores y destructores en herencia múltiple El uso de funciones constructor y destructor en clases derivadas es más complejo que en una clase simple. Al crear clases derivadas con una sola clase base, se observa que el constructor de la clase base se llama siempre antes que el constructor de la clase derivada. Además se dispone de un mecanismo para pasar los valores de los parámetros necesarios al constructor base desde el constructor de la clase derivada. Así, la sentencia siguiente pasa el puntero a carácter x y el valor del entero y a la clase base: derivada (char *x, int y, double z): base(x,y) Este método se expande para efectuar más de una clase base.La sentencia: derivada (char *x,int y, double z): base1(x),base2(y) Llama al constructor base1 y pasa el valor de cadena de caracteres x y al constructor base2 le proporciona un valor entero y. Como con la herencia simple, los constructores de la clase base se llaman antes de que se ejecuten al constructor de la clase derivada. Los constructores se llaman en el orden en que se declaran las clases base. De modo similar, las relaciones entre el destructor de la clase derivada y el destructor de la clase base se mantiene. El destructor derivado se llama antes de que se llame a los destructores de cualquier base. Los destructores de las clases base se llaman en orden inverso al orden en que los objetos base se crearon. 7.11 Herencia repetida La primera regla de la herencia múltiple es que una clase derivada no puede heredar más de una vez de una sola clase, al menos no directamente. Sin embargo, es posible que una clase se pueda derivar dos o más veces por caminos distintos, de modo que se puede reprtir una clase. La propiedad de recibir por herencia una misma clase más de una vez se conoce como herencia repetida. 7.12 Clases base virtuales Una clase base virtual es una clase que es compartida por otras clases base con independencia del número de veces que esta clase se produce en la jerarquía de derivación. Suponer , por ejemplo, que la clase T se deriva de las clases C y D cada una de las cuales se deriva de la clase A. Esto significa que la clase T tiene dos instancias de A en su jerarquía de derivación. C++ permite instancias múltiples de la misma clase base. Sin embargo, si sólo se desea una instancia de la clase A para la clase T, entonces se debe declarar A como una clase base virtual. Las clases base virtuales proporcionan un método de anular el mecanismo de herencia múltiple, permitiendo especificar una clase que es una clase base compartida. Una clase derivada puede tener instancias virtuales y no virtuales de la misma clase base. INTRODUCCIÓN A LA HERENCIA La herencia es uno de los mecanismos de la programación orientada a objetos, por medio de la cual una clase se deriva de otra de manera que extiende su funcionalidad. Una de sus funciones más importantes es la de proveer polimorfismo y late binding. Tipos de herencia Herencia sencilla: Un objeto puede extender las características de otro objeto y de ningún otro, es decir, solo puede tener un padre. Herencia múltiple: Un objeto puede extender las características de uno o más objetos, es decir, puede tener varios padres. En este aspecto hay discrepancias entre los diseñadores de lenguajes. Algunos de ellos han preferido no admitir la herencia múltiple por las posibles coincidencias en nombres de métodos o datos miembros. Por ejemplo C++ admite herencia múltiple, Java y Ada sólo herencia simple. Obtenido de "http://es.wikipedia.org/wiki/Herencia_%28programaci%C3%B3n_orientada _a_objetos%29" Herencia en PHP Hablaremos de esta peculiar característica para hacer copias independientes y personalizadas de clases ya construidas, propia de la programación orientada a objetos. La programación orientada a objetos tiene un mecanismo llamado herencia por el que se pueden definir clases a partir de otras clases. Las clases realizadas a partir de otra clase o mejor dicho, que extienden a otra clase, se llaman clases extendidas o clases derivadas. Las clases extendidas heredan todos los atributos y métodos de la clase base. Además, pueden tener tantos atributos y métodos nuevos como se desee. Para ampliar el ejemplo que venimos desarrollando, la clase Caja, vamos a crear una clase extendida llamada Caja_tematica. Esta clase hereda de caja, pero además tiene un "tema", que es la descripción del tipo de cosas que metemos en la caja. Con esto podemos tener varias cajas, cada una con cosas de un tema concreto. class Caja_tematica extends Caja{ var $tema; } function define_tema($nuevo_tema){ $this->tema = $nuevo_tema; } En esta clase heredamos de Caja, con lo que tenemos a nuestra disposición todos los atributos y métodos de la clase base. Además, se ha definido un nuevo atributo, llamado $tema, y un método, llamado define_tema(), que recibe el tema con el que se desea etiquetar la caja. Podríamos utilizar la clase Caja_tematica de manera similar a como lo hacíamos con la clase Caja original. $micaja_tematica = new Caja_tematica(); $micaja_tematica->define_tema("Cables y contectores"); $micaja_tematica->introduce("Cable de red"); $micaja_tematica->introduce("Conector RJ45"); $micaja_tematica->muestra_contenido(); En este caso, el resultado que se obtiene es parecido al que se obtiene para la clase base. Sin embargo, cuando se muestra el contenido de una caja, lo más interesante sería que se indicara también el tipo de objetos que contiene la caja temática. Para ello, tenemos que redefinir el método muestra_contenido(). Redefinir métodos en clases extendidas Redefinir métodos significa volver a codificarlos, es decir, volver a escribir su código para la clase extendida. En este caso, tenemos que redefinir el método muestra_contenido() para que muestre también el tema de la caja. Para redefinir un método, lo único que debemos hacer es volverlo a escribir dentro de la clase extendida. function muestra_contenido(){ echo "Contenido de la caja de <b>" . $this->tema . "</b>: " . $this->contenido; } En este ejemplo hemos codificado de nuevo el método entero para mostrar los datos completos. En algunas ocasiones es muy útil apoyarse en la definición de un método de la clase base para realizar las acciones de la clase extendida. Por ejemplo, para este ejemplo, tenemos que definir un constructor para la clase Caja_tematica, en el que también se inicialice el tema de la caja. Como ya existe un método constructor en la clase base, no merece la pena reescribir el código de éste, lo mejor es llamar al constructor que había definido en la clase Caja original, con lo que se inicializarán todos los datos de la clase base, y luego realizar la inicialización para los atributos de la propia clase extendida. Para llamar a un método de la clase padre dentro del código de un método que estamos redefiniendo, utilizamos una sintaxis como esta: function Caja_tematica($alto=1,$ancho=1,$largo=1,$color="negro",$tema="Sin clasificación"){ parent::Caja($alto,$ancho,$largo,$color); $this->tema=$tema; } Aquí vemos la redefinición del constructor, de la clase Caja, para la clase Caja_tematica. El constructor hace primero una llamada al constructor de la clase base, a través de una referencia a "parent". Luego inicializa el valor del atributo $tema, que es específico de la Caja_tematica. En la misma línea de trabajo, podemos redefinir el método muestra_contenido() apoyándonos en el que fue declarado en la clase base. El código quedaría como sigue: function muestra_contenido(){ echo "Contenido de la caja de <b>" . $this->tema . "</b>: "; parent::muestra_contenido(); } HERENCIA La herencia en C++ En C++ existen dos tipos de herencia: simple y múltiple. La herencia simple es aquella en la que cada clase derivada hereda de una única clase. En herencia simple, cada clase tiene un solo ascendiente. Cada clase puede tener, sin embargo, muchos descendientes. La herencia múltiple es aquella en la cual una clase derivada tiene más de una clase base. Aunque el concepto de herencia múltiple es muy útil, el diseño de clases suele ser más complejo. http://html.rincondelvago.com/clases-derivadas.html LA HERENCIA A. Introducción La verdadera potencia de la programación orientada a objetos radica en su capacidad para reflejar la abstracción que el cerebro humano realiza automáticamente durante el proceso de aprendizaje y el proceso de análisis de información. Las personas percibimos la realidad como un conjunto de objetos interrelacionados. Dichas interrelaciones, pueden verse como un conjunto de abstracciones y generalizaciones que se han ido asimilando desde la niñez. Así, los defensores de la programación orientada a objetos afirman que esta técnica se adecua mejor al funcionamiento del cerebro humano, al permitir descomponer un problema de cierta magnitud en un conjunto de problemas menores subordinados del primero. La capacidad de descomponer un problema o concepto en un conjunto de objetos relacionados entre sí, y cuyo comportamiento es fácilmente identificable, puede ser muy útil para el desarrollo de programas informáticos. B. Jerarquía La herencia es el mecanismo fundamental de relación entre clases en la orientación a objetos. Relaciona las clases de manera jerárquica; una clase padre o superclase sobre otras clases hijas o subclases. Imagen 4: Ejemplo de otro árbol de herencia Los descendientes de una clase heredan todas las variables y métodos que sus ascendientes hayan especificado como heredables, además de crear los suyos propios. La característica de herencia, nos permite definir nuevas clases derivadas de otra ya existente, que la especializan de alguna manera. Así logramos definir una jerarquía de clases, que se puede mostrar mediante un árbol de herencia. En todo lenguaje orientado a objetos existe una jerarquía, mediante la que las clases se relacionan en términos de herencia. En Java, el punto más alto de la jerarquía es la clase Object de la cual derivan todas las demás clases. C. Herencia múltiple En la orientación a objetos, se consideran dos tipos de herencia, simple y múltiple. En el caso de la primera, una clase sólo puede derivar de una única superclase. Para el segundo tipo, una clase puede descender de varias superclases. En Java sólo se dispone de herencia simple, para una mayor sencillez del lenguaje, si bien se compensa de cierta manera la inexistencia de herencia múltiple con un concepto denominado interface, que estudiaremos más adelante. D. Declaración Para indicar que una clase deriva de otra, heredando sus propiedades (métodos y atributos), se usa el término extends, como en el siguiente ejemplo: public class SubClase extends SuperClase { // Contenido de la clase } Por ejemplo, creamos una clase MiPunto3D, hija de la clase ya mostrada MiPunto: class MiPunto3D extends MiPunto { int z; MiPunto3D( ) { x = 0; // Heredado de MiPunto y = 0; // Heredado de MiPunto z = 0; // Nuevo atributo } } La palabra clave extends se utiliza para decir que deseamos crear una subclase de la clase que es nombrada a continuación, en nuestro caso MiPunto3D es hija de MiPunto. E. Limitaciones en la herencia Todos los campos y métodos de una clase son siempre accesibles para el código de la misma clase. Para controlar el acceso desde otras clases, y para controlar la herencia por las subclase, los miembros (atributos y métodos) de las clases tienen tres modificadores posibles de control de acceso: public: Los miembros declarados public son accesibles en cualquier lugar en que sea accesible la clase, y son heredados por las subclases. private: Los miembros declarados private son accesibles sólo en la propia clase. protected: Los miembros declarados protected son accesibles sólo para sus subclases Por ejemplo: class Padre { // Hereda de Object // Atributos private int numeroFavorito, nacidoHace, dineroDisponible; // Métodos public int getApuesta() { return numeroFavorito; } protected int getEdad() { return nacidoHace; } private int getSaldo() { return dineroDisponible; } } class Hija extends Padre { // Definición } class Visita { // Definición } En este ejemplo, un objeto de la clase Hija, hereda los tres atributos (numeroFavorito, nacidoHace y dineroDisponible) y los tres métodos ( getApuesta(), getEdad() y getSaldo() ) de la clase Padre, y podrá invocarlos. Cuando se llame al método getEdad() de un objeto de la clase Hija, se devolverá el valor de la variable de instancia nacidoHace de ese objeto, y no de uno de la clase Padre. Sin embargo, un objeto de la clase Hija, no podrá invocar al método getSaldo() de un objeto de la clase Padre, con lo que se evita que el Hijo conozca el estado de la cuenta corriente de un Padre. La clase Visita, solo podrá acceder al método getApuesta(), para averiguar el número favorito de un Padre, pero de ninguna manera podrá conocer ni su saldo, ni su edad (sería una indiscreción, ¿no?). http://pisuerga.inf.ubu.es/lsi/Invest/Java/Tuto/II_6.htm Herencia La herencia es un mecanismo que permite la definición de una clase a partir de la definición de otra ya existente. La herencia permite compartir automáticamente métodos y datos entre clases, subclases y objetos. La herencia está fuertemente ligada a la reutilización del código en la OOP. Esto es, el código de cualquiera de las clases puede ser utilizado sin más que crear una clase derivada de ella, o bien una subclase. Hay dos tipos de herencia: Herencia Simple y Herencia Múltiple. La primera indica que se pueden definir nuevas clases solamente a partir de una clase inicial mientras que la segunda indica que se pueden definir nuevas clases a partir de dos o más clases iniciales. Java sólo permite herencia simple. http://www.fib.unam.mx/pp/profesores/carlos/java/java_basico3_4.html http://www.dcp.com.ar/poo/poop2.htm 5.11 Herencia simple. Existen dos tipos de Herencia: Simple: Una clase se deriva de sólo una clase base. Múltiple: Una clase se deriva de más de una clase base. Nuestro ejemplo será de herencia simple. Ante todo una pregunta: ¿qué representa nuestra clase CEmpleado?. Bueno, la definimos con la intensión que represente un empleado cualquiera. Empleados existen muchos, sea cual sea la empresa en la que trabajan, todos tienen actividades específicas, por ejemplo un obrero, un jefe de sector, un gerente, etc. CEmpleado es una clase, (por como la diseñamos, aunque sea muy simple), que no hace diferencias entre los empleados, es, digamos, una clase general. Ahora bien, por lo general, un gerente, (que como dijimos, es un empleado), tiene atributos particulares que lo hace diferente a otro empleado, por ejemplo tiene a su cargo un departamento, tiene una secretaria a su disposición, etc. Por lo tanto tenemos un pequeño problema con nuestra clase CEmpleado para poder representar un gerente, puesto que se limita solamente al Apellido y el Salario, nos faltarían datos miembros para el departamento que tiene a su cargo y la secretaria. Podríamos definir una nueva clase CGerente con los datos miembros Apellido, Salario, Dpto, Secretaria y las funciones miembros ObtenerApellido(), ObtenerSalario(), ObtenerDpto() y ObtenerSecretaria(). Sí y no estaría mal, pero sería redundante, pues podríamos derivar la nueva clase CGerente de CEmpleado, puesto que los datos y funciones miembros nos resultan útiles. Derivemos, entonces, la clase CGerente de CEmpleado: #include <iostream.h> #include <string.h> //Definición de la clase CEmpleado //*************************************** class CEmpleado { protected: char ape[20]; double sueldo; public: CEmpleado() { strcpy(ape, ""); sueldo=0; } CEmpleado(char ap[20], double s) { strcpy(ape, ap); sueldo=s; } char* ObtenerApellido(); double ObtenerSueldo(); }; //funciones miembros de CEmpleado char* CEmpleado::ObtenerApellido () { return ape; } double CEmpleado::ObtenerSueldo () { return sueldo; } //*********************************************** //Definición de la clase CGerente heredada de CEmpleado //******************************************************************** class CGerente:public CEmpleado (1) { char dpto[20]; char secretaria[20]; public: CGerente(char n[20], double s, char d[20], char sec[20]) { strcpy(ape, n); sueldo=s; strcpy(dpto,d); strcpy(secretaria, sec); } char* ObtenerSecretaria(); char* ObtenerDpto(); }; //Funciones miembros de CGerente char* CGerente::ObtenerSecretaria() { return secretaria; } char* CGerente::ObtenerDpto () { return dpto; } Justamente en (1) es donde tiene lugar la derivación de CGerente de CEmpleado: class CGerente:public CEmpleado La definición de CGerente con :public CEmpleado indica que CGerente heredará de CEmpleado, de forma pública, todos los datos y funciones miembros. El operador public en esta sentencia permite definir la accesibilidad de los datos miembros derivados. Una clase hereda de otra todos los datos y funciones miembros que no sean privados. Lo que sigue es una tabla que permite reconocer la accesibilidad en clases derivadas: si el modo de derivación es: private y el miembro en la clase base es: se obtiene un miembro: private inaccesible protected private public private protected public private inaccesible protected protected public protected private inaccesible protected protected public public Como reglas de esta tabla se extrae: 1) Los datos miembros privados no son derivables sea cual sea el modo de derivación. 2) Derivando en modo privado se obtienen miembros privados. 3) Derivando en modo protegido se obtienen miembros protegidos. 4) Derivando en modo público se respetan las características de los miembros de la clase base. En la función main se podría escribir, para probar la funcionalidad de nuestra nueva clase derivada, lo siguiente: void main(void) { CGerente g("Perez", 2500.60, "Sistemas", "Juana"); cout << g.ObtenerApellido()<<endl; cout << g.ObtenerSueldo()<<endl; cout << g.ObtenerDpto()<<endl; cout << g.ObtenerSecretaria()<<endl; } Creamos una instancia de CGerente y probamos los métodos de la clase. Hay que destacar que cuando definimos la clase CGerente no escribimos nada con respecto a los atributos Apellido y Salario y los métodos ObtenerApellido() y ObtenerSalario(), y sin embargo los podemos usar. Esto es la herencia. La posibilidad de poder reutilizar código. En la clase derivada sólo hay que definir los datos y funciones miembros exclusivos de esa clase, (en nuestro ejemplo el nombre del Dpto y la secretaria y las funciones que permiten obtener estos valores). 5.12 Herencia múltiple. La herencia multiple Una de las oportunidades que nos ofrece el lenguaje c++ es la posibilidad de que un objeto tenga la herencia de mas de una clase; esta ventaja fue considerada por los desarrolladores de Java como una pega y la quitaron, e incluso hay desarrolladores de c++ que prefieren evitar este tipo de herencia ya que puede complicar mucho la depuracion de programas Para ilustrar un caso de herencia multiple hemos definido la superclase Habitante; de ella heredan dos clases distintas: Humano (que hablan) y Animal (que matan). Ahora queremos definir un ente que tiene propiedades de esas dos clases: Militar, ya que el militar habla y ademas mata. Como podemos definirlo? con una herencia multiple. Vamos la definicion de la superclase o clase padre Habitante Notas de la logia POO Conviene definir todos los metodos de un clase como const siempre que en el metodo no se modifiquen los atributos. Tu resistencia es inutil. unete a nosotros o muere. Definir metodos como const le facilitara el trabajo al compilador y al programador. Nota el codigo necesita revision y testeo /** * Habitante.hpp * Clase que define el objeto habitante * * Pello Xabier Altadill Izura * */ using namespace std; #include <iostream> class Habitante { private: char *nombre; int edad; public: Habitante(); virtual ~Habitante(); Habitante(const Habitante &); virtual void dormir(); // setter/getter o accessors virtual char *getNombre() const { return this->nombre;} // inline virtual void setNombre(char *nombre) { this->nombre = nombre; } // inline virtual int getEdad() const { return this->edad;} // inline virtual void setEdad(int edad) { this->edad = edad; } // inline }; Y su implementacion /** * Habitante.cpp * Programa que implementa la clase habitante * * Pello Xabier Altadill Izura * Compilacion: g++ -c Habitante.cpp * */ #include "Habitante.hpp" // Constructor Habitante::Habitante() { cout << "-clase habitante- Habitante construido."<< endl; } // Destructor Habitante::~Habitante() { cout << "-clase habitante- Habitante "<< this->getNombre() << " destruido."<< endl; } // constructor copia Habitante::Habitante(const Habitante & original) { nombre = new char; original.getNombre(); } // metodo dormir void Habitante::dormir() { cout << "-clase habitante- zzzzzZZZZzzzzz zzz" << endl; } Humano La clase Humano, que hereda de Habitante /** * Humano.hpp * Clase que define el objeto humano * * Pello Xabier Altadill Izura * */ #include "Habitante.hpp" // hereda atributos y metodos de la superclase Habitante class Humano : public Habitante { private: char *idioma; public: Humano(); virtual ~Humano(); Humano(const Humano &); virtual void hablar(char *bla) const; // setter/getter o accessors virtual char *getIdioma() const { return this->idioma;} // inline virtual void setIdioma(char *idioma) { this->idioma = idioma; } // inline }; Y su implementacion /** * Humano.cpp * Fichero que implementa el objeto humano * * Pello Xabier Altadill Izura * */ #include "Habitante.hpp" // Constructor Humano::Humano() { cout << "-clase Humano- Humano construido."<< endl; } // Destructor Humano::~Humano() { cout << "-clase Humano- Humano "<< this->getNombre() << " destruido."<< endl; } // constructor copia Humano::Humano(const Humano & original) { idioma = new char; idioma = original.getIdioma(); } // metodo hablar void Humano::hablar(char *bla) const { cout << "-clase Humano-" << this->getNombre() << " dice: " << bla << endl; } Animal La clase Animal, que hereda de Habitante /** * Animal.hpp * Clase que define el objeto Animal * * Pello Xabier Altadill Izura * */ #include "Habitante.hpp" // hereda atributos y metodos de la superclase Habitante class Animal : public Habitante { private: int patas; public: Animal(); virtual ~Animal(); Animal(const Animal &); virtual void matar() const; // setter/getter o accessors virtual int getPatas() const { return this->patas;} // inline virtual void setPatas(int patas) { this->patas = patas; } // inline }; Y su implementacion /** * Animal.cpp * Programa que implementa la clase Animal * * Pello Xabier Altadill Izura * Compilacion: g++ -c Animal.cpp * */ #include "Animal.hpp" // Constructor Animal::Animal() { cout << "-clase Animal- Animal construido."<< endl; } // Destructor Animal::~Animal() { cout << "-clase Animal- Animal "<< this->getNombre() << " destruido."<< endl; } // constructor copia Animal::Animal(const Animal & original) {} // metodo matar void Animal::matar() const { cout << "-clase Animal-" << this->getNombre() << " Matar! Matar! Matar! " << endl; } La herencia multiple! Aqui esta la clase Militar, que hereda de Humano y Animal. /** * Militar.hpp * Clase que define el objeto Militar * * Pello Xabier Altadill Izura * */ // Herencia multiple de Humano y Animal class Militar : public Animal { //, public Humano { private: char *rango; public: Militar(); ~Militar(); Militar(const Militar &); // sobrescribe metodos void matar() const; void hablar(char *bla) const; // un metodo poco probable entre cualquier uniformado... void razonar() const; // setter/getter o accessors char *getRango() const { return this->rango;} void setRango(char *rango) { this->rango = rango;} }; Y su implementacion /** * Militar.cpp * Programa que implementa la clase Militar * * Pello Xabier Altadill Izura * Compilacion: g++ -c Habitante.cpp * g++ -c Humano.cpp * g++ -c Animal.cpp * g++ Militar.cpp Habitante.o Humano.o Animal.o -o Militar */ #include "Militar.hpp" // Constructor Militar::Militar() { cout << "-clase Militar- Militar construido."<< endl; } // Destructor Militar::~Militar() { cout << "-clase Militar- Militar "<< this->getNombre() << " destruido."<< endl; } // constructor copia Militar::Militar(const Militar & original) { cout << "-clase Militar- Militar copia creada."<< endl; } // metodo razonar void Militar::razonar() const { cout << "-clase Militar-" << this->getNombre() << " Error: OVERFLOW " << endl; } // metodo hablar void Militar::hablar(char *bla) const { cout << "-clase Militar-" << this->getRango() << " " << this>getNombre() << " dice: "; cout << bla << endl; } // metodo matar void Militar::matar() const { cout << "-clase Militar-" << this->getRango() << " Matar! " << endl; cout << "-clase Militar- Somos... agresores por la paz " << endl; } // Aqui haremos multiples pruebas... int main () { return 0; } http://es.tldp.org/Manuales-LuCAS/doc-tutorialc++/html/c295.html Declaración por heréncia múltiple §1 Sinopsis La tercera forma de crear una nueva clase es por herencia múltiple (también llamada agregación o composición [1] ). Consiste en el ensamblando una nueva clase con los elementos de otras clases-base. C++ permite crear clases derivadas que heredan los miembros de una o más clases antecesoras. Es clásico señalar el ejemplo de un coche, que tiene un motor; cuatro ruedas; cuatro amortiguadores, etc. Elementos estos pertenecientes a la clase de los motores, de las ruedas, los amortiguadores, etc. Como en el caso de la herencia simple, aparte de los miembros heredados de cada clase antecesora, la nueva clase también puede tener miembros privativos ( 4.11.2b) §2 Sintaxis Cuando se declara una clase D derivada de varias clases base: B1, B2, ... se utiliza una lista de las bases directas ( 4.11.2b) separadas por comas. La sintaxis general es: class-key <info> nomb-clase <: lista-base> { <lista-miembros> }; El significado de cada miembro se indicó al tratar de la declaración de una clase ( 4.11.2). En este caso, la declaración de D seria: class-key <info> D : <B1, B2, ...> { <lista-miembros> }; D hereda todos los miembros de las clases antecesoras B1, B2, etc, y solo puede utilizar los miembros que derivan de públicos y protegidos en dichas clases. Resulta así que un objeto de la clase derivada contiene sub-objetos de cada una de las clases antecesoras. §2.1 Restricciones Tenga en cuenta que las clases antecesoras no pueden repetirse, es decir: class B { ... }; class D : B, B, ... { ... }; Ilegal! // Aunque la clase antecesora no puede ser base directa más que una vez, si puede repetirse como base indirecta. Es la situación recogida en el siguiente ejemplo cuyo esquema se muestra en la figura 1: class class class class B { ... }; C1 : public B { ... }; C2 : public B { ... }; D : public C1, C2 { ... }; Aquí la clase D tiene miembros heredados de sus antecesoras D1 y D2, y por consiguiente, dos sub-objetos de la base indirecta B. Fig. 1 El mecanismo sucintamente descrito, constituye lo que se denomina herencia múltiple ordinaria (o simplemente herencia). Como se ha visto, tiene el inconveniente de que si las clases antecesoras contienen elementos comunes, estos se ven duplicados en los objetos de la subclase. Para evitar estos problemas, existe una variante de la misma, la herencia virtual ( 4.11.2c1), en la que cada objeto de la clase derivada no contiene todos los objetos de las clases-base si estos están duplicados. Fig. 2 Las dependencias derivadas de la herencia múltiple suele ser expresada también mediante un grafo denominado DAG ("Direct acyclic graph"), que tiene la ventaja de mostrar claramente las dependencias en casos de composiciones complicadas. La figura 2 muestra el DAG correspondiente al ejemplo anterior. En estos grafos las flechas indican el sentido de la herencia, de forma que A --> B indica que A deriva directamente de B. En nuestro caso se muestra como la clase D contiene dos subobjetos de la superclase B. Nota: La herencia múltiple es uno de los puntos peliagudos del lenguaje C++ (y de otros que también implementan este tipo de herencia). Hasta el extremo que algunos teóricos consideran que esta característica debe evitarse, ya que además de las teóricas, presenta también una gran dificultad técnica para su implementación en los compiladores. Por ejemplo, surge la cuestión: Si dos clases A y B conforman la composición de una subclase D, y ambas tienen propiedades con el mismo nombre, ¿Que debe resultar en la subclase D? Miembros duplicados, o un miembro que sean la agregación de las propiedades de A y B?. Como veremos a continuación, el creador del C++ optó por un diseño que despeja cualquier posible ambigüedad, aunque ciertamente deriva en una serie de reglas y condiciones bastante intrincadas. §3 Ambigüedades La herencia múltiple puede originar situaciones de ambigüedad cuando una subclase contiene versiones duplicadas de sub-objetos de clases antecesoras o cuando clases antecesoras contienen miembros del mismo nombre: class B { public: int b; int b0; }; class C1 : public B { public: int b; int c; }; class C2 : public B { public: int b; int c; }; class D: public C1, C2 { public: D() { c = 10; // L1: Error ? C1::c = 110; // L2: Ok. C2::c = 120; // L3: Ok. b = 12; Error!! // L4: Error C1::b = 11; // L5: Ok. C1::B::b C2::b = 12; // L6: Ok. C2::B::b C1::B::b = 10; // L7: Error B::b = 10; // L8: Error una única base B b0 = 0; // L9: Error C1::b0 = 1; // L10: Ok. C2::b0 = 2; // L11: Ok. ambigüedad C1::c o C2::c ambigüedad C1::b domina sobre C2::b domina sobre de sintaxis! ambigüedad. No existe ambigüedad } }; Los errores originados en el constructor de la clase D son muy ilustrativos sobre los tipos de ambiguedad que puede originar la herencia múltiple (podrían haberse presentado en cualquier otro método D::f() de dicha clase). En principio, a la vista de la figura 1 , podría parecer que las ambiguedades relativas a los miembros de D deberían resolverse mediante los correspondientes especificadores de ámbito: C1::B::m // heredados de C2::B::m // heredados de C1::n // privativos C2::n // privativos miembros m en C1 B miembros m en C2 B miembros n en C1 miembros n en C2 Como puede verse en la sentencia L7 , por desgracia el asunto no es axactamente así (otra de las inconsistencias del lenguaje). El motivo es que el esquema mostrado en la figura es méramente Fig. 3 conceptual, y no tiene que corresponder necesariamente con la estructura de los objetos creados por el compilador. En realidad un objeto suele ser una región continua de memoria. Los objetos de las clases derivadas se organizan concatenando los sub-objetos de las bases directas, y los miembros privativos si los hubiere; pero el orden de los elementos de su interior no está garantizado (depende de la implementación). La figura 3 muestra una posible organización de los miembros en el interior de los objetos del ejemplo. El crador del lenguaje indica al respecto [2] que las relaciones contenidas en un grafo como el de la figura 2 representan información para el programador y para el compilador, pero que esta información no existe en el código final. El punto importante aquí es entender que la organización interna de los objetos obtenidos por herencia múltiple es idéntico al de los obtenidos por herencia simple. El compilador conoce la situación de cada miembro del objeto en base a su posición, y genera el código correspondiente sin indirecciones u otros mecanismos innecesarios (disposición del objeto D en la figura 3). §3.1 Desde el punto de vista de la sintaxis de acceso, cualquier miembro m privativo de D (zona-5) de un objeto d puede ser referenciado como d.m. Cualquier otro miembro del mismo nombre (m) en alguno de los subobjetos queda eclipsado por este. Se dice que este identificador domina a los demás [3]. Nota: Este principio de dominancia funciona también en los subobjetos C1 y C2. Por ejemplo: si un identificador n en el subobjeto C1 está duplicado en la parte privativa de C1 y en la parte heredada de B, C1::n tienen preferencia sobre C1::B::n. Cualquier objeto c privativo de los subobjetos C1 o C2 (zonas 2 y 4) podría ser accedido como d.c. Pero en este caso existe ambigüedad sobre cual de las zonas se utilizará. Para resolverla se utiliza el especificador de ámbito: C1::c o C2::c. Este es justamente el caso de las sentencias L1/L3 del ejemplo: c = 10; // L1: Error ambigüedad C1::c o C2::c C1::c = 110; C2::c = 120; // L2: Ok. // L3: Ok. ? Es también el caso de las sentencias L4/L6. Observe que en este caso no existe ambigüedad respecto a los identificadores b heredados (zonas 1, y 2) porque los de las zonas 2 y 4 tienen preferencia sobre los de las zonas 1 y 2. b = 12; Error!! C1::b = 11; C1::B::b C2::b = 12; C2::B::b // L4: Error ambigüedad // L5: Ok. C1::b domina sobre // L6: Ok. C2::b domina sobre Es interesante señalar que estos últimos, los identificadores b de las zonas 1 y 2 (heredados de B) no son accesibles porque siempre quedan ocultos por los miembros dominantes, y la gramática C++ no ofrece ninguna forma que permita hacerlo en la disposición actual del ejemplo. Son los intentos fallidos señalados en L7 y L8: C1::B::b = 10; B::b = 10; una unica base B // L7: Error de sintaxis! // L8: Error ambigüedad. No existe El error de L8 se refiere a que existen dos posibles candidatos (zonas 1 y 2). Al tratar de la herencia virtual ( 4.11.2c1) veremos un método de resolver (parcialmente) este problema. Cuando no existe dominancia, los identificadores b0 de las zonas 1 y 2 si son visibles, aunque la designación directa no es posible porque existe ambigüedad sobre la zona 12 a amplear. Es el caso de las sentencias L9/L11: b0 = 0; C1::b0 = 1; C2::b0 = 2; // L9: Error ambigüedad // L10: Ok. // L11: Ok. §4 Modificadores de acceso Los modificadores de acceso en la lista-base pueden ser cualquiera de los señalados al referirnos a la herencia simple (public, protected y private 4.11.2b-1), y pueden ser distintos para cada uno de los ancestros. Ejemplo: class D : public B1, private B2, ... { <lista-miembros> }; struct T : private D, E { <lista-miembros> }; // Por defecto E equivale a 'public E' Inicio. Para empezar, es necesario definir dos términos normalmente usados al tratar la herencia. Cuando una clase hereda otra, la clase que se hereda se llama clase base. La clase que hereda se llama clase derivada. La clase base define todas las cualidades que serán comunes a cualquier clase derivada. Otro punto importante es el acceso a la clase base. El acceso a la clase base pude tomar 3 valores, public, private y protected. Si el acceso es public, todos los atributos de la clase base son públicos para la derivada. Si el acceso es private, los datos son privados para la clase base la derivada no tiene acceso. Si el acceso es protected, datos privados para la base y derivada tiene acceso, el resto sin acceso. EJEMPLO: para comprobar los distintos tipos de acceso. #include <iostream.h> #include <stdio.h> #include <conio.h> class miclase{ int a; protected: int b; public: int c; miclase(int n,int m){a=n;b=m;} int obten_a(){return a;} int obten_b(){return b;} }; void main() { miclase objeto(10,20); clrscr(); objeto.c=30; // objeto.b=30; error,sin acceso. // objeto.a=30; error,sin acceso. cout<<objeto.obten_a() <<"\n"; cout<<objeto.obten_b() <<"\n"; cout<<objeto.c; getch(); } FORMATO DE LA CLASE DERIVADA: class nombre_derivada:acceso nombre_base{ cuerpo; }; EJEMPLO: Herencia pública. #include <iostream.h> #include <stdio.h> #include <conio.h> class base{ int x; public: void obten_x(int a){x=a;} void muestra_x(){cout<< x;} }; class derivada:public base{ int y; public: void obten_y(int b){y=b;} void muestra_y(){cout<<y;} }; void main() { derivada obj; clrscr(); obj.obten_x(10); obj.obten_y(20); obj.muestra_x(); cout<<"\n"; obj.muestra_y(); getch(); } EJEMPLO: Herencia con acceso privado. #include <iostream.h> #include <stdio.h> #include <conio.h> class base{ int x; public: void obten_x(int a){x=a;} void muestra_x(){cout<<x <<"\n";} }; class derivada:private base{ int y; public: void obten_xy(int a,int b){obten_x(a);y=b;} void muestra_xy(){muestra_x();cout<<y<<"\n";} }; void main() { clrscr(); derivada ob; ob.obten_xy(10,20); ob.muestra_xy(); // ob.obten_x(10); error,sin acceso. // ob.muestra_x(); error,sin acceso. getch(); } HERENCIA MULTIPLE: Existen dos métodos en los que una clase derivada puede heredar más de una clase base. El primero, en el que una clase derivada puede ser usada como la clase base de otra clase derivada, creándose una jerarquía de clases. El segundo, es que una clase derivada puede heredar directamente más de una clase base. En esta situación se combinan dos o más clases base para facilitar la creación de la clase derivada. SINTAXIS: Para construir la derivada mediante varias clases base. class derivada:acceso nomb_base1,nomb_base2,nomb_baseN{ cuerpo; }; SINTAXIS: Para crear herencia múltiple de modo jerárquico. class derivada1:acceso base{ cuerpo; }; class derivada2:acceso derivada1{ cuerpo; }; class derivadaN:acceso derivada2{ cuerpo; }; EJEMPLO: Herencia de tipo jerárquica. #include <iostream.h> #include <stdio.h> #include <conio.h> class base_a{ int a; public: base_a(int x){a=x;} int ver_a(){return a;} }; class deriva_b:public base_a{ int b; public: deriva_b(int x, int y):base_a(x){b=y;} int ver_b(){return b;} }; class deriva_c:public deriva_b{ int c; public: deriva_c(int x,int y,int z):deriva_b(x,y){c=z;} void ver_todo() { cout<<ver_a()<<" "<<ver_b()<<" "<<c; } }; void main() { clrscr(); deriva_c ob(1,2,3); ob.ver_todo(); cout<<"\n"; cout<<ob.ver_a()<<" "<<ob.ver_b(); getch(); } El caso de los constructores es un poco especial. Se ejecutan en orden descendente, es decir primero se realiza el constructor de la clase base y luego el de las derivadas. En las destructoras ocurre en orden inverso, primero el de las derivadas y luego el de la base. EJEMPLO: Múltiple heredando varias clases base. #include <iostream.h> #include <stdio.h> #include <conio.h> class B1{ int a; public: B1(int x){a=x;} int obten_a(){return a;} }; class B2{ int b; public: B2(int x){b=x;} int obten_b(){return b;} }; class C1:public B1,public B2{ int c; public: C1(int x,int y,int z):B1(z),B2(y) { c=x; } void muestra() { cout<<obten_a()<<" "<<obten_b()<<" "; cout<<c<<"\n"; } }; void main() { clrscr(); C1 objeto(1,2,3); objeto.muestra(); getch(); } http://www.mailxmail.com/curso/informatica/cplusplus2/capitulo13.htm LA HERENCIA A. Introducción La verdadera potencia de la programación orientada a objetos radica en su capacidad para reflejar la abstracción que el cerebro humano realiza automáticamente durante el proceso de aprendizaje y el proceso de análisis de información. Las personas percibimos la realidad como un conjunto de objetos interrelacionados. Dichas interrelaciones, pueden verse como un conjunto de abstracciones y generalizaciones que se han ido asimilando desde la niñez. Así, los defensores de la programación orientada a objetos afirman que esta técnica se adecua mejor al funcionamiento del cerebro humano, al permitir descomponer un problema de cierta magnitud en un conjunto de problemas menores subordinados del primero. La capacidad de descomponer un problema o concepto en un conjunto de objetos relacionados entre sí, y cuyo comportamiento es fácilmente identificable, puede ser muy útil para el desarrollo de programas informáticos. B. Jerarquía La herencia es el mecanismo fundamental de relación entre clases en la orientación a objetos. Relaciona las clases de manera jerárquica; una clase padre o superclase sobre otras clases hijas o subclases. Imagen 4: Ejemplo de otro árbol de herencia Los descendientes de una clase heredan todas las variables y métodos que sus ascendientes hayan especificado como heredables, además de crear los suyos propios. La característica de herencia, nos permite definir nuevas clases derivadas de otra ya existente, que la especializan de alguna manera. Así logramos definir una jerarquía de clases, que se puede mostrar mediante un árbol de herencia. En todo lenguaje orientado a objetos existe una jerarquía, mediante la que las clases se relacionan en términos de herencia. En Java, el punto más alto de la jerarquía es la clase Object de la cual derivan todas las demás clases. C. Herencia múltiple En la orientación a objetos, se consideran dos tipos de herencia, simple y múltiple. En el caso de la primera, una clase sólo puede derivar de una única superclase. Para el segundo tipo, una clase puede descender de varias superclases. En Java sólo se dispone de herencia simple, para una mayor sencillez del lenguaje, si bien se compensa de cierta manera la inexistencia de herencia múltiple con un concepto denominado interface, que estudiaremos más adelante. D. Declaración Para indicar que una clase deriva de otra, heredando sus propiedades (métodos y atributos), se usa el término extends, como en el siguiente ejemplo: public class SubClase extends SuperClase { // Contenido de la clase } Por ejemplo, creamos una clase MiPunto3D, hija de la clase ya mostrada MiPunto: class MiPunto3D extends MiPunto { int z; MiPunto3D( ) { x = 0; // Heredado de MiPunto y = 0; // Heredado de MiPunto z = 0; // Nuevo atributo } } La palabra clave extends se utiliza para decir que deseamos crear una subclase de la clase que es nombrada a continuación, en nuestro caso MiPunto3D es hija de MiPunto. E. Limitaciones en la herencia Todos los campos y métodos de una clase son siempre accesibles para el código de la misma clase. Para controlar el acceso desde otras clases, y para controlar la herencia por las subclase, los miembros (atributos y métodos) de las clases tienen tres modificadores posibles de control de acceso: public: Los miembros declarados public son accesibles en cualquier lugar en que sea accesible la clase, y son heredados por las subclases. private: Los miembros declarados private son accesibles sólo en la propia clase. protected: Los miembros declarados protected son accesibles sólo para sus subclases Por ejemplo: class Padre { // Hereda de Object // Atributos private int numeroFavorito, nacidoHace, dineroDisponible; // Métodos public int getApuesta() { return numeroFavorito; } protected int getEdad() { return nacidoHace; } private int getSaldo() { return dineroDisponible; } } class Hija extends Padre { // Definición } class Visita { // Definición } En este ejemplo, un objeto de la clase Hija, hereda los tres atributos (numeroFavorito, nacidoHace y dineroDisponible) y los tres métodos ( getApuesta(), getEdad() y getSaldo() ) de la clase Padre, y podrá invocarlos. Cuando se llame al método getEdad() de un objeto de la clase Hija, se devolverá el valor de la variable de instancia nacidoHace de ese objeto, y no de uno de la clase Padre. Sin embargo, un objeto de la clase Hija, no podrá invocar al método getSaldo() de un objeto de la clase Padre, con lo que se evita que el Hijo conozca el estado de la cuenta corriente de un Padre. La clase Visita, solo podrá acceder al método getApuesta(), para averiguar el número favorito de un Padre, pero de ninguna manera podrá conocer ni su saldo, ni su edad (sería una indiscreción, ¿no?). F. La clase Object La clase Object es la superclase de todas las clases da Java. Todas las clases derivan, directa o indirectamente de ella. Si al definir una nueva clase, no aparece la cláusula extends, Java considera que dicha clase desciende directamente de Object. La clase Object aporta una serie de funciones básicas comunes a todas las clases: public boolean equals( Object obj ): Se utiliza para comparar, en valor, dos objetos. Devuelve true si el objeto que recibe por parámetro es igual, en valor, que el objeto desde el que se llama al método. Si se desean comparar dos referencias a objeto se pueden utilizar los operadores de comparación == y !=. public int hashCode(): Devuelve un código hash para ese objeto, para poder almacenarlo en una Hashtable. protected Object clone() throws CloneNotSupportedException: Devuelve una copia de ese objeto. public final Class getClass(): Devuelve el objeto concreto, de tipo Class, que representa la clase de ese objeto. protected void finalize() throws Trowable: Realiza acciones durante la recogida de basura. http://pisuerga.inf.ubu.es/lsi/Invest/Java/Tuto/II_6.htm 5.13 Clase base y clase derivada. HERENCIA:. Una de las características más importantes de la POO, es la capacidad de derivar clases a partir de las clases existentes, (o sea obtener una nueva clase a partir de otra). Este procedimiento se denomina Herencia, puesto que la nueva clase "hereda" los miembros, (datos y funciones) de sus clases ascendientes y puede anular alguna de las funciones heredadas. La herencia permite reutilizar el código en clases descendientes. Cuando una clase se hereda de otra clase, la clase original se llama clase base y la nueva clase se llama clase derivada. Quizás lo más difícil al escribir programas que utilicen clases, es el diseño de las mismas, lo que involucra una abstracción del objeto que van a representar, pero más difícil aún es diseñar una clase que luego sirva como "base" para nuevas clases derivadas. Aquí además de la abstracción hay que seguir ciertas reglas de accesibilidad, que más adelante de describen. Veamos un ejemplo: Uno podría definir una clase CEmpleado, (a partir de aquí le agregaremos una C al comienzo del nombre de la clase para guardar una relación estrecha con la convención utilizada por Microsoft en su MFC). Esta clase, básica, solamente tendrá dos datos miembros: el Apellido y el Salario, dos constructores comunes y dos métodos, (o funciones miembros), que nos permitirán obtener el Apellido y el Salario del empleado. Así podría ser la clase CEmpleado: class CEmpleado { protected: char ape[20]; double sueldo; public: CEmpleado() { strcpy(ape, ""); sueldo=0; } CEmpleado(char ap[20], double s) { strcpy(ape, ap); sueldo=s; } char* ObtenerApellido(); double ObtenerSueldo(); }; //funciones miembros de CEmpleado char* CEmpleado::ObtenerApellido () { return ape; } double CEmpleado::ObtenerSueldo () { return sueldo; } Un programa que cree una instancia de esta clase, deberá usar alguno de los dos constructores, se puede aprovechar el constructor que recibe como parámetros los valores iniciales para el Apellido y el Salario, ya que el otro inicializa con cadena vacía y 0, no es muy útil. Luego si uno quiere saber cual es el apellido del empleado deberá usar la función ObtenerApellido() ya que los datos miembros son protegidos y "no se ven" en el programa, (esto es "encapsulamiento"). Ofrecemos funciones para acceder a los datos miembros, a modo de protección de la información. Las funciones ObtenerApellido() y ObtenerSalario() están definidas fuera de la clase, aunque podrían haber sido InLine ya que son muy cortitas, sólo retornan el dato miembro solicitado. Así que, como ejemplo se podría escribir, dentro de la función main(), lo siguiente: CEmpleado e("Perez", 1260.35); cout << "Apellido: " << e.ObtenerApellido() << endl; cout <<"Salario: " << e.ObtenerSalario() << endl; Hasta aquí una clase sencilla y hasta muy poco útil diría, pero nos va a servir para ejemplificar un caso sencillo de herencia. 5.13.1 Definición. Introducción La herencia es una propiedad esencial de la Programación Orientada a Objetos que consiste en la creación de nuevas clases a partir de otras ya existentes. Este término ha sido prestado de la Biología donde afirmamos que un niño tiene la cara de su padre, que ha heredado ciertas facetas físicas o del comportamiento de sus progenitores. La herencia es la característica fundamental que distingue un lenguaje orientado a objetos, como el C++ o Java, de otro convencional como C, BASIC, etc. Java permite heredar a las clases características y conductas de una o varias clases denominadas base. Las clases que heredan de clases base se denominan derivadas, estas a su vez pueden ser clases bases para otras clases derivadas. Se establece así una clasificación jerárquica, similar a la existente en Biología con los animales y las plantas. La herencia ofrece una ventaja importante, permite la reutilización del código. Una vez que una clase ha sido depurada y probada, el código fuente de dicha clase no necesita modificarse. Su funcionalidad se puede cambiar derivando una nueva clase que herede la funcionalidad de la clase base y le añada otros comportamientos. Reutilizando el código existente, el programador ahorra tiempo y dinero, ya que solamente tiene que verificar la nueva conducta que proporciona la clase derivada. La programación en los entornos gráficos, en particular Windows, con el lenguaje C++, es un ejemplo ilustrativo. Los compiladores como los de Borland y Microsoft proporcionan librerías cuyas clases describen el aspecto y la conducta de las ventanas, controles, menús, etc. Una de estas clases denominada TWindow describe el aspecto y la conducta de una ventana, tiene una función miembro denominada Paint, que no dibuja nada en el área de trabajo de la misma. Definiendo una clase derivada de TWindow, podemos redefinir en ella la función Paint para que dibuje una figura. Aprovechamos de este modo la ingente cantidad y complejidad del código necesario para crear una ventana en un entorno gráfico. Solamente, tendremos que añadir en la clase derivada el código necesario para dibujar un rectángulo, una elipse, etc. En el lenguaje Java, todas las clases derivan implícitamente de la clase base Object, por lo que heredan las funciones miembro definidas en dicha clase. Las clases derivadas pueden redefinir algunas de estas funciones miembro como toString y definir otras nuevas. Para crear un applet, solamente tenemos que definir una clase derivada de la clase base Applet, redefinir ciertas funciones como init o paint, o definir otras como las respuestas a las acciones sobre los controles. Los programadores crean clases base: 1. Cuando se dan cuenta que diversos tipos tienen algo en común, por ejemplo en el juego del ajedrez peones, alfiles, rey, reina, caballos y torres, son piezas del juego. Creamos, por tanto, una clase base y derivamos cada pieza individual a partir de dicha clase base. 2. Cuando se precisa ampliar la funcionalidad de un programa sin tener que modificar el código existente. Las clases pueden introducirse de muchas formas, comenzando por la que dice que representan un intento de abstraer el mundo real. Pero desde el punto de vista del programador clásico, lo mejor es considerarlas como "entes" que superceden las estructuras C en el sentido de que tanto los datos como los instrumentos para su manipulación (funciones) se encuentran encapsulados en ellos. La idea es empaquetar juntos los datos y la funcionalidad, de ahí que tengan dos tipos de componentes (aquí se prefiere llamarlos miembros). Por un lado las propiedades, también llamadas variables o campos (fields), y de otro los métodos, también llamados procedimientos o funciones [1]; más formalmente: variables de clase y métodos de clase. La terminología utilizada en la Programación Orientada a Objetos POO (OOP en inglés), no es demasiado consistente, y a veces induce a cierto error a los programadores que se acercan por primera vez con una cultura de programación procedural. De hecho, estas cuestiones semánticas suponen una dificultad adicional en el proceso de entender los conceptos subyacentes en la POO, sus ventajas y su potencial como herramienta. Las clases C++ ofrecen la posibilidad de extender los tipos predefinidos en el lenguaje (básico y derivado). Cada clase representa un nuevo tipo; un nuevo conjunto de objetos caracterizado por ciertos valores (propiedades) y las operaciones (métodos) disponibles para crearlos, manipularlos y destruirlos. Más tarde se podrán declarar objetos pertenecientes a dicho tipo (clase) del mismo modo que se hace para las variables simples tradicionales. Considerando que son vehículos para manejo y manipulación de información, las clases han sido comparadas en ocasiones con los sistemas tradicionales de manejo de datos DBMS ("DataBase Management System"); aunque de un tipo muy especial, ya que sus características les permiten operaciones que están absolutamente prohibidas a los sistemas DBMS clásicos. La mejor manera de entender las clases es considerar que se trata simplemente de tipos de datos cuya única peculiaridad es que pueden ser definidos por el usuario. Generalmente se trata de tipos complejos, constituidos a su vez por elementos de cualquier tipo (incluso otras clases). La definición que puede hacerse de ellos no se reduce a diseñar su "contenido"; también pueden definirse su álgebra y su interfaz. Es decir: como se opera con estos tipos y como los ve el usuario (que puede hacer con ellos). El propio inventor del lenguaje señala que la principal razón para definir un nuevo tipo es separar los detalles poco relevantes de la implementación de las propiedades que son verdaderamente esenciales para utilizarlos correctament CLASES DERIVADAS. En C++, la herencia simple se realiza tomando una clase existente y derivando nuevas clases de ella. La clase derivada hereda las estructuras de datos y funciones de la clase original. Además, se pueden añadir nuevos miembros a las clases derivadas y los miembros heredados pueden ser modificados. Una clase utilizada para derivar nuevas clases se denomina clase base, clase padre, superclase o ascendiente. Una clase creada de otra clase se denomina clase derivada o subclase. Se pueden construir jerarquías de clases, en las que cada clase sirve como padre o raíz de una nueva clase. Conceptos fundamentales de derivación C++ utiliza un sistema de herencia jerárquica. Es decir, se hereda una clase de otra, creando nuevas clases a partir de las clases ya existentes. Sólo se pueden heredar clases, no funciones ordinarias n variables, en C++. Una clase derivada hereda todos los miembros dato excepto, miembros dato estático, de cada una de sus clases base. Una clase derivada hereda la función miembro de su clase base. Esto significa que se hereda la capacidad para llamar a funciones miembro de la clase base en los objetos de la clase derivada. Los siguientes elementos de la clase no se heredan: - Constructores - Destructores - Funciones amigas - Funciones estáticas de la clase - Datos estáticos de la clase - Operador de asignación sobrecargado Las clases base diseñadas con el objetivo principal de ser heredadas por otras se denominan clases abstractas. Normalmente, no se crean instancias a partir de clases abstractas, aunque sea posible. 5.13.2 Declaración. La clase base ventana: Ventana.java, VentanaTitulo.java, VentanaApp.java Vamos a poner un ejemplo del segundo tipo, que simule la utilización de liberías de clases para crear un interfaz gráfico de usuario como Windows 3.1 o Windows 95. Supongamos que tenemos una clase que describe la conducta de una ventana muy simple, aquella que no dispone de título en la parte superior, por tanto no puede desplazarse, pero si cambiar de tamaño actuando con el ratón en los bordes derecho e inferior. La clase Ventana tendrá los siguientes miembros dato: la posición x e y de la ventana, de su esquina superior izquierda y las dimensiones de la ventana: ancho y alto. public class Ventana { protected int x; protected int y; protected int ancho; protected int alto; public Ventana(int x, int y, int ancho, int alto) { this.x=x; this.y=y; this.ancho=ancho; this.alto=alto; } //... } Las funciones miembros, además del constructor serán las siguientes: la función mostrar que simula una ventana en un entorno gráfico, aquí solamente nos muestra la posición y las dimensiones de la ventana. public void mostrar(){ System.out.println("posición : x="+x+", y="+y); System.out.println("dimensiones : w="+ancho+", h="+alto); } La función cambiarDimensiones que simula el cambio en la anchura y altura de la ventana. public void cambiarDimensiones(int dw, int dh){ ancho+=dw; alto+=dh; } El código completo de la clase base Ventana, es el siguiente package ventana; public class Ventana { protected int x; protected int y; protected int ancho; protected int alto; public Ventana(int x, int y, int ancho, int alto) { this.x=x; this.y=y; this.ancho=ancho; this.alto=alto; } public void mostrar(){ System.out.println("posición : x="+x+", y="+y); System.out.println("dimensiones : w="+ancho+", h="+alto); } public void cambiarDimensiones(int dw, int dh){ ancho+=dw; alto+=dh; } } Objetos de la clase base Como vemos en el código, el constructor de la clase base inicializa los cuatro miembros dato. Llamamos al constructor creando un objeto de la clase Ventana Ventana ventana=new Ventana(0, 0, 20, 30); Desde el objeto ventana podemos llamar a las funciones miembro públicas ventana.mostrar(); ventana.cambiarDimensiones(10, 10); ventana.mostrar(); La clase derivada Incrementamos la funcionalidad de la clase Ventana definiendo una clase derivada denominada VentanaTitulo. Los objetos de dicha clase tendrán todas las características de los objetos de la clase base, pero además tendrán un título, y se podran desplazar (se simula el desplazamiento de una ventana con el ratón). La clase derivada heredará los miembros dato de la clase base y las funciones miembro, y tendrá un miembro dato más, el título de la ventana. public class VentanaTitulo extends Ventana{ protected String titulo; public VentanaTitulo(int x, int y, int w, int h, String nombre) { super(x, y, w, h); titulo=nombre; } extends es la palabra reservada que indica que la clase VentanaTitulo deriva, o es una subclase, de la clase Ventana. La primera sentencia del constructor de la clase derivada es una llamada al constructor de la clase base mediante la palabra reservada super. La llamada super(x, y, w, h); inicializa los cuatro miembros dato de la clase base Ventana: x, y, ancho, alto. A continuación, se inicializa los miembros dato de la clase derivada, y se realizan las tareas de inicialización que sean necesarias. Si no se llama explícitamente al constructor de la clase base Java lo realiza por nosotros, llamando al constructor por defecto si existe. La función miembro denominada desplazar cambia la posición de la ventana, añadiéndoles el desplazamiento. public void desplazar(int dx, int dy){ x+=dx; y+=dy; } Redefine la función miembro mostrar para mostrar una ventana con un título. public void mostrar(){ super.mostrar(); System.out.println("titulo } : "+titulo); En la clase derivada se define una función que tiene el mismo nombre y los mismos parámetros que la de la clase base. Se dice que redefinimos la función mostrar en la clase derivada. La función miembro mostrar de la clase derivada VentanaTitulo hace una llamada a la función mostrar de la clase base Ventana, mediante super.mostrar(); De este modo aprovechamos el código ya escrito, y le añadimos el código que describe la nueva funcionalidad de la ventana por ejemplo, que muestre el título. Si nos olvidamos de poner la palabra reservada super llamando a la función mostrar, tendríamos una función recursiva. La función mostrar llamaría a mostrar indefinidamente. public void mostrar(){ //¡ojo!, función recursiva System.out.println("titulo : "+titulo); mostrar(); } La definición de la clase derivada VentanaTitulo, será la siguiente. package ventana; public class VentanaTitulo extends Ventana{ protected String titulo; public VentanaTitulo(int x, int y, int w, int h, String nombre) { super(x, y, w, h); titulo=nombre; } public void mostrar(){ super.mostrar(); System.out.println("titulo : "+titulo); } public void desplazar(int dx, int dy){ x+=dx; y+=dy; } } Objetos de la clase derivada Creamos un objeto ventana de la clase derivada VentanaTitulo VentanaTitulo ventana=new VentanaTitulo(0, 0, 20, 30, "Principal"); Mostramos la ventana con su título, llamando a la función mostrar, redefinida en la clase derivada ventana.mostrar(); Desde el objeto ventana de la clase derivada llamamos a las funciones miembro definidas en dicha clase ventana.desplazar(4, 3); Desde el objeto ventana de la clase derivada podemos llamar a las funciones miembro definidas en la clase base. ventana.cambiarDimensiones(10, -5); Para mostrar la nueva ventana desplazada y cambiada de tamaño escribimos ventana.mostrar(); Modificadores de acceso Ya hemos visto el significado de los modificadores de acceso public y private, así como el control de acceso por defecto a nivel de paquete, cuando no se especifica nada. En la herencia, surge un nuevo control de acceso denominado protected. Hemos puesto protected delante de los miebros dato x e y de la clase base Ventana public class Ventana { protected int x; protected int y; //... } En la clase derivada la función miembro desplazar accede a dichos miembros dato public class VentanaTitulo extends Ventana{ //... public void desplazar(int dx, int dy){ x+=dx; y+=dy; } } Si cambiamos el modificador de acceso de los miembros x e y de la clase base Ventana de protected a private, veremos que el compilador se queja diciendo que los miembro x e y no son accesibles. Los miembros ancho y alto se pueden poner con acceso private sin embargo, es mejor dejarlos como protected ya que podrían ser utilizados por alguna función miembro de otra clase derivada de VentanaTitulo. Dentro de una jerarquía pondremos un miembro con acceso private, si estamos seguros de que dicho miembro solamente va a ser usado por dicha clase. Como vemos hay cuatro modificadores de acceso a los miembros dato y a los métodos: private, protected, public y default (por defecto, o en ausencia de cualquier modificador). La herencia complica aún más el problema de acceso, ya que las clases dentro del mismo paquete tienen diferentes accesos que las clases de distinto paquete Los siguientes cuadros tratan de aclarar este problema Clases dentro del mismo paquete Modificador de Heredado Accesible acceso Por defecto (sin Si Si modificador) private No No protected Si Si public Si Si Clases en distintos paquetes Modificador de Heredado Accesible acceso Por defecto (sin No No modificador) private No No protected Si No public Si Si Desde el punto de vista práctico, cabe reseñar que no se heredan los miembros privados, ni aquellos miembros (dato o función) cuyo nombre sea el mismo en la clase base y en la clase derivada. La clase base Object La clase Object es la clase raíz de la cual derivan todas las clases. Esta derivación es implícita. La clase Object define una serie de funciones miembro que heredan todas las clases. Las más importantes son las siguientes public class Object { public boolean equals(Object obj) { return (this == obj); } protected native Object clone() throws CloneNotSupportedException; public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } protected void finalize() throws Throwable { } //otras funciones miembro... } Igualdad de dos objetos: Hemos visto que el método equals de la clase String cuando compara un string y cualquier otro objeto. El método equals de la clase Object compara dos objetos uno que llama a la función y otro es el argumento de dicha función. Representación en forma de texto de un objeto El método toString imprime por defecto el nombre de la clase a la que pertenece el objeto y su código (hash). Esta función miembro se redefine en la clase derivada para mostrar la información que nos interese acerca del objeto. La clase Fraccion redefine toString para mostrar el numerador y el denominador separados por la barra de dividir. En la misma página, hemos mejorado la clase Lista para mostrar los datos que se guardan en los objetos de dicha clase, redefiniendo toString. La función toString se llama automáticamente siempre que pongamos un objeto como argumento de la función System.out.println o concatenado con otro string. Duplicación de objetos El método clone crea un objeto duplicado (clónico) de otro objeto. Más adelante estudiremos en detalle la redefinición de esta función miembro y pondremos ejemplos que nos muestren su utilidad. Finalización El método finalize se llama cuando va a ser liberada la memoria que ocupa el objeto por el recolector de basura (garbage collector). Normalmente, no es necesario redefinir este método en las clases, solamente en contados casos especiales. La forma en la que se redefine este método es el siguiente. class CualquierClase{ //.. protected void finalize() trows Throwable{ super.finalize(); //código que libera recursos externos } } La primera sentencia que contenga la redefinición de finalize ha de ser una llamada a la función del mismo nombre de la clase base, y a continuación le añadimos cierta funcionalidad, habitualmente, la liberación de recursos, cerrar un archivo, etc. http://www.sc.ehu.es/sbweb/fisica/cursoJava/fundamentos/herencia/herencia.htm Jerarquía, clases base y clases derivadas: Una de las principales propiedades de las clases es la herencia. Esta propiedad nos permite crear nuevas clases a partir de clases existentes, conservando las propiedades de la clase original y añadiendo otras nuevas. La nueva clase obtenida se conoce como clase derivada, y las clases a partir de las cuales se deriva, clases base. Además, cada clase derivada puede usarse como clase base para obtener una nueva clase derivada. Y cada clase derivada puede serlo de una o más clases base. En este último caso hablaremos de derivación múltiple. Esto nos permite crear una jerarquía de clases tan compleja como sea necesario. Bien, pero ¿que ventajas tiene derivar clases?. En realidad, ese es el principio de la programación orientada a objetos. Esta propiedad nos permite encapsular diferentes partes de cualquier objeto real o imaginario, y vincularlo con objetos más elaborados del mismo tipo básico, que heredarán todas sus características. Lo veremos mejor con un ejemplo. Un ejemplo muy socorrido es de las personas. Supongamos que nuestra clase base para clasificar a las personas en función de su profesión sea "Persona". Presta especial atención a la palabra "clasificar", es el punto de partida para buscar la solución de cualquier problema que se pretenda resolver usando POO. Lo primero que debemos hacer es buscar categorías, propiedades comunes y distintas que nos permitan clasificar los objetos, y crear lo que después serán las clases de nuestro programa. Es muy importante dedicar el tiempo y atención necesarios a esta tarea, de ello dependerá la flexibilidad, reutilización y eficacia de nuestro programa. Ten en cuenta que las jerarquías de clases se usan especialmente en la resolución de problemas complejos, es difícil que tengas que recurrir a ellas para resolver problemas sencillos. Siguiendo con el ejemplo, partiremos de la clase "Persona". Independientemente de la profesión, todas las personas tienen propiedades comunes, nombre, fecha de nacimiento, género, estado civil, etc. La siguiente clasificación debe ser menos general, supongamos que dividimos a todas las personas en dos grandes clases: empleados y estudiantes. (Dejaremos de lado, de momento, a los estudiantes que además trabajan). Lo importante es decidir qué propiedades que no hemos incluido en la clase "Persona" son exclusivas de los empleados y de los estudiantes. Por ejemplo, los ingresos por nómina son exclusivos de los empleados, la nota media del curso, es exclusiva de los estudiantes. Una vez hecho eso crearemos dos clases derivadas de Persona: "Empleado" y "Estudiante". Haremos una nueva clasificación, ahora de los empleados. Podemos clasificar a los empleados en ejecutivos y comerciales (y muchas más clases, pero para el ejemplo nos limitaremos a esos dos). De nuevo estableceremos propiedades exclusivas de cada clase y crearemos dos nuevas clases derivadas de "Empleado": "Ejecutivo" y "Comercial". Ahora veremos las ventajas de disponer de una jerarquía completa de clases. Cada vez que creemos un objeto de cualquier tipo derivado, por ejemplo de tipo Comercial, estaremos creando en un sólo objeto un Comercial, un Empleado y una Persona. Nuestro programa puede tratar a ese objeto como si fuera cualquiera de esos tres tipos. Es decir, nuestro comercial tendrá, además de sus propiedades como comercial, su nómina como empleado, y su nombre, edad y género como persona. Siempre podremos crear nuevas clases para resolver nuevas situaciones. Consideremos el caso de que en nuestra clasificación queremos incluir una nueva clase "Becario", que no es un empleado, ni tampoco un estudiante; la derivaríamos de Persona. También podemos considerar que un becario es ambas cosas, sería un ejemplo de derivación múltiple, podríamos hacer que la clase derivada Becario, lo fuera de Empleado y Estudiante. Podemos aplicar procedimientos genéricos a una clase en concreto, por ejemplo, podemos aplicar una subida general del salario a todos los empleados, independientemente de su profesión, si hemos diseñado un procedimiento en la clase Empleado para ello. Veremos que existen más ventajas, aunque este modo de diseñar aplicaciones tiene también sus inconvenientes, sobre todo si diseñamos mal alguna clase. Derivar clases, sintaxis: La forma general de declarar clases derivadas es la siguiente: class <clase_derivada> : [public|private] <base1> [,[public|private] <base2>] {}; En seguida vemos que para cada clase base podemos definir dos tipos de acceso, public o private. Si no se especifica ninguno de los dos, por defecto se asume que es private. public: los miembros heredados de la clase base conservan el tipo de acceso con que fueron declarados en ella. private: todos los miembros heredados de la clase base pasan a ser miembros privados en la clase derivada. De momento siempre declararemos las clases base como public, al menos hasta que veamos la utilidad de hacerlo como privadas. Veamos un ejemplo sencillo basado en la idea del punto anterior: // Clase base Persona: class Persona { public: Persona(char *n, int e); const char *LeerNombre(char *n) const; int LeerEdad() const; void CambiarNombre(const char *n); void CambiarEdad(int e); protected: char nombre[40]; int edad; }; // Clase derivada Empleado: class Empleado : public Persona { public: Empleado(char *n, int e, float s); float LeerSalario() const; void CambiarSalario(const float s); protected: float salarioAnual; }; Podrás ver que hemos declarado los datos miembros de nuestras clases como protected. En general es recomendable declarar siempre los datos de nuestras clases como privados, de ese modo no son accesibles desde el exterior de la clase y además, las posibles modificaciones de esos datos, en cuanto a tipo o tamaño, sólo requieren ajustes de los métodos de la propia clase. Pero en el caso de estructuras jerárquicas de clases puede ser interesante que las clases derivadas tengan acceso a los datos miembros de las clases base. Usar el acceso protected nos permite que los datos sean inaccesibles desde el exterior de las clases, pero a la vez, permite que sean accesibles desde las clases derivadas. http://c.conclase.net/curso/index.php?cap=036 Creación de una clase derivada Cada clase derivada se debe referir a una clase base declarada anteriormente. La declaración de una clase derivada tiene la siguiente sintaxis: Class clase_derivada:<especificadores_de_acceso> clase_base {...}; Los especificadotes de acceso pueden ser: public, protected o private. Clases de derivación Los especificadores de acceso a las clases base definen los posibles tipos de derivación: public, protected y private. El tipo de acceso a la clase base especifica cómo recibirá la clase derivada a los miembros de la clase base. Si no se especifica un acceso a la clase base, C++ supone que su tipo de herencia es privado. - Derivación pública (public). Todos los miembros public y protected de la clase base son accesibles en la clase derivada, mientras que los miembros private de la clase base son siempre inaccesibles en la clase derivada. - Derivación privada (private). Todos los miembros de la clase base se comportan como miembros privados de la clase derivada. Esto significa que los miembros public y protected de la clase base no son accesibles más que por las funciones miembro de la clase derivada. Los miembros privados de la clase siguen siendo inaccesibles desde la clase derivada. - Derivación protegida (protected). Todos los miembros public y protected de la clase base se comportan como miembros protected de la clase derivada. Estos miembros no son, pues, accesibles al programa exterior, pero las clases que se deriven a continuación podrán acceder normalmente a estos miembros (datos o funciones). CREACIÓN DE UNA CLASE. Sintaxis La construcción de una clase partiendo desde cero, es decir, cuando no deriva de una clase previa, tiene la siguiente sintaxis (que es un caso particular de la sintaxis general: Class-key <info> nomb-clase {<lista-miembros>}; Ejemplo class Hotel { int habitd; int habits; char stars[5]; }; Es significativo que la declaración (y definición) de una clase puede efectuarse en cualquier punto del programa, incluso en el cuerpo de otra clase. Salvo que se trate de una declaración adelantada, el bloque <lista-miembros>, también denominado cuerpo de la clase, debe existir, y declarar en su interior los miembros que constituirán la nueva clase, incluyendo especificadotes de acceso (explícitos o por defecto) que especifican aspectos de la accesibilidad actual y futura (en los descendientes) de los miembros de la clase. la cuestión de la accesibilidad de los miembros está estrechamente relacionada con la herencia, por lo que hemos preferido trasladar la explicación de esta importante propiedad al capítulo dedicado a la herencia. Quién puede ser miembro La lista de miembros es una secuencia de declaraciones de propiedades de cualquier tipo, incluyendo enumeraciones, campos de bits etc.; así como declaración y definición de métodos, todos ellos con especificadotes opcionales de acceso y de tipo de almacenamiento. Auto, extern y register no son permitidos; si en cambio static y const. Los elementos así definidos se denominan miembros de la clase. Hemos dicho que son de dos tipo: propiedades de clase y métodos de clase. Es importante advertir que los elementos constitutivos de la clase deben ser completamente definidos para el compilador en el momento de su utilización. Esta advertencia solo tiene sentido cuando se refiere a utilización de tipos abstractos como miembros de clases, ya que los tipos simples (preconstruidos en el lenguaje) quedan perfectamente definidos con su declaración. Ver a continuación una aclaración sobre este punto. Ejemplo: Class Vuelo { // Vuelo es la clase Char nombre [30]; // nombre es una propiedad Int. capacidad; Enum modelo {B747, DC10}; Char origen[8]; Char destino [8]; Char fecha [8]; Void despegue (&operación}; // despegue es un método Void crucero (&operación); }; Los miembros pueden ser de cualquier tipo con una excepción: No pueden ser la misma clase que se está definiendo (lo que daría lugar a una definición circular), por ejemplo: Class Vuelo { Char nombre [30]; Class Vuelo; // Ilegal ... Sin embargo, si es lícito que un miembro sea puntero al tipo de la propia clase que se está declarando: class Vuelo { Char nombre [30]; Vuelo* ptr; ... En la práctica esto significa que un miembro ptr de un objeto c1 de una clase C, es un puntero que puede señalar a otro objeto c2 de la misma clase. Nota: Esta posibilidad es muy utilizada, pues permite construir árboles y listas de objetos (unos enlazan con otros). Precisamente en el capítulo dedicado a las estructuras auto referenciadas, se muestra la construcción de un árbol binario utilizando una estructura que tiene dos elementos que son punteros a objetos del tipo de la propia estructura (recuerde que las estructuras C++ son un caso particular de clases. También es lícito que se utilicen referencias a la propia clase: class X { int i; char c; public: X(const X& ref, int x = 0); { // Ok. correcto i = ref.i; }; c = ref.c; De hecho, un grupo importante de funciones miembro, los constructores-copia, se caracterizan precisamente por aceptar una referencia a la clase como primer argumento. Las clases pueden ser miembros de otras clases, clases anidadas. Por ejemplo: class X { // clase contenedora (exterior) public: int x; class Xa { // clase dentro de clase (anidada) public: int x; }; }; Ver aspectos generales en: clases dentro de clases. Las clases pueden ser declaradas dentro de funciones, en cuyo caso se denominan clases locales, aunque presentan algunas limitaciones. Ejemplo: void foo() { // función contenedora ... int x; class C { // clase local public: int x; }; } También pueden ser miembros las instancias de otras clases (objetos): class Vertice { public: int x, y; }; class Triangulo { // Clase contenedora public: Vertice va, vb, vc; // Objetos dentro de una clase }; Es pertinente recordar lo señalado al principio: que los miembros de la clase deben ser perfectamente conocidos por el compilador en el momento de su utilización. Por ejemplo: class Triangulo { ... Vertice v; // Error: Vertice no definido }; class Vertice {...}; En estos casos no es suficiente realizar una declaración adelantada de Vértice: class Vertice; class Triangulo { public: Vértice v; // Error: Información insuficiente de Vértice }; Class Vértice {...}; Ya que el compilador necesita una definición completa del objeto v para insertarlo como miembro de la clase Triangulo. La consecuencia es que importa el orden de declaración de las clases en el fuente. Debe comenzarse definiendo los tipos más simples (que no tienen dependencia de otros) y seguir en orden creciente de complejidad (clases que dependen de otras clases para su definición). También se colige que deben evitarse definiciones de clases mutuamente dependientes: class A { ... B b1; }; class B { ... A a1; }; ya que conducirían a definiciones circulares como las señaladas antes Los miembros de la clase deben ser completamente declarados dentro del cuerpo, sin posibilidad de que puedan se añadidos fuera de él. Las definiciones de las propiedades se efectúan generalmente en los constructores (un tipo de función-miembro), aunque existen otros recursos inicialización de miembros. La definición de los métodos puede realizarse dentro o fuera del cuerpo ( funciones inline. Ejemplo: class C { int x; char c; void foo(); }; int C::y; // Error!! declaración off-line void C::foo() { ++x; } // Ok. definición off-line Las funciones-miembro, denominadas métodos, pueden ser declaradas inline, static virtual, const y explicit si son constructores. Por defecto tienen enlazado externo. Clases vacías Los miembros pueden faltar completamente, en cuyo caso tendremos una clase vacía. Ejemplo: Class empty {}; La clase vacía es una definición completa y sus objetos son de tamaño distinto de cero, por lo que cada una de sus instancias tiene existencia independiente. Suelen utilizarse como clases-base durante el proceso de desarrollo de aplicaciones. Cuando se sospecha que dos clases pueden tener algo en común, pero de momento no se sabe exactamente que. Inicialización de miembros Lo mismo que ocurre con las estructuras, que a fin de cuentas son un tipo de clase en su declaración solo está permitido señalar tipo y nombre de los miembros, sin que se pueda efectuar ninguna asignación, ni aún en el caso de que se trate de una constante. Así pues, en el bloque <lista-miembros> no pueden existir asignaciones. Por ejemplo: class C { ... int x = 33; // Asignación ilegal !! ... }; Las únicas excepciones permitidas son la asignación a constantes estáticas enteras y los enumeradores (ver a continuación), ya que los miembros estáticos tienen unas características muy especiales. Ejemplo: class C { ... static const int kte = 33; // Ok: static const kt1 = 33.0 // Error: No entero cont int kt2 = 33; static kt3 = 33; // Error: No estática // Error: No constante static const int kt4 = f(33); // Error: inicializador no constante }; El sitio idóneo para situar las asignaciones a miembros es en el cuerpo de las funciones de clase (métodos). En especial las asignaciones iniciales (que deben efectuarse al instanciar un objeto de la clase) tienen un sitio específico en el cuerpo de ciertos métodos especiales denominados constructores. En el epígrafe "Inicializar miembros" se ahonda en esta cuestión. Si es posible utilizar y definir un enumerador (que es una constante simbólica dentro de una clase. Por ejemplo: class C { ... enum En { E1 = 3, E2 = 1, E3, E4 = 0}; ... }; En ocasiones es posible utilizar un enumerador para no tener que definir una constante estática. Ejemplo-1 Las tres formas siguientes serían aceptables: class C { static const int k1 = 10; char v1[k1]; enum e {E1 = 10}; char v2[E1]; enum {KT = 20}; char v3[KT]; ... }; Ejemplo-2: class CAboutDlg : public CDialog { ... enum { IDD = IDD_ABOUTBOX }; ... }; La definición de la clase CAboutDlg pertenece a un caso real tomado de MS VC++. El enumerador anónimo es utilizado aquí como un recurso para inicializar la propiedad IDD con el valor IDD_ABOUTBOX que es a su vez una constante simbólica para el compilador. De no haberse hecho así, se tendría que haber declarado IDD como constante estática, en cambio la forma adoptada la convierte en una variable enumerada anónima que solo puede adoptar un valor (otra forma de designar al mismo concepto). Ejemplo-3: Class C { ... enum {CERO = 0, UNO = 1, DOS = 2, TRES = 3}; }; ... void foo(C& c1) { std::cout << c1.CERO; // -> 0 Std::cout << c1.TRES; // -> 3 } Téngase en cuenta que las clases son tipos de datos que posteriormente tienen su concreción en objetos determinados. Precisamente una de las razones de ser de las variables de clase, es que pueden adoptar valores distintos en cada instancia concreta de la clase. Por esta razón, a excepción de las constantes y los miembros estáticos, no tiene mucho sentido asignar valores concretos a las variables de clase, ya que los valores concretos los reciben las instancias, bien por asignación directa, o a través de los constructores. En el apartado dedicado a Inicialización de miembros volvemos sobre la cuestión, exponiendo con detalle la forma de realizar estas asignaciones, en especial cuando se trata de constantes. http://html.rincondelvago.com/clases-derivadas.html 5.14 Parte protegida. 5.14.1 Propósito de la parte protegida. 5.15 Redefinición de los miembros de las clases derivadas. 5.16 Clases virtuales y visibilidad. 5.17 Constructores y destructores en clases derivadas. Las clases pueden introducirse de muchas formas, comenzando por la que dice que representan un intento de abstraer el mundo real. Pero desde el punto de vista del programador clásico, lo mejor es considerarlas como "entes" que superceden las estructuras C en el sentido de que tanto los datos como los instrumentos para su manipulación (funciones) se encuentran encapsulados en ellos. La idea es empaquetar juntos los datos y la funcionalidad, de ahí que tengan dos tipos de componentes (aquí se prefiere llamarlos miembros). Por un lado las propiedades, también llamadas variables o campos (fields), y de otro los métodos, también llamados procedimientos o funciones [1]; más formalmente: variables de clase y métodos de clase. La terminología utilizada en la Programación Orientada a Objetos POO (OOP en inglés), no es demasiado consistente, y a veces induce a cierto error a los programadores que se acercan por primera vez con una cultura de programación procedural. De hecho, estas cuestiones semánticas suponen una dificultad adicional en el proceso de entender los conceptos subyacentes en la POO, sus ventajas y su potencial como herramienta. Las clases C++ ofrecen la posibilidad de extender los tipos predefinidos en el lenguaje (básico y derivado). Cada clase representa un nuevo tipo; un nuevo conjunto de objetos caracterizado por ciertos valores (propiedades) y las operaciones (métodos) disponibles para crearlos, manipularlos y destruirlos. Más tarde se podrán declarar objetos pertenecientes a dicho tipo (clase) del mismo modo que se hace para las variables simples tradicionales. Considerando que son vehículos para manejo y manipulación de información, las clases han sido comparadas en ocasiones con los sistemas tradicionales de manejo de datos DBMS ("DataBase Management System"); aunque de un tipo muy especial, ya que sus características les permiten operaciones que están absolutamente prohibidas a los sistemas DBMS clásicos. La mejor manera de entender las clases es considerar que se trata simplemente de tipos de datos cuya única peculiaridad es que pueden ser definidos por el usuario. Generalmente se trata de tipos complejos, constituidos a su vez por elementos de cualquier tipo (incluso otras clases). La definición que puede hacerse de ellos no se reduce a diseñar su "contenido"; también pueden definirse su álgebra y su interfaz. Es decir: como se opera con estos tipos y como los ve el usuario (que puede hacer con ellos). El propio inventor del lenguaje señala que la principal razón para definir un nuevo tipo es separar los detalles poco relevantes de la implementación de las propiedades que son verdaderamente esenciales para utilizarlos correctament CLASES DERIVADAS. En C++, la herencia simple se realiza tomando una clase existente y derivando nuevas clases de ella. La clase derivada hereda las estructuras de datos y funciones de la clase original. Además, se pueden añadir nuevos miembros a las clases derivadas y los miembros heredados pueden ser modificados. Una clase utilizada para derivar nuevas clases se denomina clase base, clase padre, superclase o ascendiente. Una clase creada de otra clase se denomina clase derivada o subclase. Se pueden construir jerarquías de clases, en las que cada clase sirve como padre o raíz de una nueva clase. Conceptos fundamentales de derivación C++ utiliza un sistema de herencia jerárquica. Es decir, se hereda una clase de otra, creando nuevas clases a partir de las clases ya existentes. Sólo se pueden heredar clases, no funciones ordinarias n variables, en C++. Una clase derivada hereda todos los miembros dato excepto, miembros dato estático, de cada una de sus clases base. Una clase derivada hereda la función miembro de su clase base. Esto significa que se hereda la capacidad para llamar a funciones miembro de la clase base en los objetos de la clase derivada. Los siguientes elementos de la clase no se heredan: - Constructores - Destructores - Funciones amigas - Funciones estáticas de la clase - Datos estáticos de la clase - Operador de asignación sobrecargado Las clases base diseñadas con el objetivo principal de ser heredadas por otras se denominan clases abstractas. Normalmente, no se crean instancias a partir de clases abstractas, aunque sea posible. La herencia en C++ En C++ existen dos tipos de herencia: simple y múltiple. La herencia simple es aquella en la que cada clase derivada hereda de una única clase. En herencia simple, cada clase tiene un solo ascendiente. Cada clase puede tener, sin embargo, muchos descendientes. La herencia múltiple es aquella en la cual una clase derivada tiene más de una clase base. Aunque el concepto de herencia múltiple es muy útil, el diseño de clases suele ser más complejo. Creación de una clase derivada Cada clase derivada se debe referir a una clase base declarada anteriormente. La declaración de una clase derivada tiene la siguiente sintaxis: Class clase_derivada:<especificadores_de_acceso> clase_base {...}; Los especificadotes de acceso pueden ser: public, protected o private. Clases de derivación Los especificadores de acceso a las clases base definen los posibles tipos de derivación: public, protected y private. El tipo de acceso a la clase base especifica cómo recibirá la clase derivada a los miembros de la clase base. Si no se especifica un acceso a la clase base, C++ supone que su tipo de herencia es privado. - Derivación pública (public). Todos los miembros public y protected de la clase base son accesibles en la clase derivada, mientras que los miembros private de la clase base son siempre inaccesibles en la clase derivada. - Derivación privada (private). Todos los miembros de la clase base se comportan como miembros privados de la clase derivada. Esto significa que los miembros public y protected de la clase base no son accesibles más que por las funciones miembro de la clase derivada. Los miembros privados de la clase siguen siendo inaccesibles desde la clase derivada. - Derivación protegida (protected). Todos los miembros public y protected de la clase base se comportan como miembros protected de la clase derivada. Estos miembros no son, pues, accesibles al programa exterior, pero las clases que se deriven a continuación podrán acceder normalmente a estos miembros (datos o funciones). CREACIÓN DE UNA CLASE. Sintaxis La construcción de una clase partiendo desde cero, es decir, cuando no deriva de una clase previa, tiene la siguiente sintaxis (que es un caso particular de la sintaxis general: Class-key <info> nomb-clase {<lista-miembros>}; Ejemplo class Hotel { int habitd; int habits; char stars[5]; }; Es significativo que la declaración (y definición) de una clase puede efectuarse en cualquier punto del programa, incluso en el cuerpo de otra clase. Salvo que se trate de una declaración adelantada, el bloque <lista-miembros>, también denominado cuerpo de la clase, debe existir, y declarar en su interior los miembros que constituirán la nueva clase, incluyendo especificadotes de acceso (explícitos o por defecto) que especifican aspectos de la accesibilidad actual y futura (en los descendientes) de los miembros de la clase. la cuestión de la accesibilidad de los miembros está estrechamente relacionada con la herencia, por lo que hemos preferido trasladar la explicación de esta importante propiedad al capítulo dedicado a la herencia. Quién puede ser miembro La lista de miembros es una secuencia de declaraciones de propiedades de cualquier tipo, incluyendo enumeraciones, campos de bits etc.; así como declaración y definición de métodos, todos ellos con especificadotes opcionales de acceso y de tipo de almacenamiento. Auto, extern y register no son permitidos; si en cambio static y const. Los elementos así definidos se denominan miembros de la clase. Hemos dicho que son de dos tipo: propiedades de clase y métodos de clase. Es importante advertir que los elementos constitutivos de la clase deben ser completamente definidos para el compilador en el momento de su utilización. Esta advertencia solo tiene sentido cuando se refiere a utilización de tipos abstractos como miembros de clases, ya que los tipos simples (preconstruidos en el lenguaje) quedan perfectamente definidos con su declaración. Ver a continuación una aclaración sobre este punto. Ejemplo: Class Vuelo { // Vuelo es la clase Char nombre [30]; // nombre es una propiedad Int. capacidad; Enum modelo {B747, DC10}; Char origen[8]; Char destino [8]; Char fecha [8]; Void despegue (&operación}; // despegue es un método Void crucero (&operación); }; Los miembros pueden ser de cualquier tipo con una excepción: No pueden ser la misma clase que se está definiendo (lo que daría lugar a una definición circular), por ejemplo: Class Vuelo { Char nombre [30]; Class Vuelo; ... // Ilegal Sin embargo, si es lícito que un miembro sea puntero al tipo de la propia clase que se está declarando: class Vuelo { Char nombre [30]; Vuelo* ptr; ... En la práctica esto significa que un miembro ptr de un objeto c1 de una clase C, es un puntero que puede señalar a otro objeto c2 de la misma clase. Nota: Esta posibilidad es muy utilizada, pues permite construir árboles y listas de objetos (unos enlazan con otros). Precisamente en el capítulo dedicado a las estructuras auto referenciadas, se muestra la construcción de un árbol binario utilizando una estructura que tiene dos elementos que son punteros a objetos del tipo de la propia estructura (recuerde que las estructuras C++ son un caso particular de clases. También es lícito que se utilicen referencias a la propia clase: class X { int i; char c; public: X(const X& ref, int x = 0); { // Ok. correcto i = ref.i; }; c = ref.c; De hecho, un grupo importante de funciones miembro, los constructores-copia, se caracterizan precisamente por aceptar una referencia a la clase como primer argumento. Las clases pueden ser miembros de otras clases, clases anidadas. Por ejemplo: class X { // clase contenedora (exterior) public: int x; class Xa { // clase dentro de clase (anidada) public: int x; }; }; Ver aspectos generales en: clases dentro de clases. Las clases pueden ser declaradas dentro de funciones, en cuyo caso se denominan clases locales, aunque presentan algunas limitaciones. Ejemplo: void foo() { // función contenedora ... int x; class C { // clase local public: int x; }; } También pueden ser miembros las instancias de otras clases (objetos): class Vertice { public: int x, y; }; class Triangulo { // Clase contenedora public: Vertice va, vb, vc; // Objetos dentro de una clase }; Es pertinente recordar lo señalado al principio: que los miembros de la clase deben ser perfectamente conocidos por el compilador en el momento de su utilización. Por ejemplo: class Triangulo { ... Vertice v; // Error: Vertice no definido }; class Vertice {...}; En estos casos no es suficiente realizar una declaración adelantada de Vértice: class Vertice; class Triangulo { public: Vértice v; // Error: Información insuficiente de Vértice }; Class Vértice {...}; Ya que el compilador necesita una definición completa del objeto v para insertarlo como miembro de la clase Triangulo. La consecuencia es que importa el orden de declaración de las clases en el fuente. Debe comenzarse definiendo los tipos más simples (que no tienen dependencia de otros) y seguir en orden creciente de complejidad (clases que dependen de otras clases para su definición). También se colige que deben evitarse definiciones de clases mutuamente dependientes: class A { ... B b1; }; class B { ... A a1; }; ya que conducirían a definiciones circulares como las señaladas antes Los miembros de la clase deben ser completamente declarados dentro del cuerpo, sin posibilidad de que puedan se añadidos fuera de él. Las definiciones de las propiedades se efectúan generalmente en los constructores (un tipo de función-miembro), aunque existen otros recursos inicialización de miembros. La definición de los métodos puede realizarse dentro o fuera del cuerpo ( funciones inline. Ejemplo: class C { int x; char c; void foo(); }; int C::y; // Error!! declaración off-line void C::foo() { ++x; } // Ok. definición off-line Las funciones-miembro, denominadas métodos, pueden ser declaradas inline, static virtual, const y explicit si son constructores. Por defecto tienen enlazado externo. Clases vacías Los miembros pueden faltar completamente, en cuyo caso tendremos una clase vacía. Ejemplo: Class empty {}; La clase vacía es una definición completa y sus objetos son de tamaño distinto de cero, por lo que cada una de sus instancias tiene existencia independiente. Suelen utilizarse como clases-base durante el proceso de desarrollo de aplicaciones. Cuando se sospecha que dos clases pueden tener algo en común, pero de momento no se sabe exactamente que. Inicialización de miembros Lo mismo que ocurre con las estructuras, que a fin de cuentas son un tipo de clase en su declaración solo está permitido señalar tipo y nombre de los miembros, sin que se pueda efectuar ninguna asignación, ni aún en el caso de que se trate de una constante. Así pues, en el bloque <lista-miembros> no pueden existir asignaciones. Por ejemplo: class C { ... int x = 33; // Asignación ilegal !! ... }; Las únicas excepciones permitidas son la asignación a constantes estáticas enteras y los enumeradores (ver a continuación), ya que los miembros estáticos tienen unas características muy especiales. Ejemplo: class C { ... static const int kte = 33; // Ok: static const kt1 = 33.0 // Error: No entero cont int kt2 = 33; static kt3 = 33; // Error: No estática // Error: No constante static const int kt4 = f(33); // Error: inicializador no constante }; El sitio idóneo para situar las asignaciones a miembros es en el cuerpo de las funciones de clase (métodos). En especial las asignaciones iniciales (que deben efectuarse al instanciar un objeto de la clase) tienen un sitio específico en el cuerpo de ciertos métodos especiales denominados constructores. En el epígrafe "Inicializar miembros" se ahonda en esta cuestión. Si es posible utilizar y definir un enumerador (que es una constante simbólica dentro de una clase. Por ejemplo: class C { ... enum En { E1 = 3, E2 = 1, E3, E4 = 0}; ... }; En ocasiones es posible utilizar un enumerador para no tener que definir una constante estática. Ejemplo-1 Las tres formas siguientes serían aceptables: class C { static const int k1 = 10; char v1[k1]; enum e {E1 = 10}; char v2[E1]; enum {KT = 20}; char v3[KT]; ... }; Ejemplo-2: class CAboutDlg : public CDialog { ... enum { IDD = IDD_ABOUTBOX }; ... }; La definición de la clase CAboutDlg pertenece a un caso real tomado de MS VC++. El enumerador anónimo es utilizado aquí como un recurso para inicializar la propiedad IDD con el valor IDD_ABOUTBOX que es a su vez una constante simbólica para el compilador. De no haberse hecho así, se tendría que haber declarado IDD como constante estática, en cambio la forma adoptada la convierte en una variable enumerada anónima que solo puede adoptar un valor (otra forma de designar al mismo concepto). Ejemplo-3: Class C { ... enum {CERO = 0, UNO = 1, DOS = 2, TRES = 3}; }; ... void foo(C& c1) { std::cout << c1.CERO; // -> 0 Std::cout << c1.TRES; // -> 3 } Téngase en cuenta que las clases son tipos de datos que posteriormente tienen su concreción en objetos determinados. Precisamente una de las razones de ser de las variables de clase, es que pueden adoptar valores distintos en cada instancia concreta de la clase. Por esta razón, a excepción de las constantes y los miembros estáticos, no tiene mucho sentido asignar valores concretos a las variables de clase, ya que los valores concretos los reciben las instancias, bien por asignación directa, o a través de los constructores. En el apartado dedicado a Inicialización de miembros volvemos sobre la cuestión, exponiendo con detalle la forma de realizar estas asignaciones, en especial cuando se trata de constantes. NATURALEZA: TIPO U OBJETOS. Gestión dinámica de tipos. GLib incluye un sistema dinámico de tipos, que no es más que una base de datos en la que se van registrando las distintas clases. En esa base de datos, se almacenan todas las propiedades asociadas a cada tipo registrado, información tal como las funciones de inicialización del tipo, el tipo base del que deriva, el nombre del tipo, etc. Todo ello identificado por un identificador único, conocido como GType. Tipos basados en clases (objetos). Para crear instancias de una clase, es necesario que el tipo haya sido registrado anteriormente, de forma que esté presente en la base de datos de tipos de GLib!. El registro de tipos se hace mediante una estructura llamada GTypeInfo, que tiene la siguiente forma: Tipos no instanciables (fundamentales). Muchos de los tipos que se registran no son directamente instanciables (no se pueden crear nuevas instancias) y no están basados en una clase. Estos tipos se denominan tipos “fundamentales” en la terminología de GLib! y son tipos que no están basados en ningún otro tipo, tal y como sí ocurre con los tipos instanciables (u objetos). Entre estos tipos fundamentales se encuentran algunos viejos conocidos, como por ejemplo gchar y otros tipos básicos, que son automáticamente registrados cada vez que se inicia una aplicación que use GLib!. Como en el caso de los tipos instanciables, para registrar un tipo fundamental es necesaria una estructura de tipo GTypeInfo, con la diferencia que para los tipos fundamentales bastará con rellenar con ceros toda la estructura. La mayor parte de los tipos no instanciables están diseñados para usarse junto con GValue, que permite asociar fácilmente un tipo GType con una posición de memoria. Se usan principalmente como simples contenedores genéricos para tipos sencillos (números, cadenas, estructuras, etc.). Implementación de nuevos tipos. En el apartado anterior se mostraba la forma de registrar una nueva clase en el sistema de objetos de GLib!, y se hacía referencia a distintas estructuras y funciones de inicialización. En este apartado, se va a indagar con más detalle en ellos. Tanto los tipos fundamentales como los no fundamentales quedan definidos por la siguiente información: Tamaño de la clase. Funciones de inicialización (constructores en C++!). Funciones de destrucción (destructores en C++!), llamadas de finalización en la jerga de GLib!. Tamaño de las instancias. Normas de instanciación de objetos (uso del operador new en C++!). Funciones de copia. Toda esta información queda almacenada, como se comentaba anteriormente, en una estructura de tipo GTypeInfo. Todas las clases deben implementar, aparte de la función de registro de la clase (my_object_get_type), al menos dos funciones que se especificaban en la estructura GTypeInfo a la hora de registrar la clase. Estas funciones son: my_object_class_init: función de inicialización de la clase, donde se inicializará todo lo relacionado con la clase en sí, tal como las señales que tendrá la clase, los manejadores de los métodos virtuales si los hubiera, etc. my_object_init: función de inicialización de las instancias de la clase, que será llamada cada vez que se solicite la creación de una nueva instancia de la clase. En esta función, las tareas a desempeñar son todas aquellas relacionadas con la inicialización de una nueva instancia de la clase, tales como los valores iniciales de las variables internas de la instancia. http://html.rincondelvago.com/clases-derivadas.html Constructores Contenido 1 Sinopsis 8 Necesidad de un constructor explícito 2 Descripción 9 Orden de construcción 3 Técnicas de buena construcción 10 Constructores y funciones virtuales 4 Invocación de constructores 11 Constructores de conversión 5 Propiedades de los constructores 12 Constructor explicit 6 Constructor oficial 13 Constructores privados y protegidos 7 Constructor por defecto §1 Sinopsis Podemos imaginar que la construcción de objetos tiene tres fases: (I) instanciación, que aquí representa el proceso de asignación de espacio al objeto, de forma que este tenga existencia real en memoria. (II) Asignación de recursos. Por ejemplo, un miembro puede ser un puntero señalando a una zona de memoria que debe ser reservada; un "handle" a un fichero; el bloqueo de un recurso compartido o el establecimiento de una línea de comunicación. (III) Iniciación, que garantiza que los valores iniciales de todas sus propiedades sean correctos (no contengan basura). La correcta realización de estas fases es importante, por lo que los diseñadores del lenguaje decidieron asignar esta tarea a un tipo especial de funciones (métodos) denominadas constructores. En realidad, la consideraron tan importante que, como veremos a continuación, si el programador no declara ninguno explícitamente, el compilador se encarga de definir un constructores de oficio , encargándose de utilizarlo cada vez que es necesario. Aparte de las invocaciones explícitas que pueda realizar el programador, los constructores son frecuentemente invocados de forma implícita por el compilador. Es significativo señalar que las fases anteriores se realizan en un orden, aunque todas deben ser felizmente completadas cuando finaliza la labor del constructor. §2 Descripción Para empezar a entender como funciona el asunto, observe este sencillo ejemplo en el que se definen sendas clases para representar complejos; en una de ellas definimos explícitamente un constructor; en otra dejamos que el compilador defina un constructor de oficio: #include <iostream> using namespace std; class CompleX { // Una clase para representar complejos public: float r; float i; // Partes real e imaginaria CompleX(float r = 0, float i = 0) { // L.7: construtor explícito this->r = r; this->i = i; cout << "c1: (" << this->r << "," << this->i << ")" << endl; } }; class CompX { // Otra clase análoga public: float r; float i; // Partes real e imaginaria }; void main() { CompleX c1; CompleX c2(1,2); CompX c3; cout << "c3: (" << c3.r << } Salida: c1: (0,0) // ====================== // L.18: // L.19: // L.20: "," << c3.i << ")" << endl; c2: (1,2) c3: (6.06626e-39,1.4013e-45) Comentario: En la clase CompleX definimos explícitamente un constructor que tiene argumentos por defecto ( ), no así en la clase CompX en la que es el propio compilador el que define un constructor de oficio. Es de destacar la utilización explícita del puntero this ( 4.11.6) en la definición del constructor (Ls.8-9). Ha sido necesario hacerlo así para distinguir las propiedades i, j de las variables locales en la función-constructor (hemos utilizado deliberadamente los mismos nombres en los argumentos, pero desde luego, podríamos haber utilizado otros :-) En la función main se instancian tres objetos; en todos los casos el compilador realiza una invocación implícita al constructor correspondiente. En la declaración de c1, se utilizan los argumentos por defecto para inicializar adecuadamente sus miembros; los valores se comprueban en la primera salida. La declaración de c2 en L.19 implica una invocación del constructor por defecto pasándole los valores 1 y 2 como argumentos. Es decir, esta sentencia equivaldría a: c2 = CompleX::CompleX(1, 2); // Hipotética invocación explícita al constructor Nota: En realidad esta última sentencia es sintácticamente incorrecta; se trata solo de un recurso pedagógico, ya que no es posible invocar de esta forma al constructor de una clase ( 4.11.2d). Una alternativa correcta a la declaración de L.19 sería: CompleX c2 = CompleX(1,2); El resultado de L.19 puede verse en la segunda salida. Finalmente, en L.20 la declaración de c3 provoca la invocación del constructor de oficio construido por el propio compilador. Aunque la iniciación del objeto con todos sus miembros es correcta, no lo es su inicialización ( 4.1.2). En la tercera salida vemos como sus miembros adoptan valores arbitrarios. En realidad se trata de basura existente en las zonas de memoria que les han sido adjudicadas. El corolario inmediato es deducir lo que ya señalamos en la página anterior: aunque el constructor de oficio inicia adecuadamente los miembros abstractos ( 4.11.2d), no hace lo mismo con los escalares. Además, por una u otra causa, en la mayoría de los casos de aplicaciones reales es imprescindible la definición explícita de uno o varios de estos constructores ( ). §3 Técnias de buena construcción Recordar que un objeto no se considera totalmente construido hasta que su constructor ha concluido satisfactoriamente. En los casos que la clase contenga sub-objetos o derive de otras, el proceso de creación incluye la invocación de los constructores de las subclases o de las super-clases en una secuencia ordenada que se detalla más adelante . Los constructores deben ser diseñados de forma que no puedan (ni áun en caso de error) dejar un objeto a medio construir. En cas que no sea posible alistar todos los recursos exigidos por el objeto, antes de terminar su ejecucón debe preverse un mecanismo de destrucción y liberación de los recursos que hubiesen sido asignados. Para esto es posible utilizar el mecanismo de excepciones. §4 Invocación de constructores Al margen de la particularidad que representan sus invocaciones implícitas, en general su invocación sigue las pautas del resto de los métodos. Ejemplos: X x1; constructor X::X(); [4] X x2 = X::X() X x3 = X(); constructor [5] X x4(); anterior [6] // L.1: Ok. Invocación implícita del // Error: invocación ilegal del constructor // Error: invocación ilegal del constructor // L.4: Ok. Invocación legal del // L.5: Ok. Variación sintáctica del Nota: Observe como la única sentencia válida con invocación explícita al constructor (L.4) es un caso de invocación de función miembro muy especial desde el punto de vista sintáctico (esta sintaxis no está permitida con ningún otro tipo de función-miembro, ni siquiera con funciones estáticas o destructores). La razón es que los constructores se diferencian de todos los demás métodos no estáticos de la clase en que no se invocan sobre un objeto (aunque tienen puntero this 4.11.6). En realidad se asemejan a los dispositivos de asignación de memoria, en el sentido que son invocados desde un trozo de memoria amorfa y la convierten en una instancia de la clase [7]. Como ocurre con los tipos básicos (preconstruidos en el lenguaje), si deseamos crear objetos persistentes de tipo abstracto (definidos por el usuario), debe utilizarse el operador new ( 4.9.20). Este operador está íntimamente relacionado con los constructores. De hecho, para invocar la creación de un objeto a traves de él, debe existir un constructor por defecto ( ). Si nos referimos a la clase CompleX definida en el ejemplo ( ), las sentencias: { CompleX* pt1 = new(CompleX); CompleX* pt2 = new(CompleX)(1,2); } provocan la creación de dos objetos automáticos, los punteros pt1 y pt2, así como la creación de sendos objetos (anónimos) en el montón. Observe que ambas sentencias suponen un invocación implícita al constructor. La primera al constructor por defecto sin argumentos, la segunda con los argumentos indicados. En consecuencia producirán las siguientes salidas: c1: (0,0) c1: (1,2) Observe también, y esto es importante, que los objetos pt1 y pt2 son destruidos automáticamente al salir de ámbito el bloque. No así los objetos señalados por estos punteros (ver comentario al respecto 4.11.2d2). §5 Propiedades de los constructores Aunque los constructores comparten muchas propiedades de los métodos normales, tienen algunas características que las hace ser un tanto especiales. En concreto, se trata de funciones que utilizan rutinas de manejo de memoria en formas que las funciones normales no suelen utilizar. §5.1 Los constructores se distinguen del resto de las funciones de una clase porque tienen el mismo nombre que esta. Ejemplo: class X { public: X(); }; // definición de la clase X // constructor de la clase X §5.2 No se puede obtener su dirección, por lo que no pueden declararse punteros a este tipo de métodos. §5.3 No pueden declararse virtuales ( class C { ... virtual C(); }; 4.11.8a). Ejemplo: // Error !! La razón está en la propia idiosincrasia de este tipo de funciones. En efecto, veremos que declarar que un método es virtual ( 4.11.8a) supone indicar al compilador que el modo concreto de operar la función será definido más tarde, en una clase derivada; sin embargo, un constructor debe conocer el tipo exacto de objeto que debe crear, por lo que no puede ser virtual. §5.4 Otras peculiaridades de los constructores es que se declaran sin devolver nada, ni siquiera void, lo que no es óbice para que el resultado de su actuación (un objeto) si pueda ser utilizado como valor devuelto por una función: class C { ... }; ... C foo() { return C(); } §5.5 No pueden ser heredados, aunque una clase derivada puede llamar a los constructores y destructores de la superclase siempre que hayan sido declarados public o protected ( 4.11.2a). Como el resto de las funciones (excepto main), los constructores también pueden ser sobrecargados; es decir: Una clase puede tener varios constructores. En estos casos, la invocación (incluso implícita) del constructor adecuado se efectuará según los argumentos involucrados. Es de destacar que en ocasiones, esta multiplicidad de constructores puede conducir a situaciones realmente curiosas; incluso se ha definido una palabra clave, explicit, para evitar los posibles efectos colaterales ( ). §5.5 Un constructor no puede ser friend ( 4.11.2a1) de ninguna otra clase. §5.6 Una peculiaridad sintáctica de este tipo de funciones es la posibilidad de incluir iniciadores ( 4.11.2d3), una forma de expresar la inicialización de variables fuera del cuerpo del constructor. Ejemplo: class X { const int i; char c; public: X(int entero, char caracter): i(entero), c(caracter) { }; }; §5.7 Como en el resto de las funciones, los constructores pueden tener argumentos por defecto. Por ejemplo, el constructor: X::X(int, int = 0) puede aceptar uno o dos argumentos. Cuando se utiliza con uno, el segundo se supone que es un cero int. De forma análoga, el constructor X::X(int = 5, int = 6) puede aceptar dos, uno o ningún argumento, con sus correspondientes valores por defecto para cuando faltan. Observe que un constructor sin argumentos, como X::X(), no debe ser confundido con X::X(int=0), que puede ser llamado sin argumentos o con uno; aunque en realidad siempre tendrá un argumento. En otras palabras: Que una función pueda ser invocada sin argumentos no implica necesariamente que no los acepte. §5.8 Cuando se definen constructores deben evitarse ambigüedades. Es el caso de los constructores por defecto del ejemplo siguiente: class X { public: X(); X(int i = 0); }; int main() { X uno(10); X dos; X::X(int = 0) return 0; } // Ok; usa el constructor X::X(int) // Error: ambigüedad cual usar? X::X() o §5.9 Los constructores de las variables globales son invocados por el módulo inicial antes de que sea llamada la función main y las posibles funciones que se hubiesen instalado mediante la directiva #pragma startup ( 1.5). §5.10 Los objetos locales se crean tan pronto como se inicia su ámbito. También se invoca implícitamente un constructor cuando se crea o copia un objeto de la clase (incluso temporal). El hecho de que al crear un objeto se invoque implícitamente un constructor por defecto si no se invoca ninguno de forma explícita, garantiza que siempre que se instancie un objeto será inicializado adecuadamente. En el ejemplo que sigue se muestra claramente como se invoca el constructor tan pronto como se crea un objeto. #include <iostream> using namespace std; class A { // definición de una clase public: int x; A(int i = 1) { // constructor por defecto x = i; cout << "Se ha creado un objeto" << endl; } }; int main() { // ========================= A a; // se instancia un objeto cout << "Valor de a.x: " << a.x << endl; return 0; } Salida: Se ha creado un objeto Valor de a.x: 1 §5.11 El constructor de una clase no puede admitir la propia clase como argumento (se daría lugar a una definición circular). Ejemplo: class X { public: X(X); }; // Error: ilegal §5.12 Los parámetros del constructor pueden ser de cualquier tipo, y aunque no puede aceptar su propia clase como argumento. En cambio si pueden aceptar una referencia a objetos de su propia clase, en cuyo caso se denomina constructor-copia (su sentido y justificación lo exponemos con más detalle en el apartado correspondiente 4.11.2d4). Ejemplo: class X { public: X(X&); }; // Ok. correcto Aparte del referido constructor-copia, existe otro tipo de constructores de nombre específico: el constructor oficial y el constructor por defecto ( ). §6 Constructor oficial Si el programador no define explícitamente ningún constructor, el compilador proporciona uno por defecto al que llamaremos oficial o de oficio. Es público, "inline" ( 4.11.2a), y definido de forma que no acepta argumentos. Es el responsable de que funcionen sin peligro secuencias como esta: class A { int x; }; ... A a; // C++ ha creado un constructor "de oficio" // invocación implícita al constructor de oficio Recordemos que el constructor de oficio invoca implícitamente los constructores de oficio de todos los miembros. Si algunos miembros son a su vez objetos abstractos, se invocan sus constructores. Así sucesivamente con cualquier nivel de complejidad hasta llegar a los tipos básicos (preconstruidos en el lenguaje 2.2) cuyos constructores son también invocados. Recordar que los constructores de los tipos básicos inician (reservan memoria) para estos objetos, pero no los inicializan con ningún valor concreto. Por lo que en principio su contenido es impredecible (basura) [1]. Dicho en otras palabras: el constructor de oficio se encarga de preparar el ambiente para que el objeto de la clase pueda operar, pero no garantiza que los datos contenidos sean correctos. Esto último es responsabilidad del programador y de las condiciones de "runtime". Por ejemplo: struct Nombre { char* nomb; }; struct Equipo { Nombre nm; size_t sz; }; struct Liga { int year; char categoria; Nombre nLiga; Equipo equipos[10]; }; ... Liga primDiv; En este caso la última sentencia inicia primDiv mediante una invocación al constructor por defecto de Liga, que a su vez invoca a los constructores por defecto de Nombre y Equipo. para crear los miembros nLiga y equipos (el constructor de Equipo es invocado diez veces, una por cada miembro de la matriz). A su vez, cada invocación a Equipo() produce a su vez una invocación al constructor por defecto de Nombre (size_t es un tipo básico y no es invocado su constructor 4.9.13). Los miembros nLiga y equipos son iniciados de esta forma, pero los miembros year y categoria no son inicializados ya que son tipos simples, por lo que pueden contener basura. Si el programador define explícitamente cualquier constructor, el constructor oficial deja de existir. Pero si omite en él la inicialización de algún tipo abstracto, el compilador añadirá por su cuenta las invocaciones correspondientes a los constructores por defecto de los miembros omitidos ( Ejemplo). §6.1 Constructor trivial Un constructor de oficio se denomina trivial si cumple las siguientes condiciones: La clase correspondiente no tiene funciones virtuales ( 4.11.8a) y no deriva de ninguna superclase virtual. Todos los constructores de las superclases de su jerarquía son triviales Los constructores de sus miembros no estáticos que sean clases son también triviales §7 Constructor por defecto Constructor por defecto de la clase X es aquel que "puede" ser invocado sin argumentos, bien porque no los acepte, bien porque disponga de argumentos por defecto ( 4.4.5). Como hemos visto en el epígrafe anterior, el constructor oficial creado por el compilador si no hemos definido ningún constructor es también un constructor por defecto, ya que no acepta argumentos. Tenga en cuenta que diversas posibilidades funcionales y sintácticas de C++ precisan de la existencia de un constructor por defecto (explícito u oficial). Por ejemplo, es el responsable de la creación del objeto x en una declaración del tipo X x;. §8 Un constructor explícito puede ser imprescindible En el primer ejemplo ( ), el programa ha funcionado aceptablemente bien utilizando el constructor de oficio en una de sus clases, pero existen ocasiones en que es imprescindible que el programador defina uno explícitamente, ya que el suministrado automáticamente por el compilador no es adecuado. Consideremos una variación del citado ejemplo en la que definimos una clase para contener las coordenadas de puntos de un plano en forma de matrices de dos dimensiones: #include <iostream> using namespace std; class Punto { public: int coord[2]; }; int main() { // ================== Punto p1(10, 20); // L.8: cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl; } Este programa produce un error de compilación en L.8. La razón es que si necesitamos este tipo de inicialización del objeto p1, utilizando una lista de argumentos, es imprescindible la existencia de un constructor explícito ( 4.11.2d3). Es decir, la versión correcta del programa seria: #include <iostream> using namespace std; class Punto { public: int coord[2]; Punto(int x = 0, int y = 0) { coord[0] = x; coord[1] = y; } }; // construtor explícito // inicializa int main() { // ================== Punto p1(10, 20); // L.8: Ok. cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl; } §8.1 La anterior no es por supuesto la única causa que hace necesaria la existencia de constructores explícitos. Más frecuente es el caso de que algunas de las variables de la clase deban ser persistentes. Por ejemplo: supongamos que en el caso anterior necesitamos que la matriz que almacena las coordenadas necesite este tipo de almacenamiento. En este caso, puesto que la utilización del especificador static aplicado a miembros de clase puede tener efectos colaterales indeseados ( 4.11.7), el único recurso es situar el almacenamiento en el montón ( 1.3.2), para lo que utilizamos el operador new ( 4.9.20) en un constructor definido al efecto. La definición de la clase tendría el siguiente aspecto: class Punto { public: int* coord; Punto(int x = 0, int y = defecto coord = new int[2]; coord[0] = x; coord[1] cout << "Creado punto; << coord[0] << "; } }; 0) { // construtor por // asigna espacio = y; // inicializa X == " Y == " << coord[1] << endl; Posteriormente se podrían instanciar objetos de la clase Punto mediante expresiones como: Punto p1; Punto p2(3, 4); argumentos Punto p3 = Punto(5, 6); argumentos Punto* ptr1 = new(Punto) argumentos Punto* ptr2 = new(Punto)(7, 8) argumentos // invocación implícita // invocación implícita con // invocación explícita con // invocación implícita sin // invocación implícita con §9 Orden de construcción Dentro de una clase los constructores de sus miembros son invocados antes que el constructor existente dentro del cuerpo de la propia clase. Esta invocación se realiza en el mismo orden en que se hayan declarado los elementos. A su vez, cuando una clase tiene más de una clase base (herencia múltiple 4.11.2c), los constructores de las clases base son invocados antes que el de la clase derivada y en el mismo orden que fueron declaradas. Por ejemplo en la inicialización: class Y {...} class X : public Y {...} X one; los constructores son llamados en el siguiente orden: Y(); X(); // constructor de la clase base // constructor de la clase derivada En caso de herencia múltiple: class X : public Y, public Z X one; los constructores de las clase-base son llamados primero y en el orden de declaración: Y(); Z(); X(); // constructor de la primer clase base // constructor de la segunda clase base // constructor de la clase derivada Nota: Al tratar de la destrucción de objetos ( 4.11.2d2), veremos que los destructores son invocados exactamente en orden inverso al de los constructores. §9.1 Los constructores de clases base virtuales ( 4.11.8a) son invocados antes que los de cualquier clase base no virtual. Si la jerarquía contiene múltiples clases base virtuales, sus constructores son invocados en el orden de sus declaraciones. A continuación de invocan los constructores del resto de las clase base, y por último el constructor de la clase derivada. §9.2 Si una clase virtual deriva de otra no virtual, primero se invoca el constructor de la clase base (no virtual), de forma que la virtual (derivada) pueda ser construida correctamente. Por ejemplo, el código: class X : public Y, virtual public Z X one; origina el siguiente orden de llamada en los constructores: Z(); Y(); X(); // constructor de la clase base virtual // constructor de la clase base no virtual // constructor de la clase derivada Un ejemplo más complicado: class base; class base2; class level1 : public base2, virtual public base; class level2 : public base2, virtual public base; class toplevel : public level1, virtual public level2; toplevel view; El orden de invocación de los constructores es el siguiente: base(); base2(); level2(); base2(); level1(); // // // // // // // clase virtual de jerarquía más alta base es construida solo una vez base no virtual de la base virtual level2 debe invocarse para construir level2 clase base virtual base no virtual de level1 otra base no virtual toplevel(); §9.3 Si una jerarquía de clases contiene múltiples instancias de una clase base virtual, dicha base virtual es construida solo una vez. Aunque si existen dos instancias de la clase base: virtual y no virtual, el constructor de la clase es invocado solo una vez para todas las instancias virtuales y después una vez para cada una de las instancias no virtuales. §9.4 En el caso de matrices de clases, los constructores son invocados en orden creciente de subíndices. §10 Los constructores y las funciones virtuales Debido a que los constructores de las clases-base son invocados antes que los de las clases derivadas, y a la propia naturaleza del mecanismo de invocación de funciones virtuales ( 4.11.8a), el mecanismo virtual está deshabilitado en los constructores, por lo que es peligroso incluir invocaciones a tales funciones en ellos, ya que podrían obtenerse resultados no esperados a primera vista. Considere los resultados del ejemplo siguiente, donde se observa que la versión de la función fun invocada no es la que cabría esperar en un funcionamiento normal del mecanismo virtual. #include <string> #include <iostream> using namespace std; class B { // superclase public: virtual void fun(const string& ss) { cout << "Funcion-base: " << ss << endl; } B(const string& ss) { // constructor de superclase cout << "Constructor-base\n"; fun(ss); } }; class D : public B { // clase derivada string s; // private por defecto public: void fun(const string& ss) { cout << "Funcion-derivada\n"; s = ss; } D(const string& ss) :B(ss) { // constructor de subclase cout << "Constructor-derivado\n"; } }; int main() { D d("Hola mundo"); constructor D } // ============= // invocación implícita a Salida: Constructor-base Funcion-base: Hola mundo Constructor-derivado Nota: La invocación de destructores ( 4.11.2d2) se realiza en orden inverso a los constructores: Las clases derivadas se destruyen antes que las clases-base [2]. Por esta razón el mecanismo virtual también está deshabilitado en los destructores (lo que no tiene nada que ver con que los destructores puedan ser en sí mismos funciones virtutales 4.11.2d2). Así pues, en la ejecución de un destructor solo se invocan las definiciones locales de las funciones implicadas. De lo contrario se correría el riesgo de referenciar la parte derivada del objeto que ya estaría destruida. §11 Constructores de conversión Normalmente a una clase con constructor de un solo parámetro puede asignársele un valor que concuerde con el tipo del parámetro. Este valor es automáticamente convertido de forma implícita en un objeto del tipo de la clase a la que se ha asignado. Por ejemplo: la definición: class X { public: X(); X(int); X(const char*, int = 0); }; // constructor C-1 // constructor C-2 // constructor C-3 en la que se han definido dos constructores que pueden ser utilizados con un solo argumento, permite que las siguientes asignaciones sean legales: void f() { X a; X b = X(); X c = X(1); X d(1); X e = X("Mexico"); X f("Mexico"); X g = 1; X h = "Madrid"; a = 2; } // // // // // // // // // Ok invocado C-1 Ok idem. Ok invocado C-2 Ok igual que el anterior Ok invocado C-3 Ok igual que el anterior L.1 Ok. L.2 Ok. L.3 Ok. La explicación de las tres últimas sentencias es la siguiente: En L.1, el compilador intenta convertir el Rvalue (que aquí es una constante numérica entera de valor 1) en el tipo del Lvalue, que aquí es la declaración de un nuevo objeto (una instancia de la clase). Como necesita crear un nuevo objeto, utilizará un constructor, de forma que busca si hay uno adecuado en X que acepte como argumento el tipo situado a la derecha. El resultado es que el compilador supone un constructor implícito a la derecha de L.1: X a = X(1); // interpretación del compilador para L.1 El proceso se repite en la sentencia L.2 que es equivalentes a: X B = X("Madrid"); // L.2bis La situación en L.3 es completamente distinta, ya que en este caso ambos operandos son objetos ya construidos. Para poder realizar la asignación, el compilador intenta convertir el tipo del Rvalue al tipo del Lvalue, para lo cual, el mecanismo de conversión de tipos busca si existe un constructor adecuado en X que acepte el operando derecho. Caso de existir se creará un objeto temporal tipoX que será utilizado como Rvalue de la asignación. La asignación propiamente dicha es realizada por el operador correspondiente (explícito o implícito) de X. La página adjunta incluye un ejemplo que muestra gráficamente el proceso seguido ( Ejemplo) Este tipo de conversión automática se realiza solo con constructores que aceptan un argumento o que son asimilables (como C-2), y suponen una conversión del tipo utilizado como argumento al tipo de la clase. Por esta razón son denominadas conversiones mediante constructor, y a este tipo de constructores constructores de conversión ("Converting constructor"). Su sola presencia habilita no solo la conversión implícita, también la explícita. Ejemplo: class X { public: X(int); }; // constructor C-2 la mera existencia del constructor C-2 en la clase X, permite las siguientes asignaciones: void f() { X a = X(1) constructor X a = 1; a = 2; a = (X) 2; tradicional) a = static_cast<X>(2); C++) } // L1: Ok. invocación explícita al // Ok. invocación implícita X(1) // Ok. invocación implícita X(2) // Ok. casting explícito (estlo // Ok. casting explícito (estilo Si eliminamos el constructor C-2 de la declaración de la clase, todas estas sentencias serían erróneas. Observe que en L1 cabría hacerse una pregunta: ¿Se trata de la invocación del constructor o un modelado explícito al estilo tradicional?. La respuesta es que se trata de una invocación al constructor, y que precisamente el modelado (explícito o implícito) se apoya en la existencia de este tipo de constructores para realizar su trabajo. Temas relacionados: Operadores de conversión ( 4.9.18k) Conversión automática a tipos simples ( 4.13.6) §12 Constructor explicit El problema es que en ocasiones el comportamiento descrito en el epígrafe anterior puede resultar indeseable y enmascarar errores. Es posible evitarlo declarando el constructor de la clase con la palabra clave explicit, dando lugar a los denominados constructores explicit [3]. En estos casos, los objetos de la clase solo podrán recibir asignaciones de objetos del tipo exacto. Cualquier otra asignación provocará un error de compilación. §12.1 La sintaxis de utilización es: explicit <declaración de constructor de un solo parámetro> Aplicándolo al ejemplo anterior: class X { public: explicit X(int); // constructor C-2b explicit X(const char*, int = 0); // constructor C-3b }; ... void f() { X a = 1; // L.1 Error!! X B = "Madrid"; // L.2 Error!! a = 2; // L.3 Error!! } Ahora los objetos de la clase X, dotada con constructores implicit, solo pueden recibir asignaciones de objetos del mismo tipo: void f() { X a = X(1); X b = X("Madrid", 0); a = (X) 2; } // L.1 Ok. // L.2 Ok. // L.3 Ok. En L.3 se ha utilizado una conversión de tipos ("Casting") explícita ( 4.9.9). Para realizarla el mecanismo de conversión busca si en la clase X existe un constructor que acepte como argumento el tipo de la derecha, con lo que estaríamos en el caso de L.1. §13 Constructores privados y protegidos Cuando los constructores no son públicos (son privados o protegidos 4.11.2b-1), no pueden ser accedidos desde el exterior, por lo que no pueden ser invocados explícita ni implícitamente al modo tradicional (§4 ). Ejemplo: class C { int x; C(int n=0): x(n) {} // privado por defecto }; ... void foo() { C c(1); // Error!! cannot access private member } Además, como los clases derivadas necesitan invocar los constructores de las superclases para instanciar sus objetos, caso de no ser protegidos o públicos también pueden existir limitaciones para su creación. Ejemplo: class B { int x; B (): x(10) {} }; class D : public B { ... }; void foo() { D d; // Error!! no appropriate default constructor available ... } Puesto que los miembros private o protected no pueden ser accedidos desde el exterior de la clase, este tipo de constructores se suelen utilizar siempre a través de funciones-miembro públicas con objeto de garantizar cierto control sobre los objetos creados. El esquema de utilización sería el siguiente: class C { C(int n) { /* constructor privado */ } public: static C makeC(int m) { ... return C(m); } ... }; void foo() { C c = C::makeC(1); } // Ok. Observe que makeC() es estática para que pueda ser invocada con independencia de la existencia de cualquier objeto ( 4.11.7). Observe también que mientras una expresión como: C c = C(1); es una invocación al constructor. En cambio la sentencia C c = C::makeC(1); es una invocación al operador de asignación ya que la función devuelve un objeto que será tomado como Rvalue de la asignación. Esta técnica, que utiliza constructores privados o protegidos junto con métodos públicos para accederlos, puede prevenir algunas conversiones de tipo no deseadas, pudiendo constituir una alternativa al uso de constructores explicit (§12 ). http://www.zator.com/Cpp/E4_11_2d1.htm :: CONSTRUCTORES :: Cuando una clase base tiene un constructor y una clase derivada también, al crear el objeto, se llama primero al constructor de la clase base, y cuando la ejecución de éste termina, se llama al constructor de la clase derivada. Gracias a que el constructor de la clase base es llamado, es posible inicializar la clase base, desde el constructor de la clase derivada. Esto se logra pasando una lista de los constructores de las clases base, con sus respectivos parametros. class base { //Cuerpo de la clase base }; class derivada :[public/private/..] base { //Cuerpo de la clase derivada }; :: DESTRUCTORES :: En el caso de los destructores, el orden en que son llamados, es inverso al de los constructores, es decir es llamado primero el destructor de la clase derivada, después son llamados los destructores de las clases miembro y, luego el destructor de la clase base. Como los destructores no requieren argumentos, no hay que usar una sintaxis especial. :: EJEMPLO :: En la sintaxis para la declaración de constructores para clases derivadas, encontramos después de los parametros del constructor de la clase derivada, en este caso la clase CBiblioteca, dos puntos y un llamado al constructor de la clase base con los parametros que se requieran. //Clase mueble class CMueble { public: //Constructor con parametros CMueble(int Alto,int Ancho); //Declaracion de la clase }; class CBiblioteca :public CMueble { //Cuerpo de la clase derivada public: CBiblioteca(int Alto,int Ancho); }; //Constructor clase derivada CBiblioteca::CBiblioteca(int Alto,int Ancho): CMueble(Alto,Ancho) { } http://ieee.udistrital.edu.co/concurso/programacion_orientada_objetos/poo/herenci1.html 5.18 Aplicaciones. Unidad 6. Polimorfismo y reutilización 6.9 Concepto del polimorfismo. POLIMORFISMO Y REUTILIZACIÓN. Otro concepto interesante, con Importantes aportaciones en áreas tales como la flexibilidad o la legibilidad del software, es el de polimorfismo. Tras este termino, un tanto oscuro, subyace una idea bastante simple. En su más amplia expresión, el polimorfismo puede definirse como: "el mecanismo que permite definir e Invocar funciones idénticas en denominación e interfaz, pero con implementaron diferente". Esta definición introduce un aspecto muy importante del polimorfismo: la asociación, o vinculo, entre cada llamada a una de estas funciones polimorfismo y la implementación concreta finalmente invocada. Cuando este vinculo puede establecerse en tiempo de compilación, se suele hablar de vinculación estatica (static binding). Por contra, cuando la implementación a emplear, s610 puede determinarse en tiempo de ejecución, el termino empleado es el de vinculación dinámica (dynamic binding). En C++, por ejemplo, la vinculación dinámica de las llamadas a funciones polimórficas (en C++ reciben el calificativo de funciones virtuales) se consigue en base a la posibilidad que ofrece este lenguaje de utilizar un puntero a objetos de una clase como puntero a objetos de cualquiera de las clases descendientes de la anterior. Así, cuando la llamada a una función virtual, definida en una clase y en una o varias de sus descendientes, se realiza sobre un objeto que viene referenciado mediante un puntero a la clase padre, el compilador es incapaz de determinar que implementación debe asociar a la llamada, ya que desconoce cual será la clase del objeto en el momento de su ejecución. Dicha determinación debe quedar aplazada, por tanto, hasta ese instante. Como se puede observar, el concepto de polimorfismo en C++, y en general en casi todos los lenguajes de programación basados en el paradigma de objeto, esta estrechamente ligado al concepto de herencia, dado que las funciones polimórficas sólo pueden definirse entre clases que guardan entre sí una relación de parentesco (clases con un antecesor común). Aunque el concepto de polimorfismo es una de las principales innovaciones del desarrollo orientado a objetos, posee antecedentes históricos en otros mecanismos más sencillos, como son la conversión forzada (casting) y la sobrecarga de identificadores, ideados con el fin de introducir un cierto grado de flexibilidad en el manejo de tipos de datos heterogéneos. En el siguiente apartado se desarrolla un pequeño ejemplo de aplicación de ambos conceptos, en el que quedan claramente de manifiesto las ventajas potenciales de una correcta utilización de los mismos. Para su implementación se ha elegido el lenguaje C ++, fundamentalmente por dos razones: La amplia difusión que esta adquiriendo este lenguaje en los ambientes de desarrollo. La enorme similitud de su sintaxis con la del lenguaje C, del cual deriva http://www.tid.es/presencia/publicaciones/comsid/esp/articulos/vol23/herencia/herencia.html Polimorfismo: En un sentido literal, Polimorfismo significa la cualidad de tener más de una forma. En el contexto de POO, el Polimorfismo se refiere al hecho de que una simple operación puede tener diferente comportamiento en diferentes objetos. En otras palabras, diferentes objetos reaccionan al mismo mensaje de modo diferente. Los primeros lenguajes de POO fueron interpretados, de forma que el Polimorfismo se contemplaba en tiempo de ejecución. Por ejemplo, en C++, al ser un lenguaje compilado, el Polimorfismo se admite tanto en tiempo de ejecución como en tiempo de compilación Decimos entonces que: El tema de la Programación Orientada a Objetos (Object Oriented Programming O-O-P) sigue siendo para el que escribe un territorio inquietante, interesante y en gran medida desconocido, como parece ser también para la gran mayoría de los que estamos en el campo de la programación. Sin tratar de excluir a aquellos que han afrontado este desarrollo desde el punto de vista académico y formal (maestrías y doctorados) el tema se antoja difícil para los no iniciados. Con este breve artículo me dirigiré en particular a la gran base de programadores prácticos que andamos en búsqueda de mejores herramientas de desarrollo de programas, que faciliten el trabajo de nuestros usuarios y a la vez disminuyan la gran cantidad de considerandos que aparecen al empeñarnos en un proyecto de cómputo. Como muchos de ustedes, me topé con el concepto de O-O-P como parte de esa búsqueda y al explorarlo apareció el gusanillo de la curiosidad. A lo largo de mi actividad como programador, y cuando se dio la necesidad, no tuve ningún problema en convertir mis habilidades de programación en FORTRAN de IBM 1130 al BASIC de la PDP, pues sólo era cuestión de aprender la sintaxis del lenguaje, ya que las estrategias de programación y los algoritmos eran iguales. Posteriormente, al manejar el PASCAL se requirió un importante esfuerzo en entender la filosofía de las estructuras, lo cual modificaba la manera de ver (conceptualizar) a los datos y a las partes constitutivas de un programa. Posteriormente aparece el QuickBasic, que adopté inmediatamente por la familiaridad con el BASIC (ley del menor esfuerzo). Ofrecía estructuras de datos (tipos y registros complejos), además de estructuras de instrucciones en procedimientos y módulos; editor "inteligente" que revisa la sintaxis y ejecución de las instrucciones mientras se edita el programa, generación de ejecutable una vez terminado (.EXE), existencia de bibliotecas externas y enlace con módulos objeto generados en otro lenguaje. ¿Qué más podía yo pedir? Pero la necesidad de estar en la ola de moda es más fuerte que el sentido común. Las aplicaciones en Windows siempre han despertado la envidia de los programadores, al hacer ver sus programas pálidos e insulsos por comparación. Solución: programar en Windows. Originalmente programar en Windows representaba un largo y tedioso camino para dominar las complejas herramientas de desarrollo. Sólo recientemente han aparecido desarrolladores de aplicaciones para Windows que le permiten al programador pintar sus ventanas y realizar los enlaces entre los objetos con programación tradicional, evitando en gran medida involucrarse con los conceptos complicados de los objetos. Sin embargo no dejaron de inquietarme algunos conceptos marcados por O-O-P, según los cuales serán los pilares del futuro de la programación de componentes y de objetos distribuidos en redes, en donde la actual programación cliente/servidor pareciera por comparación el FORTRAN o el COBOL de ahora. Pidiendo perdón de antemano a los puristas de las definiciones y conceptos de O-O-P, expondré el resultado de mis propias indagaciones sobre este campo, esperando que al paciente lector y posible programador le resulte menos complicado que a mí asimilar los elementos básicos de O-O-P. Los principales conceptos que se manejan en la Programación Orientada a Objetos son: 1. encapsulado, 2. herencia y 3. Polimorfismo. Según esto, la encapsulación es la creación de módulos autosuficientes que contienen los datos y las funciones que manipulan dichos datos. Se aplica la idea de la caja negra y un letrero de "prohibido mirar adentro". Los objetos se comunican entre sí intercambiando mensajes. De esta manera, para armar aplicaciones se utilizan los objetos cuyo funcionamiento está perfectamente definido a través de los mensajes que es capaz de recibir o mandar. Todo lo que un objeto puede hacer está representado por su interfase de mensajes. Para crear objetos, el programador puede recurrir a diversos lenguajes como el C++, el Smalltalk, el Visual Objects y otros. Si se desea solamente utilizar los objetos y enlazarlos en una aplicación por medio de la programación tradicional se puede recurrir al Visual Basic, al CA-Realizer, al Power Builder, etc. El concepto de herencia me pareció sencillo de entender una vez que capté otro concepto de O-O-P: las clases. En O-O-P se acostumbra agrupar a los objetos en clases. Esto es muy común en la vida diaria. Todos nosotros tendemos a clasificar los objetos comunes por clases. Manejamos la clase mueble, la clase mascota, la clase alimento, etc. Obviamente en el campo de la programación esta clasificación es más estricta. ¿Cuál es el sentido de las clases? Fundamentalmente evitar definir los objetos desde cero y facilitar su rehuso. Si trabajamos con clases, al querer definir un nuevo objeto, partimos de alguna clase definida anteriormente, con lo que el objeto en cuestión hereda las características de los objetos de su clase. Imaginemos que creamos una clase "aves" y describimos las características de las aves (plumas, pico, nacen de huevo, etc.). Más adelante necesitamos una clase "pingüino". Como pertenece a "aves" no requerimos volver a declarar lo descrito sino marcamos que "pingüino" es una subclase de "aves" con lo que "pingüino" hereda todas sus características. A continuación sólo declaramos los detalles que determinan lo que distingue a "pingüino" de "aves". Asimismo podemos declarar "emperador" como una subclase de "pingüino", con lo que "emperador" heredará todas las características de las superclases "pingüino" y "aves" más las características que nosotros declaremos en particular para "emperador". En un programa (imaginario por supuesto) yo puedo utilizar estas clases (aves, pingüino y emperador). El hecho de colocar a un individuo en particular en estas clases es lo que se llama objeto y se dice que es una instancia de una clase. Así, si yo coloco a Fredy (un pingüino emperador) en mi programa, se dice que el objeto Fredy es una instancia de la clase emperador. Fredy aparecerá en mi programa con todas las características (herencia) de aves, de pingüino y de emperador. Por otra parte, entender el concepto de Polimorfismo implicó un buen número de horas de indagación y búsqueda de ejemplos. Espero que éste resulte claro: supóngase que declaramos un objeto llamado Suma. Este objeto requiere dos parámetros (o datos) como mensaje para operar. En la programación tradicional tendríamos que definir el tipo de datos que le enviamos, como por ejemplo dos números enteros, dos números reales, etc. En O-O-P el tipo de dato se conoce hasta que se ejecuta el programa. http://www.monografias.com/trabajos/refercomp/refercomp.shtml Funciones virtuales: polimorfismo. En esta ocasión vamos abordar un concepto muy importante de la programación orientada a objetos: el polimorfismo. Esta característica de la POO, permite que podamos construirnos métodos para nuestras clase derivadas que parten de una misma clase base, para que adopten comportamientos totalmente distintos. Es un concepto realmente potente y que se lleva a cabo mediante la utilización de funciones virtuales. Nosotros ya las hemos utilizado. Si te acuerdas, en el capítulo anterior declarábamos la clase que redefiníamos como virtual. ¡Hemos utilizado el polimorfismo y sin enterarnos!. Una función virtual es un mecanismo que permite a clases derivadas redefinir a las funciones de las clases base. Por tanto, hasta ahora, nos debemos de quedar con que el polimorfismo es una acción que se puede implementar en clases distintas pero que tienen en común el hecho de heredar de una clase base común. Dicho método, pese a ser común para los objetos derivados, es tratado de forma distinta y, por tanto, dando resultados también diferentes dependiendo de con qué clase lo invoquemos. ¿Cómo conseguirlo?. Para poder implementar el polimorfismo tenemos las funciones virtuales. Las funciones virtuales se definen en la clase base y son las que serán redefinidas, luego, en las clases derivadas. La declaración de una función virtual se consigue mediante la palabra clave virtual precediendo a la declaración de la función. Por ejemplo: virtual void PonInformacion(void); En este caso, hemos definido una función virtual llamada PonInformacion que pertenece a una clase base y que podrá ser redefinida, completamente, en una clase derivada para que actúe de forma totalmente distinta a cómo lo hace en la clase base. Es muy importante que la función de la clase base que vamos a redefinir lleve el identificador virtual pues de lo contrario, la cosa no funcionará como es de esperar. Veremos estos problemas más adelante. A continuación vamos a poner un ejemplo en el que, utilizando la función de arriba, vamos a ver cómo podemos hacer para que una clase que herede dicha función y produzca resultados totalmente distintos a los que se han definido en la clase base para ella. Así mismo, utilizamos new y delete para refrescar la memoria (y nunca mejor dicho :-). Recordad que con new y delete podemos crear objetos (o variables) dinámicamente y de forma muy sencilla. #include <iostream.h> class ClaseBase { // Esta es la clase base. // Definimos una función de miembro para la clase base // que además es virtual e "inline" public: vitual void PonInformacion(void) { cout << "\n¡Hola!"; } }; class ClaseDerivada : public ClaseBase { // // // // Hemos definido una clase derivada de la clase ClaseBase. Esta clase, va a utilizar el concepto de polimorfismo redefiniendo por completo la función PonInformacion que hereda de la clase ClaseBase. También es "inline". public: void PonInformacion(void) { cout << "\n¡Adios!"; } }; int main(void) { ClaseBase *base = new ClaseBase; ClaseDerivada *derivada = new ClaseDerivada; // Ahora llamamos a los métodos de cada una para observar // que el resultado es distinto. base->PonInformacion(); derivada->PonInformacion(); delete base; delete derivada; return 0; } Bueno, el resultado está claro, ¿no?. Mientras que la clase declarada como ClaseBase e instanciada con base, pone en pantalla "¡Hola!", la clase derivada, que ha redefinido la función virtual PonInformacion, pone "¡Adiós!". Es un ejemplo, en definitiva, muy sencillo en el que se puede ver cómo hacer que una función ya definida en una clase base cambie totalmente según nuestras necesidades: es el polimorfismo. Otra de las cosas que vimos en el capítulo anterior era el hecho de utilizar dentro de la función de la clase derivada, a la función virtual de la clase base. Si nosotros hubiéramos implementado así a la función virtual de la clase derivada: void PonInformacion(void) { // Suponemos que la declaración es inline ya que si no fuera // inline deberíamos de poner // void ClaseDerivada::PonInformacion(void) ClaseBase::PonInformacion(); cout << "\n¡Adios!"; } Cuando llamáramos a la función PonInformacion() por medio de la clase derivada, esto es, cuando hiciéramos: derivada->PonInformacion(); Lo que nos saldría por pantalla sería: ¡Hola! ¡Adios! En contraposición a lo que nos sale en el programa original en el que no llamamos a la función de la clase base, es decir, en el ejemplo original saldría nada más ¡Adiós!. Esto es así porque dentro del cuerpo PonInformacion() que está implementado en la clase ClaseDerivada, llamamos antes de nada a la función de la clase base ClaseBase que se trata como una función heredada más. Aclarando todo un poco Si recuerdas el capítulo anterior del curso, habrás observado que, en cierta forma, ya hemos utilizado el polimorfismo, es decir, en el anterior capítulo redefiníamos funciones miembro de una clase base utilizando sus características comunes a la clase derivada en la que trabajábamos pero añadiendo una serie de especificaciones extra para que "además se hiciera otra cosa". Hasta ahora hemos tratado funciones que son virtuales simples. Recordemos que una función virtual es aquella que, habiéndose declarado en una clase base, vuelve a declararse y a implementarse en una clase derivada, es decir, se redefine en la clase derivada. De esta forma, cuando el objeto instanciado a una clase derivada, llama a esa función virtual, lo que se hace es llamar a la función de la clase derivada no a la función de la clase base (a no ser que, dentro de la función de la clase derivada, se llame a la función de la clase base). El ejemplo clásico de utilización de funciones virtuales está en el de crear una clase base llamada Forma con una función de miembro para dibujar y de nombre Dibujar. Si nosotros creamos dos clases derivadas de la clase base Forma llamadas Circulo y Rectangulo, respectivamente, y redefinimos la función virtual Dibujar en cada una de las clases base, cuando la llamemos desde cada una de las instancias a la función Dibujar, el compilador sabrá a qué función llamar. Cabe decir también, y esto es importante pues puede dar lugar a confusiones, que si tenemos una función en una clase base que no esté marcada como virtual y después creamos en una clase derivada otra función con el mismo nombre los resultados no van a ser los esperados... Lo que hará el compilador será llamar directamente a la función implementada en la clase base y pasar "olímpicamente" de la implementación de la clase derivada. ¿y si, habiendo declarado una función en la clase base como virtual, luego se nos olvida redefinirla en la clase derivada?. Bueno, en este caso, se llamará a la función de la clase base y no pasará nada. Polimorfismo Otro concepto de la OOP es el polimorfismo. Un objeto solamente tiene una forma (la que se le asigna cuando se construye ese objeto) pero la referencia a objeto es polimórfica porque puede referirse a objetos de diferentes clases (es decir, la referencia toma múltiples formas). Para que esto sea posible debe haber una relación de herencia entre esas clases. Por ejemplo, considerando la figura anterior de herencia se tiene que: o o o o o Una referencia a un objeto a un objeto de la clase A. Una referencia a un objeto a un objeto de la clase A. Una referencia a un objeto a un objeto de la clase A. Una referencia a un objeto a un objeto de la clase D. Una referencia a un objeto a un objeto de la clase A. de la clase B también puede ser una referencia de la clase C también puede ser una referencia de la clase D también puede ser una referencia de la clase E también puede ser una referencia de la clase E también puede ser una referencia http://www.fi-b.unam.mx/pp/profesores/carlos/java/java_basico3_5.html 6.10 Clases abstractas. 6.10.1 Definición. Funciones virtuales puras implican clases abstractas. Se puede decir que las funciones virtuales puras son aquellas que, para implementarse, han de ser redefinidas, es decir, que no sólo tienen sino que deben ser redefinidas. Aquellas clases que tengan una función virtual pura se denominan clases abstractas y tienen la gran particularidad de que de ellas no se puede crear instancias u objetos. Quedamos, pues, en que para crear una clase abstracta sólo hace falta definir una función virtual pura. ¿Y cómo definimos una función virtual pura?. Para definir una función virtual pura, tenemos que asignar a la función un puntero NULL o, lo que es lo mismo, un valor 0. Suponiendo que queremos implementar una función llamada Forma como virtual pura, deberíamos de poner así: virtual void Forma() = 0; La clase que contenga una sola función virtual pura pasa a ser una clase abstracta independientemente de que el resto de sus funciones no sean virtuales puras. Ya veis que fácil es crear una función virtual pura y que poder tienen al implicar también la creación de una clase abstracta que, recordad, no puede ser nunca instanciada. Las funciones virtuales puras han de ser definidas cuando queramos crear una verdadera clase raíz de una serie de clases derivadas ciertamente importante. No conviene abusar de esta característica del C++ a no ser que vuestro diseño lo necesite necesariamente y estéis delante de un proyecto con una complejidad notable. http://usuarios.lycos.es/macedoniamagazine/poo6.htm Clases abstractas §1 Sinopsis La abstracción es un recurso de la mente (quizás el más característico de nuestra pretendida superioridad respecto del mundo animal). Por su parte los lenguajes de programación permiten expresar la solución de un problema de forma comprensible simultáneamente por la máquina y el humano. Son como un puente entre la abstracción de la mente y una serie de instrucciones ejecutables por un dispositivo electrónico. En consecuencia, la capacidad de abstracción es una característica deseable de los lenguajes artificiales, pues cuanto mayor sea, mayor será su aproximación al lado humano, es decir, con la imagen existente en la mente del programador. En este sentido, la introducción de las clases en los lenguajes orientados a objetos ha representado un importante avance respecto de la programación tradicional y dentro de ellas, las denominadas clases abstractas son las que representan el mayor grado de abstracción. De hecho, las clases abstractas presentan un nivel de "abstracción" tan elevado que no sirven para instanciar objetos de ellas. Representan los escalones más elevados de algunas jerarquías de clases y solo sirven para derivar otras clases, en las que se van implementando detalles y concreciones, hasta que finalmente presentan un nivel de definición suficiente para poder instanciar objetos concretos. Se suelen utilizar en aquellos casos en que se quiere que una serie de clases mantengan una cierta característica o interfaz común. Por esta razón a veces se dice de ellas que son "pura interfaz". Resulta evidente en el ejemplo de la figura que los diversos tipos de motores tienen características diferentes. Realmente tienen poco en común un motor eléctrico de corriente alterna y una turbina de vapor. Sin embargo, la construcción de una jerarquía en la que todos motores desciendan de un ancestro común, la clase abstracta "Motores", presenta la ventaja de unificar la interfaz. Aunque evidentemente su definición será tan "abstracta", que no pueda ser utilizada para instanciar directamente ningún tipo de motor. El creador del lenguaje dice de ellas que soportan la noción de un concepto general del que solo pueden utilizarse variantes más concretas [2]. §2 Clases abstractas Una clase abstracta es la que tiene al menos una función virtual pura (como hemos visto, una función virtual es especificada como "pura" haciéndola igual a cero 4.11.8a). Nota: Recordemos que las clases que tienen al menos una función virtual (o virtual pura) se denominan clases polimórficas ( 4.11.8). Resulta por tanto que todas las clases abstractas son también polimórficas, pero no necesariamente a la inversa. §3 Reglas de uso: Una clase abstracta solo puede ser usada como clase base para otras clases, pero no puede ser instanciada para crear un objeto 1. Una clase abstracta no puede ser utilizada como argumento o como retorno de una función 2. Si puede declararse punteros-a-clase abstracta 3 [1]. Se permiten referencias-a-clase abstracta, suponiendo que el objeto temporal no es necesario en la inicialización 4. §4 Ejemplo: class Figura { // clase abstracta (C.A.) point centro; ... public: getcentro() { return center; } mover(point p) { centro = p; dibujar(); } virtual void rotar(int) = 0; // función virtual pura virtual void dibujar() = 0; // función virtual pura virtual void brillo() = 0; // función virtual pura ... }; ... Figura x; // ERROR: intento de instanciar una C.A.1 Figura* sptr; // Ok: puntero a C.A. 3 Figura f(); // ERROR: función NO puede devolver tipo C.A. 2 int g(Figura s); // ERROR: C.A. NO puede ser argumento de función 2 Figura& h(Figura&); // Ok: devuelve tipo "referenciaa-C.A." 4 int h(Figura&); // Ok: "referencia-a-C.A." si puede ser argumento 4 §5 Suponiendo que A sea una clase abstracta y que D sea una clase derivada inmediata de ella, cada función virtual pura fvp de A, para la que D no aporte una definición, se convierte en función virtual pura para D, en cuyo caso D resulta ser también una clase abstracta. Por ejemplo, suponiendo la clase Figura definida previamente: class Circulo : public Figura { // Circulo deriva de una C.A. int radio; // privado por defecto public: void rotar(int); // convierte rotar en función no virtual }; En esta clase el método heredado Circulo::dibujar() es una función virtual pura. Sin embargo, Circulo::rotar() no lo es (suponemos que definición se efectúa off-line). En consecuencia, Circulo es también una clase abstracta. En cambio si hacemos: class Circulo : public Figura { // Circulo deriva de una C.A. int radio; public: void rotar(int); // convierte rotar en función no virtual pura void dibujar(); // convierte dibujar en función no virtual pura }; la clase Circulo deja de ser abstracta. §6 Las funciones-miembro pueden ser llamadas desde el constructor de una clase abstracta, pero la llamada directa o indirecta de una función virtual pura desde tal constructor puede provocar un error en tiempo de ejecución. Sin embargo, son permitidas disposiciones como la siguiente: class CA { // clase abstracta public: virtual void foo() = 0; // foo virtual pura CA() { // constructor CA::foo(); // Ok. }; ... void CA::foo() { // definición en algún sitio ... } La razón es la ya señalada ( 4.11.8a) de que la utilización del operador :: de acceso a ámbito anula el mecanismo de funciones virtuales. [1] Precisamente, la invocación de métodos de clases derivadas mediante punteros a la superclase, es una de las características esenciales de la tecnología COM de Microsoft. [2] Stroustrup & Ellis: ACRM §10.3. http://www.zator.com/Cpp/E4_11_8c.htm Clases Abstractas A diferencia de lo visto con las clases visuales y las clases no visuales, las clases abstractas son clases que sólo pueden tener subclases (no pueden ser instanciadas directamente). 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 una interfaz de programación completa, incluso proporciona a sus subclases la declaración de todos los métodos necesarios para implementar el interfaz de programación. Sin embargo, las clases abstractas pueden dejar algunos detalles o toda la implementación de aquellos métodos a sus subclases. http://www.microsoft.com/spanish/msdn/comunidad/dce/1/entrenamiento/foxpro/2b.asp Clases abstractas Una clase que declara la existencia de métodos pero no la implementación de dichos métodos (o sea, las llaves { } y las sentencias entre ellas), se considera una clase abstracta. Una clase abstracta puede contener métodos no-abstractos pero al menos uno de los métodos debe ser declarado abstracto. Para declarar una clase o un metodo como abstractos, se utiliza la palabra reservada abstract. abstract class Drawing { abstract void miMetodo(int var1, int var2); String miOtroMetodo( ){ ... } } Una clase abstracta no se puede instanciar pero si se puede heredar y las clases hijas serán las encargadas de agregar la funcionalidad a los métodos abstractos. Si no lo hacen así, las clases hijas deben ser también abstractas. http://www.fib.unam.mx/pp/profesores/carlos/java/java_basico4_8.html 6.10.2 Redefinición. 6.11 Definición de una interfaz. Interfaces Son sintácticamente como las clases, pero no tienen variables de instancia y los métodos declarados no contienen cuerpo. Se utilizan para especificar lo que debe hacer una clase, pero no cómo lo hace. Una clase puede implementar cualquier número de interfaces. 1 Definición de una interfaz Una interfaz se define casi igual que una clase: acceso interface nombre { tipo_devuelto m鴯do1(lista_de_par᭥tros); tipo_devuelto m鴯do2(lista_de_par᭥tros); tipo var_final1=valor; tipo var_final2=valor; // ... tipo_devuelto m鴯doN(lista_de_par᭥tros); tipo var_finalN=valor; } acceso puede ser public o no usarse. o Si no se utiliza (acceso por defecto) la interfaz está sólo disponible para otros miembros del paquete en el que ha sido declarada. o Si se usa public, puede ser usada por cualquier código (todos los métodos y variables son implícitamente públicos). Los métodos de una interfaz son básicamente métodos abstractos (no tienen cuerpo de implementación). Un interfaz puede tener variables pero serán implícitamente final y static. Ejemplo definición de interfaz interface Callback { void callback(int param); } http://leo.ugr.es/~fjgc/CLDC/transjava/node9.html#SECTION000940000000000000000 6.12 Implementación de la definición de una interfaz. Implementación de una interfaz Para implementar una interfaz, la clase debe implementar todos los métodos de la interfaz. Se usa la palabra reservada implements. Una vez declarada la interfaz, puede ser implementada por varias clases. Cuando implementemos un método de una interfaz, tiene que declararse como public. Si una clase implementa dos interfaces que declaran el mismo método, entonces los clientes de cada interfaz usarán el mismo método. Ejemplo de uso de interfaces: P28 class Client implements Callback { public void callback(int p) { System.out.println("callback llamado con " + p); } void nonIfaceMeth() { System.out.println("Las clases que implementan interfaces " + "adem᭥pueden definir otros miembros."); } } class TestIface { public static void main(String args[]) { Callback c = new Client(); c.callback(42); } } 3 Acceso a implementaciones a través de referencias de la interfaz Se pueden declarar variables usando como tipo un interfaz, para referenciar objetos de clases que implementan ese interfaz. El método al que se llamará con una variable así, se determina en tiempo de ejecución. Ejemplo class TestIface { public static void main(String args[]) { Callback c = new Client(); c.callback(42); } } Con estas variables sólo se pueden usar los métodos que hay en la interfaz. Otro ejemplo: P29 // Otra implementaci᭥e Callback. class AnotherClient implements Callback { public void callback(int p) { System.out.println("Otra versi᭥e callback"); System.out.println("El cuadrado de p es " + (p*p)); } } class TestIface2 { public static void main(String args[]) { Callback c = new Client(); AnotherClient ob = new AnotherClient(); c.callback(42); c = ob; // c hace referencia a un objeto AnotherClient c.callback(42); } } 4 Implementación parcial Si una clase incluye una interfaz, pero no implementa todos sus métodos, entonces debe ser declarada como abstract. 5 Variables en interfaces Las variables se usan para importar constantes compartidas en múltiples clases. Si una interfaz no tiene ningún método, entonces cualquier clase que incluya esta interfaz no tendrá que implementar nada. Ejemplo: P30/AskMe.java import java.util.Random; interface SharedConstants { int NO = 0; int YES = 1; int MAYBE = 2; int LATER = 3; int SOON = 4; int NEVER = 5; } class Question implements SharedConstants { Random rand = new Random(); int ask() { int prob = (int) (100 * rand.nextDouble()); if (prob < 30) return NO; // 30% else if (prob < 60) return YES; // 30% else if (prob < 75) return LATER; // 15% else if (prob < 98) return SOON; // 13% else return NEVER; // 2% } } class AskMe implements SharedConstants { static void answer(int result) { switch(result) { case NO: System.out.println("No"); break; case YES: System.out.println("Si"); break; case MAYBE: System.out.println("Puede ser"); break; case LATER: System.out.println("Mas tarde"); break; case SOON: System.out.println("Pronto"); break; case NEVER: System.out.println("Nunca"); break; } } public static void main(String args[]) { Question q = new Question(); answer(q.ask()); answer(q.ask()); answer(q.ask()); answer(q.ask()); } } 6 Las interfaces se pueden extender Una interfaz puede heredar otra utilizando la palabra reservada extends. Una clase que implemente una interfaz que herede de otra, debe implementar todos los métodos de la cadena de herencia. Ejemplo: P31/IFExtend.java interface A { void meth1(); void meth2(); } interface B extends A { void meth3(); } class MyClass implements B { public void meth1() { System.out.println("Implemento meth1()."); } public void meth2() { System.out.println("Implemento meth2()."); } public void meth3() { System.out.println("Implemento meth3()."); } } class IFExtend { public static void main(String arg[]) { MyClass ob = new MyClass(); ob.meth1(); ob.meth2(); ob.meth3(); } } 6.13 Reutilización de la definición de una interfaz. 6.14 Definición y creación de paquetes / librería. Paquetes Un paquete es un contenedor de clases, que se usa para mantener el espacio de nombres de clase, dividido en compartimentos. Se almacenan de manera jerárquica: Java usa los directorios del sistema de archivos para almacenar los paquetes Ejemplo: Las clases del paquete MiPaquete (ficheros .class y .java) se almacenarán en directorio MiPaquete Se importan explícitamente en las definiciones de nuevas clases con la sentencia import nombre-paquete; Permiten restringir la visibilidad de las clases que contiene: Se pueden definir clases en un paquete sin que el mundo exterior sepa que están allí. Se pueden definir miembros de una clase, que sean sólo accesibles por miembros del mismo paquete 1 Definición de un paquete Incluiremos la siguiente sentencia como primera sentencia del archivo fuente package nombre-paquete .java Todas las clases de ese archivo serán de ese paquete Si no ponemos esta sentencia, las clases pertenecen al paquete por defecto Una misma sentencia package puede incluirse en varios archivos fuente Se puede crear una jerarquía de paquetes: package paq1[.paq2[.paq3]]; Ejemplo: java.awt.image 2 La variable de entorno CLASSPATH Supongamos que construimos la clase PaquetePrueba perteneciente al paquete prueba, dentro del fichero PaquetePrueba.java (directorio prueba) Compilación: Situarse en directorio prueba y ejecutar javac -bootclasspath $CLDC_PATH/common/api/classes -d tmpclasses prueba/PaquetePrueba.java Preverificación preverify -classpath $CLDC_PATH/common/api/classes:tmpclasses -d . prueba/PaquetePrueba o bien preverify -classpath $CLDC_PATH/common/api/classes:tmpclasses -d . tmpclasses Ejecución: Situarse en directorio padre de prueba y ejecutar kvm -classpath . prueba.PaquetePrueba o bien añadir directorio padre de prueba a CLASSPATH y ejecutar kvm prueba.PaquetePrueba 3 Ejemplo de paquete: P25/MyPack package MyPack; class Balance { String name; double bal; Balance(String n, double b) { name = n; bal = b; } void show() { if(bal<0) System.out.print("-->> "); System.out.println(name + ": $" + bal); } } class AccountBalance { public static void main(String args[]) { Balance current[] = new Balance[3]; current[0] = current[1] = current[2] = for(int i=0; new Balance("K. J. Fielding", 123.23); new Balance("Will Tell", 157.02); new Balance("Tom Jackson", -12.33); i<3; i++) current[i].show(); } } 2 Protección de acceso Las clases y los paquetes son dos medios de encapsular y contener el espacio de nombres y el ámbito de las variables y métodos. Paquetes: Actúan como recipientes de clases y otros paquetes subordinados. Clases: Actúan como recipientes de datos y código. 1 Tipos de acceso a miembros de una clase Desde método en ... private sin modif. protected public misma clase sí sí sí sí subclase del mismo paquete no sí sí sí no subclase del mismo paquete no sí sí sí subclase de diferente paquete no no sí sí no subclase de diferente paquete no no no sí 2 Tipos de acceso para una clase Acceso por defecto: Accesible sólo por código del mismo paquete Acceso public: Accesible por cualquier código Ejemplo: P26 Ejemplo: P26 package p1; public class Protection { int n = 1; private int n_pri = 2; protected int n_pro = 3; public int n_pub = 4; public Protection() { System.out.println("constructor System.out.println("n = " + n); System.out.println("n_pri = " + System.out.println("n_pro = " + System.out.println("n_pub = " + } } class Derived extends Protection { Derived() { System.out.println("constructor System.out.println("n = " + n); base "); n_pri); n_pro); n_pub); de Derived"); // System.out.println("n_pri = " + n_pri); System.out.println("n_pro = " + n_pro); System.out.println("n_pub = " + n_pub); } } class SamePackage { SamePackage() { Protection p = new Protection(); System.out.println("constructor de SamePackage"); System.out.println("n = " + p.n); // System.out.println("n_pri = " + p.n_pri); System.out.println("n_pro = " + p.n_pro); System.out.println("n_pub = " + p.n_pub); } } package p2; class Protection2 extends p1.Protection { Protection2() { System.out.println("constructor de Protection2"); // System.out.println("n = " + n); // System.out.println("n_pri = " + n_pri); System.out.println("n_pro = " + n_pro); System.out.println("n_pub = " + n_pub); } } class OtherPackage { OtherPackage() { p1.Protection p = new p1.Protection(); System.out.println("other package constructor"); // System.out.println("n = " + p.n); // System.out.println("n_pri = " + p.n_pri); // System.out.println("n_pro = " + p.n_pro); System.out.println("n_pub = " + p.n_pub); } } 3 Importar paquetes Todas las clases estándar de Java están almacenadas en algún paquete con nombre. Para usar una de esas clases debemos usar su nombre completo. Por ejemplo para la clase Date usaríamos java.util.Date. Otra posibilidad es usar la sentencia import. import: Permite que se puedan ver ciertas clases o paquetes enteros sin tener que introducir la estructura de paquetes en que están incluidos, separados por puntos. Debe ir tras la sentencia package: import paquete[.paquete2].(nombre_clase|*); Al usar * especificamos que se importa el paquete completo. Esto incrementa el tiempo de compilación, pero no el de ejecución. Las clases estándar de Java están dentro del paquete java. Las funciones básicas del lenguaje se almacenan en el paquete. java.lang, el cual es importado por defecto. Al importar un paquete, sólo están disponibles los elementos públicos de ese paquete para clases que no sean subclases del código importado. Ejemplo: P27 MyPack/Balance.java package MyPack; /* La clase Balance, su constructor, y su metodo show() deben ser p?os. Esto significa que pueden ser utilizados por c᭥o que no sea una subclase y est頦uera de su paquete. */ public class Balance { String name; double bal; public Balance(String n, double b) { name = n; bal = b; } public void show() { if(bal<0) System.out.print("-->> "); System.out.println(name + ": $" + bal); } } TestBalance.java import MyPack.*; class TestBalance { public static void main(String args[]) { /* Como Balance es p?a, se puede utilizar la clase Balance y llamar a su constructor. */ Balance test = new Balance("J. J. Jaspers", 99.88); test.show(); // tambi鮠se puede llamar al metodo show() } } 6.15 Reutilización de las clases de un paquete / librería. 6.16 Clases genéricas (Plantillas). Clases genéricas §1 Sinopsis Hemos indicado ( 1.12) que las clases-plantilla, clases genéricas o generadores de clases, son un artificio C++ que permite definir una clase mediante uno o varios parámetros. Este mecanismo es capaz de generar la definición de clases (instancias o especializaciones de la plantilla) distintas, pero compartiendo un diseño común. Podemos imaginar que una clase genérica es un constructor de clases, que como tal acepta determinados argumentos (no confundir con el constructor deuna-clase, que genera objetos). Para ilustrarlo recordemos la clase mVector que utilizamos al tratar la sobrecarga de operadores ( 4.9.18d). En aquella ocasión los objetos mVector eran matrices cuyos elementos eran objetos de la clase Vector; que a su vez representaban vectores de un espacio de dos dimensiones. El diseño básico de la clase es como sigue: class mVector { // definición de la clase mVector int dimension; public: Vector* mVptr; mVector(int n = 1) { // constructor por defecto dimension = n; mVptr = new Vector[dimension]; } ~mVector() { delete [] mVptr; } // destructor Vector& operator[](int i) { // operador subíndice return mVptr[i]; } void showmem (int); // función auxiliar }; void mVector::showmem (int i) { if((i >= 0) && (i <= dimension)) mVptr[i].showV(); else cout << "Argumento incorrecto! pruebe otra vez" << endl; } El sistema de plantillas permite definir una clase genérica que instancie versiones de mVector para matrices de cualquier tipo especificado por un parámetro. La ventaja de este diseño parametrizado, es que cualquiera que sea el tipo de objetos utilizados por las especializaciones de la plantilla, las operaciones básicas son siempre las mismas (inserción, borrado, selección de un elemento, etc). §2 Definición La definición de una clase genérica tiene el siguiente aspecto: template<lista-de-parametros> class nombreClase { // Definición ... }; Una clase genérica puede tener una declaración adelantada para ser declarada después: template<lista-de-parametros> class nombreClase; // Declaración ... template<lista-de-parametros> class nombreClase { // Definición ... }; pero recuerde que debe ser definida antes de su utilización [5], y la regla de una sola definición ( 4.1.2). Observe que la definición de una plantilla comienza siempre con template<...>, y que los parámetros de la lista <...> no son valores, sino tipos de datos . En la página adjunta se muestra la gramática C++ para el especificador template ( Gramática). La definición de la clase genérica correspondiente al caso anterior es la siguiente: template<class T> class mVector { // L.1 Declaración de plantilla int dimension; public: T* mVptr; mVector(int n = 1) { // constructor por defecto dimension = n; mVptr = new T[dimension]; } ~mVector() { delete [] mVptr; } // destructor T& operator[](int i) { return mVptr[i]; } void showmem (int); }; template<class T> void mVector<T>::showmem (int i) { // L.16: if((i >= 0) && (i <= dimension)) mVptr[i].showV(); else cout << "Argumento incorrecto! pruebe otra vez" << endl; } Observe que aparte del cambio de la declaración (L.1), se han sustituido las ocurrencias de Vector (un tipo concreto) por el parámetro T. Observe también la definición de showmem() en L.16, que se realiza off-line con la sintaxis de una función genérica ( 4.12.1). §2.1 Recordemos que en estas expresiones, el especificador class puede ser sustituido por typename ( 3.2.1e), de forma que la expresión L.1 puede ser sustituida por: tamplate<typename T> class mVector { // L.1bis ... }; También que la definición puede corresponder a una estructura (recordemos que son un tipo de clase en las que todos sus miembros son públicos). Por ejemplo: template<class Arg> struct all_true : public unary_function<Arg, bool> { bool operator()(const Arg& x){ return 1; } }; §2.2 Ejemplo-2 En la página adjunta se incluye un ejemplo operativo. Tomando como punto de partida la versión definitiva de mVector ( 4.9.18d1), se ha reproducido el mismo programa, pero cambiando el diseño, de forma que mVector es ahora una clase genérica en vez de una clase específica ( Ejemplo-2). §2.3 Miembros de clases genéricas Los miembros de las clases genéricas se definen y declaran exactamente igual que los de clases concretas. Pero debemos señalar que las funciones-miembro son a su vez plantillas parametrizadas (funciones genéricas) con los mismos parámetros que la clase genérica a que pertenecen. Consecuencia de lo anterior es que si las funciones-miembro se definen fuera de la plantilla, sus prototipos deberían presentar el siguiente aspecto: emplate<class T> class mVector { // Clase genérica int dimension; public: T* mVptr; template<class T> mVector<T>& operator=(const mVector<T>&); template<class T> mVector<T>(int); // constructor por defecto template<class T> ~mVector<T>(); // destructor template<class T> mVector<T>(const mVector<T>& mv); // constructor-copia T& operator[](int i) { return mVptr[i]; } template <class T> void showmem (int); // función auxiliar template <class T> void show (); // función auxiliar }; Sin embargo, no es exactamente así por diversas razones: La primera es que, por ejemplo, se estaría definiendo la plantilla showmem sin utilizar el parámetro T en su lista de argumentos (lo que no está permitido 4.12.1). Otra es que no está permitido declarar los destructores como funciones genéricas. Además los especificadores <T> referidos a mVector dentro de la propia definición son redundantes. Estas consideraciones hacen que los prototipos puedan ser dejados como sigue (los datos faltantes pueden deducirse del contexto): template<class T> class mVector { // Clase genérica int dimension; public: T* mVptr; mVector& operator= (const mVector&); mVector(int); // constructor por defecto ~mVector(); // destructor mVector(const mVector& mv); // constructor-copia T& operator[](int i) { return mVptr[i]; } void showmem (int); // función auxiliar void show (); // función auxiliar }; §2.3.1 Las definiciones de métodos realizadas off-line (fuera del cuerpo de una plantilla) deben ser declaradas explícitamente como funciones genéricas ( 4.12.1). Por ejemplo: template <class T> void mVector<T>::showmem (int i) { ... } §2.3.2 Ejemplo-3: Observe la sintaxis del siguiente ejemplo. Es igual que el anterior (Ejemplo-2 ), con la diferencia de que en este, las funciones-miembro se definen off-line ( Ejemplo3). §2.3.3 Miembros estáticos Las clases genéricas pueden tener miembros estáticos (propiedades y métodos). Posteriormente cada especialización dispondrá de su propio conjunto de estos miembros. Estos miembros estáticos deben ser definidos fuera del cuerpo de la plantilla, exactamente igual que si fuesen miembros estáticos de clases concretas ( 4.11.7). Ejemplo: template<class T> class mVector { // Clase genérica ... static T* mVptr; static void showmem (int); ... }; template<class T> T* mVector<T>::nVptr; // no es necesario poner static template<class T> void mVector<T>::showmem(int x){ ... }; §2.3.4 Métodos genéricos Hemos señalado que, por su propia naturaleza, los métodos de clases genéricas son a su vez (implícitamente) funciones genéricas con los mismos parámetros que la clase, pero pueden ser además funciones genéricas explícitas (que dependan de parámetros distintos de la plantilla a que pertenecen): template<class X> class A { // clase genérica template<class T> void func(T& t); // método genérico de clase genérica ... } Según es usual, la definición del miembro genérico puede efectuarse de dos formas: on-line (en el interior de la clase genérica Ejemplo), u off-line (en el exterior Ejemplo). §3 Observaciones: La definición de una clase genérica puede suponer un avance importante en la definición de clases relacionadas, sin embargo son pertinentes algunas advertencias: §3.1 Las clases genéricas son entes de nivel superior a las clases concretas. Representan para las clases normales (en este contexto preferimos llamarlas clases explícitas) lo mismo que las funciones genéricas a las funciones concretas. Como aquellas, solo tienen existencia en el código. Como el mecanismo de plantillas C++ se resuelve en tiempo de compilación, ni en el fichero ejecutable ni durante la ejecución existe nada parecido a una clase genérica, solo existen especializaciones (ver a continuación §4 ). En realidad la clase genérica que se ve en el código, actúa como una especie de "macro" que una vez ejecutado su trabajo en la fase de compilación, desaparece de escena. Como ha señalado algún autor, el mecanismo de plantillas representa una especie de polimorfismo en tiempo de compilación, similar al que proporciona la herencia de métodos virtuales en tiempo de ejecución. §3.2 Aconsejamos realizar el diseño y una primera depuración con una clase explícita antes de convertirla en una clase genérica. Es más fácil imaginarse el funcionamiento referido a un tipo concreto que a entes abstractos. Además es más fácil entender los problemas que pueden presentarse si se maneja una imagen mental concreta [1]. En estos casos es más sencillo ir de lo particular a lo general. §3.3 Contra lo que ocurre con las funciones genéricas, en la instanciación de clases genéricas el compilador no realiza ninguna suposición sobre la naturaleza de los argumentos a utilizar, de modo que se exige que sean declarados siempre de forma explícita. Por ejemplo: mVector<char> mv1; mVector mv2 = mv1; // Error !! mVector<char> mv2 = mv1; // Ok. Nota: Como veremos a continuación , las clases genéricas pueden tener argumentos por defecto, por lo que en estos casos la declaración puede no ser explícita sino implícita (referida a los valores por defecto de los argumentos). La consecuencia es que en estos casos el compilador tampoco realiza ninguna suposición sobre los argumentos a utilizar. §3.4 Las clases genéricas pueden ser utilizadas en los mecanismos de herencia. Por ejemplo: template <class T> class Base { ... }; template <class T> class Deriv : public Base<T> {...}; §3.5 Los typedef ( 3.2.1a) son muy adecuados para acortar la notación de objetos de clases genéricas cuando se trata de declaraciones muy largas o no interesan los detalles. Por ejemplo : typedef basic_string <char> string; más tarde, para obtener un objeto puede escribirse: string st1; // equivale a: basic_string <char> st1; §4 Instanciación (obtener especializaciones) Al tratar las funciones genéricas vimos que el compilador genera el código apropiado en cuanto aparece una invocación a la función. Por ejemplo si max(a, b) es una función genérica: UnaClase a, b, m; m = max(a, b); // invoca especialización para objetos UnaClase Para utilizar un objeto de una clase genérica, es necesario previamente instanciar la especialización de la clase que instanciará a su vez el objeto. La primera instanciación (de la clase concreta) se realiza mediante la invocación de la clase genérica utilizando argumentos [4]. Por ejemplo: mVector<Vector> mV1, mV2; // Ok. matrices de Vectores mVector<Complejo> mC1, mC2; // Ok. matrices de Complejos Durante la compilación, la primera sentencia crea una instancia (función-clase 4.11.5) de mVector específica para miembros Vector; que a su vez creará un objeto-clase en tiempo de ejecución que será el encargado de instanciar los objetos mV1 y mV2. En este contexto mVector<Vector> representa la clase que genera los objetos mV1 y mV2. La segunda sentencia es análoga aunque para complejos. Observe que si mVector es una clase genérica, el identificador mVector debe ir acompañado siempre por un tipo T entre ángulos ( <T> ), ya que los ángulos vacíos ( <> ) no pueden aparecer solos excepto en algunos casos de la definición de la plantilla o cuando tenga valores por defecto. Nota: Como veremos a continuación , las clases genéricas pueden tener argumentos por defecto, en cuyo caso, el tipo T puede omitirse, pero no los ángulos <>. Por ejemplo: template<class T = int> class mVector {/* ... */}; ... mVector<char> mv1; // Ok. argumento char explícito mVector<> mv2; // Ok. argumento int implícito (valor por defecto) Cada instancia de una clase genérica es realmente una clase, y sigue las reglas generales de las clases. Por ejemplo, como se verá a continuación , pueden recibir referencias y punteros, y dispone de su propia versión de todos los miembros estáticos si los hubiere ( 4.11.7). Estas clases son denominadas implícitas, para distinguirlas de las definidas "manualmente", que se denominan explícitas. La primera vez que el compilador encuentra una sentencia del tipo mVector<Vector> crea la función-clase para dicho tipo; es el punto de instanciación. Con objeto de que solo exista una definición de la clase, si existen más ocurrencias de este mismo tipo, las funciones-clase redundantes son eliminados por el enlazador. Por la razón inversa, si el compilador no encuentra ninguna razón para instanciar una clase (generar la funciónclase), esta generación no se producirá y no existirá en el código ninguna instancia de la plantilla. §5 Evitar la generación automática de especializaciones (especializaciones explícitas) Lo mismo que ocurre con las funciones genéricas ( 4.12.1a), en las clases genéricas también puede evitarse la generación de versiones implícitas para tipos concretos proporcionando una especialización explícita. Por ejemplo: class mVector<T*> { ... }; // L.1: definición genérica ... class mVector<char*> { ... }; // L.2: definición específica más tarde, las declaraciones del tipo mVector<char*> mv1, mv2; generará objetos utilizando la definición específica proporcionada por L.2. En este caso mv1 y mv2 serán matrices alfanuméricas (cadenas de caracteres). Observe que la definición explícita comporta dos requisitos: Aunque es una versión específica (para un tipo concreto), se utiliza la sintaxis de plantilla: class mVector<...> {...}; Se ha sustituido el tipo genérico <T*> por un tipo concreto <char*>. Resulta evidente que una definición específica, como la incluída en L.2, solo tiene sentido si se necesitan algunas modificaciones en el diseño L.1 cuando la clase se refiera a tipos char* (punteros-a-char) §6 Argumentos de la plantilla La declaración de clases genéricas puede incluir una lista con varios parámetros. Estos pueden ser casi de cualquier tipo: complejos, fundamentales ( 2.2), por ejemplo un int, o incluso otra clase genérica (plantilla). Además en todos los casos pueden presentar valores por defecto. Ejemplo: template<class T, int dimension = 128> class mVector { ... }; §6.1 En la instanciación de clases genéricas, los valores de los parámetros que no sean tipos complejos deben ser constantes o expresiones constantes ( 3.2.3). Por ejemplo: const int K = 128; int i = 256; mVector<int, 2*K> mV1; // OK mVector<Vector, i> mV2; // Error: i no es constante Este tipo de parámetros constantes son adecuados para establecer tamaños y límites. Por ejemplo: template <class T, int max = 128 > class Matriz { ... int dimension; T* ptr; Matriz (int d = 0) { // constructor dimension = d > max ? max : d; ptr = new T [dimension]; } ... }; Sin embargo, por su propia naturaleza de constantes, cualquier intento posterior de alterar su valor genera un error. Cuando es necesario pasar valores numéricos, enteros o fraccionarios, para los que pueda existir ambiguedad en el tipo, se aconseja incluir sufijos ( 3.2.3b y 3.2.3c). Por ejemplo, en el caso de instanciar un objeto para la plantilla anterior: Matriz<Vector, 256U> mV1; §6.2 Los argumentos pueden ser otras plantillas, pero solo clases genéricas. Las funciones genéricas no están permitidas: template <class T, template<class X> class C> class MatrizC { // Ok. ... }; template <class T, template<class X> void Func(X a)> class MatrizF { // Error!! ... }; §6.3 Tenga en cuenta que no existe algo parecido a un mecanismo de "sobrecarga" de las clases genéricas paralelo al de las funciones genéricas. Por ejemplo las siguientes declaraciones producen un error de compilación. template<class T> class mVector { ... }; template<class T, int dimension> class mVector { ... }; Error: Number of template parameters does not match in redeclaration of 'Matriz<T>'. template<class T, class D> class mVector { ... }; template<class T, int dimension> class mVector { ... }; Error: Argument kind mismatch in redeclaration of template parameter 'dimension'. §7 La cuestión de los "Tipos" en las plantillas La "Plantilla" que ve el programador en el código no tiene existencia cuando termina la compilación. En el ejecutable solo existen funciones-clase específicas ( 4.11.5) que darán lugar a objetos-clase durante la ejecución. Esto hace que no sea posible obtener el "Tipo" de una plantilla con el operador typeid ( 4.9.14); en cualquier caso hay que aplicarlo sobre una instancia concreta. Por ejemplo: template <class T, int size = 0 > class Matriz { ... }; ... const type_info & ref1 = typeid(Matriz); // Error!! Con este intento compilador muestra un mensaje muy esclarecedor: Error ... Cannot use template 'Matriz<T, max>' without specifying specialization parameters. En cambio el código: const type_info & ref = typeid(Matriz<int, 5>); // Ok. cout << "El tipo es: " << ref.name() << endl; Produce la siguiente salida: El tipo es: Matriz<int,5> Es el mismo resultado que para el objeto m1: Matriz<int, 5> m1; Matriz<int, 2> m2; Matriz<char, 5> m3; Tenga en cuenta que m2 es tipo Matriz<int, 2> y que m3 es tipo Matriz<char, 5>. Para el compilador todos ellos son tipos distintos. Justamente debido a esto, para conseguir clases lo más genéricas posibles conviene circunscribir al mínimo los argumentos de la plantilla, ya que el lenguaje C++ es fuertemente tipado ( 2.2), y cada nuevo argumento aumenta la posibilidad de que los tipos sean distintos entre si, lo que conduce a una cierta rigidez posterior [3]. Por ejemplo, en el caso anterior m1 y m2 son tipos distintos, en consecuencia quizás no se puedan efectuar determinadas operaciones entre ellos (m1 + m2) a pesar que el operador suma + esté definido en la plantilla. En cambio si la definición de esta es del tipo: template < T > class Matriz { ...... }; Y dejamos el argumento size para el constructor, las matrices m1 y m2: Matriz<int> m1(5); Matriz<int> m2(2); Son del mismo tipo y seguramente se podrá efectuar la operación m1 + m2. §7.1 Ejemplo-4: El ejemplo adjunto muestra claramente esta influencia, así como las diferencias obtenidas con varias formas del programa. En la primera, se utiliza una clase explícita; en la segunda esta clase se transforma en una clase genérica, y en la tercera se incluye un segundo argumento que evita incluirlo en el constructor ( Ejemplo-4). §8 Punteros y referencias a clases implícitas Como hemos señalado, las clases implícitas gozan de todas las prerrogativas de las explícitas, incluyendo por supuesto la capacidad de definir punteros y referencias. En este sentido no se diferencian en nada de aquellas; la única precaución es tener presente la cuestión de los tipos a la hora de efectuar asignaciones, y no perder de vista que la plantilla es una abstracción que representa múltiples clases (tipos), cada una representada por argumentos concretos. §8.1 Consideremos la clase genérica Matriz ya utilizada anteriormente ( Ejemplo-4) cuya declaración es: template <class T, int dim =1> class Matriz { /* ... */}; La definición de punteros y referencias sería como sigue: Matriz<int,5> m1; Matriz<char,5> m2; Matriz<char> m3; ... Matriz<int,5>* ptrMi5 = &m2 distintos Matriz<char,5>* ptrMch5 = &m2; Matriz<char,1>* ptrMch1 = &m2; distintos Matriz<char,1>* ptrMch1 = &m4; ptrMch5->show(); método Matriz<char> m4 = *ptrMch1; asignación mediante puntero // Ok. Tres objetos // de tipos // distintos. void (Matriz<char,5>::* fptr1)(); puntero a método fptr1 = &Matriz<char,5>::show; (m3.*fptr1)(); incorrecto (m2.*fptr1)(); método // L.11: Ok. declara Matriz<char,5>& refMch5 = m2; Matriz<char>& refMch1 = m4; refMch5.show(); método // Ok. referencia // Ok. // Ok. invocación de // Error!! tipos // Ok. // Error!! tipos // Ok. // Ok. invocación de // L.10: Ok // Ok. asignación // Error!! tipo de m3 // Ok. invocación de Comentario: Observe que la asignación L.10 exige que la clase genérica Matriz tenga definido el constructor-copia y que esté sobrecargado el operador de indirección * para los miembros de la clase ( 4.9.11). Merecen especial atención las sentencias L.11/14. En este caso se trata de punteros-a-miembros de clases implícitas. El tipo de clase está definido en los parámetros. Observe que fptr1 es puntero-a-método-de-clase-Matriz<char, 5> [2], y no puede referenciar un método de m3, que es un objeto de tipo Matriz<char,1>. Nota: Para comprender cabalmente las sentencias anteriores, puede ser de gran ayuda un repaso previo a los capítulos: Punteros a Clases ( 4.2.1f) y Punteros a Miembros ( 4.2.1g). §8.2 Ejemplo-5: En este ejemplo se muestra la utilización de punteros a clases implícitas así como de la sobrecarga del operador de indirección ( Ejemplo-5). [1] Esto es solo una generalización de un principio universal de programación: Realizar pruebas con casos lo más simples posibles e ir complicándolos paulatinamente. [2] Más formalmente: Puntero a método que no acepta argumentos y devuelve void de la clase Matriz<char, 5>. [3] Suponiendo naturalmente que esta mayor "rigidez" no sea justamente el efecto que se pretende al definir nuevos argumentos. [4] Esta notación es análoga a la utilizada con las funciones genéricas que hemos denominado instanciación implícita específica ( 4.12.1) [5] En este sentido no son como las funciones genéricas que pueden ser declaradas antes de su utilización y definidas en cualquier otro sitio (incluso después de su uso -en el código-). http://www.zator.com/Cpp/E4_12_2.htm Plantillas (INF-121 Lic. Menfy Morales – Apuntes del paralelo) La generacidad es la propiedad que permite definir una clase (o una función) sin especificar el tipo de datos de uno o más de sus miembros (parámetros). De esta forma la clase escrita se adaptará al uso con diferentes tipos de datos, sin tener que rescribirla. Una plantilla es un patrón para crear funciones y clases. Las plantillas permiten la construcción de procesos no dependientes de tipos de datos, siendo estos procesos genéricos. Tipos de plantillas 1) plantillas de función (permiten construir funciones generales) 2) plantillas de clase (permiten construir clase generales) Plantillas de función Supongamos que se desea escribir una función min(a, b) que devuelva el valor más pequeño entre a y b. int método min(int a, int b) if (a <= b) then return (a); else return ( b); endmin; real método min(real a, real b) if (a <= b) then return (a); else return ( b); endmin; Este proceso puede diseñarse para datos de tipo largo int, o string, en todos los casos el proceso en si es el mismo, varían los tipos de datos. El propósito de usar plantillas es el de liberar al programador de escribir múltiples versiones de la misma función para llevar a cabo la misma operación sobre datos de distinto tipo. Una plantilla de función especifica un conjunto infinito de funciones sobrecargadas y describe las propiedades genéricas de una función. Comienza con la palabra reservada Template seguida de una lista de parámetros formales para la plantilla de función. Estructura Template <lista de tipo parametrizado> Tipo Nombre(Conjunto de parámetros) { .... sentencias; } Ejemplo. Escribir una plantilla para hallar el menor de dos números template <class T> T Menor ( T a, T b) { if (a <= b) then return ( a ); else return ( b ); } ? 7, 3, 5.2, 6.7, mano, limón 3 5.2 limón main() { int a, b; real c, d; string e, f; read(a, b, c, d, e, f ); print (Menor (a, b)); print (Menor (c, d)); print (Menor (e, f)); } Ejemplo 2. Escribir una plantilla para cambiar 2 valores template <class T> método Cambia ( T a, T b) { T aux; aux a; a b; b aux; } main() { int a, b; real c, d; string e, f; read(a, b, c, d, e, f ); Cambia(a, b); print (a, b); Cambia (c, d); print (c, d); Cambia ( e, f ); print (e, f ); } ? 8, 4, 2.5, 3.6, taza, plato 4, 8 3.6, 2.5 plato, taza Ejemplo. Escribir una plantilla para ordenar 3 valores. Plantillas de clases Clases genéricas que trabajan sobre tipos de datos parametrizados. Permiten definir clases genéricas que pueden manipular diferentes tipos de datos. Una aplicación importante es la creación de contenedores, clases que contienen objetos de un tipo de dato, como vectores, matrices, listas, tablas, etc. Los contenedores manejan estructuras de datos. Se usan plantillas de clases para no crear múltiples clases con los mismos atributos y operaciones. Estructura template <lista de tipos parametrizados> class Nombre { //miembro dato (parametrizado) public //miembro función (parametrizada) } main() {} Ejemplo. Escribir una plantilla de clase para la clase fracción template <class X, class Y> class Fracción { X num; Y den; public método leer() read(num, den); endleer; método mostrar() print(num, den); endmostrar; Fracción () num 7; den 3; endFracción; Fracción (X n, Y d) num n; den d; endFracción; método operator ++ () num ++; den++; end++; Fracción método suma (Fracción F) F.num F.num * den + F.den * num; F.den den * F.den + num * F.num; return ( F ); endsuma; ¿leer? 7, 4 7, 4 ¿leer? 3, 6 54, 45 } main() { Fracción <int, int> F1, F4; Fracción <char, char > F2(“x”,”y”); Fracción <char, int > F3(“*”, 7 ); F1.leer(); F1.mostrar(); F4.leer(); F1.suma( F4 ).mostrar(); } Ejemplo. Escribir una plantilla de clase para buscar un elemento X en una matriz a(n,m) y contar cuantas veces se repite. template <class T > class Matriz { T a(50, 50); int n; int m; public Matriz (int x, int y) n x; m y; endMatriz; método leer(); método mostrar(); int método cuenta( T x ); } template <class T> int Matriz <class T>:: cuenta(T x) int c 0; for ( i 1 to n) do for ( j 1 to m) do if ( a(i, j)) = x then c c + 1; endif; endfor; endfor; return (c ); endcuenta; template <class T > Matriz<class T >:: leer() for (i 1 to n ) do for (j 1 to m) do read(a ( i, j)); endfor; endfor; endleer; template <class T > Matriz<class T >:: mostrar() for (i 1 to n ) do for (j 1 to m) do print(a ( i, j)); endfor; endfor; endmostrar; main() { } Matriz <int > A (2, 3); Matriz <char > B ( 3, 3 ); A.leer(); A.mostrar(); print(A.cuenta(8)); B.leer(); B.mostrar(); print(B.cuenta(“m”)); ¿? 5 9 2 ¿? m f w s 3 5, 6, 8, 9, 8, 2 6 8 8 2 f, d, m, w, m, q, s, a, d m m q a m http://www.umsanet.edu.bo/docentes/menfy/Plantillas.html Una clase genérica de C++ es una clase que se define o se deriva de una clase definida por el usuario. Puede agregar una clase genérica de C++ desde la Vista de clases. Para agregar una clase genérica de C++ a un proyecto 1. En la Vista de clases, haga clic con el botón secundario en el nombre del proyecto al que desee agregar una clase nueva. 2. En el menú contextual, haga clic en Agregar y, después, en Agregar clase. 3. En el cuadro de diálogo Agregar clase, haga clic en Clase genérica de C++ del panel Plantillas. Haga clic en Abrir para mostrar el Asistente para clases genéricas de C++. 4. Defina la configuración en el Asistente para clases genéricas de C++. Debe proporcionar al menos el nombre de una clase. 5. Haga clic en Finalizar para cerrar el asistente y ver la nueva clase genérica de C++ en el proyecto. Fuente: http://msdn.microsoft.com/library/spa/default.asp?url=/library/SPA/vccore/html/_core_Adding_ a_Generic_Class.asp Unidad 7. Excepciones. 7.4 Definición. 7.4.1 Que son las excepciones. Definición: Excepciones ``Alternativa más elaborada para la gestión de errores y situaciones anormales: se invoca directamente un procedimiento corrector'' http://gsyc.escet.urjc.es/docencia/cursos/fse-distribuidos/transpas/node5.html Manejo de Errores Usando Excepciones Java Autor: Traductor: Juan Antonio Palos (Ozito) ¿Qué es un Excepción y Por Qué Debo Tener Cuidado? o Ventaja 1: Separar el Manejo de Errores del Código "Normal" o Ventaja 2: Propagar los Errores sobre la Pila de Llamadas Sun o o Ventaja 3: Agrupar Errores y Diferenciación ¿ Y ahora qué? ¿Qué es un Excepción y Por Qué Debo Tener Cuidado? El término excepción es un forma corta da la frase "suceso excepcional" y puede definirse de la siguiente forma. Definición: 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 y 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 Ventaja 3: Agrupar los Tipos de Errores y la Diferenciación de éstos Ventaja 1: Separar el Manejo de Errores del Código "Normal" En la programación tradicional, la detección, el informe y el manejo de errores se convierte en un código muy liado. Por ejemplo, supongamos que tenemos una función que lee un fichero completo dentro de la memeoria. En pseudocódigo, la función se podría parecer a esto. leerFichero { abrir el fichero; determinar su tamaño; asignar suficiente memoria; leer el fichero a la memoria; cerrar el fichero; } A primera vista esta función parece bastante sencilla, pero ignora todos aquello errores potenciales. ¿Qué sucede si no se puede abrir el fichero? ¿Qué sucede si no se puede determinar la longitud del fichero? ¿Qué sucede si no hay suficiente memoria libre? ¿Qué sucede si la lectura falla? ¿Qué sucede si no se puede cerrar el fichero? Para responder a estas cuestiones dentro de la función, tendríamos que añadir mucho código para la detección y el manejo de errores. El aspecto final de la función se parecería esto. codigodeError leerFichero { inicializar codigodeError = 0; abrir el fichero; if (ficheroAbierto) { determinar la longitud del fichero; if (obtenerLongitudDelFichero) { asignar suficiente memoria; if (obtenerSuficienteMemoria) { leer el fichero a memoria; if (falloDeLectura) { codigodeError = -1; } } else { codigodeError = -2; } } else { codigodeError = -3; } cerrar el fichero; if (ficheroNoCerrado && codigodeError == 0) { codigodeError = -4; } else { codigodeError = codigodeError and -4; } } else { codigodeError = -5; } return codigodeError; } Con la detección de errores, las 7 líneas originales (en negrita) se han covertido en 29 líneas de código-- a aumentado casi un 400 %. Lo peor, existe tanta detección y manejo de errores y de retorno que en las 7 líneas originales y el código está totalmente atestado. Y aún peor, el flujo lógico del código también se pierde, haciendo díficil poder decir si el código hace lo correcto (si ¿se cierra el fichero realmente si falla la asignación de memoria?) e incluso es díficil asegurar que el código continue haciendo las cosas correctas cuando se modifique la función tres meses después de haberla escrito. Muchos programadores "resuelven" este problema ignorádolo-- se informa de los errores cuando el programa no funciona. Java proporciona una solución elegante al problema del tratamiento de errores: las excepciones. Las excepciones le permiten escribir el flujo principal de su código y tratar los casos excepcionales en otro lugar. Si la función leerFcihero utilizara excepciones en lugar de las técnicas de manejo de errores tradicionales se podría parecer a esto. leerFichero { try { abrir el fichero; determinar su tamaño; asignar suficiente memoria; leer el fichero a la memoria; cerrar el fichero; } catch (falloAbrirFichero) { hacerAlgo; } catch (falloDeterminacionTamaño) { hacerAlgo; } catch (falloAsignaciondeMemoria) { hacerAlgo; } catch (falloLectura) { hacerAlgo; } catch (falloCerrarFichero) { hacerAlgo; } } Observa que las excepciones no evitan el esfuerzo de hacer el trabajo de detectar, informar y manejar errores. Lo que proporcionan las excepciones es la posibilidad de separar los detalles oscuros de qué hacer cuando ocurre algo fuera de la normal. Además, el factor de aumento de cáodigo de este es programa es de un 250% -- comparado con el 400% del ejemplo anterior. Ventaja 2: Propagar los Errores sobre la Pila de Llamadas Una segunda ventaja de las exepciones es la posibilidad del propagar el error encontrado sobre la pila de llamadas a métodos. Supongamos que el método leerFichero es el cuarto método en una serie de llamadas a métodos anidadas realizadas por un programa principal: metodo1 llama a metodo2, que llama a metodo3, que finalmente llama a leerFichero. metodo1 { call metodo2; } metodo2 { call metodo3; } metodo3 { call leerFichero; } Supongamos también que metodo1 es el único método interesado en el error que ocurre dentro de leerFichero. Tradicionalmente las técnicas de notificación del error forzarían a metodo2 y metodo3 a propagar el código de error devuelto por leerFichero sobre la pila de llamadas hasta que el código de error llegue finalmente a metodo1 -- el único método que está interesado en él. metodo1 { codigodeErrorType error; error = call metodo2; if (error) procesodelError; else proceder; } codigodeErrorType metodo2 { codigodeErrorType error; error = call metodo3; if (error) return error; else proceder; } codigodeErrorType metodo3 { codigodeErrorType error; error = call leerFichero; if (error) return error; else proceder; } Como se aprendió anteriormente, el sistema de ejecución Java busca hacia atrás en la pila de llamadas para encontrar cualquier método que esté interesado en manejar una excepción particular. Un método Java puede "esquivar" cualquier excepción lanzada dentro de él, por lo tanto permite a los métodos que están por encima de él en la pila de llamadas poder capturarlo. Sólo los métodos interesados en el error deben preocuparse de detectarlo. metodo1 { try { call metodo2; } catch (excepcion) { procesodelError; } } metodo2 throws excepcion { call metodo3; } metodo3 throws excepcion { call leerFichero; } Sin embargo, como se puede ver desde este pseudo-código, requiere cierto esfuerzo por parte de los métodos centrales. Cualquier excepción chequeada que pueda ser lanzada dentro de un método forma parte del interface de programación público del método y debe ser especificado en la clausula throws del método. Así el método informa a su llamador sobre las excepciones que puede lanzar, para que el llamador pueda decidir concienzuda e inteligentemente qué hacer con esa excepción. Observa de nuevo la diferencia del factor de aumento de código y el factor de ofuscación entre las dos técnicas de manejo de errores. El código que utiliza excepciones es más compacto y más fácil de entender. Ventaja 3: Agrupar Errores y Diferenciación Frecuentemente las excepciones se dividen en categorias o grupos. Por ejemplo, podríamos imaginar un grupo de excepciones, cada una de las cuales representara un tipo de error específico que pudiera ocurrir durante la manipulación de un array: el índice está fuera del rango del tamaño del array, el elemento que se quiere insertar en el array no es del tipo correcto, o el elemento que se está buscando no está en el array. Además, podemos imaginar que algunos métodos querrían manejar todas las excepciones de esa categoria (todas las excepciones de array), y otros métodos podría manejar sólo algunas excepciones específicas (como la excepción de índice no válido). Como todas las excepciones lanzadas dentro de los programas Java son objetos de primera clase, agrupar o categorizar las excepciones es una salida natural de las clases y las superclases. Las excepciones Java deben ser ejemplares de la clase Throwable, o de cualquier descendiente de ésta. Como de las otras clases Java, se pueden crear subclases de la clase Throwable y subclases de estas subclases. Cada clase 'hoja' (una clase sin subclases) representa un tipo específico de excepción y cada clase 'nodo' (una clase con una o más subclases) representa un grupo de excepciones relacionadas. InvalidIndexException, ElementTypeException, y NoSuchElementException son todas clases hojas. Cada una representa un tipo específico de error que puede ocurrir cuando se manipula un array. Un método puede capturar una excepción basada en su tipo específico (su clase inmediata o interface). Por ejemplo, una manejador de excepción que sólo controle la excepción de índice no válido, tiene una sentencia catch como esta. catch (InvalidIndexException e) { . . . } ArrayException es una clase nodo y representa cualquier error que pueda ocurrir durante la manipulación de un objeto array, incluyendo aquellos errores representados específicamente por una de sus subclases. Un método puede capturar una excepción basada en este grupo o tipo general especificando cualquiera de las superclases de la excepción en la sentencia catch. Por ejemplo, para capturar todas las excepciones de array, sin importar sus tipos específicos, un manejador de excepción especificaría un argumento ArrayException. catch (ArrayException e) { . . . } Este manejador podría capturar todas las excepciones de array, incluyendo InvalidIndexException, ElementTypeException, y NoSuchElementException. Se puede descubrir el tipo de excepción preciso que ha ocurrido comprobando el parámtero del manejador e. Incluso podríamos seleccionar un manejador de excepciones que controlara cualquier excepción con este manejador. catch (Exception e) { . . . } Los manejadores de excepciones que son demasiado generales, como el mostrado aquí, pueden hacer que el código sea propenso a errores mediante la captura y manejo de excepciones que no se hubieran anticipado y por lo tanto no son manejadas correctamente dentro de manejador. Como regla no se recomienda escribir manejadores de excepciones generales. Como has visto, se pueden crear grupos de excepciones y manejarlas de una forma general, o se puede especificar un tipo de excepción específico para diferenciar excepciones y manejarlas de un modo exacto. http://www.programacion.net/tutorial/excepciones/2/ Excepciones Excepcion es, o sencillamente problemas. En la programación siempre se producen errores, más o menos graves, pero que hay que gestionar y tratar correctamente. Por ello en java disponemos de un mecanismo consistente en el uso de bloques try/catch/finally . La técnica básica consiste en colocar las instrucciones que podrían provocar problemas dentro de un bloque try, y colocar a continuación uno o más bloques catch, de tal forma que si se provoca un error de un determinado tipo, lo que haremos será saltar al bloque catch capaz de gestionar ese tipo de error específico. El bloque catch contendrá el codigo necesario para gestionar ese tipo específico de error. Suponiendo que no se hubiesen provocado errores en el bloque try, nunca se ejecutarían los bloques catch. Veamos ahora la estructura del bloque try/catch/finally: try { //Código que puede provocar errores } catch(Tipo1 var1) { //Gestión del error var1, de tipo Tipo1 } [ ... catch(TipoN varN) { //Gestión del error varN, de tipo TipoN }] [ finally { //Código de finally } ] Como podemos ver es obligatorio que exista la zona try, o zona de pruebas, donde pondremos las instrucciones problemáticas. Después vienen una o más zonas catch, cada una especializada en un tipo de error o excepción. Por último está la zona finally, encargada de tener un código que se ejecutará siempre, independientemente de si se produjeron o no errores. Se puede apreciar que cada catch se parece a una función en la cuál sólo recibimos un objeto de un determinado tipo, precisamente el tipo del error. Es decir sólo se llamará al catch cuyo argumento sea coincidente en tipo con el tipo del error generado. http://www.mailxmail.com/curso/informatica/java/capitulo57 .htm La instrucción try (try-statement) proporciona un mecanismo para capturar las excepciones que ocurren durante la ejecución de un bloque. La instrucción try permite además especificar un bloque de código que siempre se ejecuta cuando el control abandona la instrucción try. try-statement: try block try block try block catch-clauses finally-clause catch-clauses finally-clause catch-clauses: specific-catch-clauses general-catch-clauseopt specific-catch-clausesopt general-catch-clause specific-catch-clauses: specific-catch-clause specific-catch-clauses specific-catch-clause specific-catch-clause: catch ( class-type identifieropt ) catch finally block general-catch-clause: block block finally-clause: Existen tres formas posibles de instrucciones try: Un bloque try seguido de uno o varios bloques catch. Un bloque try seguido de un bloque finally. Un bloque try seguido de uno o varios bloques catch y seguidos de un bloque finally. Cuando una cláusula catch especifica un tipo-de-clase (class-type), el tipo debe ser System.Exception o un tipo que derive de System.Exception. Cuando una cláusula catch especifica tanto un tipo-de-clase (class-type) como un identificador (identifier) se declara una variable de excepción con el nombre y tipo dados. La variable de excepción es una variable local con un ámbito que se extiende a lo largo de todo el bloque catch. Durante la ejecución de un bloque catch, la variable de excepción representa la excepción que se está controlando en ese momento. Desde el punto de vista de comprobación de asignación definitiva, la variable de excepción se considera asignada definitivamente en todo su ámbito. Si la cláusula catch no incluye el nombre de una variable de excepción, no es posible tener acceso al objeto de excepción en el bloque catch. Una cláusula catch general es una cláusula catch que no especifica el tipo ni la variable de excepción. Una instrucción try sólo puede tener una cláusula catch general y, si existe, debe ser la última cláusula catch. Algunos lenguajes de programación pueden aceptar excepciones que no son representables como un objeto derivado de System.Exception, aunque estas excepciones nunca pueden ser generadas por código C#. Una cláusula catch general puede utilizarse para detectar este tipo de excepciones. Por lo tanto, una cláusula catch general es semánticamente diferente de otra que especifica el tipo System.Exception, ya que la primera también puede detectar excepciones de otros lenguajes. Las cláusulas catch se examinan en orden léxico para encontrar un controlador para la excepción. Si una cláusula catch especifica un tipo que es igual o derivado de un tipo especificado en una cláusula catch anterior de la misma instrucción try, se produce un error en tiempo de compilación. Si no existiera esta restricción, sería posible escribir cláusulas catch inalcanzables. Dentro de un bloque catch, puede utilizar una instrucción throw (Sección 8.9.5) sin expresión para volver a iniciar la excepción que capturó el bloque catch. Las asignaciones a una variable de excepción no modifican la excepción que vuelve a iniciarse. En el ejemplo using System; class Test { static void F() { try { G(); } catch (Exception e) { Console.WriteLine("Exception in F: " + e.Message); e = new Exception("F"); throw; // re-throw } } static void G() { throw new Exception("G"); } static void Main() { try { F(); } catch (Exception e) { Console.WriteLine("Exception in Main: " + e.Message); } } } el método F captura una excepción, escribe información de diagnóstico en la consola, modifica la variable de excepción y vuelve a iniciar la excepción. La excepción que se vuelve a iniciar es la excepción original, de modo que el resultado producido es el siguiente: Exception in F: G Exception in Main: G Si el primer bloque catch iniciara e en lugar de volver a iniciar la excepción actual, el resultado producido sería el siguiente: Exception in F: G Exception in Main: F Se produce un error en tiempo de compilación cuando una instrucción break, continue o goto transfiere el control fuera del bloque finally. Cuando ocurre una instrucción break, continue o goto dentro de un bloque finally, el destino de la instrucción debe encontrarse dentro del propio bloque finally. En caso contrario, se producirá un error en tiempo de compilación. También se produce un error en tiempo de compilación si una instrucción return ocurre dentro de un bloque finally. Una instrucción try se ejecuta de la siguiente forma: El control se transfiere al bloque try. Cuando el control alcanza el punto final del bloque try: Si la instrucción try tiene un bloque finally, se ejecuta éste. El control se transfiere al punto final de la instrucción try. Si se propaga una excepción a la instrucción try durante la ejecución del bloque try: Si existen cláusulas catch, se examinan en orden de aparición para buscar un controlador adecuado para la excepción. La primera cláusula catch que especifique el tipo de excepción o un tipo base del tipo de excepción se considera una coincidencia. Una cláusula catch general es una coincidencia para cualquier tipo de excepción. Si se encuentra una cláusula catch coincidente: Si la cláusula catch coincidente declara una variable de excepción, se asigna el objeto de excepción a dicha variable. El control se transfiere al bloque catch coincidente. Cuando el control alcanza el punto final del bloque catch: Si la instrucción try tiene un bloque finally, se ejecuta éste. El control se transfiere al punto final de la instrucción try. Si se propaga una excepción a la instrucción catch durante la ejecución del bloque try: Si la instrucción try tiene un bloque finally, se ejecuta éste. La excepción se propaga a la siguiente instrucción try envolvente. Si la instrucción try no tiene cláusulas catch o si ninguna cláusula catch coincide con la excepción: Si la instrucción try tiene un bloque finally, se ejecuta éste. La excepción se propaga a la siguiente instrucción try envolvente. Las instrucciones de un bloque finally siempre se ejecutan cuando el control abandona la instrucción try. Esto se cumple cuando el control se transfiere como resultado de una ejecución normal, de la ejecución de una instrucción break, continue, goto o return, o de la propagación de una excepción fuera de la instrucción try. Si se vuelve a iniciar una excepción durante la ejecución de un bloque finally, la excepción se propaga a la siguiente instrucción try envolvente. Si se estaba propagando otra excepción anterior, ésta se pierde. El proceso de propagación de una excepción se explica con mayor detalle en la descripción de la instrucción throw (Sección 8.9.5). El bloque try de una instrucción try es alcanzable si la instrucción try es alcanzable. El bloque catch de una instrucción try es alcanzable si la instrucción try es alcanzable. El bloque finally de una instrucción try es alcanzable si la instrucción try es alcanzable. El punto final de una instrucción try es alcanzable si se cumplen las dos condiciones siguientes: El punto final de un bloque try es alcanzable o el punto final de al menos uno de sus bloques catch es alcanzable. Si existe un bloque finally, el punto final del bloque finally es alcanzable. http://msdn.microsoft.com/library/SPA/csspec/html/vclrfcsh arpspec_8_10.asp 7.4.2 Clases de excepciones, excepciones predefinidas por el lenguaje. Generar Excepciones en Java Cuando se produce una condición excepcional en el transcurso de la ejecución de un programa, se debería generar, o lanzar, una excepción. Esta excepción es un objeto derivado directa, o indirectamente, de la clase Throwable. Tanto el intérprete Java como muchos métodos de las múltiples clases de Java pueden lanzar excepciones y errores. La clase Throwable tiene dos subclases: Error y Exception. Un Error indica que se ha producido un fallo no recuperable, del que no se puede recuperar la ejecución normal del programa, por lo tanto, en este caso no hay nada que hacer. Los errores, normalmente, hacen que el intérprete Java presente un mensaje en el dispositivo estándar de salida y concluya la ejecución del programa. El único caso en que esto no es así, es cuando se produce la muerte de un thread, en cuyo caso se genera el error ThreadDead, que lo que hace es concluir la ejecución de ese hilo, pero ni presenta mensajes en pantalla ni afecto a otros hilos que se estén ejecutando. Una Exception indicará una condición anormal que puede ser subsanada para evitar la terminación de la ejecución del programa. Hay nueve subclases de la clase Exception ya predefinidas, y cada una de ellas, a su vez, tiene numerosas subclases. Para que un método en Java, pueda lanzar excepciones, hay que indicarlo expresamente. void MetodoAsesino() throws NullPointerException,CaidaException Se pueden definir excepciones propias, no hay por qué limitarse a las nueve predefinidas y a sus subclases; bastará con extender la clase Exception y proporcionar la funcionalidad extra que requiera el tratamiento de esa excepción. También pueden producirse excepciones no de forma explícita como en el caso anterior, sino de forma implícita cuando se realiza alguna acción ilegal o no válida. Las excepciones, pues, pueden originarse de dos modos: el programa hace algo ilegal (caso normal), o el programa explícitamente genera una excepción ejecutando la sentencia throw (caso menos normal). La sentencia throw tiene la siguiente forma: throw ObtejoExcepction; El objeto ObjetoException es un objeto de una clase que extiende la clase Exception. El siguiente código de ejemplo, java901.java, origina una excepción de división por cero: class java901 { public static void main( String[] a ) { int i=0, j=0, k; k = i/j; } // Origina un error de division-by-zero } Si compilamos y ejecutamos esta aplicación Java, obtendremos la siguiente salida por pantalla: % javac java901.java % java java901 java.lang.ArithmeticException: / by zero at java901.main(java901.java:25) Las excepciones predefinidas, como ArithmeticException, se conocen como excepciones runtime. Actualmente, como todas las excepciones son eventos runtime, sería mejor llamarlas excepciones irrecuperables. Esto contrasta con las excepciones que se generan explícitamente, a petición del programador, que suelen ser mucho menos severas y en la mayoría de los casos no resulta complicado recuperarse de ellas. Por ejemplo, si un fichero no puede abrirse, se puede preguntar al usuario que indique otro fichero; o si una estructura de datos se encuentra completa, siempre se podrá sobreescribir algún elemento que ya no se necesite. Todas las excepciones deben llevar un mensaje asociado a ellas al que se puede acceder utilizando el método getMessage(), que presentará un mensaj describiendo el error o la excepción que se ha producido. Si se desea, se pueden invocar otros métodos de la clase Throwable que presentan un traceado de la pila en donde se ha producido la excepción, o también se pueden invocar para convertir el objeto Exception en una cadena, que siempre es más intelegible y agradable a la vista. Excepciones Predefinidas Las excepciones predefinidas por la implementación actual del lenguaje Java y su jerarquía interna de clases son las que se representan en el esquema de la figura que aparece a continuación: Los nombres de las excepciones indican la condición de error que representan. Las siguientes son las excepciones predefinidas más frecuentes que se pueden encontrar: ArithmeticException Las excepciones aritméticas son típicamente el resultado de división por 0: int i = 12 / 0; NullPointerException Se produce cuando se intenta acceder a una variable o método antes de ser definido: class Hola extends Applet { Image img; paint( Graphics g ) { g.drawImage( img,25,25,this ); } } IncompatibleClassChangeException El intento de cambiar una clase afectada por referencias en otros objetos, específicamente cuando esos objetos todavía no han sido recompilados. ClassCastException El intento de convertir un objeto a otra clase que no es válida. y = (Prueba)x; // donde x no es de tipo Prueba NegativeArraySizeException Puede ocurrir si hay un error aritmético al cambiar el tamaño de un array. OutOfMemoryException ¡No debería producirse nunca! El intento de crear un objeto con el operador new ha fallado por falta de memoria. Y siempre tendría que haber memoria suficiente porque el garbage collector se encarga de proporcionarla al ir liberando objetos que no se usan y devolviendo memoria al sistema. NoClassDefFoundException Se referenció una clase que el sistema es incapaz de encontrar. ArrayIndexOutOfBoundsException < Es la excepción que más frecuentemente se produce. Se genera al intentar acceder a un elemento de un array más allá de los límites definidos inicialmente para ese array. UnsatisfiedLinkException Se hizo el intento de acceder a un método nativo que no existe. Aquí no existe un método a.kk() class A { native void kk(); } y se llama a a.kk(), cuando debería llamar a A.kk(). InternalException Este error se reserva para eventos que no deberían ocurrir. Por definición, el usuario nunca debería ver este error y esta excepción no debería lanzarse. El compilador Java obliga al programador a proporcionar el código de manejo o control de algunas de las excepciones predefinidas por el lenguaje. Por ejemplo, el siguiente programa java902.java, no compilará porque no se captura la excepción InterruptedException que puede lanzar el método sleep(). import java.lang.Thread; class java902 { public static void main( String args[] ) { java902 obj = new java902(); obj.miMetodo(); } void miMetodo() { // Aqui se produce el error de compilacion, porque no se esta // declarando la excepcion que genera este metodo Thread.currentThread().sleep( 1000 ); // currentThread() genera // una excepcion } } Este es un programa muy simple, que al intentar compilar, producirá el siguiente error de compilación que se visualizará en la pantalla tal como se reproduce a continuación: % javac java902.java java902.java:41: Exception java.lang.InterruptedException must be caught, or it must be declared in the throws clause of this method. Thread.currentThread().sleep( 1000 ); // currentThread() genera ^ Como no se ha previsto la captura de la excepción, el programa no compila. El error identifica la llamada al método sleep() como origen del problema. Así que, la siguiente versión del programa, java903.java, soluciona el problema generado por esta llamada. import java.lang.Thread; class java903 { public static void main( String args[] ) { // Se instancia un objeto java903 obj = new java903(); // Se crea la secuencia try/catch que llamara al metodo que // lanza la excepcion try { // Llamada al metodo que genera la excepcion obj.miMetodo(); }catch(InterruptedException e){} // Procesa la excepcion } // Este es el metodo que va a lanzar la excepcion void miMetodo() throws InterruptedException { Thread.currentThread().sleep( 1000 ); // currentThread() genera // una excepcion } } Lo único que se ha hecho es indicar al compilador que el método miMetodo() puede lanzar excepciones de tipo InterruptedException. Con ello conseguimos propagar las excepción que genera el método sleep() al nivel siguiente de la jerarquía de clases. Es decir, en realidad no se resuelve el problema sino que se está pasando a otro método para que lo resuelva él. En el método main() se proporciona la estructura que resuelve el problema de compilación, aunque no haga nada, por el momento. Esta estructura consta de un bloque try y un bloque catch, que se puede interpretar como que intentará ejecutar el código del bloque try y si hubiese una nueva excepción del tipo que indica el bloque catch, se ejecutaría el código de este bloque, si ejecutar nada del try. La transferencia de control al bloque catch no es una llamada a un método, es una transferencia incondicional, es decir, no hay un retorno de un bloque catch. Crear Excepciones Propias También el programador puede lanzar sus propias excepciones, extendiendo la clase System.exception. Por ejemplo, considérese un programa cliente/servidor. El código cliente se intenta conectar al servidor, y durante 5 segundos se espera a que conteste el servidor. Si el servidor no responde, el servidor lanzaría la excepción de time-out: class ServerTimeOutException extends Exception {} public void conectame( String nombreServidor ) throws Exception { int exito; int puerto = 80; exito = open( nombreServidor,puerto ); if( exito == -1 ) throw ServerTimeOutException; } Si se quieren capturar las propias excepciones, se deberá utilizar la sentencia try: public void encuentraServidor() { ... try { conectame( servidorDefecto ); catch( ServerTimeOutException e ) { g.drawString( "Time-out del Servidor, intentando alternativa",5,5 ); conectame( servidorAlterno ); } ... } Cualquier método que lance una excepción también debe capturarla, o declararla como parte del interfaz del método. Cabe preguntarse entonces, el porqué de lanzar una excepción si hay que capturarla en el mismo método. La respuesta es que las excepciones no simplifican el trabajo del control de errores. Tienen la ventaja de que se puede tener muy localizado el control de errores y no hay que controlar millones de valores de retorno, pero no van más allá. Y todavía se puede plantear una pregunta más, al respecto de cuándo crear excepciones propias y no utilizar las múltiples que ya proporciona Java. Como guía, se pueden plantear las siguientes cuestiones, y si la respuesta es afirmativa, lo más adecuado será implementar una clase Exception nueva y, en caso contrario, utilizar una del sistema. ¿Se necesita un tipo de excepción no representado en las que proporciona el entorno de desarrollo Java? ¿Ayudaría a los usuarios si pudiesen diferenciar las excepciones propias de las que lanzan las clases de otros desarrolladores? ¿Si se lanzan las excepciones propias, los usuarios tendrán acceso a esas excepciones? ¿El package propio debe ser independiente y auto-contenido? http://www.itapizaco.edu.mx/paginas/JavaTut/froufe/parte9/cap9-2.html 7.4.3 Propagación. PROPAGACION DE EXCEPCIONES La cláusula catch comprueba los argumentos en el mismo orden en que aparezcan en el programa. Si hay alguno que coincida, se ejecuta el bloque y sigue el flujo de control por el bloque finally (si lo hay) y concluye el control de la excepción. Si ninguna de las cláusulas catch coincide con la excepción que se ha producido, entonces se ejecutará el código de la cláusula finally (en caso de que la haya). Lo que ocurre en este caso, es exactamente lo mismo que si la sentencia que lanza la excepción no se encontrase encerrada en el bloque try. El flujo de control abandona este método y retorna prematuramente al método que lo llamó. Si la llamada estaba dentro del ámbito de una sentencia try, entonces se vuelve a intentar el control de la excepción, y así continuamente. Veamos lo que sucede cuando una excepción no es tratada en la rutina en donde se produce. El sistema Java busca un bloque try..catch más allá de la llamada, pero dentro del método que lo trajo aquí. Si la excepción se propaga de todas formas hasta lo alto de la pila de llamadas sin encontrar un controlador específico para la excepción, entonces la ejecución se detendrá dando un mensaje. Es decir, podemos suponer que Java nos está proporcionando un bloque catch por defecto, que imprime un mensaje de error y sale. No hay ninguna sobrecarga en el sistema por incorporar sentencias try al código. La sobrecarga se produce cuando se genera la excepción. Hemos dicho ya que un método debe capturar las excepciones que genera, o en todo caso, declararlas como parte de su llamada, indicando a todo el mundo que es capaz de generar excepciones. Esto debe ser así para que cualquiera que escriba una llamada a ese método esté avisado de que le puede llegar una excepción, en lugar del valor de retorno normal. Esto permite al programador que llama a ese método, elegir entre controlar la excepción o propagarla hacia arriba en la pila de llamadas. La siguiente línea de código muestra la forma general en que un método declara excepciones que se pueden propagar fuera de él: tipo_de_retorno( parametros ) throws e1,e2,e3 { } Los nombres e1,e2,... deben ser nombres de excepciones, es decir, cualquier tipo que sea asignable al tipo predefinido Throwable. Observar que, como en la llamada al método se especifica el tipo de retorno, se está especificando el tipo de excepción que puede generar (en lugar de un objeto exception). He aquí un ejemplo, tomado del sistema Java de entrada/salida: byte readByte() throws IOException; short readShort() throws IOException; char readChar() throws IOException; void writeByte( int v ) throws IOException; void writeShort( int v ) throws IOException; void writeChar( int v ) throws IOException; Lo más interesante aquí es que la rutina que lee un char, puede devolver un char; no el entero que se requiere en C. C necesita que se devuelva un int, para poder pasar cualquier valor a un char, y además un valor extra (-1) para indicar que se ha alcanzado el final del fichero. Algunas de las rutinas Java lanzan una excepción cuando se alcanza el fin del fichero. En el siguiente diagrama se muestra gráficamente cómo se propaga la excepción que se genera en el código, a través de la pila de llamadas durante la ejecución del código: Cuando se crea una nueva excepción, derivando de una clase Exception ya existente, se puede cambiar el mensaje que lleva asociado. La cadena de texto puede ser recuperada a través de un método. Normalmente, el texto del mensaje proporcionará información para resolver el problema o sugerirá una acción alternativa. Por ejemplo: class SinGasolina extends Exception { SinGasolina( String s ) { // constructor super( s ); } .... // Cuando se use, aparecerá algo como esto try { if( j < 1 ) throw new SinGasolina( "Usando deposito de reserva" ); } catch( SinGasolina e ) { System.out.println( o.getMessage() ); } Esto, en tiempo de ejecución originaría la siguiente salida por pantalla: > Usando deposito de reserva Otro método que es heredado de la superclase Throwable es printStackTrace(). Invocando a este método sobre una excepción se volcará a pantalla todas las llamadas hasta el momento en donde se generó la excepción (no donde se maneje la excepción). Por ejemplo: // Capturando una excepción en un método class testcap { static int slice0[] = { 0,1,2,3,4 }; public static void main( String a[] ) { try { uno(); } catch( Exception e ) { System.out.println( "Captura de la excepcion en main()" ); e.printStackTrace(); } } static void uno() { try { slice0[-1] = 4; } catch( NullPointerException e ) { System.out.println( "Captura una excepcion diferente" ); } } } Cuando se ejecute ese código, en pantalla observaremos la siguiente salida: > Captura de la excepcion en main() > java.lang.ArrayIndexOutOfBoundsException: -1 at testcap.uno(test5p.java:19) at testcap.main(test5p.java:9) Con todo el manejo de excepciones podemos concluir que proporciona un método más seguro para el control de errores, además de representar una excelente herramienta para organizar en sitios concretos todo el manejo de los errores y, además, que podemos proporcionar mensajes de error más decentes al usuario indicando qué es lo que ha fallado y por qué, e incluso podemos, a veces, recuperarnos de los errores. La degradación que se produce en la ejecución de programas con manejo de excepciones está ampliamente compensada por las ventajas que representa en cuanto a seguridad de funcionamiento de esos mismos programas. http://www.cica.es/formacion/JavaTut/Cap6/propaga.html Propagación de excepciones Cuando ocurre se lanza una excepción dentro de la sección ejecutable de un bloque ocurre lo siguiente: 1. Si el bloque actual tiene un manejador para la excepción, se ejecutan las instrucciones asociadas al manejador y control pasa al bloque que engloba a éste. 2. Si no hay un manejador para la excepción, éste se propaga lanzándola en el bloque que engloba al actual. El primer paso se repite para el bloque que engloba. Ejemplos DECLARE A EXCEPTION; BEGIN BEGIN RAISE A; EXCEPTION WHEN A THEN ... END; END; Se aplica la regla 1 para manejar la excepción DECLARE A EXCEPTION; B EXCEPTION; BEGIN RAISE A; EXCEPTION WHEN A THEN RAISE B; WHEN B THEN ... END; La excepción se propaga y el bloque no concluye exitósamente. BEGIN DECLARE v1 NUMBER(3):='abc'; BEGIN ... EXCEPTION WHEN OTHERS THEN ... END; EXCEPTION WHEN OTHERS THEN ... END; DECLARE A EXCEPTION; B EXCEPTION; BEGIN BEGIN RAISE B; EXCEPTION WHEN A THEN ... END; EXCEPTION WHEN B THEN ... END; Se aplica la regla 2 al bloque interno DECLARE v1 NUMBER(3) := 'abc'; BEGIN ... EXCEPTION WHEN OTHERS THEN ... END; Esta excepción es inmediatamente propagada, sin ejecutar las instrucciones asociadas al manejador WHEN OTHERS DECLARE A EXCEPTION; B EXCEPTION; BEGIN RAISE A; EXCEPTION WHEN A THEN RAISE B; WHEN B THEN ... END; En este caso, el bloque interno lanza la excepción, y el bloque externo la maneja. BEGIN DECLARE A EXCEPTION; B EXCEPTION; BEGIN RAISE A; EXCEPTION WHEN A THEN RAISE B; WHEN B THEN ... END; EXCEPTION WHEN B THEN ... END; En este caso, si se tiene una conclusión exitósa. La excepción A es lanzada y manejada, pero su manejador lanza B y esta es propagada hacia un bloque exterior, por lo que este bloque no termina exitósamente DECLARE A EXCEPTION; BEGIN RAISE A; EXCEPTION WHEN A THEN ... RAISE; END; Cuando RAISE no tiene argumento, la excepción actual es propagada a un bloque externo. http://www.lania.mx/biblioteca/seminarios/basedatos/plsql/errores/propagac ion.html 7.5 Gestión de excepciones. Gestión de excepciones Es posible escribir programas que gestionen ciertas excepciones. Mira el siguiente ejemplo, que pide datos al usuario hasta que se introduce un entero válido, pero permite al usuario interrumpir la ejecución del programa (con Control-C u otra adecuada para el sistema operativo en cuestion); Observa que una interrupción generada por el usuario se señaliza haciendo saltar la excepcion KeyboardInterrupt. >>> while 1: ... try: ... x = int(raw_input("Introduce un número: ")) ... break ... except ValueError: ... print "¡Huy! No es un número. Prueba de nuevo..." ... La sentencia try funciona de la siguiente manera: Primero se ejecuta la cláusula try (se ejecutan las sentencias entre try y except). Si no salta ninguna excepción, se omite la cláusula except y termina la ejecución de la sentencia try. Si salta una excepción durante la ejecución de la cláusula try, el resto de la cláusula se salta. Seguidamente, si su tipo concuerda con la excepción nombrada tras la palabra clave except, se ejecuta la cláusula except y la ejecución continúa tras la sentencia try. Si salta una excepción que no concuerda con la excepción nombrada en la cláusula except, se transmite a sentencias try anidadas exteriormente. Si no se encuentra un gestor de excepciones, se convierte en una excepción imprevista y termina la ejecución con un mensaje como el mostrado anteriormente. Una sentencia try puede contener más de una cláusula except, para capturar diferentes excepciones. Nunca se ejecuta más de un gestor para una sola excepción. Los gestores sólo capturan excepciones que saltan en la cláusula try correspondiente, no en otros gestores de la misma sentencia try. Una cláusula try puede capturar más de una excepción, nombrándolas dentro de una lista: ... except (RuntimeError, TypeError, NameError): ... pass La última cláusula except puede no nombrar ninguna excepción, en cuyo caso hace de comodín y captura cualquier excepción. Se debe utilizar esto con mucha precaución, pues es muy fácil enmascarar un error de programación real de este modo. También se puede utilizar para mostrar un mensaje de error y relanzar la excepción (permitiendo de este modo que uno de los llamantes gestione la excepción a su vez): import string, sys try: f = open('mifichero.txt') s = f.readline() i = int(string.strip(s)) except IOError, (errno, strerror): print "Error de E/S(%s): %s" % (errno, strerror) except ValueError: print "No ha sido posible covertir los datos a entero." except: print "Error no contemplado:", sys.exc_info()[0] raise La sentencia try ... except tiene una cláusula else opcional, que aparece tras las cláusulas except. Se utiliza para colocar código que se ejecuta si la cláusula try no hace saltar ninguna excepción. Por ejemplo: for arg in sys.argv[1:]: try: f = open(arg, 'r') except IOError: print 'no se puede abrir', arg else: print arg, 'contiene', len(f.readlines()), 'líneas' f.close() El uso de la cláusula else es mejor que añadir código adicional a la cláusula try porque evita que se capture accidentalemente una excepción que no fue lanzada por el código protegido por la sentencia try ... except. Cuando salta una excepción, puede tener un valor asociado, también conocido como el/los argumento/s de la excepción. Que el argumento aparezca o no y su tipo dependen del tipo de excepción. En los tipos de excepción que tienen argumento, la cláusula except puede especificar una variable tras el nombre de la excepción (o tras la lista) que recibirá el valor del argumento, del siguiente modo: >>> try: ... fiambre() ... except NameError, x: ... print 'nombre', x, 'sin definir' ... nombre fiambre sin definir Si una excepción no capturada tiene argumento, se muestra como última parte (detalle) del mensaje de error. Los gestores de excepciones no las gestionan sólo si saltan inmediatamente en la cláusula try, también si saltan en funciones llamadas (directa o indirectamente) dentro de la cláusula try. Por ejemplo: >>> def esto_casca(): ... x = 1/0 ... >>> try: ... esto_casca() ... except ZeroDivisionError, detalle: ... print 'Gestión de errores:', detalle ... Gestión de errores: integer division or modulo 7.5.1 Manejo de excepciones. Manejo de excepciones en VB.net Fecha: 24/Feb/2005 (16 de Febrero del 2005) Autor: Jesús Enrique Gonzáles Azcarate Email: [email protected] Lo siguiente fue extraído de la ayuda, que otra mejor explicación que esa. Visual Basic admite un control de excepciones (errores) tanto estructurado como no estructurado. El control de errores estructurado y no estructurado permite establecer un plan para detectar posibles errores, y así impedir que éstos interfieran en los supuestos objetivos de la aplicación. Si se produce una excepción en un método que no esté preparado para controlarla, la excepción se propagará de vuelta al método de llamada o al método anterior. Si el método anterior tampoco tiene controlador de excepciones, la excepción se propagará de vuelta al llamador del método, y así sucesivamente. La búsqueda de un controlador continuará hasta la pila de llamadas, que es la serie de procedimientos a los que se llama dentro de la aplicación. Si ésta tampoco encuentra un controlador para la excepción, se mostrará un mensaje de error y la aplicación finalizará. Nota Un método puede contener un método de control de excepciones estructurado o uno no estructurado, pero no ambos. Control estructurado de excepciones En el control estructurado de excepciones, los bloques de código se encapsulan y cada uno de ellos tiene uno o varios controladores asociados. Cada controlador especifica una forma de condición de filtro para el tipo de excepción que controla. Cuando el código de un bloque protegido genera una excepción, se busca por orden en el conjunto de controladores correspondientes y se ejecuta el primero que contenga una condición de filtro coincidente. Un método puede tener varios bloques de control estructurado de excepciones y dichos bloques pueden además estar anidados. La instrucción Try...Catch...Finally se utiliza específicamente para el control estructurado de excepciones. Aquí en este manualillo solo se explicará este método, el control estructurado de excepciones. Visual Basic admite el control estructurado de excepciones, que facilita la tarea de crear y mantener programas mediante controladores de errores consistentes y exhaustivos. El control estructurado de excepciones es un código diseñado para detectar y dar respuesta a los errores que se producen durante la ejecución, mediante la combinación de una estructura de control (similar a Select Case o While) con excepciones, bloques de código protegidos y filtros. El uso de la instrucción Try…Catch…Finally permite proteger bloques de código con posibilidades de generar errores. Los controladores de excepciones pueden anidarse, y las variables que se declaren en cada bloque tendrán un ámbito local. Try ‘El código que puede producir el error Catch [filtros opcionales o tipos de errores a capturar] ‘Código para cuando se produzca un error [bloques catch adicionales] Finally ‘Código para cuando aparezca o no un error End Try El bloque Try de un controlador de excepción Try...Catch...Finally contiene la sección del código que el controlador de errores va a supervisar. Si se produce un error durante la ejecución de cualquier parte del código de esta sección, Visual Basic examinará cada instrucción Catch de Try...Catch...Finally hasta que encuentre una cuya condición coincida con el error. Si la encuentra, el control se transferirá a la primera línea de código del bloque Catch. Si no se encuentra una instrucción Catch coincidente, la búsqueda continuará en las instrucciones Catch del bloque Try...Catch...Finally exterior que contiene el bloque en el que ocurrió la excepción. Este proceso se prolongará a lo largo de toda la pila hasta que se encuentre un bloque Catch coincidente en el procedimiento actual. De no encontrarse, se produciría un error. El código de la sección Finally siempre se ejecuta en último lugar, inmediatamente antes que el bloque de control de errores pierda su ámbito, con independencia de que se ejecute el código de los bloques Catch. Sitúe el código de limpieza (el código que cierra los archivos y libera los objetos, por ejemplo) en la sección Finally. Filtrar errores en el bloque Catch Los bloques Catch ofrecen tres opciones para filtrar errores específicos. En una de ellas, los errores se filtran basándose en la clase de la excepción (en este caso ClassLoadException), como se muestra en el siguiente código: Try ' "Try" block. Catch e as ClassLoadException ' "Catch" block. Finally ' "Finally" block. End Try Si se produce un error ClassLoadException, se ejecuta el código del bloque Catch especificado. En la segunda opción para filtrar errores, la sección Catch puede filtrar cualquier expresión condicional. Un uso común de este formato del filtro Catch consiste en comprobar números de error específicos, como se muestra en el código siguiente: Try ' "Try" block. Catch When ErrNum = 5 'Type mismatch. ' "Catch" block. Finally ' "Finally" block. End Try Cuando Visual Basic encuentra el controlador de errores coincidente, ejecuta el código de este controlador y pasa el control al bloque Finally. Nota Al buscar un bloque Catch que controle una excepción, se evalúa el controlador de cada bloque hasta encontrar uno que coincida. Puesto que estos controladores pueden ser llamadas a funciones, es posible que se produzcan efectos secundarios no esperados; por ejemplo, una llamada de este tipo puede cambiar una variable pública y provocar que ésta se utilice en el código de un bloque Catch distinto que termina controlando la excepción. La tercera alternativa, consiste en combinar las primeras dos opciones y utilizar ambas para el control de excepciones. El bloque Finally siempre se ejecuta, con independencia de cualquier otra acción que tenga lugar en los bloques Catch anteriores. No puede utilizar Resume o Resume Next en el control estructurado de excepciones. Ahora se realizarán una serie de ejemplos que aclararán lo expuesto Ejemplo 1. Module Module1 Sub Main() Dim sValor As String Dim iNumero As Integer Try 'Aqui comienza el control de errores Console.WriteLine("Introducir un número") sValor = Console.ReadLine 'Si no hemos introducido ningún número iNumero = sValor 'aquí se producira un error Catch 'Si se produce un error, se generará una excepción 'que capturamos en este bloque de código 'manipulador de excepción, definido por Catch Console.WriteLine("Error al introducir el número" & ControlChars.CrLf & "el valor {0} es incorrecto", sValor) End Try Console.Read() End Sub End Module Esta y las siguientes 2 líneas no pertenecen al código anterior Observen que antes de module1 no están declaradas Option Explicit On o Option Strict On. Ejemplo 2. El clásico de la división por cero el que está en todos los libros Module Module1 Sub Main() Dim x As Integer = 5 Dim y As Integer = 0 Try x /= y Catch ex As Exception When y = 0 Console.WriteLine(ex.Message) Finally Console.WriteLine("Este código siempre se ejecuta") End Try Console.Read() End Sub End Module Esta y las siguientes 2 líneas no pertenecen al código anterior Observen que antes de module1 no están declaradas Option Explicit On o Option Strict On. Otra forma de generar la anterior excepción es: Ejemplo 3 Module Module1 Sub Main() Dim x As Integer = 0 Try Dim y As Integer = 100 / x Catch e As ArithmeticException Console.WriteLine("ArithmeticException Handler: {0}", e.ToString()) Catch e As Exception Console.WriteLine("Generic Exception Handler: {0}", e.ToString()) End Try Console.Read() End Sub End Module Ejemplo 4 El siguiente ejemplo es el mismo del ejemplo 1 pero se le adicionó al final más código Module Module1 Sub Main() Dim sValor As String Dim iNumero As Integer Try 'Aqui comienza el control de errores Console.WriteLine("Introducir un número") sValor = Console.ReadLine 'Si no hemos introducido ningún número iNumero = sValor 'aquí se producira un error Catch 'Si se produce un error, se generará una excepción 'que capturamos en este bloque de código 'manipulador de excepción, definido por Catch Console.WriteLine("Error al introducir el número" & ControlChars.CrLf & "el valor {0} es incorrecto", sValor) End Try Dim dtFecha As Date Console.WriteLine("Introducir una fecha") 'Si ahora se produce un error, 'al no disponer de una estructura para controlarlo 'se cancelará la ejecución dtFecha = Console.ReadLine Console.WriteLine("La fecha es {0}", dtFecha) Console.ReadLine() End Sub End Module Se debe tener en cuenta la forma de capturar los errores desde los más específicos hasta los más generales de lo contrario los errores serán tomados dentro del primer catch no permitiendo capturar el error específico que necesitamos. Por ejemplo en el ejercicio anterior el catch es muy general esto quiere decir que cualquier error que se produzca entrará a este bloque. http://www.elguille.info/colabora/NET2005/cyber_Manejo_de_excepciones_en_VB.net.htm Manejo de excepciones en Turbo Pascal Revisión 4.0 Adolfo Di Mare Resumen Se presenta un paquete que implementa las construcciones requeridas para manejar excepciones en el ámbito del Turbo Pascal, en las versiones v4.0 y posteriores 5.0, 5.5 y 6.0. El paquete sigue el modelo de terminación para manejo de excepciones, y es relativamente fácil de usar. A software package for exception handling in Turbo Pascal version v4.0 and later is discussed. This package follows the exception termination model, and it is relatively easy to use. Una excepción es un evento inesperado, generalmente con proporciones de calamidad. El buen programador desea que su programa sea robusto, de forma que ante estos eventos reaccione adecuadamente; si no es posible corregir los problemas que se producen después de una falla, es por lo menos deseable que el programa tenga una degradación suave, evitando de esta manera un estruendoso fin de ejecución. En lo posible se trata siempre de sobreponerse de la mejor manera a la catástrofe. Existen muchos tipos de excepciones. Por ejemplo, cuando en tiempo de ejecución se usa un índice de vector que está fuera de rango se produce una excepción. También sucede cuando se hace una división por cero, o cuando el resultado de una expresión es demasiado pequeño (desbordamiento y bajo rebase). Los principales tipos de excepción son los siguientes: Errores en tiempo de ejecución, como división por cero, sobre rebase (overflow), bajo rebase (underflow), o intentar leer de un archivo que no existe. Excepciones generadas por las bibliotecas de programas, que aprovechan el mecanismo de excepciones para simplificar el uso de la biblioteca. Excepciones generadas por el programador, porque mediante la generación y manejo de excepciones es posible construir programas más robustos o retornar de invocaciones que están profundamente anidades [LG-86]. . Un ejemplo práctico del uso de excepciones es el siguiente: supóngase que se está escribiendo una hoja de cálculo, que tiene un largo y complicado proceso para recalcular el valor de todas las celdas. Se desea permitirle al usuario interrumpir en cualquier momento el proceso, lo que puede implementarse de dos formas. La primera implementación, y más complicada, consiste en agregar un montón de parámetros de retorno a cada rutina usada para recalcular la hoja. En el momento en que se detecta la interrupción, se comenzaría una cadena de retornos de procedimientos, hasta llegar al nivel superior. En cada procedimiento debe incluirse código para retornar adecuadamente después de que se produce la interrupción, mientras realiza su trabajo. La otra forma es usar excepciones. Inicialmente, se establece un contexto de excepción en el nivel principal. Luego cualquier rutina (hasta las recursivas) pueden retornar abruptamente a este nivel generando una excepción. De esta manera el programa que no estará plagado de códigos de retorno para manejar esta situación. El ejemplo anterior muestra que si no se usan excepciones, el programador que desea implementar un programa muy robusto debe incluir en cada procedimiento muchos parámetros que le permitan retornar códigos de error. Además, cada procedimiento debe también incluir bastante código para tomar las acciones apropiadas, dependiendo de cada posible problema. De esta forma quedan mezcladas en el mismo módulo la lógica que resuelve un problema con la que maneja las situaciones insólitas que surgen cuando ocurren fallas. El resultado de todo esto es programas que son tan difíciles de leer y como de mantener. Una manera de resolver este problema es usar un manejador de excepciones. Cada lenguaje de computación da soporte al manejo de excepciones de forma y grado diferente. En el lenguaje PL/I se implementa mediante la cláusula ON; CLU tiene un mecanismo a este efecto (que es, por cierto, muy completo) y en C se usan los famosos procedimientos de biblioteca setjump() y longjump(). Lisp no podía ser la excepción, y cuenta con CATCH y THROW. Para el caso de Turbo Pascal existen varias bibliotecas que implementan este mismo concepto, usando alguno de los nombres anteriores. En ADA se usan los verbos "exception" y "raise"; la implementación que aquí se describe es muy parecida al mecanismo de ADA, aumentado con el verbo Signal() propuesto en [Lee-83]. Uso del manejador except.pas La implementación del manejador de excepciones para Turbo Pascal está en la unidad except.pas, la que cuenta con los siguientes procedimientos: - FUNCTION FUNCTION FUNCTION PROCEDURE PROCEDURE PROCEDURE PROCEDURE PROCEDURE Exception_Code : WORD; Catch : WORD; Exception : BOOLEAN; Throw (code : WORD); Signal (code : WORD); Re_Throw; Leave_Exception_Context; Release_Nested_Contexts; Una buena parte de la implementación de estas operaciones está escrita en lenguaje de máquina, pues para manejar las excepciones es necesario modificar la pila de ejecución del programa. Su funcionamiento es realmente interesante, pues aunque estos procedimientos son llamados como si fueran procedimientos, al ejecutar un Throw(), Signal() o Re_Throw() se regresa a donde se llamó al último Exception(). IF NOT Exception THEN BEGIN { <=== } Proceso_Normal({...}); Leave_Exception_Context; END ELSE BEGIN { Manejadores de excepciones } { ==> Exception = TRUE } CASE Catch OF 1: Interrupcion_Operador; 2: Datos_Invalidos({...}); 3: Error_Fatal; 6: Throw(7); 8: Re_Throw; ELSE Re_Throw; END; { CASE } END; { Exception } Figura 1 Para atender excepciones, el programador debe establecer un Contexto de Excepción, que es un bloque de código que tiene asociados varios manejadores de excepciones. En el caso de Turbo Pascal, para establecer el contexto de excepción se usa el procedimientos Exception(), en la forma ilustrada en la Figura 1. En la Figura 1 se muestra cómo el código que normalmente se ejecuta para resolver un problema dado, que se denota por medio del procedimiento Proceso_Normal(), está físicamente separado de los manejadores de excepciones, los que se encuentran después de la instrucción "CASE Catch OF". Cuando el procedimiento Exception() se ejecuta el resultado es que guarda, en un variable global definida dentro de la unidad except.pas, su propia dirección de retorno. Además, Exception() siempre retorna FALSE como su valor. Como se ha guardado la dirección de retorno de Exception(), las demás operaciones que implementan el manejo de excepciones pueden luego simular un retorno de la función Exception() pero con el valor TRUE, lo que hace que se ejecute el código envuelto en le ELSE del IF que envuelve al contexto de excepción [Col-91]. Esto quiere decir que cuando Exception() se ejecuta siempre retorna el valor FALSE, mientras que cuando se intercepta una excepción la forma de procesarla es simular un retorno de la función Exception(), pero con el valor de retorno TRUE. Este elegante truco es muy conocido en ambientes C, en los que las funciones setjump() y longjump() soportan este tipo de comportamiento [Vid-92]. Lo importante del manejo de excepciones es que en general no se puede saber de antemano cuando se producirá una. El programador puede generar una en un módulo anidado profundamente, o puede ser que un dato recién leido de un archivo tenga un formato incorrecto que provoque una falla en el programa. Independientemente de cómo se genera una excepción, en el ambiente de programación debe definirse como interceptarlas. Por eso el manejo de excepciones depende mucho de la plataforma en que corra el programa. En el caso de los sistemas Unix, el soporte para manejo de excepciones se da por medio de las funciones setjump() y longjump(), aunque los lenguajes de programación para una plataforma a veces incluyen un soporte más extenso, como es el caso de ADA y C++. Lo más usual es identificar cada una de las posibles excepciones por medio de un Código de Excepción. La siguiente es pequeña lista de algunas excepciones "famosas" de Turbo Pascal: CONST No_Exception = 0; { ZERO } { DOS Errors: [1..99] } File_Not_Found File_Access_Denied = = 2; 5; {...} { I/O Errors: [100..149] } Disk_Read_Error = 100; Disk_Write_Error Disk_Full File_Not_Open = 101; = Disk_Write_Error; = 103; { Critical Errors: [150..199] } Device_Write_Fault = 160; Device_Read_Fault = 161; { Fatal Errors: [200..255] } Division_by_Zero = 200; Range_Check_Error = 201; Floating_Point_Overflow = 205; Floating_Point_Underflow = 206; Invalid_Floating_Point_Opr = 207; En el caso del Turbo Pascal, lo que sucede es que cuando un procedimiento de biblioteca se encuentra con una falla, o cuando se recibe un código que amerita la cancelación del programa, entonces el ambiente Turbo Pascal invoca a la función apuntada por la variable global System.ExitSave. Como este comportamiento está claramente documentado, para implementar la unidad except.pas bastó usar a ExitSave para interceptar todas las excepciones que se producen en tiempo de ejecución. El código de excepción siempre se encuentra en la variable global System.ExitCode. La programación con Excepciones Una vez que el programador cuenta con la posibilidad de manejar excepciones también necesita poder generarlas en sus programas. Para esto debe utilizar a las rutinas Signal(n), Throw(n) y Re_Throw(). Estas operaciones lo que hacen es simular que en tiempo de ejecución se ha producido la excepción número "n". Por ejemplo, para simular que se ha dado un error por división por cero, el programador puede ejecutar una de estas operaciones: Throw(Division_by_Zero); Signal(Division_by_Zero); Throw(200); Signal(200); La diferencia al regresar a Exception() desde Throw(), Re_Throw() o Signal() es que en este caso el valor retornado por Exception() es TRUE. De esta manera el programador puede depurar el código de manejo de excepciones, o usarlas para cualquier otro propósito en sus programas. Cuando se ejecuta alguna de las funciones Throw(), Signal() o Re_Throw(), lo que sucede es que estas operaciones simulan un retorno a la última invocación de la función Exception(), pero el valor retornado es TRUE. Esto implica que dentro de la unidad except.pas debe manetenerse una pila de invocaciones a Exception(). Es muy importante mencionar que para evitar retornar a un contexto de excepción que ya no existe, la última instrucción en todo contexto de excepción debe ser una invocación a la operción Leave_Exception_Context(), la que se encarga de resincronizar la pila interna que contiene except.pas con la pila de ejecución del programa. Desgraciadamete, en esta implementación si el programador olvida invocar a Leave_Exception_Context() al final de cada bloque de excepción el resultado es un error que es muy difícil de encontrar, pues el paquete de excepciones tratará de ejecutar procedimientos que ya han terminado, por lo que su pila de ejecución contendrá basura. Encontrar un error de estos es realmente muy difícil, y desgraciadamente no es posible dejar de obligar al programador a invocar a Leave_Exception_Context(); la única salida de este problema es que el compilador provea el manejo de excepciones, de forma que al compilar incluya que haga el trabajo de Leave_Exception_Context(). En la Figura 1 se muestra cómo se usan los el manejadores de excepciones. La primera vez que se invoque a Exception() el valor retornado será FALSE, con lo que el computador continuará la ejecución en el bloque denotador por Proceso_Normal({...});. Cuando el programador desea invocar al manejador de excepciones, posiblemente porque detecta una situación anómala, entonces debe ejecutar el siguiente código: Throw(3); { vuelve a Exception; Catch = 3 } También podría ocurrir que un error de tiempo de ejecución produzca una excepción de código número 3 (que significa "Path_Not_Found" en Turbo Pascal), con lo que el manejador de excepciones sería invocado. El resultado será que se transferirá el control de ejecución a la última (más reciente) invocación activa del procedimiento Exception(), y el valor que se retornará será TRUE. Esto hará que el control se transfiera a la parte ELSE del IF, y luego el valor retornado por Catch() será 3. El argumento de Throw(3) es el código de error, o número de excepción, que sirve para identificar a cada una de las excepciones para las que el programador ha implementado un manejador de excepciones. En la Figura 1 se incluye código para procesar las excepciones 1, 2, 3, 6 y 8. Si el código enviado por Throw() no es uno de éstos, entonces la cláusula ELSE del CASE se ejecutará. La utilidad de la pila interna de except.pas es precisamente recordar cuáles son todos los contextos de excepción a los que se ha entrado. El Re_Throw() actúa como un Throw(), pero envía el control al contexto asociado con el Exception() inmediatamente anterior (si lo enviara al mismo contexto entonces el programa se enciclaría). Re_Throw() simplemente ejecuta un Throw() con el último código de excepción, por lo que debe usarse únicamente dentro del código de manejo de excepciones. Si se ejecuta un Re_Throw() cuando no se ha producido una excepción, el resultado es, como mínimo, desastroso, pues el paquete de excepciones no sabrá cual es el código de excepción que debe retorno. Los nodos más viejos que están en la pila de except.pas corresponden a las primeras invocaciones de Exception(). De esta manera los procedimientos de nivel superior puedan atender excepciones que los de nivel inferior no manejan. La cadena de transferencias de control terminaría si se llegara al primer contexto de excepción, pues simplemente se cancelaría el programa para luego regresas el control al sistemas operativo. En este caso, el código de excepción indicará la razón de cancelación del programa. Excepciones y ADTs Un Tipo Abstracto de Datos [ADT] es un módulo que encapsula el acceso a una estructura de datos particular. Los ADTs más conocidos son el Arreglo, la Lista y el Arbol. Además de la modularidad de programación que los ADTs ayudan a alcanzar, también son muy importantes porque con ellos se introdujeron los concepto de constructores y destructores, que son las operaciones del ADT encargadas de inicializar y destruir los objetos que usa un programa. Un manejador de excepciones no está completo si no tiene soporte para ADTs. Para esto, es necesario que el manejador de excepciones sea capaz de destruir los objetos que están en un contexto de excepción que debe ser abandonado abruptamente cuando se produce una excepción. Como el compilador de Pascal no tiene soporte para excepciones, entonces para implementar el soporte para ADTs es necesario que el programador incluya en sus ADTs un campo que luego le permita al manejador de excepciones invocar a los destructores de los ADTs. Para esto el programador debe incluir dentro de su ADT un campo de tipo TException, que sirve para crear una lista doblemente enlazada que une a todos los ADTs que deben ser destruidos. Este campo tiene un puntero que apunta al objeto a destruir y otro que apunta al destructor del objeto. De esta manera, cuando se produce una excepción, el manejador de excepciones puede invocar al destructor del ADT. Para enlazar un ADT a la lista de excepciones el programador debe invocar a la operación: PROCEDURE Register_ADT( VAR ADT; VAR snode: TException; done : POINTER); { Instancia del ADT } { campo dentro del ADT } { Puntero al destructor } En la implementación del destructor del ADT, el programador debe invocar a la operación: PROCEDURE UnRegister_ADT(VAR snode: TException); que se encarga de desligar al ADT de la lista de excepciones. Throw() vs Signal() La diferencia entre Throw() y Signal() es que Signal() no vuelve a un contexto que esté dentro del mismo procedimiento en que es invocado, sino que siempre retorna al contexto más cercano que esté en algún procedimiento que haya invocado, directa o indirectamente, al procedimiento en donde se invoca a Signal(). Signal() ignora los contextos de excepción que están a su mismo nivel, y siempre retorna a los de algún procedimiento llamador. Por el contrario, Throw() siempre retorna al contexto de excepción más inmediato, aunque ese contexto se encuentre en el mismo procedimiento en que se invoca a Throw(). Las operaciones Throw(), Signal() y Re_Throw() tienen el mismo objetivo: transferir el control al llamado de Exception(), pero devolviendo el valor TRUE. El Throw() transfiere control al contexto de excepción más próximo, que puede estar en el mismo procedimiento en el que la invocación Throw() aparece. Signal() actúa diferente, pues inmediatamente causa la terminación del procedimiento en que aparece, y transfiere el control al siguiente contexto de manejo de excepciones. Es perfectamente válido invocar a Throw(), Signal() o Re_Throw() dentro del mismo manejador de excepciones. La sutil diferencia entre Throw() y Signal() surge para darle apoyo a una técnica de construcción de programas en la que los módulos se organizan en capas de diferente profundidad. Como estas capas corresponden a la invocación de procedimientos, la diferencia entre Throw() y Signal() puede expresarse de la siguiente manera: Signal() siempre transfiere el control a la capa superior de procesamiento, mientras que Throw() no necesariamente lo hace. En aquellos casos en que el programador no usa contextos de excepción anidados, por lo que en cada momento cualquier procedimiento tiene sólo un contexto para manejo de excepciones activo, no hay diferencia entre Throw() y Signal(). Esta implementación de Signal() difiere de la propuesta en [Lee-83], en la que invocar a Signal() siempre es equivalente a invocar a Throw() seguido por Re_Throw(). Signal() ignora todos los contextos de manejo de excepciones que están dentro del mismo procedimiento, y bifurca a algún procedimiento llamador. Programa de ejemplo useexcp.pas Para mostrar las cualidades (y defectos) del uso de excepciones hay que hecharle una ojeada al programa useexcp.pas, que realiza un trabajo bastante reducido, aunque ejercita toda la funcionalidad de except.pas. Este programa insistentemente invoca varios procedimientos para mostrar el resultado de usar las excepciones. La forma de ver qué ocurre en el programa es utilizar el depurador simbólico del ambiente de programación para ejecutarlo paso por paso, instrucción por instrucción (con las tecla F7 y F8). Es necesario establecer un punto de corte (breakpoint) para el manejador de excepciones antes de entrar al contexto de excepción, pues cuando se produce una excepción para interceptarla hay que modificar la pila de ejecución del programa, y si de antemano el operador no ha establecido el punto de corte entonces el programa continuará ejecutándose hasta terminar. En cualquier momento es posible ver cuáles son los procedimientos activos examinando la pila de ejecución del programa con la tecla Ctrl-F3. VAR clean : ARRAY[0..5] OF BYTE; {...} IF NOT Exception THEN BEGIN FOR i := 0 TO 6 DO BEGIN clean[i] := 0; { Range_Check_Error ==> => =>} END; {||} {||} Leave_Exception_Context; {\/} END { } ELSE BEGIN {||} { Exception handler } {\/} CASE Catch OF { <= <= <= <= <= <= <=} Range_Check_Error : BEGIN WriteLn('clean[',i:1,'] is out of bounce'); i := 0; END; ELSE EXIT; END { CASE } END; { Exception } Figura 2 Todo el trabajo se realiza dentro del procedimiento WORK(). Al principio se define el vector clean[] de 6 componentes con rango [0..5]. La Figura 2 contiene el primer ejemplo de uso de except.pas. Cuando en el contexto de excepción se trata de accesar el campo clean[6], que no existe pues el rango máximo es 5, se produce la excepción de rango y el control pasa al contexto de excepción que envuelve al ciclo FOR en que se produce la falla del programa. Para facilitarle al lector ubicar los puntos de salto, cada punto de salto está marcado con flechas que indican adónde será transferido el control una vez que se produzca la excepción (en este caso el lector debe poner un punto de corte en la instrucción CASE Catch OF, que es adónde comienza el manejador de excepciones). Luego se muestra que es factible detectar excepciones de sobre rebase (overflow) cuando dentro de un contexto de excepción se ejecuta la instrucción: a := Exp(100000); En este mismo ejemplo se muestra que la invocación a Re_Thorw() hace que el control pase al contexto de excepción inmediatamente superior. Más adelante al invocar al procedimiento Cicle() se muestra que es posible retornar de un procedimiento recursivo usando Signal() o Throw(). Como el procedimiento Cicle() no establece su propio contexto de excepción, el efecto de usar Signal() o Throw() en este caso es el mismo, aunque más adelante, en el procedimiento Throw_vs_Signal(), se muestra la diferencia. En todo momento es válido anidar contextos de excepción, ya sea directamente en el programa, o indirectamente al invocar una rutina que tiene su propio contexto de excepción. Algunas veces este paquete de excepciones puede detectar que el programador olvidó liberar un contexto de excepción invocando a Leave_Exception_Context(). Un ejemplo se muestra en la rutina Exception_without_Leave_Exception(). Otro ejemplo interesante del uso de excepciones es la invocación al procedimiento Deep_Throat(0), que lo único que hace es llamarse recursivamente y de cuando en cuando imprimir un puntico. Eventualmente la pila de ejecución se agota, y se produce una excepción por rebase de la pila del programa. Este ejemplo muestra una de las fortalezas de except.pas, pues pocos programas pueden soportar que se les rebase la pila de ejecución. ************ clean[6] is out of bounce Overflow found a := 0.0 ==> call Cicle(7)==> call Cicle(6)==> Cicle(6)::Throw(6) ==> call Cicle(9)==> call Cicle(8)==> Cicle(8)::Signal(8) ==> call Cicle(2)==> call Cicle(1)==> call Cicle(0)==> Cicle(0)::Throw(0) 0 1 2 3 Level #3 Throw(25) received ==> Signal(NINE) a := 246913578.0 i := 0 I will create some exception contexts ==> I will NOT release them!!! ++++++++++++++++++----- (40) As except.pas is dumb, this gets executed ====> I cleaned up the mess!!! [but I should fix the program] Couldn't fix the exception stack Exception stack cleared! Ctrl-Break cannot be detected within the IDE (0)... Program_Stack_Overflow clean[6] is out of bounce ExitProcedure [System.ExitCode = 0] Figura 3 A fin de cuentas, el resultado de ejecutar useexcp.pas se muestra en la Figura 3. Algunos de los mensajes fueron producidos por la rutina ExitProcedure() que se ejecuta cuando el programa termina; los otros sirven para ver cómo se desarrolla la ejecución cada vez que se produce una excepción. Al ejecutar a useexcp.pas también se puede observar cómo funciona except.pas, entrando con F7 a ver qué hace cada uno de las rutinas que exporta esa unidad. Restricciones de except.pas Ya se mencionó que esta implementación de excepciones requiere que el programador "recuerde" siempre cerrar cada contexto de excepción invocando a Leave_Exception_Context(). Esto es muy incómodo, pero necesario porque no siempre es posible saber cuál contexto de excepción está todavía activo. Si except.pas no fuera un módulo externo al lenguaje Turbo Pascal, entonces el compilador podría insertar la invocación a Leave_Exception_Context() automáticamente; pero no parece que este tipo de soporte se vaya a incluir para el lenguaje Turbo Pascal. El otro problema que debe enfrentar el programador cuando usa el paquete de excepciones es que en algunos casos pueden quedar variables asignadas en memoria dinámica que no pueden destruirse porque para manejar excepción se ha debido abandonar abruptamente un procedimiento. Por ejemplo, si el procedimiento Proc() llama a Explota(), y Explota() crea algunas variables en memoria dinámica, como los punteros a esas variables son valores locales a Explota(), cuando el manejador de excepciones transfiere el control de ejecución a Proc() a causa de una excepción, los punteros a las variables creadas en memoria dinámica por Explota() se pierden porque ya no existen las variables locales de Explota(). El resultado es que queda un poco de memoria dinámica asignada que no puede ser recuperada. Una solución paracial para este problema es que el programador use ADTs, y nuevamente recuerde registrarlos al contexto de excepción al implementar los constructores y destructores, para que el manejador de excepciones se encarga de invocar a sus destructores si han sido debidamente registrados en su contexto de excepción. Otra solución al problema de la memoria dinámica es que el programador se preocupe de desasignar la memoria dinámica utilizado en antes de invocar a Throw() o Signal(). Nuevamente, la responsabilidad de restaurar el estado correcto del programa recae sobre el programador, quien para hacerlo estaría obligado a incluir código en el ELSE del IF en que se invoca a Exception(), que es donde se establece el contexto de excepción: este código se encargaría de desasignar la memoria dinámica que haya sido asignada en el contexto de excepción, para luego ejecutar un Re_Throw() que pase la excepción sea al contexto de excepción inmediatamente superior. En [Ich-79] se discute cómo lograr esto. Como el código del manejador excepciones tiene acceso a las variables del programa, pues el manejador aparece en el ELSE del IF que implementa el contexto de excepción, el programdor puede encargarse de destruir manualmente las variables de memoria dinámica que haya usado. A veces da pereza destruir esas variables, principalmente si no es posible utilizar el código que las destruye cuando no ha ocurrido una excepción. Cuando se produce una excepción el manejador de excepciones la intercepta, y retorna al contexto de excepción adecuado indicando un código de error. Sin embargo, en muchas aplicaciones este código de error no da suficiente información como para resolver adecuadamente el problema que produjo la excepción. Por eso algunos autores exigen que en lugar de un "código", el manejador de excepciones pueda producir un "mensaje" de excepción. Este es el enfoque que se ha usado en C++ [Str-88b]. A todas luces es mejor retornar mensajes que códigos, pero para eso se necesita asistencia del compilador. La solución que aquí se presenta es parcial, pero tiene la ventaja de que es suficiente para muchas como aplicaciones, y es adecuada para que los estudiantes aprendan a escribir programas usando contextos de excepción. El problema de como mezclar bien un paquete de excepciones con un eficiente manejador de memoria dinámica es muy difícil de resolver. Por ejemplo, el manejo de excepciones que se incluyó en el lenguaje C++ debió esperar casi dos años antes de ser aceptado, pues el uso excepciones complica la administración de la memoria dinámica. Para implementar except.pas ha sido necesario manipular los registros de estado del programa en tiempo de ejecución. El código ha sido probado con las versiones más importantes del compilador Turbo Pascal (v4.0, v5.0, v5.5, v6.0, v7.0) pero podría darse el caso de que cambiara la forma de invocar procedimientos en una versión posterior: entonces debería cambiarse esta implementación. Sin embargo, lo más posible es que no haya que hacer esto cambios para soportar futuras versiones de Turbo Pascal. Este paquete sólo da soporte a ambientes MS-DOS monousuario. Si se desea escribir programas que puedan crear tareas concurrentes, será necesario modificar significativamente la implementación de esta biblioteca, pues en estos momentos además de que ha sido implementada usando lenguaje de máquina x86, la pila para anotar cuáles son los contextos de excepción que está dentro de la unidad except.pas es una variable global. Esto impide compartirla entre varias tareas que nacen del mismo programa. Conclusiones Esta implementación obliga al programador a ser cuidadoso al usar excepciones, pues por un pequeño descuido pueden quedar variables asignadas en memoria dinámica "guindando". Pese al cúmulo de restricciones que presenta, esta paquete para el manejo de excepciones en Pascal es muy útil por lo menos en un ambiente académico, pues los estudiantes pueden usarlo para aprender a usar esta importante herramienta de programación. Como Turbo Pascal corre en máquinas muy pequeñas, este paquete puede ser usado prácticamente en cualquier ambiente. Agradecimientos Cuatro personas han trabajado para lograr implementar except.pas. David Chaves fue quien programó inicialmente la mayor parte del código ensamblador de la unidad Except. Luego William Martínez tuvo la paciencia de buscarle errores a la primera implementación. Por último, Gustavo Alonso Sanabria quien tuvo la pacienca de depurar el código. Además, William y Gustavo le hicieron algunas modificaciones al paquete original definido por Adolfo, lo que ha resultado en un sistema más fácil de usar. Reconocimientos Esta investigación se realizó dentro del proyecto de investigación 32989-019 "Estudios en la tecnología de programación por objetos y C++", inscrito ante la Vicerrectoría de Investigación de la Universidad de Costa Rica. La Escuela de Ciencias de la Computación e Informática también ha aportado fondos para este trabajo. Bibliografía [DiM- Di Mare, Adolfo: Convenciones de Programación para Pascal, 88] Reporte Técnico ECCI-01-88, Proyecto 326-86-053, Escuela de Ciencias de la Computación e Informática, Universidad de Costa Rica, 1988. [BIBorland International: Turbo Pascal Reference Manual version 88] 5.5, Borland International, California (U.S.A.), 1988. [Col- Colvin, Gegory: Exception Handling in ANSI C, C/C++ Users 91] Journal, Vol.9 No.8, pp [77-78,83-88], Agosto 1991. [Ich- Ichbiah, J.D et al: Rationale for the Design of the ADA 79] Programming Language, SigPlan Notices, Vol.14 No.6, Junio 1979. [Ker- Kernighan, Brian: El lenguaje de programación C, Prentice Hall; 86] 1986. [Lee- Lee, P. A.: Exception Handling in C Programs, Software Practice 83] and Experience, Vol.13, pp 389-405. [LG- Liskov, Barbara & Gutag, John: Abstraction and Specification in 86] Program Development, McGraw-Hill, 1986. [Str- Stroustrup, Bjarne: The C++ Programming Language, Addison86] Wesley, 1986. [Str- Stroustrup, Bjarne: What is Object-Oriented Programming, IEEE 88a] Transactions on Software Engineering, Mayo 1988. [Str- Stroustrup, Bjarne: C++ Exception Handling, IEEE Transactions on 88b] Software Engineering, Mayo 1988. [Vid- Vidal, Carlos: Exception Handling, C/C++ Users Journal, 92] Vol.10 No.9, pp [19-27], Septiembre 1992. http://www.di-mare.com/adolfo/p/except.htm 7.5.2 Lanzamiento de excepciones. Manejo de excepciones Introducción ¿Qué es el Manejo de Excepciones? El manejo de excepciones es un mecanismo de algunos lenguajes de programación, que permite definir acciones a realizar en caso de producirse una situaciones anomala (excepción) producida durante la ejecución del programa. La acción a realizar suele consistir en una acción correctiva, o la generación de un informe acerca del error producido. Ejemplos de lenguajes con gestión de excepciones son Java, Net, C++, Objective-C, Ada, Eiffel, y Ocaml. ¿Como se implementa? Los lenguajes con gestión de excepciones incorporan en sus bibliotecas la capacidad de detectar y notificar errores. Cuando un error es detectado se siguen estas acciones: 1. Se interrumpe la ejecución del código en curso. 2. Se crea un objeto excepción que contiene información del problema. Esta acción es conocida como "lanzar una excepción".Existe un mecanismo estandar de gestión de error 3. Si hay un manejador de excepciones en el contexto actual le transfiere el control. En caso contrario, pasa la referencia del objeto excepción al contexto anterior en la pila de llamadas. 4. Si no hay ningún manejador capaz de gestionar la excepción, el hilo que la generó es terminado. Por ejemplo, si intentamos leer un fichero inexistente usando la clase FileReader del lenguaje Java, la implementación de la propia clase detectará el problema, y lanzará una excepción de tipo FileNotFoundException. ¿Por qué es importante usar excepciones? El código de tratamiento del error está separado del resto del programa. Esto aumenta la legibilidad y permite centrarse en cada tipo de código. Un mismo manejador puede gestionar las excepciones de varios ámbitos inferiores. Esto reduce la cantidad de código necesaria para gestionar los errores. Existe un mecanismo estandar de gestión de error. Lenguajes anteriores como C empleaban el retorno de valores especiales en cada método para señalar condiciones anomalas. Esto implicaba que a cada llamada debía seguirle un código de gestión de errores que enturbiaba la legibilidad del código. Implementación en Java La captura de excepciones se implementa en el lenguaje Java con el bloque try/catch/finally. El capítulo 9 de Thinking in Java es una excelente introducción al tema (que vale la pena leer aunque seas un programador senior). 1 2 3 4 5 6 7 try { // código sospechoso de producir excepciones } catch(Excepcion e){ // registra el error y/o relanza la excepción } finally { // libera recursos } La excepción a capturar puede ser cualquier subclase de Throwable. El código del finally se ejecutará siempre. Una excepción en el try, o en el bloque catch no impedirán su ejecución. Si el bloque finally lanza una excepción, quedarán descartadas excepciones anteriores. Es decir, una excepción en el finally oculta excepciones anteriores. Throwable Throwable es la superclase de todos los objetos excepción: java.lang.Object java.lang.Throwable java.lang.Exception java.lang.RuntimeException Según su superclase, hay dos tipos de excepciones: Checked: el programa no compila si no existe un manejador de excepciones. Son las subclases de Exception que no son subclases de RuntimeException. Unchecked: su captura no es obligatoria. Son las subclases de RuntimeException. Nota Lo lógico sería que RuntimeException fuera subclase de Throwable, pero por algún motivo historico (?) no es así. En el constructor de Throwable se puede especificar: Un mensaje indicando el motivo de la excepción. Una referencia a otro objeto Throwable asociado (solo desde 1.4). Esto permite indicar que la causa de esta excepción fue otra. Throwable() Throwable(String message) Throwable(Throwable cause) // 1.4 Throwable(String message, Throwable cause) // 1.4 Nota En JDK 1.3. puedes usar NestableException para implementar excepciones anidadas. El método getStackTrace() devuelve un array de elementos StackTraceElement, que representan cada uno (excepto el primero en la pila) una llamada a un método. Cuando ejecutamos Throwable.printStacktrace() obtenemos una representación como cadena de estos objetos. Observa que según esto, todos los objetos derivados de Throwable proporcionan la información siguiente: El tipo de excepción (la clase). Donde ocurrió la excepción (el stacktrace). Un mensaje de error. Un ejemplo completo Un ejemplo completo: El siguiente programa realiza la división entera de dos números pasados por consola: 1 import java.math.BigInteger; 3 public class Main { 5 private static void usage(){ 6 System.out.println("USAGE: java Main number1 number2"); 7 } 9 private static void divide(String a, String b){ 10 try { 11 BigInteger bi1 = new BigInteger(a); 12 BigInteger bi2 = new BigInteger(b); 13 System.out.println( bi1.divide(bi2) ); 14 } catch (NumberFormatException e2) { 15 System.err.println("Not a number! " + e2.getMessage()); 16 usage(); 17 } 18 } 20 public static void main(String[] args) { 21 divide(args[0],args[1]); 22 } 24 } http://www.1x4x9.info/files/excepciones/html/online-chunked/ Tratamiento de excepciones §1 Introducción El problema de la seguridad es uno de los clásicos quebraderos de cabeza de la programación. Los diversos lenguajes han tenido siempre que lidiar con el mismo problema: ¿Que hacer cuando se presenta una circunstancia verdaderamente imprevista? (por ejemplo un error). El asunto es especialmente importante si se trata de lenguajes para escribir programas de "Misión crítica"; digamos por ejemplo controlar los ordenadores de una central nuclear o de un sistema de control de tráfico aéreo. Antes que nada, digamos que en el lenguaje de los programadores C++ estas "circunstancias imprevistas" reciben el nombre de excepciones, por lo que el sistema que implementa C++ para resolver estos problemas recibe el nombre de manejador de excepciones. Así pues, las excepciones son condiciones excepcionales que pueden ocurrir dentro del programa durante su ejecución (por ejemplo, que ocurra una división por cero, se agote la memoria disponible, etc.) que requieren recursos especiales para su control. En este capítulo trataremos del manejador de excepciones C++; una serie de técnicas que permiten formas normalizadas de manejar los errores, intentando anticiparse a los problemas potenciales previstos e imprevistos. Así como permitir al programador reconocerlos, fijar su ubicación y corregirlos. §2 Manejo de excepciones en C++ El manejo de excepciones C++ se basa en un mecanismo cuyo funcionamiento tiene tres etapas básicas: 1: Se intenta ejecutar un bloque de código y se decide que hacer si se produce una circunstancia excepcional durante su ejecución. 2: Se produce la circunstancia: se "lanza" una excepción (en caso contrario el programa sigue su curso normal). 3: La ejecución del programa es desviada a un sitio específico donde la excepción es "capturada" y se decide que hacer al respecto. ¿Pero que es eso de "lanzar" y "capturar" una excepción"? En general la frase se usa con un doble sentido: Por un lado es un mecanismo de salto que transfiere la ejecución desde un punto (el que "lanza" la excepción) a otro dispuesto de antemano para tal fin (el que "captura" la excepción). A este último se le denomina manejador o "handler" de la excepción. Además del salto (como un goto), en el punto de lanzamiento de la excepción se crea un objeto, a modo de mensajero, que es capturado por el "handler" (como una función que recibe un argumento). El objeto puede ser cualquiera, pero lo normal es que pertenezca a una clase especial definida al efecto que contiene la información necesaria para que el receptor sepa que ha pasado; cual es la naturaleza de la circunstancia excepcional que ha "lanzado" la excepción [6]. Para las tres etapas anteriores existen tres palabras clave específicas: try, throw y catch. El detalle del proceso es como sigue. §2.1 Intento ( try ). En síntesis podemos decir que el programa se prepara para cierta acción, decimos que "lo intenta". Para ello se especifica un bloque de código cuya ejecución se va a intentar ("Tryblock") utilizando la palabra clave try. try { // bloque de código-intento ... } El juego consiste en indicar al programa que si existe un error durante el "intento", entonces debe lanzar una excepción y transferir el control de ejecución al punto donde exista un manejador de excepciones ("Handler") que coincida con el tipo lanzado. Si no se produce ninguna excepción, el programa sigue su curso normal. De lo dicho se deduce inmediatamente que se pueden lanzar excepciones de varios tipos y que pueden existir también receptores (manejadores) de varios tipos; incluso manejadores "universales", capaces de hacerse cargo de cualquier tipo de excepción. A la inversa, puede ocurrir que se lance una excepción para la que no existe manejador adecuado, en cuyo caso... (la solución más adelante). Así pues, try es una sentencia que en cierta forma es capaz de especificar el flujo de ejecución del programa. Un bloqueintento debe ser seguido inmediatamente por el bloque manejador de la excepción. §2.2 Se lanza una excepción ( throw ). Si se detecta una circunstancia excepcional dentro del bloque-intento, se lanza una excepción mediante la ejecución de una sentencia throw. Por ejemplo: if (condicion) throw "overflow"; Es importante advertir que, salvo los casos en que la excepción es lanzada por las propias librerías C++ (como consecuencia de un error 1.6.1a), estas no se lanzan espontáneamente. Es el programador el que debe utilizar una sentencia (generalmente condicional) para en su caso, lanzar la excepción. El lenguaje C++ especifica que todas las excepciones deben ser lanzadas desde el interior de un bloque-intento y permite que sean de cualquier tipo. Como se ha apuntado antes, generalmente son un objeto (instancia de una clase) que contiene información. Este objeto es creado y lanzado en el punto de la sentencia throw y capturado donde está la sentencia catch. El tipo de información contenido en el objeto es justamente el que nos gustaría tener para saber que tipo de error se ha producido. En este sentido puede pensarse en las excepciones como en una especie de correos que transportan información desde el punto del error hasta el sitio donde esta información puede ser analizada. §2.3 La excepción es capturada en un punto específico del programa ( catch ). Esta parte del programa se denomina manejador ("handler"); se dice que el "handler" captura la excepción. El handler es un bloque de código diseñado para manejar la excepción precedido por la palabra catch. El lenguaje C++ requiere que exista al menos un manejador inmediatamente después de un bloque try. Es decir, se requiere el siguiente esquema: try { // bloque de código que se intenta ... } catch (...) { // bloque manejador de posibles excepciones ... } ... // continua la ejecución normal El "handler" es el sitio donde continua el programa en caso de que ocurra la circunstancia excepcional (generalmente un error) y donde se decide que hacer. A este respecto, las estrategias pueden ser muy variadas (no es lo mismo el programa de control de un reactor nuclear que un humilde programa de contabilidad). En último extremo en caso de errores absolutamente irrecuperables, la opción adoptada suele consistir en mostrar un mensaje explicando el error. Puede incluir el consabido "Avise al proveedor del programa" o bien generar un fichero texto (por ejemplo: error.txt) con la información pertinente, que se guarda en disco con objeto de que pueda ser posteriormente analizado y corregido en sucesivas versiones de la aplicación [2]. Llegados a este punto debemos recordar que, como veremos en los ejemplos, las excepciones generadas pueden ser de diverso tipo (según el tipo de error), y que también pueden existir doversos manejadores. De hecho se debe incluir el manejador correspondiente a cada excepción que se pueda generar. §3 Resumen Hemos dicho que try es una sentencia que en cierta forma es capaz de especificar el flujo de ejecución del programa; en el fondo el mecanismo de excepciones de C++ funciona como una especie de sentencia if ... then .... else, que tendría la forma: If { este bloque se ejecuta correctamente } then seguir la ejecución normal de programa else // tres acciones sucesivas. a. Crear un objeto con información del suceso (excepción) b. Transferir el control de ejecución al "handler" correspondiente c. Recibir el objeto para su análisis y decisión de la acción a seguir en este caso la sintaxis utilizada es la siguiente: try { // bloque de código que se intenta ... } catch (...) { // captura de excepciones ... } ... // continua la ejecución normal El diseño del mecanismo de excepciones C++, someramente expuesto, tiene la ventaja de permitir resolver una situación muy frecuente: el bloque en que se detecta el error no sabe que hacer en tal caso (cuando se presenta el error o excepción); la acción depende en realidad de un nivel anterior, el módulo que invocó la operación. Como decimos, esta situación es muy frecuente ( ), entre otras razones porque si un módulo pudiera anticipar un error por si mismo, también podría evitarlo, con lo que no habría necesidad de mecanismo de excepciones. Esta circunstancia es especialmente patente en el caso de librerías, en las que el autor generalmente no sabe ni puede hacer nada al respecto de ciertos errores a excepción de informar al usuario. Lo anterior no es óbice para que, como buena práctica de programación, se intente la captura sistemática de errores lo más próximo posible a su identificación, dejando el mecanismo de excepciones para las situaciones realmente imprevisibles. Por ejemplo, siempre que sea posible: if (b == 0) { /* alguna acción... */; } else x = a/b; §4 Precauciones Cuando se plantean este tipos de cuestiones de seguridad surge inevitablemente una pregunta: ¿Que sucede si se producen errores (circunstancias excepcionales) durante el proceso de control de excepciones?. La respuesta más honesta es que el sistema perfecto e invulnerable no existe. Aunque el sistema de excepciones de C++ es una formidable herramienta para controlar imprevistos, que permite hasta cierto punto controlar imprevistos dentro de imprevistos. A pesar de ello, nada puede sustituir a una programación cuidadosa. El propio Stroustrup advierte: "Aunque las excepciones se pueden usar para sistematizar el manejo de errores, cuando se adopta este esquema, debe prestarse atención para que cuando se lance una excepción no cause más problemas de los que pretende resolver. Es decir, se debe prestar atención a la seguridad de las excepciones. Curiosamente, las consideraciones sobre seguridad del mecanismo de excepciones conducen frecuentemente a un código más simple y manejable". Comentarios sobre la idoneidad del mecanismo de excepciones ( 1.6w2). §5 Tipos de excepciones Durante la ejecución de un programa pueden existir dos tipos de circunstancias excepcionales: síncronas y asíncronas. Las primeras son las que ocurren dentro del programa. Por ejemplo, que se agote la memoria o cualquier otro tipo de error. Son a estas a las que nos hemos estado refiriendo. En la introducción (§1 ) hemos indicado: las excepciones son condiciones excepcionales que pueden ocurrir dentro del programa..." y las únicas que se consideran. Las excepciones asíncronas son las que tienen su origen fuera del programa, a nivel del Sistema Operativo. Por ejemplo que se pulsen las teclas Ctrl+C. Generalmente las implementaciones C++ solo consideran las excepciones síncronas, de forma que no se pueden capturar con ellas excepciones tales como la pulsación de una tecla. Dicho con otras palabras: solo pueden manejar las excepciones lanzadas con la sentencia throw. Siguen un modelo denominado de excepciones síncronas con terminación, lo que significa que una vez que se ha lanzado una excepción, el control no puede volver al punto que la lanzó. Nota: El "Handler" no puede devolver el control al punto de origen del error mediante una sentencia return. En este contexto, un return en el bloque catch supone salir de la función que contiene dicho bloque. El sistema Estándar C++ de manejo de excepciones no está diseñado para manejar directamente excepciones asíncronas, como las interrupciones de teclado, aunque pueden implementarse medidas para su control. Además las implementaciones más usuales disponen de recursos para menejar las excepciones del Sistema Operativo. Por ejemplo, C++ Builder dispone de los mecanismos adecuados para manejar excepciones de Windows-32 (asíncronas) a través de su librería VCL [1] ( 1.6w1). §6 Secuencia de ejecución Como puede verse, la filosofía C++ respecto al manejo de excepciones no consiste en corregir el error y volver al punto de partida. Por el contrario, cuando se genera una excepción el control sale del bloque-intento try que lanzó la excepción (incluso de la función), y pasa al bloque catch cuyo manejador corresponde con la excepción lanzada (si es que existe). A su vez el bloque catch puede hacer varias cosas: Relanzar la misma excepción ( 1.6.1). Saltar a una etiqueta ( 1.6.2) Terminar su ejecución normalmente (alcanzar la llave } de cierre). Si el bloque-catch termina normalmente sin lanzar una nueva excepción, el control se salta todos los bloques-catch que hubiese a continuación y sigue después del último. Puede ocurrir que el bloque-catch lance a su vez una excepción. Lo que nos conduce a excepciones anidadas. Esto puede ocurrir, por ejemplo, cuando en el proceso de limpieza de pila ("Stack unwinding") que tienen lugar tras una excepción, un destructor lanza una excepción . Como se verá en los ejemplos, además del manejo y control de errores, las excepciones C++ pueden utilizarse como un mecanismo de return o break multinivel, controlado no por una circunstancia excepcional, sino como un acto deliberado del programador para controlar el flujo de ejecución [5]. Ejemplos: ( 4.5.8). §7 Constructores y destructores en el manejo de excepciones Cuando en el lanzamiento de excepciones se utilizan objetos por valor, throw llama al constructor copia ( 4.11.2d4). Este constructor inicializa un objeto temporal en el punto de lanzamiento. Si ocurren errores durante la construcción de un objeto, los constructores pueden lanzar excepciones [3]. Si un constructor lanza una excepción, el destructor del objeto no es llamado necesariamente. Cada vez que desde dentro de un bloque-try se lanza una excepción y el control sale fuera de bloque, tiene lugar un proceso de búsqueda y desmontaje descendente en la pila hasta encontrar el manejador ("Catcher") correspondiente. Durante este proceso, denominado "Stack unwinding", todos los objetos de duración automática que se crearon hasta el momento de ocurrir la excepción, son destruidos de forma controlada mediante llamadas a sus destructores. Si uno de los destructores invocados provoca a su vez una excepción que no tiene un "handler" adecuado, se produce una llamada a la función terminate ( 1.6.3). Nota: Observe que no se menciona para nada la destrucción de objetos persistentes que se hubiesen creado entre el inicio del bloque try y el punto de lanzamiento de la excepción. Esto origina una difícil convivencia entre el mecanismo de excepciones y el operador new. Para resolver los problemas potenciales deben adoptarse precauciones especiales ( 4.9.20). La invocación a los destructores de los objetos automáticos, se realiza solo para aquellos objetos que hubiesen sido construidos totalmente a partir de la entrada en el bloque-intento (objetos cuyos constructores hubiesen finalizado satisfactoriamente). Si los objetos tienen sub-objetos, la invocación solo se realiza para los destructores de la clase-base. Tema relacionado: Control de recursos ( 4.1.5a) Nota: En el caso del C++Builder, los destructores son llamados por defecto, pero puede evitarse mediante la opción -xd- del compilador como se indica a continuación. Esta opción, como otras de este tipo, está gobernada por el valor de una constante global ( 1.4.1a). §8 Establecer opciones de manejo de excepciones En C++Builder se pueden establecer comandos de compilación ( 1.4.3) para determinar el tratamiento que seguirá el manejo de excepciones; son los siguientes: Opción Descripción -x Habilita manejo de excepciones C++ (activo por defecto). -xd Habilita limpieza total. Llamada a los destructores para todos los objetos declarados automáticos (locales) entre el ámbito del capturador (catch) y el lanzador (throw) de la excepción cuando es lanzada la excepción (activo por defecto). Si se activa esta opción, también hay que activar la opción –RT ( 4.9.14). -xp Habilita información sobre las excepciones. Posibilita identificación de excepciones en tiempo de ejecución mediante la inclusión en el objeto del número de líneas del código fuente. Esto permite al programa interrogar el fichero y número de línea donde ha ocurrido una excepción utilizando los identificadores globales __ThrowFileName y __ThrowLineNumber. http://www.zator.com/Cpp/E1_6.htm Excepciones Durante la ejecución, las clases pueden provocarse errores de diferentes tipos y diversos grados de gravedad.. Cuando se invocan métodos sobre un objeto, se puede encontrar con problemas internos de estado(valores incongruentes ), detectar errores con los objetos o datos que manipula ( como la dirección a un archivo o red), querer accesar sobre un archivo ya cerrado u otros problemas. Proporcionan una manera de verificar los errores y poder controlarlos si fuera el caso si abortar el código. Las excepciones hacen que las situaciones de error que puede señalar un método sean parte explícita del contrato del mismo. Proceso de excepción: - se lanza una excepción cuando detecta un error - la excepción es capturada por una cláusula que delimita al método Para Java las excepciones son objetos. try, catch y finally Las excepciones se capturan encerrando código en bloques try: try bloque catch (excepción_tipo identificador) bloque catch (excepción_tipo identificador) bloque ... finally bloque El cuerpo de la sentencia try se ejecuta hasta que se lanza una excepción o se acaba con éxito. Si se lanza una excepción se examinan las cláusulas catch con el fin de encontrar la excepción que se quiere controlar. Si en un try también se encuentra un finally, su código se ejecuta una vez que se haya completado todas las sentencias del try. Se ejecutan todas las sentencias del bloque finally se lance o no una excepción. public boolean searchFor(String file, String world) throws StreamException { Stream input =null; try{ input= new Stream(file); while(!input.eof()) { if (input.next() == world) return true; return false; } } finally { if (input !=null) input.close(); } } El contrato definido por la cláusula throws se aplica estrictamente: sólo se puede lanzar un tipo de excepción que haya sido declarado en la cláusula throws. En el siguiente bloque se atrapa la excepción que se alcanza el final de la entrada: try { while(( token = stream.next() ) !=Stream.END) process(token); } catch (StreamEndException e) {stream.close(); } La clase Throwable es la superclases de todos los errores y excepciones en Java. Existen excepciones implícitas y explícitas: Un ejemplo de atrapar una excepción(explícita): try { int a[] = new int[2]; a[4]=10; } catch (ArrayIndexOutOfBoundsException e) { System.out.println("exception: " + e.getMessage()); e.printStackTrace(); } (excepción implícita) int a[]=new int[2]; a[4]=10; public class Prueba{ public static void main(String args[]) { int dato1=0, dato2=0, dato3=0; try{ dato1++; dato3=dato1/dato2; dato2++; } catch (ArithmeticException t) { System.out.println("Error de division "+t.getMessage()); dato3=0; } System.out.println(dato1 + " "+dato2+" "+dato3); } } Las excepciones en Java son objetos de clases derivas de la clase Throwable definida en el paquete java.lang Throwable Error Exception o o o RuntimeException ClassNotFoundException IOException EOFException La clase Exception cubre las excepciones que puede ejecutar una aplicación. RuntimeException controla las excepciones ocurridas al ejecutar operaciones sobre datos que maneja una aplicación y que se encuentran en memoria. IOException controla las excepciones sobre operaciones de entrada y salida. Lanzar una excepción equivale a lanzar un objeto de la clase de la excepción que se va a manipular. if (t== null) throw new NullPointerException(); Creación de propias Excepciones Para denotar un error en particular de las clases que están en un paquete. En donde también la herencia es importante. class SimpleException extends Exception {} public class SimpleExeptionDemo { public void f() throws SimpleException { System.out.println(" Throwing SimpleException form f() "); trow new SimpleException(); } public static void main(String[] args) { SimpleExceptionDemo sed=new SimpleExceptionDemo(); try{ sed.f(); }catch(SimpleException e) { System.out.println("Caught it!");} } } http://mail.udlap.mx/~sainzmar/is117/excepciones.html Relanzar una Exception Existen algunos casos en los cuales el código de un método puede generar una Exception y no se desea incluir en dicho método la manejo del error. Java permite que este método pase o relance (throws) la Exception al método desde el que ha sido llamado, sin incluir en el método los bucles try/catch correspondientes. Esto se consigue mediante la adición de throws más el 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(). El ejemplo anterior (metodo1) realizaba la manejo de las excepciones dentro del propio método. Ahora se presenta un nuevo ejemplo (metodo2) que relanza las excepciones al siguiente método: void metodo2() throws IOException, MyException { ... // Código que puede lanzar las excepciones IOException y MyException ... } // Fin del metodo2 Según lo anterior, si un método llama a otros métodos que pueden lanzar excepciones (por ejemplo de un paquete de Java), tiene 2 posibilidades: 1. 1.Capturar las posibles excepciones y manejarlas. 2. 2.Desentenderse de las excepciones y remitirlas hacia otro método anterior en el stack para éste se encargue de manejarlas. Si no hace ninguna de las dos cosas anteriores el compilador da un error, salvo que se trate de una RuntimeException. http://sai.azc.uam.mx/apoyodidactico/edoo/Unidad5/edoo5.h tml 7.6 Excepciones definidas por el usuarios. Excepciones definidas por el usuario Los programas pueden nombrar sus propias excepciones asignando una cadena a una variable o creando una nueva clase de excepción. Por ejemplo: >>> class MiError: ... def __init__(self, valor): ... self.valor = valor ... def __str__(self): ... return `self.valor` ... >>> try: ... raise raise MiError(2*2) ... except MiError, e: ... print 'Ha saltado mi excepción, valor:', e.valor ... Ha saltado mi excepción, valor: 4 >>> raise mi_exc, 1 Traceback (innermost last): File "<stdin>", line 1 mi_exc: 1 Muchos módulos estándar utilizan esto para informar de errores que pueden ocurrir dentro de las funciones que definen. Hay más información sobre las clases en el capítulo , ``Clases''. http://pyspanishdoc.sourceforge.net/tut/node10.html#SECTION0010500000000000000000 Hay dos tipos de excepciones: las predefinidas y las definidas por el usuario. Excepciones definidas por el usuario Son errores que están definidas en el programa, el cual no necesariamente es un error de Oracle. Las excepciones se declaran en la sección declarativa de un bloque PL/SQL. Las excepciones tiene un tipo (EXCEPTION) y un ambiente. DECLARE e_error1 BEGIN . . . END; EXCEPTION Excepciones predefinidas Las excepciones predefinidas corresponden a errores comunes en SQL. Error de Oracle ORA-0001 ORA-0051 ORA-0061 ORA-1001 ORA_1012 ORA-1017 ORA-1403 ORA-1422 ORA-1476 ORA-1722 ORA-6500 ORA-6501 ORA-6502 ORA-6511 Excepción equivalente Descripción DUP_VAL_ON_INDEX Restricción de unicidad violada Tiempo fuera ocurrido mientras TIMEOUT_ON_RESOURCE esperaba un recurso La transacció fue desecha por un TRANSACTION_BACKED_OUT "bloqueo mortal" INVALID_CURSOR Operación ilegal con un cursor NOT_LOGGED_ON Sin conexión a Oracle LOGIN_DENIED Nombre de usuario o password inválido NO_DATA_FOUND No se encontraron datos La instrucción SELECT ... INTO TOO_MANY_ROWS devuelve más de un registro ZERO_DIVIDED División por cero INVALID_NUMBER Conversión inválida a un número Error PL/SQL interno lanzado al STORAGE_ERROR exceder la memoria PROGRAM_ERROR Error PL/SQL interno Error al trucar o convertir valores, o en VALUE_ERROR una operación aritmética Al intentar abrir un cursor que ya está CURSOR_ALREADY_OPEN abierto http://www.lania.mx/biblioteca/seminarios/basedatos/plsql/errores/declaracion.html 7.6.1 Clase base de las excepciones. Excepciones internas Las excepciones pueden ser objetos de una clase u objetos cadena. Aunque la mayoría de las excepciones eran objetos cadena en las anteriores versiones de Python, en Python 1.5 y versiones posteriores, todas las excepciones estándar han sido convertidas en objetos de clase y se anima a los usuarios a que hagan lo propio. Las excepciones están definidas en el módulo exceptions. Nunca es necesario importar este módulo explícitamente, pues las excepciones vienen proporcionadas por el espacio nominal interno. Dos objetos de cadena distintos con el mismo valor se consideran diferentes excepciones. Esto es así para forzar a los programadores a usar nombres de excepción en lugar de su valor textual al especificar gestores de excepciones. El valor de cadena de todas las excepciones internas es su nombre, pero no es un requisito para las excepciones definidas por el usuario u otras excepciones definidas por módulos de biblioteca. En el caso de las clases de excepción, en una sentencia try con una cláusula except que mencione una clase particular, esta cláusula también gestionará cualquier excepción derivada de dicha clase (pero no las clases de excepción de las que deriva ella). Dos clases de excepción no emparentadas mediante subclasificación nunca son equivalentes, aunque tengan el mismo nombre. Las excepciones internas enumeradas a continuación pueden ser generadas por el intérprete o por funciones internas. Excepto en los casos mencionados, tienen un ``valor asociado'' indicando en detalle la causa del error. Este valor puede ser una cadena o tupla de varios elementos informativos (es decir, un código de error y una cadena que explica el código). El valor asociado es el segundo argumento a la sentencia raise. En las cadenas de excepción, el propio valor asociado se almacenará en la variable nombrada como el segundo argumento de la cláusula except (si la hay). En las clases de excepción, dicha variable recoge la instancia de la excepción. Si la clase de excepción deriva de la clase raíz estándar Exception, el valor asociado está disponible en el atributo args de la instancia de excepción y es probable que aparezca en otros atributos. El código de usuario puede lanzar excepciones internas. Se puede usar para comprobar un gestor de excepciones o para informar de una condición de error del mismo modo que el intérprete lanza la misma excepción. Hay que ser precavido, pues nada incluye que el código de usuario lance una excepción inadecuada. Las siguientes excepciones sólo se usan como clase base de otras excepciones. Exception La clase base de las excepciones. Todas las excepciones internas derivan de esta clase. Todas las excepciones definidas por usuario deberían derivarse de esta clase, aunque no es obligatorio (todavía). La función str(), aplicada a una instancia de una clase (o la mayoría de sus clases derivadas) devuelve un valor cadena a partir de sus argumentos o una cadena vacía si no se proporcionaron argumentos al constructor. Si se usa como secuencia, accede a los argumentos proporcionados al constructor (útil para compatibilidad con código antiguo). Los argumentos también están disponibles en el atributo args de la instancia, como tupla. StandardError La clase base para todas las excepciones internas excepto SystemExit. StandardError deriva de la clase raíz Exception. ArithmeticError La clase base de las excepciones lanzadas por diversos errores aritméticos: OverflowError, ZeroDivisionError, FloatingPointError. LookupError La clase base de las excepciones lanzadas cunado una clave o índice utilizado en una correspondencia (diccionario) o secuencia son incorrectos: IndexError, KeyError. EnvironmentError La clase base de las excepciones que pueden ocurrir fuera del sistema Python: IOError, OSError. Cuando se crean excepciones de este tipo con una tupla de dos valores, el primer elemento queda disponible en el atributo errno de la instancia (se supone que es un número de error) y el segundo en el atributo strerror (suele ser el mensaje de error asociado). La propia tubpla está disponible en el atributo args. Nuevo en la versión 1.5.2. Cuando se instancia una excepción EnvironmentError con una tupla de tres elementos, los primeros dos quedan disponibles como en el caso de dos elementos y el tercero queda en el atributo filename. Sin embargo, por compatibilidad con sistemas anteriores, el atributo args contiene sólo una tupla de dos elementos de los dos primeros argumentos del constructor. El atributo filename es None cuando se cree la excepción con una cantidad de argumentos diferente de 3. Los atributos errno y strerror son también None cuando la instancia no se cree con 2 ó 3 argumentos. En este último caso, args contiene los argumentos del constructor tal cual, en forma de tupla. Las siguientes excepciones son las realmente lanzadas. AssertionError Se lanza cuando una sentencia assert es falsa. AttributeError Se lanza cuando una referencia o asignación a atributo fracasa (cuando un objeto no tenga referencias o asignaciones a atributos en absoluto, se lanza, TypeError.) EOFError Se lanza cuando las funciones internas (input() o raw_input()) alcanzan un final de fichero (EOF) sin leer datos. N.B.: Los métodos read() y readline() de los objetos fichero devuelven una cadena vacía al alcanzar EOF. FloatingPointError Se lanza cuando falla una operación de coma flotante. Esta excepción siempre está definida, pero sólo se puede lanzar cuando Python esta configurado con la opción --with-fpectl o se ha definido el símbolo WANT_SIGFPE_HANDLER en el fichero config.h. IOError Se lanza cuando una operación de E/S (tal como una sentencia print, la función interna open() o un método de un objeto fichero) fracasa por motivos relativos a E/S, por ejemplo, por no encontrarse un fichero o llenarse el disco. Esta clase se deriva de EnvironmentError. En la explicación anterior se proporciona información adicional sobre los atributos de instancias de excepción. ImportError Se lanza cuando una sentencia import no encuentra la definición del módulo o cuando from ... import no encuentra un nombre a importar. IndexError Se lanza cuando un subíndice de una secuencia se sale del rango .Los índices de corte se truncan silenciosamente al rango disponible. Si un índice no es un entero simple, se lanza TypeError. KeyError Se lanza cuando no se encuentra una clave de una correspondencia (diccionario) en el conjunto de claves existentes. KeyboardInterrupt Se lanza cuando el usuario pulsa la tecla de interrupción (normalmente Control-C o DEL2.7). A lo largo de la ejecución se comprueba si se ha interrumpido regularmente. Las interrupciones ocurridas cuando una función input() o raw_input()) espera datos también lanzan esta excepción. MemoryError Se lanza cuando una operación agota la memoria pero aún se puede salvar la situación (borrando objetos). El valor asociado es una cadena que indica qué tipo de operación (interna) agotó la memoria. Obsérvese que por la arquitectura de gestión de memoria subyacente (la función de C malloc()), puede que el intérprete no siempre sea capaz de recuperarse completamente de esta situación. De cualquier modo, se lanza una excepción para que se pueda imprimir una traza, por si la causa fue un programa desbocado. NameError Se lanza cuando no se encuentra un nombre local o global. Sólo se aplica a nombre no calificados. El valor asociado es el nombre no encontrado. NotImplementedError Esta excepción se deriva de RuntimeError. En clases base definidas por el usuario, los métodos abstractos deberían lanzar esta excepción cuando se desea que las clases derivadas redefinan este método. Nuevo en la versión 1.5.2. OSError Esta clase se deriva de EnvironmentError y se usa principalmente como excepción os.error de os. En EnvironmentError hay una descripción de los posibles valores asociados. Nuevo en la versión 1.5.2. OverflowError Se lanza cuando el resultado de una operación aritmética es demasiado grande para representarse (desbordamiento). Esto no es posible en los enteros largos (que antes que rendirse lanzarían MemoryError). Por la falta de normalización de la gestión de excepciones de coma flotante en C, la mayoría de las operaciones de coma flotante, tampoco se comprueban. En el caso de enteros normales, se comprueban todas las operaciones que pueden desbordar excepto el desplazamiento a la izquierda, en el que las aplicaciones típicas prefieren perder bits que lanzar una excepción. RuntimeError Se lanza cuando se detecta un error que no cuadra en ninguna de las otras categorías. El valor asociado es una cadena que indica qué fue mal concretamente. Esta excepción es mayormente una reliquia de versiones anteriores del intérprete; ya casi no se usa. SyntaxError Se lanza cuando el analizador encuentra un error en la sintaxis. Esto puede ocurrir en una sentencia import, en una sentencia exec, en una llamada a la función interna eval() o input(), o al leer el guion inicial o la entrada estándar (por ejemplo, la entrada interactiva). Si se usan excepciones de clase, las instancias de esta clase tienen disponibles los atributos filename (nombre del fichero), lineno (nº de línea), offset (nº de columna) y text (texto), que ofrecen un acceso más fácil a los detalles. En las excepciones de cadena, el valor asociado suele ser una tupla de la forma (mensaje, (nombreFichero, numLinea, columna, texto)). En las excepciones de clase, str() sólo devuelve el mensaje. SystemError Se lanza cuando el intérprete encuentra un error interno, pero la situación no parece tan grave como para perder la esperanza. El valor asociado es una cadena que indica qué ha ido mal (en términos de bajo nivel). Se debería dar parte de este error al autor o mantenedor del intérprete Python en cuestión. Se debe incluir en el informe la cadena de versión del intérprete Python (sys.version, que también se muestra al inicio de una sesión interactiva), la causa exacta del error y, si es posible, el código fuente del programa que provocó el error. SystemExit Lanzada por la función sys.exit(). Si no se captura, el intérprete de Python finaliza la ejecución sin presentar una pila de llamadas. Si el valor asociado es un entero normal, especifica el estado de salida al sistema (se pasa a la función de C exit()), Si es None, el estado de salida es cero (que indica una salida normal sin errores). En el caso de ser de otro tipo, se presenta el valor del objeto y el estado de salida será 1. Las instancias tienen un atributo code cuyo valor se establece al estado de salida o mensaje de error propuesto (inicialmente None). Además, esta excepción deriva directamente de Exception y no de StandardError, ya que técnicamente no es un error. Una llamada a sys.exit() se traduce a un error para que los gestores de limpieza final (las cláusulas finally de las sentencias try) se puedan ejecutar y para que un depurador pueda ejecutar un guion sin riesgo de perder el control. Se puede usar la función os._exit() si es total y absolutamente necesario salir inmediatamente (por ejemplo, tras un fork() en el proceso hijo). TypeError Se lanza cuando una operación o función interna se aplica a un objeto de tipo inadecuado. El valor asociado es una cadena con detalles de la incoherencia de tipos. UnboundLocalError Se lanza cuando se hace referencia a una variable local en una función o método, pero no se ha asignado un valor a dicha variable. Deriva de NameError. Nuevo en la versión 2.0. UnicodeError Se lanza cuando se da un error relativo a codificación/descodificación Unicode. Deriva de ValueError. Nuevo en la versión 2.0. ValueError Se lanza cuando una operación o función interna recibe un argumento del tipo correcto, pero con un valor inapropiado y no es posible describir la situación con una excepción más precisa, como IndexError. WindowsError Se lanza cuando se da un error específico de Windows o el número de error no corresponde a un valor errno. Los valores errno y strerror se crean a partir de los valores devueltos por las funciones GetLastError() y FormatMessage() del API de plataforma de Windows. Deriva de OSError. Nuevo en la versión 2.0. ZeroDivisionError Se lanza cuando el segundo argumento de una operación de división o módulo es cero. El valor asociado es una cadena que indica el tipo de operandos y la operación. http://pyspanishdoc.sourceforge.net/lib/module-exceptions.html 7.6.2 Creación de un clase derivada del tipo excepción. 7.6.3 Manejo de una excepción definida por el usuario. Unidad 8. Flujos y archivos. 8.4 Definición de Archivos de texto y archivos binarios. El Concepto de Datos Datos son los hechos que describen sucesos y entidades."Datos" es una palabra en plural que se refiere a más de un hecho. A un hecho simple se le denomina "data-ítem" o elemento de dato. Los datos son comunicados por varios tipos de símbolos tales como las letras del alfabeto, números, movimientos de labios, puntos y rayas, señales con la mano, dibujos, etc. Estos símbolos se pueden ordenar y reordenar de forma utilizable y se les denomina información. Los datos son símbolos que describen condiciones, hechos, situaciones o valores. Los datos se caracterizan por no contener ninguna información. Un dato puede significar un número, una letra, un signo ortográfico o cualquier símbolo que represente una cantidad, una medida, una palabra o una descripción. La importancia de los datos está en su capacidad de asociarse dentro de un contexto para convertirse en información. Por si mismos los datos no tienen capacidad de comunicar un significado y por tanto no pueden afectar el comportamiento de quien los recibe. Para ser útiles, los datos deben convertirse en información para ofrecer un significado, conocimiento, ideas o conclusiones. 2. El Concepto de Información La información no es un dato conjunto cualquiera de ellos. Es más bien una colección de hechos significativos y pertinentes, para el organismo u organización que los percibe. La definición de información es la siguiente: Información es un conjunto de datos significativos y pertinentes que describan sucesos o entidades. DATOS SIGNIFICATIVOS. Para ser significativos, los datos deben constar de símbolos reconocibles, estar completos y expresar una idea no ambigua. Los símbolos de los datos son reconocibles cuando pueden ser correctamente interpretados. Muchos tipos diferentes de símbolos comprensibles se usan para transmitir datos. La integridad significa que todos los datos requeridos para responder a una pregunta específica están disponibles. Por ejemplo, un marcador de béisbol debe incluir el tanteo de ambos equipos. Si se oye el tanteo "New York 6" y no oyes el del oponente, el anuncio será incompleto y sin sentido. Los datos son inequívocos cuando el contexto es claro. Por ejemplo, el grupo de signos 2-x puede parecer "la cantidad 2 menos la cantidad desconocida llamada x" para un estudiante de álgebra, pero puede significar "2 barra x" a un vaquero que marca ganado. Tenemos que conocer el contexto de estos símbolos antes de poder conocer su significado. Otro ejemplo de la necesidad del contexto es el uso de términos especiales en diferentes campos especializados, tales como la contabilidad. Los contables utilizan muchos términos de forma diferente al público en general, y una parte de un aprendizaje de contabilidad es aprender el lenguaje de contabilidad. Así los términos Debe y Haber pueden significar para un contable no más que "derecha" e "izquierda" en una contabilidad en T, pero pueden sugerir muchos tipos de ideas diferentes a los no contables. DATOS PERTINENTES. Decimos que tenemos datos pertinentes (relevantes) cuando pueden ser utilizados para responder a preguntas propuestas. Disponemos de un considerable número de hechos en nuestro entorno. Solo los hechos relacionados con las necesidades de información son pertinentes. Así la organización selecciona hechos entre sucesos y entidades particulares para satisfacer sus necesidades de información. 3. Diferencia entre Datos e información 1. Los Datos a diferencia de la información son utilizados como diversos métodos para comprimir la información a fin de permitir una transmisión o almacenamiento más eficaces. 2. Aunque para el procesador de la computadora hace una distinción vital entre la información entre los programas y los datos, la memoria y muchas otras partes de la computadora no lo hace. Ambos son registradas temporalmente según la instrucción que se le de. Es como un pedazo de papel no sabe ni le importa lo que se le escriba: un poema de amor, las cuentas del banco o instrucciones para un amigo. Es lo mismo que la memoria de la computadora. Sólo el procesador reconoce la diferencia entre datos e información de cualquier programa. Para la memoria de la computadora, y también para los dispositivos de entrada y salida (E/S) y almacenamiento en disco, un programa es solamente más datos, más información que debe ser almacenada, movida o manipulada. 3. La cantidad de información de un mensaje puede ser entendida como el número de símbolos posibles que representan el mensaje."los símbolos que representan el mensaje no son más que datos significativos. 4. En su concepto más elemental, la información es un mensaje con un contenido determinado emitido por una persona hacia otra y, como tal, representa un papel primordial en el proceso de la comunicación, a la vez que posee una evidente función social. A diferencia de los datos, la información tiene significado para quien la recibe, por eso, los seres humanos siempre han tenido la necesidad de cambiar entre sí información que luego transforman en acciones. "La información es, entonces, conocimientos basados en los datos a los cuales, mediante un procesamiento, se les ha dado significado, propósito y utilidad" 4. El Concepto de Procesamiento de Datos Hasta el momento hemos supuesto que los datos que maneja una aplicación no son tan voluminosos y por lo tanto caben en memoria. Cuando recurrimos a archivos se debe a la necesidad de conservar datos después de que termina un programa, por ejemplo para apagar el computador. Sin embargo, existen problemas en donde el volumen de datos es tan grande que es imposible mantenerlos en memoria. Entonces, los datos se almacenan en un conjunto de archivos, los que forman una base de datos. Una base de datos es por lo tanto un conjunto de archivos que almacenan, por ejemplo, datos con respecto al negocio de una empresa. Cada archivo se forma en base a un conjunto de líneas y cada línea esta formada por campos de información. Todas las líneas de un mismo archivo tienen la misma estructura, es decir los mismos campos de información. Diferentes archivos poseen estructuras distintas, i.e. campos de información. Por ejemplo, el archivo de postulantes post.dat, visto en capítulos anteriores, tiene la siguiente información: ci: carnet de identidad de la persona. nombre. En lo que sigue supondremos que ambos archivos son lo suficientemente grandes como para que no quepan en la memoria del computador. A continuación resolveremos eficientemente el problema de generar un archivo con los tres campos de información, sin colocar previamente el contenido de un archivo en un arreglo. Algunas definiciones Recolección de datos: Provee un vínculo para obtener la información interoperacionables racional y las parametrizaciones. Almacenamiento de datos: Las unidades de disco de la computadora y otros medios de almacenamiento externo permiten almacenar los datos a más largo plazo, manteniéndolos disponibles pero separados del circuito principal hasta que el microprocesador los necesita. Una computadora dispone también de otros tipos de almacenamiento. La memoria de sólo lectura (ROM) es un medio permanente de almacenamiento de información básica, como las instrucciones de inicio y los procedimientos de entrada/salida. Asimismo, una computadora utiliza varios buffers (áreas reservadas de la memoria) como zonas de almacenamiento temporal de información específica, como por ejemplo los caracteres a enviar a la impresora o los caracteres leídos desde el teclado. Procesamiento de datos: a. El objetivo es graficar el Procesamiento de Datos, elaborando un Diagrama que permita identificar las Entradas, Archivos, Programas y Salidas de cada uno de los Procesos. b. Su antecedente es el Diagrama de Flujo. c. Los elementos claves son los Programas. d. Se confecciona el Diagrama de Procesamiento de Datos e. Este Diagrama no se podrá elaborar por completo desde un primer momento ya que depende del Flujo de Información. f. En este primer paso sólo se identifican las Salidas y Programas. Los elementos restantes se identifican en forma genérica. Validación de datos: Consiste en asegurar la veracidad e integridad de los datos que ingresan a un archivo. Existen numerosas técnicas de validación tales como: Digito verificador, chequeo de tipo, chequeo de rango. 5. Concepto de Procesamiento Distribuido y Centralizado Procesamiento Centralizado: En la década de los años 50’s las computadoras eran máquinas del tamaño de todo un cuarto con las siguientes características: • Un CPU • Pequeña cantidad de RAM • Dispositivos DC almacenamiento secundario (cintas) • Dispositivos d salida (perforadoras de tarjetas) • Dispositivos de entrada (lectores de tarjeta perforada) Con el paso del tiempo, las computadoras fueron reduciendo su tamaño y creciendo en sofisticación, • Aunque la industria continuaba siendo dominada por las computadoras grandes "mainframes". A medida que la computación evolucionaba, las computadoras, fueron capaces de manejar aplicaciones múltiples simultáneamente, convirtiéndose en procesadores centrales "hosts" a los que se les Conectaban muchos periféricos y terminales tontas que consistían solamente de dispositivos de entrada/salida (monitor y teclado) y quizá poco espacio de almacenamiento, pero que no podían procesar por sí mismas. Las terminales locales se conectaban con el procesador central a través de interfaces seriales ordinarias de baja velocidad, mientras que las terminales remotas se enlazaban con • El "host" usando módems y líneas telefónicas conmutadas. En este ambiente, se ofrecían velocidades de transmisión de 1200, 2400, o 9600 bps. Un ambiente como el descrito es lo que se conoce como procesamiento centralizado en su forma más pura "host/terminal". Aplicaciones características de este tipo de ambiente son: • Administración de grandes tuses de datos integradas • Algoritmos científicos de alta velocidad • Control de inventarios centralizado Al continuar la evolución de los "mainframes", estos se comenzaron a conectar a enlaces de alta velocidad donde algunas tareas relacionadas con las comunicaciones se delegaban a otros dispositivos llamados procesadores comunicaciones "Front End Procesos" (I7EP’s) y controladores de grupo "Cluster Controllers" (CC’s). Procesamiento Distribuido: El procesamiento centralizado tenía varios inconvenientes, entre los que podemos mencionar que un número limitado de personas controlaba el acceso a la información y a los reportes, se requería un grupo muy caro de desarrolladores de sistemas para crear las aplicaciones, y los costos de mantenimiento y soporte eran extremadamente altos. La evolución natural de la computación fue en el sentido del procesamiento distribuido, así las minicomputadoras (a pesar de su nombre siguen siendo máquinas potentes) empezaron a tomar parte del procesamiento que tenían los "mainframes". Ventajas Existen cuatro ventajas del procesamiento de bases de datos distribuidas. La primera, puede dar como resultado un mejor rendimiento que el que se obtiene por un procesamiento centralizado. Los datos pueden colocarse cerca del punto de su utilización, de forma que el tiempo de comunicación sea mas corto. Varias computadoras operando en forma simultánea pueden entregar más volumen de procesamiento que una sola computadora. Segundo, los datos duplicados aumentan su confiabilidad. Cuando falla una computadora, se pueden obtener los datos extraídos de otras computadoras. Los usuarios no dependen de la disponibilidad de una sola fuente para sus datos .Una tercera ventaja, es que los sistemas distribuidos pueden variar su tamaño de un modo más sencillo. Se pueden agregar computadoras adicionales a la red conforme aumentan el número de usuarios y su carga de procesamiento. A menudo es más fácil y más barato agregar una nueva computadora más pequeña que actualizar una computadora única y centralizada. Después, si la carga de trabajo se reduce, el tamaño de la red también puede reducirse. Por último, los sistemas distribuidos se pueden adecuar de una manera más sencilla a las estructuras de la organización de los usuarios. Archivos: Es una es estructura de datos que reside en memoria secundaria o almacenamiento permanente (cinta magnética, disco magnético, disco óptico, disco láser, etc.). La forma de clasificación más básica se realiza de acuerdo al formato en que residen estos archivos, de esta forma hablamos de archivos ASCII (de texto) y archivos binarios. En este capítulo nos centraremos en estos últimos. Definición archivo binario: Estructura de datos permanente compuesto por registros (filas) y éstos a su vez por campos (columnas). Se caracteriza por tener un tipo de dato asociado, el cual define su estructura interna. Definición archivo texto: Estructura de datos permanente no estructurado formado por una secuencia de caracteres ASCII. Tipos de Acceso a los Archivos a.)Secuencial: Se accesan uno a uno los registros desde el primero hasta el último o hasta aquel que cumpla con cierta condición de búsqueda. Se permite sobre archivos de Organización secuencial y Secuencial Indexada. b.)Random: Se accesan en primera instancia la tabla de índices de manera de recuperar la dirección de inicio de bloque en donde se encuentra el registro buscado. (dentro del rea primaria o de overflow). Se permite para archivos con Organización Sec.Indexada. c.)Dinámico: Se accesan en primera instancia la tabla de índices de manera de recuperar la dirección de inicio de bloque en donde se encuentra el registro buscado. (dentro del rea primaria o de overflow). Se permite para archivos con Organización Sec.Indexada. d.)Directo: Es aquel que utiliza la función de Hashing para recuperar los registros. Sólo se permite para archivos con Organización Relativa. Constantes Las constantes son similares a una variable pero tienen un valor determinado que se mantiene igual en toda la ejecución del programa. El contenido de una variable puede cambiar tantas veces sea necesario. ¿Porque usar una constante si no puede cambiar de valor?. Hacemos esto cuando deseamos usar un mismo número o una palabra (string) varias veces. Variables Magnitud que puede tomar diferentes valores y se representa con una letra o letras. La variable real es el conjunto de los números reales, y se puede representar por cualquier letra o conjunto de letras y nos sirve para poder utilizar dicha letra para cálculos o para obtener resultados. http://www.monografias.com/trabajos14/datos/datos.shtml# 8.5 Operaciones básicas en archivos texto y binario. Operaciones con archivos o carpetas. Para trabajar con archivos o carpetas primero hay que seleccionarlos. Existen dos modos de seleccionar: Selección continua. 1. 2. 3. 4. Pico el primer archivo que quiero seleccionar. Pulso la tecla "Shift ( )" y la mantengo pulsada Pico el último archivo a seleccionar Suelto la tecla "Shift ( )" Selección discontinua. 1. 2. 3. 4. Pico un archivo Pulso la tecla "CTRL" y la mantengo pulsada Pico otro archivo, y otro, y otro y otro... Suelto la tecla "CTRL" Una vez seleccionados los archivos o carpetas sobre los que deseamos actuar, elegimos la operación a realizar, tomándola de los botones o usando el botón derecho del ratón. El método que aconsejo ya que no requiere memorizar, pasa por la utilización del botón derecho del ratón. Copiar archivos. 1. 2. Una vez seleccionados los archivos (pico con el botón derecho del ratón) y elijo "COPIAR". Selecciono la nueva ubicación (pico con el botón derecho del ratón) y elijo "PEGAR". Mover archivos. 1. 2. Una vez seleccionados los archivos (pico con el botón derecho del ratón) y elijo "CORTAR". Selecciono la nueva ubicación (pico con el botón derecho del ratón) y elijo "PEGAR". Borrar archivos. 1. Una vez seleccionados los archivos (pico con el botón derecho del ratón) y elijo "ELIMINAR". (o presiono la tecla "Supr") Observaciones. Al borrar archivos o carpetas si estos se encuentran en el disco duro "C:" del ordenador no se borran totalmente, sino que se envían a la papelera de reciclaje. En la papelera permanecen un tiempo hasta que ésta se vacía. Durante ese tiempo se pueden recuperar. Por el contrario si los archivos borrados se encuentran en un disquete serán eliminados directamente sin que podamos posteriormente recuperarlos. Recuperar archivos de la papelera de reciclaje. 1. 2. 3. Activamos la papelera de reciclaje. Seleccionamos los archivos que queremos recuperar. Picamos con el botón derecho sobre los elementos y elegimos la opción RESTAURAR. De esta manera el archivo se habrá colocado en su ubicación original igual que antes de borrarlo. Abrir archivos. Abrir un archivo consiste en recuperarlo con el programa que lo ha creado (si el archivo es un documento), o en ejecutar un programa si el archivo es una aplicación (tienen extensión EXE). 1. Una vez seleccionados los archivos (pico con el botón derecho del ratón) y elijo "ABRIR". Observaciones. Esta opción es interesante realizarla de uno en uno. Se realiza también haciendo doble Click sobre el archivo que queremos abrir. Crear carpetas 1. 2. 3. Activar la unidad o carpeta en la que deseo crear una nueva carpeta Ir a la opción del menú ARCHIVO / NUEVO / CARPETA (También se puede hacer con el botón derecho) Escribir el nombre de la nueva carpeta y pulsar INTRO. Operaciones sobre disco. Para operar con un disco picamos con el botón derecho sobre la unidad. Copiar disco. Duplica un disquete. Formatear. Prepara el disquete para trabajar. OJO (Se borra toda la información del disquete) Propiedades. Muestra la capacidad total de la unidad, el espacio libre, y el consumido. http://www.adrformacion.com/guias/explorador.htm Operaciones soportadas por el subsistema de archivos Independientemente de los algoritmos de asignación de espacio, de los métodos de acceso y de la forma de resolver las peticiones de lectura y escritura, el subsistema de archivos debe proveer un conjunto de llamadas al sistema para operar con los datos y de proveer mecanismos de protección y seguridad. Las operaciones básicas que la mayoría de los sistemas de archivos soportan son: Crear ( create ) : Permite crear un archivo sin datos, con el propósito de indicar que ese nombre ya está usado y se deben crear las estructuras básicas para soportarlo. Borrar ( delete ): Eliminar el archivo y liberar los bloques para su uso posterior. Abrir ( open ): Antes de usar un archivo se debe abrir para que el sistema conozca sus atributos, tales como el dueño, la fecha de modificación, etc. _ Cerrar ( close ): Después de realizar todas las operaciones deseadas, el archivo debe cerrarse para asegurar su integridad y para liberar recursos de su control en la memoria. Leer o Escribir ( read, write ): Añadir información al archivo o leer el caracter o una cadena de caracteres a partir de la posición actual. _ Concatenar ( append ): Es una forma restringida de la llamada `write', en la cual sólo se permite añadir información al final del archivo. _ Localizar ( seek ): Para los archivos de acceso directo se permite posicionar el apuntador de lectura o escritura en un registro aleatorio, a veces a partir del inicio o final del archivo. Leer atributos: Permite obtener una estructura con todos los atributos del archivo especificado, tales como permisos de escritura, de borrado, ejecución, etc. Poner atributos: Permite cambiar los atributos de un archivo, por ejemplo en UNIX, donde todos los dispositivos se manejan como si fueran archivos, es posible cambiar el comportamiento de una terminal con una de estas llamadas. Renombrar ( rename ): Permite cambiarle el nombre e incluso a veces la posición en la organización de directorios del archivo especificado. Los subsistemas de archivos también proveen un conjunto de llamadas para operar sobre directorios, las más comunies son crear, borrar, abrir, cerrar, renombrar y leer. Sus funcionalidades son obvias, pero existen también otras dos operaciones no tan comunes que son la de `crear una liga' y la de `destruir la liga'. La operación de crear una liga sirve para que desde diferentes puntos de la organización de directorios se pueda accesar un mismo directorio sin necesidad de copiarlo o duplicarlo. La llamada a `destruir nla liga' lo que hace es eliminar esas referencias, siendo su efecto la de eliminar las ligas y no el directorio real. El directorio real es eliminado hasta que la llmada a `destruir liga' se realiza sobre él. http://www.tau.org.ar/base/lara.pue.udlap.mx/sistoper/capit ulo3.html 8.5.1 Crear. 8.5.2 Abrir. 8.5.3 Cerrar. 8.5.4 Lectura y escritura. 8.5.5 Recorrer. 8.6 Aplicaciones.