To implement virtual functions, C++ uses a special form of late binding known as the virtual table. The virtual table is a lookup table of functions used to resolve function calls in a dynamic/late binding manner. The virtual table sometimes goes by other names, such as “vtable”, “virtual function table”, “virtual method table”, or “dispatch table”.
为了实现虚函数,C++使用了一个特别的表叫做虚表。这个虚表是一个查询表,用来解析在动态绑定时的函数调用。虚表有时候会用别的词来描述,例如 “vtable”, “virtual function table”, “virtual method table”, or “dispatch table”。
Because knowing how the virtual table works is not necessary to use virtual functions, this section can be considered optional reading.
因为知道虚表怎么回事并不是必要的,所以这一节可以被看作是选读内容。
The virtual table is actually quite simple, though it’s a little complex to describe in words. First, every class that uses virtual functions (or is derived from a class that uses virtual functions) is given its own virtual table. This table is simply a static array that the compiler sets up at compile time. A virtual table contains one entry for each virtual function that can be called by objects of the class. Each entry in this table is simply a function pointer that points to the most-derived function accessible by that class.
虚表实际上相当简单。尽管他用言语描述的时候有一点点复杂。首先,每一个使用了虚函数的类会被给设置一张虚表。这张表其实是一个编译器在编译的时候设置的静态数组。虚表的每一行是一个可以被对象调用的虚函数。虚表中的每一行是一个函数指针,指向最派生的那个虚函数版本。
Second, the compiler also adds a hidden pointer to the base class, which we will call *__vptr. *__vptr is set (automatically) when a class instance is created so that it points to the virtual table for that class. Unlike the *this pointer, which is actually a function parameter used by the compiler to resolve self-references, *__vptr is a real pointer. Consequently, it makes each class object allocated bigger by the size of one pointer. It also means that *__vptr is inherited by derived classes, which is important.
另外,编译器也添加了一个到基类的隐藏指针,叫做vptr,在一个类的对象实例化的时候vptr就被设置指向那个类的虚表。不想this指针,实际上是编译器用的一个函数参数。vtpr是一个真的指针。所以他会是的每一个类的对象的大小增大一个指针,所以这也意味着vptr也会被派生类继承,了解这一点是非常重要的。
By now, you’re probably confused as to how these things all fit together, so let’s take a look at a simple example:
到现在你可能会对这些事情是怎么糅合在一块的感到困惑,所以让我们看简单的例子:
class Base
{
public:
virtual void function1() {};
virtual void function2() {};
};
class D1: public Base
{
public:
virtual void function1() {};
};
class D2: public Base
{
public:
virtual void function2() {};
};
Because there are 3 classes here, the compiler will set up 3 virtual tables: one for Base, one for D1, and one for D2.
因为这里有三个带有虚函数的类,所以编译器会设置三张虚表。一个给Base,一个给D1,一个给D2.
The compiler also adds a hidden pointer to the most base class that uses virtual functions. Although the compiler does this automatically, we’ll put it in the next example just to show where it’s added:
编译器也添加了一个隐藏的指针给使用了虚函数的最基类。尽管我们的编译器自动的做这件事,我们会在下一个例子中展示这个指针被添加到了哪里。
class Base
{
public:
FunctionPointer *__vptr;
virtual void function1() {};
virtual void function2() {};
};
class D1: public Base
{
public:
virtual void function1() {};
};
class D2: public Base
{
public:
virtual void function2() {};
};
When a class object is created, *__vptr is set to point to the virtual table for that class. For example, when an object of type Base is created, *__vptr is set to point to the virtual table for Base. When objects of type D1 or D2 are constructed, *__vptr is set to point to the virtual table for D1 or D2 respectively.
当一个类对象被创建的时候,虚指针被设置指向那个类的虚表。举个例子当一个Base类的对象被创建的时候,vptr就设置指向那个Base的虚表。当D1或者D2的对象实例化的时候,vptr就指向D1或者D2的虚表。
Now, let’s talk about how these virtual tables are filled out. Because there are only two virtual functions here, each virtual table will have two entries (one for function1() and one for function2()). Remember that when these virtual tables are filled out, each entry is filled out with the most-derived function an object of that class type can call.
我们现在谈一下这些虚表是怎么填的 。因为这里只有两个虚函数,每一个虚表会有两个元素。记住在填充虚表的时候,会填充最派生的那个函数版本。
The virtual table for Base objects is simple. An object of type Base can only access the members of Base. Base has no access to D1 or D2 functions. Consequently, the entry for function1 points to Base::function1() and the entry for function2 points to Base::function2().
虚表对于基类对象来说是简单的,一个基类对象只能访问基类对象成员。基类对象没有访问D1或者D2的能力。因此,虚表中的第一项指向Base::function1,虚表中的第二项指向Base::function2。
The virtual table for D1 is slightly more complex. An object of type D1 can access members of both D1 and Base. However, D1 has overridden function1(), making D1::function1() more derived than Base::function1(). Consequently, the entry for function1 points to D1::function1(). D1 hasn’t overridden function2(), so the entry for function2 will point to Base::function2().
对于D1类来说虚表有一点点复杂 。一个D1对象可以访问基类的部分和派生类的部分。然而D1有一个重写函数,让D1的function1比基类的function1更派生,所以在D1的虚表中第一项写的是派生类版本的函数function1,而D1没有重写function2,所以虚表中写的是基类的function2.
The virtual table for D2 is similar to D1, except the entry for function1 points to Base::function1(), and the entry for function2 points to D2::function2().
D2的虚表和D1的虚表非常相似。
Here’s a picture of this graphically:
下面是一个图形表示
三个类画三个虚表,虚表的内容给出之后,连线就可以了。
Although this diagram is kind of crazy looking, it’s really quite simple: the *__vptr in each class points to the virtual table for that class. The entries in the virtual table point to the most-derived version of the function objects of that class are allowed to call.
尽管这个图看起来有点乱。但实际上它非常简单,每一个类中的vptr指向一个虚表,虚表中的每个指针指向最派生的那个函数版本。
So consider what happens when we create an object of type D1:
思考一下我们创建D1这个对象的时候,会发生什么。
int main()
{
D1 d1;
}
Because d1 is a D1 object, d1 has its *__vptr set to the D1 virtual table.
因为d1是D1的一个对象,所以d1的vptr指向了D1的虚表
Now, let’s set a base pointer to D1:
现在我们设置一个基类的指针指向D1.
int main()
{
D1 d1;
Base *dPtr = &d1;
return 0;
}
Note that because dPtr is a base pointer, it only points to the Base portion of d1. However, also note that *__vptr is in the Base portion of the class, so dPtr has access to this pointer. Finally, note that dPtr->__vptr points to the D1 virtual table! Consequently, even though dPtr is of type Base, it still has access to D1’s virtual table (through __vptr).
注意到因为dPtr是一个基类指针,它仅指向Base的部分。然而,也要注意的事情是:vptr是在类中的基类部分中的。所以dPtr有访问这个指针的能力。最后要注意的是dPtr的虚表指针指向了D1这个类的虚表。因此即便dPtr是一个虚类指针。它仍然可以访问到D1的虚表。
So what happens when we try to call dPtr->function1()?
在我们进行这个函数调用的时候会发生什么
int main()
{
D1 d1;
Base *dPtr = &d1;
dPtr->function1();
return 0;
}
First, the program recognizes that function1() is a virtual function. Second, the program uses dPtr->__vptr to get to D1’s virtual table. Third, it looks up which version of function1() to call in D1’s virtual table. This has been set to D1::function1(). Therefore, dPtr->function1() resolves to D1::function1()!
首先程序认出来function1是一个虚函数,然后程序使用dPtr的虚表指针找到那个虚表,然后通过查表。dPtr->function1被解析到了派生类的函数版本。
Now, you might be saying, “But what if dPtr really pointed to a Base object instead of a D1 object. Would it still call D1::function1()?”. The answer is no.
现在你可能会说,但是如果dPtr指向一个基类的对象而不是派生类对象的话,他还会调用派生类的函数吗?答案是No。
int main()
{
Base b;
Base *bPtr = &b;
bPtr->function1();
return 0;
}
In this case, when b is created, __vptr points to Base’s virtual table, not D1’s virtual table. Consequently, bPtr->__vptr will also be pointing to Base’s virtual table. Base’s virtual table entry for function1() points to Base::function1(). Thus, bPtr->function1() resolves to Base::function1(), which is the most-derived version of function1() that a Base object should be able to call.
在本例中,当b被创建的时候,虚表指针指向Base的虚表,而不是D1的虚表,在进行函数调用的时候,解析到了Base表中的最派生的那个函数版本,也就是Base的那个函数版本,所以派生类的函数版本不会被调用。
By using these tables, the compiler and program are able to ensure function calls resolve to the appropriate virtual function, even if you’re only using a pointer or reference to a base class!
用过使用这些虚表,编译器和程序能够确保函数调用被解析到合适的虚函数,即便你仅仅使用了一个指向基类的指针或者引用。
Calling a virtual function is slower than calling a non-virtual function for a couple of reasons: First, we have to use the *__vptr to get to the appropriate virtual table. Second, we have to index the virtual table to find the correct function to call. Only then can we call the function. As a result, we have to do 3 operations to find the function to call, as opposed to 2 operations for a normal indirect function call, or one operation for a direct function call. However, with modern computers, this added time is usually fairly insignificant.
**调用一个虚函数是比调用普通函数慢的。**有几个原因:首先,我们要用虚表指针找到合适的虚表。然后我们要在虚表中定位对应的函数来调用。只有这样我们才能调用这个函数。结果是,我们需要做三个步骤来找到真实的要有调用的函数。相对于普通的函数指针的两个过程,或者直接调用函数的一个过程,我们的步骤比较多。然而,在现代的计算机上面,这个增加的时间是相当划算的。
Also as a reminder, any class that uses virtual functions has a *__vptr, and thus each object of that class will be bigger by one pointer. Virtual functions are powerful, but they do have a performance cost.
提醒一下,任何使用了虚函数的函数都有一个虚表指针,因此每一个那个类的对象都会增大一个指针的大小,虚函数是非常强大的,但是他们有性能的代价。