一、什么是多态
1.1多态的通俗解释
同一件事,不同人去做会产生多种不同的结果,就是“多态”
更深一点说就是:不同类型的对象去用同一个方法做事,产生的结果不同
1.2虚函数的重写(覆盖)与多态的条件
1.2.1virtual修饰的虚函数
只可以修饰成员函数,不可以修饰全局函数,如
class A{
public:
virtual void func(){//...}
};
1.2.2多态的条件
①虚函数在父类中定义,在子类中完成重写(即在子类中再次完成虚函数的实现)
②父类的指针或引用调用虚函数
1.2.3效果
可以达到“指向谁调用谁”这一多态的效果
1.2补:定义成员函数的时候,static与virtual不可以同时使用
①会出现存储位置不确定的问题,其函数一个要在静态区,一个要在代码段里
②静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,
可以通过类名::成员函数名 直接调用,
此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
1.3重写是一种特殊的隐藏
重写,即在派生类中重新实现一个与基类声明完全相同的虚函数,包括返回值类型,函数名,参数列表
1.4多态的使用举例
//基类:这里虚函数的virtual绝对不可以省略
class Person
{public: virtual void BuyTicket(){cout<<"全价买票"<<endl;}};
//派生类:这里虚函数的virtual可以省略,但不建议
class Student : public Person
{public: virtual void BuyTicket(){cout<<"半价买票"<<endl;}};
class Soilder : public Person
{public: virtual void BuyTicket(){cout<<"优先买票"<<endl;}};
//含有父类指针调用的函数
void func(Person& p)
{
p.BuyTicket();
}
int main()
{
//实例化出三个对象
Person p;
Student st;
Soilder so;
//多态的调用
func(p);//打印全价买票
func(st);//打印半价买票
func(so);//打印优先买票
//与隐藏做对比
p.BuyTicket();//打印全价买票
st.BuyTicket();//打印半价买票
st.Person:BuyTicket();//打印全价买票
return 0;
}
1.5关于多态调用的总结
多态调用,看的是指向对象的类型,指向谁调用谁的虚函数
普通调用,看的是调用者本身的类型,实现调用者的函数
1.6特殊的多态:协变,析构函数名改变
①:协变
虚函数重写要求三个“相同”,但有时即便没有满足也会出现多态,那就是协变:
当返回值的类型不同,且返回值类型为基类或派生类的指针或者引用(不需要自身的基类与派生类)的时候,会产生协变
例如:
class A{};
class B : public A{};
此时
class Person
{public: virtual A* BuyTicket(){cout<<"全价买票"<<endl;};
class Student : public Person
{public: virtual B* BuyTicket(){cout<<"半价买票"<<endl;};
class Soilder : public Person
{public: virtual B* BuyTicket(){cout<<"优先买票"<<endl;};
尽管虚函数返回值类型不同,也可以实现多态
②:为了让析构函数实现重写,所有虚构函数名会被统一为destructor(详情见本文1.7)
1.7析构函数的重写
析构函数是否建议设置成虚函数呢?
分析一下不难发现:析构函数一定要设计成虚函数
试想,如果使用前面的基类与派生类,但在派生类Student中加入了资源配置
class Student : public Person
{public:
virtual B* BuyTicket(){cout<<"半价买票"<<endl;
~Student()
{
delete _ptr;
_ptr=nullptr;
}
int * _ptr=new int[10];
};
此时在主函数中实现了如下代码:
Person* p1=new Student;
delete p1;
如果析构函数并不构成重写,一旦运行的话,new出来的空间包括了_ptr,但是在delete的时候却会调用Person类的析构函数从而错过_ptr的释放,造成——ptr空间泄露。
此时的问题很明显:希望调用~Student()这一个构造,而要解决只能通过重写,要重写的话需要满足两个条件,此时分析这两个条件
①同名不满足:一个是~Student(),一个是~Person()
要解决这一问题,编译器选择了处理所有析构函数的名为统一的~destructor(),完成同名
②虚函数的重写:还不是虚函数
要解决这一问题,我们应该在书写析构函数的时候就加上virtual
virtual ~Person(){//...}
1.7补:虚函数不可以随便写,派生类重写虚函数可以不加virtual但不建议
虚函数会放到虚表里,本身比起普通函数会有一些额外消耗;
1.8C++11中的final与override
1.8.1final
可以修饰类也可以修饰虚函数
①修饰虚函数的时候,表示这一虚函数不能被重写
用法:
class Parent{
virtual void func() final{}
};
②如果我们希望要实现一个类不能被继承这一目的
1>使用final修饰,class A final{//...};
2>将构造函数设为私有
1.8.1补:当通过构造函数设置为私有来达到类不可被继承的时候,如何实例化对象?
因为构造函数被设置为私有,所以想要调用它只能通过一个可以共有的,可以访问构造函数的函数,如在私有构造函数对应的类A中加
public:
A CreateObj(){return A()}
但这时直接运行会报错,仔细观察一下会发现还有这样一个矛盾没有解决:
要调用成员函数需要先有对象
要生成对象需要调用这一成员函数
那么该如何破局呢?改变编译中的顺序,利用static
public:
static A CreateObj(){return A()}
这样以后,成员函数在编译的过程中会直接被编译,要调用成员函数不需要先有对象了
一切完成,在主函数中即可
A a1 = A::CreateObj();
来实例化了
1.8.2override
检查派生类的虚函数是否重写了基类某个虚函数,未重写则报错
用法:
//在子类中
~Student() override{}
二、重载、重写(覆盖)、重定义(隐藏)的对比
2.1重载
①两个函数在同一个作用域
②函数名相同,但是参数列表不同
2.2重写(覆盖)
①两个函数分处基类和派生类两个不同的作用域
②函数名,返回值,参数列表都需要相同
③重写的两个函数都需要为虚函数
2.3重定义(隐藏)
①两个函数分处基类和派生类两个不同的作用域
②函数名相同
③父类与子类中同名的两个函数不构成重写就是重定义
三、抽象类
3.1纯虚函数与抽象类
3.1.1纯虚函数
在虚函数的后面加上=0来代替实现,则这个函数称为纯虚函数,如:
virtual void Drive()=0;
3.1.1补:纯虚函数可以有函数体,但是没什么意义
3.1.2抽象类
包含纯虚函数的类叫做抽象类(也叫接口类),特点是不能实例化出对象,且在派生类继承该基类后也无法实例化出对象,除非重写纯虚函数
假如3.1.1中的例子位于基类(一个抽象类)中,那么只要派生类中含有
virtual void Drive(){cout<<"大众"<<endl;}
那么这个派生类就可以实例化出对象了。
3.1.2补:抽象类虽然不能定义对象,但是可以定义指针
经常会用这一性质,例如父类指针指向子类,然后重写形成多态。
3.1.3总结
纯虚函数规定了派生类必须要重写,纯虚函数体现出了接口继承这一思想
3.2重写函数调用问题
(云笔记learn_7_8例题)
①继承的时候本质上不会直接把父类拷贝到子类,而是在生成对象时先把父类“放到”子类中,再放子类成员,这样的效果就是子类中父类部分的this指针仍然是父类指针类型,仅子类部分this指针是子类指针类型
②当虚函数构成重写的时候,如果子类指针或者引用调用函数,用的是父类中虚函数的声明和子类中虚函数的实现,也因此子类中的virtual可以省略
3.3影响类sizeof结果的虚函数
假设Person类变成这样
class Person
{
public:
virtual void BuyTicket(){cout<<"全价买票"<<endl;}
protected:
int _age=0;
char id='0';
};
(X86下)此时我们对Person类进行sizeof求大小会出现什么呢?
根据对结构体内存对齐的分析,我们得出的结果是8,但实际运行一下会发现结果是12,
原来,虚函数都放在了虚函数表里,而为了确定虚函数表的位置,我们在类的成员函数中加了一个指向虚函数表的指针(数组指针)_vfptr
它的本意就是virtual function pointor
3.3补:C++中打印函数地址的方式
C++中规定打印如数组的地址,函数的地址等地址的时候,需要加上“&”符号
那么当我们写了下面的代码
cout<<&Person::Buyticket()<<endl;
运行之后会发现预想的地址没有出现,而是打印出了一个1,可这是怎么回事呢?
原来是因为流插入的重载中包含了函数指针
ostream& operator<< (ostream& (*pf)(ostream&)); ostream& operator<< (ios& (*pf)(ios&)); ostream& operator<< (ios_base& (*pf)(ios_base&));
这就与我们的代码形成了一些冲突,导致结果不符合预期
因此如果要打印函数地址,我们推荐使用printf配合占位符%p来完成
3.4父类与子类中的虚函数
如果我们在一个类中实现了两个虚函数和一个普通函数,此时打印一下他们的地址,会发现三者十分接近,这说明了虚函数的实现其实也是与普通函数一起放在内存中的代码段区域,所谓虚函数表只是一个函数指针构成的数组
因此对于父类与子类中的虚函数位置可以用如下图来表示
此处注意:红蓝箭头所示并不是直接指向函数首语句地址,在汇编层面需要经过依据jump指令再跳转过去
3.5多继承当中虚表的问题
①一个子类继承多个父类,不论子类是否单独定义了新的虚函数,都是有几个父类就有几张虚表
②子类如果单独定义了新的虚函数,则会放在第一张虚表最后
四、动态绑定和静态绑定
4.1动态绑定
即动态确认地址,对应本节中虚函数的情况:运行时通过_vfptr到所需对象的虚表里去找不同函数,在汇编过程中call过去执行
4.2静态绑定
普通函数会直接到类中去找有没有声明和实现,没有就报错
只有声明:链接时候再找定义
有了声明和定义:直接找到函数实现第一条语句的地址
4.2补:
调用的时候指定类域的话,不会被识别为多态调用,只当作普通函数来处理
4.3总结
以3.4中的图示为基础说明
①基类对象与派生类对象的虚表不一样,图中虚函数1完成了重写,那么派生类对象的虚表中存的就是重写以后的虚函数1,因此也可以成为覆盖
②完成虚函数1的存储后,未被重写的虚函数2直接放到派生类虚表,普通函数则只被基础,不放入虚表
③对象中存虚表指针,虚表中存各个虚函数指针,虚函数与普通函数一样放在代码段中(通常为只读格式)
4.3补:强制类型转换与虚表地址的打印
强制类型转换:
只有本身就有一定联系的两者才可以进行强制类型转换
如:
int/double/char相互转换(均用于数据的记录)
int与int*等指针(int记录数据大小,指针是一个编号,有一些关联)
指针之间
虚表地址的打印:
如果我们想要取Person类型对象p1的虚表地址,可以怎么办呢?
p1本身是Person类型,那么&p1就是Person*类型
而①指针之间可以相互转换②_vfptr如果存在,那一定会存在对象的前四个字节(X86)
因此,我们只要把&p1的类型强转为int*,就可以取出前四个字节的指针,再解引用一下就可以达到我们的目的了:打印虚表地址
printf("%p\n",*(int*)&p1);
4.4虚表的存储位置
通过 4.3补 中的方式,可以打印出虚表的地址,对比可知距离常量区是最近的
(内存中有栈区,堆区,静态区,常量区,代码段)
因为虚表的内容只因实现过程而产生变化,所以同一类型的对象共享虚表
五、广义上多态
可以分为
①动态多态
即本节中所讲述的内容
②静态多态
函数重载,类模板,函数模板等都属于
六、多继承中的指针偏移问题
在定义类的多继承的时候,写在前面的父类实际存储位置也靠前
如:
class Base1 {public: int _b1;};
class Base2 {public: int _b2;};
class Real : public Base1,public Base2{public: int _b1;};
int main()
{
Real r;
Base1* p1=&r;
Base2* p2=&r;
Real* p3=&r;
return 0;
}
此时p1,p2,p3的关系是
p1==p3!=p2
如图