C++中的虚函数及虚函数表


一、虚函数及虚函数表的定义

虚函数虚函数就是在基类中定义一个未实现的函数名,使用虚函数的核心目的就是通过基类访问派生类定义的函数,从而实现运行时多态

虚函数主要有如下限制:

  1. 只有类的成员函数才能声明为虚函数。
  2. 静态成员函数不能是虚函数。
  3. 内联函数不能是虚函数。
  4. 构造函数不能为虚函数。
  5. 基类的析构函数可以是虚函数且通常声明为虚函数。

虚函数表:对于一个类来说,如果类中存在虚函数,那么该类的大小就会多出4个字节,这刚好是一个指针的大小,而这个指针指向的就是一个虚函数表。所以如果对象存在虚函数,那么编译器就会生成一个指向虚函数表的指针,所有的虚函数指针都存在于这个表中,虚函数表就可以理解为一个函数指针数组,每个单元用来存放虚函数的地址

虚函数表位于只读数据段(.rodata),即C++内存模型中的常量区,虚函数代码则位于代码段(.text),也就是C++内存模型中的代码区。二者在内存中的位置大致如下:


二、虚函数表指针和虚函数表的创建时机

虚函数表指针:对于虚函数表指针vptr来说,由于vptr是基于对象的,所以对象在实例化的时候,vptr就会被创建,即在运行时创建。即在实例化对象的时候调用构造函数,执行vptr的赋值代码,从而将虚函数表的地址赋值给vptr。不过更具体地说,其实在编译时vptr就会被放到类的内存空间里,只是我们看不到而已,但是只有在类实际构造时vptr才会被真正实例化,指向虚函数表,具有实际意义

虚函数表:对于虚函数表来说,在编译的过程中编译器就会为含有虚函数的类创建虚函数表,并且编译器会在构造函数中插入一段代码,这段代码用来给虚函数指针赋值。因此虚函数表是在编译的过程中创建


三、虚函数实现多态的原理

虚函数的调用过程:当一个对象需要调用虚函数时,先通过对象内存空间中的虚函数表指针(vptr)找到该类对应的虚函数表(vtbl),然后在vtbl中通过虚函数指针寻找想要调用的虚函数,进而完成虚函数的调用。

派生类重写了部分基类虚函数的多继承为例,考虑以下代码:

#include <iostream>

using namespace std;

class BaseA {
    
    
public:
    virtual void a1() {
    
     cout << "Base a1()" << endl; }

    virtual void a2() {
    
     cout << "Base a2()" << endl; }
};

class BaseB {
    
    
public:
    virtual void b1() {
    
     cout << "Base b1()" << endl; }

    virtual void b2() {
    
     cout << "Base b2()" << endl; }
};

class Derive : public BaseA, public BaseB {
    
    
public:
    void a1() override {
    
     cout << "Derive a1()" << endl; }

    void b1() override {
    
     cout << "Derive b1()" << endl; }
};

int main() {
    
    
    cout << "----------BaseA----------" << endl;
    BaseA *base_a = new BaseA;
    long *vptr_a = (long *) base_a; // base_a的虚函数表指针
    long *vtbl_a = (long *) (*vptr_a); // base_a的虚函数表
    for (int i = 0; i < 2; i++) {
    
    
        printf("BaseA-vtbl[%d]: %p\n", i, vtbl_a[i]);
    }

    cout << "----------BaseB----------" << endl;
    BaseB *base_b = new BaseB;
    long *vptr_b = (long *) base_b; // base_b的虚函数表指针
    long *vtbl_b = (long *) (*vptr_b); // base_b的虚函数表
    for (int i = 0; i < 2; i++) {
    
    
        printf("BaseB-vtbl[%d]: %p\n", i, vtbl_b[i]);
    }

    cout << "----------Derive----------" << endl;
    Derive *derive = new Derive;
    long *vptr_d_a = (long *) derive;  // derive的第一个虚函数表指针
    long *vtbl_d_a = (long *) (*vptr_d_a); // derive的第一个虚函数表
    long *vptr_d_b = (long *) derive + 1; // derive的第二个虚函数表指针,加一表示偏移一个long的长度,刚好越过第一个虚函数表指针
    long *vtbl_d_b = (long *) (*vptr_d_b); // derive的第二个虚函数表
    for (int i = 0; i < 2; i++) {
    
    
        printf("Derive-BaseA-vtbl[%d]: %p\n", i, vtbl_d_a[i]);
    }
    for (int i = 0; i < 2; i++) {
    
    
        printf("Derive-BaseB-vtbl[%d]: %p\n", i, vtbl_d_b[i]);
    }

    return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -w -std=c++11
atreus@MacBook-Pro % ./main                                
---------- BaseA ----------
BaseA-vtbl[0]: 0x1028aefa8
BaseA-vtbl[1]: 0x1028aefe4
---------- BaseB ----------
BaseB-vtbl[0]: 0x1028af044
BaseB-vtbl[1]: 0x1028af080
---------- Derive ----------
Derive-BaseA-vtbl[0]: 0x1028af11c
Derive-BaseA-vtbl[1]: 0x1028aefe4
Derive-BaseB-vtbl[0]: 0x1028af194
Derive-BaseB-vtbl[1]: 0x1028af080
atreus@MacBook-Pro % 

本例对应内存分配模型为:

在这里插入图片描述

当子类重写了父类的虚方法后,会在代码段中生成属于自己的虚函数。而多态的函数调用语句在被编译后,会根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的一系列指令,从而实现多态。


参考:

https://cloud.tencent.com/developer/article/1599283
https://blog.csdn.net/qq_41963107/article/details/107852352
https://blog.csdn.net/weixin_44693625/article/details/117393578
https://blog.csdn.net/bailang_zhizun/article/details/117124494

猜你喜欢

转载自blog.csdn.net/qq_43686863/article/details/130007100