📌 相关专栏
- 【Linux专栏】
- 【C语言专栏】
- 【测试专栏】
- 【MySQL专栏】
- 【C++ 专栏】
📌 相关文章推荐
- 【C++】多态门(上):多态的概念、虚函数、析构函数的重写、override和final关键字
- 【C++】 深入容器适配器:从零实现stack与queue
- 【C++】手写双向链表:list容器模拟实现
- 【C++】C++ vector 完全指南:构造、遍历、扩容、增删查改与二维数组
很高兴你点开这篇文章✨
这里会持续更新更多有用的内容,关注我,一起慢慢变好呀
👍 点赞 ⭐ 收藏 💬 评论
文章目录
- 前言
- 1. 纯虚函数与抽象类
-
- 1.1 纯虚函数的定义
- 1.2 抽象类
- 2. 虚表指针与内存布局
-
- 2.1 虚表指针(vptr)的引入
- 2.2 内存布局图
- 2.3 核心结论
- 3. 虚函数表的结构
-
- 3.1 什么是虚函数表?
- 3.2 Bash类虚表
- 3.3 Derive派生类虚表结构
- 3.4 虚函数与普通函数的存储位置
- 4. 派生类虚构表
- 5. 派生类虚表的构建过程
-
- 5.1 代码示例
- 5.2 派生类虚表的构建步骤
-
- 第1步:继承基类虚表
- 第2步:重写的虚函数覆盖对应位置
- 第3步:新增虚函数追加到尾部
- 5.3 核心结论
- 6. 多态的底层原理
-
- 6.1 完整示例
- 6.2 底层执行过程
- 6.3 图示
-
- 核心结论
- 本文全部代码
-
- 🐾 test.cpp
前言
在上一篇文章中,我们学习了多态的基本语法和使用条件,但你是否想过:
- 编译器是如何在运行时知道应该调用哪个函数?
- 为什么基类指针指向派生类对象时,能正确调用派生类的虚函数?
- 虚函数表存放在哪里?虚表指针占用多少内存?
🐶 🐾 ✨ 🐾 🐶
1. 纯虚函数与抽象类
1.1 纯虚函数的定义
在虚函数后面写上 = 0,该函数即为纯虚函数 :
- 核心作用:强制派生类重写该函数(因为不重写实例化不出对象
class Animal
{
public:
// 纯虚函数:只声明接口,无函数实现
// 强制派生类必须重写此函数
virtual void talk() = 0;
virtual void color() = 0;
};
注意 :纯虚函数不需要定义实现(虽然语法上可以给出实现,但通常没有意义,因为会被派生类重写)。
1.2 抽象类
包含纯虚函数的类称为抽象类,它具有以下特性:
- 不能实例化对象 :Animal a; 编译错误
- 强制派生类重写 :派生类必须重写所有纯虚函数
- 指针/引用可用 :可以定义抽象类指针或引用
- 未完全重写 :若派生类未重写所有纯虚函数,则仍是抽象类
class Animal
{
public:
virtual void talk() = 0; // 纯虚函数
virtual void color() = 0;
};
// 派生类 Dog:重写了talk、color(重写了全部纯虚函数,变成普通类),可以实例化出对象
class Dog : public Animal
{
public:
virtual void talk() { cout << "汪汪" << endl; }
virtual void color() {} // 必须重写所有纯虚函数,若只重写了talk,则仍是抽象类,因为未重写基类中所有的纯虚函数
};
// 派生类 Cat:同样重写了所有纯虚函数
class Cat : public Animal
{
public:
virtual void talk() { cout << "喵喵" << endl; }
virtual void color() {}
};
int main()
{
// Animal a; // 抽象类无法实例化
Dog d;
Cat c;
Animal* pd = &d; // 抽象类指针可以指向派生类对象
Animal* pc = &c;
pd->talk(); // 汪汪(多态)
pc->talk(); // 喵喵(多态)
return 0;
}

🐶 🐾 ✨ 🐾 🐶
2. 虚表指针与内存布局
-
在构成多态时,基类指针类型实际上取决执行对象的类型,而不是自身类型,当调用虚函数则是根据指向对象的类型来调用对应类中的虚函数。
-
编译器就是通过“虚标表针”和“虚函数表”知道调用哪个类的函数的;
2.1 虚表指针(vptr)的引入
类中包含虚函数(或继承了带虚函数的基类)时,该类的每个对象都会额外存储一个虚表指针(vptr) :
- 虚表指针通常位于对象内存的起始位置(不同编译器的实现可能略有差异),其作用是指向该类的虚函数表(vtable);
- 同一个类的所有对象,会共用一张虚函数表;但每个对象都有自己独立的vptr,且这些vptr的值相同,都指向这张虚函数表;
//虚表指针
class base
{
public:
// 构成虚函数:生成虚表指针(_vptr)
virtual void func1()
{
cout << "func1()" << endl;
}
// 构成虚函数:生成虚表指针(_vptr)
virtual void func2()
{
cout << "func2()" << endl;
}
// 普通函数:不属于对象,不占对象内存
void func3()
{
cout << "func3()" << endl;
}
protected:
int _b = 1;//4字节
char _ch = 'x';//1字节
};
int main()
{
base b;
base c;
cout << sizeof(b) << endl;
//32位:4+4+1->12(指针的大小在32位环境是4字节,在64位环境是8字节)
//输出结果:32位环境下为12字节,64位环境下为16字节
cout << sizeof(c) << endl;
return 0;
}
🐾 内存大小计算+内存对齐 (1) 成员原始字节
- 虚表指针vptr:32位=4字节,64位=8字节
- _b:4字节
- char _ch:1字节
(2) 内存对齐规则: 类整体大小要对齐到最大成员对齐数,这里最大成员为 int/指针 ,对齐数4
- 32位:4+4+1=9,补对齐空位 -> 凑4的倍数:12字节
- 64位:8+4+1=13,补对齐空位 -> 凑8的倍数:16字节
2.2 内存布局图

2.3 核心结论
| 虚表指针(vptr) | 每个对象都有一个,指向虚函数表 |
| 虚函数表(vtable) | 每个类共享一张,存储虚函数地址 |
| 普通成员函数 | 存放在代码区,不占用对象内存 |
| 虚函数越多 | 虚表中地址越多,但对象只多一个 vptr |
| 同类对象 | 共用同一张虚表,vptr 值相同 |
🐶 🐾 ✨ 🐾 🐶
3. 虚函数表的结构
3.1 什么是虚函数表?
虚函数表(vtable)是编译器为每个含虚函数的类生成的虚函数指针数组,存储该类所有虚函数的地址
- 同类型的对象共用一张虚表,不同类型的对象各有各独立的虚表,即基类和派生类各有独立的虚表
//虚函数表
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;
};
3.2 Bash类虚表
Base 类的虚表结构 :

