首页 > 编程语言 >C++虚函数表

C++虚函数表

时间:2024-11-19 19:18:12浏览次数:3  
标签:函数 对象 Derived Dog C++ 基类 指针

一、概念

在 C++ 中,虚函数表(Virtual Function Table,简称 vtable)是实现多态机制的一个重要底层数据结构。当一个类中包含了虚函数时,编译器会为这个类创建一个虚函数表,用来存放该类的虚函数的地址。每个包含虚函数的类的对象实例中,会隐含一个指针(通常称为虚指针,vptr),它指向所属类的虚函数表。

二、作用

虚函数表的核心作用就是支持多态性,也就是让通过基类指针或引用调用虚函数时,能够根据对象的实际类型(是基类对象还是派生类对象)来决定调用哪个类中重写后的虚函数版本。

例如,有如下基类和派生类的定义:

class Base {
public:
    virtual void func() {
        std::cout << "Base::func()" << std::endl;
    }
};

class Derived : public Base {
public:
    void func() override {
        std::cout << "Derived::func()" << std::endl;
    }
};

当通过基类指针来操作对象时:

Base* ptr = new Derived();
ptr->func();  // 会调用Derived类中重写后的func函数,这就是多态的体现,而虚函数表在背后起到了关键作用

三、虚函数表的结构与存储

  • 结构:虚函数表本质上就是一个函数指针数组,数组中的每个元素都是一个虚函数的地址。在这个数组中,虚函数的存放顺序和类中声明虚函数的顺序是一致的。例如,如果一个类先声明了虚函数 func1,再声明虚函数 func2,那么在虚函数表中,存放 func1 地址的元素就在前面,存放 func2 地址的元素紧随其后。

  • 存储:对于一个类来说,它的虚函数表在内存中只有一份副本,不管这个类创建了多少个对象实例,所有对象共享这同一个虚函数表(通过各自的虚指针指向它)。并且虚函数表一般是在编译阶段就确定好了其内容和布局,存放在只读数据段(因为函数地址在程序运行过程中通常是固定不变的)。

四、虚指针(vptr)

  • 虚指针是类对象中的一个隐含成员,它的类型是指向虚函数表的指针。在对象创建时(构造函数调用时),编译器会自动将对象的虚指针初始化为指向其所属类的虚函数表。例如,对于前面定义的 Derived 类的对象,当创建它时:
Derived d;
// 在d对象的内存布局中,有一个隐含的vptr,它指向Derived类对应的虚函数表
  • 不同类型(基类或派生类)但存在继承关系且包含虚函数的对象,其虚指针指向不同的虚函数表,从而实现了多态调用时函数的正确选择。比如基类 Base 的对象的虚指针指向 Base 类的虚函数表,而 Derived 类对象的虚指针指向 Derived 类的虚函数表。

五、单继承下的虚函数表示例

以下通过一个简单的代码示例结合内存布局来更深入理解单继承情况下的虚函数表:

class Animal {
public:
    virtual void eat() {
        std::cout << "Animal is eating" << std::endl;
    }
    virtual void sleep() {
        std::cout << "Animal is sleeping" << std::endl;
    }
};

class Dog : public Animal {
public:
    void eat() override {
        std::cout << "Dog is eating" << std::endl;
    }
    void sleep() override {
        std::cout << "Dog is sleeping" << std::endl;
    }
};
  • 内存布局上,Animal 类对象有一个虚指针(假设对象起始地址为 0x1000),这个虚指针指向 Animal 类的虚函数表(假设虚函数表地址为 0x2000),在 0x2000 这个地址开始的虚函数表中,存放着 Animal::eat 和 Animal::sleep 这两个虚函数的地址,顺序和类中声明顺序一致。
  • 对于 Dog 类对象(假设起始地址为 0x3000),它也有一个虚指针,这个虚指针指向 Dog 类自己的虚函数表(假设地址为 0x4000),在 0x4000 开始的虚函数表中,存放的是 Dog::eat 和 Dog::sleep 的地址,因为 Dog 重写了 Animal 的虚函数,所以这里存放的是 Dog 类中重写后版本的虚函数地址。

当执行如下代码:

Animal* a = new Dog();
a->eat();

通过 a 这个基类指针(实际指向 Dog 类对象)调用 eat 函数时,会顺着对象中的虚指针找到 Dog 类的虚函数表,然后根据虚函数表中存放的函数地址调用 Dog::eat 函数,实现了多态行为。

六、多继承下的虚函数表情况(更复杂一些)

在多继承场景下,情况会复杂一些,因为一个派生类有多个基类,每个基类如果有虚函数表,那派生类对象中就会有多个虚指针(每个基类对应一个虚指针,指向各自基类的虚函数表),同时还可能存在一个额外的属于派生类自己的虚函数表(如果派生类新增了虚函数的话)。

