C++中的虚函数表介绍

时间:2022-08-12 01:43:34

        在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。通常情况下,如果我们不使用某个函数,则无须为该函数提供定义。但是我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为连编译器也无法确定到底会使用哪个虚函数。虚函数的作用就是实现多态性

        对虚函数的调用可能在运行时才被解析:当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

        动态绑定只有当我们通过指针或引用调用虚函数时才会发生。当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本中。当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同

        派生类中的虚函数:当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。同样,派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配

        虚函数与默认实参:和其它函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

        回避虚函数的机制:在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的。通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。什么时候我们需要回避虚函数的默认机制呢?通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

        纯虚函数一个纯虚函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处。我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个=0的函数提供函数体

        含有纯虚函数的类是抽象基类:含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类(abstract base class)。抽象基类负责定义接口,而后续的其它类可以覆盖该接口。我们不能(直接)创建一个抽象基类的对象

        派生类构造函数只初始化它的直接基类

        多重继承(multiple inheritance):是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性。

        虚继承(virtual inheritance):尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类。在默认情况下,派生类中含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类(virtual base class)。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。

        使用虚基类:我们指定虚基类的方式是在派生列表中添加关键字virtual。virtual说明符表明了一种愿望,即在后续的派生类当*享虚基类的同一份实例。至于什么样的类能够作为虚基类并没有特殊规定。如果某个类指定了虚基类,则该类的派生仍按常规方式进行。不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作

        虚基类成员的可见性:因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问这个被覆盖的成员。但是如果成员被多余一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新的版本。

        在虚派生中,虚基类是由最低层的派生类初始化的

        虚继承的对象的构造方式:含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关

        构造函数与析构函数的次序:一个类可以有多个虚基类。此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。和往常一样,对象的销毁顺序与构造顺序正好相反。

        每个含有虚函数的类有一张虚函数表,表中的每一项是一个虚函数的地址。虚函数表的指针占4个字节大小,存在于对象实例中最前面的位置。我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中的函数指针,并调用相应的函数。

        类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。

        虚函数(Virtual Function)是通过一张虚函数表(VirtualTable)来实现的,简称为V-Table。

        虚函数表的结构:它是一个函数指针表,每一个表项都指向一个函数。任何一个包含至少一个虚函数的类都会有这样一张表。Virtual Table只包含虚函数的指针,没有函数体。每个派生类的Virtual Table继承了它各个基类的Virtual Table,如果基类Virtual Table中包含某一项,则其派生类的Virtual Table中也将包含同样的一项,但是两项的值可能不同。如果派生类覆盖了该项对应的虚函数,则派生类Virtual Table的该项指向覆盖后的虚函数,没有覆盖的话,则沿用基类的值。

        每一个类只有唯一的一个Virtual Table,不是每个对象都有一个VirtualTable。虚函数表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚函数表即可。同一个类的所有对象都使用同一个虚函数表。Virtual Table是编译期间建立,执行期间查表执行

        在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。所以,在类对象的内存布局中,首先是该类的Virtual Table指针,然后才是对象数据。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

        虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚函数表,所以虚函数表的元素并不包括普通函数的函数指针。虚函数表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚函数表就可以构造出来了。

        为了指定对象的虚函数表,对象内部包含一个虚函数表的指针,来指向自己所使用的虚函数表。为了让每个包含虚函数表的类的对象都拥有一个虚函数表指针,编译器在类中添加了一个指针即*__vptr,用来指向虚函数表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚函数表。

        C++中的虚函数(Virtual Function)是用来实现动态多态性的,指的是当基类指针指向其派生类实例时,可以用基类指针调用派生类中的成员函数。如果基类指针指向不同的派生类,则它调用同一个函数就可以实现不同的逻辑,这种机制可以让基类指针有”多种形态”,它的实现依赖于虚函数表。虚函数表(Virtual Table)是指在每个包含虚函数的类中都存在着一个函数地址的数组。

    A virtual methodtable (VMT), virtual function table, virtual call table, dispatch table,vtable, or vftable is a mechanism used in a programming language to support dynamic dispatch (or run-time method binding).

        Whenever a class defines a virtual function(ormethod), most compilers add a hidden member variable to the class which pointsto an array of pointers to (virtual) functions called the virtual method table.These pointers are used at runtime to invoke the appropriate function implementations, because at compile time it may not yet be known if the base function is to be called or a derived one implemented by a class that inheritsfrom the base class.

        An object's virtual method table will contain the addresses of the object's dynamically bound methods. Method calls are performedby fetching the method's address from the object's virtual method table. The virtual method table is the same for all objects belonging to the same class,and is therefore typically shared between them.Objects belonging to type-compatible classes (for example siblings in an inheritance hierarchy) will have virtual method tables with the same layout: the address of a given method will appear at the same offset for all type-compatible classes. Thus, fetching the method's address from a given offset into a virtual method table will get the method corresponding to the object's actual class.

        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”.

        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 isa 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.

        Calling a virtual function is slower than calling anon-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.

        The virtual table is a structure used at runtime to resolve function calls. But it doesn't control access -- the compiler handles whether you should or should not be able to call a function.

        以上内容主要摘自:《C++Primer(Fifth Edition 中文版)》、Learn C++  、 陈皓 Blog 

        测试代码:

