Disponible la versión 6 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.8a   Funciones virtuales

§1  Sinopsis:

Las funciones virtuales permiten que clases derivadas de una misma base (clases hermanas) puedan tener diferentes versiones de un método. Se utiliza la palabra-clave virtual para avisar al compilador que un método será polimórfico y que en las clases derivadas existen distintas definiciones del mismo. En respuesta, el "Linker" utiliza para ella una técnica especial, enlazado retrasado ( 1.4.4). La declaración de virtual en un método de una clase, implica que esta es polimórfica ( 4.11.8), y que probablemente no se utilizará directamente para instanciar objetos, sino como super-clase de una jerarquía.

Observe que esta posibilidad, que un mismo método puede exhibir distintos comportamientos en los descendientes de una base común, es precisamente lo que posibilita y define el polimorfismo. En estos casos se dice que las descendientes de la función virtual solapan o sobrecontrolan ("Override") la versión de la superclase, pero esta versión de la superclase puede no existir en absoluto . Es probable que en ella solo exista una declaración del tipo: vitual void foo(); sin que exista una definición de la misma. Para que pueda existir la declaración de un método sin que exista la definición correspondiente, el método debe ser virtual puro (un tipo particular dentro de los virtuales).

Utilizaremos la siguiente terminología:  "función sobrecontrolada" o "solapada", para referirnos a la versión en la superclase y "función sobrecontroladora" o "que solapa" para referirnos a la nueva versión en la clase derivada. Cualquier referencia al sobrecontrol o solapamiento ("Overriding") indica que se está utilizando el mecanismo C++ de las funciones virtuales. Parecido pero no idéntico al de sobrecarga; conviene no confundir ambos conceptos. Más adelante intentamos aclarar sus diferencias .

§2  Sintaxis

Para declarar que un método de una clase base es virtual, su prototipo se declara como siempre, pero anteponiendo la palabra-clave virtual [3], que indica al compilador algo así como: "Será definido más tarde en una clase derivada". Ejemplo:

virtual void dibujar();

Cuando se aplica a métodos de clase [6], el especificador virtual debe ser utilizado en la declaración, pero no en la definición si esta se realiza offline (fuera del cuerpo de la clase). Ejemplo:

class CL {
  ...
  virtual void func1();
  virtual void func2();
  virtual void func3() { ... }    // Ok. definición inline
};
virtual void CL::func1() { ... }  // Error!!
void CL::func2() { ... }          // Ok. definición offline

§3  Descripción

Al tratar del enlazado se indicaron las causas que justifican la existencia de este tipo de funciones en los lenguajes orientados a objeto (que aconsejamos repasar ahora E1.4.4). Para ayudarnos a comprender el problema que se pretende resolver con este tipo de funciones, exponemos una continuación del ejemplo de la clase Poligono al que nos referimos al tratar de las clases polimórficas:


class Color { public: int R, G, B; };   // clase auxiliar
class Punto { public: float x, y; };    // clase auxiliar

class Poligono {        // clase-base (polimórfica)
  protected:            // interfaz para las clases derivadas
  Punto centro;
  Color col;
  ...
  public:               // interfaz para los usuarios de poligonos
  virtual void dibujar() const;
  virtual void rotar(int grados);
  ...
};

class Circulo : public Poligono {       // Un tipo de polígono
  protected:
  int radio;
  ...
  public:
  void dibujar() const;
  void rotar(int) { }
  ...
};

class Triangulo : public Poligono {     // Otro tipo de pológono
  protected:
  Punto a, b, c;
  ...
  public:
  void dibujar() const;
  void rotar(int);
  ...
};


Lo que se pretende con este tipo de jerarquía es que los usuarios de subclases manejen los distintos polígonos a través de su interfaz pública (sus miembros públicos), mientras que los implementadores (creadores de los diversos tipos de polígonos que puedan existir en la aplicación), compartan los aspectos representados por los miembros protegidos.

Los miembros protegidos son utilizados también para aquellos aspectos que son dependientes de la implementación. Por ejemplo, aunque la propiedad color será compartida por todas las clases de polígonos, posiblemente su definición dependa de aspectos concretos de la implementación, es decir, del concepto de "color" del sistema operativo, que probablemente estará en definiciones en ficheros de cabecera.

