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