一.多态的现实意义与基本语法
多态的现实意义
- 现实意义上,多态指的是针对同一种行为,不同的对象去执行该行为时会有不同的表现和不同的执行结果,比如:
语法层面上的多态
- 语法层面上,多态是继承体系的不同类对象,调用了同一函数,但却得到不同的执行过程和结果。
构成多态的语法条件:
- 构成多态的第一个条件:
继承体系的父类中定义了虚函数(由virtual
关键字修饰的函数),并且该虚函数在其子类中完成了重写(父类虚函数的重写指的是,在其子类中实现了返回值,函数名,形参表完全相同的对应函数),比如:
- 虚函数的重写有两种特例:一是子类中的重写虚函数可以不加关键字
virtual
;二是父类虚函数返回值为父类对象的指针或者引用时,子类的重写虚函数返回值可以是其自身的指针或者引用(称为重写中的协变),比如: - 这两种特殊的虚函数重写语法实际中用的很少,是C++语法设计上的冗余
- 构成多态的第二个条件:
子类对象通过继承体系的父类的指针或者引用来调用重写的虚函数,此时便实现了多态,比如:
- 实现多态时,父类的指针或者引用所调用的具体虚函数重写版本是由父类的指针或者引用所指向的子类所决定的
子类和父类中重名函数间的关系梳理:
继承体系中析构函数的多态:
- 在继承体系中,子类和父类的析构函数名会统一被编译器改成
destructor()
- 如果父类中析构函数没有被设计成虚函数(并在子类中完成重写),可能会出现以下情形:
- 由于poly1和poly2都是父类指针,因此delete执行时都会去调用父类的析构函数,这会导致子类对象的内存空间得不到完全清理,从而可能造成内存泄漏,造成隐患.因此需要将父类的析构函数设计成虚函数,并且在子类中完成其重写,如下:
- 因此,在继承体系中,管理了动态内存的类的析构函数应统一设计成虚函数以防止内存泄漏的情形出现
C++11中针对多态编程的语法保护
- 在子类中的重写虚函数首部后加上
override
关键字,编译器就会自动检查虚函数是否重写成功,若没有重写成功则报错 - 纯虚函数:
为了规范多态编程,C++11引入了纯虚函数(在父类的虚函数后加上"=0"),比如:
- 含有纯虚函数的父类称为抽象类,抽象类不能实例化出对象.继承了抽象类的子类必须重写虚函数才能实例化出子类对象.抽象类使多态编程得到了良好地规范化
一道关于多态的恶心面试题:
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 分析:
- 普通函数的继承是一种实现继承,子类继承了父类的整个函数,可以使用该函数.
- 虚函数的继承在语法上是一种接口继承,子类继承的是父类虚函数的函数接口(首部)
二.多态的底层实现原理–虚函数表
虚函数表与多态
-
虚函数表:
- 虚函数表实质上是函数指针数组(C语言中的转移表),如果一个类中定义了虚函数,那么这个类对象的内存模型中就会存在虚表指针–指向函数指针的指针,该虚表指针会指向一个存放了虚函数地址的函数指针数组的首地址:
-
继承体系中的虚函数表:
- 在继承体系中,父类定义了虚函数,并且虚函数在子类中完成了重写,那么子类对象内存模型中的虚表指针会存在于子类对象的父类内存区块中,这也解释了为什么多态的实现条件必须是通过父类指针或者引用去调用重写的虚函数:
- 此时子类对象所对应的虚表中存放的函数指针指向的是子类的重写虚函数(而不是指向父类的虚函数)
- 父类的指针或者引用可以通过子类对象的虚表指针找到子类对象所对应的虚表,并在虚表中找到子类的重写虚函数的地址,从而实现多态调用:
- Tips:为了节省内存空间,同类型的类对象会共用虚表
- 在继承体系中,无论父类虚函数是否在子类中完成了重写,父类和子类分别拥有自己的虚表
- 只要类中定义了虚函数,程序运行时内存中就会生成相应的类的虚表,因此如果不实现多态就不要定义虚函数,避免内存浪费
接口的多态调用与普通调用
- 普通函数调用:
- 普通函数调用是一种编译时决策,也就是说,特定的函数调用语句在程序编译时就已经确定了要调用哪一个具体的函数
- 多态函数调用:
- 多态函数调用是一种运行时决策,特定的函数调用语句具体会调用哪一个虚函数重写版本取决于运行时父类的引用或指针在子类的虚函数表中的寻址定位结果
多态(动态绑定)是面向对象编程的三大特性之一,接口的多态调用指令相比于普通函数调用指令更具有灵活性和可扩展性,这一点为面向对象编程提供了更丰富的可能性。
三.多继承体系下类对象的虚函数表–图解
四.小探究–虚函数表的打印
- 简单的继承体系:
- Teenager类对象的内存模型:
- 定义一个teenager对象teen
- 先对
void(*)()
类型的函数指针进行typedef重命名:typedef void(*VFPTR)(void);
- 通过指针类型强制转换操作取出teen对象的虚表指针:
VFPTR* Tptr =((VFPTR*)*((int*)&teen))
代码段解释:先将teen的地址转换为int*
类型(缩短其运算单位大小)是为了解引用时获取teen对象中前sizeof(int*)
个字节的内容(也就是teen的虚表指针的内容)(在特定机器下,任何类型的指针大小都是确定的)
- 简单的虚表打印函数(同时通过函数指针调用虚函数)
typedef void(*VFPTR) ();
void printvtable(VFPTR *vtable)
{
cout <<"虚表地址>"<< vtable << endl;
for (int i = 0; i<2; ++i)
{
printf(" 第%d个虚函数地址 :0x%x,->", i, vtable[i]);
VFPTR f = vtable[i];
f();
}
cout << endl;
}
#include
#include
using namespace ::std;
class Person
{
public:
virtual void BuyTicket() = 0;
virtual void display() = 0;
};
class Teenager : public Person
{
public:
virtual void BuyTicket()override
{
cout << "买票-半价" << endl;
}
virtual void display()override
{
cout << "Teenager:display" << endl;
}
private:
int _age;
};
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()
{
Teenager teen;
VFPTR* vTableb = (VFPTR*)(*(int*)&teen);
printvtable(vTableb);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- Linux(CentOS)环境下的运行结果:
- 代码段在vs2022环境中无法正确提取到对象的虚表指针,个人推测vs2022存在封装保护机制
- 从小实验可以看见指针的灵活性,通过指针可以实现对类域访问限定机制的破坏
五.多态常见的面试知识点
- 类的构造函数不能定义成虚函数,类对象的虚表指针是在类的构造函数初始化列表中完成初始化的(注意虚函数表的生成是在调用类的构造函数之前完成的,虚函数表并不存在于类对象的内存空间中)
- 类的赋值运算符重载可以定义成虚函数,但是没有实际意义
- 类的虚函数表存放在代码段(只读常量区)中:
面试问题合集:
- 什么是多态?
- 什么是重载、重写(覆盖)、重定义(隐藏)?
- 多态的实现原理
- inline函数可以是虚函数吗?
- 答:可以,不过编译器会忽略inline的建议,函数不再是
内联函数,因为虚函数的地址要放到虚表中去。
- 静态成员可以是虚函数吗?
- 答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表.
- 构造函数可以是虚函数吗?
- 答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
- 对象访问普通函数快还是虚函数更快?
- 答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?
- 答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
- 什么是抽象类?抽象类的作用?
- 答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。