当前位置: 首页 > article >正文

C++ 虚函数、虚函数表、静态绑定与动态绑定笔记

C++ 虚函数、虚函数表、静态绑定与动态绑定笔记


1. 什么是虚函数?

  • 虚函数(virtual function)是类的成员函数,由 virtual 关键字修饰。
  • 虚函数的调用方式在运行时通过对象的实际类型决定,而不是在编译时由指针或引用的静态类型决定。
  • 作用
    • 支持 运行时多态
    • 允许通过基类指针或引用调用派生类的重写函数。
  • 语法
    class Base {
    public:
        virtual void func();  // 定义虚函数
    };
    

2. 虚函数表(V-Table)

虚函数表是 C++ 编译器实现动态绑定的核心机制。

虚函数表的定义
  • 每个包含虚函数的类都有一张 虚函数表 (Virtual Table, V-Table)
  • 表中存储:
    1. RTTI (Run-Time Type Information):运行时类型信息,存储类的类型信息。
    2. 虚函数地址:类中所有虚函数的函数地址。
虚函数表的生成
  • 基类:当类中定义了虚函数时,编译器会为该类生成一张虚函数表,存储该类虚函数的地址。
  • 派生类:派生类继承基类时会生成自己的虚函数表:
    • 如果派生类未重写基类的虚函数,则直接继承基类的虚函数表。
    • 如果派生类重写了基类的虚函数,则其虚函数表中对应位置的地址会被派生类的实现覆盖。

3. 虚函数表指针(V-Ptr)

  • 每个对象在内存中包含一个 虚函数表指针 (V-Ptr),指向该类的虚函数表。
  • 位置
    • 对象的前 4 个字节(在 32 位系统下)或前 8 个字节(在 64 位系统下)存储 V-Ptr。
  • 作用
    • 指向所属类的虚函数表,用于动态绑定。

4. 虚函数表的内存布局

假设有如下类结构:

class Base {
public:
    virtual void func1();
    virtual void func2();
};

class Derived : public Base {
public:
    void func1() override;
};
  • 基类 Base 的虚函数表

    V-Table for Base:
    -----------------
    | RTTI Pointer   | --> 存储 "Base" 类型信息
    | Address of func1 --> &Base::func1
    | Address of func2 --> &Base::func2
    
  • 派生类 Derived 的虚函数表

    V-Table for Derived:
    ---------------------
    | RTTI Pointer       | --> 存储 "Derived" 类型信息
    | Address of func1   | --> &Derived::func1 (重写后的地址)
    | Address of func2   | --> &Base::func2 (未重写的函数地址)
    

5. 静态绑定与动态绑定

绑定类型绑定时间适用函数类型特点
静态绑定编译时 (Compile-time)普通函数函数的调用地址在编译阶段确定,直接绑定到函数地址。
动态绑定运行时 (Run-time)虚函数函数的调用地址在运行时通过虚函数表查找,根据对象实际类型决定。

6. 静态绑定与动态绑定的代码对比

示例代码
#include <iostream>
using namespace std;

class Base {
public:
    void staticFunc() {
        cout << "Base::staticFunc()" << endl;
    }
    virtual void dynamicFunc() {
        cout << "Base::dynamicFunc()" << endl;
    }
};

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

int main() {
    Base* basePtr = new Derived();

    basePtr->staticFunc();  // 静态绑定,调用 Base::staticFunc()
    basePtr->dynamicFunc(); // 动态绑定,调用 Derived::dynamicFunc()

    delete basePtr;
    return 0;
}
输出结果
Base::staticFunc()
Derived::dynamicFunc()
代码解析
  • basePtr->staticFunc()
    • 静态绑定:调用 Base 中的 staticFunc,因为静态绑定与指针的静态类型相关。
  • basePtr->dynamicFunc()
    • 动态绑定:调用 Derived 中的 dynamicFunc,通过虚函数表实现。

7. 虚函数表的工作流程

编译阶段
  1. 为包含虚函数的类生成虚函数表。
  2. 如果派生类重写了虚函数,则更新虚函数表中对应的函数地址。
  3. 对象在内存中存储虚函数表指针。
运行阶段
  1. 通过对象的虚函数表指针访问虚函数表。
  2. 根据虚函数表中的地址,调用对应的函数。