Los miembros en la superclase (polimórfica) representan las partes que son comunes a todos los miembros (a todos los polígonos) pero en la mayoría de los casos no es sencillo decidir cuales serán estos miembros (propiedades o métodos) compartidos por todas las clases derivadas. Por ejemplo, aunque se puede definir un centro para cualquier polígono, mantener su valor puede ser una molestia innecesaria en la mayoría de los polígonos y sobre todo en los triángulos, mientras es imprescindible en los círculos y muy útil en el resto de polígonos equiláteros.

§3.1 Ejemplo-1

#include <iostream>
using namespace std;

class B {                 // L.4:  Clase-base
  public: virtual int fun(int x) {return x * x;}   // L.5 virtual
};
class D1 : public B {     // Clase derivada-1
  public:  int fun (int x) {return x + 10;}        // L.8 virtual
};
class D2 : public B {     // Clase derivada-2
  public:  int fun (int x) {return x + 15;}        // L.11 virtual
};

int main(void) {          // =========
  B b; D1 d1; D2 d2;      // M.1
  cout << "El valor es: " << b.fun(10)     << endl;  // M.2:
  cout << "El valor es: " << d1.fun(10)    << endl;  // M.3:
  cout << "El valor es: " << d2.fun(10)    << endl;  // M.4:
  cout << "El valor es: " << d1.B::fun(10) << endl;  // M.5:
  cout << "El valor es: " << d2.B::fun(10) << endl;  // M.6:
  return 0;
}

Salida:

El valor es: 100
El valor es: 20
El valor es: 25
El valor es: 100
El valor es: 100

Comentario

Definimos una clase-base B, en la que definimos una función virtual fun; a continuación derivamos de ella dos subclases D1 y D2, en las que definimos sendas versiones de fun que solapan la versión existente en la clase-base.

Observe que según las reglas §4.1 y §4.2 indicadas más adelante , las versiones de fun en L.8 y L.11 son virtuales, a pesar de no tener el declarador virtual indicado explícitamente. Estas versiones son un caso de polimorfismo, y solapan a la versión definida en la clase-base.

La línea M.1 instancia tres objetos de las clases anteriormente definidas.  En las líneas M.2 a M.4 comprobamos sus valores, que son los esperados (en cada caso se ha utilizado la versión de fun adecuada al objeto correspondiente). Observe que la elección de la función adecuada no se realiza por el análisis de los argumentos pasados a la función como en el caso de la sobrecarga (aquí los argumentos son iguales en todos los casos), sino por la "naturaleza" del objeto que invoca la función; esta es precisamente la característica distintiva del del polimorfismo.

Las líneas M.5 y M.6 sirven para recordarnos que a pesar del solapamiento, la versión de fun de la superclase sigue existiendo, y es accesible en los objetos de las clases derivadas utilizando el sobrecontrol adecuado ( 4.11.2b).

Nota: la utilización del operador :: de acceso a ámbito ( 4.9.19) anula el mecanismo de funciones virtuales, pero es aconsejable no usarlo en demasía, pues conduce a programas más difíciles de mantener. Como regla general, este tipo de calificación solo debe utilizarse para acceder a miembros del subobjeto heredado ( 4.11.2b) como es el caso del ejemplo.

§3.2  Ejemplo-2

Hagamos ahora de abogados del diablo compilando el ejemplo anterior con la única modificación de que el método fun de B no sea virtual, sino un método normal. Es decir, la sentencia L.5, quedaría como:

public: int fun(int x) {return x * x;}    // L.5b no virtual!!

En este caso, comprobamos que el resultado coincide exactamente con el anterior, lo que nos induciría a preguntar ¿Para qué diablos sirven entonces las funciones virtuales?.

§3.3  Ejemplo-3

La explicación podemos encontrala fácilmente mediante una pequeña modificación en el programa: en vez de acceder directamente a los miembros de los objetos utilizando el selector directo de miembro . ( 4.9.16) en las sentencias de salida, realizaremos el acceso mediante punteros a las clases correspondientes. De estos punteros declararemos dos tipos: a la superclase y a las clases derivadas (por brevedad hemos suprimido las sentencias de comprobación M.5 y M.6).

El nuevo diseño sería el siguiente:

#include <iostream>
using namespace std;

class B {                 // L.4: Clase-base
  public: virtual int fun(int x) {return x * x;}  // L.5 virtual
};
class D1 : public B {     // Clase derivada-1
  public: int fun (int x) {return x + 10;}        // L.8 virtual
};
class D2 : public B {     // Clase derivada-2
  public: int fun (int x) {return x + 15;}        // L.11 virtual
};

