多态的原理
一:构成多态的两个条件
一个多态的列子:
class Person {
public:
virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "Student-买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;//创建一个Person类的对象 ps
Student st;//创建一个Student类的对象 st
//Func函数参数不同 结果不同
Func(ps);
Func(st);
return 0;
}
运行结果:
代码体现的多态的两个条件的地方:
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() {
cout << "Person-买票-全价" << endl;
}
//直接在类中封装多态调用
void PurchaseTicket() {
this->BuyTicket(); // 通过 隐藏的this 指针调用虚函数,触发多态
//this指针就是基类的指针
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "Student-买票-半价" << endl;
}
};
int main() {
//两个类创建了各自的对象
Person ps;
Student st;
// 直接调用对象的成员函数,内部自动处理多态
ps.PurchaseTicket(); // 输出: Person-买票-全价
st.PurchaseTicket(); // 输出: Student-买票-半价
return 0;
}
运行结果:
总结:
之前用额外的Func函数去展示多态,因为可以显式的看见我们Func函数的参数是引用,可以更好的突出多态的条件;
而写进类中的时候,往往是通过基类指针调用,但this看不见,不太好观察
二:虚函数指针
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base ba;
cout << sizeof(ba);
return 0;
}
运行结果:
Q:按照vs最大对齐数为8,再结合内存对齐的规则,这里怎么都该是4,为什么是8?
A:内存窗口,观察ba对象的存储的内容有哪些
解释:
①:原来ba对象中还有一个指针,指针的大小是4字节,所以这里总大小是8字节
Q:那为什么会多存储一个指针
A:往下看
三:虚函数表
对象中的这个名为_vfptr的指针我们叫做虚函数表指针(v代 表virtual,f代表function)。
顾名思义该指针指向虚函数表,虚函数表(简称虚表)是一个数组,且是一个指针数组,也就是存放虚函数地址的数组。
提示:监视窗口中的红线的内容 在内存中不存在 仅仅是监视窗口方便我们观察而生成的
Q:叽里咕噜的说这么多,所以多态的原理到底是什么?
A:请往下看
四:重写即覆盖
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;
}

