由于虚拟表是在编译时创建的,那么为什么我们将其称为 c++ 中的运行时多态性呢?
8 回答
因为查找发生在运行时。
在典型的实现中,每个类都有一个虚拟表,它在编译时是已知的。
在运行时,类型指针BaseClass *
可能指向类型为 的对象BaseClass
,或者指向类型为 的对象的基类子对象DerivedClass
,其中BaseClass
是 的基类DerivedClass
。这同样适用于引用。
在前一种情况下,会在 的 vtable 中查找虚拟调用BaseClass
。在后一种情况下,虚拟调用会在 的 vtable 中查找DerivedClass
。由于调用站点不“知道”调用哪个函数,直到调用在运行时实际执行,这称为动态或运行时多态性。
同样在一个典型的实现中,它找出要使用哪个 vtable 的方式是具有一个或多个虚函数的类型的对象包含一个“隐藏的”附加字段,该字段指向其完整类型的 vtable。那是为了简单的继承。多重继承和虚拟继承增加了复杂性,但原理相同,对象提供了指向应使用的任何 vtable 的指针。
将此与非虚拟调用进行比较,其中编译器不需要使用任何 vtable 或知道完整对象的类型。它根据指针或引用的类型选择函数。
虚拟表无关紧要。C++ 中的运行时多态性意味着:
struct B {
virtual void f() { std::cout << "In B\n"; }
};
struct D1 : B {
virtual void f() { std::cout << "In D1\n"; }
};
struct D2 : b {
virtual void f() { std::cout << "In D2\n"; }
};
B *bp = new B;
bp->f(); // calls B::f
B *bp1 = new D1;
bp1->f(); // calls D1::f
B *bp2 = new D2;
bp2->f(); // calls D2::f
即使所有三个指针都有 type B*
,调用的行为f()
取决于指针指向的对象的运行时类型。
正如您所说,每个类的虚函数表(和其他多态信息)是在编译时生成的。
每个对象都包含一个指向其动态类型的正确表的指针。该指针在创建对象时在运行时初始化,并在运行时用于选择要调用的正确虚函数。这就是为什么它被称为运行时多态性。
虚拟表在编译期间创建,但在运行时使用。
虚拟表是C++ 和其他一些 OO 语言中类类型的运行时表示的一个元素。它用于虚拟方法调用的动态调度。换句话说,它是 C++ 动态多态特性的一个实现细节。
构建此表的时间与调度方案无关,调度方案定义了多态性是静态的还是动态的。
为此,您必须简要了解什么是运行时多态性以及它是如何工作的。
如果您有这样的类层次结构:
class Animal
{
...
virtual void sayHello() {
cout << "hello" << endl;
}
};
class Dog : public Animal
{
...
/*virtual*/ void sayHello() {
cout << "woof!" << endl;
}
};
并调用Dog
实例上的方法,您希望能够调用 的(虚拟)覆盖方法Dog
,而不是Animal
。我想你知道我们为什么要这样做,所以我不想进一步解释。
但是我们怎么能在运行时知道 anAnimal*
实际上是 aDog*
呢?问题是,我们不必知道它是什么,我们只需要知道要调用哪些函数,或者更好地说,要调用哪个函数指针。虚函数的所有函数指针都存储在每个类的虚表中。您可以将其想象为根据实际类调用哪个代码来调用哪个虚函数的“指南” 。
这个虚拟表是在编译期间创建的(编译器将“指南”写入可执行文件),每个实例都指向一个可用的虚拟表。因此,如果您说它Dog *dog = new Dog
指向 Dog 的虚拟表。类似的调用编译为对尚未指定的类中dog->sayHello()
的虚拟函数的虚拟调用...sayHello
然后,在运行时, like 调用dog->sayHello()
将首先查找存储在对象中的具体虚拟表(代码不知道它是 Dog,只知道它是 Animal)并找到方法的函数指针Dog::sayHello()
。
为了回答您的问题,这种机制称为运行时多态性,因为我们可以调用重载 this 指针的方法(这意味着它们重载了您调用它们的对象的类型),而决策是在运行时做出的。您可以调用对应的编译时多态性,其中编译器可以知道对象的具体类型,例如在Dog dog; dog.sayHello()
.
虽然 v-table 确实是在编译时创建的,但是当编译以下代码时,编译器不知道将调用哪个函数:
struct A {
virtual void f() { cout << "A::f" << endl;}
};
struct B : public A {
void f() { cout << "B::f" << endl;}
};
int main() {
A* b = new B();
b->f(); // prints "B::f", chosen at runtime
}
因此,尽管对象在编译时和运行时之间没有变化,但方法 B::f 仅在运行时被选择,因为编译器不知道对象的动态类型(这决定了调用哪个方法)。