👋 欢迎阅读

🔥愿旖旎 · 个人主页
📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》
🌄 钱塘江上潮信来,今日方知我是我
✨当前学习内容: 《C++学习》
📑 目录
一、前置讲解
在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。
🧠 前置知识一:虚函数表指针(vfptr)
含虚函数的对象里多出一个指针 ,放在对象最前面 ,指向该类的虚函数表。
📋 前置知识二:虚函数表(vtable)
本质是存虚函数指针的数组 ,末尾一般放 nullptr;每个含虚函数的类各有一份。
🔁 前置知识三:重写 = 覆盖
语法层叫重写 ,原理层叫覆盖 ------指子类函数地址替换掉虚表里父类对应位置。
⚙️ 前置知识四:动态绑定 vs 静态绑定
虚函数走运行时 查表(动态);普通函数在编译期就定死(静态)。
🧩 前置知识五:多态两条件的原理
虚函数覆盖 保证表里有正确地址,指针/引用调用保证运行时才去查表。
二、虚函数表
2.1 sizeof(Base) 是多少?
cpp
// 这里常考一道笔试题:sizeof(Base) 是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
答案是:跟平台有关。
| 平台 | 组成 | sizeof(Base) |
|---|---|---|
| 32 位 | 虚表指针 4 字节 + int _b 4 字节 |
8 字节 |
| 64 位 | 虚表指针 8 字节 + int _b 4 字节 + 对齐填充 4 字节 |
16 字节 |
通过观察和测试发现:对象里除了 _b 成员,还多一个 __vfptr 放在对象的最前面 。对象中的这个指针我们叫虚函数表指针 (v 代表 virtual,f 代表 function)。
一个含有虚函数的类中,都至少有一个虚函数表指针 ------因为虚函数的地址要放到虚函数表中 ,虚函数表也简称虚表。

💡 面试常问"为什么加了
virtual对象就变大了":因为每个对象都多存了一个指针 (注意是每个对象一个 vfptr,不是每个函数)。
2.2 派生类的虚表长什么样
先给代码加上三个变化:派生类 Derive 继承 Base、Derive 中重写 Func1、Base 再增加一个虚函数 Func2 和一个普通函数 Func3。
cpp
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
两个类的虚表内容对比:
| 虚表中的槽位 | Base 的虚表 |
Derive 的虚表 |
|---|---|---|
| 第 1 项 | Base::Func1 |
Derive::Func1(被重写覆盖) |
| 第 2 项 | Base::Func2 |
Base::Func2(继承来的虚函数) |
| 末尾 | nullptr |
nullptr |
Func3(普通函数) |
不进虚表 | 不进虚表 |

2.3 六个关键结论
| 序号 | 结论 |
|---|---|
| 1 | 派生类对象 d 中也有一个虚表指针 :d 对象由两部分构成------一部分是父类继承下来的成员 + 虚表指针 (注意父类的虚表指针和子类的虚表指针是不同的 ),另一部分是自己的成员 |
| 2 | 基类 b 对象和派生类 d 对象的虚表不一样 :Func1 完成了重写,所以 d 的虚表中存的是重写后的 Derive::Func1 。所以虚函数的重写也叫作覆盖 ------重写是语法的叫法,覆盖是原理层的叫法 |
| 3 | Func2 继承下来后仍是虚函数 ,所以放进虚表 ;Func3 也继承下来了,但不是虚函数 ,所以不会放进虚表 |
| 4 | 虚函数表本质是一个存虚函数指针的指针数组 ,一般情况下数组最后面放了一个 nullptr |
| 5 | 派生类虚表的生成过程见下表(三步) |
| 6 | 虚函数存在哪?虚表存在哪? 很多人的回答是"虚函数存在虚表,虚表存在对象中"------这个回答是错的 ,但很多人深以为然。虚表存的是虚函数指针,不是虚函数 ;虚函数和普通函数一样,都存放在代码段 ,只是它的指针又存到了虚表中 。另外对象中存的不是虚表,存的是虚表指针 。至于虚表本身存在哪 ------实际验证会发现 VS 下虚表存在代码段 |
派生类虚表的生成过程:
| 步骤 | 做法 |
|---|---|
| a | 先将基类中的虚表内容拷贝一份到派生类的虚表中 |
| b | 如果派生类重写了 基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 |
| c | 派生类自己新增加的虚函数 ,按其在派生类中的声明次序 ,增加到虚表的最后 |
再强调一遍(这段没看懂就多看几遍):
| 东西 | 存在哪 | 说明 |
|---|---|---|
| 成员函数(含虚函数) | 代码段 | 只要是成员函数都在代码段,不会在对象内显示 |
| 虚函数表 | 代码段 | 表里存的是函数指针 |
虚函数表指针 __vfptr |
对象内部 | 对象里只有指针,没有表 |
再总结成五条:
| 序号 | 总结 |
|---|---|
| 1 | 虚表归属 :每个包含虚函数的类都有自己独立的虚表 ;继承会对父类虚表复制 ,再通过重写 修改;虚表存放的是虚函数指针,不是虚函数 |
| 2 | 重写的作用 :子类重写虚函数时,会替换虚表中父类对应的函数地址 ,这就是多态的核心 |
| 3 | 新虚函数 :新增虚函数按声明次序 进入虚表;虚函数和普通函数一样都存放在代码段 ,不在虚表里 |
| 4 | 非虚函数 :不会进虚表 ,调用时直接静态绑定(编译时确定调用哪个类的函数) |
| 5 | 虚表何时产生 :虚表内容在编译期 就由编译器安排好(函数地址在链接期 确定);而对象里的虚表指针在构造时设置 |
三、多态原理
cpp
class Person // 父类(含虚函数,支持多态)
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person // 子类,重写虚函数
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; } // 覆盖父类实现
};
void Func(Person& p) // 多态调用:通过基类引用调用虚函数
{
p.BuyTicket(); // 运行时根据实际对象类型动态绑定
}
int main()
{
Person Mike;
Func(Mike); // 买票-全价(调用 Person::BuyTicket)
Student Johnson;
Func(Johnson); // 买票-半价(调用 Student::BuyTicket,体现多态)
return 0;
}
运行结果:
买票-全价
买票-半价

