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

八股文-C++ 运行时多态与函数调用机制详解

C++ 运行时多态与函数调用机制详解

    • 1. 重载与覆盖的对比
      • 重载示例
      • 覆盖示例
    • 2. 运行时多态的本质
    • 3. 虚函数表的实现机制
      • 代码示例
        • 运行结果
      • 虚函数表(vtable)和虚指针(vptr)的实现
        • Base 类的内存布局
        • Derived 类的内存布局
      • 动态绑定的过程
    • 4. 关键问题解答
      • 为什么 `Base` 的析构函数需要是 `virtual`?
      • 虚函数表是否会影响性能?
    • 5. C 语言的函数调用过程
      • 栈帧(Stack Frame)的结构
      • 栈帧的创建过程
        • 1. 初始状态(`main()`开始执行)
        • 2. `func(3, 5)`被`main()`调用
        • 3. `func`开始执行
        • 4. `func`执行完毕,准备返回
      • 函数调用流程示意图
        • 函数调用前(`main()`运行中)
        • 调用 `func(3, 5)`
        • `func`执行完毕,返回`main()`
    • 6. 总结
      • 关于虚函数表和运行时多态
      • 关于函数调用机制

1. 重载与覆盖的对比

特性重载(Overloading)覆盖(Override)
关系类型同一个类中的水平关系父子类之间的垂直关系
方法数量多个方法之间的关系一个方法或一对方法之间的关系
决定机制根据调用的实参表和形参表选择方法体根据对象类型(对象对应存储空间类型)决定
确定时间编译期确定运行时确定
目的提供多种实现同一功能的方法,适应不同参数类型或数量通过多态性允许子类提供与父类不同的行为

重载示例

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

覆盖示例

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

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

2. 运行时多态的本质

C++ 运行时多态(动态绑定)的核心在于:

  • 函数调用的确定时间不同

    • 静态绑定(编译时多态):编译阶段确定函数调用,如函数重载、模板等
    • 动态绑定(运行时多态):运行时根据对象类型决定函数调用,如继承加虚函数
  • 实现方式

    • C++ 通过**虚函数表(vtable)和虚指针(vptr)**来支持动态绑定

3. 虚函数表的实现机制

代码示例

#include <iostream>

class Base {
public:
    virtual void show() { std::cout << "Base::show()" << std::endl; }
    virtual void display() { std::cout << "Base::display()" << std::endl; }
    virtual ~Base() {}  // 析构函数设为虚函数,保证子类对象析构正确
};

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

int main() {
    Base* b = new Derived();
    b->show();     // 调用 Derived::show()
    b->display();  // 调用 Derived::display()
    delete b;      // 确保正确调用 Derived 析构函数
    return 0;
}
运行结果
Derived::show()
Derived::display()

虚函数表(vtable)和虚指针(vptr)的实现

Base 类的内存布局
成员地址/偏移量说明
vptr 指针0x1000指向 Base 的虚函数表
Base::show()0x2000在 vtable 中的第 1 个槽位
Base::display()0x2008在 vtable 中的第 2 个槽位
Derived 类的内存布局
成员地址/偏移量说明
vptr 指针0x3000指向 Derived 的虚函数表
Derived::show()0x4000覆盖 Base::show()
Derived::display()0x4008覆盖 Base::display()

说明

  • BaseDerived的对象中,都有一个隐藏的虚指针(vptr),用于指向该类的虚函数表(vtable)
  • BaseDerived各自的vtable存储了类的虚函数地址,按声明顺序存储
  • 当子类重写虚函数时,子类的vtable替换了父类vtable中对应函数的地址

动态绑定的过程

Base* b = new Derived(); 时:

  1. b实际存储的是 Derived 对象,但指针类型是Base*
  2. b指向Derivedvptr,即b->vptr = &Derived_vtable
  3. 调用b->show();的步骤
    • b通过vptr找到Derivedvtable
    • vtable第 1 个函数地址指向Derived::show()
    • 最终调用Derived::show(),而不是Base::show()

同理,b->display();也会调用Derived::display()

