Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


Sig.

4.11.2b  Herencia simple (I)

§1  Sinopsis

A continuación exponemos otra alternativa a la definición de clases cuando no se parte desde cero ( 4.11.2a), sino de una clase previamente definida (clase-base o superclase). Esta forma es conocida como herencia simple; cuando una clase deriva de una antecesora heredando todos sus miembros (la herencia múltiple es tratada en 4.11.2c).

Fig. 1

Nota: las uniones ( 4.6) no pueden tener clases-base ni pueden utilizarse como tales.


Es clásico señalar como ejemplo, que la clase Triángulo deriva de la clase general Polígono, de la que también derivan las clases Cuadrado, Círculo, Pentágono, etc. (ver figura) [1].  Cualquier tipo de polígono comparte una serie de propiedades generales con el resto, aunque los triángulos tienen particularidades específicas distintas de los cuadrados, estos de los pentágonos y de los círculos, etc. Es decir, unas propiedades (comunes) son heredadas, mientras que otras (privativas) son específicas de cada descendiente.

Puesto que una clase derivada puede servir a su vez como base de una nueva herencia, se utilizan los términos base directa para designar la clase que es directamente antecesora de otra, y base indirecta para designar a la que es antecesora de una antecesora.  En nuestro caso, la clase Poligono es base directa de Triángulo, y base indirecta de Isósceles.

Nota: las denominaciones superclase directa y superclase indirecta tienen respectivamente el mismo significado que base directa e indirecta.

§2  Sintaxis

Cuando se declara una clase D derivada de otra clase-base B, se utiliza la siguiente sintaxis:

class-key nomb-clase : <mod-acceso> clase-base {<lista-miembros>};

  <mod-acceso> es un especificador opcional denominado modificador de acceso, que determina como será la accesibilidad de los miembros que se heredan de la clase-base en los objetos de las subclases ( 4.11.2b-II).

El significado del resto de miembros es el que se indicó al tratar de la declaración de una clase ( 4.11.2), con la particularidad que, en la herencia simple, <: lista-base> se reduce a un solo identificador: clase-base.

Ejemplo

class Circulo : public Poligono { <lista-miembros> };

En este caso, la nueva clase Circulo hereda todos los miembros de la clase antecesora Poligono (con las excepciones que se indican a continuación), pero debido al modificador de acceso utilizado (public), solo son utilizables los miembros que derivan de públicos y protegidos de la superclase Poligono (las cuestiones relativas al acceso se detallan en la página siguiente Pág. 2).

En este supuesto, resulta evidente que la hipotética función mover (en el plano) de la clase círculo será distinta de la misma función en la clase triángulo, aunque ambas desciendan de la misma clase polígono. En el primer caso sería suficiente definir las nuevas coordenadas del centro, mientras que en el segundo habría que definir las nuevas coordenadas de, al menos, dos puntos.

§3  Excepciones en la herencia

En principio, una clase derivada hereda todos los miembros (propiedades y métodos) de la clase base [2], con las excepciones que siguen de elementos que no pueden heredarse:

§4  Razón de la herencia

Como se ha indicado ( 4.11.1), derivar una nueva clase de otra existente, solo tiene sentido si se modifica en algo su comportamiento y/o su interfaz, y esto se consigue de tres formas no excluyentes:

  • Añadiendo miembros (propiedades y/o métodos), que no existían en la clase base. Estos nuevos miembros serían privativos de la clase derivada.

  • Sobrescribiendo métodos con distintos comportamientos que en la clase primitiva (sobrecarga).

  • Redefiniendo propiedades que ya existían en la clase base con el mismo nombre. En este caso, se crea un nuevo tipo de variable en la clase derivada con el mismo nombre que la anterior, con el resultado de que coexisten dos propiedades distintas del mismo nombre, la nueva y la heredada.

Nota: una nueva clase no puede eliminar ningún miembro de sus ancestros. Es decir, no puede realizarse una herencia selectiva de determinados miembros. Como se ha indicado se heredan todos; otra cosa es que resulten accesibles u ocultos. Solo en circunstancias muy particulares de herencia múltiple puede evitarse que se repitan miembros de clases antecesoras ( 4.11.2c1 Herencia virtual).

Fig. 2


