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 un dibujo del array de punteros A[] de Instrument4.cpp:
El array de punteros a Instruments 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 que Instrument, y puedan responder a los mismos mensajes), por lo que sus direcciones también pueden colocarse en el array. Sin embargo, el compilador no sabe que sean otra cosa que objetos de tipo Instrument, por lo que, si se le dejara actuar por su cuenta, normalmente llamaría a las versiones de la clase base de todas las funciones. Pero en este caso, todas esas 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 una VTABLE única para esa clase, que se puede ver a la derecha en el diagrama. En esa tabla se colocan las direcciones de todas las funciones que son declaradas virtuales en la clase o en la clase base. Si no se redefine una función que ha sido declarada como virtual en la clase base, el compilador usa en la clase derivada la dirección de la versión de la clase base (esto se puede ver en la entrada adjust de la VTABLE de Brass). Despué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, como se verá más adelante con mayor detalle).
Una vez que el VPTR ha sido inicializado a la VTABLE apropiada, el objeto, en efecto, «sabe» de qué 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 una dirección 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. Éste es el aspecto de 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 dirección inicial de la VTABLE. Todas las direcciones de funciones de las VTABLE están dispuestas en el mismo orden, independientemente del tipo específico del objeto. play() es el primero, what() es el segundo y adjust() es el tercero. El compilador sabe que, independientemente 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, en efecto, «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 real de la función ocurren en tiempo de ejecución, se consigue la deseada ligadura dinámica. Se envía un mensaje al objeto, y el objeto averigua qué debe hacer con él.