¿Cómo funciona la ligadura dinámica? Todo el trabajo se realiza detrás del telón gracias al compilador, que instala los mecanismos necesarios de la ligadura dinámica cuando se crean funciones virtuales. Debido a que los programadores se suelen beneficiar de la comprensión del mecanismo de las funciones virtuales en C++, esta sección mostrará la forma en que el compilador implementa este mecanismo.
La palabra reservada virtual
le dice al
compilador que no debe realizar ligadura estática. Al
contrario, debe instalar automáticamente todos los mecanismos
necesarios para realizar la ligadura dinámica. Esto significa
que si se llama a play()
para un objeto
Brass
a través una dirección a la
clase base Instrument
, se usará la
función apropiada.
Para que funcione, el compilador típico
[75]
crea una única tabla (llamada VTABLE) por cada clase que
contenga funciones virtuales
. El compilador
coloca las direcciones de las funciones virtuales de esa clase en
concreto en la VTABLE. En cada clase con funciones virtuales el
compilador coloca de forma secreta un puntero llamado
vpointer
(de forma abreviada VPTR), que apunta
a la VTABLE de ese objeto. Cuando se hace una llamada a una
función virtual a través de un puntero a la clase base (es decir,
cuando se hace una llamada usando el polimorfismo), el compilador
silenciosamente añade código para buscar el VPTR y así conseguir
la dirección de la función en la VTABLE, con lo que se llama a la
función correcta y tiene lugar la ligadura dinámica.
Todo esto - establecer la VTABLE para cada clase, inicializar el VPTR, insertar código para la llamada a la función virtual - sucede automáticamente sin que haya que preocuparse por ello. Con las funciones virtuales, se llama a la función apropiada de un objeto, incluso aunque el compilador no sepa el tipo exacto del objeto.
Se puede ver que no hay almacenada información de tipo de forma explícita en ninguna de las clases. Pero los ejemplos anteriores, y la simple lógica, dicen que debe existir algún tipo de información almacenada en los objetos; de otra forma el tipo no podría ser establecido en tiempo de ejecución. Es verdad, pero la información de tipo está oculta. Para verlo, aquí está un ejemplo que muestra el tamaño de las clases que usan funciones virtuales comparadas con aquellas que no las usan:
//: C15:Sizes.cpp // Object sizes with/without virtual functions #include <iostream> using namespace std; class NoVirtual { int a; public: void x() const {} int i() const { return 1; } }; class OneVirtual { int a; public: virtual void x() const {} int i() const { return 1; } }; class TwoVirtuals { int a; public: virtual void x() const {} virtual int i() const { return 1; } }; int main() { cout << "int: " << sizeof(int) << endl; cout << "NoVirtual: " << sizeof(NoVirtual) << endl; cout << "void* : " << sizeof(void*) << endl; cout << "OneVirtual: " << sizeof(OneVirtual) << endl; cout << "TwoVirtuals: " << sizeof(TwoVirtuals) << endl; } ///:~
Listado 15.4. C15/Sizes.cpp
Sin funciones virtuales el tamaño del objeto es exactamente el
que se espera: el tamaño de un único
[76] int
. Con una única función
virtual en OneVirtual
, el tamaño del
objeto es el tamaño de NoVirtual
más el
tamaño de un puntero a void
. Lo que implica
que el compilador añade un único puntero (el VPTR) en la
estructura si se tienen una o más funciones
virtuales. No hay diferencia de tamaño entre
OneVirtual
y
TwoVirtuals
. Esto es porque el VPTR
apunta a una tabla con direcciones de funciones. Se necesita
sólo una tabla porque todas las direcciones de las funciones
virtuales están contenidas en esta tabla.
Este ejemplo requiere como mínimo un miembro de datos. Si no
hubiera miembros de datos, el compilador de C++ hubiera forzado
a los objetos a ser de tamaño no nulo porque cada objeto debe
tener direcciones distintas (¿se imagina cómo indexar un array
de objetos de tamaño nulo?). Por esto se inserta en el objeto un
miembro "falso" ya que de otra forma tendríá un tamaño
nulo. Cuando se inserta la información de tipo gracias a la
palabra reservada virtual
, ésta ocupa el
lugar del miembro "falso". Intente comentar el int
a
en todas las clases del ejemplo anterior para
comprobarlo.