C++虚函数表

前言

使用过Java的同学应该特别熟悉多态,即耳熟能详的一句话:可以使用父类引用来指向子类对象,而实现起来非常容易,只需要重写父类的方法或者实现接口的方法即可:

java 复制代码
//Java代码

class Parent {
    void show() {
        System.out.println("Inside Parent");
    }
}

class Child extends Parent {
    void show() {
        System.out.println("Inside Child");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child();
        obj.show(); // 调用的是子类的 show() 方法
    }
}

在Java中,这里是使用父类Parent的引用obj,指向了子类对象,然后调用的是子类的show()方法。

但是在C++中,如果按同样类似的写法,却得到的结果不一样:

C++ 复制代码
//C++代码

#include <iostream>

class Parent {
public:
    void show() {
        std::cout << "Inside Parent" << std::endl;
    }
};

class Child : public Parent {
public:
    void show() {
        std::cout << "Inside Child" << std::endl;
    }
};

int main() {
    Parent* obj = new Child();
    obj->show(); // 调用的是父类的 show() 方法
    delete obj;
    return 0;
}

在C++中,这里使用父类的指针,指向子类对象,但是最终调用的是父类的show()方法。那如何实现像Java那种效果呢?就需要使用虚函数,本篇文章深入探究虚函数以及虚函数的原理--虚函数表(后面简称虚表)的相关知识。

正文

关于虚函数的使用,以及纯虚函数定义接口等,这些基础知识,暂时就不详细说明了,重点关注虚表。

虚表是属于类

虚表是函数指针类型的数组,指向该类定义的虚函数。

在C++中,只要一个类包含了一个虚函数,那么就有由一个虚表来维护。虚表是属于类的,而非属于某个对象,一个类只需要一个虚表,所以该类对象共用一个虚表。

当类B继承至类A,继承类也可以调用A的函数,如果A是一个包含虚表的基类,那么继承类B也拥有自己的虚表。

理解类的虚表非常重要,比如有代码:

C++ 复制代码
class A {
public:
    A();
    virtual void vfunc1() { qDebug() << "A vfunc1()"; }
    virtual void vfunc2() { qDebug() << "A vfunc2()"; }
    void func1() { qDebug() << "A func1()"; }
    void func2() { qDebug() << "A func2()"; }

private:
    int m_data1, m_data2;
};

那么类A的虚表如图:

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针 。非虚函数的函数,调用不需要虚表,所以虚表中不会保存。虚表的创建在编译时期,即在编译时期,属于一个类的虚表就构造出来了。

对象的虚表指针

我们使用IDE调试模式,来看一下类A的对象包含哪些东西:

可以发现除了其数据成员外,还有一个__vptr指针,这个指针是虚表指针,指向类的虚表 。为了指定对象的虚表,编译器在类中添加了__vptr指针,专门用来指向虚表。

这里可以知道,同一个类的多个对象,其虚表指针所指向的虚表是一样的:

效果如图:

动态绑定

重点来了,C++是如何通过虚表实现多态的,即动态绑定,有如下3个类:

C++ 复制代码
class A {
public:
    A();
    virtual void vfunc1() { qDebug() << "A vfunc1()"; }
    virtual void vfunc2() { qDebug() << "A vfunc2()"; }
    void func1() { qDebug() << "A func1()"; }
    void func2() { qDebug() << "A func2()"; }

private:
    int m_data1, m_data2;
};

class B : public A {
public:
    B();
    virtual void vfunc1() { qDebug() << "B vfunc1()"; }
    void func1() { qDebug() << "B func1()"; }

private:
    int m_data3;
};

class C : public B {
public:
    C();
    virtual void vfunc2() { qDebug() << "c vfunc2()"; }
    void func2() { qDebug() << "C func2()"; }

private:
        int m_data1, m_data4;
};

记住:虚表是属于类的,上面代码的虚表通过IDE调试模式如下:

虚表关系如图:

我们来仔细分析:

  1. 类A包含2个虚函数,所以A的虚表元素是2个。

  2. 类B继承至A,但是重写了虚函数,对于重写的虚函数,会增加新的地址来保存新的函数指针 。所以对于类B的虚表,有一个是继承至A,即A::vfunc2*(),还有一个是新的函数,即B::vfunc1()。

  3. 而对于C来说,它继承至B,但是重写了虚函数,所以会多出一个C::vfunc2(),那么另一个虚函数指针是执行类B的vfunc1()还是类A的vfunc1()呢?

    这里会指向B::vfunc1(),规律非常简单:对象的虚表指针用来指向类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数,所以C的虚表先是指向B的虚表,然后由于重写虚函数的原因,会修改其中一个指针的值。

基类指针指向子类对象

搞明白上面的继承关系后,多态就非常容易理解了,比如下面代码:

C++ 复制代码
B b;
A *p = &b;
p->vfunc1();

这里使用p来调用vfunc1(),最终会调用类B中的虚函数,过程分析如下:

  1. 程序执行p->vfunc1()时,会发现p是一个指针,而且调用的是虚函数。
  2. 根据虚表指针p->__vptr来访问对象b对应的虚表 。虽然指针p是基类A的类型,但是__vptr也是基类的一部分,是编译器加的,所以p->__ptr可以访问对象的虚表。
  3. 在虚表中查找调用的函数 ,由于虚表是在编译时就确定,所以p->vfunc1()根据上面的图可知会调用B的虚函数 ,即重写的虚函数B::vfunc1()。

这个过程就是动态绑定来实现的多态,非常容易理解。

总结

什么情况下会发生动态绑定呢?有如下几个条件:

  1. 通过指针调用函数。
  2. 指针upcast向上转型,比如继承类向基类的转换。
  3. 调用的是虚函数。
相关推荐
充值内卷8 分钟前
我给桌面加了个鱼缸:C++ 写后端,WebView2 里养一座 Three.js 小岛
开发语言·javascript·c++
三无少女指南30 分钟前
从这里开始:Java 后端与 AI 工程的三条阅读路线
java·学习路线·ai工程
香蕉大香蕉(06敬畏市场)1 小时前
MySQL 单点详解(07)
java·数据库·mysql·ai·博客·八股
nice_lcj5201 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
骑着蜗牛撵大象3272 小时前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp
tryxr2 小时前
JVM 垃圾回收机制
java·jvm·gc·垃圾回收
传奇开心果编程2 小时前
【Jetpack Compose娓娓道来】第29课:状态容器与UI事件建模——把“散装状态”变成“可穷举的契约”
android·kotlin·移动开发·android jetpack·ui框架·开发范式·声明式ui
揣一尾游鱼yu3 小时前
Spring上(IOC)笔记
java·笔记·spring·ioc
荣码3 小时前
长上下文与大文档处理:200页PDF怎么喂给AI,我踩了4个坑
java·python
樱花树下的数学喵3 小时前
手写简易 STL string|C++ 字符串底层实现
开发语言·c++