- func5 是普通函数,不进虚表
- 成员变量 int a 单独占对象内存,和虚表无关
3.3 Derive派生类虚表结构
- func4 普通函数,不进虚表
3.4 虚函数与普通函数的存储位置
🐾 1. 为什么普通函数地址和虚函数地址都在代码区?
- 不管是不是虚函数,只要是成员函数,都会被编译成机器指令存放在代码区;
- 虚表只是记录了虚函数的地址,并不是把函数本身存到虚表里。
🐾 2. 为什么 Derive 的虚表地址和 Base 不一样?
- 因为 Derive 重写了 func1 ,还新增了 func3 ,编译器会为它生成一张新的虚表,所以地址和基类不同
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;
};
int main()
{
int i = 0; // 栈区
static int j = 1; // 静态区/数据段
int* p1 = new int; // 堆区
const char* p2 = "xxxxxxxx"; // 常量区/只读数据段
Base b;
printf("栈区: %p\\n", &i);
printf("静态区: %p\\n", &j);
printf("堆区: %p\\n", p1);
printf("常量区: %p\\n", p2);
printf("虚函数表地址: %p\\n", *(void**)&b); // 指向虚表
printf("虚函数地址: %p\\n", &Base::func1); // 代码区
printf("普通函数地址: %p\\n", &Base::func5); // 代码区
}

