2010-06-09 24 views
6

Estaba buscando información sobre la tabla virtual, pero no encuentro nada que sea fácil de entender.
¿Alguien puede darme buenos ejemplos (no de Wiki, por favor) con explicaciones o enlaces?¿Por qué necesitamos mesa virtual?

+1

* Usted * no lo necesita, pero el compilador sí. – EJP

Respuesta

6

Sin tablas virtuales no sería posible hacer que el polimorfismo en tiempo de ejecución funcione, ya que todas las referencias a las funciones se vincularían en tiempo de compilación. Un ejemplo sencillo

struct Base { 
    virtual void f() { } 
}; 

struct Derived : public Base { 
    virtual void f() { } 
}; 

void callF(Base *o) { 
    o->f(); 
} 

int main() { 
    Derived d; 
    callF(&d); 
} 

Dentro de la función callF, es suficiente saber que o apunta a un objeto Base. Sin embargo, en tiempo de ejecución, el código debe llamar al Derived::f (ya que Base::f es virtual). En tiempo de compilación, el compilador no puede saber qué código va a ejecutar la llamada o->f(), ya que no sabe a qué apunta o.

Por lo tanto, necesita algo llamado "tabla virtual" que es básicamente una tabla de indicadores de función. Cada objeto que tiene funciones virtuales tiene un "puntero de tabla v" que apunta a la tabla virtual para objetos de su tipo.

El código en la función callF anterior solo necesita buscar la entrada para Base::f en la tabla virtual (que encuentra en función del puntero v-table en el objeto), y luego llama a la función que ingresa a la tabla puntos a. Que podría ser Base::f, pero también es posible que apunte a algo más - Derived::f, por ejemplo.

Esto significa que debido a la tabla virtual, puede tener polimorfismo en tiempo de ejecución porque la función real que se llama se determina en tiempo de ejecución buscando un puntero de función en la tabla virtual y luego llamando a la función mediante ese puntero - en lugar de llamar directamente a la función (como es el caso de las funciones no virtuales).

1

Respuesta corta: llamada de función virtual, basePointer-> f(), significa diferentes cosas dependiendo del historial de basePointer. Si apunta a algo que realmente es una clase derivada, se llamará a una función diferente.

Para esto, el compilador hace un juego simple de indicadores de función. Las direcciones de las funciones que deben invocarse para diferentes tipos se almacenan en la tabla virtual.

La tabla virtual no se usa solo para punteros a funciones. La maquinaria RTTI lo usa para información de tipo de tiempo de ejecución (obteniendo tipos reales de un objeto referenciado por una dirección de uno de los tipos base).

Algunas implementaciones nuevas/borrar almacenarían el tamaño del objeto en la tabla virtual.

La programación COM de Windows usa una tabla virtual para crackearla e insertarla como interfaz.

5

Para responder a su pregunta principal, no lo hace, y el Estándar C++ no especifica que se le debe proporcionar uno. Lo que sí quiere es poder decir:

struct A { 
    virtual ~A() {} 
    virtual void f() {} 
}; 

struct B : public A { 
    void f() {} 
}; 

A * p = new B; 
p->f(); 

y tener B :: f llamado y no A :: f. Una tabla de funciones virtuales es una forma de implementar esto, pero francamente no es de interés para el programador promedio de C++; solo lo pienso al responder preguntas como esta.

+0

Para un ejemplo de alternativa, python almacena los métodos junto con los atributos directamente en el objeto. De este modo, logra este comportamiento sin usar una tabla 'virtual', aunque es muy similar. –

+0

@Matthieu M, python hace exactamente lo mismo que C++ - almacena alguna referencia a un objeto de método (en c-python implementado con punteros), mientras que C++ almacena la dirección del método. La diferencia radica principalmente en que las tablas python se almacenan por objeto y no por clase, ya que Python permite agregar atributos y métodos en el tiempo de ejecución. – gnud

+0

En realidad estoy en desacuerdo: en C++ la tabla virtual no tiene información real sobre los punteros para la función almacenada, el compilador simplemente sabe que el método requerido está en un índice dado.Por otro lado en python, los atributos y métodos se almacenan (generalmente) en un dictionario y usted hace su búsqueda por nombre. Es una gran diferencia en la implementación, que le da a python más flexibilidad a costa del rendimiento. –

0

Supongamos Player y Monster heredan de una clase base abstracta Actor que define una operación virtual name(). Además suponga que tiene una función que pide a un actor por su nombre:

void print_information(const Actor& actor) 
{ 
    std::cout << "the actor is called " << actor.name() << std::endl; 
} 

Es imposible deducir en tiempo de compilación si el actor será en realidad un jugador o un monstruo. Dado que tienen diferentes métodos name(), la decisión sobre qué método llamar debe diferirse hasta el tiempo de ejecución. El compilador agrega información adicional a cada objeto actor que permite que esta decisión se tome en tiempo de ejecución.

En cada compilador que sé, esta información adicional es un puntero (a menudo llamado VPTR) a una tabla de punteros de función (a menudo llamadas VTBL) que son específicos de la clase concreta. Es decir, todos los objetos del jugador comparten la misma tabla virtual que contiene punteros a todos los métodos del jugador (lo mismo vale para los monstruos). En tiempo de ejecución, el método correcto se encuentra eligiendo el método del vtbl apuntado por el vptr del objeto sobre el que se debe invocar el método.

1

La tabla de funciones virtuales es un detalle de implementación, es la forma en que el compilador implementa los métodos polimórficos en las clases.

Considere

class Animal 
{ 
    virtual void talk()=0; 
} 

class Dog : Animal 
{ 
    virtual void talk() { 
     cout << "Woof!"; 
    } 
} 

class Cat : Animal 
{ 
    virtual void talk() { 
     cout << "Meow!"; 
    } 
} 

Y ahora tenemos

A* animal = loadFromFile("somefile.txt"); // from somewhere 
    animal->talk(); 

¿Cómo saber qué versión de talk() se llama? El objeto animal tiene una tabla que apunta a las funciones virtuales que se usan con ese animal. Por ejemplo, puede haber talk en la tercera offset, si hay otros dos métodos virtuales:

dog 
    [function ptr for some method 1] 
    [function ptr for some method 2] 
    [function ptr for talk -> Dog::Talk] 

    cat 
    [function ptr for some method 1] 
    [function ptr for some method 2] 
    [function ptr for talk -> Cat::Talk] 

Cuando tenemos una instancia de Animnal, no sabemos lo que talk() método para llamar. Lo encontramos buscando en la tabla virtual y buscando la tercera entrada, ya que el compilador sabe que corresponde al puntero talk (el compilador conoce los métodos virtuales en Animal, y así conoce el orden de los punteros en el vtable).

Dado un Animal, para llamar al método de talk() correcto, el compilador agrega código para buscar el puntero de la tercera función y usar eso. Esto luego dirige a la implementación apropiada.

Con métodos no virtuales, esto no es necesario ya que la función real que se llama se puede determinar en tiempo de compilación: solo hay una función posible que se puede llamar para una llamada no virtual.

+0

A * animal = ....; // desde algún lugar animal-> talk(); en esta línea puede Usted ser más específico, gracias – lego69

+1

Claro - imagine que el animal está cargado desde el archivo, o se crea un animal diferente dependiendo de la entrada del usuario. La intención es que el compilador no tenga forma de saber qué tipo de animal tenemos. – mdma

Cuestiones relacionadas