clase base

Anuncio
Herencia y
Polimorfismo en C++
Informática II
Fundamentos de Programación
18 de Febrero de 2002
Escuela Superior de Ingenieros de San Sebastián - Tecnun
1
Herencia
•
Concepto de "herencia":
– Una clase -clase derivada- puede definirse a partir de otra clase ya existente (clase base), de la
que hereda sus variables y funciones miembro.
– La clase derivada puede añadir y/o redefinir nuevas variables y/o funciones miembro.
– La clase base suele ser más general que la clase derivada. Ésta añade nuevas determinaciones
o especificaciones (nuevas variables y/o funciones miembro).
– A su vez, la clase derivada puede ser clase base de una nueva clase derivada, que hereda sus
variables y funciones miembro. Se puede constituir una jerarquía de clases.
•
Además de public y private, C++ permite también definir miembros protected.
– Los miembros protected, al igual que los private, no son accesibles desde fuera de la clase.
– En una clase base, los miembros protected se diferencian de los private en que sí pueden ser
accesibles para las clases derivadas de dicha clase base.
•
Para la clase derivada, la clase base se puede heredar como pública o como privada:
– La clase derivada no tiene acceso a los miembros private de la clase base. Sí tiene acceso a los
miembros public y protected.
– Si la clase base se hereda como public, la clase derivada hereda los miembros public y
protected de la clase base como miembros public y protected, respectivamente.
– Si la clase base se hereda como private, la clase derivada hereda todos los miembros de la
clase base como private.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
2
Ej. de herencia: clase "empleado"
•
Supóngase que un departamento de una empresa utiliza tres tipos de
empleados:
– subcontratado: cobra por horas.
– vendedor: cobra por horas, más una comisión de las ventas.
– encargado: cobra una cantidad fija por mes.
•
y que se desea hacer una función llamada calcular_pago() que calcule lo que
hay que pagar cada mes a cada empleado.
Empleado
Se puede pensar en una jerarquía de empleados
de la forma indicada en la figura:
Subcontratado
Encargado
Vendedor
•
Los principios de esta jerarquía son los siguientes:
Enc_ ventas
– Todos son empleados.
– Los vendedores cobran por horas como los subcontratados (de hecho son un tipo
especial de subcontratados), pero tienen además una comisión, por lo que pueden
ser considerados como una particularización de éstos.
– Los encargados sólo tienen en común con los demás el hecho de ser empleados.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
3
Ej. de herencia (cont.)
Clase base empleado:
class empleado
{
private:
char nombre[31];
public:
empleado();
empleado(const char *nom);
char *devolver_nombre();
float calcular_pago() {return 0.0;}
};
Clase derivada subcontratado:
class subcontratado : public empleado
{
private:
float tarifa;
float no_horas;
public:
subcontratado(const char *nom);
void fijar_tarifa(float tar);
void contar_horas(float horas);
float calcular_pago();
};
Clase derivada vendedor:
class vendedor : public subcontratado
{
private:
float comision;
float ventas_realizadas;
public:
vendedor(const char *nom);
void fijar_comision(float com);
void contar_ventas(float ventas);
float calcular_pago();
};
Obsérvese la forma en que, en cada clase derivada, se define la forma en que se hereda la clase base
como public o private.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
4
Ej. de herencia (cont.)
Definición de la clase derivada encargado:
class encargado : public empleado
{
private:
float sueldo_mensual;
public:
encargado(const char *nom);
void fijar_sueldo(float sueldo);
float calcular_pago();
};
•
•
•
•
•
Recuérdese que cada una de las clase derivadas hereda todos los miembros de
la clase base correspondiente.
La clase vendedor hereda los miembros de las clases subcontratado y
empleado.
La clase derivada no puede acceder directamente a las variables miembro
privadas de la clase base: hay que utilizar las funciones públicas de la clase
base.
Una clase derivada puede redefinir (definir una función diferente con el
mismo nombre) alguna de las funciones miembro de la clase base.
La función de la clase base es accesible en la clase derivada por medio del
operador (::). La función redefinida es utilizable directamente en la clase
derivada.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
5
Ej. de herencia (cont.)
• Definición de la función calcular_pago() en cada una de las clases:
float subcontratado::calcular_pago() {
return tarifa * no_horas;
};
float vendedor::calcular_pago() {
return (subcontratado::calcular_pago() +
};
comision * ventas_realizadas);
float encargado::calcular_pago() {
return sueldo_mensual;
};
• No sería correcto utilizar las definiciones:
float vendedor::calcular_pago() {
return (tarifa * no_horas + comision * ventas_realizadas);
};
float vendedor::calcular_pago() {
return (calcular_pago() + comision * ventas_realizadas);
};
pues vendedor::calcular_pago() no tiene acceso a las variables privadas de
subcontratado. La función calcular_pago() de vendedor no debe llamarse a sí misma,
sino a la de subcontratado.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
6
Constructores de clases derivadas
•
•
•
Un objeto de una clase derivada contiene todos los miembros de la clase base.
El constructor de la clase derivada debe llamar al de la clase base.
Cuando se define un constructor para una clase derivada, se debe especificar
un inicializador base (llamada al constructor de la clase base).
El inicializador base se especifica poniendo, a continuación de los argumentos
del constructor, el carácter (:) y un constructor de la clase base seguido de
una lista de argumentos entre paréntesis. Por ejemplo:
// constructor para la clase subcontratado
subcontratado::subcontratado(const char *nom)
: empleado(nom)
{
tarifa = 0.0;
no_horas = 0.0;
};
•
•
•
Al declarar un objeto de la clase derivada, se ejecuta primero el constructor
de la clase base y luego el de la clase derivada.
El inicializador base puede ser omitido si la clase base tiene un constructor
por defecto.
El constructor de una clase derivada debe disponer de valores para sus
propias variables y para el constructor de la clase base.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
7
Herencia múltiple.
Clases base virtuales
•
•
Una clase derivada puede heredar variables y funciones miembro de varias clases base.
Por ejemplo, la clase encargado_ventas puede heredar los miembros de las clases encargado y
vendedor. Se definiría en la forma:
class encargado_ventas : public encargado, public vendedor
{
...
// definición de variables y funciones miembro
};
•
•
•
•
Una clase base no puede figurar directamente más de una vez en la definición de la clase
derivada. Sin embargo, indirectamente una clase puede ser clase base más de una vez de una
clase derivada.
Por ejemplo, la clase empleado es clase base por duplicado de la clase encargado_ventas. La
clase encargado_ventas tiene por duplicado los miembros de la clase base empleado. Se puede
utilizar el operador (::).
Al llamar a la función devolver_nombre() dará error de ambigüedad, ya que no sabe cual de las
dos utilizar (por encargado o por vendedor).
Para evitar esto, la clase base empleado debe ser declarada clase base virtual. En las
definiciones de las clases derivadas hay que hacer:
class subcontratado : public virtual empleado {...};
class encargado : public virtual empleado {...};
•
El constructor de la clase base virtual (empleado) siempre se llama antes que los constructores
de las clases base no virtuales (encargado o subcontratado), por lo tanto en hay que llamar
primero al constructor de empleado: vendedor(..,..,..) : empleado(..), subcontratado(...){ ; }
Escuela Superior de Ingenieros de San Sebastián - Tecnun
8
Conversiones entre objetos de
clases base y clases derivadas
•
Es posible realizar conversiones o asignaciones entre un objeto de una clase
derivada a un objeto de la clase base (se puede ir de lo más particular a lo más
general, aunque se pueda perder información). Es posible hacer:
un_empleado = un_encargado;
un_subcontratado = un_vendedor;
•
•
•
•
•
No son posibles las conversiones en sentido contrario (de lo más general a lo
más particular, pues no se dispone de valores para todas las variables miembro
de la clase derivada y habría variables que quedarían sin inicializar).
De forma análoga, se puede convertir un puntero a una clase derivada en un
puntero a la clase base.
Un puntero a la clase base puede almacenar la dirección de un objeto de
cualquiera de las clases derivadas de esa clase base.
Se puede hacer referencia a un objeto de una clase derivada con un puntero
a la clase base.
Cuando se hace referencia a un objeto por medio de un puntero, el tipo de
puntero determina la función miembro que se aplica a dicho objeto, en el caso
de que dicha función esté definida en las clases base y derivada.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
9
Punteros a la clase base para manejo
de objetos de clases derivadas
•
En una jerarquía de clases con funciones del mismo nombre definidas en
todas las clases, el tipo de objeto determina la función que se aplica.
– por ejemplo, con nombres de objetos:
vendedor1.calcular_pago();
operario3.calcular_pago();
– y con punteros a objetos:
encargado *pntr1;
pntr1->calcular_pago();
•
•
•
•
A una función que espera un puntero a la clase base se le puede pasar un
puntero a cualquiera de sus clases derivadas (por las leyes de conversión de
punteros).
De esta forma, una lista de objetos de distintas clases puede tratarse con la
interface -con las funciones miembro- de la clase base.
Este tratamiento es genérico, sin tener en cuenta las peculiaridades de las
clases derivadas, pues si se utiliza un puntero a una clase base con un objeto
de una clase derivada, se utiliza la función de la clase base.
El tratamiento personalizado con punteros genéricos (a objetos de la clase
base) se consigue por medio del polimorfismo.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
10
Polimorfismo y funciones
virtuales
• La palabra polimorfismo hace referencia a la capacidad de llamar a funciones
miembro:
–
–
–
con un único nombre y análoga pero diferente misión,
que actúen sobre objetos distintos de una jerarquía de clases,
que se llamen del modo preciso sin tener que especificar
el tipo exacto de los objetos.
for (i=0; i<n_empleados; i++)
pempleado[i]->calcular_pago();
Empleado
Operario
Oficial
Directivo
Tecnico
• La función calcular_pago() debe actuar correctamente con cada objeto de las clases
derivadas de la clase Empleado, sin tener que pasar el tipo de objeto.
• Esto se hace:
–
con punteros genéricos a la clase base Empleado:
Empleado *pempleado;
cantidad = pempleado->calcular_pago();
–
–
declarando calcular_pago() como función virtual en la clase base Empleado.
si la función no se declara como virtual se utiliza siempre la función de la clase base
Empleado::calcular_pago().
• Si una clase no tiene definida una función, utiliza la heredada de la clase base.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
11
Polimorfismo y
funciones virtuales (cont.)
•
•
•
•
De ordinario, en C++ la llamada a una función determinada se establece en el
momento de la compilación (vinculación estática o temprana).
Con funciones virtuales esto no es posible, pues no se sabe en ese momento a
qué tipo de objeto apuntará el puntero en el instante de la llamada. La función
a llamar se determina en tiempo de ejecución (vinculación dinámica o tardía).
Con funciones virtuales es posible que un usuario añada nuevas clases a una
jerarquía de clases, sin modificar o recompilar el código original (por tanto, sin
necesidad de conocer los ficheros fuentes).
Las funciones virtuales son algo menos eficientes que las funciones normales:
– cada clase que utiliza funciones virtuales tiene un vector de punteros (uno por cada
función virtual) llamado v-table.
– cada uno de estos punteros apunta a la función virtual que es apropiada para esa
clase, en principio a la propia definición si existe.
– si una clase no tiene su propia definición de la función virtual, el puntero apunta a
la función virtual de su clase base más próxima que tenga una definición propia.
– cada objeto contiene un puntero oculto a la v-table de su clase. Con el puntero al
objeto se accede a la v-table de la clase y a través de ella se accede a la definición
adecuada de la función virtual. Éste es el trabajo extra.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
12
Funciones virtuales puras
•
•
•
Una función virtual debe estar definida en la clase base de la jerarquía, aunque no se vaya a
utilizar nunca:
– porque no existen objetos de esa clase,
– porque cada clase derivada tiene su propia definición de esa función.
Una función virtual de la clase base que no se va a utilizar nunca no necesita ser definida:
basta declararla como función virtual pura.
Una función virtual pura se declara en la forma:
virtual float calcular_pago() const = 0;
•
// virtual pura
En este caso:
– no hace falta definir el código de empleado::calcular_pago().
– no se pueden definir objetos de la clase base empleado, pues las funciones virtuales
puras no pueden ser llamadas.
– sí se pueden definir punteros a la clase empleado, pues a través de ellos pueden
manejarse objetos de clases derivadas.
– Es necesario redefinir la función como const: float calcular_pago() const { ... }
• Clases abstractas:
– A las clases que declaran funciones virtuales puras se les llama clases abstractas, pues
no tienen objetos concretos de esa clase.
– Si una clase derivada no redefine una función virtual pura heredada, la clase derivada la
hereda como función virtual pura y se convierte también en clase abstracta.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
13
Func. virtuales puras (cont.)
•
•
•
•
•
Es habitual utilizar jerarquías de clases en las que las clases superiores son clases abstractas, que
definen funciones virtuales puras que las clases concretas derivadas redefinen del modo adecuado.
El constructor de la clase base se llama antes que el constructor de la clase derivada.
Los destructores se llaman en orden opuesto a los constructores: primero el de la clase derivada y
luego el de la clase base.
Problema que puede presentarse:
– con reserva dinámica de memoria, si delete se aplica a un puntero de la clase base, se llama al
destructor de la clase base, aunque dicho puntero apunte a un objeto de una clase derivada (se
debería llamar primero al destructor de la clase derivada).
– la solución es declarar el destructor de la clase base como virtual: virtual ~empleado();
– esto hace que los destructores de las clases derivadas sean virtuales, aunque tengan nombres
diferentes.
– así, si delete se aplica a un puntero de la clase base, el destructor adecuado se aplica según el
objeto de que se trate.
– si se define una clase con funciones virtuales, conviene definir un destructor virtual aunque la
clase no lo necesite (puede haber clases derivadas que lo necesiten)
No hay constructores virtuales.
empleado *emp1 = new encargado("Carla",2500);
delete emp1;
Al eliminar el objeto con delete, sólo se ejecutará
el destructor de la clase base, si éste no es declarado
como virtual.
Escuela Superior de Ingenieros de San Sebastián - Tecnun
14
Ejemplo 1
// Fichero Empleado.h
// ejemplo de funciones virtuales
#include <iostream.h>
class Empleado {
public:
virtual void imprime_cargo()
{ cout << "es un cargo no definido\n"; }
};
class Directivo : public Empleado {
public:
void imprime_cargo()
{ cout << "es un directivo\n"; }
};
Empleado
Operario
Oficial
Directivo
Tecnico
class Operario : public Empleado {
public:
void imprime_cargo()
{ cout << "es un operario\n"; }
};
class Oficial : public Operario {
public:
void imprime_cargo()
{ cout << "es un oficial\n"; }
};
class Tecnico : public Operario {
public:
void imprime_cargo()
{ cout << "es un tecnico\n"; }
};
Escuela Superior de Ingenieros de San Sebastián - Tecnun
15
Ejemplo 1 (cont.)
// Fichero dpto.cpp
Empleado
#include "empleado.h"
#include <string.h>
void main(void)
{
Empleado Rafa;
Directivo Mario;
Operario Anton;
Oficial
Luis;
Tecnico
Pablo;
// El tipo del objeto determina la función que se llama
cout << "Primero con nombres de objetos:" << endl;
cout << "Rafa ";
Rafa.imprime_cargo();
cout << "Mario ";
Mario.imprime_cargo();
cout << "Anton ";
Anton.imprime_cargo();
cout << "Luis ";
Luis.imprime_cargo();
cout << "Pablo ";
Pablo.imprime_cargo();
Operario
Oficial
Directivo
Tecnico
Con funciones virtuales, el tipo de
objeto apuntado por un puntero a
clase base determina la función que
es llamada
// cont.
pe->imprime_cargo();
pe = &Anton;
cout << "Anton ";
pe->imprime_cargo();
pe = &Luis;
cout << "Luis ";
pe->imprime_cargo();
pe = &Pablo;
cout << "Pablo ";
pe->imprime_cargo();
Empleado *pe;
cout << "\ny ahora con punteros:" << endl;
pe = &Rafa;
cout << "Rafa ";
pe->imprime_cargo();
pe = &Mario;
cout << "Mario ";
// continúa a la dcha.
}
Escuela Superior de Ingenieros de San Sebastián - Tecnun
16
Ejemplo 2
// Fichero empleos.h
#include <iostream.h>
class empleado {
protected :
char nombre[25];
long salario;
public:
empleado(char*, long);
virtual void display() {cout << "Nada\n";}
};
Empleado
Operario
Oficial
class directivo : public empleado {
private:
char titulo[25];
public:
directivo(char*, long, char*);
void display() {
cout << " El directivo " << nombre << " gana " << salario
<< " y tiene el titulo de " << titulo << endl;
}
};
Directivo
Tecnico
class operario : public empleado {
protected:
char puesto[25];
public:
operario(char*, long, char*);
void display() {
cout << " El operario " << nombre << " gana " << salario
<< " y ocupa un puesto de " << puesto << endl;
}
};
Escuela Superior de Ingenieros de San Sebastián - Tecnun
17
Ejemplo 2 (cont.)
class tecnico : public operario {
public:
tecnico(char*, long, char*);
void display() {
cout << " El tecnico " << nombre
<< " gana " << salario
<< " y ocupa un puesto de "
<< puesto << endl;
}
};
Empleado
Operario
Oficial
Directivo
Tecnico
class oficial : public operario {
public:
oficial(char*, long, char*);
void display() {
cout << " El oficial " << nombre
<< " gana " << salario
<< " y ocupa un puesto de "
<< puesto << endl;
}
};
Escuela Superior de Ingenieros de San Sebastián - Tecnun
18
Ejemplo 2 (cont.)
// Fichero Empleos.cpp
// funciones miembro de clases Directivo y Operario
#include "empleos.h"
#include <iostream.h>
#include <string.h>
Empleado
Operario
// definición de los constructores
empleado::empleado(char* name = "", long sueldo = 0)
: salario(sueldo) {
strcpy(nombre, name);
}
Oficial
Directivo
Tecnico
directivo::directivo(char* name = "", long sueldo = 0,
char* title = "") : empleado(name, sueldo) {
strcpy(titulo, title);
}
operario::operario(char* name = "", long sueldo = 0,
char* job = "") : empleado(name, sueldo) {
strcpy(puesto, job);
}
tecnico::tecnico(char* name = "", long sueldo = 0,
char* job ="") : operario(name, sueldo, job)
{ ; }
oficial::oficial(char* name = "", long sueldo = 0,
char* job ="") : operario(name, sueldo, job)
{ ; }
Escuela Superior de Ingenieros de San Sebastián - Tecnun
19
Ejemplo 2 (cont.)
// fichero empresa.cpp
// utilización de la clase empleos
#include <iostream.h>
#include "empleos.h"
void main(void)
{
// punteros a objetos de distintas clases
directivo *m;
operario *j;
oficial
*k;
tecnico
*i;
Empleado
Operario
// creación de cuatro objetos de distintas clases
m = new directivo("Maria", 260000, "Ingeniera");
j = new operario("Juan",
160000, "Soldador");
k = new oficial("Koldo",
180000, "Teniente");
i = new tecnico("Ignacio", 250000, "Ingeniero");
// vector de punteros a la clase base
empleado *lista[8];
// se guardan direcciones de objetos de
//
distintas clases
lista[0] = m;
lista[1] = j;
lista[2] =
lista[4] = new directivo("PEDRO", 2500000,
lista[5] = new operario("Leire", 25000,
lista[6] = new oficial("Jon",
1500000,
lista[7] = new tecnico("Mario",
1600000,
Oficial
Directivo
Tecnico
k; lista[3] = i;
"Economista");
"Becaria");
"Entrenador");
"Electricista");
// Se tratan todos los objetos de las distintas clases de una forma unificada
for (int ii=0; ii<8; ii++)
lista[ii]->display();
cout << "Ya he terminado." << endl;
}
Escuela Superior de Ingenieros de San Sebastián - Tecnun
20
Descargar