| 观察 | 结论 |
|---|---|
p 指向 Mike 对象时 |
p->BuyTicket 在 Mike 的虚表 中找到的是 Person::BuyTicket |
p 指向 Johnson 对象时 |
p->BuyTicket 在 Johnson 的虚表 中找到的是 Student::BuyTicket |
| 结果 | 这样就实现了不同对象去完成同一行为时,展现出不同的形态 |
反过来思考:为什么多态需要那两个条件?
| 条件 | 在原理上对应什么 |
|---|---|
| 虚函数覆盖 | 保证虚表里那个槽位存的是子类的函数地址------表里地址不对,查表也查不到正确的函数 |
| 对象的指针或引用调用 | 保证调用动作推迟到运行时------用对象直接调用,编译器早就按静态类型定死了,根本不会去查表 |
再看汇编层面: 满足多态以后的函数调用,不是在编译时确定的 ,而是运行起来以后到对象中去找的 ;不满足多态的函数调用,是编译时确认好的。

💡 一句话概括原理:虚表是"虚函数地址的目录",vfptr 是"目录在哪" 。运行时先顺着
this找到 vfptr → 找到虚表 → 取出对应槽位的地址 → 再 call。所以多态是**"多一次查表"换来的灵活性**。
四、动态绑定与静态绑定
| 对比项 | 静态绑定(前期绑定 / 早绑定) | 动态绑定(后期绑定 / 晚绑定) |
|---|---|---|
| 确定时机 | 编译期间 | 运行期间 |
| 依据 | 变量的静态类型 | 具体拿到的对象类型 |
| 又被称为 | 静态多态 | 动态多态 |
| 典型例子 | 函数重载、普通函数调用 | 虚函数 + 基类指针/引用调用 |
| 效率 | 更高(直接 call 地址) | 略低(要查虚表) |
💡 记住这条对应关系:重载是静态多态(编译期挑版本),虚函数重写是动态多态(运行期查虚表)。两者都能叫"多态",但机制完全不同。
五、复盘(附答案)
💡 思考题
-
sizeof(Base)为什么会比只有成员变量时更大? 32 位和 64 位下分别是多少? -
派生类对象里有没有自己的虚表指针? 父类和子类的虚表指针是同一个吗?
-
为什么说"重写"和"覆盖"是同一件事的两种叫法? 分别在什么语境下用?
-
派生类的虚表是怎么生成的? 基类的
Func2和普通函数Func3分别会不会进虚表? -
"虚函数存在虚表里、虚表存在对象里"这句话错在哪? 正确说法是什么?
-
满足多态的调用为什么不满足多态更快? 动态绑定和静态绑定分别是什么?
-
多态的两个条件在原理上分别保证了什么? 缺一个为什么就不行?
📝 答案
-
因为只要类里有虚函数,对象里就会多一个虚表指针
__vfptr(放在对象最前面),对象大小因此增加。32 位 下:vfptr 4 +int4 = 8 字节 ;64 位 下:vfptr 8 +int4 + 对齐填充 4 = 16 字节。 -
有 。派生类对象
d中也有一个虚表指针 ,d由"父类继承下来的成员 + 虚表指针"和"自己的成员"两部分构成。父类的虚表指针和子类的虚表指针是不同的 (d的 vfptr 指向d自己的虚表,b的指向b的虚表)。 -
因为它们描述的是同一件事的两个层面 :语法层说派生类写了一个和基类一模一样的虚函数,叫"重写" ;原理层说派生类函数地址把虚表里基类那个槽位的内容替换掉了,叫"覆盖" 。所以可以记成"重写是语法,覆盖是原理"。
-
生成过程三步:a. 先把基类虚表内容拷贝一份 到派生类虚表;b. 重写了的虚函数,用派生类版本的地址覆盖 掉基类的那一项;c. 派生类新增的虚函数按声明次序追加到虚表末尾 。基类的
Func2是虚函数,会进虚表 (原样继承);Func3是普通函数,不进虚表。 -
错在把"指针"和"实物"混为一谈 。正确说法:虚表里存的是虚函数的指针,不是虚函数本身 ;虚函数和普通函数一样存在代码段 ;对象里存的不是虚表,而是虚表指针 ;虚表本身(VS 下)也存在代码段 。对象内只有那个
__vfptr。 -
因为不满足多态(静态绑定)的调用在编译期就把目标函数地址定死了,直接
call即可 ;而满足多态(动态绑定)的调用必须运行时先取 vfptr、再查虚表、最后才能拿到函数地址 ,多一次内存访问。静态绑定 =前期绑定/早绑定,编译期确定行为(如函数重载);动态绑定=后期绑定/晚绑定,运行期按实际对象类型确定(如虚函数通过基类指针/引用调用)。 -
虚函数覆盖 保证虚表里那个槽位存的确实是子类函数的地址 ------否则查表也查不到正确的实现;指针/引用调用 保证调用动作被推迟到运行时 ------如果用对象直接调用,编译器按静态类型就把函数调用定死了,根本不会走查表这条路。两者缺一不可。
结语
如果本文对你有帮助,欢迎:
👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充
你的每一次互动都是我继续更新的动力,我们下一篇见!🚀