8. 动态绑定的实现机制

动态绑定的汇编实现

以以下代码为例:

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

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

int main() {
    Base* basePtr = new Derived();
    basePtr->func();
    delete basePtr;
    return 0;
}

对应的汇编代码:

mov eax, [basePtr]       ; 将对象的地址加载到寄存器 eax
mov edx, [eax]           ; 通过 V-Ptr 获取虚函数表地址
mov ecx, [edx + offset]  ; 从虚函数表中读取虚函数地址
call ecx                 ; 调用虚函数
过程解析
  1. basePtr 是一个 Derived 类型的对象,[basePtr] 的前 4 字节存储虚函数表指针。
  2. mov edx, [eax]:将虚函数表地址加载到寄存器 edx
  3. mov ecx, [edx + offset]:通过偏移量查找虚函数地址。
  4. call ecx:调用从虚函数表中找到的虚函数地址。

9. 虚函数对内存的影响

对象的内存布局
对象类型内存组成
无虚函数对象仅包含类的成员变量。
有虚函数对象成员变量 + 虚函数表指针 (V-Ptr)。
虚函数表对内存的影响
  • 虚函数表存储在只读数据区(.rodata),在运行时不可修改。
  • 每个类对应一张虚函数表,虚函数数量决定了虚函数表的大小。
  • 注意:虚函数的数量不影响对象的大小,但会增加虚函数表的大小。

10. 覆盖、隐藏与重载

概念定义适用范围
覆盖派生类中重写基类的虚函数,要求返回值、函数名、参数列表均相同。虚函数
隐藏派生类中定义了与基类同名但参数列表不同的函数,基类的函数被隐藏。普通函数、非虚函数
重载同一类中定义的同名函数,通过参数列表的不同实现区分。所有成员函数

11. 面试常见问题总结

  1. 虚函数表的作用是什么?

    • 用于实现动态绑定。
    • 存储虚函数地址和 RTTI 类型信息。
  2. 虚函数会影响对象的大小吗?

    • 对象大小增加一个虚函数表指针(V-Ptr)。
    • 虚函数数量不会直接影响对象大小,但会增加虚函数表的大小。
  3. 动态绑定的核心机制是什么?

    • 通过虚函数表在运行时查找函数地址并调用。
  4. 静态绑定与动态绑定的区别?

    • 静态绑定在编译时确定调用函数,适用于普通函数。
    • 动态绑定在运行时通过虚函数表查找函数地址,适用于虚函数。


http://www.kler.cn/a/449765.html

相关文章:

  • 【python高级】342-TCP服务器开发流程
  • 绕组识别标签规范
  • 网络管理 详细讲解
  • Java基础面试题19:解释什么是Servlet链
  • Java - 日志体系_Apache Commons Logging(JCL)日志接口库
  • Docker快速入门到项目部署
  • 记录--uniapp 安卓端实现录音功能,保存为amr/mp3文件
  • Blazor项目中使用EF读写 SQLite 数据库
  • 在Ubuntu上通过Docker部署NGINX服务器
  • 第三节:GLM-4v-9B数据加载之huggingface数据加载方法教程(通用大模型数据加载实列)
  • 96 vSystem
  • 区块链与比特币:技术革命的双子星
  • ImportError: DLL load failed while importing jiter
  • 工信部“人工智能+”制造行动点亮CES Asia 2025
  • 便捷的线上游戏陪玩、线下家政预约以及语音陪聊服务怎么做?系统代码解析
  • 基于Spring Boot的电影网站系统
  • K8S Ingress 服务配置步骤说明
  • 1114 Family Property (25)
  • 【环境搭建】Python、PyTorch与cuda的版本对应表
  • 在Vue2中,el-tree组件的页面节点前三角符号仅在有下级节点时显示
  • LeetCode 54. 螺旋矩阵 (C++实现)
  • Deformable DETR:Deformable Transformers for End-to-End Object Detection论文学习
  • 【从零开始入门unity游戏开发之——C#篇26】C#面向对象动态多态——接口(Interface)、接口里氏替换原则、密封方法(`sealed` )
  • Springboot项目本地连接并操作MySQL数据库
  • 数据结构概念介绍
  • Javascript数据结构——二叉树篇