百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

C++ 对象模型:虚函数表的底层结构与多态实现

itomcoil 2025-09-18 01:24 1 浏览

0.简介

多态作为面向对象三大核心特性(封装、继承、多态)之一,其可以分为静态多态(模板,重载,重定义)和动态多态。理解动态多态的底层实现原理对于掌握C++对象模型有重要意义。本文将从虚函数表、内存布局、调用的汇编代码等底层结构出发,深入解析动态多态的实现机制。

1.C++对象内存结构

要理解C++的动态多态实现,需要从对象模型(Object Model)的内存结构入手。让我们先通过设计者的视角来分析这个机制:

1)核心问题:在复杂的继承体系中,对象如何动态确定要调用的函数地址?如果使用地址记录应该怎么记录?如果每个对象都存储所有虚函数的地址,会导致严重的空间浪费(每个对象都保存一份完整的函数指针表)。

2)解决方案设计:C++采用的是一种间接访问的方案,我们分别在类和对象角度来看:

  1. 类级别:每个包含虚函数的类会维护一个虚函数表(vtable),这是一个函数指针数组。
  2. 对象级别:每个对象只需存储一个指向对应vtable的指针(vptr)。
|----------------|          |------------------|
| 对象实例        |          | 类的虚函数表     |
|----------------|          |------------------|
| vptr ----------------> | 虚函数1的地址  |
| 成员变量       |          | 虚函数2的地址  |
|----------------|          | ...             |
                            |------------------|

3)关键优势:

空间效率:所有同类对象共享同一个vtable。

动态绑定:通过vptr在运行时确定实际调用的函数。

继承扩展:派生类可以创建新的vtable,只需修改差异部分

这种设计完美平衡了空间效率(每个对象只需一个指针开销)和时间效率(通过一次指针间接访问实现动态绑定),是C++多态机制的基石。

2.虚函数表基本结构

虚表是以指针数组存在的,即内部又指向不同的函数地址。我们通过一个例子来看。

#include <iostream>
using namespace std;
// 基类
class Base {
public:
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
    int a;
};
// 派生类
class Derived : public Base {
public:
    void f() override { cout << "Derived::f()" << endl; } // 重写f()
    virtual void h() { cout << "Derived::h()" << endl; }   // 新增虚函数h()
    int b;
};
// 定义函数指针类型(匹配虚函数签名)
typedef void (*VFuncPtr)();
int main() {
    Derived d;
    // 获取对象的虚表指针(vptr):对象首地址即为vptr
    void**vtable =*(void*** )&d;
    cout << "虚表地址: " << vtable << endl;
    // 打印虚表中的函数
    cout << "虚表内容(函数地址及调用结果):" << endl;
    for (int i = 0; i < 3; ++i) {  // Derived有3个虚函数:f、g、h
        VFuncPtr func = (VFuncPtr)vtable[i];  
        cout << "  索引" << i << ":地址=" << (void*)func << ",调用:";
        func();  // 调用虚函数
    }
    return 0;
}

运行结果如下:

所以我们可以得到其层级结构如下:

我们也可以使用如下命令去查看,可以看到其前两个是记录一些别的信息,所以虚表指针是从16偏移开始。

g++ -fdump-lang-class your_code.cpp

3.不同继承下的虚函数表

3.1 单继承下的虚函数表

单继承情况我们在第2节中已经描述了,其简单来说就是将自己的函数替换到虚表中,其余继续使用基类的,这种继承无论多少层都是先继承上一层的虚表,然后把自己实现的替换进去。

3.2 多继承下的虚函数表

多继承是通过多个虚表指针来实现的,其中如果函数同名在调用时会报错,比如基类Base1和基类Base2都定义了虚函数f(),如果调用是直接调用会报错,需要d.Base1::f(), 我们通过下面这个代码来看其结构:

#include<iostream>
using namespace std;
class Base1 { 
public:
    virtual void f(){cout<<"a"<<endl;}; 
};
class Base2 { 
public:
    virtual void g(){cout<<"a"<<endl;}; 
};
class Derived : public Base1, public Base2 {};
int main()
{
    Derived d;
    d.Base1::f();
    return 0;
}

其虚表结构如下:

依旧可以使用g++ -fdump-lang-class your_code.cpp命令验证:

3.3 菱形继承下的虚函数表

菱形继承即多继承有着共同的父类,我们以下面例子来看,如果不是虚继承且f()在Bottom中没有实现,其直接使用b.f()就会报错,因为其两个父类中都有(来源于Top类),需要指定调用那个的,加上作用域;当然如果Bottom实现了的话就可以直接调用:

#include<iostream>
using namespace std;
class Top {                  // 顶层基类
public:
    virtual void f() { cout << "Top::f()" << endl; }
    int x;
};
class Middle1 : public Top {  // 中间基类1
public:
    virtual void g1() { cout << "Middle1::g1()" << endl; }
};
class Middle2 : public Top {  // 中间基类2
public:
    virtual void g2() { cout << "Middle2::g2()" << endl; }
};
class Bottom : public Middle1, public Middle2 {  // 菱形顶点
public:
    //void f() override { cout << "Bottom::f()" << endl; }  // 重写Top::f
};
int main()
{
    Bottom b;
    b.Middle1::f();
    return 0;
}

这个的结构如下,其同样是两个虚函数指针:

我们依旧使用g++ -fdump-lang-class your_code.cpp命令验证。

可以看到其中Top的存储存在重复且调用需要指定类名,这个可以通过虚继承解决,代码如下:

#include<iostream>
using namespace std;
class Top {                  // 顶层基类
public:
    virtual void f() { cout << "Top::f()" << endl; }
    int x;
};
class Middle1 : virtual public Top {  // 中间基类1
public:
    virtual void g1() { cout << "Middle1::g1()" << endl; }
};
class Middle2 : virtual public Top {  // 中间基类2
public:
    virtual void g2() { cout << "Middle2::g2()" << endl; }
};
class Bottom : public Middle1, public Middle2 {  // 菱形顶点
public:
    //void f() override { cout << "Bottom::f()" << endl; }  // 重写Top::f
};
int main()
{
    Bottom b;
    b.f();
    return 0;
}

这个我们直接通过g++ -fdump-lang-class your_code.cpp来看其效果。

4.虚函数调用流程

虚函数调用流程我们从编译期和运行期分别来看,使用简单代码来做分析:

#include<iostream>
using namespace std;
class Base {
public:
    virtual void f() { cout << "Base::f()" << endl; } // 虚函数
    int a;
};
class Derived : public Base {
public:
    void f() override { cout << "Derived::f()" << endl; } // 重写虚函数
    int b;
};
int main()
{
    Base* b = new Derived();
    b->f();
    return 0;
}

1)编译期:编译期在处理Base类时,会给虚函数f()分配一个在虚表中的固定索引,派生类重写f()时会在自己的虚表中同一索引位置替换成自己的函数地址,也就是说,虚表在编译期就生成完成。

2)运行期:在创建对象时,其头部存放虚表指针,指向类对应的虚表,调用时通过指针找到虚表,根据索引找到函数地址,然后调用,我们来看一下其汇编代码:

g++ -S -fverbose-asm -g hhh.cpp -o test.s
# hhh.cpp:18:     b->f();
    .loc 1 18 10                  # 源代码位置标记(第18行)


    # 第一步:获取对象指针
    movq    -24(%rbp), %rax       # 从栈帧偏移-24处加载对象指针b到rax寄存器
                                  # 这里-24(%rbp)是局部变量b在栈上的存储位置
                                  # rax现在持有Base*类型的对象地址


    # 第二步:获取虚函数表指针(vptr)
    movq    (%rax), %rax          # 解引用对象指针,获取虚表指针(vptr)
                                  # 在C++对象内存布局中,vptr通常位于对象起始位置
                                  # 现在rax保存的是该对象对应的虚函数表地址


    # 第三步:获取虚函数地址
    movq    (%rax), %rdx          # 解引用虚表指针,获取第一个虚函数地址
                                  # 虚函数表是函数指针数组,首元素对应第一个虚函数f()
                                  # rdx现在保存的是Base::f()的实际函数地址


# hhh.cpp:18:     b->f();
    .loc 1 18 9                   # 源代码位置标记(同一条语句的另一种位置表示)


    # 第四步:准备this指针参数
    movq    -24(%rbp), %rax       # 再次加载对象指针到rax(可能因优化被重用)
    movq    %rax, %rdi            # 将对象指针移动到rdi寄存器(x64调用约定的第一个参数)
                                  # 在成员函数调用时,this指针作为隐式参数传递


    # 第五步:执行虚函数调用
    call    *%rdx                 # 通过函数指针间接调用
                                  # 实际会跳转到rdx存储的地址执行Base::f()
                                  # 这是动态多态的核心机制

5.总结