4. 关键问题解答

为什么 Base 的析构函数需要是 virtual

Base* b = new Derived();
delete b;  // 调用 ~Base() 还是 ~Derived()?

如果 ~Base() 不是虚函数

  • delete b;只会调用Base::~Base()不会调用Derived::~Derived(),可能导致内存泄漏

如果 ~Base() 是虚函数

  • vtable记录Derived::~Derived()的地址,delete b;时会正确调用Derived::~Derived()确保正确析构

虚函数表是否会影响性能?

  • 额外的内存开销

    • 每个对象多了一个vptr 指针(通常 4/8 字节)
    • 每个类多了一个vtable(存储在静态存储区)
  • 额外的函数调用开销

    • 虚函数调用需要间接寻址(查表)
    • 比普通函数调用多一次指针解引用(比直接调用慢)
    • 但现代 CPU对间接寻址优化较好,性能损失很小(一般小于 10%)

如果在性能关键代码中,可以:

  • 减少不必要的虚函数(用final禁止继承优化)
  • 避免小函数的虚拟调用(如inline + constexpr)
  • **使用 CRTP(Curiously Recurring Template Pattern)**避免虚函数

5. C 语言的函数调用过程

C 语言的函数调用依赖**栈(Stack)**来管理参数传递、局部变量存储以及返回地址保存。每次函数调用都会创建一个栈帧(Stack Frame),它是管理函数执行所需信息的结构。

栈帧(Stack Frame)的结构

一个函数的栈帧通常包括以下部分(从高地址到低地址):

栈帧组成部分作用
返回地址存储调用者的返回地址(即call指令保存的地址)
上一帧指针(EBP)记录前一个函数的EBP,用于恢复调用者的栈环境
函数参数存储传递给被调用函数的参数(按调用约定决定具体位置)
局部变量存储该函数的局部变量,占用栈上的空间
临时寄存器保存区存放函数调用前需要保存的寄存器值(如EBX, EDI, ESI等)

💡 栈的增长方向

  • 通常栈从高地址向低地址增长,即新数据压栈时地址递减

栈帧的创建过程

假设我们有如下代码:

#include <stdio.h>

void func(int a, int b) {
    int sum = a + b;  // 局部变量
    printf("sum: %d\n", sum);
}

int main() {
    func(3, 5);
    return 0;
}

main()调用func(3, 5)时,栈的变化如下(以x86体系结构为例):

1. 初始状态(main()开始执行)

ESP(栈指针)EBP(帧指针)仅用于main函数本身的运行,假设栈的初始状态如下:

高地址 → ────────────────────────
        | main() 的返回地址     |
EBP →   | main() 的上一帧指针   |
ESP →   | main() 局部变量        |
低地址 → ────────────────────────
2. func(3, 5)main()调用

main()调用func(3, 5)时,CPU 会执行call func,在栈上创建func的栈帧

高地址 → ───────────────────────────
        | main() 的返回地址          |
EBP →   | main() 的上一帧指针        |
        | main() 局部变量            |
        | ───────────────────────── |
        | `func` 返回地址 (main 的下一条指令) |
        | `func` 的上一帧指针(原 EBP)  |
ESP →   | `func` 的参数:b=5           |
        | `func` 的参数:a=3           |
低地址 → ───────────────────────────
  • ESP递减,为func分配空间
  • EBP保存了main的栈帧起始位置,方便返回时恢复
3. func开始执行

func内部操作

int sum = a + b;  // 分配局部变量 sum

栈帧更新如下:

高地址 → ───────────────────────────
        | main() 的返回地址          |
EBP →   | main() 的上一帧指针        |
        | main() 局部变量            |
        | ───────────────────────── |
        | `func` 返回地址 (main 的下一条指令) |
        | `func` 的上一帧指针(原 EBP)  |
        | `func` 的参数:b=5           |
        | `func` 的参数:a=3           |
ESP →   | `func` 局部变量:sum=8       |
低地址 → ───────────────────────────

