🌇前言
多态
是面向对象三大基本特征中的最后一个,多态
可以实现 “一个接口,多种方法”,比如父子类中的同名方法,在增加了多态后,调用同名函数时,可以根据不同的对象(父类对象或子类对象)调用属于自己的函数,实现不同的方法,因此 多态
的实现依赖于 继承
同一个售票地点,为不同的购票方式提供了不同的取票窗口(多种状态 ->
多态
)
🏙️正文
1、多态基本概念
在使用多态的代码中,不同对象完成同一件事会产生不同的结果
比如在购买高铁票时,普通人原价,学生半价,而军人可以优先购票,对于 购票 这一相同的动作,需要 根据不同的对象提供不同的方法
#include <iostream> using namespace std; class Person { public: virtual void identity() { cout << "普通人原价" << endl; } }; class Student : public Person { public: virtual void identity() { cout << "学生半价" << endl; } }; class Soldier : public Person { public: virtual void identity() { cout << "军人优先购票" << endl; } }; void BuyTickets(Person& people) { //根据不同的身份,选择不同的购票方案 people.identity(); } int main() { Person ordinary; //普通人 Student student; //学生 Soldier soldier; //军人 //调用同一个购票函数 BuyTickets(ordinary); BuyTickets(student); BuyTickets(soldier); return 0; }
可以看到在调用同一函数、同一方法的情况下,不同对象的执行结果不同
注:父类 Peoson
中使用的 virtual
关键字和 BuyTickets
函数中的父类引用 是实现多态的关键
2、多态的定义及实现
实现多态需要借助虚表(虚函数表),而构成虚表又需要虚函数,即 virtual
修饰的函数,除此之外还需要使用虚表指针来进行函数定位、调用
2.1、构成多态的两个必要条件
必要条件
virtual
修饰后形成的虚函数,与其他类中的虚函数形成 重写(三同:返回值、函数名、参数均相同)- 必须通过【父类指针】或【父类引用】进行虚函数调用
virtual
修饰后,成为 虚函数
virtual void identity() { cout << "普通人原价" << endl; } virtual void identity() { cout << "学生半价" << endl; } virtual void identity() { cout << "军人优先购票" << endl; }
通过 【父类指针】或【父类引用】 调用 虚函数
void BuyTickets(Person& people) { //根据不同的身份,选择不同的购票方案 people.identity(); }
除了后续两种特殊情况外,上述两个构成多态的必备条件缺一不可!
缺少条件一:没有虚函数
缺少条件二:不是【父类指针】或【父类引用】进行虚函数调用
显然,缺少其中任意一个条件,都不构成多态
当然还存在两个例外:
- 除父类外,其他子类中的函数不必使用
virtual
修饰,此时仍然能构成多态(注意三同,需要构成重写) - 父子类中的虚函数返回值可以不相同,但此时需要返回对应的父类指针或子类指针,确保构成多态,这一现象称为 协变(了解)
例外一:子类虚函数没有使用 virtual
修饰
例外一有点违反 必要条件一 的意思,不过在某些场景中,这个例外很实用,比如:给父类的析构函数加上 virtual
修饰,这样在进行析构函数调用时,得益于 多态
,父类指针可以针对不同对象调用不同的析构函数释放资源
- 无论是谁的析构函数,最终函数名都为
destructor
,可能存在析构错误调用的问题,因此可以利用virtual
修饰父类的析构函数,这样子类在继承时,自动形成多态
#include <iostream> using namespace std; class Person { public: //此时未构成多态 ~Person() { cout << "~Person" << endl; } }; class Student : public Person { public: ~Student() { cout << "~Student" << endl; } }; class Soldier : public Person { public: ~Soldier() { cout << "~Soldier" << endl; } }; int main() { //父类指针 指向子类对象 Person* p1 = new Person(); //普通人 Person* p2 = new Student(); //学生 Person* p3 = new Soldier(); //军人 //释放空间 delete p1; delete p2; delete p3; return 0; }
假若不使用 virtual
修饰父类析构函数,直接运行代码,结果如下:
显然此时并未释放两个子类的资源,导致内存泄漏,可以给父类析构函数加上 virtual
,构成多态
//此时构成多态(利用例外一) virtual ~Person() { cout << "~Person" << endl; }
面试题:为什么要在 父类/基类 的析构函数中加上
virtual
修饰?
- 为了构成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏
建议:例外一会破坏代码的可阅读性,可能无法让别人一眼看出多态,因此除了析构函数外,不推荐在子类虚函数中省略 virtual
例外二:协变
如何快速判断是否构成多态?
- 首先观察父类的函数中是否出现了
virtual
关键字 - 其次观察是否出现虚函数重写现象,三同:返回值、函数名、参数(协变例外)
- 最后再看调用虚函数时,是否为【父类指针】或【父类引用】
父类指针或引用调用函数时,如何判断函数调用关系?
- 若满足多态:看其指向对象的类型,调用这个类型的成员函数
- 不满足多态:看具体调用者的类型,进行对应的成员函数调用
2.2、虚函数及重写
所以什么是虚函数?为什么类中被 virtual
修饰的函数能变成虚函数?
虚函数的作用是在目标函数(想要构成多态的函数)之间构成 重写(覆盖)
,一旦构成了 重写(覆盖)
,那么子类对象在实现此虚函数时,会 继承父类中的虚函数接口(返回值、函数名、参数列表),然后覆盖至子类对应的虚函数处,因此 重写又叫做覆盖
#include <iostream> using namespace std; class Person { public: virtual void func(int a = 10) { cout << "Person a: " << a << endl; } }; class Student : public Person { public: virtual void func(int a = 20) { cout << "Student a: " << a << endl; } }; int main() { Person* p = new Student(); p->func(); return 0; }
预想结果:输出 Student a: 20
,但实际情况不是如此
不难看出,子类 Student
中虚函数 func
实际上是 Person
中 func
的返回值、函数名、参数列表 + Student
中 func
的函数体 组合而成
所以虚函数就是 虚拟 的函数,可以被覆盖的、实际形态未确定的函数,使用 virtual
修饰后,就是在告诉编译器:标记此函数,调用时要触发 覆盖 行为,同时虚表指针需要找到正确的函数进行调用
注意:
- 除了类中的成员函数外,普通函数不能添加
virtual
关键字进行修饰,因为虚函数、虚函数表、虚表指针是一体的,普通函数没有 - 此处的
virtual
修饰函数为虚函数,与virtual
修饰类继承为虚继承没有关系:一个是实现多态的基础,而另一个是解决菱形继承的问题 - 同样的,假设不是父类指针或引用进行调用,不会构成多态,也不会发生重写(覆盖)行为
2.3、final 与 override
在 C++11
中,新增了两个多态相关的关键字:final
、override
final
:修饰父类的虚函数,不让子类的虚函数与其构成重写,即不构成多态
override
:修饰子类的虚函数,检查是否构成重写(是否满足重写的必要条件),若不满足,则报错
显然一个是 避免被重写 --> 不实现多态,而另一个是 检查是否完成重写 --> 后续实现多态
对父类的虚函数加上 final
:无法构成重写
对子类的虚函数加上 override
进行 重写检查
新标准中的小工具,在某些场景下很实用
final
还可以修饰父类,修饰后,父类不可被继承
注:final
可以修饰子类的虚函数,因为子类也有可能成为父类;但 override
无法修饰父类的虚函数,因为父类之上没有父类了,自然无法构成重写
2.4、重载、重写、重定义
截至目前为止,我们已经学习了三个 “重” 相关函数知识:重载、重写、重定义
这三兄弟不止名字很像,而是功能也都差不多,很多面试题中也喜欢考这三者的区别
重载:即函数重载,函数参数 不同而触发,不同的 函数参数 最终修饰结果不同,确保链接时不会出错,构成重载
重写(覆盖):发生在类中,当出现虚函数且符合重写的三同原则时,则会发生重写(覆盖)行为,具体表现为 父类虚函数接口 + 子类虚函数体,是实现多态的基础
重定义(隐藏):发生在类中,当子类中的函数名与父类中的函数名起冲突时,会隐藏父类同名函数,默认调用子类的函数,可以通过 ::
指定调用
重写和重定义比较容易记混,简言之 先看看是否为虚函数,如果是虚函数且三同,则为重写;若不是虚函数且函数名相同,则为重定义
注:在类中,仅仅是函数名相同(未构成重写的情况下),就能触发 重定义(隐藏)
3、抽象类
什么是抽象?难道是 围棋大师柯洁直播 “云顶之弈” 下电子围棋 吗?
当然不是,抽象类是一种极其特殊的类:不允许实例化对象
什么年代了还下传统围棋~
3.1、定义与特点
如何实现一个抽象类:在虚函数之后加上 =0
,此时虚函数升级为 纯虚函数
纯虚函数也可以与普通虚函数构成重写,也能实现多态,不过包含纯虚函数的类不能实例化对象,因此也被称为抽象类
注意:只要类中有一个函数被修饰为纯虚函数,那么这个类就会变成抽象类
纯虚函数:
virtual void func(int a = 10) = 0 { cout << "Person a: " << a << endl;
抽象的线条画无法直接看出作者的意图,抽象类也是如此,无法实例化出具体对象,你只知道这个类存在
出自著名画家 彼埃·蒙德里安
尝试使用 纯虚函数 构成的 抽象类 实例化对象
#include <iostream> using namespace std; //抽象类 class Person { public: //纯虚函数 virtual void func(int a = 10) = 0 { cout << "Person a: " << a << endl; } }; int main() { //纯虚函数无法实例化对象 Person p; Person pp = new Person(); //也不能new出对象 return 0; }
3.2、抽象类的用途
抽象类适合用于描述无法拥有实体的类,比如 人、动物、植物,毕竟这些都是不能直接使用的,需要经过 继承 赋予特殊属性后,才能作为一个独立存在的个体(对象)
#include <iostream> #include <string> using namespace std; class Person { public: Person(const string& name = string()) :_name(name) {} virtual void func() = 0 {}; protected: string _name; }; class Student : public Person { public: Student(const string& name = string()) :Person(name) {} //子类继承抽象类后,需要重写纯虚函数,否则仍然是抽象类 virtual void func() {}; }; int main() { //抽象类无法直接实例化对象 //Person p("newnew"); Student s("newnew"); return 0; }
抽象类的继承很好的体现了函数重写时,继承的是父类虚函数接口的事实,这正是实现多态的基础
普通继承:子类可以直接使用父类中的函数
接口继承:子类虚函数继承父类虚函数的接口,进行重写,构成多态
建议:假如不是为了多态,那么最好不要使用 virtual
修饰函数,更不要尝试定义纯虚函数
注意: 若父类中为抽象类,那么子类在继承后,必须对其中的纯虚函数进行重写,否则无法实例化出对象
4、多态实现原理
所以如此神奇的多态究竟是如何实现的?先来看一段简单的代码
#include <iostream> using namespace std; class Test { virtual void func() {}; }; int main() { Test t; //创建一个对象 cout << "Test sizeof(): " << sizeof(t) << endl; return 0; }
这是一个空类,其中什么成员都没有,但有一个虚函数
所以一个对象的大小为多少?
是 0
吗 ?
答案是 4
,当前是 32
位平台下,如果是在 64
位平台,大小会变为 8
大小随平台而变的只能是指针了,因此可以推测当前类中藏着一个 虚表指针
就是依靠这个 虚表指针+虚表 实现了多态
4.1、虚表与虚表指针
虚函数表(虚表)即 virtual function table
-> vft
,指向虚表的指针称为 虚表指针 virtual function pointer
-> vfptr
,在 vs
的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr
(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表
- 虚函数表中存储的是虚函数指针,可以在调用函数时根据不同的地址调用不同的方法
在下面这段代码中,父类 Person
有两个虚函数(func3
不是虚函数),子类 Student
重写了 func1
这个虚函数,同时新增了一个 func4
虚函数
#include <iostream> using namespace std; class Person { public: virtual void func1() { cout << "Person::fun1()" << endl; }; virtual void func2() { cout << "Person::fun2()" << endl; }; void func3() { cout << "Person::fun3()" << endl; }; //fun3 不是虚函数 }; class Student : public Person { public: virtual void func1() { cout << "Student::fun1()" << endl; }; virtual void func4() { cout << "Student::fun4()" << endl; }; }; int main() { Person p; Student s; return 0; }
如何通过程序验证虚表的真实性?
- 虚表指针指向虚表,虚表中存储的是虚函数地址,而
32
位平台中指针大小为4
字节 - 因此可以先将虚表指针强转为 指向首个虚函数 的指针,然后遍历虚表打印各个虚函数地址验证即可
vs
中对虚表做了特殊处理:在虚表的结尾处放了一个nullptr
,因此下面这段代码可能在其他平台中跑不了
//打印虚表 typedef void(*VF_T)(); void PrintVFTable(VF_T table[]) //也可以将参数类型设为 VF_T* { //vs中在虚表的结尾处添加了 nullptr //如果运行失败,可以尝试清理解决方案重新编译 int i = 0; while (table[i]) { printf("[%d]:%p->", i, table[i]); VF_T f = table[i]; f(); //调用函数,相当于 func() i++; } cout << endl; } int main() { //提取出虚表指针,传递给打印函数 Person p; Student s; //第一种方式:强转为虚函数地址(4字节) PrintVFTable((VF_T*)(*(int*)&p)); PrintVFTable((VF_T*)(*(int*)&s)); return 0; }
子类重写后的虚函数地址与父类不同
因为平台不同指针大小不同,因此上述传递参数的方式
(VF_T*)(*(int*)&p
具有一定的局限性
假设在64
位平台下,需要更改为(VF_T*)(*(long long*)&p
//64 位平台下指针大小为 8字节 PrintVFTable((VF_T*)(*(long long*)&p)); PrintVFTable((VF_T*)(*(long long*)&s));
除此之外还可以间接将虚表指针转为
VF_T*
类型进行参数传递
//同时适用于 32位 和 64位 平台 PrintVFTable(*(VF_T**)&p); PrintVFTable(*(VF_T**)&s);
传递参数时的类型转换路径
不能直接写成 PrintVFTable((VF_T*)&p);
,因为此时取的是整个虚表区域的首地址地址,无法定位我们所需要虚表的首地址,打印时会出错
- 类似于
int* arr[]
,int*
是第一个指针数组的首地址,遍历的是第一个指针数组;而int**
是整个指针数组的首地址,遍历的是整个指针数组,+1
会直接跳过一个指针数组
错误写法:
//错误写法 PrintVFTable((VF_T*)&p); PrintVFTable((VF_T*)&s);
综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系
虚表相关知识补充:
- 虚表是在 编译 阶段生成的
- 虚表指针是在构造函数的 初始化列表 中初始化的
- 虚表一般存储在 常量区(代码段),有的平台中可能存储在 静态区(数据段)
通过一段简单的代码验证 虚表的存储位置
int main() { //验证虚表的存储位置 Person p; Student s; int a = 10; //栈 int* b = new int; //堆 static int c = 0; //静态区(数据段) const char* d = "xxx"; //常量区(代码段) printf("a-栈地址:%p\n", &a); printf("b-堆地址:%p\n", b); printf("c-静态区地址:%p\n", &c); printf("d-常量区地址:%p\n", d); printf("p 对象虚表地址:%p\n", *(VF_T**)&p); printf("s 对象虚表地址:%p\n", *(VF_T**)&s); return 0; }
显然,虚表地址与常量区的地址十分接近,因此可以推测 虚表位于常量区中,因为它需要被同一类中的不同对象共享,同时不能被修改(如同代码一样)
函数代码也是位于 常量区(代码段),可以在监视窗口中观察两者的差异
4.2、虚函数调用过程
现在来看,虚函数的调用过程就非常简单了
- 首先确保存在虚函数且构成重写
- 其次使用【父类指针】或【父类引用】指向对象,其中包含切片行为
- 切片后,将子类中不属于父类的切掉,只保留父类指针可调用到的部分函数
- 实际调用时,父类指针的调用逻辑是一致的:比如虚表第一个位置调用第一个函数,虚表第二个位置调用第二个函数,但是因为此时的虚表是切片得到的,所以 同一位置可以调用到不同的函数,这就是多态
int main() { Person* p1 = new Person(); Person* p2 = new Student(); p1->func1(); p2->func1(); delete p1; delete p2; return 0; }
通过汇编代码观察:
注:下图中的函数地址仅供参考,与上图中的调用演示并不是同一次运行
4.3、动态绑定与静态绑定
静态绑定(前期绑定/早绑定)
- 在编译时确定程序的行为,也称为静态多态
动态绑定(后期绑定/晚绑定)
- 在程序运行期间调用具体的函数,也称为动态多态
p1->func1(); p2->func1(); add(1, 2); add(1.1, 2.2);
简单来说,静态绑定就像函数重载,在编译阶段就确定了不同函数的调用;而动态绑定是虚函数的调用过程,需要 虚表指针+虚表,在程序运行时,根据不同的对象调用不同的函数
5、单继承与多继承中的虚表
5.1、单继承中的虚表
单继承中的虚表比较简单,无非就是 子类中的虚函数对父类中相应的虚函数进行覆盖
- 单继承不会出现虚函数冗余的情况,顶多就是子类与父类构成重写
向父类中新增虚函数:父类的虚表中会新增,同时子类会继承,并纳入自己的虚表之中
向子类中新增虚函数:只有子类能看到,因此只会纳入子类的虚表中,父类是看不到并且无法调用的
向父类/子类中添加非虚函数时:不属于虚函数,不进入虚表,仅当作普通的类成员函数处理
5.2、多继承中的虚表
C++
中支持多继承,这也就意味着可能出现 多个虚函数重写 的情况,当父类指针面临 不同虚表中的相同虚函数重写 时,该如何处理呢?
#include <iostream> using namespace std; //父类1 class Base1 { public: virtual void func1() { cout << "Base1::func1()" << endl; } virtual void func2() { cout << "Base1::func2()" << endl; } }; //父类2 class Base2 { public: virtual void func1() { cout << "Base2::func1()" << endl; } virtual void func2() { cout << "Base2::func2()" << endl; } }; //多继承子类 class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1()" << endl; } virtual void func3() { cout << "Derive::func3()" << endl; } //子类新增虚函数 }; int main() { Derive d; return 0; }
此时的子类 Derive
中拥有两张虚表,分别为 Base1 + Derive::func1
构成的虚表、Base2 + Derive::func1
构成的虚表
此时出现了两个问题:
- 子类
Derive
中新增的虚函数func3
位于哪张虚表中? - 为什么重写的同一个
func1
函数,在两张虚表中的地址不相同?
这两个问题是多继承多态中的主要问题
5.2.1、子类新增虚函数的归属问题
在单继承中,子类中新增的虚函数会放到子类的虚表中,但这里是多继承,子类有两张虚表,所以按照常理来说,应该在两张虚表中都新增虚函数才对
但实际情况是 子类中新增的虚函数默认添加至第一张虚表中
通过 PrintVFTable
函数打印虚表进行验证
因此此时有两张虚表,所以需要分别打印
- 第一张虚表简单,直接取地址+类型强转,如法炮制即可
- 第二张虚表就比较麻烦了,需要在第一张虚表的起始地址处,跳过第一张虚表的大小,然后才能获取第二张虚表的起始地址
//打印虚表 typedef void(*VF_T)(); void PrintVFTable(VF_T table[]) { //vs中在虚表的结尾处添加了 nullptr int i = 0; while (table[i]) { printf("[%d]:%p->", i, table[i]); VF_T f = table[i]; f(); //调用函数,相当于 func() i++; } cout << endl; } int main() { Derive d; PrintVFTable(*(VF_T**)&d); //第一张虚表 PrintVFTable(*(VF_T**)((char*)&d + sizeof(Base1))); //第二张虚表 return 0; }
可以看出新增的 func3
函数确实在第一张虚表中
可能有的人觉得取第二张虚表的起始地址很麻烦,那么可以试试利用 切片 机制,天然的取出第二张虚表的地址
切片行为是天然的,可以完美取到目标地址
Base2* table2 = &d; //切片 PrintVFTable(*(VF_T**)table2); //第二张虚表
此时已经解决问题一:子类新增虚函数的归属问题 —> 添加至第一张虚表中
5.2.2、冗余虚函数的调用问题
在上面的多继承多态代码中,子类分别重写了两个父类中的 func1
函数,但最终通过监视窗口发现:同一个函数在两张虚表中的地址不相同
因此可以推测:编译器在调用时,根据不同的地址寻找到同一函数,解决冗余虚函数的调用问题
至于实际调用链路,还得通过汇编代码展现:
ptr2 在调用时的关键语句 sub ecx 4
sub
表示减法,ecx
通常存储this
指针,4
表示Base1
的大小- 这条语句表示将当前的
this
指针向前偏移sizeof(Base1)
,后续再jmp
时,调用的就是同一个func1
这一过程称为 this
指针修正,用于解决冗余虚函数的调用问题
为什么是 Base2
修正?
- 因为先继承了
Base1
,后继承了Base2
,假设先继承的是Base2
,那么修正的就是Base1
这种设计很大胆也很巧妙,完美解决了多继承多态带来的问题
因此回答问题二:两张虚表中同一个函数的地址不同,是因为调用方式不同,后继承类中的虚表需要通过 this
指针修正的方式调用虚函数
5.3、菱形继承多态与菱形虚拟继承多态(了解)
菱形继承问题是 C++
多继承中的大坑,为了解决菱形继承问题,提出了 虚继承 + 虚基表 的相关概念,那么在多态的加持之下,菱形继承多态变得更加复杂:需要函数调用链路设计的更加复杂
菱形虚拟继承多态就更不得了:需要同时考虑两张表:虚表、虚基表
- 虚基表中空余出来的那一行是用来存储偏移量的:表示当前虚基表距离虚表有多远
因为这种写法过于复杂,所以在实际中一般不会使用,更不会去考
如果感兴趣的同学可以看看下面这两篇相关文章:
6、多态相关面试题
一些简单的概念题,主要是为了回顾面向对象特性
6.1、基本概念(选择)
1.下面哪种面向对象的方法可以让你变得富有( )
A: 继承
B: 封装
C: 多态
D: 抽象
2.以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象
B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数
D:纯虚函数必须是空函数
3.关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
4.下面程序输出结果是什么? ()
#include<iostream> using namespace std; class A { public: A(const char* s) { cout << s << endl; } ~A() {} }; class B :virtual public A { public: B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; } }; class C :virtual public A { public: C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; } }; class D :public B, public C { public: D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2), C(s1, s3), A(s1) { cout << s4 << endl; } }; int main() { D* p = new D("class A", "class B", "class C", "class D"); delete p; return 0; }
A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D
5.多继承中指针偏移问题,下面说法正确的是( )
class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
答案:
A
A
D
A
C
6.2、综合问答(简答)
1.什么是多态?
多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。多态又分为静态的多态和动态的多态
2.为什么要在父类析构函数前加上 virtual
修饰?
与子类析构函数构成多态,确保析构函数能被成功调用
3.什么是重载、重写、重定义?三者区别是什么?
重载:同名函数因参数不同而形成不同的函数修饰名,因此同名函数可以存在,并且能被正确匹配调用
重写:父子类中的函数被virtual
修饰为虚函数,并且符合 “三同” 原则,构成重写
重定义:父子类中的同名函数,在不被重写的情况下,构成重定义,父类同名函数被隐藏重载可以出现任何位置,只要函数在同一作用域中,而重定义是重写的基础,或者是重写包含重定义,假设因为没有
virtual
修饰不构成重写,那么必然构成重定义,重写和重定义只能发生在继承关系中
4.为什么内联修饰可以构成多态?
不同环境下结果可能不同
内联对编译器只是建议,当编译器识别为虚函数时,会忽略
inline
5.静态成员函数为什么不能构成多态?
没有
this
指针,不进虚表,构造函数也不能构成多态
6.普通函数与虚函数的访问速度?
没有实现多态时,两者一样快
实现多态后,普通函数速度快,因为虚函数还需要去虚表中调用
🌆总结
以上就是本次关于 C++【多态】的全部内容了,在本篇文章中,我们重点介绍了多态的相关知识,如什么是多态、如何使用多态、构成多态的两个必要条件及两个例外该,最后还学习了多继承模式下多态引发的相关问题,探究了其原理。本文中最重要的莫过于 虚表 的相关概念,只有自己多测试、多调试、多画图 才能加深对虚表的理解
相关文章推荐
C++ 进阶知识
C++【继承】
STL 之 泛型思想
C++【模板进阶】
C++【模板初阶】
STL 之 适配器
C++ STL学习之【优先级队列】
C++ STL学习之【反向迭代器】
C++ STL学习之【容器适配器】