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).
[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.