一、原理
1.1 什么是多态?
- 多态就是不同对象对同一行为会有不同的状态。(举例 : 学生和成人都去买票时,学生会打折,成人不会)
- 实现多态有两个条件:
1) 一是虚函数重写,重写就是用来设置不同状态的
2)二是对象调用虚函数时必须是指针或者引用
ps:没有这两个条件无法构成多态,很多笔试题都会利用这个陷阱让你上当!
实际上,代码上体现(动态)多态就是当父类指针指向子类对象,然后通过父类指针能调用子类的成员函数。
1.2 什么是虚函数?什么是重写?
虚函数是带有 virtual 关键字的成员函数
子类有个和父类完全相同(函数名,形参,返回值都相同,协变和析构函数除外)的虚函数,就称子类虚函数重写父类虚函数
总结:
在父类的函数前加上virtual关键字,在子类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,如果对象类型是子类,就调用子类的函数,如果对象类型是父类,就调用父类的函数。
- 虚函数机制(virtual function) , 用以支持执行期绑定,实现多态。
- 虚基类 (virtual base class) ,虚继承关系产生虚基类,用于在多重继承下保证基类在子类中拥有唯一实例。
1.3 多态的原理?
多态是用虚函数表实现的。
有虚函数的类都会生成一个虚函数表,这个表在编译时生成。
虚函数表是一个存储虚函数地址的数组,以 NULL 结尾。
如果要生成子类虚表,就要经过三个步骤:
- 第一步,将父类虚表内容拷贝到子类虚表上;
- 第二步,将子类重写的虚函数覆盖掉表中父类的虚函数;
- 第三步,如果子类有新增加的虚函数,按声明次序加到最后
1.4 多态如何调用?
- 满足多态的函数调用,程序运行起来后,根据对象中的虚表指针来找实际应该调用的函数;
- 而不满足多态的函数在函数编译时就确定函数地址了。
1.5 动态绑定与静态绑定?
- 静态绑定是程序编译时确定程序行为。
- 动态绑定是程序运行时根据具体的对象确定程序行为。
1.6 继承中的多态
-
单继承无虚函数覆盖:
虚函数按声明顺序存放,父类虚函数在子类虚函数前面.
-
单继承有虚函数覆盖:
覆盖的 f ( ) f() f() 替代原有父类虚函数位置, 没覆盖的不变
-
多继承无虚函数覆盖:
每个父类都有自己的虚表,子类成员函数被放入第一个父类表中
-
多继承有虚函数覆盖:
三个父类虚表中的 f ( ) f() f() 都会被子类函数指针覆盖
-
多继承规则:
多继承子类未重写的虚函数放在第一个继承父类部分的虚函数表中,继承的虚表都会覆盖 -
重复继承: B类数据重复,具有二义性.
二、面试常考
-
inline函数可以是虚函数吗?
答案:不能,因为inline函数没有地址,无法放到虚函数表中 -
静态成员可以是虚函数吗?
答案:不能, 因为静态成员函数没有 this 指针, 因为有 this 指针才能访问到虚表指针,有虚表指针才能找到虚表从而调用实际应该调用的函数。 -
构造函数可以是虚函数吗? 虚函数指针在什么时候生成的的?
答案:不能,因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的 -
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答案:可以, 并且最好把基类的析构函数定义成虚函数。当父类指针指向子类对象时,如果析构函数不是虚函数,析构就只会释放父类对象,造成内存泄漏。(因为析构重名,只能调用一个,调用默认的父类析构函数)。 定义成虚函数后,调用析构时就会取出虚表指针找到实际应该调用的函数。(指针虽然都是父类类型,但是指针内取出的虚表是不一样的,所以析构能调用子类析构) -
对象访问普通函数快还是虚函数更快?
答案:首先如果是普通对象,是一样快的,如果是指针对象或者是引用对象,则调用的普通函数快,因为普通对象在编译时就确定地址了,虚函数构成多态,运行时调用虚函数需要到虚函数表中去查找
参考:
三、代码
// 虚函数和多态
/* 基类成员函数用virtual修饰 1. 基类大小多出来4个字节 指针 _vfptr _vfptr 指向一个 函数指针数组 数组中保存所有虚函数的地址 2. 派生类继承基类, 会继承基类的函数指针数组里的元素 3. 如果派生类有重写,即在派生类里面有与基类重名的函数, 那么重写后的函数地址会覆盖函数指针 数组里的元素 4. 调用函数时, 会去虚函数表中找函数 */
#include "pch.h"
#include <iostream>
using namespace std;
class A
{
int n;
public:
virtual void show() {
cout << "A show" << endl; }
virtual void show2() {
cout << "A show2" << endl; }
};
class B :public A
{
public:
void show() {
cout << "B show" << endl; }
};
// 给没有返回值,没有参数的函数指针取个别名,叫做pFUNC
// 那么pFUNC 就是指向这样的函数的指针类型
typedef void(*pFUNC)();
class C :public A
{
public:
void show() {
cout << "C show" << endl; }
};
int main()
{
int num = 100;
int *po = #
cout << "Hello============== World!\n";
A a;
B b;
C c;
/* A* p = NULL; p = &b; p->show(); p = &c; p->show(); cout << "====================================" << endl; */
// pvfptr 指向 B类的对象b, 获得虚函数表地址 pvfptr
int* pvfptr;
pvfptr = (int*)&b; // 强行把&b(B*)转成(int*)类型 ,取得虚函数表的地址
cout << "&b: " << &b << endl; //
cout << "pvfptr: " << pvfptr << endl; //
// 获得对象b中的虚函数表的地址,就是虚函数表这个数组的首地址
int* _vfptr;
_vfptr = (int*)*pvfptr; // 然后,再次取值就可以得到第一个虚函数的地址了
cout << "*pvfptr: " << *pvfptr << endl;
cout << "(int*)*pvfptr: " << (int*)*pvfptr << endl;
//int** _vfptr;
//_vfptr = &pvfptr;
cout << "_vfptr: " << _vfptr << endl; // 获得虚函数表首地址
int* pfunc1 = (int*)*_vfptr;
cout << "pfunc1: " << pfunc1 << endl; // 获得虚函数表第一个函数地址
int* pfunc2 = (int*)*(_vfptr + 1);
cout << "pfunc2: " << pfunc2 << endl; // 获得虚函数表第二个函数地址
pFUNC pf1 = (pFUNC)pfunc1;
pf1();
pFUNC pf2 = (pFUNC)pfunc2;
pf2();
pFUNC pf3 = (pFUNC)_vfptr;
pf3();
pFUNC pf4 = (pFUNC)_vfptr;
pf4();
return 0;
}
/* _vfptr: 0046F084 pfunc1: 00401096 pfunc2: 00401285 B show A show2 */