目录
1.4.3 C++11里的override和final关键字
前言
我认为多态是因为有了虚函数,通过继承,派生类可以重写虚函数,才有了C++的多态。所以要了解多态的原理,需要重点了解虚函数和虚基表。
一.多态的概念
1.1 概念
多态就是当要完成某个行为,当不同的对象去完成时会产生不同的效果。或者是说,不同的对象处理某一件事有不同的方法。
比如:在火车站买票,普通成年人,需要全价买票,学生可以半价买票,军人可以优先买票。
1.2 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
条件:
- 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写。
- 必须通过基类的指针或者引用调用虚函数
第一点注意点是:调用的函数只能是虚函数,并且派生类必须对基类的虚函数进行重写。如果不重写,只是继承,实现方法一样。如果基类的不是虚函数,重写函数只是构成隐藏。
第二个注意点就是:必须通过基类的指针或者引用调用虚函数。
满足多态:跟对象的类型无关,跟指向的对象有关,指向哪个对象调用就是它的虚函数。
不满足多态:跟调用对象的类型有关,类型是什么就调用谁的虚函数。
如果不满足多态条件:
1.不重写虚函数
2.不是虚函数,重写
3.不用指针或者引用调用
1.3 虚函数
虚函数:被virtual修饰的成员函数称为虚函数。注意:修饰的是成员函数。
class Person
{
public:
//虚函数
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
protected:
};
1.4 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数的返回值类型,函数名字,参数列表完全相同),但是函数的实现不同,称派生类重写了基类的虚函数。
class Person
{
public:
//虚函数
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
protected:
};
class Student :public Person
{
public:
//虚函数重写
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
//注意:在在虚函数重写时,派生类的虚函数不加virtual关键字,也可以构成重写
//因为继承,将基类的虚函数继承了下来,在派生类依旧保持虚函数的属性。
//但是这种写法不规范,不建议这样写。
//void BuyTicket()
//{
//cout << "半价买票" << endl;
//}
protected:
};
注意:
虚函数的重写,需要函数名,参数,返回值类型一样。但是派生类只是继承了函数的接口,接口就是函数名,参数,返回值类型。派生类重写只是实现不同。
1.4.1 协变
派生类重写基类虚函数,与基类虚函数的返回值不同。并且基类虚函数的返回值基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。
也可以是返回别的类的指针或者引用
class A{};
class B : public A {};
class Person {
public:
//返回A的
virtual A* f() {return new A;}
};
class Student : public Person {
public:
//返回B的
virtual B* f() {return new B;}
};
1.4.2 析构函数的重写
问题:
所以析构函数需要定义成虚函数,来构成多态:
这里是一个注意点,申请空间为派生类,但是赋值给基类时,需要将析构函数写成虚函数,构成多态。
1.4.3 C++11里的override和final关键字
1. final:修饰虚函数,表示该虚函数不能被继承,不能进行重写。
修饰类,类不能被继承
在C++98中,为了不让类被继承,可以将基类的构造函数私有化(private),于是派生类就不能构造属于基类的成员。
2.override 检查派生类虚函数是否重写了基类的虚函数,如果没有编译错误。
这个只能修饰派生类的虚函数,不能修饰基类虚函数
override关键字最好用来检查派生类虚函数接口(函数名,参数,返回值)是否写错
1.5 抽象类
在虚函数后面写上=0,这个函数称为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写的纯虚函数,派生类才能实例化出对象。
只有派生类经过纯虚函数重写才能实例化出对象。但是基类还是抽象类,不能实例化对象。
纯虚函数的作用:
1.一定程度上强制了派生类对纯虚函数的重写,如果不重写,派生类就不能实例化对象。
2.表示抽象的类型
应用场景:
比如:比如,只说一辆车,车是抽象的,是一个很笼统的概念。因为有多车,不知道具体什么车,并且你不会拿车实例化对象,车这个类就可以定义成抽象类,里可以写成员函数,但是不写具体实现。
但是某个品牌的车,比如奔驰,可以继承车这个抽象类,只需要重写纯虚函数,就可以实例化对象。
具体的继承抽象的。
1.6 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。只是可以有隐藏。
虚函数的继承是一种接口继承,派生类只是继承了基类虚函数的接口,目的是为了重写,达成多态。
所以如果不实现多态,不要定义成虚函数。
二.多态的原理
2.1 虚函数表
通过观察我们发现,对象b是8个字节,除了_num外,还有_vfptr指针。这个指针是我们叫做虚函数表指针,简称虚表指针。一个含有虚函数的类中至少有一个这样的指针。
_vfptr指针变量保存的是虚函数表的起始地址。
虚函数表实际是一个函数指针数组,虚函数表简称虚表。虚表里面保存的都是虚函数的地址。
2.2 派生类中的虚表指针
派生类不重写基类的虚函数。
通过上面现象说明一个结论:
派生类会继承基类的虚函数,会继承基类的虚表。但是派生类和基类的_vfptr变量内容不相等,说明两个虚表不是同一种虚表,只是虚表里的内容相同,所以会调用同一个函数。
派生类重写虚函数
通过上面现象说明一个结论:
派生类重写基类虚函数,会重写派生类虚函数表里的内容,将对应位置覆盖层重写虚函数的指针。
派生类增加虚函数
注意:
1.类中有虚函数只是这个类中多了虚函数表指针,不是将虚函数表保存到类中。
2.虚函数表最后会以nullptr结尾。
3.同类型的对象共用一张虚表,可以理解成一个类的虚表属于这个类的,实例化的对象,都公用这一张虚表。
总结派生类虚表的生成:
1.派生类会继承基类的虚表,当然两个虚表表不是一张虚表。派生类先将基表虚表的内容拷贝一份到派生类的虚表中
2.如果虚表重写虚函数,用派生类重写虚函数的地址覆盖掉虚表中对应虚函数的地址。
3.派生类增加虚函数,会在派生类虚表中声明次序增加到虚表的最后。
2.3 虚表保存在哪
首先说明,一个具有虚函数的类_vfptr保存在前面还是后面是有平台决定的,根据上面的现象,我们平台_vfptr是保存在最前面的。
2.4 多态原理
多态是基于虚函数的虚函数表。构成多态,跟对象有关。如果是基类对象,会去基类的虚表中找要调用虚函数的地址,去执行虚函数的代码。如果是派生类对象,会去派生类类的虚表中找要调用虚函数的地址,去执行虚函数的代码。
派生类虚函数重写之后,可以实现不同的对象,有不同的实现方法,展现不同的效果。
再来段代码理解一下:
通过汇编分析,看出满足多态以后函数调用,不是在编译时确定的,是在运行起来后到对象的需表中找的。
不满足多态,是在编译时确定好的。
2.5 动态绑定和静态绑定
- 静态绑定又称为前期绑定,在程序编译期间确定了程序的行为,也称静态多态。比如函数重载。在编译的时候确定了调用的函数。
- 动态绑定也称后期绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称动态多态。就像上面的,运行时在到虚表中找调用函数的地址。
- 多态,多数都是动态绑定。
三.单继承和多继承的虚函数表
3.1 单继承中的虚表
结论是否如上图所述:我们来打印一下虚表
打印代码
class Person
{
public:
virtual void func1(){ cout << "Person func1()" << endl; }
virtual void func2(){ cout << "Person func2()" << endl; }
};
class Student :public Person
{
public:
virtual void func1(){ cout << "Student func1()" << endl; }
virtual void func3(){ cout << "Student func3()" << endl; }
protected:
};
typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR;
//打印代码
void PrintVfTable(VFPTR *vftable){
for (int i = 0; vftable[i] != nullptr; i++){
//打印虚表内容
printf("vftable[%d]:%p\n", i, vftable[i]);
//调用这个函数
VFPTR fun = vftable[i];
fun();
}
cout << endl;
}
int main()
{
Person p;
Student s;
//要得到虚表指针的内容,由于这个平台,虚表指针是保存在开始的
//先得到对象地址,强转成int *得到前四个字节,就是虚表指针的地址
//再解引用,得到虚表指针的内容
//再强转成函数二级指针
PrintVfTable((VFPTR *)*(int *)&p);
PrintVfTable((VFPTR *)*(int *)&s);
getchar();
return 0;
}
结果和我们想的一样:
3.2 多继承中的虚表
根据上面的结论可以得到这样一张图:
#include<iostream>
using namespace std;
class Base1
{
public:
virtual void func1(){
cout << "Base1 : func1()" << endl;
}
virtual void func2(){
cout << "Base1 : func2()" << endl;
}
protected:
int _a;
};
class Base2
{
public:
virtual void func1(){
cout << "Base2 : func1()" << endl;
}
virtual void func2(){
cout << "Base2 : func2()" << endl;
}
virtual void func3(){
cout << "Base2 : func3()" << endl;
}
protected:
int _b;
};
//多继承
class Deirve :public Base1, public Base2
{
public:
virtual void func1(){
cout << "Deirve : func1()" << endl;
}
virtual void func4(){
cout << "Deirve : func4()" << endl;
}
virtual void func5(){
cout << "Deirve : func5()" << endl;
}
protected:
int _c;
};
typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR;
void PrintVfTable(VFPTR *vftable){
printf("虚表地址:%p\n", vftable);
for (int i = 0; vftable[i] != nullptr; i++){
//打印虚表内容
printf("vftable[%d]:%p\n", i, vftable[i]);
//调用这个函数
VFPTR fun = vftable[i];
fun();
}
cout << endl;
}
int main()
{
Deirve d;
//打印继承Base1的虚表
PrintVfTable((VFPTR *)*(int *)&d);
//打印继承Base2的虚表
//加Base1大小的字节数,到Base2的虚表指针。
//要加先强转成char *,步长为一个字节。不强转的话,步长为Deirve
PrintVfTable((VFPTR *)*(int *)((char *)&d + sizeof(Base1)));
getchar();
return 0;
}
画图表示为: