【C++】透视C++多态:从虚函数表到底层内存布局的完全拆解
🔥小陈又菜:个人主页
📖个人专栏:《MySQL:菜鸟教程》 《小陈的C++之旅》《Java基础》
✨️想多了都是问题,做多了都是答案!
目录
问题引入:
1. 多态原理
2. 动态绑定与静态绑定
3. 单继承和多继承中的虚函数表
3.1. 单继承中的虚函数表
3.2. 多继承中的虚函数表
问题引入:
上篇文章我们知道了虚表的存在,虚表中存储了虚函数的指针,所以sizeof()展现出来会包括指针的大小,那么今天我们从原理的角度来理解一下多态。
【C++】面试官爱的C++多态八股文,这次让你彻底搞懂!
1. 多态原理
下面这段代码中,Buy()函数,如果传入的是Person调用的就是Person::BuyTicket(),传Student调用的是Student::BuyTicket。这样就构成了多态,而多态的调用实现,是依靠运行时,去指向对象的虚表中查调用的函数地址。
class Person
{
public:Person(const char* name = "张三"):_name(name){}virtual void BuyTicket(){cout << _name << "购票,需要排队,每人 100 ¥" << endl;}
protected:string _name;
};class Student : public Person
{
public:Student(const char* name):_name(name){}virtual void BuyTicket(){cout << _name << "购票,需要排队,每人 50 ¥" << endl;}
private:string _name;
};void Buy(Person* p)
{p->BuyTicket();
}int main()
{Person p("张三");Buy(&p);Student st("张同学");Buy(&st);return 0;
}
通过监视窗口我们可以发现:
- Person指向对象p时,p->BuyTicket()在p的虚表中找到虚函数是Person::Ticket
- Student指向对象st时,st->BugTicket在st的虚表中找到虚函数是Student::Ticket
- 通过查找不同的虚函数就实现了,不同的对象调用会有不同的行为,也就是多态
- 我们再明确一下实现多态的两个条件:存在虚函数、需要对象指针或引用调用虚函数
- 通过反汇编窗口可以发现,构成了多态之后,函数的调用是在运行了程序过程中去对象中取的,而不是编译时就决定的(如果不是多态,函数调用会在编译时就决定好)
多态调用:运行时决议,运行时才确定函数的地址
普通函数:编译时决议,编译时确认调用函数的地址
2. 动态绑定与静态绑定
- 静态绑定(前期绑定):编译时就决定了调用哪个函数,根据变量或表达式的静态类型决定,也就形成了静态多态,如函数重载
- 动态绑定(后期绑定):在程序运行时,根据拿到的具体类型来确定函数的具体行为,也就形成动态动态
如下面这个和上面那个p->BuyTicket()对比就知道,动态绑定和静态绑定的区别:
3. 单继承和多继承中的虚函数表
3.1. 单继承中的虚函数表
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}
private:int _b = 1;
};class Derive :public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func3(){cout << "Derive::Func3()" << endl;}virtual void Func4(){cout << "Derive::Func4()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
通过调试时我们发现似乎有些问题,理论上派生类d,应该会有三个虚函数(继承基类的两个,新增加的两个),但是我们发现虚表中只有两个指针,我们看不到Func3()和Func4(),这里是编译器隐藏了这两个函数,可以认为是VS的一个Bug。
这里我们采用比较底层的方式打印出虚表的指针:
- 首先我们明确一点,虚函数指针会隐藏的存储在对象内存的开头
- 我们先 &b 取地址
- 然后强制转换成三重指针 (void***)(&d),这相当于告诉编译器,将这块内存看做指向void**的指针
- 然后进行解引用 *(void***)(&d),这也就是对象开头的虚函数表指针
// 打印虚表并执行函数
void PrintVFTable_Safe(void** vtable, int max_entries = 10)
{cout << "Virtual Table Address: " << vtable << endl;if (vtable == nullptr) {cout << "Invalid vtable pointer!" << endl;return;}for (int i = 0; i < max_entries; ++i){// 检查地址是否有效if (vtable[i] == nullptr || (uintptr_t)vtable[i] < 0x1000) {cout << " [" << i << "]: END OF TABLE" << endl;break;}cout << " [" << i << "]: " << vtable[i];// 直接执行函数typedef void(*FuncPtr)();FuncPtr func = (FuncPtr)vtable[i];cout << " -> ";func(); // 执行函数// 安全限制,避免无限循环if (i >= max_entries - 1) {cout << " ... (reached max entries)" << endl;break;}}cout << endl;
}int main()
{Base b;Derive d;void** vtable_b = *(void***)(&b);void** vtable_d = *(void***)(&d);cout << "=== Base Virtual Table ===" << endl;PrintVFTable_Safe(vtable_b);cout << "=== Derive Virtual Table ===" << endl;PrintVFTable_Safe(vtable_d);return 0;
}
3.2. 多继承中的虚函数表
下面我们给出一段多继承的代码,来分析一下:
#include <iostream>
using namespace std;class Base1
{
public:virtual void Func1(){cout << "Base1::Func1()" << endl;}virtual void Func2(){cout << "Base1::Func2()" << endl;}
private:int _b1 = 1;
};class Base2
{
public:virtual void Func1(){cout << "Base2::Func1()" << endl;}virtual void Func2(){cout << "Base2::Func2()" << endl;}
private:int _b2 = 1;
};class Derive :public Base1, public Base2
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func3(){cout << "Derive::Func3()" << endl;}
private:int _d1 = 2;
};// 打印虚表并执行函数
void PrintVFTable_Safe(void** vtable, int max_entries = 10)
{cout << "Virtual Table Address: " << vtable << endl;if (vtable == nullptr) {cout << "Invalid vtable pointer!" << endl;return;}for (int i = 0; i < max_entries; ++i){// 检查地址是否有效if (vtable[i] == nullptr || (uintptr_t)vtable[i] < 0x1000) {cout << " [" << i << "]: END OF TABLE" << endl;break;}cout << " [" << i << "]: " << vtable[i];// 直接执行函数typedef void(*FuncPtr)();FuncPtr func = (FuncPtr)vtable[i];cout << " -> ";func(); // 执行函数// 安全限制,避免无限循环if (i >= max_entries - 1) {cout << " ... (reached max entries)" << endl;break;}}cout << endl;
}int main()
{Derive d;void** vtable_d = *(void***)(&d);PrintVFTable_Safe(vtable_d);return 0;
}
构成这样一个继承关系:
我们通过调试先来看一下对象d的虚函数指针:
然后通过打印的结果我们可看出派生类新增的虚函数一般会储存在第一个继承基类的虚函数表中:
通过下面这张图大家应该可以更好地理解:
(本篇完)