Professional Documents
Culture Documents
La funcin put, definida tres veces con el mismo nombre est sobrecargada; el compilador sabe
en cada caso qu definicin usar por la naturaleza de los argumentos (puntero-a-char, long o int).
Nota: no confundir estos casos de sobrecarga en mtodos con el polimorfismo; este ltimo
se refiere a jerarquas de clases en las que pueden existir diversas definiciones de mtodos en
miembros de distintas generaciones (super-clases y clases derivadas) con los mismos
argumentos. Son las denominadas funciones virtuales (
4.11.8a).
// L.1:
Es evidente que en L.4 y L.5 los argumentos formales y los actuales concuerdan, bien porque se
trata del mismo tipo, bien porque se ha realizado una promocin explcita (
4.9.9).
Lo que no resulta ya tan evidente, ni deseable a veces, es que en L.6 el compilador autorice la
invocacin a funcion pasando un tipo distinto del esperado. Esta promocin del argumento actual
realizada de forma automtica por el compilador (sin que medie ningn error o aviso -"Warning"- de
compilacin) puede ser origen de errores, y de que el programa use inadvertidamente una funcin
distinta de la deseada. En este sentido podemos afirmar que el lenguaje C++ es dbilmente tipado.
Posiblemente esta caracterstica sea una de las desafortunadas herencias del C clsico [1], y es
origen incluso de ambigedades que no deberan existir. Por ejemplo, no es posible definir las
siguientes versiones de un constructor (
4.11.2d):
class Entero {
public: int x;
Entero(float fl = 1.0) { x = int(fl); }
Entero(char c) { x = int(c); }
}
A pesar de su evidente disparidad, el compilador interpreta que existe ambigedad en los tipos, ya
que char puede ser provomido a float.
// L.5
La invocacin a getx en L.15 es correcta, a pesar de no existir una definicin concordante para
esta funcin cuyos argumentos formales son dos objetos tipo C, y en este caso el primer
argumento x es un int. El mecanismo utilizado por el compilador es el siguiente:
Dentro del mbito de visibilidad existe una funcin fun (L.6) que acepta un float y devuelve un
objeto del tipo necesitado por getx. Aunque el argumento x disponible no es precisamente
un float, puede ser promovido fcilmente a dicho tipo. En consecuencia el compilador utiliza en
L.15 la siguiente invocacin:
int z = getx( fun( float(x) ), c1);
Este tipo de adecuaciones automticas son realizadas por el compilador tanto con funcionesmiembro [2] como con funciones normales. Considere cuidadosamente el ejemplo que sigue, e
intente justificar el proceso seguido para obtener cada uno de sus resultados.
#include <iostream>
using namespace std;
int x = 10;
long lg = 10.0;
class Entero {
public:
int x;
int getx (int i) { return x * i; }
int getx () { return x; }
int getx (Entero e1, Entero e2) { return e1.x + e2.x; }
Entero(float f= 1.0) { x = f; }
};
int getx (int i) { return x * i; }
int getx () { return x; }
int getx (Entero e1, Entero e2) { return e1.getx() + e2.getx(); }
Entero fun (float f= 1.0) {
Entero e1 = { f }; return e1;
}
void main () {
// ==========================
Entero c1 = Entero(x);
cout << "E1 = " << c1.getx(0) << endl;
cout << "E2 = " << c1.getx() << endl;
cout << "E3 = " << c1.getx('a') << endl;
cout << "E4 = " << c1.getx(lg) << endl;
cout << "E5 = " << c1.getx(x, c1) << endl;
cout << "E6 = " << c1.getx('a', c1) << endl;
cout << "F1 = " << getx(0) << endl;
cout << "F2 = " << getx() << endl;
cout << "F3 = " << getx('a') << endl;
cout << "F4 = " << getx(lg) << endl;
cout << "F5 = " << getx(x, c1) << endl;
cout << "F6 = " << getx('a', c1) << endl;
}
Salida (reformateada en dos columnas):
E1
E2
E3
E4
=
=
=
=
0
10
970
100
F1
F2
F3
F4
=
=
=
=
0
10
970
100
E5 = 20
E6 = 107
F5 = 20
F6 = 107
Temas relacionados
Sobrecarga de funciones (
4.4.1a)
Sobrecarga de funciones genricas (plantillas) 4.12.1-2
2 La funcin contenedora no tiene especial acceso al mbito de la clase local (salvo el dictado
por las reglas generales):
void foo() {
...
class C {
int x;
public:
char c;
};
C c;
c.x = 10;
c.c = 'X';
}
// Funcin contenedora
// clase local a foo
// privado por defecto
El error puede ser eliminado con un pequeo cambio que permita la visibilidad de foo en el interior
de la clase:
void foo() {
...
class C {
friend void foo();
int x;
public:
char c;
};
// se permite el acceso
C c;
c.x = 10;
c.c = 'X';
4.11.7) y
void foo() {
static int counter;
enum { KT1 = 33 };
int y = 2;
class C {
int x;
public:
C() {
x = KT1 + counter;
C(int n) {
x = n + y;
}
};
// Ok.
// Error!! y es automtica
4.11.7). Ejemplo:
// Ok.
// Error!!
// Ok.
// ERror!!
Tema relacionado
4.13.2)
conocida como herencia simple; cuando una clase deriva de una antecesora heredando todos sus
miembros (la herencia mltiple es
tratada en
4.11.2c).
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 declaracin 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 continuacin), pero debido al
modificador de acceso utilizado (public), solo son utilizables los miembros que derivan
3 Excepciones en la herencia
En principio, una clase derivada hereda todos los miembros (propiedades y mtodos) de la clase
base [2], con las excepciones que siguen de elementos que no pueden heredarse:
Constructores (
4.11.2d1)
Destructores ( 4.11.2d2)
Miembros estticos ( 4.11.7).
Operador de asignacin = sobrecargado (
Funciones friend ( 4.11.2a1).
4.9.18a)
4 Razn 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:
Aadiendo miembros (propiedades y/o mtodos), que no existan en la clase base. Estos
nuevos miembros seran privativos de la clase derivada.
Sobrescribiendo mtodos con distintos comportamientos que en la clase primitiva
(sobrecarga).
Redefiniendo propiedades que ya existan 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 ningn 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 mltiple puede evitarse que se repitan miembros de clases
antecesoras (
4.11.2c1 Herencia virtual).
4.1
Al hilo de estas consideraciones, es importante
resaltar que en una clase derivada existen dos tipos de
miembros (utilizaremos repetidamente esta terminologa):
Fig. 2
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; tambin que unas y otras se direccionan del
mismo modo.
#include <iostream.h>
class B {
public: int x;
};
class D : public B {
public: int y;
};
void main() {
B b1;
b1.x =1;
cout << "b1.x = "
// clase raz
// D deriva de B
// y es privativa de la clase D
// ================================
// b1 es instancia de B (clase raz)
<< b1.x
D d1;
d1.x = 2;
d1.y = 3;
D d2;
d2.x = 4;
d2.y = 5;
cout << "d1.x = " <<
cout << "d1.y = " <<
cout << "d2.x = " <<
//
//
//
//
<< endl;
=
=
=
=
=
1
2
3
4
5
5 Ocultacin 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 ocultacin 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; };
class Y : public X { public: char x; };
class Z : public Y { };
// Clase raz
// hija
// nieta
void main() {
// =============
X mX;
// instancias de raz, 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
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 mtodo funcion heredado de su ancestro, con lo que
en Cderivada existen dos versiones de esta funcin; una es privativa, la otra es heredada ( ).
Cuando se solicita al objeto clasederivada que ejecute el mtodo funcion, invoca a la versin
privativa; entonces se dice que la versin privativa oculta ("override") a la heredada. Para que sea
invocada la versin heredada (tercera salida), es preciso indicarlo expresamente. Esto se realiza
en la ltima lnea de main (M.5). La tcnica se denomina sobrecontrol de mbito y es explicada
con ms detenimiento en el siguiente epgrafe.
Sin embargo, la conclusin ms 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 funcin 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 afirmacin anterior puede extraar al lector, ya que en captulos anteriores (
4.1.11c1) hemos sealado que no es posible definir subespacios dentro de las clases. Por
supuesto la afirmacin es cierta en lo que respecta al usuario (programador), pero no para el
compilador, que realiza automticamente esta divisin para poder distinguir ambas clases de
miembros (privativos y heredados).
6 Sobrecontrol de mbito
Hemos sealado 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; };
class D : public B {
public: char x;
};
// B clase raz
// D clase derivada
// redefinimos 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.
// B clase raz
// D clase derivada
// exclusiva (no existe en B)
#include <iostream.h>
class B { public: int x; };
class D : public B { public: char x; };
class E : public D { public: float x; };
int main (void) {
E e1;
e1.x = 3.14;
e1.D::x = 'd';
e1.B::x = 15;
cout << "Valores en
cout << " e1.x == "
cout << " e1.x == "
cout << " e1.x == "
}
// clase raz
// clase derivada
// clase derivada
// ========================
// instancia de E
// Ok: x pblico (foat)
// Ok: x pblico (char)
// Ok: x pblico (int)
e1: " << endl;
<< e1.x << endl;
<< e1.D::x << " (oculto)" << endl;
<< e1.B::x << " (oculto)" << endl;
Salida:
Valores en e1:
e1.x == 3.14
e1.x == d (oculto)
e1.x == 15 (oculto)
Temas relacionados:
4.11.2b). Expandiendo
Observe que en los tres casos, se estn utilizando punteros a una superclase para designar
objetos de la clase derivada. O dicho de otro modo: la direccin 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].
2 Teorema
Estas consideraciones pueden generalizarse en el siguiente enunciado: si una subclase S tiene
una clase-base pblica B, entonces S* puede ser asignada a una variable de tipo B* sin ninguna
conversin explcita 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 conversin explcita 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 clasesbase 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 jerarqua (una especie de "puntero genrico" para los objetos de
la familia), se cumple tambin en otros lenguajes como Eiffel o Java. Como estos lenguajes
proporcionan una superclase de la que derivan todas las dems (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).
Sin embargo, cuando las clases pertenecen a una jerarqua, su acceso mediante punteros puede
convertirse en una pesadilla si no se conoce ntimamente como se comportan frente a los espacios
de nombres implcitos en tales clases (
4.11.2b). Considere detenidamente el siguienteejemplo:
#include <iostream.h>
class B {
// Superclase (raz)
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;
D* dptr = &d;
B* bptr = dptr;
subclase
cout << "d.f(1)
";
cout << d.f(1) << endl;
pblico)
//
//
//
//
==========
instancia de subclase
puntero-a-subclase sealando objeto
puntero-a-superclase sealando objeto de
cout
cout
cout
cout
<<
<<
<<
<<
"dptr->f(1) ";
dptr->f(1) << endl;
"bptr->f(1) ";
bptr->f(1) << endl;
}
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
definicin de f en la clase derivada, oculta la definicin en la superclase. La sorpresa ocurre en la
tercera salida, donde a pesar de que el puntero seala al mismo (y nico objeto) d, se accede
directamente al subespacio B del objeto, con lo que la versin f utilizada es la existente en el
mismo; la heredada de la superclase [2].
Este ltimo resultado podra obtenerse tambin 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 implcita 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 pequesima modificacin en la definicin del mtodo f de la superclase
(declarndolo virtual).
class D : public B {
// Subclase
public: float f(float f) { cout << "Funcion-Subclase "; return f+0.1; }
};
int main() {
D d;
D* dptr = &d;
B* bptr = dptr;
subclase
cout << "d.f(1)
cout << "d.f(1.1)
//
//
//
//
=================
instancia de subclase
puntero-a-subclase sealando objeto
puntero-a-superclase sealando objeto de
Funcion-Subclase 1.1
Funcion-Subclase 1.2
Funcion-Subclase 1.1
Funcion-Subclase 1.2
Funcion-Superclase 1
Funcion-Superclase 1
Comentario
Comprobamos que cualquiera que sea la forma de invocacin utilizada, en ningn caso se produce
sobrecarga de la funcin; siempre se accede a la misma versin, dependiendo del subespacio de
nombres B o D referenciado.
Nota: si el mecanismo de sobrecarga de funciones hubiese funcionado entre los subespacios de
nombres del objeto d, las salidas habran sido:
d.f(1)
d.f(1.1)
dptr->f(1)
dptr->f(1.1)
bptr->f(1)
bptr->f(1.1)
Funcion-Superclase 1
Funcion-Subclase 1.2
Funcion-Superclase 1
Funcion-Subclase 1.2
Funcion-Superclase 1
Funcion-Subclase 1.2
#include <iostream.h>
class B {
// Superclase
public: int f(int i) { cout << "Funcion-Superclase "; return i; }
};
class D : public B {
// Subclase
public:
using B::f;
// Ok. acceder a las versiones de f en B
float f(float f) { cout << "Funcion-Subclase "; return f+0.1; }
};
int main() {
D d;
D* dptr = &d;
B* bptr = dptr;
cout
cout
cout
cout
<<
<<
<<
<<
// =================
}
Salida [1]:
Funcion-Superclase 1
Funcion-Subclase 1.1
Funcion-Superclase 1
Funcion-Superclase 1
Comentario
En este caso comprobamos como la sobrecarga ha funcionado en el sentido D
B. Cuando
accedemos al espacio de nombres D, se invoca correctamente la versin adecuada de f, pero
cuando accedemos a B, el espacio D sigue siendo invisible para el mecanismo de sobrecarga.
La representacin grfica idealizada de la situacin para la clase derivada D sera la siguiente:
namespace D {
using B::f;
float f(float f);
namespace B {
int f(inf i);
}
}
Cuando se utiliza un identificador como d.f(), o su equivalente dptr->f(), se accede
directamente al subespacio D, aunque las versiones interiores de f son accesibles por la
declaracin using; pero cuando se accede directamente al subespacio B mediante expresiones
como bptr->f(), la versin de f en este subespacio sigue ocultando cualquier otro identificador
exterior.
Ver otro ejemplo en: "Acceso a subespacios en clases" (
4.1.11c1)
Inicio.
2 Sintaxis
Cuando se declara una clase D derivada de varias clases base: B1, B2, ... se utiliza una lista de
las bases directas (
4.11.2b) separadas por comas. La sintaxis general es:
class-key <info> nomb-clase <: lista-base> { <lista-miembros> };
El significado de cada miembro se indic al tratar de la declaracin de una clase (
este caso, la declaracin de D seria:
4.11.2). En
D hereda todos los miembros de las clases antecesoras B1, B2, etc, y solo puede utilizar los
miembros que derivan de pblicos y protegidos en dichas clases. Resulta as que un objeto de la
clase derivada contiene sub-objetos de cada una de las clases antecesoras.
2.1 Restricciones
// Ilegal!
B { ... };
C1 : public B { ... };
C2 : public B { ... };
D : public C1, C2 { ... };
Fig. 1
El mecanismo sucintamente descrito, constituye lo que se
denomina herencia mltiple ordinaria (o simplemente
herencia). Como se ha visto, tiene el inconveniente de que
si las clases antecesoras contienen elementos comunes,
estos se ven duplicados en los objetos de la subclase. Para
evitar estos problemas, existe una variante de la misma,
la herencia virtual (
4.11.2c1), en la que cada objeto de
la clase derivada no contiene todos los objetos de las
clases-base si estos estn duplicados.
Fig. 2
3 Ambigedades
La herencia mltiple puede originar situaciones de ambigedad cuando una subclase contiene
versiones duplicadas de sub-objetos de clases antecesoras o cuando clases antecesoras
contienen miembros del mismo nombre:
class B {
public:
int b;
int b0;
};
class C1 : public B {
public:
int b;
int c;
};
class C2 : public B {
public:
int b;
int c;
};
class D: public C1, C2 {
public:
D() {
c = 10;
// L1: Error
C1::c = 110;
// L2: Ok.
C2::c = 120;
// L3: Ok.
b = 12; Error!!
// L4: Error
C1::b = 11;
// L5: Ok.
C2::b = 12;
// L6: Ok.
C1::B::b = 10;
// L7: Error
B::b = 10;
// L8: Error
B
b0 = 0;
// L9: Error
C1::b0 = 1;
// L10: Ok.
C2::b0 = 2;
// L11: Ok.
}
};
// miembros m en C1 heredados de
// miembros m en C2 heredados de
// miembros n en C1 privativos
// miembros n en C2 privativos
3.1 Desde el punto de vista de la sintaxis de acceso, cualquier miembro m privativo de D (zona5) de un objeto d puede ser referenciado comod.m. Cualquier otro miembro del mismo nombre (m)
en alguno de los subobjetos queda eclipsado por este. Se dice que este identificador domina a los
dems [3].
Nota: este principio de dominancia funciona tambin en los subobjetos C1 y C2. Por ejemplo:
si un identificador n en el subobjeto C1 est duplicado en la parte privativa de C1 y en la parte
heredada de B, C1::n tienen preferencia sobre C1::B::n.
Es tambin el caso de las sentencias L4/L6. Observe que en este caso no existe ambigedad
respecto a los identificadores b heredados (zonas 1, y 2) porque los de las zonas 2 y 4 tienen
preferencia sobre los de las zonas 1 y 2.
b = 12; Error!!
C1::b = 11;
C2::b = 12;
Es interesante sealar que estos ltimos, los identificadores b de las zonas 1 y 2 (heredados de
B) no son accesibles porque siempre quedan ocultos por los miembros dominantes, y la gramtica
C++ no ofrece ninguna forma que permita hacerlo en la disposicin actual del ejemplo. Son los
intentos fallidos sealados en L7 y L8:
C1::B::b = 10;
B::b = 10;
El error de L8 se refiere a que existen dos posibles candidatos (zonas 1 y 2). Al tratar de la
herencia virtual (
4.11.2c1) veremos un mtodo de resolver (parcialmente) este problema.
Cuando no existe dominancia, los identificadores b0 de las zonas 1 y 2 si son visibles, aunque la
designacin directa no es posible porque existe ambigedad sobre la zona 1-2 a emplear. Es el
caso de las sentencias L9/L11:
b0 = 0;
C1::b0 = 1;
C2::b0 = 2;
4 Modificadores de acceso
Los modificadores de acceso en la lista-base pueden ser cualquiera de los sealados al
referirnos a la herencia simple (public, protected yprivate
4.11.2b-1), y pueden ser distintos
para cada uno de los ancestros. Ejemplo:
class D : public B1, private B2, ... { <lista-miembros> };
1 Sinopsis
Hemos sealado que en herencia mltiple, las clases
antecesoras no pueden repetirse:
class B { .... };
class D : B, B, ... { ... };
// Ilegal!
B { .... };
C1 : public B { ... };
C2 : public B { ... };
D : public C1, public C2 { ... };
//
B { .... };
C1 : virtual public B { ... };
C2 : virtual public B { ... };
D : public C1, public C2 { ... };
Fig. 2
// Ok.
2 virtual (palabra-clave)
virtual es una palabra-clave C++ que tiene dos acepciones completamente diferentes
dependiendo del contexto de su utilizacin. Utilizada con nombres de clase sirve para controlar
aspectos del mecanismo de herencia; utilizada con nombres de funciones-miembro, controla
aspectos del polimorfismo y del tipo de enlazado que se utiliza para tales funciones [1].
2.1 Sintaxis
La primera forma sintctica, cuando se antepone al nombre de una clase-base, como en el caso
anterior, declara una clase-base virtual, que da lugar a un mecanismo denominado herencia
virtual (en contraposicin con la herencia ordinaria), cuya descripcin abordamos en este epgrafe.
Ejemplo:
class D : virtual B { /* ... */ };
es equivalente a:
class D : private virtual B { /* ... */ };
Tenga en cuenta que en estos casos, el calificador virtual solo afecta al identificador que le sigue
inmediatamente. Ejemplo:
class E : public virtual B, C, D { /* ... */ };
En este caso las clases B, C y D son bases pblicas directas, pero solo B es virtual.
La segunda forma, cuando se aplica a la funcin miembro de una clase-base, define dicho
mtodo como funcin virtual, lo que permite que las clases derivadas puedan definir diferentes
versiones de la misma funcin-base (virtual) an en el caso de que coincidan los argumentos (no
se trate por tanto de un caso de sobrecarga), y les proporciona un mtodo especial de enlazado
(Enlazado Retrasado
1.4.4).
Las funciones virtuales redefinidas en las clases derivadas, solapan u ocultan a la versin
definida en la superclase, dndole a las clases a que pertenecen el carcter de polimrficas (
4.11.8). Todos los aspectos de este segundo uso, se detalla en el apartado correspondiente a
las Funciones virtuales (
4.11.8a). Las clases-base virtuales son clases perfectamente normales,
y nada impide que puedan definirse en ellas funciones virtuales.
3 Herencia virtual
Hemos sealado, que en una herencia mltiple ordinaria, en la que indirectamente se repite una
superclase (como en el ejemplo inicial
), los objetos de la clase derivada contienen mltiples
subobjetos de la superclase. Esta duplicidad puede evitarse declarando virtuales las superclases.
Como ejemplo de aplicacin se incluye un caso anlogo al estudiado en el captulo anterior (
4.11.2c) en el que se utiliza herencia virtual (en esta ocasin utilizamos una versin ejecutable
para comprobar las salidas).
#include <iostream>
using namespace std;
class B {
public:
int b;
int b0;
};
class C1 : public virtual B {
public:
int b;
int c;
};
class C2 : public virtual B {
public:
int b;
int c;
};
class D: public C1, C2 {
public:
D() {
c = 10;
// L1: Error
C1::c = 110;
// L2: Ok.
C2::c = 120;
// L3: Ok.
b = 12; Error!!
// L4: Error
C1::b = 11;
// L5: Ok.
C2::b = 12;
// L6: Ok.
C1::B::b = 10;
// L7: Error
B::b = 10;
// L8: Ok.!!
b0 = 0;
// L9: Ok.!!
C1::b0 = 1;
// L10: Ok.
C2::b0 = 1;
// L11: Ok.
}
};
int main() {
// ================
D d;
cout << "miembro b0: " << d.C1::b0 << endl; // M2:
++d.C2::b0;
cout << "miembro b0: " << d.C1::b0 << endl; // M4:
return 0;
}
Salida:
miembro b0: 1
miembro b0: 2
Comentario:
La figura 3 muestra la posible disposicin de los miembros
en los objetos de las clases utilizadas. Comprese con la
correspondiente figura del captulo anterior cuando la
herencia no es virtual (
Figura). El comportamiento y las
medidas adoptadas para evitar los errores son los ya
descritos. Con la diferencia de que aqu han desaparecido
los errores de las sentencias L8 y L9. La razn es la misma
en ambos casos: como se indica en la figura, los objetos de
la clase D solo tienen un subobjeto heredado de la
superclase B, por lo que no hay ambigedad al respecto.
Es significativo que a pesar de ser posible la asignacin
directa de L9. An son posibles las sentencias L10 y L11.
En realidad, las tres ltimas sentencias inicializan al mismo
miembro. Esto puede comprobarse en las sentencias de
salida M2 y M4, en las que se incrementa el
miembro b0 como subobjeto de C2 y se interroga como
subobjeto de C1.
3.1 Para mayor abundamiento, se aade otro ejemplo
que muestra como, en una herencia mltiple ordinaria, en Fig. 3
la que indirectamente se repite una superclase, los objetos
de la clase derivada contienen mltiples subobjetos de la superclase. Su existencia se pone de
manifiesto sobrecontrolando adecuadamente los identificadores (
4.11.2b):
#include <iostream.h>
class B {
//
public: int pub;
B() { pub = 0; }
//
};
class X : public B {};
//
class Y : public B {};
//
class Z : public X, public Y
int main ()
Z z1;
cout <<
cout <<
cout <<
z1.X::pub
z1.Y::pub
cout <<
cout <<
cout <<
}
{
"Valores
" z1.pub
" z1.pub
= 1;
= 2;
"Valores
" z1.pub
" z1.pub
clase raz
constructor por defecto
L.7
L.8
{};
// =====================
// instancia de Z
iniciales:" << endl;
(de X) == " << z1.X::pub << endl;
(de Y) == " << z1.Y::pub << endl;
// sobrecontrolamos el identificador
// L.17
finales:" << endl;
(de X) == " << z1.X::pub << endl;
(de Y) == " << z1.Y::pub << endl;
Salida:
Valores
z1.pub
z1.pub
Valores
z1.pub
z1.pub
iniciales:
(de X) == 0
(de Y) == 0
finales:
(de X) == 1
(de Y) == 2
Cuando no sea deseable esta repeticin de subobjetos, la herencia virtual evita la multiplicidad.
Observe el resultado del ejemplo anterior declarando las clases X e Y como clases-base
virtuales. El programa sera exactamente igual, excepto el cambio de las lneas 7 y 8 por las
siguientes:
class X : virtual public B {};
class Y : virtual public B {};
// L.7 bis
// L.8 bis
Salida:
Valores iniciales:
z1.pub (de X) ==
z1.pub (de Y) ==
Valores finales:
z1.pub (de X) ==
z1.pub (de Y) ==
0
0
2
2
En este caso, los identificadores z1.X::pub y z1.Y::pub sealan al mismo elemento, rezn por
la que las dos ltimas salidas corresponden a la ltima asignacin realizada (lnea 17).
1022