§4.1  Al hilo de estas consideraciones, es importante resaltar que en una clase derivada existen dos tipos de miembros (utilizaremos repetidamente esta terminología):

  • Heredados:  que existen en la clase derivada simplemente por herencia de la superclase. Este conjunto de miembros, que existen por herencia de la superclase, es denominado por Stroustrup subobjeto de la superclase en el objeto de la clase derivada [5]

  • Privativos:  que existen en la clase derivada porque se han añadido (no existen en la superclase) o redefinido (lo que significa a la postre que existen dos versiones con el mismo nombre; una heredada y otra privativa).

Cuando varias clases son "hermanas" (derivan de una misma superclase) los miembros heredados son comunes a todas ellas, por lo que suelen utilizarse indistintamente ambos términos "heredados" y "comunes" para referirse a tales miembros.  La situación se ha esquematizado en la figura 2 en la que dos subclases D1 y D2 derivan de un mismo ancestro B. Cada una tiene sus propios miembros privativos y una copia de los miembros de su ancestro (esta parte es igual en ambas clases).

§4.2  Ejemplo

En este ejemplo puede comprobarse que cada instancia de la clase derivada tiene su propio juego de variables, tanto las privativas como las heredadas; también que unas y otras se direccionan del mismo modo.

#include <iostream.h>
class B {              // clase raíz
  public: int x;
};
class D : public B {   // D deriva de B
  public: int y;       // y es privativa de la clase D
};

void main() {          // ================================
  B b1;                // b1 es instancia de B (clase raíz)
  b1.x =1;
  cout << "b1.x = "  << b1.x  << endl;

  D d1;                // d1 es instancia de D (clase derivada)
  d1.x = 2;            // este elemento es herdado de la clase padre
  d1.y = 3;            // este elemento es privativo de d1
  D d2;                // otra instancia de D
  d2.x = 4;
  d2.y = 5;
  cout << "d1.x = " << d1.x << endl;
  cout << "d1.y = " << d1.y << endl;
  cout << "d2.x = " << d2.x << endl;
  cout << "d2.y = " << d2.y << endl;
}

Salida:

b1.x = 1
d1.x = 2
d1.y = 3
d2.x = 4
d2.y = 5

§5  Ocultación de miembros

Cuando se redefine un miembro heredado (que ya existe con el mismo nombre en la clase base) el original queda parcialmente oculto o eclipsado para los miembros de la clase derivada y para sus posibles descendientes. Esta ocultación se debe a que los miembros privativos dominan sobre los miembros del subobjeto heredado ( 4.11.2c). Tenga en cuenta que los miembros de las clases antecesoras que hayan sido redefinidos son heredados con sus modificaciones.

§5.1  Ejemplo

#include <iostream.h>
#include <typeinfo.h>

class X { public: int x; };              // Clase raíz
class Y : public X { public: char x; };  // hija
class Z : public Y { };                  // nieta

void main() {       // =============
   X mX;            // instancias de raíz, hija y nieta
   Y mY;
   Z mZ;
   cout << "mX.x es tipo: " << typeid(mX.x).name() << endl;
   cout << "mY.x es tipo: " << typeid(mY.x).name() << endl;
   cout << "mZ.x es tipo: " << typeid(mZ.x).name() << endl;
}

Salida:

mX.x es tipo: int
mY.x es tipo: char
mZ.x es tipo: char

5.2  Cuando los miembros redefinidos son funciones

Los miembros de las clases antecesoras que pueden ser redefinidos en las clases derivadas no solo pueden ser propiedades (como en el ejemplo anterior), también pueden ser métodos. Considere el siguiente ejemplo [4]:

#include <iostream.h>


class Cbase {

  public:

    int funcion(void) { return 1; }

};

class Cderivada : public Cbase {

  public:

    int funcion(void) { return 2; }

};

 

int main(void) {         // =================

  Cbase clasebase;

  Cderivada clasederivada;

  cout << "Base: " << clasebase.funcion() << endl;

  cout << "Derivada: " << clasederivada.funcion() << endl;

  cout << "Derivada-bis: " << clasederivada.Cbase::funcion() << endl; // M.5

}

Salida:

Base: 1
Derivada: 2

Derivada-bis: 1