发现:
五:多态的原理解析
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);
//派生类调用函数
Student Johnson;
Func(Johnson);
return 0;
}
多态的原理如下:
也就是说,编译器根本不管你传过来的是基类指针/引用,还是派生类的指针/引用
它只会直接去找传过来的对象中的虚表(vptr),然后通过虚表找到正确的虚函数进行调用。
而此时因为虚函数已经被重写了,所以就实现了多态的“不同对象不同行为”。
如果没有虚函数覆盖,所有对象调用的都是父类的版本,无法实现“不同对象不同行为”。
Q:那必须通过基类的指针或者引用调用虚函数的原因是什么?
A:
②:要求对象的指针或引用调用虚函数原因是:
让程序在运行时决定调用哪个版本的函数(动态绑定)。为什么需要?
如果直接用对象调用(如 obj.BuyTicket()),编译器在编译时就确定了调用哪个函数(静态绑定),无法实现多态。
如果用指针或引用(如 Person* p = &student; p->BuyTicket()),由于指针/引用可以指向不同的对象,程序在运行时才能确定实际调用的函数(动态绑定)。
六:验证虚函数表中存储的是虚函数的地址
需要知道一些知识:
①:__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),博主是vs所以放在最前面的
②:虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
③:先暂且理解为__vfptr指向了一个指针数组,且其是首元素的地址(后面会拓展虚函数表的真实内存布局)
所以现在我们的思路是:
①:获取对象的前四个节点的地址 进行解引用 得到的就是__vfptrd的内容,也就是数组的起始地址
②:通过__vfptrd的内容去遍历打印整个数组的元素(虚函数的地址),终止条件为nullptr
③:每次遍历得到的是虚函数的地址,在通过得到的地址验证它确实指向相应的虚函数,因为我们的虚函数里面会打印内容,此时运行结果有虚函数内部打印的内容,则代表验证成功!!
思路化作代码如下:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
:_b(2)
{
}
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;
}
virtual void Func3()//新增的虚函数
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
typedef void(*VF_PTR)();
//打印虚表,本质打印指针数组,每个元素都是虚函数的地址
void PrintVFT(VF_PTR* vft)
{
for (size_t i = 0; vft[i] != nullptr; i++)
{
printf("虚函数表的下标元素[%d]对应的地址是:%p->", i, vft[i]);
//回调该地址验证是否为虚函数
cout << "调用函数地址产生的内容是:";
VF_PTR f = vft[i];
f();
//(*f)();这种调用也可以
}
cout << endl << endl;
}
int main()
{
//派生类的对象d
Derive d;
//取到对象d中的前四个字节 给PrintVFT函数
PrintVFT((VF_PTR*)(*((int*)&d)));
return 0;
}
运行结果:
代码解释:
①:如何在main函数取到虚函数表起始地址
int main()
{
//派生类的对象d
Derive d;
//取到对象d中的前四个字节 给PrintVFT函数
PrintVFT((VF_PTR*)(*((int*)&d)));
return 0;
}
a:&d获取对象d的地址
b:(int*)&d将其转换为int指针(此处指针大小和int相同,都是4字节)
c:*((int*)&d)解引用获取vptr的值(虚函数表起始地址)
d:(VF_PTR*)将vptr转换为函数指针数组类型
②:如何遍历打印虚函数表的元素(虚函数的地址)
typedef void(*VF_PTR)();
//打印虚表,本质打印指针数组,每个元素都是虚函数的地址
void PrintVFT(VF_PTR* vft)
{
for (size_t i = 0; vft[i] != nullptr; i++)
{
printf("虚函数表的下标元素[%d]对应的地址是:%p->", i, vft[i]);
//回调该地址验证是否为虚函数
cout << "调用函数地址产生的内容是:";
VF_PTR f = vft[i];
f();
//(*f)();这种调用也可以
}
cout << endl << endl;
}
解释:
①:typedef void(*VF_PTR)();
-
这是一个函数指针类型的定义
-
它定义了一个名为
VF_PTR
的类型,表示"指向无参数、无返回值函数的指针" -
分解:
-
void()
:函数类型(无参数,返回void) -
(*VF_PTR)
:声明一个指针类型 -
typedef
:为这个指针类型创建别名
-
等效的现代C++写法:
using VF_PTR = void(*)();
注意:我们平时的typedef 给其他类型 换一个新的名字 是分开的,很好观察
但是typedef给函数指针类型起一个名字 是揉在一起的 不易观察 明白是C++写法那种就OK了
②:此时main函数传过来一个数组的首地址,咱们PrintVFT函数也接收到了,然后就是遍历打印了,截止条件是nullptr
③:重点来了,我们取到了每个元素,也就是每个虚函数的地址,咱们如何回调验证是否就是虚函数的地址呢?
VF_PTR f = vft[i];
f();
// 或者 (*f)();
-
将函数指针赋值给
f
-
通过函数指针调用函数
-
两种调用方式等价:
-
f()
:直接调用 -
(*f)()
:先解引用再调用
-
所以运行结果为:
好的。现在再来回顾需要知道的知识中的第③点:
③:先暂且理解为__vfptr指向了一个指针数组,且其是首元素的地址(后面会拓展虚函数表的真实内存布局)
在之前的很多截图中,我们都能发现__vfptr的内容和数组的首元素地址并不一致,如图
Q:那你的_vfptr都不是一个数组的首地址,为什么能通过这个直接打印数组的元素呢??
A:vptr(虚函数表指针)的值和第一个虚函数的地址确实不同,这是由虚函数表的内存结构决定的。
1. 虚函数表的真实内存布局
虚函数表(vtable)并不只存储虚函数指针,它的完整结构是:
-
vptr指向的是整个虚函数表的起始地址(即type_info的位置)
-
第一个虚函数实际是表的第1项(索引1)
2. 验证代码的问题所在
实际上跳过了type_info项,因为:
-
当强制转换为
VF_PTR*
时,编译器认为这是"纯函数指针数组" -
但实际内存中,虚函数表前面还有隐藏的RTTI数据
了解到这里就可以了,跳过了type_info项,也方便我们更简单理解!
七:单继承/多继承的虚函数表的区别
①:单继承的虚函数表
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
}
和我们预想的一致
②:多继承的虚函数表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[]) {
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
Q:此时的func函数放在那一个表中?是两张都放一份,还是选择一份放呢?
A:
八:多态面试大题
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}

这个例子很好地展示了为什么在C++中建议:
-
避免在虚函数中使用默认参数(缺省参数)
-
确保派生类和基类的虚函数使用相同的默认参数,以避免混淆。
九:多态面试问答题
十:虚表的存储区域
