多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
多态的构成条件
1.继承
2.必须通过基类的指针或者引用调用虚函数
3.被调用的函数必须是虚函数,而且派生类必须对基类的虚函数进行重写
虚函数及虚函数的重写
被virtual修饰的类成员函数称为虚函数
class Person{
public:
virtual void BuyTicket(){
cout << "buy ticket " << endl;
}
};
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数的返回值,参数列表,函数名完全相同),称子类的虚函数重写了基类的虚函数
class Person{
public:
virtual void BuyTicket(){
cout << "buy ticket " << endl;
}
};
class Student:public Person{
public:
virtual void BuyTicket(){
cout << "buy ticket " << endl;
}
};
注意:基类和派生类的访问权限可以不同:一般讲基类的虚函数设为public
虚函数重写的两个例外
1.协变(基类与派生类虚函数的返回值不同)
派生类重写基类虚函数时,与基类虚函数的返回值类型不同.即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用------称为协变
class A
{
};
class B :public A
{
};
class Person{
public:
virtual A* f(){
return new A;
}
};
class Student:public Person{
public:
virtual B* f(){
return new B;
}
};
2.析构函数的重写
如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,斗鱼基类的析构函数构成重写,虽然基类和派生类的析构函数的函数名不同,但可以理解为编译后统一变成了destructor
C++的多态性用一句话概括就是:
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数
C++11中检测虚函数是否重写的关键字- - - -final和override
1.final修饰虚函数,表示该虚函数不能被派生类重写
final修饰类,表示类 不能被继承
2.override:用来检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则报错
注意:override只能加在派生类虚函数后边,如图
抽象类
在虚函数的后面写上=0,这个函数就被称为纯虚函数,而包含储蓄函数的类就叫做抽象类(也叫接口类)
注意:抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写虚函数,派生类才能实例化出对象
class Person
{
public:
virtual void Ticket() = 0;
};
class Student :public Person
{
public:
virtual void Ticket()
{
cout << "50% off" << endl;
}
};
class Soldier :public Person
{
public:
virtual void Ticket()
{
cout << "first priority " << endl;
}
};
void Test(){
Person* pStudent = new Student;
pStudent->Ticket();
Person* pSoldier = new Soldier;
pSoldier->Ticket();
}
注意:
普通函数的继承是一种实现继承,派生类继承了基类的函数,在派生类中也可以实现基类函数的使用,而虚函数的继承目的是重写然后实现多态,继承的是接口,所以不实现多态,不要把函数定义为虚函数
多态的原理
纯虚函数和虚函数表
先看一段代码
class Base
{
public:
virtual void Func(){
cout << "Func()" << endl;
}
private:
int _b = 1;
};
int main(){
Base b;
cout << sizeof(b) << endl;
return 0;
}
窗口和输出如下
编译器给带有虚函数的类多增加了4个字节
我们知道,如果是一个普通的类,对象成员int _b=1,输出的是4,从图中可以看到,除了_b以外还多一个_vfptr在对象的前面
对象中的_vfptr称为虚函数表指针,一个含有虚函数的类中至少含有一个虚函数指针,因为虚函数的地址要放到虚函数表(虚表)中
对以上的类做出以下改动:
1.增加一个派生类Dev去继承Base
2.Dev中重写Func()
3.在基类Base中再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
virtual void Func(){
cout << "Func()" << endl;
}
virtual void Func2(){
cout << "Func()" << endl;
}
void Func3(){
cout << "Func()" << endl;
}
private:
int _b = 1;
};
class Dev :public Base
{
public:
virtual void Func(){
cout << "Func()" << endl;
}
private:
int _d = 2;
};
int main(){
Base b;
Dev d;
cout << sizeof(b) << endl;
cout << sizeof(d) << endl;
return 0;
}
结果和窗口
注意b和d的_vfptr都是指针,指针指向的空间里面存放的正是下面的Func和Func2
两个Func2的地址是相同的,都是是基类的,但是两个Func的地址是不同的,Func完成了重写
但是如果在派生类中再增加一个虚函数Func4,按照正常情况来讲,Func4也是会存在于虚表中的,但是在内存窗口却观察不到Func4的存在,那Func4倒地存在于哪里呢?
对象中多出的四个字节是虚表指针,虚表指针指向的是虚函数表,虚函数表中又存放的是虚函数的入口地址,如果能将虚函数的入口地址拿到,就可以调用到这些虚函数,所以第一步需要先将对象的前四个字节内容拿到
先创建一个对象
Base b;
&b 拿到对象的空间,但是Base*直接解引用的话又变成了b
所以想拿到前四个字节的内容,需要将其转换一下
*(int*)&b; 刚好是int*指向的空间的内容,但是拿到的是一个整型数字,
与指向虚表的地址在数值上是相等的,所以需要对数字的类型进行强转
虚表中存放的是(多个)虚函数的地址,即存放的是函数指针,所以虚表可以看做是一个函数指针数组 ------存放内容的类型为void(*)()
以为void(*)()不太好使用,所以取一个别名 typdef void(*P)();
即虚表中存放的都是P*类型的参数,所以强转之后要得到的为(P*)* (int*)&b
//打印虚表
//传基类对象的指针或引用,不光可以打印基类的虚表,还可以打印派生类的虚表.
//基类的引用可以直接引用派生类的对象
typedef void(*P)();
void Print(Base &b)
{
P* p=(P*)*(int*)&b;//从对象的前四个字节中获取虚表的地址,用P*类型的变量p将转换的地址接收一下
//通过虚表指针获取虚函数的入口地址并调用
while (*p)//循环打印虚表中的虚函数,因为虚表最后位置为空,所以循环条件设为*p
{
(*p)();//对p这个指针解引用,相当于调用该函数
++p;//每次++移动四个字节
}
}
int main(){
Base b;
Dev d;
cout << "Base:" << endl;
Print(b);//打印基类对象的虚函数
cout << "Dev:" << endl;
Print(d);//打印派生类对象的虚函数
return 0;
}
总结:
1.派生类对象中也有一个虚表指针,d对象由两部分组成:第一部分是继承自基类的成员,另一部分是自己的成员
2.基类b对象和派生类d对象的虚表是不一样的,Func()完成了重写,所以d的虚表中存放的是重写的Dev::Func ,所以虚函数的重写也叫作覆盖,覆盖是指虚表中指向虚函数的指针的覆盖(注意这里:虚函数不存在于虚表中,虚表中存放的是虚函数的指针,虚函数同普通函数一样,存在于代码段,另外,对象中存的不是虚表,而是虚表的指针,虚表在VS中是存在于代码段的)
3.因为Func2是虚函数,所以继承下来以后也还是虚函数,其指针放进了虚表,Func3也继承下来,但是由于Func3是普通函数,不会放进虚表
(而且在派生类中调用Func3时会调用基类的Func3)
4.虚函数表本身是一个存放虚函数指针的指针数组,这个数组最后面放了一个nullptr
5.多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
派生类虚表的构建规则
1.将基类虚表中的内容拷贝一份到派生类的虚表中
2.如果派生类重写了基类的某个虚函数,则用派生类自己的虚函数替换虚表中相同偏移量位置的基类虚函数
3.派生类自己新增加的虚函数按照其在类中的声明次序依次放在虚表的最后
动态绑定与静态绑定
1.静态绑定(前期绑定/早绑定),在程序编译期间确定了程序的行为,也成为了静态多态,例如,函数模板函数重载
2.动态绑定(后期绑定/晚绑定),在程序运行期间,根据具体拿到的类型决定程序的具体行为,调用具体的函数,也成为了动态绑定
多态常见的面试题
1.什么是多态?
答:通俗来讲就是多种形态,在不同继承关系的类对象,去调用不同函数,产生了不同的行为
2.什么是重载,重写(覆盖),重定义(隐藏)?
答:
重载- - -同一个类中函数名相同,函数的参数列表不相同的两个及两个以上的函数就是函数重载
重写—是在子类继承父类的时候,对父类的虚函数进行了覆盖。重写会使程序发生动态联编,产生多态。
重定义—是在子类继承父类的时候,对父类的非虚函数进行了覆盖。
3.多态的实现原理?
答:多态的实现原理是通过虚函数表和vptr指针实现的
多态的三个条件:1.继承,2.虚函数重写,3.父类指针或引用指向子类对象
重写就是派生类中有一个跟基类完全相同的虚函数(即派生类虚函数的返回值,参数列表,函数名完全相同)
当类中声明虚函数时,编译器会在类中生成一个虚函数表;
虚函数表是一个存储类成员函数指针的数据结构; 它是由编译器自动生成与维护的;
virtual成员函数会被编译器放入虚函数表中;
存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr,称为虚函数表指针)
在调用时,编译器就会根据func是否为虚函数来进行相应的解释:
1.如果不是虚函数:编译器可直接确定被调用的成员函数(也叫做静态绑定)
2.如果是虚函数:编译器根据对象的vptr指针所指向的虚函数表中来查找函数并调用(也叫做动态绑定)
4.inline(内联函数)可以是虚函数吗?
答:不能,内联函数没有地址,无法将地址放在虚函数表中
5.构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
6.静态成员可以是虚函数吗?
答:不能,静态成员函数没有this指针,静态成员不属于任何类对象或者类实例,而虚函数调用需要从对象前四个字节取到虚表指针,所以把静态成员设为虚函数没有任何意义
7.析构函数可以是虚函数吗?
答:可以,在继承体系中,基类的析构函数最好定义为虚函数,因为在创建子类对象之前,会调用父类的构造函数,即除了子类对象之外,还会产生一个父类对象。如果我们不将子类和父类的析构函数定义为虚函数时:将只调用父类的析构函数,而不会调用子类的析构函数,造成内存泄漏
8.对象访问普通函数快还是访问虚函数快?
答:
1.如果是普通对象,速度是一样快的
2.如果是指针对象或者是引用对象,则调用普通函数快,因为构成多态时,运行时调用虚函数要到虚函数表中去查找
9.虚函数表是什么时候生成的?存在于哪里?
答:虚函数表实在编译阶段生成的,存在于代码段,虚表指针是在构造函数时放在对象前四个字节中的