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. 调用的是虚函数。
相关推荐
Re.不晚几秒前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐6 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
ULTRA??9 分钟前
C加加中的结构化绑定(解包,折叠展开)
开发语言·c++
码农派大星。10 分钟前
Spring Boot 配置文件
java·spring boot·后端
HerayChen16 分钟前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野17 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing112319 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
江深竹静,一苇以航19 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself35 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq041540 分钟前
J2EE平台
java·java-ee