🐾 虚函数和普通函数编译后都是指令,存放在代码区。虚表只是记录了虚函数的地址(指针),不是把函数本身存到虚表里。
4. 派生类虚构表
- 继承下来的基类
- ⾃⼰的成员
- ⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针;
- 但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针“不是同⼀个”,
- 就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
- 基类的虚函数地址
- 派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分
- 虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
(1) 只有虚函数能进虚表,普通成员函数绝不进虚表;
(2) 派生类继承基类,“直接继承基类整张虚表”;
(3) 重写虚函数:“虚表对应位置地址被覆盖”;
(4) 派生类新增虚函数:“追加虚表最后”;
(5) 同类对象共用一张虚表;
🐶 🐾 ✨ 🐾 🐶
5. 派生类虚表的构建过程
5.1 代码示例
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:
// 重写基类的func1:会覆盖Derive对象虚表中原本是Base类func1的地址
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 b1;
Base b2;
Derive d1;
Derive d2;
return 0;
}
5.2 派生类虚表的构建步骤
第1步:继承基类虚表

第2步:重写的虚函数覆盖对应位置

第3步:新增虚函数追加到尾部

5.3 核心结论
| 基类 | 直接继承整张虚表 |
| 重写虚函数 | 虚表中对应位置地址被覆盖 |
| 新增虚函数 | 追加到虚表尾部 |
| 普通函数 | 不进虚表 |
🐶 🐾 ✨ 🐾 🐶
6. 多态的底层原理
多态的底层原理:虚表与虚指针
6.1 完整示例
class person
{
public:
//基类虚函数:派生类重写后实现多态
virtual void buyticket()
{
cout << "买票-全价" << endl;
}
protected:
string _name;
};
class student : public person
{
public:
//重写基类虚函数
virtual void buyticket()
{
cout << "买票-打折" << endl;
}
protected:
string _id;
};
class soldier : public person
{
public:
virtual void buyticket()
{
cout << "买票-优先" << endl;
}
private:
string _codename;
};
//统一接口:接收基类指,实现多态调用
void func(person* ptr)
{
//多态核心:静态看指针类型,动态看对象类型
//调用哪个版本的BuyTicket(),由ptr指向的实际对象决定,而不是指针本身类型
ptr->buyticket();
}
int main()
{
person ps;
student st;
soldier sr;
func(&ps);//调用Person::BuyTicket()
func(&st);//调用Student::BuyTicket()
func(&sr);//调用Soldier::BuyTicket()
//同类型对象共用一张虚函数表
//这三个对象的虚表指针的值是相同的(虚函数表是一样的),同类型的对象共用一虚表
//person p1;
//person p2;
//person p3;
return 0;
}

6.2 底层执行过程
1. 编译器为每个类生成虚函数表:
Person 虚表: &Person::BuyTicket
Student 虚表: &Student::BuyTicket
Soldier 虚表: &Soldier::BuyTicket
2. 每个对象包含 vptr,指向所属类的虚表:
ps.vptr -> Person 虚表
st.vptr -> Student 虚表
sr.vptr -> Soldier 虚表
3. 调用 ptr->BuyTicket() 时:
– 通过 ptr 找到对象的 vptr
– 通过 vptr 找到虚函数表
– 从虚表中取出 BuyTicket 的函数地址
– 调用该函数
6.3 图示

