You are on page 1of 85

P.L.E.

P.O.O.

PROGRAMACION ORIENTADA A OBJETOS EN C++


1. NOVEDADES DEL C++ FRENTE AL C.
De todos es sabido que, el C++, es una ampliacin al C no slo por la nueva metodologa que incorpora sino por una serie de caractersticas que nos van a permitir programar de un modo mucho ms cmodo y potente. 1.1. CAMBIO EN LA EXTENSIN DEL NOMBRE DE LOS FICHEROS El primer cambio que tiene que conocer cualquier programador es que los ficheros fuente de C++ tienen la extensin *.cpp (de C plus plus, que es la forma oral de llamar al lenguaje en ingls), en lugar de *.c. Esta distincin es muy importante, pues determina ni ms ni menos el que se utilice el compilador de C o el de C++. La utilizacin de nombres incorrectos en los ficheros puede dar lugar a errores durante el proceso de compilacin. 1.2. DECLARACIN ENUMERACIN SIMPLIFICADA DE VARIABLES TIPO

Las enumeraciones (variables enum) permiten definir variables de tipo entero con un nmero pequeo de valores que estn representados por identificadores alfanumricos. Estos identificadores permiten que el programa se entienda ms fcilmente, dando un significado a cada valor de la variable entera. Las variables tipo enum son adecuadas para representar de distintas formas valores binarios (SI o NO; VERDADERO o FALSO; EXITO o FRACASO, etc.), los das de la semana (LUNES, MARTES, MIERCOLES, ...), los meses del ao (ENERO, FEBRERO, MARZO, ...), y cualquier conjunto anlogo de posibles valores. En C las variables de tipo enum se hacan corresponder con enteros, y por tanto no hacan nada que no se pudiera hacer tambin con enteros. En C++ las variables enum son verdaderos tipos de variables, que necesitan un cast para que un valor entero les pueda ser asignado (ellas son promovidas a enteros cuando hace falta de modo automtico). Esto quiere decir que si una funcin espera recibir como argumento un tipo enum slo se le puede pasar un entero con un cast. Por el contrario, si espera recibir un entero se le puede pasar un valor enum directamente. La principal razn de ser de las variables enum es m ejorar la claridad y facilidad de comprensin de los programas fuente. Por ejemplo, si se desean representar los colores rojo, verde, azul y amarillo se podra definir un tipo de variable enum llamada color cuyos cuatro valores estaran representados por l s constantes ROJO, VERDE, AZUL Y a AMARILLO, respectivamente. Esto se puede hacer de la siguiente forma:
enum color {ROJO, VERDE, AZUL, AMARILLO};

Utilizar maysculas para los identificadores que representan constantes es una convencin estilstica ampliamente adoptada. En el ejemplo anterior se ha definido el tipo color, pero no se ha creado todava ninguna variable con ese tipo.

P.L.E.

P.O.O.

Por defecto los valores enteros asociados empiezan en 0 y van aumentando de uno en uno. As, por defecto, los valores asociados sern:
ROJO = 0 VERDE = 1 AZUL = 2 AMARILLO = 3

Sin embargo, el programador puede asignar el valor que desee a cada uno de esos identificadores, asignando incluso el mismo entero a varios identificadores diferentes. Por ejemplo, siguiendo con el tipo color:
enum color {ROJO = 3, VERDE = 5, AZUL = 7, AMARILLO};

Lgicamente en este caso los valores enteros asociados sern:


ROJO = 3 VERDE = 5 AZUL = 7 AMARILLO = 8

Cuando no se establece un entero determinado para un identificador dado, se toma el entero siguiente al anteriormente asignado. Por ejemplo, en el caso anterior al AMARILLO se le asigna un 8, que es el nmero siguiente al asignado al AZUL. Una vez que se ha definido un tipo enum, se pueden definir cuantas variables de ese tipo se desee. Esta definicin es distinta en C y en C++. Por ejemplo, para definir las variables pintura y fondo, de tipo color, en C hay que utilizar la sentencia:
enum color pintura, fondo; /* esto es C */

mientras que en C++ bastara hacer:


color pintura, fondo; // esto es C++

As pues en C++ no es necesario volver a utilizar la palabra enum. Los valores que pueden tomar las variables pintura y fondo son los que puede tomar una variable del tipo color, es decir: ROJO, VERDE, AZUL Y AMARILLO. Se puede utilizar, por ejemplo, la siguiente sentencia de asignacin:
pintura = ROJO;

Hay que recordar que al imprimir una variable enum se imprime su valor entero y no su valor asociado.

1.3. ESPECIFICADOR CONST PARA VARIABLES En C++ el especificador const se puede utilizar con variables y con punteros. Las variables definidas como const no son lo mismo que las constantes simblicas, aunque evidentemente hay una cierta similitud en las reas de aplicacin. Si una variable se define como const se tiene la garanta de que su valor no v a cambiar durante toda la a ejecucin del programa. Si en alguna sentencia del programa se intenta variar el valor de una variable definida como const, el compilador produce un mensaje de error. Esta precaucin permite detectar errores durante la compilacin del programa, lo cual siempre es ms sencillo que detectarlos en tiempo de ejecucin.

P.L.E.

P.O.O.

Las variables de este tipo pueden ser inicializadas pero no pueden estar a la izquierda de una sentencia de asignacin. Las variables declaradas como const tienen importantes diferencias con las constantes simblicas definidas con la directiva #define del preprocesador. Aunque ambas representan valores que no se puede modificar, las variables const estn sometidas a las mismas reglas de visibilidad y duracin que las dems variables del lenguaje. Las variables const de C++ pueden ser utilizadas para definir el tamao de un vector en la declaracin de ste, cosa que no est permitida en C. As las siguientes sentencias, que seran ilegales en C, son ahora aceptadas en C++:
void main(void) { const int SIZE = 5; char cs[SIZE] ; }

De todas formas, nunca puede declararse ninguna variable array cuyo tamao sea desconocido en tiempo de compilacin. Si el tamao de una variable va a ser conocido slo en tiempo de ejecucin, hay que utilizar reserva dinmica de memoria tanto en C como en C++. Es muy frecuente que las funciones a las que por motivos de eficiencia (para no tener que sacar copias de los mismos) se les pasan los argumentos por referencia, stos sern declarados como const en la definicin y en el prototipo de la funcin, con objeto de hacer imposible una modificacin accidental de dichos datos. Esto sucede por ejemplo con las funciones de manejo de cadenas de caracteres. El prototipo de la funcin strcpy() puede ser como sigue:
char *strcpy(char *s1, const char *s2);

donde s1 es la cadena copia y s2 es la cadena original. Como no tiene sentido tratar de modificar la cadena original dentro de la funcin, sta se declara como const. En este caso el valor de retorno es un puntero a la cadena copia s1. En el caso de los punteros hay que distinguir entre dos formas de aplicar el cualificador const: 1. un puntero variable apuntando a una variable constante y 2. un puntero constante apuntando a una variable cualquiera. Un puntero a una variable const no puede modificar el valor de esa variable (si se intentase el compilador lo detectara e imprimira un mensaje de error), pero ese puntero no tiene por qu apuntar siempre a la misma variable. En el caso de un puntero const, ste apunta siempre a la misma direccin de memoria pero el valor de la variable almacenada en esa direccin puede cambiar sin ninguna dificultad. Un puntero a variable const se declara anteponiendo la palabra const:
const char *nombre1 // no se puede modificar el valor de la variable

P.L.E.

P.O.O.

Por otra parte, un puntero const a variable cualquiera se declara interponiendo la palabra const entre el tipo y el nombre de la variable:
char* const nombre2 // no se puede modificar la direccin del puntero, pero s el valor.

En ANSI C una variable declarada como const puede ser modificada a travs de un puntero a dicha variable. Por ejemplo, el siguiente programa compila y produce una salida i=3 con el compilador de C, pero da un mensaje de error con el compilador de C++.
#include <stdio.h> void main(void) { const int i = 2; int *p; p = &i; *p = 3; printf("i = %d", i); }

1.4. CONVERSIONES EXPLCITAS DE TIPO Adems de las conversiones implcitas de tipo que tienen lugar al realizar operaciones aritmticas entre variables de distinto tipo promociones y en las sentencias de asignacin, el lenguaje C dispone de una conversin explcita de tipo de variables, directamente controlada por el programador, llamada cast. El cast se realiza anteponiendo al nombre de la variable o expresin el tipo al que se desea hacer la conversin encerrado entre parntesis. Por ejemplo, pera devolver como int un cociente entre las variables double x e y:
return (int) (x/y);

El lenguaje C++ dispone de otra conversin explcita de tipo con una notacin similar a la de las funciones y ms sencilla que la del cast. Se utiliza para ello el nombre del tipo al que se desea convertir seguido del valor a convertir entre parntesis. As, las siguientes expresiones son vlidas en C++:
y = double(25); return int(x/y);

1.5. SOBRECARGA DE FUNCIONES La sobrecarga (overload) de funciones consiste en declarar y definir varias funciones distintas que tienen un mismo nombre. Dichas funciones se definen de forma diferente. En el momento de la ejecucin se llama a una u otra funcin dependiendo del nmero y/o tipo de los argumentos actuales de la llamada a la funcin. Por ejemplo, se pueden definir varias funciones para calcular el valor absoluto de una variable, todas con el mismo nombre abs(), pero cada una aceptando un tipo de argumento diferente y con un valor de retorno diferente.

P.L.E.

P.O.O.

La sobrecarga de funciones no admite funciones que difieran slo en el tipo del valor de retorno, pero con el mismo nmero y tipo de argumentos. De hecho, el valor de retorno no influye en la determinacin de la funcin que es llamada; slo influyen el nmero y tipo de los argumentos. Tampoco se admite que la diferencia sea el que en una funcin un argumento se pasa por valor y en otra funcin ese argumento se pasa por referencia. A continuacin se presenta un ejemplo con dos funciones sobrecargadas, llamadas ambas string_copy(), para copiar cadenas de caracteres. Una de ellas tiene dos argumentos y la otra tres. Cada una de ellas llama a una de las funciones estndar del C: strcpy() que requiere dos argumentos, y strncpy() que requiere tres. El nmero de argumentos en la llamada determinar la funcin concreta que vaya a ser ejecutada:
// Ejemplo de funcin sobrecargada #include <iostream.h> #include <string.h> inline void string_copy(char *copia, const char *original) { strcpy(copia, original); } inline void string_copy(char *copia, const char *original, const int longitud) { strncpy(copia, original, longitud); } static char string_a[20], string_b[20]; void main(void) { string_copy(string_a, "Aquello"); string_copy(string_b, "Esto es una cadena", 4); cout << string_b << " y " << string_a; // La ltima sentencia es equivalente a un printf() de C }

1.6. VALORES POR DEFECTO DE PARMETROS DE UNA FUNCIN En ANSI C se espera encontrar una correspondencia biunvoca entre la lista de argumentos actuales (llamada) y la lista de argumentos formales (declaracin y definicin) de una funcin. Por ejemplo, supngase la siguiente declaracin de una funcin para calcular el mdulo de un vector x con n elementos:
double modulo(double x[], int n);

En C esta funcin tiene que ser necesariamente llamada con dos argumentos actuales que se corresponden con los dos argumentos formales de la declaracin. En C++ la situacin es diferente pues se pueden definir valores por defecto para todos o algunos de los argumentos formales. Despus, en la llamada, en el caso de que algn argumento est ausente de la lista de argumentos actuales, se toma el valor asignado por defecto a ese argumento. Por ejemplo, la funcin modulo() poda haberse declarado del siguiente modo:
double modulo(double x[], int n=3);

P.L.E.

P.O.O.

La funcin modulo() puede ser llamada en C++ de las formas siguientes:


v = modulo(x, n); v = modulo(x);

En el segundo caso se utiliza el valor por defecto n=3 incluido en la declaracin. En C++ se exige que todos los argumentos con valores por defecto estn al final de la lista de argumentos. En la llamada a la funcin pueden omitirse alguno o algunos de los ltimos argumentos de la lista. Si se omite un argumento deben de omitirse todos aquellos que se encuentren detrs suyo.

1.7. AMPLIACIONES IMPORTANTES EN STRUCT De todos es sabido que las estructuras (struct) tienen una importancia vital en la programacin pues nos permiten construir complejas "aglutinaciones" de datos que nos van a permitir, despus, trabajar con "tablas de datos" distintas de un modo fcil y directo. Cuando utilizbamos las estructuras en C, no estbamos utilizando un tipo de datos completo, es decir, cuando definimos algo as en C
struct Guerrero { char *nombre; int fuerza; int resistencia; int experiencia; int edad; };

y queramos crear alguna variable de tipo Guerrero, tenamos dos opciones. Una de ellas era definir la variable de este modo:
struct Guerrero jugador1;

y la otra, la final de la estructura poniendo el nombre de las variables en la misma definicin, es decir:
struct Guerrero { char *nombre; int fuerza; int resistencia; int experiencia; int edad; } jugador1;

En ambos casos, tenamos una variable de tipo Guerrero que era, a su vez, de tipo estructura. La nica pega de todo esto es que no vamos a definir un tipo de datos nuevo en el sentido estricto de la palabra, esto es, siempre definimos estructuras no tipos de datos. Con la llegada del C++, las estructuras pasan a ser tipos de datos completos pudindose definir directamente variables del tipo estructura. Volvamos a poner la definicin anterior:

P.L.E.

P.O.O.

struct Guerrero { char* nombre; int fuerza; int resistencia; int experiencia; int edad; }

Ahora, para declarar una variable de tipo Guerrero slo tenemos que hacer lo siguiente:
Guerrero jugador1;

Como podemos comprobar ahora la creacin de variables del tipo estructura anterior es idntica que cuando hacemos lo propio con un especificador entero, carcter, etc. Si recordamos lo ms parecido en C lo conseguamos con la palabra reservada typedef. Sin embargo, no slo se ha convertido a struct en un tipo de datos completo sino que, adems y como mayor diferencia, las estructuras en C++ pueden contener funciones junto a los datos de toda la vida. As, ya tenemos que empezar a diferenciar el contenido que se puede encontrar en una estructura, por un lado tendremos las variables miembro que sern todas aquellas variables contenidas en la estructura y, por otro lado, tendremos las funciones miembro que, al encontrarse ms relacionadas con las clases las veremos cuando lleguemos a ellas. De esta manera, recordad que las estructuras en C++ son un tipo de datos completo y nos permiten definir variables de una manera mucho ms ptima. Sabed tambin que podemos incluir funciones (llamadas funciones miembro) dentro de las estructuras, esto es, junto a las variables (llamadas variables miembro) que siempre hemos sabido que debemos definir. De todos modos, no estudiaremos por ahora las capacidades derivadas de poder utilizar tales funciones incluidas en las estructuras. Por ahora, basta con conocer, por encima, las novedades. Por si alguno se lo estaba preguntando, el acceso a las estructuras sigue siendo el mismo, esto es, utilizando el punto (.). En el caso de que tengamos punteros a estructuras, podremos utilizar la flecha de toda la vida, (->). Ya acabando con las estructuras, si nos fijamos en la ltima definicin que se realiz del tipo de datos Guerrero para mostrar el ejemplo de uso en C++, veremos que la variable nombre es un puntero y que se define con el * despus del tipo de variable, esto es, despus del char. Esto es as porque es la metodologa tpica del C++ si bien, los que no quieran utilizarla pueden seguir definiendo las variables como en C, es decir, con el operador unario * justo antes del nombre de la variable.

1.8. COUT Y CIN DOS NOVEDADES MUY INTERESANTES. A la hora de trabajar con C++ la construccin cout << se suele utilizar para la salida de datos apartando a un lado las funciones de la familia printf del C tradicional. Debemos de tener claro que cout no es ms que un objeto al cual le mandamos la informacin numrica y de texto mediante el smbolo <<. Esto es as porque en la representacin de cout << el operador << est sobrecargado para actuar de tal modo que se escriban en la

P.L.E.

P.O.O.

salida estndar el contenido de cout. En los sucesivos captulos se expondrn todos los pormenores de lo que es la sobrecarga de operadores de todos modos y como avance hasta que lleguemos aclarar que gracias a la sobrecarga de operadores nos podemos permitir "personalizar" smbolos de operaciones como son +,-,= y ++ para que se comporten de forma distinta cuando se utilicen con objetos de clases tambin diferentes, sirva esta breve introduccin. Es fcilmente imaginable que si podemos enviar datos a la salida mediante cout tambin existir en C++ un objeto similar pero para realizar entradas. El objeto en cuestin ser cin y su funcionamiento ser muy sencillo pues slo deberemos de utilizar el formato explicado ms arriba para cout. Es la operacin clsica de un scanf. As, si quisisemos realizar una peticin de una variable entera, por poner un ejemplo, podramos poner este cdigo:
#include <iostream.h> // Ejemplo de utilizacin de cin y cout void main(void) { int valor; cout << "\nIntroduce un valor entero"; // equivalente a hacer scanf("%d",&valor); cin >> valor; cout << "\nHas introducido el valor " << valor; }

Como podemos observar no hay ningn misterio de utilizacin de estas dos novedades del C++. Antes de pasar a las referencias, debemos darnos cuenta de que para trabajar con cin y cout, debemos de utilizar siempre el archivo de cabecera "iostream.h". Podemos tomarlo por el equivalente a "stdio.h" del C.

1.9. VARIABLES DE TIPO REFERENCIA. Las referencias son novedades absolutas del C++ (no se encuentran disponibles en C). Una referencia es un nuevo tipo de datos que nos va a permitir utilizar las caractersticas de los punteros pero tratndolos como variables ordinarias. Podemos imaginarnos una referencia como un "alias" de una variable o, mejor dicho, como la misma variable disponible pero con un nombre distinto. La inicializacin de una referencia es bien fcil ya que slo tendremos que asociar la referencia que deseemos a otra variable que ya est creada por nosotros. Una vez que hemos realizado tal inicializacin, la referencia va a estar continuamente asociada con su variable correspondiente. Cabe aadir que, si quisiramos hacer que nuestra referencia fuese el "alias" de otra variable o lo que es lo mismo que referenciase a otra variable no podramos, tendramos un error de compilacin. La declaracin de una referencia es bien sencilla:
// dato es una variable definida por nosotros y es de tipo entero. int dato; // referenciaDato es la referencia que hemos creado. int& referenciaDato = dato;

P.L.E.

P.O.O.

Como podemos observar para crear una referencia no necesitamos ms que la variable a la que queremos referenciar, q en el ejemplo es dato, junto la referencia en s que se ue va a definir con el smbolo &. De este modo, referenciaDato es la referencia o alias de la variable dato. Una vez hechas las dos operaciones anteriores cualquier cambio que hagamos sobre dato se ver reflejado en referenciaDato y viceversa, es decir, si realizamos una modificacin en referenciaDato, esta tambin se va a ver reflejada en la variable dato. Veamos un ejemplo de todo esto:
#include <iostream.h> void main() { int dato = 50; int& refDato = dato; cout << "La variable dato vale " << dato << \n; cout << "La variable refDato vale " << refDato << \n; // multiplicamos la variable dato por 2, ahora dato = 100 dato *= 2; cout << "La variable dato vale " << dato << \n; cout << "La variable refDato vale " << refDato << \n; // incrementamos el valor de la referencia, ahora refDato = 101; refDato ++; cout << "La variable dato vale " << dato << \n; cout << "La variable refDato vale " << refDato; }

Para cercioraros de que tanto la variable como la referencia poseen la misma direccin tan slo tenemos que ejecutar este listado que se muestra a continuacin y comprobar que la direccin de dato y refDato es la misma.
#include <iostream.h> void main( ) { int dato = 50; int& refDato = dato; cout << \n << "La direccin de la variable dato es " << &dato << \n; cout << \n << "La direccin de la referencia refDato es " << &refDato << \n; }

Una de las cosas que tenemos que tener bien claro es la utilizacin del operador unario & con una variable y con una referencia. Cuando declaramos una referencia estamos definiendo un tipo de datos que es exclusivo del C++, en este caso, el operador & va junto al tipo de datos que estamos definiendo como referencia as, si nosotros queremos crear una referencia que va a referenciar a una variable de tipo entero pondremos algo como:
int& referencia.

Otro uso bien distinto es cuando lo que queremos obtener es la direccin en memoria de cierta variable o dato. En este caso tanto en C como en C++ el operador unario & se comporta de igual modo dndonos la direccin en memoria de tal variable o dato. Tan slo tenemos que ejecutar el ltimo de los listados expuestos el cual nos mostrar, por pantalla, la direccin en memoria de una variable de tipo entero y la de una referencia que tiene con alias precisamente tal variable de tipo entero.
9

P.L.E.

P.O.O.

1.10.

REFERENCIAS Y PUNTEROS.

Todo lo que hemos hecho con punteros lo podemos realizar con las referencias de un modo mucho ms intuitivo. Pese a que nosotros cuando trabajamos con las referencias lo hacemos como si de otra variable ms se tratara, el compilador lo que realmente est haciendo es utilizar esa referencia como un valor asociado a la direccin en memoria de la variable en cuestin, es decir, nosotros utilizamos las referencias como si de otra variable ms se tratar mientras que el compilador realmente las utiliza como los tan temidos e importantes punteros del C. Como lo mejor para ver esto es con un ejemplo ahora se muestra cmo realizar el cambio del valor de una variable local desde una funcin con C, es decir, con punteros y, despus, la versin en C++ utilizando las referencias.
#include <stdio.h> /* Prototipo de la funcin para cambiar valor */ void CambiaVble (int *punt); void main(void) { int vble = 50; printf("El valor de vble es %d",vble); CambiaVble (&vble); printf("\n El valor de vble es %d",vble); } void CambiaVble (int *punt) { *punt = 0; }