int main(void) {          // =========
  B b; D1 d1; D2 d2;      // M.1
  B* bp = &b; B* bp1 = &d1; B* bp2 = &d2;           // M.1a
  D1* d1p = &d1; D2* d2p = &d2;                     // M.1b
  cout << "El valor es: " << bp->fun(10) << endl;   // M.2a:
  cout << "El valor es: " << bp1->fun(10) << endl;  // M.3a:
  cout << "El valor es: " << bp2->fun(10) << endl;  // M.3b:
  cout << "El valor es: " << d1p->fun(10) << endl;  // M.4a:
  cout << "El valor es: " << d2p->fun(10) << endl;  // M.4b:
  return 0;
}

Salida:

El valor es: 100
El valor es: 20
El valor es: 25
El valor es: 20
El valor es: 25

Comentario

La sentencia M.1a define tres punteros a la clase-base. No obstante, dos de ellos se utilizan para señalar objetos de las sub-clases. Esto es típico de los punteros en jerarquías de clases ( 4.11.2b1). Precisamente se introdujo esta "relajación" en el control de tipos, para facilitar ciertas funcionalidades de las clases polimórficas.

La sentencia M.1b define sendos punteros a subclase, que en este caso si son aplicados a entidades de su mismo tipo.

Teniendo en cuenta las modificaciones efectuadas, y como no podía ser menos, las nuevas salidas son exactamente análogas a las del ejemplo inicial.

§3.3  Ejemplo-4

Si suprimimos la declaración de virtual para la sentencia L.5 y volvemos a compilar el programa, se obtienen los resultados siguientes:

El valor es: 100
El valor es: 100
El valor es: 100
El valor es: 20
El valor es: 25

Observamos como, en ausencia de enlazado retrasado, las sentencias M.3a y M.3b, que acceden a métodos de objetos a través de punteros a la superclase, se refieren a los métodos heredados (que se definieron en la superclase). En este contexto pueden considerarse equivalentes los siguientes pares de expresiones:

bp1->fun(10)  ==  d1.B::fun(10)
bp2->fun(10)  ==  d2.B::fun(10)


Hay ocasiones es que este comportamiento no interesa. Precisamente en las clases abstractas, en las que la definición de B::fun() no existe en absoluto, y expresiones como M.3a y M.3b conducirían a error si fun() no fuese declarada virtual pura en B.

§3.4  Ejemplo-5

Presentamos una variación muy interesante del primer ejemplo , en el que simplemente hemos eliminado la línea 11, de forma que no existe definición específica de fun en la subclase D2.

#include <iostream>
using namespace std;

class B {                 // L.4:  Clase-base
  public:  virtual int fun(int x) {return x * x;}   // L.5 virtual
};
class D1 : public B {     // Clase derivada
  public:  int fun (int x) {return x + 10;}         // L.8 virtual
};
class D2 : public B { };  // Clase derivada

int main(void) {          // =========
  B b; D1 d1; D2 d2;      // M.1
  cout << "El valor es: " << b.fun(10)     << endl;  // M.2:
  cout << "El valor es: " << d1.fun(10)    << endl;  // M.3:
  cout << "El valor es: " << d2.fun(10)    << endl;  // M.4:
  cout << "El valor es: " << d1.B::fun(10) << endl;  // M.5:
  cout << "El valor es: " << d2.B::fun(10) << endl;  // M.6:
  return 0;
}

Salida:

El valor es: 100
El valor es: 20
El valor es: 100
El valor es: 100
El valor es: 100

Comentario

El objeto d2 no dispone ahora de una definición específica de la función virtual fun, por lo que cualquier invocación a la misma supone utilizar la versión heredada de la superclase. En este caso las invocaciones en M.4 y M.6 utilizan la misma (y única) versión de dicha función.


§3.5  No confundir el mecanismo de las funciones virtuales con el de sobrecarga y ocultación. Sea el caso siguiente:

class Base {
  public:
  void fun (int);    // No virtual!!
  ...
};
class Derivada : public Base {
  public:
  void fun (int);    // Oculta a Base::fun
  void fun (char);   // versión sobrecargada de la anterior
  ...
};