核心结论
1. 有虚函数 -> 对象有 vptr
2. vptr 指向 vtable(虚函数地址数组)
3. 同类对象共享同一张 vtable
4. 派生类虚表 = 继承基类虚表 + 覆盖重写函数 + 追加新虚函数
5. 虚函数调用 = 对象 -> vptr -> vtable -> 函数地址 -> 调用
🐶 🐾 ✨ 🐾 🐶
本文全部代码
🐾 test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
/*
纯虚数:在虚函数后边写的上“=0”,纯虚函数不需要定义实现(实现没有大号意义,因为会被派生类重写,但是语法上可以实现)
只需声明即可;
-核心作用:强制派生类重写该函数(因为不重写实例化不出对象)
抽象类:包含虚函数的类
– 不能实例化出对象
– 基类的所有纯虚函数都未被派生类重写,则该派生类也会成为抽象类
*/
// 抽象类:包含纯虚函数talk()和color()
class Animal
{
public:
// 纯虚函数:只声明接口,无函数实现,强制子类必须重写
virtual void talk() = 0;
virtual void color() = 0;
//抽象类无法实例化出对象,没有对象也就无法调用成员函数,所以即使实现了也没有实际意义
};
// 派生类Dog:重写了talk、color(重写了全部纯虚函数,变成普通类),可以实例化出对象
class Dog : public Animal
{
void talk()
{
cout << "汪汪" << endl;
}
//若只重写了talk,则仍是抽象类,因为未重写基类中所有的纯虚函数
void color()
{
}
//重写完成
};
// 派生类Cat:重写了所有基类的纯虚函数,变成普通类,可以实例化出对象
class Cat : public Animal
{
void talk()
{
cout << "喵喵" << endl;
}
void color(){}
};
int main()
{
// 抽象类无法实例化对象
//Animal a;
//error C2259: “Animal”: 无法实例化抽象类
// 用抽象类指针指向派生类对象(多态核心用法)
Dog d;
Cat c;
Animal* pd = &d;
Animal* pc = &c;
pd->talk();// 绑定Dog::talk->输出“汪汪”
pc->talk();// 绑定Cat::talk->输出“喵喵”
return 0;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
1.在构成多态时,基类指针类型实际上取决执行对象的类型,而不是自身类型,
当调用虚函数则是根据指向对象的类型来调用对应类中的虚函数。
2.编译器就是通过“虚标表针”和“虚函数表”知道调用哪个类的函数的;
3.虚表指针:
– 类中包含虚函数(或继承了带虚函数的基类)时,该类的每个对象都会额外存储一个虚表指针(vptr);
– 虚表指针通常位于对象内存的起始位置(不同编译器的实现可能略有差异),其作用是指向该类的虚函数表(vtable);
– 同一个类的所有对象,会共用一张虚函数表;但每个对象都有自己独立的vptr,且这些vptr的值相同,都指向这张虚函数表;
4.核心重点:
– (1) 普通成员函数存放在代码区,不占用类对象内存
– (2) 只要有至少一个虚函数,对象内部自动多出一个虚函数表指针(vptr)
– (3) 虚函数越多,虚表里存的地址越多,但对象永远只存1个vptr(虚表指针)
5.内存大小计算+内存对齐
(1) 成员原始字节
– 虚表指针vptr:32位=4字节,64位=8字节
– int _b:4字节
– char _ch:1字节
(2) 内存对齐规则:类整体大小要对齐到最大成员对齐数,这里最大成员为 int/指针 ,对齐数4
– 32位:4+4+1=9,补对齐空位 → 凑4的倍数:12字节
– 64位:8+4+1=13,补对齐空位 → 凑8的倍数:16字节
*/
//虚表指针
class base
{
public:
// 构成虚函数:生成虚表指针(_vptr)
virtual void func1()
{
cout << "func1()" << endl;
}
// 构成虚函数:生成虚表指针(_vptr)
virtual void func2()
{
cout << "func2()" << endl;
}
// 普通函数:不属于对象,不占对象内存
void func3()
{
cout << "func3()" << endl;
}
protected:
int _b = 1;//4字节
char _ch = 'x';//1字节
};
int main()
{
base b;
base c;
cout << sizeof(b) << endl;
//32位:4+4+1->12(指针的大小在32位环境是4字节,在64位环境是8字节)
//输出结果:32位环境下为12字节,64位环境下为16字节
cout << sizeof(c) << endl;
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
同一个基类指针调用不同派生类的重写虚函数,实现多态
– 基类Preson 有虚函数ButTicket(),派生类Student、Soldier都重写了它;
func(Person* ptr)接收基类指针,调用ptr->BuyTicket(),运行时会根据ptr指向的实际对象,调用对应类的版本
*/
//多态的底层原理:虚表与虚指针
class person
{
public:
//基类虚函数:派生类重写后实现多态
virtual void buyticket()
{
cout << "买票-全价" << endl;
}
protected:
string _name;
};
class student : public person
{
public:
//重写基类虚函数
virtual void buyticket()
{
cout << "买票-打折" << endl;
}
protected:
string _id;
};
class soldier : public person
{
public:
virtual void buyticket()
{
cout << "买票-优先" << endl;
}
private:
string _codename;
};
//统一接口:接收基类指,实现多态调用
void func(person* ptr)
{
//多态核心:静态看指针类型,动态看对象类型
//调用哪个版本的BuyTicket(),由ptr指向的实际对象决定,而不是指针本身类型
ptr->buyticket();
}
int main()
{
person ps;
student st;
soldier sr;
func(&ps);//调用Person::BuyTicket()
func(&st);//调用Student::BuyTicket()
func(&sr);//调用Soldier::BuyTicket()
//同类型对象共用一张虚函数表
//这三个对象的虚表指针的值是相同的(虚函数表是一样的),同类型的对象共用一虚表
person p1;
person p2;
person p3;
return 0;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
1.虚函数表(简称 “虚表”)是编译器为每个含虚函数的类生成的一张 “虚函数指针数组”,数组中存储的是该类所有虚函数的地址
2.同类型的对象共用一张虚表,不同类型的对象各有各独立的虚表,即基类和派生类各有独立的虚表
3.派⽣类由两部分构成:
– 继承下来的基类
– ⾃⼰的成员
⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针;
但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针“不是同⼀个”,
就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
4.派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
5.派⽣类的虚函数表中包含
– (1)基类的虚函数地址,
– (2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分
6.虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。
(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
7.虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
8.虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定
核心结论:
1.只有虚函数能进虚表,普通成员函数绝不进虚表;
2.派生类继承基类,“直接继承基类整张虚表”;
3.重写虚函数:“虚表对应位置地址被覆盖”;
4.派生类新增虚函数:“追加虚表最后”;
5.同类对象共用一张虚表;
一、Base 类虚表结构
Base虚表:&Base::func1 、 &Base::func2
– func5 是普通函数,不进虚表
– 成员变量 int a 单独占对象内存,和虚表无关
二、Derive 派生类虚表结构
1. 先继承Base虚表:原本 func1、func2
2. 重写 func1 :把虚表里原 Base::func1 覆盖替换为 Derive::func1
3. 新增虚函数 func3 :追加到虚表尾部
最终 Derive 虚表:&Derive::func1 、 &Base::func2 、 &Derive::func3
– func4 普通函数,不进虚表
*/
//虚函数表
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:
// 重写基类的func1:会覆盖Derive对象虚表中原本是Base类func1的地址
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 b1;
Base b2;
Derive d1;
Derive d2;
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
关键知识点补充
1. 为什么普通函数地址和虚函数地址都在代码区?
不管是不是虚函数,只要是成员函数,都会被编译成机器指令存放在代码区;
虚表只是记录了虚函数的地址,并不是把函数本身存到虚表里。
2. 为什么 Derive 的虚表地址和 Base 不一样?
因为 Derive 重写了 func1 ,还新增了 func3 ,编译器会为它生成一张新的虚表,所以地址和基类不同。*/
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 = "xxxxxxxx";//常量区/只读数据段
//虚表地址打印
printf("栈:%p\\n", &i);
printf("静态区:%p\\n", &j);
printf("堆:%p\\n", p1);
printf("常量区:%p\\n", p2);
Base b;
Derive d;
//64位环境下,直接用int*强转会截断地址,建议改成void**来兼容32/64
printf("Base虚函数表地址:%p\\n",*(void**)&b);
printf("Derive虚函数表地址:%p\\n",*(void**)&d);
//printf("Base虚函数表地址:%p\\n", *((int*)&b));
//printf("Derive虚函数表地址:%p\\n", *((int*)&d));
//这里Base*强转成int*只是为了便于观察地址
//由于VS环境下虚表指针位于对象内存的最前端
//所以对象地址解引用得到的就是虚表指针_vptr的值(也就是虚函数表的地址)
printf("虚函数地址:%p\\n", &Base::func1);
printf("普通函数地址:%p\\n", &Base::func5);
}
🐶 🐾 ✨ 🐾 🐶
谢谢你看到这里呀
如果喜欢这篇内容,点个关注,下次更新不迷路✨
👍 点赞 ⭐ 收藏 💬 评论



