【C++】多态(2):纯虚函数多态底层原理
目录
一 纯虚函数和抽象类
二 虚函数表指针
1 例题引入
2 定义
三 多态的原理
1 多态是如何实现的
2 动态绑定与静态绑定
3 面试常考-->什么是多态
四 虚函数表扩充内容
多态(1)回顾:【C++】多态(1):多态定义&&实现及虚函数的重写
一 纯虚函数和抽象类
在虚函数的后面写上=0,则这个函数为纯虚函数。纯虚函数不需要定义实现,其实现本身意义不大,因为它的设计目的就是要被派生类重写,但从语法层面来说,它可以拥有实现,实际使用中只需声明即可。
包含纯虚函数的类被称为抽象类。抽象类的核心特性是**不能实例化出对象**。如果派生类继承了抽象类后,没有对其中的纯虚函数进行重写,那么该派生类也会成为抽象类,同样无法实例化。
纯虚函数在某种程度上强制了派生类必须重写对应的虚函数。其底层逻辑是,若派生类不完成重写,它就会继承抽象类的属性,导致自身也无法实例化,从而间接“迫使”开发者在派生类中实现该函数。
示例:
class Car
{
public:virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
int main()
{// 编译报错:error C2259: “Car”: ⽆法实例化抽象类 Car car;Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}
(1)上面这个示例展现了抽象类无法实例化出对象:Car car会报错。
(2)

多态允许基类(如Car)的指针来引用派生类(如pBenz)的对象,并调用在派生类中重写的虚函数。
需要基类中声明Drive()为虚函数,并在派生类中重写该函数,在通过基类指针调用Drive()时,会根据实际对象的类型动态决定调用哪个派生类的实现
二 虚函数表指针
1 例题引入
下⾯编译为32位程序的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8 D. 12
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
答案选D 注意内存对齐原则和在多少位系统下
2 定义
上面题目运行结果为 12 字节,除了成员_b 和_ch 之外,还多了一个__vfptr,它通常放在对象的前面(不过需要注意,有些平台可能会将其放在对象的最后面,这与具体平台有关)。对象中的这个指针被称为虚函数表指针(其中 v 代表 virtual,f 代表 function)。
一个包含虚函数的类,其对象中至少会有一个虚函数表指针。这是因为类中所有虚函数的地址都要存放在该类对象的虚函数表中,虚函数表也简称为虚表。
虚函数表,是一个虚函数指针数组(数组里面的元素都是指针)

注意:在32位下,指针是四个字节,但是在64位下,指针是8个字节
普通函数只能实现普通调用,虚函数能实现多态调用,要实现多态调用,要把它自己放进虚函数表
基类的虚函数指向一个地址,派生类也有一个基类,这个虚函数指向重写之后的地址
三 多态的原理
1 多态是如何实现的
多态一定要用基类的指针或者引用进行调用,必须调用的是重写的虚函数
那重写的虚函数内部发生了什么改变呢?
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
private: string _name;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
private: string _id;
};class Soldier: public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
private: string _codename;
};void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。 ptr->BuyTicket();
}int main()
{// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后 // 多态也会发⽣在多个派⽣类之间。 Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}
注意这里的Func函数参数是指针。这里也可以换成引用
图示原理解释:


上面的这张图,ptr指向的Person对象,调用的是Person的虚函数。
下面这张图,ptr指向Student对象,调用的是Student的虚函数

从底层角度来看,Func函数中执行ptr->BuyTicket()时,当ptr指向Person对象就调用Person::BuyTicket,指向Student对象就调用Student::BuyTicket,这一过程是如何实现的呢?
通过图示我们可以了解到,在满足多态条件后,底层不再是在编译时通过调用对象来确定函数地址,而是在运行时到指针所指向对象的虚表中去查找并确定对应虚函数的地址。 正是基于这样的机制,实现了指针或引用指向基类对象时就调用基类的虚函数,指向派生类对象时就调用派生类对应的虚函数。
第二张图中,ptr指向Person对象,因此调用的是Person类的虚函数;第三张图中,ptr指向Student对象,所以调用的是Student类的虚函数。
传的ptr的地址不一样,调用的函数不同(在运行是去指定的对象的虚函数表中去找)

满足多态,运行到指向对象的虚函数表中找到相应的虚函数进行调用
不满足多态,编译时变成调用person::BuyTicket()
2 动态绑定与静态绑定
对于不满足多态条件(即不是通过指针或引用调用虚函数)的函数调用,其绑定发生在编译阶段。也就是说,在编译时就已经确定了要调用的函数地址,这种方式被称为静态绑定。
而满足多态条件(通过指针或引用调用虚函数)的函数调用,则是在运行时进行绑定。具体来说,会在程序运行时到指针或引用所指向对象的虚函数表中,查找并确定要调用的函数地址,这种方式被称为动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。 // 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址 ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax// BuyTicket不是虚函数,不满⾜多态条件。 // 这⾥就是静态绑定,编译器直接确定调⽤函数地址 ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)
3 面试常考-->什么是多态
多态分为:静态多态和动态多态。
静态多态时编译时确定的多态,是一个同名函数的调用表现出多种形态(例如同一个swap,传不同的参数就调用不同的函数)
动态多态是用一个基类的指针,指向不同的对象(基类对象或派生类对象),指向谁就调用谁的虚函数(前提是需要完成虚函数重写),底层是依靠对象让它存在虚函数表中(基类里存的是基类的,派生类里存的是派生类重写的虚函数),运行时指向谁,就到谁的虚函数表里找到对应的虚函数进行调用
四 虚函数表扩充内容
• 基类对象的虚函数表中存放着基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象则各自拥有独立的虚表,因此基类和派生类分别有独立的虚表。
• 派生类由两部分构成:继承而来的基类部分和自身的成员。一般情况下,若继承的基类部分中包含虚函数表指针,派生类自身就不会再生成新的虚函数表指针。但需要注意的是,派生类中继承自基类部分的虚函数表指针,与基类对象的虚函数表指针并非同一个,这就如同基类对象的成员和派生类对象中基类部分的成员是相互独立的一样。
• 当派生类重写了基类的虚函数时,派生类虚函数表中对应位置的虚函数地址会被覆盖为派生类重写后的虚函数地址。
• 派生类的虚函数表包含三部分内容:(1)基类的虚函数地址;(2)被派生类重写后完成覆盖的虚函数地址;(3)派生类自身新增的虚函数地址。
• 虚函数表本质上是一个存储虚函数指针的指针数组,通常在数组的最后会有一个0x00000000作为标记(不过这一点C++标准并未规定,由各编译器自行定义,例如VS系列编译器会添加该标记,而g++系列编译器则不会)。
• 虚函数存放在哪里?虚函数和普通函数一样,编译后都是一段指令,存储在代码段中,只是虚函数的地址会被额外存放到虚表中。
• 虚函数表存放在哪里?严格来说这个问题没有标准答案,因为C++标准并未对此作出规定。通过代码可以对比验证,在VS环境下,虚函数表存放在代码段(常量区)。
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 virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};
int main()
{
1Base b;Derive d;return 0;
}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;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
运行结果为:
栈:010FF954
静态区:0071D000
堆:0126D740
常量区:0071ABA4
Person虚表地址:0071AB44
Student虚表地址:0071AB84
虚函数地址:00711488
普通函数地址:007114BF