Aquí pueden declararse las funciones void Base::fun(int) y void Derivada::fun(int); incluso sin ser virtuales. En este caso, se dice que void Derivada::fun(int) oculta cualquier otra versión de fun(int) que exista en cualquiera de sus ancestros ( 4.11.2b). Además, si la clase Derivada define otras versiones de fun(), es decir, existen versiones de Derivada::fun() con diferentes definiciones, entonces se dice de estas últimas son versiones sobrecargadas de Derivada::fun(). Por supuesto, estas versiones sobrecargadas deberán seguir las reglas correspondientes ( 4.4.1a).

§3.6 Ejemplo-6

Para ilustrar el mecanismo de ocultación en un ejemplo ejecutable, utilizaremos una pequeña variación del ejemplo anterior (Ejemplo-3 ):

#include <iostream>
using namespace std;

class B {                // L.4: Clase-base
  public: virtual int fun(int x) {return x * x;}    // L.5 virtual
};
class D1 : public B {    // Clase derivada-1
  public: int fun (int x, int y) {return x + 10;}   // L.8 NO virtual
};
class D2 : public B {    // Clase derivada-2
  public: int fun (int x) {return x + 15;}          // L.11 virtual
};

int main(void) {         // =========
  B b; D1 d1; D2 d2;     // M.1
  B* bp = &b; B* bp1 = &d1; B* bp2 = &d2;           // M.1a
  D1* d1p = &d1; D2* d2p = &d2;     // M.1b
  cout << "El valor es: " << bp->fun(10) << endl;   // M.2a:
  cout << "El valor es: " << bp1->fun(10) << endl;  // M.3a:
  cout << "El valor es: " << bp2->fun(10) << endl;  // M.3b:
  cout << "El valor es: " << d1p->fun(10) << endl;  // M.4a:
  cout << "El valor es: " << d2p->fun(10) << endl;  // M.4b:
  return 0;
}

En este caso nos hemos limitado a añadir un segundo argumento a la definición de D1::fun de la clase derivada-1 (L8). Como consecuencia, la nueva función no es virtual, ya que no cumple con las condiciones exigidas . El resultado es que en las instancias de D1, la definición B::fun de la superclase queda ocultada por la nueva definición (más detalles del macanismo utilizado en Name-lookup), con la consecuencia de que la sentencia M.4a, que antes de la modificación funcionaba correctamente, produce ahora un error de compilación porque los argumentos actuales (un entero) no concuerdan con los argumentos formales esperados por la función (dos enteros).

Además el compilador nos advierte de la ocultación mediante una advertecia; en Borland C++: 'D1::fun(int,int)' hides virtual function 'B::fun(int)' [5].

Nota: al lector puede parecerle (no sin razón) que las diferencias enumeradas son meramente sintácticas, y en realidad, salvo que se utilicen punteros en jerarquías de clases polimórficas y normales, las diferencias son indetectables. En la página adjunta intentamos aclarar un poco más estas diferencias ( Polimorfismo versus Sobrecarga).


&4
  Cuando se declare una función como virtual tenga en mente que:

  • Solo pueden ser métodos (funciones-miembro).
  • No pueden ser declaradas friend de otras clases ( 4.11.2a1).
  • No pueden ser miembros estáticos ( 4.11.7)
  • Los constructores no pueden ser declarados virtuales ( 4.11.2d1)
  • Los destructores sí pueden ser virtuales ( 4.11.2d2).


§4.1  Como hemos comprobado en el Ejemplo-5 , las funciones virtuales no necesitan ser redefinidas en todas las clases derivadas. Puede existir una definición en la clase base y todas, o algunas de las subclases, pueden llamar a la función-base.


§4.2  Para redefinir una función virtual en una clase derivada, las declaraciones en la clase base y en la derivada deben coincidir en cuanto a número y tipo de los parámetros. Excepcionalmente pueden diferir en el tipo devuelto; este caso es discutido más adelante .


§4.3  Una función virtual redefinida, que solapa la función de la superclase, sigue siendo virtual y no necesita el especificador virtual en su declaración en la subclase (caso de las declaraciones de L.8 y L.11 en el ejemplo anterior ). La propiedad virtual es heredada automáticamente por las funciones de la subclase. Aunque si la subclase va a ser derivada de nuevo, entonces es necesario el especificador.

Ejemplo:

class B {       // Superclase
   public:
   virtual int func();
};
...
class D1 : public B {     // Derivada
  public:
  int fun ();             // virtual por defecto
};
class D2 : public B {     // Derivada
  public:
  virtual int fun ();     // virtual explícita
};
class D1a : public D1 {   // Derivada
  public:
  int fun ();             // No virtual!!
};
class D2a : public D2 {   // Derivada
  public:
  int fun ();             // Ok Virtual!!

  int fun (char);         // No virtual!!
};

Como puede verse, de la simple inspección de las dos últimas líneas, no es posible deducir que el método fun de la clase D2a es un método virtual. Esto representa en ocasiones un problema y puede ser motivo de confusión, ya que es muy frecuente que las definiciones de las superclases se encuentren en ficheros de cabecera, y el programador que utiliza tales superclases para derivar versiones específicas debe consultar dichos ficheros. Sobre todo, porque como se indicó en el epígrafe anterior, la declaración en la subclase debe coincidir en cuanto a número y tipo de los parámetros. En caso contrario se trataría de una nueva definición, con lo que estamos ante un caso de sobrecarga y se ignora el mecanismo de enlazado retrasado [8].

§5  Invocación de funciones virtuales

Lo que realmente caracteriza a las funciones virtuales es la forma muy especial que utiliza el compilador para invocarlas; forma que es posible gracias a su tipo de enlazado (dinámico 1.4.4). Por lo demás, el mecanismo externo (la sintaxis utilizada) es exactamente igual que la del resto de funciones miembro. He aquí un resumen de esta sintaxis:

class CB {              // Clase-base
   public:
   void fun1(){...}     // definición-10
   void virtual fun2(){...}    // definición-20
   ...
};
class D1 : public CB {  // Derivada-1
   public:
   void fun1() {...}    // definición-11
   void fun2() {...}    // definición-12
};
class D2 : public CB {  // Derivada-2
   public:
   void fun1() {...}    // definición-21
   void fun2() {...}    // definición-22
};
...
CB obj; D1 obj1;  D2 obj2;     // se instancian objetos de las clases
...
obj.fun1();             // invoca definición-10
obj.fun2();             // invoca definición-20
obj1.fun1();            // invoca definición-11
obj1.fun2();            // invoca definición-12
obj1.CB::fun1();        // invoca definición-10
obj1.CB::fun2();        // invoca definición-20
obj2.fun1();            // invoca definición-21
obj2.fun2();            // invoca definición-22
obj2.CB::fun1();        // invoca definición-10
obj2.CB::fun2();        // invoca definición-20

Observe que la dierencia entre las invocaciones obj.fun1() y obj1.CB::fun1(), estriba en que la misma función se ejecuta sobre distinto juego de variables. Por contra, en obj.fun1() y obj.fun2(), funciones distintas se ejecutan sobre el mismo juego de variables.

CB* ptr = &obj;         // se definen punteros a los objetos
D1* ptr1= &obj1;
D2* ptr2= &obj2;
...
ptr->fun1();            // invoca definición-10
ptr1->fun1();           // invoca definición-11
ptr2->fun1();           // invoca definición-21


Cuando desde un objeto se invoca un método (virtual o no) utilizando el nombre del objeto mediante los operadores de acceso a miembros, directo . ( 4.9.16) o indirecto -> ( 4.9.16),  se invoca el código de la función correspondiente a la clase de la que se instancia el objeto. Es decir, el código invocado solo depende del tipo de objeto (y de la sintaxis de la invocación). Esta información puede conocerse en tiempo de compilación, en cuyo caso se utilizaría enlazado estático. En otros casos este dato solo es conocido en tiempo de ejecución, por lo que debería emplearse enlazado dinámico.

Recuerde que cualquiera que sea el mecanismo para referenciar al objeto que invoca a la función (operador de acceso directo -1- o indirecto -2-):

obj.fun1()      // -1-
ptr->fun1()     // -2-

la función conoce cual es el objeto que la invoca -que juego de variables debe utilizar- a través del argumento implícito this ( 4.11.6).

§5.1  Invocación en jerarquías de clases

Sea B es una clase base, y otra D derivada públicamente de B. Cada una contiene una función virtual vf, entonces si vf es invocada por un objeto de D, la llamada que se realiza es D::vf(), incluso cuando el acceso se realiza vía un puntero o referencia a la superclase B. Ejemplo:

#include <iostream>
using namespace std;

class C {               // Clase-base
  public: virtual int get() {return 10;}
};
class D : public C {    // Derivada
  public: virtual int get() {return 100;}
};

int main(void) {        // =========
  D d;
  C* cptr = &d;
  C& cref = d;
  cout << d.get()     << endl;
  cout << cptr->get() << endl;
  cout << cref.get()  << endl;
  return 0;
}

Salida:

100
100
100


§5.1a
Resulta muy oportuno aquí volver al caso mostrado al tratar el uso de punteros en jerarquías de clases ( 4.11.2b1), donde nos encontrábamos con una "sorpresa" al acceder al método de un objeto a través de un puntero a su superclase (repase el ejemplo original para situarse en la problemática que allí se describe).

Como ya anunciábamos en el referido ejemplo, basta una pequeña modificación en la definición de la función f en la superclase B para evitar el problema. En este caso basta la definición de dicha función como virtual. Observe la diferencia de salidas en ambos casos y como el nuevo diseño es en lo demás exactamente igual al original.

#include <iostream.h>

class B {             // Superclase (raíz)
  public: virtual 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;
  cout << "dptr->f(1) ";
  cout << dptr->f(1) << endl;  // acceso mediante puntero a subclase
  cout << "bptr->f(1) ";       // acceso mediante puntero a superclase
  cout << bptr->f(1) << endl;
}

Salida:

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

§6  Tabla de funciones virtuales

Las funciones virtuales pagan un tributo por su versatilidad. Cada objeto de la clase derivada tiene que incluir un puntero (vfptr) a una tabla de direcciones de funciones virtuales, conocida como vtable [2]. La utilización de dicho puntero permite seleccionar desde el objeto, la función correspondiente en tiempo de ejecución. Como resultado, el mecanismo de invocación de estas funciones (enlazado dinámico 1.4.4) es mucho menos eficiente que el de los métodos normales (enlazado estático), por lo que debe reservarse su utilización a los casos estrictamente necesarios.

Es fácil poner en evidencia la existencia del puntero vfptr mediante un sencillo experimento [7] que utiliza el operador sizeof ( 4.9.13):

struct S1 {
  int n;
  double get() { return n; } // método auxiliar normal
};

struct S2 {
  int n;
  virtual double get() { return n; } // método virtual
};

...

size_t tamS1 = sizeof(S1);  // -> 4
size_t tamS2 = sizeof(S2);  // -> 8

Comentario

El resultado del tamaño de ambos tipos es respectivamente 4 y 8 en cualquiera de los compiladores comprobados: Borland C++ 5.5 y gcc-g++ 3.4.2-20040916-1 para Windows. La diferencia de 4 Bytes obtenida corresponde precisamente a la presencia del mencionado puntero oculto.

Nota:  Tenga en cuenta que, en ambos casos, el tamaño resultante puede venir enmascarado por fenómenos de alineamiento interno. De forma que los tamaños respectivos y sus diferencias, generalmente no coincidirán con los valores teóricos que cabría esperar.

§7  Función virtual pura

En ocasiones se lleva al extremo el concepto "virtual" en la declaración de una súper clase ("esta función será redefinida más tarde en las clases derivadas"), por lo que en ella solo existe una declaración de la función, relegándose las distintas definiciones a las clases derivadas. Entonces se dice que esta función es virtual pura. Esta circunstancia hay que advertirla al compilador; es una forma de decirle que la falta de definición no es un olvido por nuestra parte (de lo contrario el compilador nos señala que se nos ha olvidado la definición); esto se hace igualando a cero la declaración de la función [1]:

virtual int funct1(void);      // Declara función virtual
virtual int funct2(void) = 0;  // Declara función virtual pura


