为什么我们需要虚拟桌面?
没有虚拟表格,你将无法使运行时多态性工作,因为所有对函数的引用都将在编译时被绑定。一个简单的例子
struct Base {
virtual void f() { }
};
struct Derived : public Base {
virtual void f() { }
};
void callF(Base *o) {
o->f();
}
int main() {
Derived d;
callF(&d);
}
功能callF
里面,你只知道o
指向一个Base
对象。但是,在运行时,代码应该调用Derived::f
(因为Base::f
是虚拟的)。在编译时,编译器无法知道o->f()
调用将执行哪个代码,因为它不知道o
指向哪个代码。
因此,你需要一个叫做“虚拟表”的东西,它基本上是一个函数指针表。每个具有虚函数的对象都有一个“虚表指针”,它指向虚表的类型对象。
在callF
函数上面的代码然后只需要查找为Base::f
在虚拟表中的条目(它发现基于所述v表指针中的对象),并且然后它调用函数表条目指着。那可能是Base::f
但它也可能指向其他 - Derived::f
,例如。
这意味着由于虚拟表,您可以在运行时拥有多态性,因为被调用的实际函数是在运行时通过查找虚拟表中的函数指针,然后通过该指针调用函数来确定的 - 而不是直接调用函数(就像非虚函数一样)。
简短回答:虚函数调用basePointer-> f()意味着不同的事情取决于basePointer的历史。如果它指向真的是派生类的东西,则会调用一个不同的函数。
为此,编译器做了一个简单的函数指针游戏。要为不同类型调用的函数地址存储在虚拟表中。
虚拟表不仅用于函数指针。 RTTI机制将其用于运行时类型信息(获取由某个基本类型的地址引用的对象的实际类型)。
一些新的/删除实现会将对象大小存储在虚拟表中。
Windows COM编程使用虚拟表来破解它并将其作为接口推送。
要回答您的标题问题 - 您没有,并且C++标准没有指定您必须提供一个。你想要的是能够说:
struct A {
virtual ~A() {}
virtual void f() {}
};
struct B : public A {
void f() {}
};
A * p = new B;
p->f();
并且B :: f被调用而不是A :: f。虚拟函数表是实现这一点的一种方式,但是对于普通的C++程序员来说,坦率地说并不感兴趣 - 我只是在回答这样的问题时想到它。
有关替代方法的示例,python将方法和属性直接存储在对象中。它因此完成了这种行为,但不使用“虚拟”表格,尽管它非常相似。 – 2010-06-09 09:46:28
@Matthieu M,python与C++完全相同 - 它存储对方法对象的一些引用(在用指针实现的c-python中),而C++存储方法的地址。区别主要在于python表按每个对象而不是按类存储,因为python允许在运行时添加属性和方法。 – gnud 2010-08-24 09:09:59
我真的不同意:在C++中,虚拟表没有指向函数存储的指针的实际信息,编译器只知道所需的方法在给定的索引处。另一方面,在python中,属性和方法(通常)被存储在一个字典中,你可以按名称查找。这是实现中的一个主要区别,它使python以性能为代价获得更大的灵活性。 – 2010-08-24 16:29:32
假设Player
和Monster
继承自定义虚拟name()
操作的抽象基类Actor
。进一步假设你有要求的演员,他的名字的函数:
void print_information(const Actor& actor)
{
std::cout << "the actor is called " << actor.name() << std::endl;
}
这是不可能在编译时推断演员是否会真正成为一名球员或一个怪物。由于它们有不同的方法,决定调用哪个方法必须推迟到运行时。编译器为每个角色对象添加附加信息,以便在运行时作出此决定。
在每一个编译器,我知道,这些额外的信息是一个指针(通常称为的vptr)函数指针(通常称为VTBL)特定于具体类的表。也就是说,所有玩家对象共享相同的虚拟表,其中包含指向所有玩家方法的指针(对于怪物也是如此)。在运行时,通过从应该在其上调用方法的对象的vptr所指向的vtbl中选择方法来找到正确的方法。
虚函数表是一个实现细节 - 这是编译器如何在类中实现多态方法。
考虑
class Animal
{
virtual void talk()=0;
}
class Dog : Animal
{
virtual void talk() {
cout << "Woof!";
}
}
class Cat : Animal
{
virtual void talk() {
cout << "Meow!";
}
}
现在我们必须
A* animal = loadFromFile("somefile.txt"); // from somewhere
animal->talk();
我们怎么知道这些talk()
版本被称为?动物对象有一张表格,指向与该动物一起使用的虚拟功能。例如,talk
可能是在第三偏移,如果有两个其他的虚拟方法:
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]
当我们有一个Animnal
例如,我们不知道哪个talk()
方法调用。我们通过查看虚拟表并获取第三个条目来找到它,因为编译器知道对应于talk
指针(编译器知道Animal上的虚拟方法,因此知道vtable中指针的顺序。)
给定一个Animal,为了调用正确的talk()方法,编译器添加代码来获取第三个函数指针并使用它。然后,这指向适当的实现。
对于非虚拟方法,这不是必需的,因为被调用的实际函数可以在编译时确定 - 只有一个可能的函数可以被调用用于非虚拟调用。
*你*不需要它,但编译器。 – EJP 2017-02-13 23:53:05