例如:

class Base1 {
public:
    virtual void func1() {
        std::cout << "Base1::func1()" << std::endl;
    }
};

class Base2 {
public:
    virtual void func2() {
        std::cout << "Base2::func2()" << std::endl;
    }
};

class Derived : public Base1, public Base2 {
public:
    void func1() override {
        std::cout << "Derived::func1()" << std::endl;
    }
    void func2() override {
        std::cout << "Derived::func2()" << std::endl;
    }
};

在内存布局上,Derived 类对象中,首先会有一个指向 Base1 类虚函数表的虚指针(因为 Base1 是第一个基类),接着是 Base1 类相关的数据成员(如果有的话),然后是指向 Base2 类虚函数表的虚指针,再之后是 Base2 类相关的数据成员(如果有话)。如果 Derived 类自己新增了虚函数,还会有一个属于 Derived 类自己的虚函数表来存放这些新增虚函数的地址(当然这涉及到更复杂的内存布局调整和函数调用查找机制了)。

总之,虚函数表是 C++ 多态机制实现的关键底层支撑,理解它对于深入掌握 C++ 面向对象编程以及多态特性有着非常重要的意义。

 

 

 

 

 

 

标签:函数,对象,Derived,Dog,C++,基类,指针
From: https://blog.csdn.net/Visual_progress/article/details/143852322

相关文章

  • 什么是 C++ 中的友元函数和友元类?友元的作用是什么?有什么注意事项?
    友元函数定义友元函数是在类中声明的非成员函数,它可以访问类的私有(private)和保护(protected)成员。友元函数虽然不是类的成员函数,但它被授予了访问类内部成员的特殊权限。声明方式在类的定义中,使用friend关键字来声明友元函数。classMyClass{private:intprivateDat......
  • c++等级考试第8级第2卷
                                       道路(2024.3八级)代码#include<iostream>#include<algorithm>#include<cmath>#include<cstdio>#include<vector>#include<cstring>usingnamespacestd;st......
  • 【入门】字符串的反码c++
    #include<bits/stdc++.h>usingnamespacestd;intmain(){ //一、分析问题 //已知:一个长度不超过80个字符的字符串。 //未知:字符串的反码。 //关系:如果这是一个小写字符,则它和字符a的距离与它的反码和字符z的距离相同;如果是一个大写字符,则它和字符A的距离与它......
  • 在bug中巩固C++
    记录自己的各种报错,在错误中学习ing结构体全局变量的声明与初始化问题#include<iostream>usingnamespacestd;//声明一个结构体BooksstructBook{stringname;stringauthor;stringsubject;intid;//构造函数Book(stringname,stringa......
  • STM32(hal库)中,为什么DMA没有MSP函数?
            在STM32HAL库中,DMA(直接存储器访问)并没有像其他某些外设(如USART、SPI等)那样拥有专门的MSP(MCUServicesPackage)初始化函数,这主要是由于DMA的特性和HAL库的设计哲学所决定的。        首先,需要明确的是,MSP函数通常是由STM32CubeMX工具为特定的外设生成......
  • 【C++】十六进制数据的字节序排列问题、大小端
    十六进制数据的字节序排列问题,涉及到大小端(Endianness)的概念。包括它与大小端存储方式的关系。1.十六进制数据在内存中的存储定义的数据#defineFRAME_TYPE_PARAM_SET0x30010x3001是一个16位(2字节)整数。在内存中,0x3001的存储方式依赖于系统的字节序:小端......
  • C++编程:通过多线程与协程优化阻塞型任务的调度性能
    文章目录0.引言1.多线程VS多线程+协程1.1示例1:使用传统的多线程进行矩阵乘法1.2.示例2:使用协程优化阻塞型任务3.分析与对比0.引言我们知道:多线程:适用于处理计算密集型任务或IO操作较少的场景,但会因为线程切换和创建销毁的开销而影响性能。协程:适用于处......
  • 实验4 C++
    任务2:GradeCalc.cpp1#pragmaonce2#include<iostream>3#include<vector>4#include<string>5#include<algorithm>6#include<numeric>7#include<iomanip>89usingstd::vector;10usingstd::......
  • JavaScript函数式编程指南
    前言本文内容来自于《JavaScript函数式编程指南》,可以看作是对原书内容进行提炼和总结,若您有需要或感觉有出入请参原书。一、走进函数式面向对象编程(OOP)通过封装变化使得代码更易理解。函数式编程(FP)通过最小化变化使得代码更易理解。——MichaelFeathers(Twitter)函......
  • 建立函数及其参数的结果缓存
    fromfunctoolsimportwrapsimporttimeclassCacheManager:def__init__(self):self._cache={}defget_cache_obj(self,key):"""获取缓存对象"""returnself._cache.get(key)defadd_cache_obj(......