Como hemos señalado, la existencia de una función virtual basta para que la clase que estamos definiendo sea polimórfica ( 4.11.8). Si además igualamos la función a cero, la estaremos declarando como función virtual pura, lo que automáticamente declara la clase como abstracta ( 4.11.8c

Es muy frecuente que las funciones virtuales puras se declaren además con el calificador const ( 3.2.1c), de forma que es usual encontrar expresiones del tipo:

virtual int funct2(void) const = 0;  // Declara función virtual pura y constante

§7.1  Ejemplo

El ejemplo que sigue es una modificación del anterior , en el que declaramos la función fun de la clase-base B como virtual pura, con lo que podemos omitir su definición.

#include <iostream>
using namespace std;

class B {              // L.4:  Clase-base
  public:  virtual int fun(int x) = 0;       // L.5 virtual pura
};
class D1 : public B {  // Clase derivada
  public:  int fun (int x) {return x + 10;}  // L.8 virtual
};
class D2 : public B {  // Clase derivada
  public:  int fun (int x) {return x + 15;}  // L.11 virtual
};
int main(void) {       // =========
  D1 d1; D2 d2;        // M.1

  cout << "El valor es: " << d1.fun(10)    << endl;  // M.3:
  cout << "El valor es: " << d2.fun(10)    << endl;  // M.4:
  return 0;
}

Salida:

El valor es: 20
El valor es: 25

Comentario

El resto del programa se mantiene prácticamente igual al modelo anterior, con la salvedad de que ahora en M1 no podemos instanciar un objeto directamente de la superclase B; la razón es que la superclase está incompleta (falta la definición de fun). Si lo intentáramos, el compilador nos mostraría un error señalando que no se puede instanciar un objeto de la clase B y que dicha clase es abstracta; además las clases abstractas no son instanciables por definición.

En lo que respecta a las salidas, comprobamos que son los valores esperados.


§7.2   Una declaración de función no puede ser al mismo tiempo una definición y una declaración de virtual pura. Ejemplo:

struct Est {
    virtual void f() { /* ... */ } = 0;    // Error!!
};

La forma legal de proporcionar una definición es:

struct Est {
  virtual void f(void) = 0;    // declara f virtual pura
};
virtual void Est::f(void) {    // definición posterior de f
   /* código de la función f  */
};

§7.3  Un mundo de excepciones

Seguramente el lector que haya llegado hasta aquí experimente una cierta perplejidad (que comparto). En el párrafo §7 decimos que se utiliza el recurso de declarar una función virtual pura para poder omitir la definición, y a continuación, en el párrafo §7.2 exponemos la forma legal de proporcionarla...  El lector puede comprobar que esta aparente contradicción es asumida por el compilador sin protestas. En efecto, considere el ejemplo siguiente en el que modificamos el anterior añadiendo una definición a la función virtual pura fun.

#include <iostream>
using namespace std;

class B {               // L.4:  Clase-base
  public:  virtual int fun(int x) = 0;       // L.5 virtual pura
};
int B::fun(int x) {return x * x}             // L.7 definición de fun

class D1 : public B {   // Clase derivada
  public:  int fun (int x) {return x + 10;}  // L.9 virtual
};

class D2 : public B {   // Clase derivada
  public:  int fun (int x) {return x + 15;}  // L.12 virtual
};

int main(void) {        // =========
  D1 d1; D2 d2;         // M.1

  cout << "El valor es: " << d1.fun(10)    << endl;  // M.3:
  cout << "El valor es: " << d2.fun(10)    << endl;  // M.4:
  cout << "El valor es: " << d1.B::fun(10) << endl;  // M.5:
  cout << "El valor es: " << d2.B::fun(10) << endl;  // M.6:
  return 0;
}

Salida:

El valor es: 20
El valor es: 25
El valor es: 100
El valor es: 100

Comentario

En M1 seguimos sin poder instanciar un objeto de la superclase B (porque es abstracta). En cambio, ahora podemos insertar las líneas M5 y M6 (que ya utilizamos en la primera versión del ejemplo ). La razón es que ahora invocamos las versiones de fun (que sí está definida) heredadas de la superclase en los objetos d1 y d2.

  Ver clases abstractas (4.11.8c) para una discusión más amplia de las funciones virtuales puras.

§8  Tipos devueltos por las funciones virtuales

Generalmente, cuando se redefine una función virtual no se puede cambiarse el tipo de valor devuelto. Para redefinir una función virtual en alguna clase derivada, la nueva función debe coincidir exactamente en número y tipo con los parámetros de la declaración inicial (la "firma" -Signature- de ambas funciones deben ser iguales). Si no coinciden en esto, el compilador C++ considera que se trata de funciones diferentes (un caso de sobrecarga) y se ignora el mecanismo de funciones virtuales.

Nota: para prevenir que puedan producirse errores inadvertidos, el compilador C++ GNU dispone de la opción  -Woverloaded-virtual, que produce un mensaje de aviso, si se redefine un método declarado previamente virtual en una clase antecesora, y no se cumple la condición de igualdad de firmas.

No obstante lo anterior, hay casos en que las funciones virtuales redefinidas en clases derivadas devuelven un tipo diferente del de la función virtual de la clase base. Esto es posible solo cuando se dan simultáneamente las dos condiciones siguientes [4]:

  • La función virtual sobrecontrolada devuelve un puntero o referencia a clase base [1].
  • La nueva versión devuelve un puntero o referencia a la clase derivada [2].

§8.1  Ejemplo

struct X {};            // clase base.
struct Y : X {};        // clase derivada (:public X por defecto).
struct B {              // clase base.
   virtual void vf1();  // L.4:
   virtual void vf2();
   virtual void vf3();
   void f();
   virtual X* pf();     /* L.8: [1] devuelve puntero a clase base,
                            esta función virtual puede ser sobrecontrolada */
};
class D : public B {    // clase derivada
   public:
   virtual void vf1();  // L.12: Especificador virtual, legal pero redundante
   void vf2(int);       /* L.13: No virtual, oculta B::vf2()
                            dado que usa otros argumentos */
// char vf3();          // L.14: Ilegal! cambia solo el tipo devuelto!
   void f();            // L.15: privativa de D (no virtual)
   Y*   pf();           /* L.16: [2] función sobrecontrolante; difiere solo
                          en el tipo devuelto. Devuelve puntero a subclase */
};
void extf() {
   D d;                 // d objeto de la clase D (instancia)
   B* bp = &d;          /* L.20: Conversión estándar D* a B*
                  Inicializa bp con la tabla de funciones del objeto d.
                  Si no existe entrada para una función en dicha tabla,
                  utiliza la función de la tabla de la clase B */
   bp–>vf1();           // invoca D::vf1
   bp–>vf2();           // invoca B::vf2 (D::vf2 tiene diferentes argumentos)
   bp–>f();             // invoca B::f (not virtual)
   X* xptr = bp–>pf();  /* invoca D::pf() y convierte el resultado
                            en un puntero a X */
   D* dptr = &d;
   Y* yptr = dptr–>pf(); /* inicializa yptr; este puntero invocará a D::pf()
                            No se realiza ninguna conversión */
}


L.12:  La versión D::vf1 es virtual automáticamente, dado que devuelve lo mismo y tiene los mismos parámetros que su homónima en la superclase B. El especificador virtual puede ser utilizado en estos casos pero no es estrictamente necesario a no ser que se vayan a derivar nuevas subclases de la clase derivada.

  Tema relacionado

  Inicio.


[1]  Desde luego, el recurso notacional adoptado (igualar una función a cero) es cualquier cosa menos lógico y elegante; otra de las particularidades sintácticas de C++. Para evitarlo, Joyner ( 7) aconseja utilizar un #define ( 4.9.10b) del tipo

#define abstracta =0

con lo que la declaración de virtual pura (que da lugar a que la clase correspondiente sea abstracta)

virtual void func() =0;

podría ser escrito como

virtual void func() abstracta;

Una opción, quizás más lógica y utilizada por algunos compiladores, es utilizar un define del tipo:

#define PURE  =0

con lo que la declaración de virtual pura del caso anterior también podría ser escrita:

virtual void func() PURE;

[2]  También denominadas vtables en la literatura inglesa (pronunciado vee-tables).

[3]  El compilador GNU Cpp dispone de la opción -fall-virtual que hace que todas las funciones miembro de la clase sean declaradas implícitamente virtuales (excepto los constructores y las funciones-operador new y delete).

[4]  Esta posibilidad fue añadida en la última versión del Estándar (Julio 1998).

[5]  El compilador GNU cpp dispone de una opción especial de compilación para advertir de este tipo de errores:  -Woverloaded-virtual.

[6]  Recordemos que la palabra clave virtual puede ser utilizada también en la definición de clases ( 4.11.2c1).

[7]  Debo agradecer a Adán Román Ruiz, de la Universidad de Oviedo (España) la observación y la idea del ejemplo.

[8]  Puesto que los mecanismos de enlazado involucrados son totalmente distintos, y sus rendimientos también son distintos, para evitar confusiones, Microsoft ha introducido dos nuevas palabras-clave (que por el momento no son estándar) para la definición de métodos en su compilador Visual C++ 2005 y Visual Studio 2005.