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]


4.11.2b1   Punteros en jerarquías de clases

§1  Sinopsis

Consideremos el caso de la clase general Polígono del epígrafe anterior ( 4.11.2b). Expandiendo la subclase de los triángulos tendríamos una jerarquía de clases como en la figura:

Es evidente que todos los isósceles son triángulos y que todos los triángulos son polígonos. Esto significa que un puntero a tipo polígono (Poligono*) puede señalar a un objeto triángulo (1) y a un objeto isósceles (2). Del mismo modo, un puntero a triángulo (Triangulo*) puede señalar a un objeto isósceles (3). Es decir:

Poligono Po;

Poligono* ptrP = &Po;

Triangulo PoTr;

Triangulo* ptrT = &PoTr;

Isosceles PoTrIs;

Isosceles* ptrI = &PoTrIs;

pero también:

ptrT = &PoTrIs;    // 3 correcto todo Isósceles es un triángulo

ptrP = &PoTrIs;    // 2 correcto todo Isósceles es un polígono

ptrP = &PoTr;      // 1 correcto todo triángulo es un polígono

Observe que en los tres casos, se están utilizando punteros a una superclase para designar objetos de la clase derivada. O dicho de otro modo: la dirección de un objeto de la subclase se expresa mediante un puntero de tipo puntero-a-superclase. Esta posibilidad, conocida como "upcast", es tremendamente útil en determinadas circunstancias [3].


Tenga en cuenta que el razonamiento inverso no es necesariamente cierto. Es decir, un polígono no es necesariamente triángulo y un triángulo no es necesariamente isósceles; en otras palabras: un puntero a isósceles no puede utilizarse indiscriminadamente para señalar a un triángulo ni a un polígono:

ptrI = &PoTr;      // Error: un triángulo puede no ser isósceles

ptrI = &Po;        // Error: un polígono puede no ser isósceles

ptrT = &Po;        // Error: un polígono puede no ser un triángulo

§2  Teorema

Estas consideraciones pueden generalizarse en el siguiente enunciado: si una subclase S tiene una clase-base pública B, entonces S* puede ser asignada a una variable de tipo B* sin ninguna conversión explícita de tipo (esto es lo que expresan las sentencias 3 y 1 anteriores). Lo inverso no es cierto; en estos casos se hace necesaria una conversión explícita de tipo ( 4.9.9).

  Lo anterior puede resumirse en los siguientes axiomas (que son equivalentes):

  • Los objetos de las clases derivadas pueden tratarse como si fuesen objetos de sus clases-base cuando se manipulan mediante punteros y referencias.

  • Un puntero de una clase-base puede contener direcciones de objetos de cualquiera de sus clases derivadas. Ejemplo [1]:

class B { .... };

class D : public B { ... };

...

func () {

  B* bptr = new D;  // puntero a superclase asignado a objeto de subclase

  ...

  delete bptr;

}

Nota: el hecho de que el puntero a una superclase pueda ser utilizado como puntero a objeto de cualquier subclase de su jerarquía (una especie de "puntero genérico" para los objetos de la familia), se cumple también en otros lenguajes como Eiffel o Java. Como estos lenguajes proporcionan una superclase de la que derivan todas las demás (en Java es la clase Object), resulta que un puntero a esta superclase Java equivale funcionalmente al papel del puntero void* en C++ ( 4.2.1d).


Observará el lector que los razonamientos anteriores §1 : "Todo isósceles es un triángulo" y "un triángulo puede no ser isósceles", son desde luego válidos en geometría, pero no necesariamente en las jerarquías de clases. Sobre todo el primero de tales razonamientos podría no ser cierto en algún caso concreto, ya que el programador es totalmente libre para definirla. Sería posible diseñar una jerarquía "enloquecida" [4], en la que no se cumpliesen estas premisas lógicas. Como en cualquier caso el compilador garantizará la validez del teorema anterior, la congruencia y la lógica aconsejan que en el diseño de jerarquías de clases se cumpla el denominado principio de sustitución de Liskov o LSP ("Liskov Substitution Principle"), según el cual las clases se deben diseñar de forma que cualquier clase derivada sea aceptable donde lo sea su superclase.

