Para entender exactamente qué está pasando cuando se usan
funciones virtuales, es útil ver la actividad que hay detrás del
telón. Aquí se muestra el array de punteros
A[]
in Instrument4.cpp
:
El array de punteros a Instrument
s no
tiene información específica de tipo; cada uno de ellos apunta a
un objeto de tipo
Instrument
. Wind
,
Percussion
,
Stringed
, y Brass
encajan en esta categoría porque derivan de
Instrument
(esto hace que tengan la misma
interfaz de Instrument
, y puedan
responder a los mismos mensajes), lo que implica que sus
direcciones pueden ser metidas en el array. Sin embargo, el
compilador no sabe que sean otra cosa que objetos de tipo
Instrument
, por lo que normalmente
llamará a las versiones de las funciones que estén en la clase
base. Pero en este caso, todas las funciones han sido declaradas
con la palabra reservada virtual
, por lo que
ocurre algo diferente. Cada vez que se crea una clase que
contiene funciones virtuales, o se deriva de una clase que
contiene funciones virtuales, el compilador crea para cada clase
una única VTABLE, que se puede ver a la derecha en el
diagrama. En ésta tabla se colocan las direcciones de todas las
funciones que son declaradas virtuales en la clase o en la clase
base. Si no se sobreescribe una función que ha sido declarada
como virtual, el compilador usa la dirección de la versión que
se encuentra en la clase base (esto se puede ver en la entrada
adjusta
de la VTABLE de
Brass
). Además, se coloca el VPTR
(descubierto en Sizes.cpp
) en la clase. Hay
un único VPTR por cada objeto cuando se usa herencia simple como
es el caso. El VPTR debe estar inicializado para que apunte a la
dirección inicial de la VTABLE apropiada (esto sucede en el
constructor que se verá más tarde con mayor detalle).
Una vez que el VPTR ha sido inicializado a la VTABLE apropiada, el objeto "sabe" de que tipo es. Pero este autoconocimiento no tiene valor a menos que sea usado en el momento en que se llama a la función virtual.
Cuando se llama a una función virtual a través de la clase base
(la situación que se da cuando el compilador no tiene toda la
información necesaria para realizar la ligadura estática),
ocurre algo especial. En vez de realizarse la típica llamada a
función, que en lenguaje ensamblador es simplemente un
CALL
a una dirección en concreto, el
compilador genera código diferente para ejecutar la llamada a la
función. Aquí se muestra a lo que se parece una llamada a
adjust()
para un objeto
Brass
, si se hace a través de un puntero a
Instrument
(una referencia a
Instrument
produce el mismo efecto):
El compilador empieza con el puntero a
Instrument
, que apunta a la dirección
inicial del objeto. Todos los objetos
Instrument
o los objetos derivados de
Instrument
tienen su VPTR en el mismo
lugar (a menudo al principio del objeto), de tal forma que el
compilador puede conseguir el VPTR del objeto. El VPTR apunta a
la la dirección inicial de VTABLE. Todas las direcciones de
funciones de las VTABLE están dispuestas en el mismo orden, a
pesar del tipo específico del
objeto. play()
es el primero,
what()
es el segundo y
adjust()
es el tercero. El compilador sabe
que a pesar del tipo específico del objeto, la función
adjust()
se encuentra localizada en
VPTR+2. Debido a esto, en vez de decir, "Llama a la función en
la dirección absoluta Instrument::adjust()
(ligadura estática y acción equivocada), se genera código que
dice "Llama a la función que se encuentre en VPTR+2". Como la
búsqueda del VPTR y la determinación de la dirección de la
función actual ocurre en tiempo de ejecución, se consigue la
deseada ligadura dinámica. Se envía un mensaje al objeto, y el
objeto se figura que debe hacer con él.