本文对于动态多态机制进行了详细的分析,从对象模型到虚函数布局再到其调用流程,可以看到,其既通过中间层提供了良好的扩展机制,但也带来了部分的性能开销,这也是一些对性能要求极高的应用选择静态多态(比如模板)的原因。

相关推荐

Python GUI 编程入门教程 第11章:数据库操作与文件管理

11.1数据库操作:与SQLite结合在许多应用中,数据的存储和管理是必不可少的部分。Tkinter本身并不自带数据库支持,但你可以通过Python的sqlite3模块来将数据库功能集成到Tkint...

Python GUI 编程入门教程 第12章:图形绘制与用户交互

12.1图形绘制:Canvas控件Tkinter提供了一个非常强大的控件Canvas,可以用来绘制各种图形,如线条、矩形、圆形等。通过Canvas控件,用户可以在GUI中添加绘图、图像和其他复杂的内...

Python GUI 编程入门教程 第16章:图形绘制与动画效果

16.1使用Canvas绘制图形Tkinter的Canvas控件是一个非常强大的绘图工具,可以用来绘制各种基本图形,如线条、矩形、圆形、文本等。Canvas允许你通过编程创建和修改图形元素,非常适合...

Python GUI 编程入门教程 第10章:高级布局与界面美化

10.1高级布局管理:使用grid和placeTkinter提供了三种常用的布局管理方式:pack、grid和place。在本章中,我们重点介绍grid和place,这两种布局方式相较于pack更加...

手机Python编程神器——AidLearning

【下载和安装】1、让我们一起来看下吧,直接上图。第一眼看到是不是觉得很高逼格,暗黑画风,这很大佬。其实它就是------AidLearning。一个运行在安卓平台的linux系统,而且还包含了许多非常...

Python GUI开发:从零开始创建桌面应用

在数字化时代,桌面应用依然是我们日常生活中不可或缺的一部分。无论是办公软件、游戏还是各种工具,它们都依赖于图形用户界面(GUI)来提供直观的操作体验。Python的wxPython库为我们提供了一个强...

Python界面(GUI)编程PyQt5窗体小部件

一、简介在Qt(和大多数用户界面)中,“小部件”是用户可以与之交互的UI组件的名称。用户界面由布置在窗口内的多个小部件组成。Qt带有大量可用的小部件,也允许您创建自己的自定义和自定义小部件。二、小部件...

自学Python的8个正确顺序仅供参考

今天决定写一个Python新人的自学指南,好多人搞不清楚自学的顺序及路线,今天提供给大家参考一下,其实自学编程真的没有难。1【Python基础】安装并配置Python环境和编译软件Pycharm,这...

Python | Python交互式编程神器_python交互运行

很多Pythoner不怎么喜欢用Python交互式界面编程,例如使用Jupyter工具。感觉交互式编程没有把代码敲完再debug舒服。但是在对一些模块/功能进行调试的时候还是非常香的。例如我在写爬虫程...

Python GUI 编程入门教程 第14章:构建复杂图形界面

14.1界面布局管理在Tkinter中,界面控件的排列是通过布局管理器来实现的。Tkinter提供了三种布局管理器:pack、grid和place,每种布局管理器都有其独特的用途和优势。14.1.1...

Python数据库编程教程:第 1 章 数据库基础与 Python 连接入门

1.1数据库的核心概念在开始Python数据库编程之前,我们需要先理解几个核心概念。数据库(Database)是按照数据结构来组织、存储和管理数据的仓库,它就像一个电子化的文件柜,能让我们高效...

Python GUI 编程入门教程 第1章:Tkinter入门

1.1什么是Tkinter?Tkinter是Python的标准GUI库,它是Python语言的内置模块,无需额外安装。在Tkinter中,我们可以创建窗口、按钮、标签、文本框等常见的GUI元素。1....

用Python做个简单的登录页面_python怎么编写一个登录界面

我们上网时候,很多网站让你登录,没有账号注册会员,不能复制、粘贴都不让你操作。那我们怎么去实现这个窗口呢?很多语言都可以实现,根据你的需求去确定用哪个,这里我们学习python,就用tkinter测...

Python入门学习教程:第 16 章 图形用户界面(GUI)编程

16.1什么是GUI编程?图形用户界面(GraphicalUserInterface,简称GUI)是指通过窗口、按钮、菜单、文本框等可视化元素与用户交互的界面。与命令行界面(CLI)相比,...

推荐系统实例_推荐系统有哪三个部分组成

协同过滤算法:#第14课:推荐系统实践-完整的协同过滤推荐系统示例#1.导入必要的库importpandasaspdfromsklearn.metrics.pairwise...