En este caso, la clase derivada redefine el método funcion heredado de su ancestro, con lo que en Cderivada existen dos versiones de esta función; una es privativa, la otra es heredada (). Cuando se solicita al objeto clasederivada que ejecute el método funcion, invoca a la versión privativa; entonces se dice que la versión privativa oculta ("override") a la heredada. Para que sea invocada la versión heredada (tercera salida), es preciso indicarlo expresamente. Esto se realiza en la última línea de main (M.5). La técnica se denomina sobrecontrol de ámbito y es explicada con más detenimiento en el siguiente epígrafe.

Sin embargo, la conclusión más importante a resaltar aquí, es que las dos versiones de funcion existentes en la clase derivada, no representan un caso de sobrecarga (no se dan las condiciones exigidas 4.4.1a y ) ni de polimorfismo, ya que la función no ha sido declarada como virtual ( 4.11.8a) en la superclase. Se trata sencillamente que ambas funciones pertenecen a subespacios distintos dentro de la subclase.

Nota: la afirmación anterior puede extrañar al lector, ya que en capítulos anteriores ( 4.1.11c1) hemos señalado que no es posible definir subespacios dentro de las clases. Por supuesto la afirmación es cierta en lo que respecta al usuario (programador), pero no para el compilador, que realiza automáticamente esta división para poder distinguir ambas clases de miembros (privativos y heredados).


En el capítulo dedicado al acceso a subespacios en clases, hemos visto un método (declaración using), por el que puede evitarse la ocultación de la versión heredada, lo que sitúa a ambas versiones (privativas y heredadas) en idéntica situación de visibilidad, y conduce a una situación de sobrecarga ( 4.1.11c1)

§6  Sobrecontrol de ámbito

Hemos señalado que cuando se redefine un miembro que ya existe la clase base, el original queda oculto para los miembros de la clase derivada y para sus posibles descendientes. Pero esto no significa que el miembro heredado no exista en su forma original en la clase derivada [3]. Está oculto por el nuevo, pero en caso necesario puede ser accedido sobrecontrolando el ámbito como se muestra en el siguiente ejemplo.

#include <iostream.h>

class B { public: int x; };   // B clase raíz
class D : public B {          // D clase derivada
  public: char x;             // redefinimos x
};

int main (void) {             // ========================
  B b0;                       // instancia de B
  b0.x = 10;                  // Ok: x público (int)
    cout << "b0.x == " << b0.x << endl;
  D d1;                       // instancia de D
  d1.x = 'c';                 // Ok: x público (char)
  d1.B::x = 13;               // sobrecontrolamos el ámbito !!
    cout << "Valores actuales: " << endl;
    cout << " b0.x == " << b0.x << endl;
    cout << " d1.x == " << d1.x << endl;
    cout << " d1.x == " << d1.B::x << " (oculto)" << endl;  // M.10
}

Salida:

b0.x == 10
Valores actuales:
 b0.x == 10
 d1.x == c
 d1.x == 13 (oculto)

Comentario

Merece especial atención observar que la notación d1.B::x (M.10) representa al elemento x en d1, tal como es heredado de la case antecesora B. Esta variación sintáctica, permite en algunos casos referenciar miembros que de otra forma permanecerían ocultos.

Resulta que los miembros de la clase derivada del ejemplo se reparten en dos ámbitos de nombres, el primero D (inmediatamente accesible), contiene los miembros privativos de la clase derivada, por ejemplo d1.x. El segundo B, que contiene los miembros heredados de la superclase, es accesible solo mediante el operador de acceso a ámbito ::  ( 4.9.19). Por ejemplo: d1.B::x. En el capítulo dedicado a las clases-base virtuales ( 4.11.2c1) se muestra otro ejemplo muy ilustrativo.

Una representación gráfica idealizada de la situación para la clase derivada D sería la siguiente:

namespace D {

   char x

   namespace B {

      int x

   }

}

Cuando desde el exterior se utiliza un identificador como d1.x; los nombres exteriores ocultan los posibles identificadores interiores de igual nombre. Es decir: char x oculta a int x.

§6.1  Cuando el sobrecontrol NO es necesario