Esta es la forma tradicional de trabajar en C y la salida debera de ser: El valor de vble es 50 El valor de vble es 0 Pasemos ahora a ver cmo hacer lo mismo pero utilizando referencias.
#include <iostream.h> /* Prototipo de la funcin para cambiar valor */ void CambiaVble (int& ref); void main(void) { int vble = 50; cout << "\n El valor de vble es " << vble; CambiaVble (vble); cout << "\n El valor de vble es " << vble; } void CambiaVble (int& ref) { ref = 0; }

Si nos fijamos bien quien entienda correctamente el concepto de puntero en C notar que realmente el utilizar referencias es como utilizar cualquier otro tipo de variable y esto hace que los punteros sean mucho ms sencillos de utilizar para el programador. Estos dos listados se han realizado pensando en mostrar como ejemplo de comparativa entre punteros y referencias con lo que muy pocas ventajas pueden ser sacadas de ellos. Pese a que su uso es muy recomendado, hay que tener cuidado especialmente cuando uno empieza a codificar un programa de cierta envergadura en los cuales, rpidamente comienzan a crecer variables, constantes y dems parmetros que pueden "ocultar" a nuestras referencias.

10

P.L.E.

P.O.O.

Cuando trabajbamos en C siempre sabamos localizar un puntero por el hecho de llevar siempre el operador unario como identificativo frente al resto de variables de nuestra creacin. Con todo esto quiero decir que una referencia no tiene que ser identificada utilizando ninguna clase de smbolo especial y que puede ser confundida como una variable ms pasando por alto el gran "poder" contenido en ella.Las referencias, en definitiva, deben de ser utilizadas cuidadosamente procurando en todo lo posible, poner algn comentario explicativo que nos identifique rpidamente que eso a lo que vamos a "tocar" no es una variable como otra cualquiera sino la citada referencia.

1.11. OPERADORES NEW Y DELETE PARA GESTIN DINMICA DE MEMORIA Con los operadores new y delete el programador tiene entera libertad para decidir crear o destruir sus variables cuando las necesite. Una variable creada con el operador new dentro de cualquier bloque, perdura hasta que es explcitamente borrada con el operador delete. Puede traspasar la frontera de su bloque y ser manipulada por instrucciones de otros bloques. Un aspecto diferente con la funcin malloc(), que es el mtodo ms utilizado para reservar dinmicamente memoria en ANSI C, es que sta devuelve un puntero a void (*void) que es despus convertido al tipo de variable que se desea. Esa conversin se evita con new, eliminando as una posible fuente de problemas. Se puede utilizar el operador new para crear variables de cualquier tipo. New devuelve, en todos los casos, un puntero a la variable creada. Tambin se pueden crear variables de tipos definidos por el usuario.
struct usuario { .......... }; usuario* Un_Usuario; Un_Usuario = new usuario;

Cuando una variable ya no es necesaria se destruye con el operador delete para poder utilizar la memoria que estaba ocupando, mediante una instruccin del tipo: delete p; A continuacin se presenta a modo de ejemplo un programa que reserva memoria de modo dinmico para un vector de caracteres:
#include <iostream.h> #include <string.h> void main( ) { char Nombre[50]; cout << "Introduzca su Nombre:"; cin >> Nombre; char *CopiaNombre = new char[strlen(Nombre)+1]; // Se copia el Nombre en la variable CopiaNombre strcpy(CopiaNombre, Nombre); cout << CopiaNombre; delete [ ] CopiaNombre; }

11

P.L.E.

P.O.O.

El siguiente ejemplo reserva memoria dinmicamente para una matriz de doubles:


#include <iostream.h> void main( ) { int nfil=4, ncol=3, i, j; int **mat; mat = new int*[nfil]; // se reserva memoria para el vector de punteros for (i=0; i<nfil; i++) // se reserva memoria para cada fila mat[i] = new int[ncol]; for(i=0; i<nfil; i++) // se inicializa toda la matriz for(j=0; j<ncol; j++) mat[i][j]=i+j; for(i=0; i<nfil; i++) // se imprime la matriz { for(j=0; j<ncol; j++) cout << mat[i][j] << "\t"; cout << "\n"; } for(i=0; i<nfil; i++) // se libera memoria delete [ ] mat[i]; // se borran las filas de la matriz delete [ ] mat; // se borra el vector de punteros }

1.12.

LOS COMENTARIOS EN C++

En C los comentarios empiezan por los caracteres /* y terminan con los caracteres */. Pueden comprender varias lneas y estar distribuidos de cualquier forma, pero todo aquello que est entre el /* (inicio del comentario) y el */ (fin del comentario) es simplemente ignorado por el compilador. Algunos ejemplos de formato de comentarios son los siguientes:
/* Esto es un comentario simple. */ /* Esto es un comentario ms largo, distribuido en varias lneas. */

En C++ se admite el mismo tipo de comentarios que en C, pero adems se considera que son comentarios todo aquel texto que est desde dos barras consecutivas (//) hasta el fin de la lnea . Las dos barras marcan el comienzo del comentario y el fin de la lnea, el final. Si se desea poner comentarios de varias lneas, hay que colocar la doble barra al comienzo de cada lnea. Los ejemplos anteriores se podran escribir del siguiente modo:
// Esto es un comentario simple. // Esto es un comentario ms largo, // distribuido en varias lneas.

La ventaja de este nuevo mtodo es que no se pueden comentar inadvertidamente varias lneas de un programa abriendo un indicador de comentario que no se cierre en el lugar adecuado. En ste y en los prximos captulos iremos introduciendo nuevos conceptos que normalmente se asocian a la programacin orientada a objetos, como son: objeto, mensaje, mtodo, clase, herencia, interfaz, etc.

12

P.L.E.

P.O.O.

2. QUE ES LA PROGRAMACION ORIENTADA A OBJETOS?


Siglas de "Programacin Orientada a Objetos". En ingls se pone al revs "OOP". La idea bsica de este tipo de programacin es agrupar los datos y los procedimientos para manejarlos en una nica entidad: el objeto. Un programa es un objeto, que a su vez est formado de objetos. Las caractersticas fundamentales de la Programacin Orientada a Objetos (POO) son: Abstraccin : Es la representacin de las caractersticas esenciales de algo sin incluir los antecedentes o detalles irrelevantes. La clase es una abstraccin porque en ella se definen las propiedades y los atributos genricos de un conjunto de objetos. Las clases tratan de representar el mundo real. Encapsulacin u ocultamiento de informacin: Las variables y las funciones miembro de una clase pueden ser declaradas como public, private o protected. De esta forma se puede controlar el acceso a los miembros de la clase y evitar un uso inadecuado. Herencia: Es el mecanismo para compartir automticamente mtodos y atributos entre clases y subclases. Una clase puede derivar de otra, y en este caso hereda todas las variables y funciones miembro. As, puede aadir nuevas funciones y datos miembros. Polimorfismo: Esta caracterstica permite implementar mltiples formas de un mismo mtodo, dependiendo cada una de ellas de la clase sobre la que se realice la implementacin.

Un ejemplo de la POO es Windows, donde cada ventana, botn, men, etc. es un objeto. Cada uno tiene sus propias caractersticas (datos) y sus propios mtodos (funciones) para manipularlos.

Objeto
Un objeto es una unidad que engloba en s mismo datos y procedimientos necesarios para el tratamiento de esos datos. Hasta ahora habamos hecho programas en los que los datos y las funciones estaban perfectamente separadas. Cuando se programa con objetos esto no es as, cada objeto contiene datos y funciones, y un programa se construye como un conjunto de objetos, o incluso como un nico objeto.

Mensaje
El mensaje es el modo en que se comunican los objetos entre si. En C++, un mensaje no es ms que una llamada a una funcin de un determinado objeto. Cuando llamemos a una funcin de un objeto, muy a menudo diremos que estamos enviando un mensaje a ese objeto. En este sentido, mensaje es el trmino adecuado cuando hablamos de programacin orientada a objetos en general.

Mtodo
Se trata de otro concepto de POO, los mensajes que lleguen a un objeto se procesarn ejecutando un determinado mtodo. En C++ un mtodo no es otra cosa que una funcin o procedimiento perteneciente a un objeto.

13

P.L.E.

P.O.O.

Clase
Una clase se puede considerar como un patrn para construir objetos. En C++, un objeto es slo un tipo de variable de una clase determinada. Es importante distinguir entre objetos y clases. La clase es simplemente una declaracin, no tiene asociado ningn objeto, de modo que no puede recibir mensajes ni procesarlos, esto nicamente lo hacen los objetos.

Interfaz
Las clases y por lo tanto tambin los objetos, tienen partes pblicas y partes privadas. Algunas veces llamaremos a la parte pblica de un objeto su interfaz. Se trata de la nica parte del objeto que es visible para el resto de los objetos, de modo que es lo nico de lo que se dispone para comunicarse con ellos.

Herencia
Veremos que es posible disear nuevas clases basndose en clases ya existentes. En C++ esto se llama derivacin de clases, y en POO herencia. Cuando se deriva una clase de otra, normalmente se aadirn nuevos mtodos y datos. Es posible que algunos de estos mtodos o datos de la clase original no sean vlidos, en ese caso pueden ser enmascarados en la nueva clase o simplemente eliminados. El conjunto de datos y mtodos que sobreviven, es lo que se conoce como herencia.

3. DECLARACIN DE UNA CLASE


Las clases son tipos de datos compuestos definidos por el usuario. Pueden contener miembros dato que representan el tipo, y miembros funcin que realizan operaciones sobre ese tipo. El concepto de clase incluye la idea de ocultacin de datos, encapsulacin o abstraccin de datos, que bsicamente consiste en que no se puede acceder a los datos directamente, sino que hay que hacerlo a travs de las funciones miembro (mtodos) de la clase. De esta forma se consiguen dos objetivos: i) Que el usuario no tenga acceso directo a la estructura interna de la clase, para no poder generar cdigo basado en la estructura de los datos. ii) Si en un momento alteramos la estructura de la clase, excepto el prototipo de la funcin miembro, el cdigo del usuario no tendr que ser retocado. La primera palabra que aparece es lgicamente class que sirve para declarar una clase. Su uso es parecido a la ya conocida struct:

class <identificador de clase> [<:lista de clases base>] { <lista de miembros> } [<lista de objetos>];
La lista de clases base se usa para derivar clases, de momento no le prestaremos demasiada atencin, ya que por ahora slo declararemos clases base. La lista de miembros ser en general una lista de funciones y datos.

14

P.L.E.

P.O.O.

Los datos se declaran del mismo modo en que lo hacamos hasta ahora, salvo que no pueden ser inicializados, recuerda que estamos hablando de declaraciones de clases y no de definiciones de objetos. Las funciones pueden ser simplemente declaraciones de prototipos, que se deben definir aparte de la clase. Cuando se definen fuera de la clase se debe usar el operador de mbito "::". Ejemplo.
#include <iostream.h> class pareja { private: // Datos miembro de la clase "pareja" int a,b; public: // Funciones miembro de la clase "pareja" void Lee(int &a2, int &b2); void Guarda(int a2, int b2) // Funcin inline { a = a2; b = b2; } }; void pareja::Lee(int &a2, int &b2) { a2 = a; b2 = b; } void main( ) { int x, y; pareja par1; par1.Guarda(12, 32); par1.Lee(x, y); cout << "Valor de par1.a: " << x << endl; cout << "Valor de par1.b: " << y << endl; cin.get( ); // pulsa una tecla para continuar. }

Nuestra clase "pareja" tiene dos miembros de tipo de datos: a y b. Y dos funciones, una para leer esos valores y otra para modificarlos. En el caso de la funcin "Lee" la hemos declarado en el interior de la clase y definido fuera, observa que en el exterior de la declaracin de la clase tenemos que usar la expresin:
void pareja::Lee(int &a2, int &b2)

para que quede claro que nos referimos a la funcin "Lee" de la clase "pareja". Ten en cuenta que pueden existir otras clases que tengan funciones con el mismo nombre, y tambin que si no especificamos que estamos definiendo una funcin de la clase "pareja", en realidad estaremos definiendo una funcin corriente. En el caso de la funcin "Guarda" la hemos definido en el interior de la propia clase. Esto lo haremos slo cuando la definicin sea muy simple, ya que dificulta la lectura y comprensin del programa. Adems, las funciones definidas de este modo sern tratadas como "inline", y esto slo es recomendable para funciones cortas, ya que en estas funciones se inserta el cdigo cada vez que son llamadas.
15

P.L.E.

P.O.O.

Para restringir el acceso a los datos miembro de una clase, C++ provee las palabras clave private, public y protected. Estas palabras denominadas especificadores de acceso, se usarn en el cuerpo de la clase para indicar el nivel de acceso a los datos miembro de la misma. Para hacernos una idea aproximada, una clase sin secciones privadas es aquella en la que no se aplica la encapsulacin. Esta posibilidad no es muy interesante en la programacin habitual, pero puede servir de introduccin a las clases. Una estructura sencilla podra ser la siguiente (en este caso C y C++ coinciden):
struct C_Cuenta { double Saldo; double Interes; };