总结

  • EBP指向func栈帧的起始位置,作为稳定访问基点
  • ESP指向当前栈顶(sum 变量的地址),局部变量从ESP递减分配
4. func执行完毕,准备返回

函数返回前:

  1. EBP还原(恢复mainEBP)
  2. ESP还原(回到func调用前的位置)
  3. 从栈中弹出返回地址,并跳转到main()继续执行
高地址 → ────────────────────────
        | main() 的返回地址     |
EBP →   | main() 的上一帧指针   |
ESP →   | main() 局部变量        |
低地址 → ────────────────────────

ESPEBP恢复到main之前的状态main()继续执行。

函数调用流程示意图

函数调用前(main()运行中)
[  main() 栈帧  ]
┌────────────────┐
│   返回地址      │
├────────────────┤
│   旧 EBP       │
├────────────────┤
│   局部变量      │ ← ESP
└────────────────┘
调用 func(3, 5)
[  func() 栈帧  ]
┌────────────────┐
│   返回地址      │
├────────────────┤
│   旧 EBP       │
├────────────────┤
│   参数 b = 5   │
├────────────────┤
│   参数 a = 3   │
├────────────────┤
│   sum = a + b  │ ← ESP
└────────────────┘
func执行完毕,返回main()
[  main() 栈帧  ]  (恢复到原状态)
┌────────────────┐
│   返回地址      │
├────────────────┤
│   旧 EBP       │
├────────────────┤
│   局部变量      │ ← ESP
└────────────────┘

6. 总结

关于虚函数表和运行时多态

  1. **虚函数表(vtable) + 虚指针(vptr)**是 C++ 运行时多态的核心机制
  2. 对象存储vptr,指向其类的vtablevtable记录虚函数的地址,从而实现动态绑定
  3. 调用虚函数时,通过vptr查找vtable,获取函数地址并执行,实现运行时的多态
  4. 子类重写父类虚函数时,子类的vtable会替换相应的函数地址,确保Base*指向Derived调用Derived的实现
  5. 析构函数必须是virtual,否则会导致内存泄漏
  6. 虚函数带来一定的性能开销,但一般可接受,可通过finalCRTP等方式优化

关于函数调用机制

  1. 栈帧用于管理函数调用的状态

    • 参数传递(存储参数)
    • 局部变量存储(避免全局变量污染)
    • 返回地址管理(确保返回调用点)
    • 寄存器保存(保证函数调用前后的环境一致)
  2. 栈指针ESP

    • 指向栈顶(最近的局部变量或保存值)
    • 动态变化,函数执行时不断调整
  3. 帧指针EBP

    • 指向当前栈帧的基地址,用作稳定的访问基点
    • 调用新函数时会保存旧的EBP,方便恢复调用者栈帧
  4. 函数调用和返回的流程

    • call指令参数入栈跳转到目标函数
    • ret指令恢复EBP弹出返回地址回到调用处

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

相关文章:

  • Chrome 调试器第二次连接不上?
  • 哈希表以及封装unordered_map及其set
  • uni-app 项目源码托管指南:从零开始将项目上传至 Gitee
  • Bash中小数的大小比较以及if条件中小数的大小判断
  • 将有序数组转换为二叉搜索树 力扣108
  • 什么是 React Router?如何使用它?
  • Web-Machine-N7靶机通关攻略
  • 技术速递|.NET AI 模板现已提供预览版
  • 用Ollama部署大语言模型
  • Spring MVC 拦截器使用
  • Linux系统上后门程序的原理细节,请仔细解释一下
  • Excel处理控件Spire.XLS系列教程:C# 在 Excel 中添加或删除单元格边框
  • 编码器线:精准连接,高效传动,引领未来科技的脉动
  • “三带一”算法题
  • Python八字排盘系统实现分析
  • 【vulhub/wordpress靶场】------获取webshell
  • 音视频之H.265码流分析及解析
  • SpringBoot第四站(1):数据层开发: 配置数据源,整合jdbcTemplate
  • Node.js技术原理分析系列6——基于 V8 封装一个自己的 JavaScript 运行时
  • STM32 模拟SPI 模式0