§3  Acceso a través de punteros

Cuando se tienen objetos de clases aisladas (que no derivan de ninguna otra), el acceso mediante punteros a dichas clases, no presenta dificultad alguna ( 4.2.1f), basta utilizar el selector indirecto -> ( 4.9.16). Ejemplo:

class C { public:  int x; }

...

C c;              // Objeto (instancia) de la clase

C* ptr = &c;      // puntero a clase

ptr->x = 10;      // Ok. acceso a miembro del objeto: c.x == 10


Sin embargo, cuando las clases pertenecen a una jerarquía, su acceso mediante punteros puede convertirse en una pesadilla si no se conoce íntimamente como se comportan frente a los espacios de nombres implícitos en tales clases ( 4.11.2b). Considere detenidamente el siguiente ejemplo:

#include <iostream.h>

class B {                // Superclase (raíz)
  public: int f(int i) { cout << "Funcion-Superclase "; return i; }
};
class D : public B {     // Subclase (derivada)
  public: int f(int i) { cout << "Funcion-Subclase "; return i+1; }
};

int main() {             // ==========
  D d;                   // instancia de subclase
  D* dptr = &d;          // puntero-a-subclase señalando objeto
  B* bptr = dptr;        // puntero-a-superclase señalando objeto de subclase

  cout << "d.f(1)     ";
  cout << d.f(1) << endl;       // acceso directo al método (que es público)
  cout << "dptr->f(1) ";
  cout << dptr->f(1) << endl;   // acceso a través del puntero
  cout << "bptr->f(1) ";
  cout << bptr->f(1) << endl;   // idem.
}

Salida:

d.f(1)     Funcion-Subclase 2
dptr->f(1) Funcion-Subclase 2
bptr->f(1) Funcion-Superclase 1

Comentario

Vemos que en la primera y segunda salida las cosas ocurren como de costumbre, ya sea utilizando el operador de acceso directo o el indirecto (mediante puntero). En ambos casos, la nueva definición de f en la clase derivada, oculta la definición en la superclase. La sorpresa ocurre en la tercera salida, donde a pesar de que el puntero señala al mismo (y único objeto) d, se accede directamente al subespacio B del objeto, con lo que la versión f utilizada es la existente en el mismo; la heredada de la superclase [2].

Este último resultado podría obtenerse también mediante:

cout << d.B::f(1) << endl;

  Así pues, como corolario de lo anterior, tenga en cuenta que (en condiciones normales *), el acceso a objetos d de subclases D mediante punteros a superclases B*, lleva implícita la referencia el subespacio B existente en el objeto d.

* Al tratar de las funciones virtuales ( 4.11.8a), veremos que la "sorpresa" anterior puede evitarse con una pequeñísima modificación en la definición del método f de la superclase (declarándolo virtual).

  Inicio.


[1]  En estos casos es muy importante tener en cuenta las indicaciones al respecto en Destructores virtuales ( 4.11.2d2).

[2]  La mayoría de los textos se refieren a los elementos del subespacio B como "miembros de la superclase".  Esta terminología es poco precisa y puede inducir a confusión, ya que todos los miembros pertenecen al objeto d, y este es una instancia de la subclase. La confusión semántica es todavía mayor si se instancia un objeto b de la superclase ( B b; ) coexistiendo con el anterior.

[3]  En realidad el control de tipos realizado por el compilador podría haber sido mucho más estricto, no permitiendo que los objetos de subclases pudiesen ser señalados mediante punteros a sus superclases. Sin embargo la seguridad se relajó aquí intencionadamente para permitir ciertos artificios usados con las clases polimórficas.

[4]  Un caso similar se presenta en los casos de sobrecarga de operadores ( 4.9.18), donde son teóricamente posibles comportamientos sobrecargados que no mantengan la más mínima homogeneidad conceptual con las versiones globales.