Tenga muy en cuenta que las indicaciones anteriores relativas a ocultación y sobrecontrol se refieren exclusivamente al caso en que la redefinición de una propiedad o método de la superclase, motive la existencia de una versión privativa de la subclase con el mismo nombre que otra de la superclase. En caso contrario, los miembros heredados (públicos) son libremente accesibles sin necesidad de ningún mecanismo de acceso a ámbito.

Considere la siguiente variación del ejemplo anterior:

#include <iostream.h>
class B { public: int x; };   // B clase raíz
class D : public B {          // D clase derivada
  public: int y;              // exclusiva (no existe en B)
};

int main (void) {             // ========================
  B b0;                        // instancia de B
  b0.x = 10;                  // Ok: x público (int)
  cout << "b0.x == " << b0.x << endl;
  D d1;                        // instancia de D
  d1.x = 2;                   // No precisa sobrecontrol!!
  d1.y = 20;                  // Ok: y público (int)
  cout << "Valores actuales: " << endl;
  cout << " b0.x == " << b0.x << endl;
  cout << " d1.x == " << d1.x << " (NO oculto)" << endl;
  cout << " d1.y == " << d1.y << endl;
}

Salida:

b0.x == 10
Valores actuales:
 b0.x == 10
 d1.x == 2 (NO oculto)
 d1.y == 20


§6.2  La misma técnica de sobrecontrol de ámbito anteriormente descrita , puede utilizarse para acceder a los miembros heredados de los antepasados más lejanos de una clase. Utilizando un símil biológico, podríamos decir que la totalidad de la carga genética de los ancestros está contenida en la instancia de cualquier clase que sea resultado de una derivación múltiple. Lo pondremos de manifiesto extendiendo el ejemplo anterior con una nueva derivación de la clase D que es ahora base de E, siendo B el ancestro más remoto de E.

Ejemplo

#include <iostream.h>
class B { public: int x; };               // clase raíz
class D : public B { public: char x; };   // clase derivada
class E : public D { public: float x; };  // clase derivada

int main (void) {                // ========================
  E e1;                           // instancia de E
  e1.x = 3.14;                   // Ok: x público (foat)
  e1.D::x = 'd';                 // Ok: x público (char)
  e1.B::x = 15;                  // Ok: x público (int)
  cout << "Valores en e1: " << endl;
  cout << " e1.x == " << e1.x << endl;
  cout << " e1.x == " << e1.D::x << " (oculto)" << endl;
  cout << " e1.x == " << e1.B::x << " (oculto)" << endl;
}

Salida:

Valores en e1:
 e1.x == 3.14
 e1.x == d (oculto)
 e1.x == 15 (oculto)

  Temas relacionados:
  • Acceso a subespacios en clases ( 4.1.11c1)

  • El ámbito de nombres y la sobrecarga de funciones ( 4.4.1a)

  • Acceso a través de punteros en jerarquías de clases  ( 4.11.2b1). En este apartado se comprueba como el sobrecontrol de ámbito es innecesario (está implícito) en algunos casos de acceso mediante punteros.

  Inicio.


[1]  Es tradición que la representación gráfica de la herencia se represente por flechas que van desde la subclase a la superclase, y que adopte la forma de un árbol invertido, con las clases-base en la parte superior y las derivadas hacia abajo.

[2]  El hecho de que estos miembros sean o no accesibles es otra cuestión.

[3]  Se heredan todos los miembros de la clase base (menos las excepciones señaladas ), pero recuerde lo indicado respecto a las diferencias entre ámbito y visibilidad en C++ ( 4.1.4).

[4]  El ejemplo está inspirado en la consulta de un lector con el siguiente texto:  "Escribo el siguiente código y el compilador no genera error, todo funciona como si definiera las funciones virtuales" (sigue el código que se muestra en el ejemplo -excepto la última línea de main M.5-).

A continuación añade:  "He leído que las funciones virtuales se definen para decirle al compilador que habrá una nueva versión de la misma en una, o todas las clases derivadas, pero yo la redefino sin declararla virtual y no me presenta problemas.  Al instanciar los objetos todo funciona perfecto":

Cbase clasebase;
clasebase.funcion()       // me devuelve 1
Cderivada clasederivada;
clasederivada.funcion()   // me devuelve 2

[5]   [TC++PL-00] §4.7

Sig.