跳到主要内容
C++多态实现深度剖析:从抽象类约束到虚函数表机制 | 极客日志
C++ 算法
C++多态实现深度剖析:从抽象类约束到虚函数表机制 C++ 多态通过虚函数和抽象类实现运行时绑定。纯虚函数定义接口,派生类必须重写才能实例化。底层依赖虚函数表(vtable)和虚函数指针(vfptr)。对象内存布局包含 vfptr,指向存储虚函数地址的数组。静态绑定在编译期确定地址,动态绑定在运行期通过 vfptr 查找。VS 编译器下虚函数表位于代码段(常量区)。理解内存分区与汇编调用有助于掌握多态原理。
CodeArtist 发布于 2026/3/15 更新于 2026/6/4 19 浏览C++的两个参考文档
非官方文档:cplusplus
官方文档:cppreference
3 纯虚函数与抽象类:从语法规范到底层约束
3.1 纯虚函数的语法语义深度解析
在虚函数的后面写上 = 0,则这个函数为纯虚函数。纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。
3.2 抽象类的设计意义与使用场景
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就实例化不出对象。
3.3 实践验证:抽象类实例化的编译器级限制
#include <iostream>
using namespace std;
class Car {
public :
virtual void Drive () = 0 ;
};
class Benz : public Car {
public :
virtual void Drive () { cout << "Benz - 舒适" << endl; }
};
class BMW : public Car {
virtual void Drive () { cout << "BMW - 操纵" << endl; }
};
class Ferrari : public Car {
{ cout << << endl; }
};
{
:
{ cout << << endl; }
{ cout << << endl; }
{ cout << << endl; }
:
_b = ;
_ch = ;
};
{
Car* pBenz = Benz;
pBenz-> ();
Car* pBMW = BMW;
pBMW-> ();
Car* pFerrari = Ferrari;
pFerrari-> ();
Base b;
cout << (b) << endl;
;
}
virtual void Drive ()
"Ferrari - 极致性能"
class
Base
public
virtual void Func1 ()
"Func1()"
virtual void Func2 ()
"Func2()"
virtual void Func3 ()
"Func3()"
protected
int
1
char
'x'
int main ()
new
Drive
new
Drive
new
Drive
sizeof
return
0
3.3.1 分析:抽象类实例化编译错误 Car 就是一个抽象类,这个实践验证了抽象类确实无法实例化出对象。
3.3.2 对象实例化条件验证 多态允许使用基类(如 Car)的指针来引用派生类(如 Benz、BMW、Ferrari)的对象,并调用在派生类中重写的虚函数。
Car* pBenz = new Benz;
pBenz->Drive ();
其他两个派生类也是同样的道理。这需要基类 Car 中声明 Drive() 为虚函数(使用 virtual 关键字),然后在派生类中重写该方法。这样,通过基类指针调用 Drive() 时,会根据实际对象的类型动态决定调用哪个派生类的实现。
3.3.3 调试运行行为观察
4 多态机制的底层原理与实现剖析
4.1 虚函数表指针 (vfptr) 机制详解
4.1.1 内存布局选择题实践 下面编译为 32 位程序的运行结果是什么?
A. 编译报错 B. 运行报错 C. 8 D. 12
class Base {
public :
virtual void Func1 () { cout << "Func1()" << endl; }
virtual void Func2 () { cout << "Func2()" << endl; }
virtual void Func3 () { cout << "Func3()" << endl; }
protected :
int _b = 1 ;
char _ch = 'x' ;
};
int main () {
Base b;
cout << sizeof (b) << endl;
return 0 ;
}
正确答案:选项 D。
32 位环境下运行结果是 12,所以选择 D 选项。
4.1.2 虚函数表 (vtable) 与 vfptr 概念深度解析 上面题目运行结果 12bytes,除了 _b 和 _ch 成员,还多一个 _vfptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。对象中的这个指针我们叫做虚函数表指针 (v 代表 virtual,f 代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
4.1.3 监视窗口与内存窗口的对比分析 监视窗口会特殊处理,监视窗口看不到的话,内存窗口可以勉强一看。监视窗口会进行一些特殊处理,方便程序员看和调试,但是这导致窗口里面的东西不一定是真实的,不过地址是对的。
4.1.4 vfptr 在对象内存中的位置验证 (此处省略调试截图,显示 vfptr 位于对象起始位置)
4.1.5 反汇编窗口在原理分析中的价值 转到反汇编可以获得比监视窗口更加明了的信息——通过汇编我们可以理解代码的底层行为。如下图,我们通过对比两种调用方式的 call 指令,可以明显地感受到两者的区别:满足多态时,运行时到指向对象的虚函数表中找到对应的虚函数进行调用;不满足多态的时候,编译时变成调用特定作用域的函数,普通调用直接去调用 Person 作用域的函数了,并且普通调用是在编译的时候确定地址的;而运行时的多态调用则是到指向的对象找地址。
这里我们打开反汇编,可以看一下普通调用的汇编,对比一下多态的汇编。
像上图中多态调用的这个 call 指令就是真实的调用。
4.1.6 汇编语言在 C++ 研究中的必要性探讨 我们可以转到反汇编看一下。确实,有时候转到反汇编可以获得比监视窗口更加明了的信息——通过汇编我们可以理解代码的底层行为。但是,汇编是很偏底层的,如果理想的岗位是算法岗、研发岗、测试开发岗、测试岗位的话,对汇编的需求就只是了解即可。
4.2 多态的实现机制深度解密
4.2.1 多态调用的底层实现流程 从底层的角度 Func 函数中 ptr->BuyTicket(),是如何作为 ptr 指向 Person 对象调用到 Person::BuyTicket,ptr 指向 Student 对象调用 Student::BuyTicket 的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
class Person {
public :
virtual void BuyTicket () { cout << "买票 - 全价" << endl; }
private :
string _name;
};
class Student : public Person {
public :
virtual void BuyTicket () { cout << "买票 - 打折" << endl; }
private :
string _id;
};
void Func (Person ptr) {
ptr.BuyTicket ();
}
int main () {
Person ps;
Student st;
Func (ps);
Func (st);
Person p1;
Person p2;
Person p3;
return 0 ;
}
上面的这张图,ptr 指向的 Person 对象,调用的是 Person 的虚函数。
下面这张图,ptr 指向 Student 对象,调用的是 Student 的虚函数。
4.2.2 动态绑定与静态绑定的机器级差异
静态绑定:对不满足多态条件(指针或者引用 + 调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做 静态绑定 。
动态绑定:满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也叫 动态绑定 。
ptr->BuyTicket ();
00 EF2001 mov eax,dword ptr [ptr]
00 EF2004 mov edx,dword ptr [eax]
00 EF2006 mov esi,esp
00 EF2008 mov ecx,dword ptr [ptr]
00 EF200B mov eax,dword ptr [edx]
00 EF200D call eax
ptr->BuyTicket ();
00 EA2C91 mov ecx,dword ptr [ptr]
00 EA2C94 call Student::Student (0 EA153Ch)
4.2.3 虚函数表的结构与工作原理
基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
派生类由下面这两部分构成:一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
派生类的虚函数表中由下面三个部分组成。
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 0x00000000 标记(这个 C++ 并没有进行规定,各个编译器自行定义的,VS 系列编译器会再后面放个 0x00000000 标记,g++ 系列编译不会放)。
虚函数存在哪的?虚函数和普通函数是一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
虚函数表存在哪的?这个问题严格说并没有标准答案,而且 C++ 标准并没有规定,我们写下面的代码可以对比验证一下。至少可以肯定,VS 下是存在代码段(常量区)里面的。
写程序是一个很好的验证方法:
对比上面几个地址,我们就可以得出我们的结论了。
4.2.4 基类指针与虚函数表指针的关系辨析
4.2.5 虚函数重写为什么也叫虚函数覆盖?
4.2.6 虚函数表实践 class Base {
public :
virtual void func1 () { cout << "Base::func1" << endl; }
virtual void func2 () { cout << "Base::func2" << endl; }
void func5 () { cout << "Base::func5" << endl; }
protected :
int a = 1 ;
};
class Derive : public Base {
public :
virtual void func1 () { cout << "Derive::func1" << endl; }
virtual void func3 () { cout << "Derive::func3" << endl; }
void func4 () { cout << "Derive::func4" << endl; }
protected :
int b = 2 ;
};
int main () {
Base b;
Derive d;
return 0 ;
}
4.2.7 虚函数表实践:内存分区验证
class Base {
public :
virtual void func1 () { cout << "Base::func1" << endl; }
virtual void func2 () { cout << "Base::func2" << endl; }
void func5 () { cout << "Base::func5" << endl; }
protected :
int a = 1 ;
};
class Derive : public Base {
public :
virtual void func1 () { cout << "Derive::func1" << endl; }
virtual void func3 () { cout << "Derive::func3" << endl; }
void func4 () { cout << "Derive::func4" << endl; }
protected :
int b = 2 ;
};
int main () {
int i = 0 ;
static int j = 1 ;
int * p1 = new int ;
const char * p2 = "xxxxxxxxxxxx" ;
printf ("栈:%p\n" , &i);
printf ("静态区:%p\n" , &j);
printf ("堆:%p\n" , p1);
printf ("常量区:%p\n" , p2);
Base b;
Derive d;
printf ("Base 虚函数地址:%p\n" , *(int *)&b);
printf ("Derive 虚函数表地址:%p\n" , *((int *)&d));
printf ("虚函数地址:%p\n" , &Base::func1);
printf ("普通函数地址:%p\n" , &Base::func5);
return 0 ;
}
从运行结果可以看出,虚函数地址和常量区的地址十分接近,由此可知:
VS 下,虚函数地址是存在代码段(常量区)里面的。
在本文的最后,艾莉丝会对比 x64 环境和 x86 环境的几个地址。
4.2.8 x64 环境和 x86 环境地址对比:内存分区验证
完整代码示例与实践演示
Test.cpp:
#include <iostream>
#include <string>
using namespace std;
class Base {
public :
virtual void func1 () { cout << "Base::func1" << endl; }
virtual void func2 () { cout << "Base::func2" << endl; }
void func5 () { cout << "Base::func5" << endl; }
protected :
int a = 1 ;
};
class Derive : public Base {
public :
virtual void func1 () { cout << "Derive::func1" << endl; }
virtual void func3 () { cout << "Derive::func3" << endl; }
void func4 () { cout << "Derive::func4" << endl; }
protected :
int b = 2 ;
};
int main () {
Base b;
Derive d;
return 0 ;
}
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online