#include "virtual_function_table.hpp"#include <iostream>

namespace virtual_function_table_ {

// reference: http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/
class Base {
public:
Base() { fprintf(stdout, "Base::Base\n"); }
virtual void function1() { fprintf(stdout, "Base::function1\n"); }
virtual void function2() { fprintf(stdout, "Base::function2\n"); }
void f1() { fprintf(stdout, "Base::f1\n"); }
~Base() { fprintf(stdout, "Base::~Base\n"); }
};

class D1 : public Base {
public:
D1() { fprintf(stdout, "D1::D1\n"); }
virtual void function1() override { fprintf(stdout, "D1::function1\n"); }
virtual void function3() { fprintf(stdout, "D1::function3\n"); }
void f2() { fprintf(stdout, "D1::f2\n"); }
~D1() { fprintf(stdout, "D1::~D1\n"); }
};

class D2 : public Base {
public:
D2() { fprintf(stdout, "D2::D2\n"); }
virtual void function2() override { fprintf(stdout, "D2::function2\n"); }
void f3() { fprintf(stdout, "D2::f3\n"); }
~D2() { fprintf(stdout, "D2::~D2\n"); }
};

class D3 {
public:
D3() { fprintf(stdout, "D3::D3\n"); }
void f4() { fprintf(stdout, "D3::f4\n"); }
~D3() { fprintf(stdout, "D3::~D3\n"); }
};

int test_virtual_function_table_1()
{
D1* p1 = new D1();
fprintf(stdout, "p1 address: %p\n", (void*)p1);
Base* b = static_cast<D1*>(p1);
fprintf(stdout, "b address: %p\n", (void*)b);
b->function1();
b->function2();
// 任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法
//b->function3(); // Error: class "virtual _function_table_::Base" 没有成员 "function3"
delete p1;
fprintf(stdout, "\n");

Base* p2 = new D1();
fprintf(stdout, "p2 address: %p\n", (void*)p2);
p2->function1();
p2->function2();
delete dynamic_cast<D1*>(p2);
fprintf(stdout, "\n");

D1 d1 = D1();
fprintf(stdout, "d1 address: %p\n", (void*)&d1);
d1.function1();
d1.function2();
fprintf(stdout, "\n");

D2 d2 = D2();
fprintf(stdout, "d2 address: %p\n", (void*)&d2);
Base* b2 = &d2;
fprintf(stdout, "b2 address: %p\n", (void*)b2);
b2->function1();
b2->function2();
fprintf(stdout, "\n");

D3 d3 = D3();
fprintf(stdout, "\n");

return 0;
}

} // namespace virtual_function_table_

        以上测试代码,可以在VS2013的debug模式下,通过局部变量窗口查看相应变量的虚函数表的信息,如下:

C++中的虚函数表介绍

C++中的虚函数表介绍

C++中的虚函数表介绍

        GitHubhttps://github.com/fengbingchun/Messy_Test