|
|
|
Programando en C++ Herencia II |
En esta lección: |
En la pasada lección desarrollamos un modelo utilizando medios de transporte para ilustrar el concepto de la herencia, en éste capítulo utilizaremos el mismo modelo para ilustrar algunos conceptos adicionales relativos a la herencia y su uso.
En el código que sigue podemos ver que se trata básicamente del mismo programa que el último estudiado en el capítulo anterior excepto que hemos reordenado el texto, la mayor diferencia es que algunos de los métodos más simples en las clases han sido cambiados a código en línea reduciendo el tamaño del archivo considerablemente, el otro cambio está en el reordenamiento de las clases y sus métodos asociados definiendo en primer término a las clases y en seguida el programa principal. La implementación de los métodos se han colocado al final del archivo para demostrar la flexibilidad que se tiene al ordenar clases y métodos en C++. Hacemos todo esto por conveniencia en el presente capítulo pero lo aconsejable es manejar los archivos en forma separada tal y como lo vimos en el pasado capítulo.
#include <iostream.h>
class vehiculo
{
protected:
int llantas;
double peso;
public:
void inicializa(int in_llantas, double in_peso);
int obtiene_llantas(void) {return llantas;}
double obtiene_peso(void) {return peso;}
double carga_llanta(void) {return peso/llantas;}
};
class carro:public vehiculo
{
int carga_pasajero;
public:
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:
void inicia_camion(int cuantos=2, double max_carga = 24000.0);
double eficiencia(void);
int pasajeros(void) {return carga_pasajero;}
};
int main()
{
vehiculo monociclo;
monociclo.inicializa(1, 12.5);
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;
sedan.inicializa(4, 3500.0, 5);
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;
semi.inicializa(18, 12500.0);
semi.inicia_camion(1, 33675.0);
cout << "El semi pesa " << semi.obtiene_peso() << " libras.\n";
cout << "La eficiencia del semi es " << 100.0 * semi.eficiencia() << " por ciento.\n";
return 0;
}
// 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;
}
void camion::inicia_camion(int cuantos, double max_carga)
{
carga_pasajero = cuantos;
pasaje = max_carga;
}
double camion::eficiencia(void)
{
return pasaje / (pasaje + peso);
}
Como mencionamos anteriormente, 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 método del mismo nombre, inicializa ( ) al declarado en la super-clase llamada vehiculo. Esto demuestra que el reordenamiento del archivo no anula ésta capacidad de repetir nombres.
Como tenemos un método llamado inicializa ( ) declarado en la clase derivada carro éste oculta el método del mismo nombre el cual es parte de la clase base. Hay ocasiones en que Usted desee enviar un mensaje al método 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 en el código principal:
sedan.vehiculo::inicializa(4, 3500.0);
Obviamente el número y tipo de parámetros deben coincidir con aquellos utilizados en el método de la clase base.
Si los datos dentro de una clase base estuvieran totalmente disponibles en todas las clases derivadas sería sencillo para un programador heredar la clase base en su totalidad, ésto anularía cualquier esfuerzo por ocultar información sensible, por ésta razón los datos en una clase no están disponibles en forma automática a los métodos de una clase heredada, sin embargo hay ocasiones en las que es deseable heredar todas las variables directamente en las sub-clases y hacerlas actuar como si hubieran sido declaradas como parte de aquellas clases, por ésta razón el diseñador de C++ dotó a éste lenguaje con la palabra clave protected. En el código de arriba utilizamos la palabra protected en la línea 5 de tal manera que los datos de la clase vehiculo pueden ser directamente importados a cualquier clase derivada pero no están disponibles fuera de la clase base ó de las clases derivadas. Como mencionamos anteriormente, todos los datos son declarados automáticamante como private en caso de que no sea dada una especificación contraria. Notará que las variables llamadas llantas y peso están disponibles para utilizarse en el método llamado inicializa ( ) en las líneas 64 a la 69 como si hubieran sido declaradas como parte de la clase carro. Están disponibles porque fueron declaradas como de tipo protected en la clase base. Claro está, también estarían disponibles si hubieran sido declaradas como de tipo public en la clase base, pero en ese caso estarían disponibles fuera de ambas clases y entonces hubieramos perdido nuestra protección. Observe que las dos variables están disponibles para utilizarse en la clase base como está ilustrado en las líneas 58 a la 62. podemos ahora definir los tres formas para declarar variables y métodos:
private: Las variables y métodos no están disponibles para ninguna rutina externa así como tampoco lo están para ninguna clase derivada heredada de ésta clase.
protected: Las variables y métodos no está disponibles para ninguna rutina externa, pero están directamente disponibles para cualquier clase derivada heredada de ésta clase.
public: Todas las variables y métodos están libremente disponibles a cualquier rutina externa así como para cualquier clase derivada.
Notará que las definiciones dadas se aplican a su vez para la estructura, la única diferencia es que una estructura declara por default a todas las variables como de tipo public hasta que alguna otra palabra clave sea utilizada.
Para continuar hemos modificado el código anterior para permitir que los datos de la clase base utilizen el tipo private al comentar la línea 5. En éste programa los datos no están disponibles para utilizarse en las clases derivadas de tal manera que la única forma de utilizar los datos de la clase base es enviar mensajes a los métodos en la clase base, aún dentro de las clases derivadas:
#include <iostream.h>
class vehiculo
{
// protected:
int llantas;
double peso;
public:
void inicializa(int in_llantas, double in_peso);
int obtiene_llantas(void) {return llantas;}
double obtiene_peso(void) {return peso;}
double carga_llanta(void) {return peso/llantas;}
};
class carro:public vehiculo
{
int carga_pasajero;
public:
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:
void inicia_camion(int cuantos=2, double max_carga = 24000.0);
double eficiencia(void);
int pasajeros(void) {return carga_pasajero;}
};
int main()
{
vehiculo monociclo;
monociclo.inicializa(1, 12.5);
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;
sedan.inicializa(4, 3500.0, 5);
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;
semi.inicializa(18, 12500.0);
semi.inicia_camion(1, 33675.0);
cout << "El semi pesa " << semi.obtiene_peso() << " libras.\n";
cout << "La eficiencia del semi es " << 100.0 * semi.eficiencia() << " por ciento.\n";
return 0;
}
// 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;
vehiculo::inicializa(in_llantas, in_peso); // Enunciado agregado
}
void camion::inicia_camion(int cuantos, double max_carga)
{
carga_pasajero = cuantos;
pasaje = max_carga;
}
double camion::eficiencia(void)
{
return pasaje / (pasaje + obtiene_peso()); // Cambio con respecto al programa anterior
}
Parece tonto hacer llamadas a los métodos en la clase base para acceder a los datos que forman parte de clases derivadas pero esa es la forma en que trabaja C++. Esto es un indicativo de que se debe dedicar un tiempo para determinar cómo será utilizada una clase determinada, si Usted piensa que alguien deseara heredar su clase en una nueva considere hacer los miembros de tipo protected para que sean fácilmente accesibles en la nueva clase. Observe y estudie los cambios en el códogo con respecto al presentado en primer lugar en ésta lección para una mejor comprensión del uso de private.
Nuevamente hemos hecho algunos cambios al programa inicial, notará en éste codigo que las clases derivadas llamadas carro y camion no tienen la palabra clave public antes del nombre de la clase base en la primer línea de cada declaración. La palabra clave public, cuando se incluye antes del nombre de la clase base hace disponibles a todos los métodos definidos en la misma para utilizarse en las clases derivadas con el mismo nivel de seguridad como si hubieran sido definidos dentro de la clase base, de esta manera, en el programa previo se nos estaba permitido llamar a métodos definidos como parte de la clase base desde el programa principal aunque estuvieramos trabajando con un objeto de una clase derivada.
#include <iostream.h>
class vehiculo
{
protected:
int llantas;
double peso;
public:
void inicializa(int in_llantas, double in_peso);
int obtiene_llantas(void) {return llantas;}
double obtiene_peso(void) {return peso;}
double carga_llanta(void) {return peso/llantas;}
};
class carro:private vehiculo
{
int carga_pasajero;
public:
void inicializa(int in_llantas, double in_peso, int personas=4);
int pasajeros(void) {return carga_pasajero;}
};
class camion:private vehiculo
{
int carga_pasajero;
double pasaje;
public:
void inicia_camion(int cuantos=2, double max_carga = 24000.0);
double eficiencia(void);
int pasajeros(void) {return carga_pasajero;}
};
int main()
{
vehiculo monociclo;
monociclo.inicializa(1, 12.5);
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;
sedan.inicializa(4, 3500.0, 5);
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;
// semi.inicializa(18, 12500.0);
semi.inicia_camion(1, 33675.0);
// cout << "El semi pesa " << semi.obtiene_peso() << " libras.\n";
// cout << "La eficiencia del semi es " << 100.0 * semi.eficiencia() << " por ciento.\n";
return 0;
}
// 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;
}
void camion::inicia_camion(int cuantos, double max_carga)
{
carga_pasajero = cuantos;
pasaje = max_carga;
}
double camion::eficiencia(void)
{
return pasaje / (pasaje + peso);
}
En éste programa, todas las entidades son heredadas como private debido al uso de la palabra clave private previo al nombre de la clase base y por tanto no están disponibles para ninguna rutina fuera de la clase derivada. En el presente programa, los únicos métodos disponibles para objetos de la clase carro son aquellos definidos como parte de la clase misma y por tanto sólo tenemos los métodos llamados inicializa ( ) y pasajeros ( ) para ser utilizados con los objetos de la clase carro. Cuando declaramos un objeto de la clase carro, de acuerdo a la definición de C++, contiene tres variables, una definida como parte de su clase llamada carga_pasajero y dos más que son parte de la clase base, llantas y peso.
En el ejemplo siguiente agregamos constructores por defecto a cada clase para estudiar como se utilizan cuando utilizamos la herencia. 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 en la línea 38 utilizando el constructor por defecto y el objeto es inicializado a los valores contenidos en el constructor. La línea 39 está comentada porque ya no es necesario código de inicialización para el objeto.
#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(void) {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;}
};
int main()
{
vehiculo monociclo;
// monociclo.inicializa(1, 12.5);
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;
// sedan.inicializa(4, 3500.0, 5);
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;
// semi.inicializa(18, 12500.0);
// semi.inicia_camion(1, 33675.0);
cout << "El semi pesa " << semi.obtiene_peso() << " libras.\n";
cout << "La eficiencia del semi es " << 100.0 * semi.eficiencia() << " por ciento.\n";
return 0;
}
// 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;
}
void camion::inicia_camion(int cuantos, double max_carga)
{
carga_pasajero = cuantos;
pasaje = max_carga;
}
double camion::eficiencia(void)
{
return pasaje / (pasaje + peso);
}
Cuando definimos un objeto de una de las clases derivadas en la línea 44, el sedan existe una pequeña diferencia porque no solo necesitamos llamar al constructor para la clase derivada, tenemos que preocuparnos además de cómo inicializamos la clase base. Actualmente no hay problema porque el compilador llamará automáticamente el constructor por defecto para la clase base a menos que la clase derivada explicitamente llame a otro constructor para la clase base.
El siguiente problema a tratar es el orden de construcción, el constructor de la clase base será llamado antes de la clase derivada, ésto tiene sentido porque garantiza que la clase base esté propiamente construido cuando el constructor para la clase derivada es ejecutada, esto permite utilizar algunos de los datos de la clase base durante la construcción 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 propiamente inicializadas, esta es la razón por la cual comentamos el método de inicialización en la línea 45.
Cuando no necesitamos los objetos se deben ejecutar los respectivos destructores y como no hemos definido ninguno, serán ejecutados los destructores por defecto. Una vez más, la destrucción 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.
Enseguida se muestra una variación más al código inicial, esta vez utilizando constructores algo más complejos, notará que cada clase tiene declarado un constructor adicional. El constructor adicional agregado a la clase vehiculo en la línea 10 no tiene nada especial, se utiliza en la línea 43 del programa principal donde definimos monociclo pasandole dos valores para utilizarse cuando se ejecute éste constructor.
El constructor para la clase carro el cual se declara en las líneas 22 y 23 es un poco diferente porque le pasamos tres valores, uno, el llamado personas se utiliza dentro de la clase derivada para inicializar a la variable miembro llamada carga_pasajero. Las otras dos literales sin embargo, deben pasarse a la clase base de manera de inicializar el número de llantas y el peso. Esto se hace utilizando un inicializador miembro como se ilustra en éste constructor. Los dos puntos al final de la línea 22 indica que un miembro inicializador le sigue, todas las entidades que están después de los dos puntos y antes de la llave de apertura del cuerpo del constructor son miembros inicializadores. El primer miembro inicializador está en la línea 23 y parece una llamada a un constructor para la clase vehiculo que requiere dos parámetros de entrada, y ésto es exactamente lo que es, y llama al constructor para la clase vehiculo e inicializa esa parte del objeto sedan que es heredado de la clase vehiculo. Así podemos controlar cuál inicializador de clase base será llamado al construir un objeto de la clase derivada.
El siguiente miembro inicializador actúa como un constructor para una simple variable. Al mencionar el nombre de la variable e incluyendo un valor del tipo correcto dentro del paréntesis, el valor es asignado a aquella variable aunque la variable no sea una clase sino un simple tipo predefinido. Ésta técnica se puede utilizar para inicializar todos los miembros de la clase derivada ó una parte de ellas. Cuando todos los miembros de la lista inicializadora son ejecutados se ejecuta a su vez el código dentro de las llaves, en éste caso no hay código dentro del bloque ejecutable del constructor.
Puede parecer muy extraño, pero los miembros de la lista inicializadora no se ejecutan en el orden en que aparecen. Los constructores para las clases heredadas se ejecutan en primer lugar, en el orden de sus respectivas declaraciones en el encabezado de la clase. Cuando se utiliza herencia múltiple varias clases pueden enlistarse en la línea de encabezado pero en éste programa sólo se utiliza una. Entonces se inicializan las variables miembro, pero no en el orden dado en la lista sino en el orden en que fueron declarados en la clase, finalmente se ejecuta el bloque de código del constructor, si lo hay.
Existe una buena razón para éste extraño orden. Los destructores deben ejecutarse en el orden inverso a los constructores, pero si hay dos constructores con diferente orden de construcción definido, ¿Cúal define el orden de destrucción?. La respuesta correcta es, ninguno. El sistema utiliza el orden declarado de construcción y simplemente lo invierte para la destrucción de los objetos. Tómese un buén tiempo para estudiar el código siguiente:
#include <iostream.h>
class vehiculo
{
protected:
int llantas;
double peso;
public:
vehiculo(void) {llantas = 7; peso = 11111.0;}
vehiculo(int in_llantas, double in_peso) {llantas = in_llantas; peso = in_peso;}
void inicializa(int in_llantas, double in_peso);
int obtiene_llantas(void) {return llantas;}
double obtiene_peso(void) {return peso;}
double carga_llanta(void) {return peso/llantas;}
};
class carro:public vehiculo
{
int carga_pasajero;
public:
carro(void) {carga_pasajero = 4;}
carro(int personas, int in_llantas, double in_peso) :
vehiculo(in_llantas, in_peso), carga_pasajero(personas){ }
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;}
camion(int personas, double carga, int in_llantas, double in_peso) :
vehiculo(in_llantas, in_peso), carga_pasajero(personas), pasaje(carga) { }
void inicia_camion(int cuantos=2, double max_carga = 24000.0);
double eficiencia(void);
int pasajeros(void) {return carga_pasajero;}
};
int main()
{
vehiculo monociclo(1, 12.5);
// monociclo.inicializa(1, 12.5);
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(5, 4, 3500.0);
// sedan.inicializa(4, 3500.0, 5);
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(1, 33675, 18, 12500.0);
// semi.inicializa(18, 12500.0);
// semi.inicia_camion(1, 33675.0);
cout << "El semi pesa " << semi.obtiene_peso() << " libras.\n";
cout << "La eficiencia del semi es " << 100.0 * semi.eficiencia() << " por ciento.\n";
return 0;
}
// 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;
}
void camion::inicia_camion(int cuantos, double max_carga)
{
carga_pasajero = cuantos;
pasaje = max_carga;
}
double camion::eficiencia(void)
{
return pasaje / (pasaje + peso);
}