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. 调用的是虚函数。
相关推荐
腥臭腐朽的日子熠熠生辉23 分钟前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
Harrison_zhu24 分钟前
Ubuntu18.04 编译 Android7.1代码报错
android
ejinxian24 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之30 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
云 无 心 以 出 岫1 小时前
贪心算法QwQ
数据结构·c++·算法·贪心算法
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
俏布斯1 小时前
算法日常记录
java·算法·leetcode
27669582921 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息1 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
换一颗红豆1 小时前
【C++ 多态】—— 礼器九鼎,釉下乾坤,多态中的 “风水寻龙诀“
c++