A los datos declarados dentro de una estructura se les da el nombre de variables miembro, datos miembro, o simplemente miembros de la estructura. La estructura del ejemplo anterior tiene dos miembros: Saldo e Interes. La visibilidad de estos dos miembros es el bloque encerrado entre las llaves, pero al igual que en C se puede acceder a ellos desde fuera del bloque por medio de los operadores punto en caso de variable tipo estructura (.) y flecha en caso de variable puntero a estructura (->). Para declarar objetos de la estructura C_Cuenta, en C ANSI habra que hacer lo siguiente: struct C_Cuenta c1, c2; pero al programar en C++ bastara con escribir: C_Cuenta c1, c2; ya que en C++ una estructura o clase definida por el usuario es como un tipo de variable. As c1.Saldo se refiere a la variable Saldo de c1, y c2.Interes se refiere a la variable Interes de la c2. En C++ las estructuras definidas como en el ejemplo anterior son verdaderas clases. Se pueden aadir algunas funciones a la clase anterior para que sea un caso ms real. Estas funciones sern los mtodos que permitirn interactuar con las variables de esa clase. As la clase C_Cuenta puede quedar de esta manera:
struct C_Cuenta { // Variables miembro double Saldo; // Saldo Actual de la cuenta double Interes; // Inters aplicado // Mtodos double C_Cuenta::GetSaldo( ) { return Saldo; } double C_Cuenta::GetInteres( ) { return Interes;} void C_Cuenta::SetSaldo(double unSaldo) { Saldo = unSaldo; } void C_Cuenta::SetInteres(double unInteres) { Interes = unInteres;} }; void main(void) { C_Cuenta c1; c1.Interes = 4.0; // vlida, pero se viola el principio de encapsulacin c1.SetInteres(4.0); // correcto }

16

P.L.E.

P.O.O.

Sin embargo, podemos tambin definir la clase del siguiente modo:


class C_Cuenta { // Variables miembro private: char *Nombre; // Nombre de la persona double Saldo; // Saldo Actual de la cuenta double Interes; // Inters aplicado public: // Mtodos char *GetNombre( ) { return Nombre; } double GetSaldo( ) { return Saldo; } double GetInteres( ) { return Interes; } void SetSaldo(double unSaldo) { Saldo = unSaldo; } void SetInteres(double unInteres) { Interes = unInteres; } void Ingreso(double unaCantidad) { SetSaldo( GetSaldo() + unaCantidad ); } };

Una primera diferencia respecto a los ejemplos anteriores es que se ha sustituido la palabra struct (bien conocida en C) por la palabra class, que es propia de C++. Ambas palabras pueden ser utilizadas en la definicin de las clases de C++ de los dos modos siguientes: 1. Definiendo la clase con la palabra struct la opcin por defecto es public, de modo que todas las variables y funciones miembro son public excepto las indicadas expresamente como private. 2. Por el contrario, utilizando la palabra class la opcin por defecto es private, de modo que todas las variables y funciones miembro son private excepto las indicadas expresamente como public. Si se incluyen las dos palabras -public y private- es igual utilizar struct o class. En lo sucesivo se utilizar la palabra class, que se considera ms segura y ms propia de C++. Un miembro declarado como privado (private) puede ser utilizado solamente por las funciones miembro de su propia clase o por funciones amigas (friend) - (sinnimo de variable local) - de su clase. En cambio, no puede ser utilizado por funciones externas o por las funciones propias de una clase derivada. En cambio, los datos y las funciones publicas (public) son accesibles por todas las funciones del programa fuera de la clase (sinnimo de variable global), pero no se puede acceder desde una clase derivada de la clase base. Un miembro declarado como protegido (protected) se comporta igual que uno privado para las funciones externas, pero acta como pblico para las funciones miembro de una clase derivada. Por defecto, en una clase se define todos los datos y funciones privados, a menos de que le especifiquemos las que son publicas con la instruccin public.

17

P.L.E.

P.O.O.

Para saber si una funcin debe ser definida publica o privada, debemos ver si el resto del programa necesita conocer como funciona dicha funcin. Si la respuesta es si, entonces la funcin debe ser publica, en caso contrario debe ser privada. La forma general de describir una clase seria mas o menos:
class nombre_clase { private: //datos y funciones privadas protected: //datos y funciones protegidas public: //datos y funciones publicas funcion constructora; funcion destructora; };

La solucin que se utiliza habitualmente en C++ para resolver el problema de la encapsulacin u ocultamiento de datos (data hidding) es que las funciones miembro pblicas sean el nico camino para acceder a las variables miembro (y funciones miembro) privadas. De esta manera, el programador de la clase puede tomar las medidas necesarias para que otros usuarios de la clase no modifiquen datos o funciones sin estar autorizados para ello. Las funciones miembro pblicas son habitualmente el interfase entre los datos contenidos en los objetos de la clase y los usuarios de la clase. Estos usuarios no necesitan tener acceso a los detalles internos de dichos objetos; slo necesitan saber cmo se utilizan las funciones miembro pblicas. Esto facilita el mantenimiento y la mejora del programa: la clase y sus funciones miembro pueden ser sustituidas por una versin nueva, ms eficiente. En tanto en cuanto la declaracin de las funciones miembro pblicas se mantenga igual, el cambio no tendr ninguna repercusin en los dems programas y funciones que hacen uso de la clase.

4. CREACIN DE OBJETOS
Las clases son tipos de datos definidos por el usuario que definen un tipo de objeto. Un objeto es un modelo o instancia de ese tipo o clase, de modo que un objeto es a una clase como una variable a un tipo. Por eso, a veces, el trmino objeto se utiliza indistintamente como instancia o modelo de una clase y tambin como variable. El siguiente programa declara dos objetos rectngulo y llama a las funciones miembro de la clase dimensionar y area:
#include <stdio.h> void main(void) { rectangulo rect1, rect2; rect1.dimensionar(10, 20); rect2.dimensionar(100, 200); printf(El rea del primer rectngulo es: %d\n, rect1.area( )); printf(El rea del segundo rectngulo es: %d\n, rect2.area( )); }

18

P.L.E.

P.O.O.

4.1. MIEMBROS DE UNA FUNCIN Las funciones se incluyen en una definicin de la clase encerrando sus definiciones dentro del cuerpo de la clase. Sin embargo, es posible definir tambin el cuerpo de la funcin fuera de la clase. El mbito de la funcin se debe indicar por el operador de mbito ( :: ) con el nombre de la clase. Ejemplo de la clase rectngulo:
class rectangulo { int longitud, anchura; public: void dimensionar(int b, int h); int area(void); }; void rectangulo :: dimensionar (int b, int h) { longitud = b; anchura = h; } int rectangulo :: area (void) { return (longitud*anchura); }

4.2. CONSTRUCTORES Y DESTRUCTORES Un constructor es una funcin miembro especial de una clase que se llama de forma automtica siempre que se declara un objeto de esa clase. Su funcin es la de crear e inicializar un objeto de esa clase. Tiene el mismo nombre de la clase con la cual est asociado y puede tener parmetros como cualquier otra funcin, pero no puede devolver valores. Si no se define algn constructor para una clase, el compilador generar un constructor por defecto. Adems deben ser pblicos, no tendra ningn sentido declarar un constructor como privado, ya que siempre se usan desde el exterior de la clase, ni tampoco como protegido, ya que no puede ser heredado. Aadamos un constructor a nuestra clase pareja:
#include <iostream.h> class pareja { private: // Datos miembro de la clase "pareja" int a,b; public: pareja(int a2, int b2); // Constructor // Funciones miembro de la clase "pareja" void Lee(int &a2, int &b2); void Guarda(int a2, int b2) // funcin inline { a = a2; b = b2; } }; pareja::pareja(int a2, int b2) { a = a2; b = b2; } void pareja::Lee(int &a2, int &b2) { a2 = a; b2 = b; }

19

P.L.E.

P.O.O.

void main( ) { int x, y; pareja par1(12, 32); par1.Lee(x, y); cout << "Valor de par1.a: " << x << endl; cout << "Valor de par1.b: " << y << endl; cin.get( ); // pulsa una tecla para continuar. }

Si una clase posee constructor, ser llamado siempre que se declare un objeto de esa clase, y si requiere argumentos, es obligatorio suministrarlos. Por ejemplo, las siguientes declaraciones son ilegales:
pareja par1; pareja par1();

La primera porque el constructor de "pareja" requiere dos parmetros, y no se suministran. La segunda es ilegal por otro motivo ms complejo. Aunque existiese un constructor sin parmetros, no se debe usar esta forma para declarar el objeto, ya que el compilador lo considera como la declaracin de un prototipo de una funcin que devuelve un objeto de tipo "pareja" y no admite parmetros. Cuando se use un constructor sin parmetros para declarar un objeto no se deben escribir los parntesis. Y las siguientes declaraciones son vlidas:
pareja par1(12,43); pareja par2(45,34);

El uso del constructor es tan importante que, en el caso de que el programador no defina ningn constructor para una clase, el compilador de C++ proporciona un constructor de oficio sin argumentos. Cuando se crean objetos locales, los datos miembros no se inicializaran, contendran la "basura" que hubiese en la memoria asignada al objeto. Si se trata de objetos globales, los datos miembros se inicializan a cero. Inicializacin de objetos La idea del constructor es inicializar variables, y una sentencia de asignacin no es la nica ni la mejor forma de inicializar una variable. C++ permite inicializar variables miembro fuera del cuerpo del constructor. Slo los constructores admiten inicializadores. Cada inicializador consiste en el nombre de la variable miembro a inicializar, seguida de la expresin que se usar para inicializarla entre parntesis. Los inicializadores se aadirn a continuacin del parntesis cerrado que encierra a los parmetros del constructor, antes del cuerpo del constructor y separado del parntesis por dos puntos ":". Por ejemplo, en el caso anterior de la clase "pareja":
pareja::pareja(int a2, int b2) { a = a2; b = b2; }

20

P.L.E.

P.O.O.

Podemos sustituir el constructor por: pareja::pareja(int a2, int b2) : a(a2), b(b2) {} Por supuesto, tambin pueden usarse inicializadores en lnea, dentro de la declaracin de la clase. Ciertos miembros es obligatorio inicializarlos, ya que no pueden ser asignados, por ejemplo las constantes o las referencias. Es muy recomendable usar la inicializacin siempre que sea posible en lugar de asignaciones, ya que se desde el punto de vista de C++ es mucho ms seguro. Veremos ms sobre este tema cuando veamos ejemplos de clases que tienen como miembros objetos de otras clases. Sobrecarga de constructores Adems, tambin pueden definirse varios constructores para cada clase, es decir, la funcin constructor puede sobrecargarse. La nica limitacin es que no pueden declararse varios constructores con el mismo nmero y el mismo tipo de argumentos. Por ejemplo, aadiremos un constructor adicional a la clase "pareja" que simule el constructor por defecto:
class pareja { public: // Constructor pareja(int a2, int b2) : a(a2), b(b2) {} pareja( ) : a(0), b(0) {} // Funciones miembro de la clase "pareja" void Lee(int &a2, int &b2); void Guarda(int a2, int b2) private: // Datos miembro de la clase "pareja" int a,b; };

De este modo podemos declarar objetos de la clase pareja especificando los dos argumentos o ninguno de ellos, en este ltimo caso se inicializarn los datos miembros con ceros. Constructores con argumentos por defecto Tambin pueden asignarse valores por defecto a los argumentos del constructor, de este modo reduciremos el nmero de constructores necesarios. Se llama constructor por defecto a un constructor que no necesita que se le pasen parmetros o argumentos para inicializar las variables miembro de la clase. Un constructor por defecto es pues un constructor que no tiene argumentos o que, si los tiene, todos sus argumentos tienen asignados un valor por defecto en la declaracin del constructor. En cualquier caso, puede ser llamado sin tenerle que pasar ningn argumento. Para resolver el ejemplo anterior sin sobrecargar el constructor suministraremos valores por defecto nulos a ambos parmetros:

21

P.L.E.

P.O.O.

class pareja { public: // Constructor pareja(int a2=0, int b2=0) : a(a2), b(b2) {} // Funciones miembro de la clase "pareja" void Lee(int &a2, int &b2); void Guarda(int a2, int b2) private: // Datos miembro de la clase "pareja" int a,b; };

Asignacin de objetos Probablemente ya lo imaginas, pero la asignacin de objetos tambin est permitida. Y adems funciona como se supone que debe hacerlo, asignando los valores de los datos miembros. Con la definicin de la clase del ltimo ejemplo podemos hacer lo que se ilustra en el siguiente:
void main( ) { int x, y; pareja par1(12, 32), par2; par2 = par1; par2.Lee(x, y); cout << "Valor de par2.a: " << x << endl; cout << "Valor de par2.b: " << y << endl; cin.get(); }

La lnea "par2 = par1;" copia los valores de los datos miembros de par1 en par2. Constructor copia Un constructor de este tipo crea un objeto inicializndolo a partir de otro objeto de la misma clase.. Estos constructores slo tienen un argumento, que es una referencia a un objeto de su misma clase. En general, los constructores copia tienen la siguiente forma para sus prototipos: tipo_clase::tipo_clase(const tipo_clase &obj); De nuevo ilustraremos esto con un ejemplo y usaremos tambin "pareja":
#include <iostream.h> class pareja { public: // Constructor pareja(int a2=0, int b2=0); // Constructor copia: pareja(const pareja &p); // Funciones miembro de la clase "pareja" void Lee(int &a2, int &b2); void Guarda(int a2, int b2); private: // Datos miembro de la clase "pareja" int a, b; };

22

P.L.E. pareja::pareja(int a2, int b2) { a = a2; b = b2; } pareja::pareja(const pareja &p) { a = p.a; b = p.b; } void pareja::Lee(int &a2, int &b2) { a2 =a; b2 =b; } void pareja::Guarda(int a2, int b2) { a = a2; b = b2; } void main(void) { pareja par1(12, 32); pareja par2(par1); // Uso del constructor copia: par2 = par1 int x, y; par2.Lee(x, y); cout << "Valor de par2.a: " << x << endl; cout << "Valor de par2.b: " << y << endl; }

P.O.O.

Tambin en este caso, si no se especifica ningn constructor copia, el compilador crea uno por defecto, y su comportamiento es exactamente el mismo que el del definido en el ejemplo anterior. Para la mayora de los casos esto ser suficiente, pero en muchas ocasiones necesitaremos redefinir el constructor copia. Veamos cmo surge la necesidad de escribir un constructor de copia distinto del que proporciona el compilador. Considrese una clase Alumno con dos variables miembro: class Alumno { char* nombre; long nmat; }; En realidad, esta clase no incluye el nombre del alumno, sino slo un puntero a carcter que permitir almacenar la direccin de memoria donde est realmente almacenado el nombre. Esta memoria se reservar dinmicamente cuando el objeto vaya a ser inicializado. Lo importante es darse cuenta de que el nombre no es realmente una variable miembro de la clase: la variable miembro es un puntero a la zona de memoria donde est almacenado. Esta situacin se puede ver grficamente en la figura 1, en la que se muestra un objeto a de la clase Alumno.
Objetc Alumno a char *name long carnet Luis Prez\0

Figura1: Objeto con reserva dinmica de memoria.

23

P.L.E.

P.O.O.

Supongamos ahora que con el constructor de copia suministrado por el compilador se crea un nuevo objeto b a partir de a. Las variables miembro de b van a ser una copia bit a bit de las del objeto a. Esto quiere decir que la variable miembro b.nombre contiene la misma direccin de memoria que a.nombre. Esta situacin se representa grficamente en la figura 2: ambos objetos apuntan a la misma direccin de memoria.
Objetc Alumno a char *name long carnet Luis Prez\0

Alumno b = a

Objetc Alumno b char *name long carnet Figura2: Copia bit a bit del objeto de la figura 1.

La situacin mostrada en la figura 2 puede tener consecuencias no deseadas. Por ejemplo, si se quiere cambiar el nombre del Alumno a, lo primero que se hace es liberar la memoria a la que apunta a.nombre, reservar memoria para el nuevo nombre haciendo que a.nombre apunte al comienzo de dicha memoria, y almacenar all el nuevo nombre de a. Como el objeto b no se ha tocado, su variable miembro b.nombre se ha quedado apuntado a una posicin de memoria que ha sido liberada en el proceso de cambio de nombre de a. La consecuencia es que b ha perdido informacin y los ms probable es que el programa falle. Se llega a una situacin parecida cuando se destruye uno de los dos objetos a o b. Al destruir uno de los objetos se libera la memoria que comparten, con el consiguiente perjuicio para el objeto que queda, puesto que su puntero contiene la direccin de una zona de memoria liberada, disponible para almacenar otra informacin.
Objetc Alumno a char *name long carnet Luis Prez\0

Alumno b = a

Objetc Alumno b char *name long carnet

Luis Prez\0

Figura3: Copia correcta del objeto de la figura 1.

Finalmente, la figura 3 muestra la situacin a la que se llega con un constructor de copia correctamente programado por el usuario. En este caso, el constructor no copia bit a bit la direccin contenida en a.nombre, sino que reserva memoria, copia a esa memoria el contenido apuntado por a.nombre, y guarda en b.nombre la direccin de esa nueva memoria reservada. Ninguno de los problemas anteriores sucede ahora.

24

P.L.E.

P.O.O.

El complemento a los constructores de una clase es el destructor. As como el constructor se llama al declarar o crear un objeto, el destructor es llamado cuando el objeto va a dejar de existir por haber llegado al final de su vida, es decir, es llamado automticamente cuando se abandona el mbito en el que fue definido. En el caso de que un objeto (local o auto) haya sido definido dentro de un bloque {}, el destructor es llamado cuando el programa llega al final de ese bloque. Si el objeto es global o static su duracin es la misma que la del programa, y por tanto el destructor es llamado al terminar la ejecucin del programa. Los objetos creados con reserva dinmica de memoria (en general, los creados con el operador new) no estn sometidos a las reglas de duracin habituales, y existen hasta que el programa termina o hasta que son explcitamente destruidos con el operador delete. En este caso la responsabilidad es del programador, y no del compilador o del sistema operativo. A diferencia del constructor, el destructor es siempre nico (no puede estar sobrecargado) y no tiene argumentos en ningn caso. Tampoco tiene valor de retorno, ni puede ser heredado. Su nombre es el mismo que el de la clase precedido por el carcter tilde (~), carcter que se consigue con Alt+126 en el teclado del PC. En el caso de que el programador no defina un destructor, el compilador de C++ proporciona un destructor de oficio, que es casi siempre plenamente adecuado (excepto para liberar memoria de vectores y matrices). En general, ser necesario definir un destructor cuando nuestra clase tenga datos miembro de tipo puntero, aunque esto no es una regla estricta. Ejemplo:
#include <iostream.h> #include <cstring.h> class cadena { public: cadena(); // Constructor por defecto cadena(char *c); // Constructor desde cadena c cadena(int n); // Constructor de cadena de n caracteres cadena(const cadena &); // Constructor copia ~cadena(); // Destructor void Asignar(char *dest); char *Leer(char *c); private: char *cad; // Puntero a char: cadena de caracteres }; cadena::cadena() : cad(NULL) { } cadena::cadena(char *c) { cad = new char[strlen(c)+1]; // Reserva memoria para cadena strcpy(cad, c); // Almacena la cadena } cadena::cadena(int n) { cad = new char[n+1]; // Reserva memoria para n caracteres cad[0] = 0; // Cadena vaca }

25

P.L.E. cadena::cadena(const cadena &Cad) { // Reservamos memoria para la nueva y la almacenamos cad = new char[strlen(Cad.cad)+1]; // Reserva memoria para cadena strcpy(cad, Cad.cad); // Almacena la cadena } cadena::~cadena() { delete[] cad; // Libera la memoria reservada a cad } void cadena::Asignar(char *dest) { // Eliminamos la cadena actual: delete[ ] cad; // Reservamos memoria para la nueva y la almacenamos cad = new char[strlen(dest)+1]; // Reserva memoria para la cadena strcpy(cad, dest); // Almacena la cadena } char *cadena::Leer(char *c) { strcpy(c, cad); return c; } void main() { cadena Cadena1("Cadena de prueba"); cadena Cadena2(Cadena1); // Cadena2 es copia de Cadena1 cadena *Cadena3; // Cadena3 es un puntero char c[256]; // Modificamos Cadena1: Cadena1.Asignar("Otra cadena diferente"); // Creamos Cadena3: Cadena3 = new cadena("Cadena de prueba n 3"); // Ver resultados cout << "Cadena 1: " << Cadena1.Leer(c) << endl; cout << "Cadena 2: " << Cadena2.Leer(c) << endl; cout << "Cadena 3: " << Cadena3->Leer(c) << endl; delete Cadena3; // Destruir Cadena3. // Cadena1 y Cadena2 se destruyen automticamente cin.get(); }

P.O.O.

Voy a hacer varias observaciones sobre este programa: 1. Hemos implementado un constructor copia. Esto es necesario porque una simple asignacin entre los datos miembro "cad" no copiara la cadena de un objeto a otro, sino nicamente los punteros. Por ejemplo, si definimos el constructor copia como: cadena::cadena(const cadena &Cad) { cad = Cad.cad; } En lugar de cmo lo hacemos, lo que estaramos copiando sera el valor del puntero cad, con lo cual, ambos punteros estaran apuntando a la misma posicin de memoria. Esto es desastroso, y no simplemente porque los cambios en una cadena afectan a las dos, sino porque al abandonar el programa se intenta liberar automticamente la misma memoria dos veces. Lo que realmente pretendemos al asignar cadenas es crear una nueva cadena que sea copia de la cadena antigua. Esto es lo que hacemos con el constructor copia, y es lo que haremos ms adelante, y con ms elegancia, sobrecargando el operador de asignacin.

26

P.L.E.

P.O.O.

La definicin del constructor copia que hemos creado en este ltimo ejemplo es la equivalente a la del constructor copia por defecto. 2. La funcin Leer, que usamos para obtener el valor de la cadena almacenada, no devuelve un puntero a la cadena, sino una copia de la cadena. Esto est de acuerdo con las recomendaciones sobre la programacin orientada a objetos, que aconsejan que los datos almacenados en una clase no sean accesibles directamente desde fuera de ella, sino nicamente a travs de las funciones creadas al efecto. Adems, el miembro cad es privado, y por lo tanto debe ser inaccesible desde fuera de la clase. Ms adelante veremos cmo se puede conseguir mantener la seguridad sin crear ms datos miembro. 3. La Cadena3 debe ser destruida implcitamente usando el operador delete, que a su vez invoca al destructor de la clase. Esto es as porque se trata de un puntero, y la memoria que se usa en el objeto al que apunta no se libera automticamente al destruirse el puntero Cadena3.

5. PUNTERO this
El puntero this es una variable predefinida para todas las funciones u operadores miembro de una clase. Este puntero contiene la direccin del objeto concreto de la clase al que se est aplicando la funcin o el operador miembro. Se puede decir que *this es un alias del objeto correspondiente. Conviene tener en cuenta que cuando una funcin miembro se aplica a un objeto de su clase (su argumento implcito), accede directamente a las variables miembro (sin utilizar el operador punto o flecha), pero no tiene forma de referirse al objeto como tal, pues no le ha sido pasado explcitamente como argumento. Cada funcin de una clase puede hacer referencia a los datos de un objeto, modificarlos o leerlos, pero si slo hay una copia de la funcin y varios objetos de esa clase, cmo hace la funcin para referirse a un dato de un objeto en concreto?. Este problema se resuelve con el puntero this. Cada objeto tiene asociado un puntero a si mismo que se puede usar para manejar sus miembros. Volvamos al ejemplo de la clase pareja:
class pareja { public: // Constructor pareja(int a2, int b2); // Constructor copia pareja(const pareja &p); // Funciones miembro de la clase "pareja" void Lee(int &a2, int &b2); void Guarda(int a2, int b2) private: // Datos miembro de la clase "pareja" int a,b; };

27

P.L.E.

P.O.O.

Para cada dato podemos referirnos de dos modos distintos, lo veremos con la funcin Guarda. Esta es la implementacin que usaremos para referirnos a los miembros de las clases:
void pareja::Guarda(int a2, int b2) { a = a2; b = b2; } void pareja::Guarda(int a2, int b2) { this->a = a2; this->b = b2; }

A veces necesitamos invocar a una funcin de una clase con una referencia a un objeto de la misma clase, pero las acciones a tomar sern diferentes dependiendo de si la referencia que pasamos se refiere al mismo objeto o a otro diferente, veamos cmo podemos usar el puntero this para determinar esto:
#include <iostream.h> class clase { public: clase() {;} void EresTu(clase& c) { if(&c == this) cout << "S, soy yo." << endl; else cout << "No, no soy yo." << endl; } }; void main() { clase c1, c2; c1.EresTu(c2); c1.EresTu(c1); cin.get(); }

La funcin "EresTu" recibe una referencia a un objeto de la clase "clase". Para saber si se trata del mismo objeto, comparamos la direccin del objeto recibido con el valor de this, si son la misma, es que se trata del mismo objeto.

6. CLASES Y FUNCIONES friend


Ya sabemos que los miembros privados de una clase no son accesibles para funciones y clases exteriores a dicha clase. Esto es un concepto de POO, el encapsulamiento hace que cada objeto se comporte de un modo autnomo y que lo que pase en su interior sea invisible para el resto de objetos. Cada objeto slo responde a ciertos mensajes y proporciona determinadas salidas. De todos modos, en algunos casos, puede suceder que dos clases vayan a trabajar conjuntamente con los mismos datos, y utilizar las funciones pblicas de acceso a esos datos no es la manera ms eficiente de hacerlo; conviene recordar que llamar a una funcin tiene un coste y que es mucho ms eficiente acceder directamente a una variable. sta no es la nica limitacin de las funciones miembro, ya que una funcin slo puede ser miembro de una nica clase. Pero, en ciertas ocasiones, querremos tener acceso a determinados miembros privados de un objeto de una clase desde otros objetos de clases diferentes.
28

P.L.E.

P.O.O.

Se puede concluir que a pesar de las grandes ventajas que tiene la encapsulacin, en muchas ocasiones es necesario dotar a la programacin orientada a objetos de una mayor flexibilidad. Esto se consigue por medio de las funciones friend. Declaraciones friend Una funcin friend de una clase es una funcin que no pertenece a la clase, pero que tiene permiso para acceder a sus variables y funciones miembro privadas por medio de los operadores punto (.) y flecha (->), sin tener que recurrir a las funciones miembro pblicas de la clase. Si una clase se declara friend de otra, todas sus funciones miembro son friend de esta segunda clase. Por lo tanto el modificador friend inhibe el sistema de proteccin de una clase. Las relaciones de "amistad" entre clases son parecidas a las amistades entre personas: La amistad no puede transferirse, si A es amigo de B, y B es amigo de C, esto no implica que A sea amigo de C. (La famosa frase: "los amigos de mis amigos son mis amigos", es falsa en C++, y probablemente tambin en la vida real). La amistad no puede heredarse. Si A es amigo de B, y C es una clase derivada de B, A no es amigo de C. (Los hijos de mis amigos, no tienen por qu ser amigos mos. De nuevo, el smil es casi perfecto). La amistad no es simtrica. Si A es amigo de B, B no tiene por qu ser amigo de A. (En la vida real, una situacin como esta har peligrar la amistad de A con B, pero de nuevo me temo que en realidad se trata de una situacin muy frecuente, normalmente A no sabe que B no se considera su amigo).

Veamos un ejemplo muy sencillo:


#include <iostream.h> class A { public: A(int i=0) : a(i) {} void Ver() { cout << a << endl; } private: int a; friend void Ver(A); // "Ver" es amiga de la clase A }; void Ver(A Xa) { // La funcin Ver puede acceder a miembros privados // de la clase A, ya que ha sido declarada "amiga" de A cout << Xa.a << endl; } void main() { A Na(10); Ver(Na); // Ver el valor de Na.a usando la funcin amiga Na.Ver(); // Equivalente a la anterior cin.get(); }

Como puedes ver, la funcin "Ver", que no pertenece a la clase A puede acceder al miembro privado de A y visualizarlo. Incluso podra modificarlo.

29

P.L.E.

P.O.O.

Funciones amigas en otras clases El siguiente caso es ms comn, se trata de cuando la funcin amiga forma parte de otra clase. El proceso es ms complejo. Veamos otro ejemplo:
#include <iostream.h> class A; // Declaracin previa (forward) class B { public: B(int i=0) : b(i) {} void Ver() { cout << b << endl; } EsMayor(A Xa); // Compara b con a private: int b; }; class A { public: A(int i=0) : a(i) {} void Ver() { cout << a << endl; } private: // Funcin amiga tiene acceso // a miembros privados de la clase A friend B::EsMayor(A Xa); int a; }; B::EsMayor(A Xa) { return b > Xa.a; } void main() { A Na(10); B Nb(12); Na.Ver(); Nb.Ver(); if(Nb.EsMayor(Na)) cout << "Nb es mayor que Na" << endl; else cout << "Nb no es mayor que Na" << endl; cin.get(); }

Puedes comprobar lo que pasa si eliminas la lnea donde se declara "EsMayor" como amiga de A. Es necesario hacer una declaracin previa de la clase A (forward) para que pueda referenciarse desde la clase B. Veremos que estas "amistades" son tiles cuando sobrecarguemos algunos operadores. Clases amigas. El caso ms comn de amistad se aplica a clases completas. Lo que sigue es un ejemplo de implementacin de una lista dinmica mediante el uso de dos clases "amigas".
#include <iostream.h> /* Clase para elemento de lista enlazada */ class Elemento { public: Elemento(int t); int Tipo() { return tipo;} private: int tipo; Elemento *sig; friend class Lista;

/* Constructor */ /* Obtener tipo */ /* Datos: */ /* Tipo */ /* Siguiente elemento */ /* Amistad con lista */ };

30

P.L.E. /* Clase para lista enlazada de nmeros*/ class Lista { public: Lista() : Cabeza(NULL) {} /* Constructor */ /* Lista vaca */ ~Lista() { LiberarLista(); } /* Destructor */ void Nuevo(int tipo); /* Insertar figura */ Elemento *Primero() /* Obtener primer elemento */ { return Cabeza; } /* Obtener el siguiente elemento a p */ Elemento *Siguiente(Elemento *p) { if(p) return p->sig; else return p; }; /* Si p no es NULL */ /* Averiguar si la lista est vaca */ int EstaVacio() { return Cabeza == NULL;} private: Elemento *Cabeza; /* Puntero al primer elemento */ void LiberarLista(); /* Funcin privada para borrar lista */ }; /* Constructor */ Elemento::Elemento(int t) : tipo(t), sig(NULL) {} /* Asignar datos desde lista de parmetros */ /* Aadir nuevo elemento al principio de la lista */ void Lista::Nuevo(int tipo) { Elemento *p; p = new Elemento(tipo); /* Nuevo elemento */ p->sig = Cabeza; Cabeza = p; } /* Borra todos los elementos de la lista */ void Lista::LiberarLista() { Elemento *p; while(Cabeza) { p = Cabeza; Cabeza = p->sig; delete p; } } void main() { Lista miLista; Elemento *e; // Insertamos varios valores en la lista miLista.Nuevo(4); miLista.Nuevo(2); miLista.Nuevo(1); // Y los mostramos en pantalla: e = miLista.Primero(); while(e) { cout << e->Tipo() << " ,"; e = miLista.Siguiente(e); } cout << endl; cin.get(); }

P.O.O.

31

P.L.E.

P.O.O.

La clase Lista puede acceder a todos los miembros de Elemento, sean o no pblicos, pero desde la funcin "main" slo podemos acceder a los miembros pblicos de nuestro elemento.

7. MODIFICADORES PARA MIEMBROS


Existen varias alternativas a la hora de definir algunos de los miembros de las clases. Esto es lo que veremos en este captulo. Estos modificadores afectan al modo en que se genera el cdigo de ciertas funciones y datos, o al modo en que se tratan los valores de retorno. Funciones en lnea (inline): A menudo nos encontraremos con funciones miembro cuyas definiciones son muy pequeas. En estos casos suele ser interesante declararlas como inline. Cuando hacemos eso, el cdigo generado para la funcin cuando el programa se compila, se inserta en el punto donde se invoca a la funcin, en lugar de hacerlo en otro lugar y hacer una llamada. Esto nos proporciona una ventaja, el cdigo de estas funciones se ejecuta ms rpidamente, ya que se evita usar la pila para pasar parmetros y se evitan las instrucciones de salto y retorno. Tambin tiene un inconveniente: se generar el cdigo de la funcin tantas veces como sta se use, con lo que el programa ejecutable final puede ser mucho ms grande. Es por esos dos motivos por los que slo se usan funciones inline cuando las funciones son pequeas. Hay que elegir con cuidado qu funciones declararemos inline y cuales no, ya que el resultado puede ser muy diferente dependiendo de nuestras decisiones. Hay dos maneras de declarar una funcin como inline. 1. La primera ya la hemos visto. Las funciones que se definen dentro de la d eclaracin de la clase son inline implcitamente. Por ejemplo:
class Ejemplo { public: Ejemplo(int a = 0) : A(a) {} private: int A; };

En este ejemplo hemos definido el constructor de la clase Ejemplo dentro de la propia declaracin, esto hace que se considere como inline. Cada vez que declaremos un objeto de la clase Ejemplo se insertar el cdigo correspondiente a su constructor. Si queremos que la clase Ejemplo no tenga un constructor inline deberemos declararla y definirla as:
class Ejemplo { public: Ejemplo(int a = 0); private: int A; }; Ejemplo::Ejemplo(int a) : A(a) {}

32

P.L.E.

P.O.O.

En este caso, cada vez que declaremos un objeto de la clase Ejemplo se har una llamada al constructor y slo existir una copia del cdigo del constructor en nuestro programa. 2. La otra forma de declarar funciones inline es hacerlo explcitamente, usando la palabra reservada inline. En el ejemplo anterior sera as:
class Ejemplo { public: Ejemplo(int a = 0); private: int A; }; inline Ejemplo::Ejemplo(int a) : A(a) {}

Funciones miembro constantes Esta es una propiedad que nos ser muy til en la depuracin de nuestras clases. Adems proporciona ciertos mecanismos necesarios para mantener la proteccin de los datos. Cuando una funcin miembro no modifique el valor de ningn dato de la clase, podemos y debemos declararla como constante. Esto no evitar que la funcin intente modificar los datos del objeto; a fin de cuentas, el cdigo de la funcin lo escribimos nosotros; pero generar un error durante la compilacin si la funcin intenta modificar alguno de los datos miembro del objeto. Por ejemplo:
#include <iostream.h> class Ejemplo2 { public: Ejemplo2(int a = 0) : A(a) {} void Modifica(int a) { A = a; } int Lee() const { return A; } private: int A; }; void main() { Ejemplo2 X(6); cout << X.Lee() << endl; X.Modifica(2); cout << X.Lee() << endl; cin.get();

Para experimentar, comprueba lo que pasa si cambias la definicin de la funcin "Lee()" por estas otras:
int Lee() const { A++; return A; } int Lee() const { Modifica(A+1); return A; } int Lee() const { Modifica(3); return A; }

Vers que el compilador no lo permite.

33

P.L.E.

P.O.O.

Evidentemente, si somos nosotros los que escribimos el cdigo de la funcin, sabemos si la funcin modifica o no los datos, de modo que en rigor no necesitamos saber si es o no constante, pero frecuentemente otros programadores pueden usar clases definidas por nosotros, o nosotros las definidas por otros. En ese caso es frecuente que slo se disponga de la declaracin de la clase, y el modificador "const" nos dice si se modifica o no los datos del objeto. Miembros estticos de una clase (Static) Cada objeto de una clase tiene su propia copia de cada una de las variables miembro de esa clase. Sin embargo, a veces puede interesar que una variable miembro sea comn para todos los objetos de la clase, de modo que todos compartan el mismo valor. En tal caso se puede utilizar una variable miembro static. Los miembros static tienen algunas propiedades especiales. En el caso de los datos miembro static slo existir una copia que compartirn todos los objetos de la misma clase. Si consultamos el valor de ese dato desde cualquier objeto de esa clase obtendremos siempre el mismo resultado, y si lo modificamos, lo modificaremos para todos los objetos. Por ejemplo:
# include <iostream.h> class Ejemplo { public: Ejemplo (void); void visualiza(void); private: int ejemplo1; static int ejemplo2; }; int Ejemplo :: ejemplo2; Ejemplo :: Ejemplo (void) { ejemplo1 = 1; ejemplo2 = 1; } void Ejemplo :: visualiza (void) { ejemplo1++; ejemplo2++; cout << "ejemplo1 = " << ejemplo1 << '\n'; cout << "ejemplo2 = " << ejemplo2 << '\n'; ; } void main() { Ejemplo primero, segundo; primero.visualiza(); segundo.visualiza(); }

34

P.L.E.

P.O.O.

Las caractersticas ms importantes de las variables miembro static: Slo existe una copia de cada una de las variables miembro static. Es decir, todos los objetos declarados de esa clase hacen referencia a la misma variable, esto es, a la misma posicin de memoria. Las variables static pueden ser public o private, del mismo modo que el resto de las variables miembro. Las variables static de una clase existen aunque no se haya declarado ningn objeto de esa clase. Esto quiere decir que la memoria reservada para este tipo de variable se ocupa en el momento de la definicin de la clase, no en el momento de la declaracin de los objetos. Para referirse a una variable static se puede utilizar el nombre de un objeto y el operador punto (.), pero esta notacin es confusa ya que se est haciendo referencia a una variable comn a todos los objetos de una clase mediante el nombre de uno slo de ellos. Por eso es mejor utilizar el nombre de la clase y el scope resolution operator (::): Nombre_de_la_clase::variable_static Las variables static no se pueden inicializar en un constructor, porque se inicializaran muchas veces. Si se desea inicializarlas debe hacerse en el fichero que contiene las definiciones de las funciones miembro de esa clase: tipo Nombre_de_la_clase::variable_static = valor_inicial; Las variables static tienen una cierta similitud con las variables global, pero difieren en que el scope de las variables miembro static puede ser controlado por el programador definindolo como private, public o protected. Veamos un ejemplo ms complejo:
#include <iostream.h> class Numero { public: Numero(int v = 0); ~Numero(); void Modifica(int v); int LeeValor() const { return Valor; } int LeeCuenta() const { return Cuenta; } int LeeMedia() const { return Media; } private: int Valor; static int Cuenta; static int Suma; static int Media; void CalculaMedia(); }; Numero::Numero(int v) : Valor(v) { Cuenta++; Suma += Valor; CalculaMedia(); }

35

P.L.E. Numero::~Numero() { Cuenta--; Suma -= Valor; CalculaMedia(); } void Numero::Modifica(int v) { Suma -= Valor; Valor = v; Suma += Valor; CalculaMedia(); } //inicializacion de variables estaticas int Numero::Cuenta = 0; int Numero::Suma = 0; int Numero::Media = 0; void Numero::CalculaMedia() { if(Cuenta > 0) Media = Suma/Cuenta; else Media = 0; } void main() { Numero A(6), B(3), C(9), D(18), E(3); Numero *X; cout << "INICIAL" << endl; cout << "Cuenta: " << A.LeeCuenta() << endl; cout << "Media: " << A.LeeMedia() << endl; B.Modifica(11); cout << "Modificamos B=11" << endl; cout << "Cuenta: " << B.LeeCuenta() << endl; cout << "Media: " << B.LeeMedia() << endl; X = new Numero(548); cout << "Nuevo elemento dinmico de valor 548" << endl; cout << "Cuenta: " << X->LeeCuenta() << endl; cout << "Media: " << X->LeeMedia() << endl; delete X; cout << "Borramos el elemento dinmico" << endl; cout << "Cuenta: " << D.LeeCuenta() << endl; cout << "Media: " << D.LeeMedia() << endl; cin.get(); }

P.O.O.

Observa que es necesario declarar e inicializar los miembros static de la clase, por dos motivos. El primero es que los miembros static deben existir aunque no exista ningn objeto de la clase, declarar la clase no crea los datos miembro estticos, es necesario hacerlo explcitamente. El segundo es porque si no lo hiciramos, al declarar objetos de esa clase los valores de los miembros estticos estaran indefinidos, y los resultados no seran los esperados.

36

P.L.E.

P.O.O.

Ejemplo: contador del nmero de objetos creados de una clase dada:


#include <iostream.h> #include <string.h> class C_Cuenta { // Variables miembro private: char *Nombre; // Nombre de la persona double Saldo; // Saldo Actual de la cuenta double Interes; // Inters calculado hasta el momento static int Cuantas; // Nmero de cuentas abiertas public: // Constructor C_Cuenta(const char *unNombre, double unSaldo=0.0, double unInteres=0.0) { Nombre = new char[strlen(unNombre)+1]; strcpy(Nombre, unNombre); SetSaldo(unSaldo); SetInteres(unInteres); Cuantas++;

}
// Destructor ~C_Cuenta() { delete []Nombre; cout << "Destruyendo cuenta:" << Cuantas-- << endl; } // Mtodos inline int getCuantasCuentas() { return Cuantas; } inline char *GetNombre() { return Nombre; } inline double GetSaldo() { return Saldo; } inline double GetInteres() { return Interes; } inline void SetSaldo(double unSaldo) { Saldo = unSaldo; } inline void SetInteres(double unInteres) { Interes = unInteres; } void Ingreso(double unaCantidad) { SetSaldo( GetSaldo() + unaCantidad ); } }; // definicin e inicializacin de variable static int C_Cuenta::Cuantas = 0; void main(void) { C_Cuenta c1("Igor"); cout << c1.getCuantasCuentas() << endl; // Imprime 1 C_Cuenta c2("Juan"); cout << c2.getCuantasCuentas() << endl; // Imprime 2 }

Se ve en el ejemplo anterior cmo el nmero total de cuentas creadas hasta el momento se almacena en una variable miembro static llamada cuantas. Esta variable se incrementa en una unidad cada vez que se llama al constructor y se decrementa en uno cada vez que se llama al destructor. Para conocer en un momento dado el valor de esta variable se utiliza la funcin getCuantasCuentas(). En el caso de la funciones miembro static la utilidad es menos evidente. Estas funciones no pueden acceder a los miembros de los objetos, slo pueden acceder a los datos miembro de la clase que sean static. Esto significa que no tienen puntero this, y adems suelen ser usadas con su nombre completo, incluyendo el nombre de la clase y el operador de mbito (::).

37

P.L.E.

P.O.O.

Algunas de las caractersticas ms importantes de este tipo de funciones se comentan a continuacin: Son funciones genricas que no actan sobre ningn objeto concreto de la clase. Como ya se ha dicho anteriormente, no pueden utilizar variables miembro que no sean static. No pueden utilizar el puntero this, ya que ste hace referencia al objeto concreto de la clase sobre la que se est trabajando. Las funciones static pueden recibir objetos de su clase como argumentos explcitos, aunque no como argumentos implcitos. Esto implica que cuando se desea que una funcin acte sobre dos objetos de una clase, las funciones static son una alternativa a las funciones friend para conseguir simetra en la forma de tratar a los dos objetos de la clase (que ambos pasen como argumentos explcitos). Veamos el siguiente ejemplo:
#include <iostream.h> #include <string.h> class persona { public: long DNI; char nombre[41]; persona(long dni, char *name) { DNI = dni; strcpy(nombre, name); } // funcion static con argumento persona static long getDNI(persona P) {return P.DNI;} }; void main (void) { persona p1(47126578, "Pepe Perez"); // se llama a la funcin static con el nombre de la clase cout << p1.nombre << " DNI: " << persona::getDNI(p1) << endl; cout << "Ya he terminado." << endl; }

Otro ejemplo:
#include <iostream.h> class Numero { public: Numero(int v = 0); void Modifica(int v) { Valor = v; } int LeeValor() const { return Valor; } int LeeDeclaraciones() const { return ObjetosDeclarados; } static void Reset() { ObjetosDeclarados = 0; } private: int Valor; static int ObjetosDeclarados; }; Numero::Numero(int v) : Valor(v) { ObjetosDeclarados++; } int Numero::ObjetosDeclarados = 0; void main() { Numero A(6), B(3), C(9), D(18), E(3); Numero *X;

38

P.L.E.
cout << "INICIAL" << endl; cout << "Objetos de la clase Numeros: " << A.LeeDeclaraciones() << endl; Numero::Reset(); cout << "RESET" << endl; cout << "Objetos de la clase Numeros: " << A.LeeDeclaraciones() << endl; X = new Numero(548); cout << "Cuenta de objetos dinmicos declarados" << endl; cout << "Objetos de la clase Numeros: " << A.LeeDeclaraciones() << endl; delete X; X = new Numero(8); cout << "Cuenta de objetos dinmicos declarados" << endl; cout << "Objetos de la clase Numeros: " << A.LeeDeclaraciones() << endl; delete X; cin.get(); }

P.O.O.

Observa cmo hemos llamado a la funcin Reset con su nombre completo. Aunque podramos haber usado "A.Reset()", es ms lgico usar el nombre completo, ya que la funcin puede ser invocada aunque no exista ningn objeto de la clase.

8. MS SOBRE LAS FUNCIONES


Funciones sobrecargadas Ya hemos visto que se pueden sobrecargar los constructores. Pues bien, las funciones miembros de las clases tambin pueden sobrecargarse, como supongo que ya habrs supuesto. Pondr un ejemplo:
#include <iostream.h> struct punto3D { float x, y, z; }; class punto { public: punto(float xi, float yi, float zi) : x(xi), y(yi), z(zi) {} punto(punto3D p) : x(p.x), y(p.y), z(p.z) {} void Asignar(float xi, float yi, float zi) { x = xi; y = yi; z = zi; } void Asignar(punto3D p) { Asignar(p.x, p.y, p.z); } void Ver() { cout << "(" << x << "," << y << "," << z << ")" << endl; } private: float x, y, z; }; void main() { punto P(0,0,0); punto3D p3d = {32,45,74}; P.Ver(); P.Asignar(p3d); P.Ver(); P.Asignar(12,35,12); P.Ver(); cin.get(); }

Como se ve, en C++ las funciones sobrecargadas funcionan igual dentro y fuera de las clases.

39

P.L.E.

P.O.O.

Funciones con argumentos con valores por defecto Tambin hemos visto que se pueden usar argumentos con valores por defecto en los constructores. En las funciones miembros de las clases tambin pueden usarse parmetros con valores por defecto, del mismo modo que fuera de las clases. De nuevo ilustraremos esto con un ejemplo:
#include <iostream.h> class punto { public: punto(float xi, float yi, float zi) : x(xi), y(yi), z(zi) {} void Asignar(float xi, float yi = 0, float zi = 0) { x = xi; y = yi; z = zi; } void Ver() { cout << "(" << x << "," << y << "," << z << ")" << endl;} private: float x, y, z; }; void main() { punto P(0,0,0); P.Ver(); P.Asignar(12); P.Ver(); P.Asignar(16,35); P.Ver(); P.Asignar(34,43,12); P.Ver(); cin.get(); }

9. ARRAYS DE OBJETOS
Al igual que lo hemos hecho con cualquier tipo de dato, podemos definir arrays de objetos. Para ilustrarlo veamos un ejemplo:
#include <iostream.h> class caja { int longitud; int ancho; static int dato_extra; // Declaracion de dato_extra public: caja(void); // Constructor void coloca(int nueva_longitud, int nuevo_ancho); int area(void); int extra(void) {return dato_extra++;} }; int caja::dato_extra; caja::caja(void) { longitud = 8; ancho = 8; // Definicion de dato_extra // Implementacion del constructor

40

P.L.E.

P.O.O.

dato_extra = 1; }
// Este metodo coloca un tamao de caja a los dos parametros de entrada

void caja::coloca(int nueva_longitud, int nuevo_ancho) { longitud = nueva_longitud; ancho = nuevo_ancho; } // Este metodo calcula y devuelve el area de una instancia de caja int caja::area(void) { return (longitud * ancho); } void main(void) { caja chica, mediana, grande, grupo[4]; // Siete cajas para trabajar chica.coloca(5, 7); grande.coloca(15, 20); for (int indice = 1 ; indice < 4 ; indice++) //grupo[0] usa valor por def. grupo[indice].coloca(indice + 10, 10); cout << "El area de caja chica es " << chica.area() << "\n"; cout << "El area de caja mediana es " << mediana.area() << "\n"; cout << "El area de caja grande es " << grande.area() << "\n"; for (indice = 0 ; indice < 4 ; indice++) cout << "El array area caja es " << grupo[indice].area() << "\n"; cout << "El valor de datos extra es " << chica.extra() << "\n"; cout << "El valor de datos extra es " << mediana.extra() << "\n"; cout << "El valor de datos extra es " << grande.extra() << "\n"; cout << "El valor de datos extra es " << grupo[0].extra() << "\n"; cout << "El valor de datos extra es " << grupo[3].extra() << "\n"; }

Respecto al funcionamiento del constructor cada uno de los cuatro objetos caja sern inicializados a los valores definidos dentro del constructor ya que el constructor ser ejecutado para cada objeto caja definido. Para definir un array de objetos debemos disponer de un constructor sin parmetros para dicho objeto. Esto es una consideracin de eficiencia ya que probablemente sera un error inicializar todos los elementos de un array de objetos al mismo valor. Tenemos un bucle for que empieza con 1, dejando que el primer objeto llamado grupo[0], utilice el valor almacenado por defecto cuando se llam al constructor. Al enviar un mensaje a uno de los objetos se utiliza el mismo constructor como con cualquier objeto, el nombre del array seguido de su ndice entre corchetes se utiliza para enviar un mensaje a uno de los objetos del array. A manera de ilustracin se ha incluido una variable adicional llamada dato_extra sttica y solo existir una copia de la misma. Los siete objetos de esta clase compartirn una sola copia de la variable la cual es global para todos los objetos definidos. La variable ha sido declarada lo que nos dice que existe en algn lugar, pero an no ha sido definida. Una declaracin nos indica la existencia de la variable y le asigna un nombre, pero la definicin, valga la redundancia, define un lugar para almacenarla dentro del espacio de memoria de la computadora. Por definicin, una variable esttica puede ser

41

P.L.E.

P.O.O.

declarada en el encabezado de una clase pero no puede ser definida ah, por lo general se hace en el archivo de implementacin.

10. PUNTEROS A OBJETOS


Veamos un ejemplo:
#include <iostream.h> class caja { int longitud; int ancho; public: caja(void); // Constructor void coloca(int nueva_longitud, int nuevo_ancho); int obtiene_area(void); }; caja::caja(void) // Implementacion del constructor { longitud = 8; ancho = 8; } // Este metodo determina el tamao de la caja segun los parametros de entrada void caja::coloca(int nueva_longitud, int nuevo_ancho) { longitud = nueva_longitud; ancho = nuevo_ancho; } // Este metodo calcula y retorna el area de una instancia de caja int caja::obtiene_area(void) { return (longitud * ancho); } void main(void) { caja chica, mediana, grande; caja *puntero; chica.coloca(5, 7); grande.coloca(15, 20); puntero = new caja; cout << "El area de la caja chica es " << chica.obtiene_area() << "\n"; cout << "El area de la caja mediana es " << mediana.obtiene_area() << "\n"; cout << "El area de la caja grande es " << grande.obtiene_area() << "\n"; cout << "La nueva area de caja es " << puntero->obtiene_area() << "\n"; puntero->coloca(12, 12); cout << "La nueva area de caja es " << puntero->obtiene_area() << "\n"; delete puntero; }

Definimos un puntero a un objeto de tipo caja y como slo es un puntero con nada a qu sealar, reservamos memoria dinmicamente para dicho puntero que apunta al objeto. Cuando el objeto es creado, el constructor es llamado automticamente para asignar valores a las dos variables de almacenamiento internas. Debemos observar que el constructor no es llamado durante la definicin del puntero ya que no hay nada que inicializar, es llamado reservarle memoria. La referencia a los componentes del objeto son manejados de una manera similar a

42

P.L.E.

P.O.O.

como se hace en las estructuras, utilizando el operador ->. Finalmente, el objeto es borrado.

11.

SOBRECARGA DE OPERADORES

Los operadores de C++ (excepto los operadores . :: .* ?), al igual que las funciones, pueden ser sobrecargados. Se pueden redefinir algunos de los operadores existentes en C++ para que acten de una manera definida por el programador, con los objetos de una clase determinada. Cuando se sobrecarga un operador, el operador no pierde su comportamiento original, sino que gana un comportamiento adicional relacionado con la clase para la que se define. Un operador puede estar sobrecargado o redefinido varias veces, de tal manera que acte de un modo distinto dependiendo del tipo de objetos que tenga como operandos. Es precisamente el tipo de los operandos lo que determina qu operador se utiliza en cada caso. Cuando se sobrecarga un operador: No se puede cambiar su precedencia y asociatividad No puede modificarse el nmero de operandos que tiene dicho operador Es necesario que al menos un operando sea un objeto de la clase en la que se ha definido el operador sobrecargado.

Un operador se sobrecarga mediante el uso de una funcin operadora miembro o mediante una funcin amiga (friend) de la clase para la que se define. Habitualmente: Se suelen declarar miembros de la clase los operadores unarios (es decir, aquellos que actan sobre un nico objeto), o los que modifican el primer operando, como sucede con los operadores de asignacin. Por el contrario, los operadores que actan sobre varios objetos sin modificarlos (operadores aritmticos y lgicos) se suelen declarar como friends.

Sobrecarga de operadores binarios Empezaremos por los operadores binarios, que como recordaris son aquellos que requieren dos operandos, como la suma o la resta. Sintaxis:
tipo_devuelto nombre_clase::operador# (lista_de_argumentos) { // operaciones varias }

donde # representa el operador que va a ser sobrecargado. o Cuando se sobrecarga un operador binario con una funcin operadora miembro, el operando izquierdo se pasa implcitamente a la funcin y el operando derecho se pasa como argumento. o En cambio, cuando se sobrecarga un operador unario, la funcin operadora miembro no tiene parmetros ya que el nico operando que hay se pasa implcitamente a la funcin.

43

P.L.E.

P.O.O.

o Cuando se sobrecarga un operador relacional o lgico, devuelven un entero que indica verdadero o falso. Esto permite poder integrarlos en expresiones lgicas y relacionales ms extensas que admitan otros tipos de datos. Veamos un ejemplo para una clase para el tratamiento de tiempos:
#include <iostream.h> class Tiempo { public: Tiempo(int h=0, int m=0) : hora(h), minuto(m) {} void Mostrar(); Tiempo operator+(Tiempo h); private: int hora; int minuto; }; Tiempo Tiempo::operator+(Tiempo h) { Tiempo temp; temp.minuto = minuto + h.minuto; temp.hora = hora + h.hora; if(temp.minuto >= 60) { temp.minuto -= 60; temp.hora++; } return temp; } void Tiempo::Mostrar() { cout << hora << ":" << minuto << endl; } void main(void) { Tiempo Ahora(12,24), T1(4,45); T1 = Ahora + T1; // (1) T1.Mostrar(); (Ahora + Tiempo(4,45)).Mostrar(); // (2) }

Hemos usado un objeto temporal para calcular el resultado de la suma, esto es necesario porque necesitamos operar con los minutos para prevenir el caso en que excedan de 60, en cuyo caso incrementaremos el tiempo en una hora. Ahora observa cmo utilizamos el operador en el programa. La forma (1) es la forma ms lgica, para eso hemos creado un operador, para usarlo igual que en las situaciones anteriores. Pero vers que tambin hemos usado el operador =, a pesar de que nosotros no lo hemos definido. Esto es porque el compilador crea un operador de asignacin por defecto si nosotros no lo hacemos (constructor de copia de oficio). La forma (2) no es el mejor modo de hacer lo que buscamos, pero ilustra cmo es posible crear objetos temporales sin nombre. En esta lnea hay dos, el primero Tiempo(4,45), que se suma a Ahora para producir otro objeto temporal sin nombre, que es el que mostramos en pantalla.

44

P.L.E.

P.O.O.

Otro ejemplo:
#include <iostream.h> class Caja { double longitud; double anchura, altura; public: void set(int l, int w, int h) {longitud = l; anchura = w; altura = h;} double volumen(void) {return longitud * anchura * altura;} Caja operator+(Caja a); }; Caja Caja::operator +(Caja a) { Caja temp; temp.longitud = longitud + a.longitud; temp.anchura = anchura + a.anchura; temp.altura = altura + a.altura; return temp; } void main(void) { Caja pequena, mediana, grande; Caja temp; pequena.set(2, 4, 5); mediana.set(5, 6, 8); grande.set(8, 10, 12); cout << "El volumen es " << pequena.volumen() << "\n"; cout << "El volumen es " << mediana.volumen() << "\n"; cout << "El volumen es " << grande.volumen() << "\n"; temp = pequena + mediana; cout << "El nuevo volumen es " << temp.volumen() << "\n"; }

Los operadores binarios que pueden sobrecargarse son los siguientes:


+, -, *, /, %, ^, &, |, (,), <, >, <=, >=, <<, >>, ==, !=, &&, ||, =, +=. -=, *=, /=, %=, ^=, &=, |=, <<=,>>=, [], (),->, new y delete.

Los operadores =, [], () y -> slo pueden sobrecargarse en el interior de clases. Sobrecargar el operador de asignacin Ya sabemos que el compilador crea un operador de asignacin por defecto si nosotros no lo hacemos. Ya vimos la necesidad de crear los constructores de copia para el caso de que un miembro de la clase fuera un puntero. Veamos un ejemplo:
#include <iostream.h> #include <string.h> class Cadena { public: Cadena(char *cad); Cadena() : cadena(NULL) {}; ~Cadena() { delete [] cadena; } void Mostrar() const; Cadena& operator=(const Cadena& c); private: char *cadena; };

45

P.L.E.
Cadena::Cadena(char *cad) { cadena = new char[strlen(cad)+1]; strcpy(cadena, cad); } void Cadena::Mostrar() const { cout << cadena << endl; } Cadena& Cadena::operator=(const Cadena& c) { if(this != &c) { delete [ ] cadena; if(c.cadena) { cadena = new char[strlen(c.cadena)+1]; strcpy(cadena, c.cadena); } else cadena = NULL; } return *this; } void main(void) { Cadena C1("Cadena de prueba"), C2; C2 = C1; C1.Mostrar(); C2.Mostrar(); }

P.O.O.

El operador de asignacin (=). Como es un operador binario que modifica el primer operando debe necesariamente ser definido como miembro. Este operador asigna un objeto cadena a otro. El miembro izquierdo de la igualdad es el primer operando y en este caso es el argumento implcito del operador. En la lista de argumentos formales que figura entre parntesis slo hay que incluir el segundo operando, que es un objeto cadena que se pasa por referencia y como const, pues no debe ser modificado. Adems esta funcin debe devolver una referencia al objeto resultado de la asignacin. Esto nos permitir escribir expresiones como estas: C1 = C2 = C3; En la definicin de este operador llama la atencin la sentencia if que aparece al comienzo de la funcin. Su razn de ser es evitar los efectos perjudiciales que puede tener una sentencia de asignacin de un objeto a s mismo (c1=c1;). Esta sentencia no es verdaderamente muy til, pero no hay razn para no prever las cosas de modo que sea inofensiva. Al asignarse un objeto a otro, lo primero que se hace es liberar la memoria ocupada por el primer operando (el que est a la izquierda), para despus sacar una copia de la memoria a la que apunta el segundo operando y asignar su direccin a la variable miembro del primero. El problema es que si ambos objetos coinciden (son el mismo objeto), al liberar la memoria del primer operando, el segundo (que es el mismo) la pierde tambin y ya no hay nada p copiar ni para asignar, llegndose en realidad a ara la destruccin del objeto. El remedio es chequear, que los dos operandos son realmente objetos distintos. Para ello, es necesario utilizar el puntero this. Sobrecarga de operadores relacionales La funcin sobregargada sera:
int operator== (const cadena& c1, const cadena& c2)

46

P.L.E.
{ if(strcmp(c1.cadena, c2.cadena)==0) return 1; return 0; } int operator!= (const cadena& c1, const cadena& c2) { int dif = strcmp(c1.cadena, c2.cadena); if(dif<0) return (-1); if(dif==0) return 0; else return 1; }

P.O.O.

Sobrecarga de operadores unitarios Los operadores unitarios, que son aquellos que slo requieren un operando, como la asignacin o el incremento. Cuando se sobrecargan operadores unitarios en una clase el operando es el propio objeto de la clase donde se define el operador. Por lo tanto los operadores unitarios dentro de las clases no requieren operandos (no es necesario pasarles argumentos).
<tipo> operator<operador unitario>();

Normalmente el <tipo> es la clase para la que estamos sobrecargando el operador. Los operadores de incremento (++) y decremento (--) son un caso especial en C++. Ambos son operadores unarios que siempre se definen como miembros de una clase. En realidad estos operadores tienen dos significados, segn se antepongan o se postpongan al nombre de la variable o del objeto al que se aplican. Esto crea una dificultad de definicin, porque a cul de los dos significados se refiere la siguiente declaracin?:
cadena& operator++ ();

En s no hay nada en esta declaracin que indique a cul de los dos significados de este operador se refiere el programador. Por eso C++ utiliza un convenio especial: la declaracin anterior se refiere al operador (++) antepuesto, mientras que la siguiente declaracin:
cadena& operator++ (int j);

se refiere al operador (++) postpuesto. La variable j tiene valor 0 y no se utiliza ms que como criterio de distincin. Lo mismo sucede con el operador (--). Ejemplo:
#include <iostream.h> class Tiempo { public: Tiempo(int h=0, int m=0) : hora(h), minuto(m) {} void Mostrar(); Tiempo operator+(Tiempo h); Tiempo operator++(); // Forma prefija Tiempo operator++(int); // Forma sufija

47

P.L.E.
private: int hora; int minuto; }; Tiempo Tiempo::operator+(Tiempo h) { Tiempo temp; temp.minuto = minuto + h.minuto; temp.hora = hora + h.hora; if(temp.minuto >= 60) { temp.minuto -= 60; temp.hora++; } return temp; } Tiempo Tiempo::operator++() { minuto++; while(minuto >= 60) { minuto -= 60; hora++; } return *this; } Tiempo Tiempo::operator++(int) { Tiempo temp(*this); // Constructor copia minuto++; while(minuto >= 60) { minuto -= 60; hora++; } return temp; } void Tiempo::Mostrar() { cout << hora << ":" << minuto << endl; } void main(void) { Tiempo T1(4,45); (T1++).Mostrar(); T1.Mostrar(); (++T1).Mostrar(); T1.Mostrar(); }

P.O.O.

48

P.L.E.

P.O.O.

RELACIN DE EJERCICIOS N 1:
1. Disear la clase Cliente que recoja las propiedades Nif, Nombre, Direccin Telfono y Saldo. Programar mtodos para inicializar y asignar valores a estas variables y para presentarlos por pantalla. Los miembros privados de la clase son: char char char char double cNif[10]; cNombre[51]; cDireccion[51]; cTelefono[10]; nSaldo;

Elaborar en la funcin main() un juego de datos de prueba para instanciar objetos y probar el comportamiento. 2. Modificar la clase Cliente aadiendole un segundo constructor sobrecargando el Primero que permita asignar valores a las propiedades en la propia instanciacion. Introducir un constructor cuyo contenido sea slo un mensaje de texto mostrando el nif del objeto destruido. 3. Suprimir el constructor por defecto, dejando nicamente el constructor con parmetros y aadirle a ste parmetros por defecto. Elaborar en la funcin main() un juego de datos de prueba para instanciar objetos y probar el comportamiento del nuevo constructor con parmetros por defecto. 4. Aadir a la clase Cliente un nuevo mtodo que permita visualizar los datos del cliente suministrando como parmetros el punto de la pantalla en la que comenzar a visualizar. Elaborar en la funcin main() un juego de datos de prueba para instanciar objetos y probar el comportamiento del nuevo mtodo. 5. Dado el siguiente programa, modificarlo introduciendo los siguientes elementos: Un constructor. Un destructor. Una funcin RecopilarDatos( ) Una funcin MostrarDatos( )
#include <iostream.h> class Paciente { public: int numeroHistClin; char *nombre; char *Direccion; char *Telefono; int edad; }; void main (void) { Paciente Registro1; Registro1.nombre = new char[50]; Registro1.Direccion = new char[50]; Registro1.Telefono = new char[50]; cout << "Escriba los datos del paciente" << endl; cout << "Nmero de la historia clinica:";

49

P.L.E. cin >> Registro1.numeroHistClin; cout << "Nombre del Paciente:"; cin >> Registro1.nombre; cout << "Direccion:"; cin >> Registro1.Direccion; cout << "Telefono:"; cin >> Registro1.Telefono; cout << "Edad:"; cin >> Registro1.edad; cout << endl; cout << "Historia Clnica No. " << Registro1.numeroHistClin << endl; cout << "Nombre: " << Registro1.nombre << endl; cout << "Direccion: " << Registro1.Direccion << endl; cout << "Telefono: " << Registro1.Telefono << endl; cout << "Edad: " << Registro1.edad << endl; delete Registro1.nombre; delete Registro1.Direccion; delete Registro1.Telefono; }

P.O.O.

6. Elaborar un programa que utilice una clase llamada Codificador, que contenga una variable privada cdigo y dos funciones pblicas. La primera funcin deber ser el constructor de la clase donde almacene la variable privada cdigo el nmero 328 y una segunda funcin denominada verCodigo(), la cual devuelva al programa principal el valor de la variable privada cdigo. 7. Elaborar un programa que utilice una clase denominada Alumno, la cual tenga como variables privadas un apuntador a una cadena de caracteres denominado nombre, y una variable entera llamada nota. La clase debe contener en su parte pblica tres funciones: la primera, la funcin Alumno(), la cual recibe como parmetro una cadena de caracteres y un entero con la nota, tendr como tarea almacenar en la memoria dinmica la cadena y el entero recibidos en el constructor. La cadena debe quedar indicada por el apuntador nombre y la nota por el entero nota. La segunda funcin denominada verNombre(), debe retornar la direccin e la memoria dinmica donde est almacenada la cadena indicada por el apuntador nombre. Y la tercera llamada verNota() debe devolver un entero que refleje el contenido de la nota. El objeto debe liberar memoria al terminar. 8. Elaborar una clase Coordenada, la cual contenga dos variables de tipo entero x y y. La clase debe contener dos funciones: la primera el constructor, el cual recibe dos parmetros i y j. El valor almacenado en i se debe almacenar en la variable privada x y el valor almacenado en la variable j se debe pasar al a variable privada y. La clase debe contener la funcin verCoordenada(), la cual debe informar al programa principal los valores almacenados en x y y. la funcin no puede retorna ningn dato. Con el propsito de elaborar su tarea, la funcin verCoordenada() debe trabajar parmetros por referencia. 9. Elaborar una clase denominada Codificador. La clase debe tener un su parte privada una variable dato de tipo entero, otro entero llamado codificado que debe comenzar en 0 y debe tener una funcin privada verDatoReal() la cual tiene como tarea retornar dato si codificado est en cero y el valor 1000 - dato si codificado es igual a 1. En la parte pblica deben existir cuatro funciones: el constructor, el cual requiere de un parmetro que inicializa la variable dato. La funcin asignarDato(), la cual recibe un parmetro de tipo entero, que almacena reemplazando el contenido de dato, tambin debe inicializar el codificado en 0. La tercera funcin se debe denominar procesar(), y tiene como tarea reemplazar a dato por 1000 - dato y a codificado por 1 - codificado. La cuarta funcin verDato() debe retornar el valor interno usando, utilizando la funcin definida en el bloque privado de la clase Codificador.

50

P.L.E.

P.O.O.

10. Elaborar una clase X, la cual tiene en su parte privada una variable x y en su parte pblica dos funciones. El constructor de la clase, el cual almacena en l variable x, el valor del a parmetro enviado al constructor. Tiene adicionalmente una funcin H(), la cual retorna el valor de la variable almacenada en la parte privada de la clase X. Con base en su clase anterior, disear una clase Y, que tiene en su parte privada una variable a de tipo X. La clase Y, el cual tiene como tarea inicializar la variable privada de la clase X y adicionalmente debe tener la funcin I(), la cual debe retornar el programa principal el valor de la variable almacenada en la parte privada de la clase X, a travs de la funcin H() declarada en la parte pblica de la clase X. 11. Elabore una clase Matriz, la cual almacene en su parte privada un apuntador a una matriz en la cual en cada celda se pueda almacenar un entero. El programa debe tener un constructor que asigne automticamente memoria dinmica para almacenar la matriz. El orden de la matriz se indica en los parmetros del constructor. El constructor tiene dos parmetros de tipo entero. El primer parmetro indicar el nmero de filas de la matriz y el segundo parmetro el nmero de columnas de la matriz que se quiere trabajar. Adicionalmente en su parte pblica debe existir una funcin denominada llenar(), la cual recibe un parmetro. Esta funcin debe inicializar la matriz manejada dentro de la clase con el valor del parmetro. Debe existir una tercera funcin, que corresponde al destructor de la clase Matriz. En este destructor se debe liberar toda la memoria asignada para la matriz. La clase debe tener una cuarta funcin la cual t ene como tarea retornar al programa principal la direccin de la i matriz. 12. Elaborar una clase Lado, que contenga en su parte privada un valor de tipo entero. Denominemos esta variable longitud. La funcin debe contener un constructor, el cual inicializa la variable longitud con el valor del parmetro enviado desde un programa principal o desde una funcin. La clase Lado, debe tener adicionalmente tener una funcin, que devuelva el valor de la variable longitud. Elaborar un programa principal que construya tres objetos de tipo Lado e inicialice su variable longitud. Despus de construidos estos tres objetos, deben en las rutinas que usan la clase Lado, existir las instrucciones necesarias con el propsito de mirar si con los valores almacenados en cada una de las variables longitud de los tres objetos, se puede formar un tringulo. En caso de que se pueda formar se debe escribir "si" de lo contrario se debe escribir la palabra "no".

13. Disear una clase que contenga un vector de 10 enteros conteniendo los factoria les de los
primeros 10 nmeros. Estos valores deben ser calculados y guardados en el constructor de la clase y luego para consulta no se deben calcular sino leer del vector interno. Si el usuario da un nmero mayor a 9 se le informar que no es posible hacer su operacin. Elaborar la clase y el programa que permita probar su funcionamiento. 14. Disear un programa que defina una clase denominada vector con dos variables privadas: un puntero a un vector de enteros y un entero conteniendo el nmero de elementos del vector. Definir al menos los siguientes mtodos: Constructor por defecto: crear un vector de 10 elementos. Constructor general: crear un vector con n elementos (n pasado como argumento). Constructor de copia: crear un vector copia de otro existente en la clase. Destructor: liberar la memoria asignada al puntero que apunta al vector de enteros. Funcin LeeElemento: devolver el elemento de una posicin del vector pasada como argumento. Funcin LeeNumEle: devuelve el valor de la variable privada nmero de elementos.

Declarar una funcin externa a la clase denominada EscribeVector que reciba como argumento por referencia un objeto de tipo vector. Esta funcin mostrar, mediante un bucle, los elementos del vector pasado como argumento, dependiendo del nmero de elementos del mismo. Crear objetos tipo vector para cada uno de los constructores.

51

P.L.E.

P.O.O.

15. Aadir al anterior programa un nuevo mtodo denominado CargaVector que solicite por pantalla mediante un bucle, los elementos que queremos que estn dentro del vector. Usaremos como tamao del vector la variable privada nmero de elementos. 16. Cree una clase llamada complex para ejecutar aritmtica cono nmeros complejos. Escriba un programa manejador para probar su clase. Los nmeros complejos tienen la forma( realPart + imaginaryPart*i) donde i es la raiz cuadrada de -1. Utilice variables de punto flotante para representar los datos privados de la clase. Proporcione una funcin constructor que permita que se inicialice un objeto de esta clase cuando sea declarado. El constructor deber tener valores por omisin, por si no se proporcionan inicializadores. proporcione funciones miembro pblicas para cada uno de los siguientes: o o Suma de dos nmeros Complex: las partes reales se suman juntas y las partes imaginarias se suman juntas tambin. Resta de dos nmeros Complex: La parte real del operando derecho se resta de la parte real del operando izquierdo y la parte imaginaria del operando derecho se resta de la parte imaginaria del operando izquierdo. Imprimir nmeros Complex en la forma (a, b) donde a sea la parte real y b la parte imaginaria.

17. Escriba un programa que utilice una plantilla de funcin llamada max, para determinar el mayor de tres argumentos. Pruebe el programa utilizando pares de enteros, caracteres y nmeros de punto flotante.

18. La concatenacin de cadenas requiere de dos operandos - las dos cadenas que han de concatenarse. En el texto mostramos como poner en prctica un operador de concatenacin homnimo , que concatena el segundo objeto String a la derecha del primer objeto String, modificando as el primer objeto String. En algunas aplicaciones, es deseable producir un objeto concatenado String sin modificar los dos argumentos String. Disee un operador+ para permitir operaciones como:

string1 = string2 + string3;

19. Disear un programa que defina la clase Lista declarada del siguiente modo: Class Lista{ private: int numele; int numact; int *plist; public: Lista(int, int); ~Lista(void); int LeeElemento(int); int Verifica(int); void Insertar(int); };

// numero entre 1 y n de elementos de la lista // tamao actual de la lista // puntero a una lista de n nmeros entre 1 y m // constructor de la lista de n nmeros // destructor // dado un ndice devuelve su valor en la lista // verifica si ya existe un nmero en la lista // inserta un elemento en la lista

52

P.L.E.

P.O.O.

El constructor de la lista de n nmeros recibe como argumentos dos enteros, n y m. Debe comprobar que n est entre 1 y m (ambos inclusives). Verifica devolvera un 1 si existe el nmero y 0 en caso contrario. Se crea una nueva funcin externa a la clase denominada NumeroAlAzar que recibir como argumento el tamao de la lista (m). Esta funcin devolver un nmero aleatorio entre 1 y m. La funcin insertar utilizar este nmero aleatorio para i tentar insertarlo en la lista, n siempre que la funcin Verificar lo permita.

Disear un programa basado en las siguientes declaraciones de estructura y clase: const int NUMMAX=50; struct Registro{ char nombre[25]; char tfno[10]; }; class ListaTfnos{ public: ListaTfnos(void); int InsReg(const Registro &); Registro *LeeReg(char *); private: Registro tabla[NUMMAX]; int numele; }; Constructor por defecto: pone la variable numele a 0. InsReg: inserta un nuevo elemento en la tabla. Devuelve 0 si la tabla ya est llena y 1 en caso contrario. LeeReg: esta funcin intenta encontrar en la tabla un nombre pasado como argumento. Si lo encuentra devuelve la direccin del dato y en caso contrario devuelve un puntero nulo. La funcin externa a la clase denominada Menu, debe mostrar las opciones siguiente: Alta, Consulta y Fin.

53

P.L.E.

P.O.O.

12. HERENCIA
Jerarqua, 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 aadiendo otras nuevas. La nueva clase obtenida se conoce como clase derivada, y las clase a partir de la que se deriva, clase base. Adems, cada clase derivada puede usarse como clase base para obtener una nueva clase derivada. Y cada clase derivada puede serlo de una o ms clases base. En este ltimo caso hablaremos de derivacin mltiple. Esto nos permite crear una jerarqua de clases tan compleja como sea necesario. En algunos casos una clase no tiene otra utilidad que la de ser clase base para otras clases que se deriven de ella. A este tipo de clases base, de las que no se declara ningn objeto, se les denomina clases base abstractas (ABC, Abstract Base Class) y su funcin es la de agrupar miembros comunes de otras clases que se derivarn de ellas. Por ejemplo, se puede definir la clase vehiculo para despus derivar de ella coche, bicicleta, patinete, etc., pero todos los objetos que se declaren pertenecern a alguna de estas ltimas clases; no habr vehculos que sean slo vehculos. Las caractersticas comunes de estas clases (como una variable que indique si est parado o en marcha, otra que indique su velocidad, la funcin de arrancar y la de frenar, etc.), pertenecern a la clase base y las que sean particulares de alguna de ellas pertenecern slo a la clase derivada (por ejemplo el nmero de platos y piones, que slo tiene sentido para una bicicleta, o la funcin embragar que slo se aplicar a los vehculos de motor con varias marchas). Bien, pero que ventajas tiene derivar clases?. En realidad, ese es el principio de la programacin orientada a objetos. Esta propiedad nos permite encapsular diferentes partes de cualquier objeto real o imaginario, y vincularlo con objetos ms elaborados del mismo tipo bsico, que heredarn todas sus caractersticas, adems de la posibilidad de reutilizar cdigo sin tener que escribirlo de nuevo. Esto es posible porque todas las clases derivadas pueden utilizar el cdigo de la clase base sin tener que volver a definirlo en cada una de ellas. Vemoslo con un ejemplo. Un ejemplo muy socorrido es el de las personas. Supongamos que nuestra clase base para clasificar a las personas en funcin de su profesin sea "Persona". Presta especial atencin a la palabra "clasificar", es el punto de partida para buscar la solucin de cualquier problema que se pretenda resolver usando POO. Lo primero que debemos hacer es buscar categoras, propiedades comunes y distintas que nos permitan clasificar los objetos, y crear lo que despus sern las clases de nuestro programa. Es muy importante dedicar el tiempo y atencin necesarios a esta tarea, de ello depender la flexibilidad, reutilizacin y eficacia de nuestro programa. Ten en cuenta que las jerarquas de clases se usan especialmente en la resolucin de problemas complejos, es difcil que tengas que recurrir a ellas para resolver problemas sencillos.

54

P.L.E.

P.O.O.

Siguiendo con el ejemplo, partiremos de la clase "Persona". Independientemente de la profesin, todas las personas tienen propiedades comunes, nombre, fecha de nacimiento, gnero, estado civil, etc. La siguiente clasificacin 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 adems 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 nmina 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 clasificacin, ahora de los empleados. Podemos clasificar a los empleados en ejecutivos y comerciales (y muchas ms 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". PERSONA

EMPLEADO

ESTUDIANTE

EJECUTIVO

COMERCIAL

Ahora veremos las ventajas de disponer de una jerarqua completa de clases. o Cada vez que creemos un objeto de cualquier tipo derivado, por ejemplo de tipo Comercial, estaremos creando en un slo 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, adems de sus propiedades como comercial, su nmina como empleado, y su nombre, edad y gnero como persona. o Siempre podremos crear nuevas clases para resolver nuevas situaciones. Consideremos el caso de que en nuestra clasificacin queramos incluir una nueva clase "Becario", que no es un empleado, ni tampoco un estudiante; la derivaramos de Persona. Tambin podemos considerar que un becario es ambas cosas, sera un ejemplo de derivacin mltiple, podramos hacer que la clase derivada Becario, lo fuera de Empleado y Estudiante. o Podemos aplicar procedimientos genricos a una clase en concreto, por ejemplo, podemos aplicar una subida general del salario a todos los empleados, independientemente de su profesin, si hemos diseado un procedimiento en la clase Empleado para ello. Veremos que existen ms ventajas, aunque este modo de disear aplicaciones tiene tambin sus inconvenientes, sobre todo si diseamos mal alguna clase.

55

P.L.E.

P.O.O.

Variables y funciones miembro protected Uno de los problemas que aparece con la herencia es el del control del acceso a los datos. Puede una funcin de una clase derivada acceder a los datos privados de su clase base? En principio una clase no puede acceder a los datos privados de otra, pero podra ser muy conveniente que una clase derivada accediera a todos los datos de su clase base. Para hacer posible esto, existe el tipo de dato protected. Este tipo de datos es privado para todas aquellas clases que no son derivadas, pero pblico para una clase derivada de la clase en la que se ha definido la variable como protected. Por otra parte, el proceso de herencia puede efectuarse de dos formas distintas: siendo la clase base public o private para la clase derivada. En el caso de que la clase base sea public para la clase derivada, sta hereda los miembros public y protected de la clase base como miembros public y protected, respectivamente. Por el contrario, si la clase base es private para la clase derivada, sta hereda todos los datos de la clase base como private. La siguiente tabla puede resumir lo explicado en los dos ltimos prrafos.
Especificador de acceso a la clase PUBLIC PROTECTED PRIVATE Accesible desde la propia clase Accesible desde la clase derivada Accesible desde el exterior

SI SI SI

SI SI NO

SI NO NO

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:
class Persona { // Clase Base 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; };

56

P.L.E. class Empleado : public Persona { // Clase derivada Empleado public: Empleado(char *n, int e, float s); float LeerSalario() const; void CambiarSalario(const float s); protected: float salarioAnual; };

P.O.O.

Podrs 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 adems, las posibles modificaciones de esos datos, en cuanto a tipo o tamao, slo requieren ajustes de los mtodos de la propia clase. Pero en el caso de estructuras jerrquicas 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. Constructores de clases derivadas Cuando se crea un objeto de una clase derivada, primero se invoca al constructor de la clase o clases base y a continuacin al constructor de la clase derivada. Si la clase base es a su vez una clase derivada, el proceso se repite recursivamente. Lgicamente, si no hemos definido los constructores de las clases, se usan los constructores por defecto que crea el compilador. Veamos un ejemplo:
#include <iostream.h> class ClaseA { public: ClaseA() : datoA(10) { cout << "Constructor de A" << endl; } int LeerA() const { return datoA; } protected: int datoA; }; class ClaseB : public ClaseA { public: ClaseB() : datoB(20) { cout << "Constructor de B" << endl; } int LeerB() const { return datoB; } protected: int datoB; }; void main(void) { ClaseB objeto; cout << "a = " << objeto.LeerA() << ", b = " << objeto.LeerB() << endl; }

Se ve claramente que primero se llama al constructor de la clase base A, y despus al de la clase derivada B.

57

P.L.E.

P.O.O.

Es relativamente fcil comprender esto cuando usamos constructores por defecto o cuando nuestros constructores no tienen parmetros, pero cuando sobrecargamos los constructores o usamos constructores con parmetros, no es tan simple. Veamos otro ejemplo: Se puede pensar en dos tipos de cuentas bancarias que comparten algunas caractersticas y que tambin tienen algunas diferencias. Ambas cuentas tienen un saldo, un inters y el nombre del titular de la cuenta. La cuenta joven es un tipo de cuenta que requiere la edad del propietario, mientras que la cuenta empresarial necesita el nombre de la empresa. El problema podra resolverse estableciendo una clase base llamada C_Cuenta y creando dos tipos de cuenta derivados de dicha clase base. El cdigo necesario para crear esas tres clases quedara de la siguiente forma:
#include <iostream.h> #include <s tring.h> class C_Cuenta { private: char *Nombre; // Nombre de la persona double Saldo; // Saldo Actual de la cuenta double Interes; // Inters aplicado public: C_Cuenta(const char *unNombre, double unSaldo=0.0, double unInteres=0.0) { Nombre = new char[strlen(unNombre)+1]; strcpy(Nombre, unNombre); SetSaldo(unSaldo); SetInteres(unInteres); } ~C_Cuenta() { delete [] Nombre; } inline char *GetNombre() { return Nombre; } inline double GetSaldo() { return Saldo; } inline double GetInteres() { return Interes; } inline void SetSaldo(double unSaldo) { Saldo = unSaldo; } inline void SetInteres(double unInteres) { Interes = unInteres; } inline void Ingreso(double unaCantidad) { SetSaldo( GetSaldo() + unaCantidad ); } friend ostream& operator<<(ostream& os , C_Cuenta& unaCuenta) { os << "Nombre=" << unaCuenta.GetNombre() << endl; os << "Saldo=" << unaCuenta.GetSaldo() << endl; return os; } }; class C_CuentaJoven : public C_Cuenta { private: int Edad; public: C_CuentaJoven(const char *unNombre, int laEdad, double unSaldo=0.0, double unInteres=0.0): C_Cuenta(unNombre, unSaldo, unInteres) // se llama al constructor de la clase base en la lnea previa. { Edad = laEdad; } }; class C_CuentaEmpresarial : public C_Cuenta { private: char *NomEmpresa; public: C_CuentaEmpresarial(const char *unNombre, const char *laEmpresa, double unSaldo=0.0, double unInteres=0.0) : C_Cuenta(unNombre, unSaldo, unInteres) // se llama al constructor de la clase base en la lnea previa. { NomEmpresa = new char[strlen(laEm presa)+1]; strcpy(NomEmpresa, laEmpresa); }

58

P.L.E.

P.O.O.
// Cuando una variable de este tipo se destruye se llamar primero el destructor de // C_CuentaEmpresarial y posteriormente se llama automticamente el destructor de la // clase base. ~C_CuentaEmpresarial() { delete [] NomEmpresa; }

}; void main(void) { C_CuentaJoven c1("Igor", 18, 10000.0, 1.0); C_CuentaEmpresarial c2("Juan", "MicroComputers Corp." ,10000000.0); // Ambas cuentas pueden llamar mtodos definidos previamente cout << c1; cout << c2; }

Si un miembro heredado se redefine en la clase derivada, el nombre redefinido oculta el nombre heredado que ya queda invisible para los objetos de la clase derivada. Hay algunos elementos de la clase base que no pueden ser heredados: Constructores Destructores Funciones friend Funciones y datos estticos de la clase Operador de asignacin (=) sobrecargado Inicializacin de clases base en constructores Cuando queramos inicializar las clases base usando parmetros desde el constructor de una clase derivada lo haremos de modo anlogo a como lo hacemos con los datos miembro, usaremos el constructor de la clase base con los parmetros adecuados. Las llamadas a los constructores deben escribirse antes de las inicializaciones de los parmetros. Sintaxis:
<clase_derivada>(<lista_de_parmetros>) : <clase_base>(<lista_de_parmetros>) {}

De nuevo lo veremos mejor con otro ejemplo:


#include <iostream.h> class ClaseA { public: ClaseA(int a) : datoA(a) { cout << "Constructor de A" << endl;} int LeerA() const { return datoA; } protected: int datoA; }; class ClaseB : public ClaseA { public: ClaseB(int a, int b) : ClaseA(a), datoB(b) (1) { cout << "Constructor de B" << endl; } int LeerB() const { return datoB; } protected: int datoB; };

59

P.L.E.

P.O.O.

void main(void) { ClaseB objeto(5,15); cout << "a = " << objeto.LeerA() << ", b = " << objeto.LeerB() << endl; }

Observa cmo hemos definido el constructor de la ClaseB (1). Recibe dos parmetros: "a" y "b". El primero se usar para inicializar la clase base ClaseA, para ello, despus de los dos puntos, escribimos el constructor de la ClaseA, y usamos como parmetro el valor "a". A continuacin escribimos la inicializacin del datoB, separado con una coma y usamos el valor "b". El inicializador base puede ser omitido en el caso de que la clase base tenga un constructor por defecto. Veamos otro ejemplo: En el ejemplo siguiente agregamos constructores por defecto a cada clase para estudiar como se usan cuando utilizamos la herencia.
#include <iostream.h> class vehiculo { protected: int llantas; double peso; public: vehiculo(void) {llantas = 7; peso = 11111.0;} void inicializa(int in_llantas, double in_peso); int obtiene_llantas(void) {return llantas;} double obtiene_peso(vo id) {return peso;} double carga_llanta(void) {return peso/llantas;} }; class carro:public vehiculo { int carga_pasajero; public: carro(void) {carga_pasajero = 4;} void inicializa(int in_llantas, double in_peso, int personas=4); int pasajeros(void) {return carga_pasajero;} }; class camion:public vehiculo { int carga_pasajero; double pasaje; public: camion(void) {carga_pasajero = 3; pasaje = 22222.0;} void inicia_camion(int cuantos=2, double max_carga = 24000.0); double eficiencia(void); int pasajeros(void) {return carga_pasajero;} }; // inicializa a cualquier dato deseado void vehiculo::inicializa(int in_llantas, double in_peso) { llantas = in_llantas; peso = in_peso; } void carro::inicializa(int in_llantas, double in_peso, int personas) { carga_pasajero = personas; llantas = in_llantas; peso = in_peso; }

60

P.L.E.
void camion::inicia_camion(int cuantos, double max_carga) { carga_pasajero = cuantos; pasaje = max_carga; } double camion::eficiencia(void) { return pasaje / (pasaje + peso); } void main(void) { vehiculo monociclo; cout << "El monociclo tiene " << monociclo.obtiene_llantas() << " llanta.\n"; cout << "La llanta del monociclo carga " << monociclo.carga_llanta() << " libras \n"; cout << "El monociclo pesa " << monociclo.obtiene_peso() << " libras.\n\n"; carro sedan; cout << "El sedan transporta " << sedan.pasajeros() << " pasajeros.\n"; cout << "El sedan pesa " << sedan.obtiene_peso() << " libras.\n"; cout << "La capacidad del sedan es " << sedan.carga_llanta() << " libras por llanta.\n\n"; camion semi; cout << "El semi pesa " << semi.obtiene_peso() << " libras.\n"; cout << "La eficiencia del semi es " << 100.0 * semi.eficiencia() << " por ciento.\n"; }

P.O.O.

Cuando creamos un objeto de la clase base vehiculo no tenemos problema alguno ya que no existe el factor de la herencia, el constructor para la clase base opera exactamente de la misma manera que los constructores que ya hemos estudiado. Observe que creamos el objeto monociclo utilizando el constructor por defecto y el objeto es inicializado a los valores contenidos en el constructor. Cuando definimos un objeto de una de las clases derivadas, el objeto sedan, existe una pequea diferencia porque no solo necesitamos llamar al constructor para la clase derivada, tenemos que preocuparnos adems de cmo inicializamos la clase base. Actualmente no hay problema porque el compilador llamar automticamente el constructor por defecto para la clase base a menos que la clase derivada explicitamente llame a otro constructor para la clase base. El constructor de la clase base ser llamado antes de la clase derivada, sto tiene sentido porque garantiza que el objeto de la clase base est propiamente construido cuando el constructor para la clase derivada es ejecutado, esto permite utilizar algunos de los datos de la clase base durante la construccin de la clase derivada. En ste caso, la parte vehiculo del objeto sedan es construido en primer lugar, en seguida se construyen la parte local del objeto sedan, de esta manera todas las variables miembro son apropiadamente inicializadas. Las dos clases derivadas, carro y camion cada una tiene una variable llamada carga_pasajero lo que es perfectamente legal. La clase carro tiene un mtodo del mismo nombre, inicializa() al declarado en la clase base llamada vehiculo. Como tenemos un mtodo llamado inicializa() declarado en la clase derivada carro ste oculta el mtodo del mismo nombre el cual es parte de la clase base. Hay ocasiones en las que deseamos enviar un mensaje al mtodo en la clase base para utilizarlo en el objeto de la clase derivada. Esto se logra utilizando el operador de alcance de la siguiente manera:
sedan.vehiculo::inicializa(4, 3500.0);

61

P.L.E.

P.O.O.

Cmo se ejecutan los destructores? Cuando no necesitamos los objetos se deben ejecutar los respectivos destructores y como no hemos definido ninguno, sern ejecutados los destructores por defecto. Una vez ms, la destruccin del objeto de la clase base llamado monociclo no tiene problema pero el objeto sedan debe ejecutar dos destructores para eliminar cada una de sus partes, no es sorpresa que los destructores para ste objeto se ejecuten en orden inverso a los constructores. Inicializacin de objetos miembros de clases Tambin es posible que una clase tenga como miembros objetos de otras clases, en ese caso, para inicializar esos miembros se aade el nombre del objeto junto con sus parmetros a la lista de inicializaciones del constructor. Esto es vlido tanto en clases base como en clases derivadas. Veamos un ejemplo:
#include <iostream.h> class ClaseA { public: ClaseA(int a) : datoA(a) { cout << "Constructor de A" << endl;} int LeerA() const { return datoA; } protected: int datoA; }; class ClaseB { public: ClaseB(int a, int b) : cA(a), datoB(b) (1) { cout << "Constructor de B" << endl;} int LeerB() const { return datoB; } int LeerA() const { return cA.LeerA(); } (2) protected: int datoB; ClaseA cA; }; void main(void) { ClaseB objeto(5,15); cout << "a = " << objeto.LeerA() << ", b = " << objeto.LeerB() << endl; }

En la lnea (1) se ve cmo inicializamos el objeto de la ClaseA (cA), que hemos incluido como variables protegidas de la ClaseB. En la lnea (2) vemos que hemos tenido que aadir una nueva funcin para que sea posible acceder a los datos del objeto cA. Si hubiramos declarado cA como public, este paso no habra sido necesario. Destructores de clases derivadas Cuando se destruye un objeto de una clase derivada, primero se invoca al destructor de la clase derivada, si existen objetos miembro a continuacin se invoca a sus destructores y finalmente al destructor de la clase o clases base. Si la clase base es a su vez una clase derivada, el proceso se repite recursivamente. Al igual que pasaba con los constructores, si no hemos definido los destructores de las clases, se usan los destructores por defecto que crea el compilador. Veamos un ejemplo:

62

P.L.E.

P.O.O.

#include <iostream.h> class ClaseA { public: ClaseA() : datoA(10) { cout << "Constructor de A" << endl; } ~ClaseA() { cout << "Destructor de A" << endl; } int LeerA() const { return datoA; } protected: int datoA; }; class ClaseB : public ClaseA { public: ClaseB() : datoB(20) { cout << "Constructor de B" << endl; } ~ClaseB() { cout << "Destructor de B" << endl; } int LeerB() const { return datoB; } protected: int datoB; }; void main(void) { ClaseB objeto; cout << "a = " << objeto.LeerA() << ", b = " << objeto.LeerB() << endl; }

Se ve que primero se llama al destructor de la clase derivada B, y despus al de la clase base A. Ejemplo: Veamos el caso que bien podra darse para un sistema de generacin de personajes de un juego de rol. Imaginemos que queremos hacer un sistema para la creacin de multitud de personajes. Estos personajes podrn ser slo humanos. Al ser humanos slo existirn dos sexos, hombre y mujer. Por otro lado, existen dos tipos de personajes: los Guerreros y los Magos. Cmo podramos montarnos un sistema jerrquico con todo estos datos?. Est claro que todos los personajes son humanos por lo tanto sera una buena idea partir de una clase llamada HUMANO que contuviera todos los atributos que tiene cualquier ser humano. Al estar creando un sistema de generacin de personajes humanos, tenemos dos sexos independientes el del hombre y el de la mujer. Las diferencias son obvias pero no por ello hace falta crear dos clases distintas llamadas HOMBRE y MUJER pues no nos hacen falta tomar atributos exclusivos de los sexos para trabajar en la generacin de personajes. Basta con poner una variable en la clase HUMANO llamada sexo y dar el valor oportuno (por ejemplo si sexo se inicia con V significa que es un hombre y si se inicia con M significa que es una mujer. Internamente trabajaremos con 0 y 1). Lo que seguro necesita de clases nuevas es el tema de las dos especialidades de los personajes que podamos crear. Est claro que tanto los guerreros como los magos son humanos. Vamos a preguntarnos a nosotros mismos para ver si nos aclaramos las diferencias que pueden existir en cada uno de los personajes.

63

P.L.E.

P.O.O.

Un guerrero tiene fuerza, inteligencia, destreza, energa, peso, sexo, edad y nombre?. S, y adems esto tambin lo tiene cualquier mago, por lo tanto estas caractersticas ya han debido de ser incluidas en la clase HUMANO. Un guerrero utiliza un libro de hechizos para luchar?. No, un guerrero slo puede utilizar armas. Pero un mago s que puede utilizar libros de hechizos para luchar. Del mismo modo, un mago no puede utilizar armas para pelear. Un mago puede vestir armaduras?. No, los magos slo pueden llevar tnicas de tres colores: negro, rojo o blanco (te suena no?). Adems, en el caso de los guerreros, esto s pueden vestir armaduras pero no tnicas y las armaduras pueden ser de cuero o simplemente nada (el tpico Conan...). Podramos seguir hasta desmenuzar totalmente a cada tipo de personaje pero, con estos datos, ya podemos saber que necesitamos dos clases que heredan de la clase base HUMANO. Estas clases sern la de GUERRERO y la de MAGO. La clase GUERRERO poseer una variable llamada armas cuyo rango de valores podr ir desde espadas a "simples" puos mientras que el MAGO poseer una variable llamada libro de hechizos que trabajar con todos los distintos libros de hechizos que nosotros diseemos. As mismo, la clase GUERRERO dispondr de una variable llamada armadura que tomara los valores de cuero o nada, indicando que no tiene armadura, y la clase MAGO dispondr tambin de una variable similar llamada tnica y su rango ser el de tnica roja, negra o blanca. Debemos acostumbrarnos a trazar esquemas similares antes de comenzar a codificar. Es muy importante sentar las bases de un diseo, si este va a tener cierta envergadura. El esquema grfico podra ser el siguiente:

#include <iostream.h> // Clase Base class Humano { public: // Constructor y destructor Humano() {;} ~Humano() {;} private: char *m_nombre; int m_edad; int m_peso; int m_sexo; int m_inteligencia; int m_fuerza; int m_destreza; int m_energia;

64

P.L.E.
public: void PonNombre(char *nombre) { m_nombre = nombre; } void PonEdad(int edad) { m_edad = edad; } void PonSexo(char sexo) { if (sexo=='V') m_sexo =0; else m_sexo = 1; } void PonInteligencia(int intel) { m_inteligencia = intel; } void PonFuerza(int fuerza) { m_fuerza = fuerza; } void PonDestreza(float destreza) { m_destreza = destreza; } void PonEnergia (int energia) { m_energia = energia; } virtual void PonInformacion(void); }; // Primera clase derivada class Guerrero : public Humano { public: // Constructor y destructor Guerrero() {} ~Guerrero() {} private: int m_tipoArma; // 0 manos 1 espada int m_tipoArmadura; // 0 nada 1 cuero public: void PonArma(int arma) { m_tipoArma = arma; } void PonArmadura(int armadura) {m_tipoArmadura = armadura; } void PonInformacion(void); }; //Segunda clase derivada class Mago: public Humano { public: // Constructor y destructor Mago() {} ~Mago() {} private: int m_tipoLibroHechizos; // 0 ataque 1 curacion int m_tipoTunica; // 0 blanca 1 roja 2 negra public: void PonLibroHechizos(int libro) { m_tipoLibroHechizos = libro; } void PonTunica(int tunica) { m_tipoTunica = tunica; } void PonInformacion(void); }; // Ahora implementamos las funciones que no son inline // Mtodos de la clase Humano void Humano::PonInformacion(void) { cout << "\n Nombre: " << m_nombre; cout << "\n Edad: " << m_edad; switch(m_sexo) { case 0: cout << "\n Sexo: Hombre"; break; case 1: cout << "\n Sexo: Mujer"; break; }; cout << "\n Inteligencia: " << m_inteligencia; cout << "\n Fuerza: " << m_fuerza; cout << "\n Destreza: " << m_destreza; cout << "\n Energa: " << m_energia; }

P.O.O.

65

P.L.E.
// Mtodos de la clase Guerrero void Guerrero::PonInformacion(void) { Humano::PonInformacion(); switch(m_tipoArma) { case 0: cout << "n Arma: Puos"; break; case 1: cout << "\n Arma: Espada"; break; }; switch(m_tipoArmadura) { case 0: cout << "\n Armadura: Ninguna"; break; case 1: cout << "\n Armadura: Cuero"; break; }; } // Mtodos de la clase Mago void Mago::PonInformacion(void) { Humano::PonInformacion(); switch(m_tipoLibroHechizos) { case 0: cout << "\n Libro: Hechizos de Ataque"; break; case 1: cout << "\n Libro: Hechizos de Curacin"; break; }; switch(m_tipoTunica) { case 0: cout << "\n Tnica: Blanca"; break; case 1: cout << "\n Tnica: Roja"; break; case 2: cout << "\n Tnica: Negra"; break; }; } void main(void) { // Declaramos dos objetos. Uno de tipo Mago y otro de tipo Guerrero Guerrero Campeador; Mago Merlin; // Ponemos los datos de cada uno // Datos del guerrero Campeador.PonNombre("Campeador"); Campeador.PonEdad(26); Campeador.PonSexo('V'); Campeador.PonInteligencia(7); Campeador.PonFuerza(8); Campeador.PonDestreza(8); Campeador.PonEnergia(100); Campeador.PonArma(1); Campeador.PonArmadura(0); // Datos del Mago Merlin.PonNombre("Merlin"); Merlin.PonEdad(26); Campeador.PonSexo('V'); Merlin.PonInteligencia(9); Merlin.PonFuerza(5); Merlin.PonDestreza(6); Merlin.PonEnergia(60); Merlin.PonLibroHechizos(0); Merlin.PonTunica(1); // Ahora Ponemos los datos del Guerrero por pantalla Campeador.PonInformacion(); // Ahora Ponemos los datos del Mago por pantalla Merlin.PonInformacion(); }

P.O.O.

66

P.L.E.

P.O.O.

Como podis observar, utilizamos la clase base HUMANO y, a partir de ella, derivamos las clases GUERRERO y MAGO. Esto da mucha potencia ya que tenemos todo lo bsico en la case base y creamos clases especficas que se especializan, caso de las citadas GUERRERO y MAGO, ofreciendo esos datos u acciones concretas. Sera muy fcil crear una clase arquero, ladrn, bardo.... tan slo hay que derivar de la clase base y aadir esas caractersticas nicas. La clase derivada GUERRERO, hereda todas las funciones o mtodos y todas las variables o atributos de la clase base HUMANO. Adems, aade dos funciones nuevas, dos variables nuevas y redefine una funcin de la clase base, la funcin PonInformacin(). Por su parte, la clase MAGO, viene a hacer algo similar. Hereda todas las funciones y variables pblicas de la clase base HUMANO, al igual que la clase GUERRERO y, adems, aade dos variables, dos funciones y redefine la, ya citada, funcin PonInformacion() de la clase base. Cmo declaramos, exactamente, una clase derivada? Vamos a mirar la cabecera de la clase GUERRERO que nos vale para ilustrar el proceso. Recordemos cmo era:
// Primera clase derivada class Guerrero : public Humano {

Si nos fijamos, despus de poner class Guerrero, ponemos dos puntos "" y, despus, : public Humano. Lo que debemos de hacer es declarar primero el nombre de la clase que, como todos sabemos, es Guerrero, despus los dos puntos ":" y, por ltimo, lo ms importante, debemos de poner el nombre de la clase de la que vamos a derivar o heredar anteponiendo el formato public o private. Estos son los dos nicos especificadores de acceso que podemos poner a la hora de declarar una clase derivada. Con public, heredamos todos los elementos, tanto variables como funciones. Si pusiramos private como especificador de acceso, lo nico que conseguiramos sera hacer oculta una clase derivada del resto del programa pero, como esto no se suele utilizar (nosotros, en principio, no lo vamos a hacer), no se suele indicar, es decir, se suele declarar todo como public a la hora de derivar. Durante la utilizacin de los objetos Guerrero y Mago, utilizamos, mayormente, los mtodos de la clase base, esto es, durante el establecimiento del nombre, edad, sexo, inteligencia, fuerza, destreza o energa, lo que se hace es "echar mano" de los mtodos de la clase Humano y que son heredados. Sin embargo, mtodos como PonArma() o PonTunica(), por poner algn ejemplo, ya pertenecen exclusivamente a los objetos derivados, es decir, estos mtodos no se heredan de ninguna clase. Otro aspecto muy importante es el de la redefinicin de funciones. Cuando redefinimos funciones lo que venimos a hacer es coger una funcin ya hecha, que pertenece a una clase base, y aadirla una serie de caractersticas necesarias para el objeto derivado. Estas caractersticas pueden y suelen, ampliarlas. El ejemplo ms claro es el del mtodo PonInformacion() mtodo que ser el que estudiemos a continuacin.

67

P.L.E.

P.O.O.

Redefiniendo funciones Retomando el prrafo anterior, el tema de la redefinicin es una valiosa "arma" para poder concretar todo lo que heredamos an ms. As, en nuestros ejemplos, tanto la clase Guerrero como la clase Mago, hacen uso de la redefinicin. Vamos a recordar cmo era la funcin PonInformacion() en la clase base:
void Humano::PonInformacion(void) { cout << "\n Nombre: " << m_nombre; cout << "\n Edad: " << m_edad; switch(m_sexo) { case 0: cout << "\n Sexo: Hombre"; break; case 1: cout << "\n Sexo: Mujer"; break; }; cout << "\n Inteligencia: " << m_inteligencia; cout << "\n Fuerza: " << m_fuerza; cout << "\n Destreza: " << m_destreza; cout << "\n Energa: " << m_energia; }

Se puede observar perfectamente que lo que hace este mtodo es volcar la informacin concerniente a las variables que contienen (o contendrn) los valores acerca del nombre, edad, sexo, inteligencia, fuerza, destreza y energa del personaje. La pregunta viene ahora. Si un Guerrero o Mago, tiene estos datos, que los hereda y, adems, tiene otros que tambin deber listar, Tendremos que volver a escribir todo otra vez?. La respuesta es NO. Tan slo hace falta mirar a la funcin PonInformacion() de la clase Guerrero o Mago. Echemos un vistazo a la clase Mago y, en concreto, a dicha funcin:
void Mago::PonInformacion(void) { Humano::PonInformacion(); switch(m_tipoLibroHechizo) { case 0: cout << "\n Libro: Hechizos de Ataque"; break; case 1: cout << "\n Libro: Hechizos de Curacin"; break; }; switch(m_tipoTunica) { case 0: cout << "\n Tnica: Blanca"; break; case 1: cout << "\n Tnica: Roja"; break; case 2: cout << "\n Tnica: Negra"; break; }; }

Est ms o menos claro, no?. Lo que hacemos es llamar la funcin definida en la clase base y que se encarga de listar los valores bsicos anteriormente citados. Para ello, ponemos el nombre de la clase base, seguida de los dos puntos de resolucin de mbito y, despus, el nombre de la funcin, es decir, Humano::PonInformacion(); . Una vez hecho esto, definimos la parte de la funcin que nos hace falta y que no es otra que la concerniente al listado de los valores nicos de un Mago, esto es, tipo de libro de hechizos y tipo de tnica, es decir, completamos funcin PonInformacion() para que, adems de listar los datos bsicos sin necesidad de reescribir las instrucciones (llamamos a la funcin definida en la clase base), liste tambin los datos especficos. Esto mismo es aplicable a la clase Guerrero.

68

P.L.E.

P.O.O.

Herencia Simple y Herencia Mltiple Una clase puede heredar variables y funciones miembro de una o ms clases base. En el caso de que herede los miembros de una nica clase se habla de herencia simple y en el caso de que herede miembros de varias clases base se trata de un caso de herencia mltiple. Veamos algunos ejemplos para ilustrar este tipo de herencia:
#include <iostream.h> #include <stdlib.h> class ClaseA { public: ClaseA() : valorA(10) {} int LeerValor() const {return valorA;} protected: int valorA; }; class ClaseB { public: ClaseB() : valorB(20) {} int LeerValor() const {return valorB;} protected: int valorB; }; class ClaseC : public ClaseA, public ClaseB {}; void main(void) { ClaseC CC; // cout << CC.LeerValor() << endl; // Produce un error de compilacin ya que hay ambigedad. cout << CC.ClaseA::LeerValor() << endl; }

Si sustituimos en el ejemplo anterior lo siguiente, podremos llamar al mtodo p blico de la ClaseC LeeValor() sin ningn problema ya est definido para esa clase.
class ClaseC : public ClaseA, public ClaseB { public: int LeerValor() const {return ClaseA::LeerValor();} }; void main(void) { ClaseC CC; cout << CC.LeerValor() << endl; }

Un ejemplo ms completo que los anteriores sera incorporando nuevos constructores:


#include <iostream.h> #include <stdlib.h> class ClaseA { public: ClaseA() : valorA(10) {} ClaseA(int va) : valorA(va) {} int LeerValor() const {return valorA;} protected: int valorA; }; class ClaseB { public: ClaseB() : valorB(20) {} ClaseB(int vb) : valorB(vb) {} int LeerValor() const {return valorB;} protected: int valorB; };

69

P.L.E.
class ClaseC : public ClaseA, public ClaseB { public: ClaseC(int va, int vb) : ClaseA(va), ClaseB(vb) {}; int LeerValorA() const {return ClaseA::LeerValor();} int LeerValorB() const {return ClaseB::LeerValor();} }; void main(void) { ClaseC CC(12,14); cout << CC.LeerValorA() << "," << CC.LeerValorB() << endl; }

P.O.O.

Herencia Virtual Supongamos que tenemos una estructura de clases como sta:
CLASE A CLASE A

CLASE B

CLASE C

CLASE D

La ClaseD heredar dos veces los datos y funciones de la ClaseA, con la consiguiente ambigedad a la hora de acceder a datos o funciones heredadas de ClaseA. Para solucionar esto se usan las clases virtuales. Cuando derivemos una clase partiendo de una o varias clases base, podemos hacer que las clases base sean virtuales. Esto no afectar a la clase derivada. Por ejemplo:
class ClaseB : virtual public Clas eA {};

Desde el punto de vista de la ClaseB, no hay ninguna diferencia entre sta declaracin y la que hemos usado hasta ahora. La diferencia estar cuando declaramos la ClaseD. Veamos el ejemplo completo:
class ClaseB : virtual public ClaseA {}; class C laseC : virtual public ClaseA {}; class ClaseD : public ClaseB, public ClaseC {};

Ahora, la ClaseD slo heredar una vez la ClaseA. La estructura quedar as:
CLASE A

CLASE B

CLASE C

CLASE D

70

P.L.E.

P.O.O.

Cuando creemos una estructura de este tipo, deberemos tener cuidado con los constructores, el constructor de la ClaseA deber ser invocado desde el de la ClaseD, ya que ni la ClaseB ni la ClaseC lo harn automticamente. Veamos esto con el ejemplo de la clase "Persona". Derivaremos las clases "Empleado" y "Estudiante", y crearemos una nueva clase "Becario" derivada de estas dos ltimas. Adems haremos que la clase "Persona" sea virtual, de modo que no se dupliquen sus funciones y datos.
#include <iostream.h> #include <cstring.h> class Persona { public: Persona(char *n) { strcpy(nombre, n); } const char *LeeNombre() const { return nombre; } protected: char nombre[30]; }; class Empleado : virtual public Persona { public: Empleado(char *n, int s) : Persona(n), salario(s) {} int LeeSalario() const { return salario; } void ModificaSalario(int s) { salario = s; } protected: int salario; }; class Estudiante : virtual public Persona { public: Estudiante(char *n, float no) : Persona(n), nota(no) {} float LeeNota() const { return nota; } void ModificaNota(float no) { nota = no; } protected: float nota; }; class Becario : public Empleado, public Estudiante { public: Becario(char *n, int s, float no) : Empleado(n, s), Estudiante(n, no), Persona(n) {} (1) }; void main(void) { Becario Fulanito("Fulano", 1000, 7); cout << Fulanito.LeeNombre() << "," << Fulanito.LeeSalario() << "," << Fulanito.LeeNota() << endl; cin.get(); }

Si observamos el constructor de "Becario" en (1), veremos que es necesario usar el constructor de "Persona", a pesar de que el nombre se pasa como parmetro tanto al "Empleado" como a "Estudiante". Si no se incluye el constructor de "Persona", el compilador genera un error.

71

P.L.E.

P.O.O.

13. FUNCIONES VIRTUALES. POLIMORFISMO


Redefinicin de funciones en clases derivadas En una clase derivada se puede definir una funcin que ya exista en la clase base, esto se conoce como "overriding", o superposicin de una funcin. La definicin de la funcin en la clase derivada oculta la definicin previa en la clase base. En caso necesario, es posible acceder a la funcin oculta de la clase base mediante su nombre completo:
<objeto>.<clase_base>::<mtodo>;

Veamos un ejemplo:
#include <iostream.h> class ClaseA { public: ClaseA() : datoA(10) {} int LeerA() const { return datoA; } void Mostrar() { cout << "a = " << datoA << endl; (1) } protected: int datoA; }; class ClaseB : public ClaseA { public: ClaseB() : datoB(20) {} int LeerB() const { return datoB; } void Mostrar() { cout << "a = " << datoA << ", b = " << datoB << endl; (2) } protected: int datoB; }; void main(void) { ClaseB objeto; objeto.Mostrar(); objeto.ClaseA::Mostrar(); cin.get(); }

La definicin de la funcin "Mostrar" en la ClaseB (1) oculta la definicin previa de la funcin en la ClaseA (2). Superposicin y sobrecarga Cuando se superpone una funcin, se ocultan todas las funciones con el mismo nombre en la clase base. Supongamos que hemos sobrecargado la funcin de la clase base que despus volveremos a definir en la clase derivada.
#include <iostream.h> class ClaseA { public: void Incrementar() { cout << "Suma 1" << endl; } void Incrementar(int n) { cout << "Suma " << n << endl; } }; class ClaseB : public ClaseA { public: void Incrementar() { cout << "Suma 2" << endl; } };

72

P.L.E.
void main(void) { ClaseB objeto; objeto.Incrementar(); // objeto.Incrementar(10); objeto.ClaseA::Incrementar(); objeto.ClaseA::Incrementar(10); cin.get(); }

P.O.O.

Ahora bien, no es posible acceder a ninguna de las funciones superpuestas de la clase base, aunque tengan distintos valores de retorno o distinto nmero o tipo de parmetros. Todas las funciones "incrementar" de la clase base han quedado ocultas, y slo son accesibles mediante el nombre completo. Polimorfismo Se entiende por polimorfismo la capacidad de llamar a funciones distintas con un mismo nombre. Estas funciones pueden actuar sobre objetos distintos dentro de una jerarqua de clases, sin tener que especificar el tipo exacto de los objetos. Para entenderlo, veamos la siguiente figura: CLASE ABUELA Funcin1( ) CLASE MADRE Funcin1( ) CLASE HIJA Funcin1( ) En esta figura se observa una j rarqua de clases. En todos los niveles de esta jerarqua e est contenida una funcin llamada Funcion_1(). Esta funcin no tiene por qu ser igual en todas las clases, aunque es habitual que sea una funcin que efecte una operacin muy parecida sobre distintos tipos de objetos. Es importante comprender que el compilador no decide en tiempo de compilacin cul ser la funcin que se debe utilizar en un momento dado del programa. Esa decisin se toma en tiempo de ejecucin. A este proceso de decisin en tiempo de ejecucin se le denomina vinculacin dinmica o tarda, en oposicin a la habitual vinculacin esttica o temprana, consistente en decidir en tiempo de compilacin qu funcin se aplica en cada caso. A este tipo de funciones, incluidas en varios niveles de una jerarqua de clases con el mismo nombre pero con distinta definicin, se les denomina funciones virtuales. Las funciones virtuales son menos eficientes que las funciones normales. Cada clase que utiliza funciones virtuales tiene un vector de punteros, uno por cada funcin virtual, llamado v-table. Cada uno de los punteros contenidos en ese vector apunta a la funcin virtual apropiada para esa clase, que ser, habitualmente, la funcin virtual definida en la propia clase. En el caso de que en esa clase no est definida la funcin

73

P.L.E.

P.O.O.

virtual en cuestin, el puntero de v-table apuntar a la funcin virtual de su clase base ms prxima en la jerarqua, que tenga una definicin propia de la funcin virtual. Esto quiere decir que buscar primero en la propia clase, luego en la clase anterior en el orden jerrquico y se ir subiendo en ese orden hasta dar con una clase que tenga definida la funcin buscada. Es este trabajo extra el que hace que las funciones virtuales sean menos eficientes que las funciones normales. La idea central del polimorfismo es la de poder llamar a funciones distintas aunque tengan el mismo nombre, segn la clase a la que pertenece el objeto al que se aplican. Esto es imposible utilizando nombres de objetos ya que siempre se aplica la funcin miembro de la clase correspondiente al nombre del objeto, y esto se decide en tiempo de compilacin. Sin embargo, utilizando punteros puede conseguirse el objetivo buscado. Un puntero a la clase base puede contener direcciones de objetos de cualquiera de las clases derivadas. En principio, el tipo de puntero determina tambin la funcin que es llamada, pero si se utilizan funciones virtuales es el tipo de objeto el que apunta el puntero lo que determina la funcin que se llama. Esta es la esencia del polimorfismo. Volvamos al ejemplo inicial, el de la estructura de clases basado en la clase "Persona" y supongamos que tenemos la clase base "Persona" y dos clases derivadas: "Empleado" y "Estudiante".
#include <iostream.h> #include <cstring.h> class Persona { public: Persona(char *n) { strcpy(nombre, n); } void VerNombre() { cout << nombre << endl; } protected: char nombre[30]; }; class Empleado : public Persona { public: Empleado(char *n) : Persona(n) {} void VerNombre() { cout << "Emp: " << nombre << endl; } }; class Estudiante : public Persona { public: Estudiante(char *n) : Persona(n) {} void VerNombre() { cout << "Est: " << nombre << endl; } }; void main(void) { Persona *Pepito = new Estudiante("Jose"); Persona *Carlos = new Empleado("Carlos"); Estudiante Luis(Luis); Carlos ->VerNombre(); Pepito->VerNombre(); Luis.VerNombre(); delete Pepito; delete Carlos; cin.get(); }

Podemos comprobar que se ejecuta la versin de la funcin "VerNombre" que hemos definido para la clase base, y no la de las clases derivadas.

74

P.L.E.

P.O.O.

Funciones virtuales El ejemplo anterior demuestra algunas de las posibilidades del polimorfismo, pero tal vez sera mucho ms interesante que cuando se invoque a una funcin que se superpone en la clase derivada, se llame a sta ltima funcin, la de la clase derivada. En nuestro ejemplo, podemos preferir que al llamar a la funcin "VerNombre" se ejecute la versin de la clase derivada en lugar de la de la clase base. Esto se consigue mediante el uso de funciones virtuales. Cuando en una clase declaramos una funcin como virtual, y la superponemos en alguna clase derivada, al invocarla usando un puntero de la clase base, se ejecutar la versin de la clase derivada. Sintaxis:
virtual <tipo> <nombre_funcin>(<lista_parmetros>) [{}];

Modifiquemos en el ejemplo anterior la declaracin de la clase base "Persona".


class Persona { public: Persona(char *n) { strcpy(nombre, n); } virtual void VerNombre() { cout << nombre << endl; } protected: char nombre[30]; };

Ahora ejecutemos el programa de nuevo, veremos que la salida es ahora diferente. Ahora, al llamar a "Pepito->VerNombre()" se invoca a la funcin "VerNombre" de la clase "Estudiante", al llamar a "Carlos->VerNombre()" se invoca a la funcin de la clase "Empleado" y al llamar a Luis.VerNombre() se invoca a la funcin de la clase Empleado. Una vez que una funcin es declarada como virtual, lo seguir siendo en las clases derivadas, es decir, la propiedad virtual se hereda. Si la funcin virtual no se define exactamente con el mismo tipo de valor de retorno y el mismo nmero y tipo de parmetros que en la clase base, no se considerar como la misma funcin, sino como una funcin superpuesta. Tericamente, este mecanismo slo funciona con punteros y referencias, usarlo con objetos no tiene sentido, aunque funciona. Destructores virtuales Supongamos que tenemos una estructura de clases en la que en alguna de las clases derivadas exista un destructor. Un destructor es una funcin como las dems, por lo tanto, si destruimos un objeto referenciado mediante un puntero a la clase base, y el destructor no es virtual, estaremos llamando al destructor de la clase base. Esto puede ser desastroso, ya que nuestra clase derivada puede tener ms tareas que realizar en su destructor que la clase base de la que procede. Por lo tanto debemos respetar siempre sta regla: si en una clase existen funciones virtuales, el destructor debe ser virtual.

75

P.L.E.

P.O.O.

Constructores virtuales Los constructores no pueden ser virtuales. Esto puede ser un problema en ciertas ocasiones. Por ejemplo, el constructor copia no har siempre aquello que esperamos que haga. En general no debemos usar el constructor copia cuando usemos punteros a clases base. Para solucionar este inconveniente se suele crear una funcin virtual "clonar" en la clase base que se superpondr para cada clase derivada. Por ejemplo:
#include <iostream.h> #include <cstring.h> class Persona { public: Persona(char *n) { strcpy(nombre, n); } Persona(const Persona &p); virtual void VerNombre() { cout << nombre << endl; } virtual Persona* Clonar() { return new Persona(*this); } protected: char nombre[30]; }; Persona::Persona(const Persona &p) { strcpy(nombre, p.nombre); cout << "Per: constructor copia." << endl; } class Empleado : public Persona { public: Empleado(char *n) : Persona(n) {} Empleado(const Empleado &e); void VerNombre() { cout << "Emp: " << nombre << endl; } virtual Persona* Clonar() { return new Empleado(*this); } }; Empleado::Empleado(const Empleado &e) : Persona(e) { cout << "Emp: constructor copia." << endl; } class Estudiante : public Persona { public: Estudiante(char *n) : Persona(n) {} Estudiante(const Estudiante &e); void VerNombre() { cout << "Est: " << nombre << endl; } virtual Persona* Clonar() { return new Estudiante(*this); } }; Estudiante::Estudiante(const Estudiante &e) : Persona(e) { cout << "Est: constructor copia." << endl; } void main(void) { Persona *Pepito = new Estudiante("Jose"); Persona *Carlos = new Empleado("Carlos"); Persona *Gente[2]; Carlos ->VerNombre(); Pepito->VerNombre(); Gente[0] = Carlos->Clonar(); Gente[0]->VerNombre(); Gente[1] = Pepito->Clonar(); Gente[1]->VerNombre(); delete Pepito; delete Carlos; delete Gente[0]; delete Gente[1]; cin.get(); }

76

P.L.E.

P.O.O.

Este mtodo asegura que siempre se llama al constructor copia adecuado, ya que se hace desde una funcin virtual. Si un constructor llama a una funcin virtual, sta ser siempre la de la clase base. Esto es debido a que el objeto de la clase derivada an no ha sido creado. Funciones virtuales puras Habitualmente las funciones virtuales de la clase base de la jerarqua no se utilizan porque en la mayora de los casos no se declaran objetos de esa clase, y/o porque todas las clases derivadas tienen su propia definicin de la funcin virtual. Sin embargo, incluso en el caso de que la funcin virtual de la clase base no vaya a ser utilizada, debe declararse. De todos modos, si la funcin no va a ser utilizada no es necesario definirla, y es suficiente con declararla como funcin virtual pura. El modo de declarar una funcin virtual pura es asignndole el valor cero. Sintaxis:
virtual <tipo> <nombre_funcin>(<lista_parmetros>) = 0;

La nica utilidad de esta declaracin es la de posibilitar la definicin de funciones virtuales en las clases derivadas. De alguna manera se puede decir que la definicin de una funcin como virtual pura hace necesaria la definicin de esa funcin en las clases derivadas, a la vez que imposibilita su utilizacin con objetos de la clase base. Al definir una funcin como virtual pura hay que tener en cuenta que: No hace falta definir el cdigo de esa funcin en la clase base. No se pueden definir objetos de la clase base, ya que no se puede llamar a las funciones virtuales puras. Sin embargo, es posible definir punteros a la clase base, pues es a travs de ellos como ser posible manejar objetos de las clases derivadas. Clases abstractas Una clase abstracta es aquella que posee al menos una funcin virtual pura. No es posible crear objetos de una clase abstracta, estas clases slo se usan como clases base para la declaracin de clases derivadas. Las funciones virtuales puras sern aquellas que siempre se definirn en las clases derivadas, de modo que no ser necesario definirlas en la clase base. A menudo se mencionan las clases abstractas como tipos de datos abstractos, en ingls: Abstract Data Type, o resumido ADT. Hay varias reglas a tener en cuenta con las clases abstractas: No est permitido crear objetos de una clase abstracta. Siempre hay que definir todas las funciones virtuales de una clase abstracta en sus clases derivadas, no hacerlo as implica que la nueva clase derivada ser tambin abstracta.

77

P.L.E.

P.O.O.

Para crear un ejemplo de clases abstractas, recurriremos de nuevo a nuestra clase "Persona". Haremos que sta clase sea abstracta. De hecho, en nuestros programas de ejemplo nunca hemos declarado un objeto "Persona". Veamos un ejemplo:
#include <iostream.h> #include <cstring.h> class Persona { public: Persona(char *n) { strcpy(nombre, n); } virtual void Mostrar() = 0; protected: char nombre[30]; }; class Empleado : public Persona { public: Empleado(char *n, int s) : Persona(n), salario(s) {} void Mostrar() const; int LeeSalario() const { return salario; } void ModificaSalario(int s) { salario = s; } protected: int salario; }; void Empleado::Mostrar() const { cout << "Empleado: " << nombre << ", Salario: " << salario << endl; } class Estudiante : public Persona { public: Estudiante(char *n, float no) : Persona(n), nota(no) {} void Mostrar() const; float LeeNota() const { return nota; } void ModificaNota(float no) { nota = no; } protected: float nota; }; void Estudiante::Mostrar() { cout << "Estudiante: " << nombre << ", Nota: " << nota << endl; } void main(void) { Persona *Pepito = new Empleado("Jose", 1000); (1) Persona *Pablito = new Estudiante("Pablo", 7.56); char n[30]; Pepito->Mostrar(); Pablito->Mostrar(); cin.get(); }

La salida ser as: Empleado: Jose, Salario: 1000 Estudiante: Pablo, Nota: 7.56 En este ejemplo combinamos el uso de funciones virtuales puras con polimorfismo. Fjate que, aunque hayamos declarado los objetos "Pepito" y "Pablito" de tipo puntero a "Persona" (1), en realidad no creamos objetos de ese tipo, sino de los tipos "Empleado" y "Estudiante".

78

P.L.E.

P.O.O.

14. FLUJOS DE ENTRADA/SALIDA. STREAM


Un Stream es un flujo de datos, y para que exista es necesario que haya un origen abierto conteniendo datos, y un destino abierto y preparado para recibir datos. Entre el origen y el destino debe haber una conexin por donde puedan fluir los datos. Estas conexiones se llevan a cabo a travs de operadores y mtodos de C++. Los streams se usan de forma genrica, independientemente de cuales sean el origen y el destino. Tipos de Stream Los ms normales son los que se establecen entre un programa y la consola: el teclado y la pantalla. Tambin entre un programa y un fichero o entre dos zonas de memoria. En algunos casos es necesario abrir el stream antes de usarlo, como es el caso de los ficheros, y otras veces no, como es el caso de las consolas. Operadores de Insercin y Extraccin Todos los streams van a disponer de estos dos operadores. El operador de insercin, << , es el operador de desplazamiento de bits a la izquierda, que est definido en la clase ostream perteneciente a iostream.h, y >>, es el operador de desplazamiento a la derecha y est definido en la clase istream, definida en el mismo archivo cabecera. Estos operadores van a disponer a su izquierda un objeto stream que ser origeno destino, dependiendo del operador usado, y a la derecha lo que se va a escribir o donde se va a almacenar lo ledo. Ya se han usado streams: cout y cin que tienen por defecto la salida estndar (pantalla) y la entrada estndar (teclado). Tambin existe cerr para la salida estndar de errores del programa. Entrada/Salida con Formato La E/S de datos se realiza por medio de los operadores de insercin y extraccin en streams y puede formatearse p ejemplo para ajustar a la derecha o a la izquierda con or una longitud mnima o mxima. Para esto se definen mtodos y manipuladores. Los mtodos son aquellos a los que se les llama de forma habitual (con el nombre del objeto a la izquierda). En cambio, l s manipuladores pueden actuar en cualquier punto o a travs de los operadores de insercin y extraccin. Anchura Por defecto, los datos que entran y salen tienen una anchura que se corresponde con el espacio necesario para almacenar ese dato. Para fijar la anchura, tanto se puede usar el mtodo width() como el manipulador setw(). Si se usa width() sin parmetro devolver la anchura fijada, que por defecto es 0. Esto significa que se tomen los carcteres necesarios para la representacin del dato en el stream de salida. Si le pasamos un parmetro se fijar la anchura a ese nmero de carcteres.

79

P.L.E.

P.O.O.

Carcter de Relleno Cuando se ajusta una salida, y quedan espacios libres, estos se rellenan con espacios en blanco. Este carcter de relleno se puede modificar con el mtodo fill() o el manipulador setfill(). Llamando a fill() nos devuelve el carcter de relleno que se est usando en ese momento. Precisin Para nmeros en punto flotante, a la hora de la salida se representa con todos sus dgitos, pero esto es alterable, o bien mediante el mtodo precision(), o bien mediante el manipulador setprecision(), poniendo el nmero de dgitos a presentar en la salida como parmetro. El mtodo sin parmetros devolver el valor fijado anteriormente. Bases de Numeracin Normalmente, la entrada y salida de datos se hace en decimal, lo cual es modificable mediante dec(), hex(), oct(), y setbase(). Los tres primeros no necesitan parmetro alguno, pasando a base decimal, hexadecimal, u octal, mientras que la ltima necesitar recibir un entero indicando la base en la que se desea trabajar. No es necesario usar parntesis en los tres primeros. Veamos un ejemplo:
#include <iostream.h> #include <iomanip.h> void main(void) { unsigned long Dato; cout << Dame la direccin hexadecimal : ; cin >> hex >> Dato; cout << hex << Dato << , << dec << Dato << , << oct << Dato << \n; cout << setbase(10); cin >> setbase(10); }

Otros Manipuladores Para el objeto cout existen adems: endl() ends() sin parmetros flush() endl(): Inserta un retorno de carro. ends(): Inserta un terminador de cadena. flush(): Fuerza la salida de todos los carcteres del buffer al stream. Todos ellos se pueden llamar con cout._____ o bien con cout<<_____. Cin adems tiene el manipulador ws, sin parmetros, que permite ignorar los espacios en blanco en las operaciones de entrada o lectura. Est activado por defecto.

80

P.L.E.

P.O.O.

Indicadores de Formato Mtodo setf() Activa indicadores de formato Mtodo unsetf() Desactiva indicadores de formato Mtodo flags Asigna el estado de todos los indicadores Los dos primeros equivalen a los modificadores setioflags() y resetioflags(). Todos toman un entero largo que indica los indicadores a activar o desctivar. Pertenecen a la clase ios, de la cual se derivan las clases a las que pertenecen los objetos que se manejan. Los mtodos flags(), setf(), unsetf() devuelven un entero con el estado de los indicadores antes de alterarlos, siendo interesante almacenarlos para luego restituirlos. Otros Mtodos de E/S Los operadores de insercin y extraccin permiten realizar entradas y salidas con formato. Sin embargo, a veces puede ser necesario leer o escribir informacin sin formato alguno. Para ello tambin se dispone de algunos operadores en las clases istream y ostream. Mtodos
ios::Skipws ios::left ios::right ios::internal

Accin

Cuando est activo ignora los espacios en blanco iniciales Activa la justificacin izquierda en la salida de datos Activa la justificacin derecha en la salida de datos Permite que en las salidas el signo o indicador de base, se muestre a la izqda del relleno como primer carcter ios::dec Activa conversin decimal ios::oct Activa conversin octal ios::hex Activa conversin hexadecimal ios::showbase Mostrar el indicador de base en las salidas ios::showpoint Muestra el punto decimal y los dgitos decimales necesarios para completar la salida ios::uppercase Se usan letras maysculas en la salida ios::showpos Est desactivado por defecto y lo que hace es mostrar el signo en las salidas numricas ios::scientific Realiza las salidas de punto flotante con notacin cientfica ios::unitbuf Vuelca los caracteres de E/S en el dispositivo origen o destino ios::fixed Realiza la notacin decimal

El Polifactico Mtodo get get() es un mtodo para realizar l cturas o entradas de datos desde un objeto. Sus e posibles formas son: get(): Toma el siguiente carcter y lo devuelve. En caso de que haya encontrado final del stream, devuelve EOF. get(char *, int, char): El primer parmetro es la direccin donde se almacenar la cadena de caracteres, el segundo la longitud y el tercero el carcter delimitador, para que cuando lo encuentre se pare. Se va a detener cuando: a) Encuentre el delimitador.

81

P.L.E.

P.O.O.

b) Encuentre el final del stream. Si no se da el ltimo carcter se tomar por defecto \n. get(char &): Toma un carcter del stream y lo almacena en la variable cuya referencia se toma como parmetro. Lectura de Lneas El mtodo getline() toma los mismos parmetros que get() y el mismo valor por defecto para el delimitador. Se almacena el delimitador antes de aadir el carcter nulo. Lectura Absoluta Mediante el mtodo read() se puede leer un nmero de caracteres y almacenarlo en el espacio de memoria indicado. Slo se detiene si llega al final del stream y no ha ledo la cantidad indicada, pero no lo hace cuando se encuentre un delimitador o el carcter nulo. Como primer parmetro pasamos el puntero a donde se almacenar la cadena, y como segundo la longitud. Mediante el mtodo gcount() es posible saber el nmero de bytes ledos (nos lo devuelve). Movimiento en un Stream Es posible desplazar el puntero de lectura en un stream mediante los mtodos: Mtodos Parmetros y Accin
Seekg() ios::beg ios::cur ios::end tellg() Pasndole un entero largo se sita en una posicin absoluta Se sita en una posicin relativa movindose desde el principio del stream e indicndole un desplazamiento en forma de entero largo Lo mismo que antes pero parte de la posicin actual Lo mismo que antes pero parte desde el final Nos devuelve la posicin actual en el stream en forma de entero largo

Otras Operaciones de Lectura Mtodos Accin


peek() putback() ignore() Mtodo que devolver en forma de entero el cdigo del siguiente carcter disponible sin llegar a extraerlo Mtodo que toma como parmetro un carcter que se va a devolver al stream. Slo se usa cuando se toma un carcter que se quiera devolver Mtodo que ignora un nmero especfico de caracteres saltndolos hasta encontrar el delimitador o hasta completar el nmero especificado. Toma dos parmetros: Nmero de caracteres a saltar y el delimitador

Escritura sin Formato Existen dos mtodos principales para la escritura sin formato que son: Put(): Que toma como parmetro un carcter al que da salida por el stream. Write(): Toma los mismos parmetros que read() permitiendo escribir un bloque de bytes del tamao indicado.

82

P.L.E.

P.O.O.

Tambin existen los mtodos para desplazar el puntero de escritura en el stream: seekp()=Seekg() tellp()=tellg()
#include <iostream.h> #include <iomanip.h> void main(void) { char Cadena[20]; int N; cout << Introduzca una cadena :; cin.getline(Cadena,20); // Se toma una cadena de 20 // caracteres como mximo. cout << endl << endl; // Dos lneas en blanco cout.write(Cadena,20); // Se da salida a los 20 caracteres cout.flush(); // Vuelca el stream al correspondiente // dispositivo. cout << endl << Introduzca un nmero :; cin >> N; cout << endl << Su representacin es :; cout.write(char *) &N, sizeof(N); // Escribe su // representacin en memoria cout.flush(); }

Los Streams y los Buffers A cada stream se le asocia un buffer, para mejorar los tiempos de acceso. Mediante el mtodo rdbuf() obtenemos un puntero al buffer correspondiente a un stream. Los mtodos que nos van a permitir acceder a estos buffers podemos dividirlos en tres grupos: Grupo 1: Lectura del buffer Mtodos
in_avail() sbumpc() snextc() sgetc() stossc() sgetn()

Accin
Devuelve un entero indicando el nmero de caracteres que hay actualmente en el buffer de entrada, tomados desde el stream de lectura Lee un carcter del buffer de entrada, lo devuelve y avanza una posicin Avanza una poscin, lee un carcter del buffer y lo devuelve Lee el carcter actual apuntado en el buffer de entrada y lo devuelve, pero no avanza Avanza al siguiente carcter Este mtodo necesita dos parmetros, un puntero a carcter y un entero. Lee el nmero de caracteres indicado del buffer de entrada y lo almacena en la direccin indicada por el puntero Devuelve un carcter al buffer de entrada, especificndoselo como parmetro

sputbackc()

Grupo 2: Escritura en el buffer Mtodos


out_waiting() sputc() sputn()

Accin
Devuelve un entero indicando el nmero de caracteres que hay en el buffer de salida esperando a escribirse en el stream Permite escribir un carcter al buffer de salida, carcter que habr que pasar como parmetro Permite escribir en el buffer de salida un determinado nmero de caracteres. Toma dos

83

P.L.E. parmetros: Puntero a carcter y un entero

P.O.O.

Grupo 3: Posicionamiento en el buffer Mtodo


seekpos()

Accin
Posiciona el puntero en una posicin absoluta dada por un entero largo, el cual se pasa como parmetro. El segundo parmetro indica el puntero que se va a fijar, el de entrada o el de salida Toma tres parmetros y la posicin del puntero de forma relativa segn el segundo parmetro que puede ser: ios::beg, ios::cur, ios::end, segn se desee desplazar desde el principio, desde la posicin actual o desde el final. El tercer parmetro es el indicador del puntero a desplazar ios::in, ios::out. Por defecto, ios::in/ios::out

seekoff()

84

P.L.E.

P.O.O.

RELACIN DE EJERCICIOS N 2 Ejercicio 1: Construir una clase, denominada persona que tenga como datos miembro nombre, apellido, edad (las variables tipo string definirlas como puntero a carcter). Definir funciones constructoras, as como funciones que saquen por pantalla los valores de una variable de tipo persona utilizando las facilidades de C++ y funciones de entrada por pantalla de los datos correspondientes a dicho tipo. Construir tipos derivados de la clase persona denominados consumidor y deportista. El primero con las mismas caractersticas y funciones que persona pero aadiendo variables que determinen su consumo de gasolina, electricidad y gas (flotantes) y funciones que permitan su actualizacin. El segundo aadiendo las variables tipo_deporte y marca (este ltimo flotante). Prepara las salidas de estos ltimos para que los nmeros flotantes siempre queden alineados en su punto decimal. Ejercicio 2: Realizar un programa que implemente una lista cuyos elementos son de clase componente. Los miembros de esta clase son datos como: num_piezas, codigo,tipo, proveedor, ...; y funciones constructoras, de salida por pantalla y de consulta de la informacin. Realiza un conjunto de clases derivadas de la componente como son: pantalla, teclado y ucp (de unidad central). Cada una de estas clases derivadas tiene datos miembro adicionales: pulgadas, teclas y dimensiones. Adems, cuando se m anda a imprimir se sacan estos valores acompaados de la indicacin que se trata de un rodamiento, etc. Utiliza funciones virtuales y paso por referencia en la implementacin. Ejercicio 3: Crear tres clases bases: unidadcentral, pantalla, teclado. Define como privadas las variables que identifican cada uno de estos elementos por separado. Por ejemplo, para la unidadcentral: char codigo, char cpu, float velocidad, int memoria, int discoduro. Define funciones constructoras, destructoras, de impresin y de acceso. Crea una clase derivada de las tres anteriores llamada ordenador. Esta tendr como dato miembro un string que define su nombre comercial y un string que tambin es su cdigo. Crear funciones constructoras, destructoras, de impresin y de acceso a los datos miembro. Realiza un programa principal en el que se declare un vector de objetos unidadcentral. Realiza un bucle en que por pantalla se piden unidades centrales u ordenadores, con sus caractersticas. Almacena ambos en el mismo vector.

85

You might also like