基本概念
多态,顾名思义,就是多种形态,即不同对象去完成一个动作却产生不同结果。怎样才能实现多态呢,首先就需要了解虚函数。
虚函数
在类定义中,前面有virtual关键字的成员函数就是虚函数
class Base{
virtual int get()
};
int Base::get(){}
需要注意:
*virtual关键字只用在类定义时函数声明的时候,在写函数体时不需要写
*构造函数和静态成员函数不能声明为虚函数
多态的操作就是基于虚函数进行的,下面通过几种形式来认识多态(也可直接移步表现形式末尾看小结)
多态的表现形式
本部分主要讲动态多态
*派生类的指针可以赋给基类的指针
*通过基类指针调用基类和派生类中的同名虚函数时,
1)当指针指向基类的对象,被调用的就是基类的虚函数
2)当指针指向派生类的对象,被调用的就是派生类的虚函数
这种机制就叫多态
#include<iostream>
using namespace std;
class CBase{
public:
virtual void Func1() {cout<<"CBase virtual func1 constructed"<<endl;}
void Func2(){ cout<<"CBase func2 construct";
}
};
class CDerived:public CBase{
public:
virtual void Func1(){//与基类同名的虚函数
cout<<"CDerived virtual fun1 constructed"<<endl;
}
void Func2(){//与基类同名的普通成员函数
cout<<"CDerived func2 construct"<<endl;
}
};
int main(){
CDerived a;
CBase *p=a;//让基类的指针指向派生类的对象
p->Func1();//虚函数
p->Func2(); //普通成员函数
}
运行结果:
CDerived virtual fun1 constructed
CBase func2 construct
同理,将上面的指针改为引用,同样成立,这也是多态表现形式的另一种
CDerived a;
CBase &p=a;
p.Func1();
p.Func2();
再看下面一段代码
#include<iostream>
using namespace std;
class Base{
public:
void fun1(){
this->fun2(); //这里的this指针相当于fun1()的形参了
}
virtual void fun2(){
cout<<"Base fun2()"<<endl;
}
};
class Derived:public Base{
public:
virtual void fun2(){
cout<<"Derived fun2()"<<endl;
}
};
int main(){
Derived d;
Base *pBase=&d;
pBase->fun1();
return 0;
}
运行结果是
Derived fun2()
所以,我们又可以得出
在非构造函数,非析构函数的成员函数中调用虚函数,也是多态。
再看另一段代码
#include<iostream>
using namespace std;
class CBase{
public:
virtual void Func1() {cout<<"CBase virtual func1 constructed"<<endl;}
void Func2(){ cout<<"CBase func2 construct";
}
};
class CDerived:public CBase{
public:
void Func1(){//与基类同名的虚函数
cout<<"CDerived virtual fun1 constructed"<<endl;
}
void Func2(){//与基类同名的普通成员函数
cout<<"CDerived func2 construct"<<endl;
}
};
int main(){
CDerived a;
CBase *p=a;//让基类的指针指向派生类的对象
p->Func1();//虚函数
p->Func2(); //普通成员函数
}
这里可不是我为了偷懒写一个一摸一样的,大家仔细观察他与刚才的代码有何不同,没错,派生类里面和基类同名的函数没有加virtual关键字,但是运行结果确没有不同,是因为只要是基类的派生类的同名同参同返回值函数,派生类会自动把他转化为虚函数。
下面来做一个小结:
1)当通过基类指针调用同名虚函数时,指针指向派生类就调用派生类虚函数,指向基类就调用基类虚函数;
2)通过引用调用虚函数时,基类对象的引用就调用基类的虚函数,派生类对象的引用就调用派生类的虚函数;
3)在非构造非析构函数中调用虚函数也是多态;
4)派生类中和基类中同名同参数表的函数不叫virtual关键字也会自动成为虚函数。
多态实现原理
先看下面一个例子:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
运行结果是
8
按理来说一个int型不应该是4吗,为什么多了4呢,因为Base类中有虚函数,编译器在编译时会生成一个虚函数表,这张表大概是下面这个样子
每一个有虚函数的类,都有一个虚函数表,该类的任何对象都放着虚函数表的指针,虚函数表中列出了该类的虚函数地址 。
多态的函数调用语句,都被编译成一系列根据基类指针所指向的对象中存放的虚函数表的地址,在虚函数表中查找虚函数的地址,并且调用虚函数的指令。
虚析构函数
来看下面一段代码
#include<iostream>
using namespace std;
class son{
public:
~son(){
cout<<"bye from son"<<endl;
}
};
class grandson :public son{
public:
~grandson(){
cout<<"bye from grandson"<<endl;
}
};
int main(){
son *pson;
pson=new grandson;
delete pson;
return 0;
}
输出结果为
bye from son
可以看到,如果是普通的析构函数,在程序完成后,因为是基类的指针,并不会执行派生类的构造函数,那么开辟的派生类的空间就不会被释放,会一直占用内存,这时就需要声明一个虚构函数来完成对派生类开辟的空间的释放
#include<iostream>
using namespace std;
class son{
public:
virtual ~son(){
cout<<"bye from son"<<endl;
}
};
class grandson :public son{
public:
~grandson(){
cout<<"bye from grandson"<<endl;
}
};
int main(){
son *pson;
pson=new grandson;
delete pson;
return 0;
}
只需要将基类的析构函数定义为虚函数,派生类的析构函数就会自动变成虚析构函数
纯虚函数和抽象类
纯虚函数:没有函数体的虚函数;
抽象类:拥有纯虚函数的类。
class A{
private:
int n;
public:
virtual void fun()=0;//纯虚函数
void print(){cout<<"123"}
};
关于抽象类有几个性质:
1)抽象类只能作为基类使用;
2)不能创建抽象类的对象;
3)抽象类可以定义指针和引用;
4)继承了抽象类的派生类,如果没有实现基类全部的纯虚函数,就仍为抽象类,不能创建对象;
5)在成员函数中可以调用纯虚函数,在构造函数/析构函数中不能调用纯虚函数。
#include<iostream>
using namespace std;
class A{
public:
virtual void f()=0;//声明纯虚函数
void g(){
this->f();//普通成员函数可以调用纯虚函数
}
// A(){f()}//在构造函数中调用纯虚函数编译会直接报错
};
class B:public A{
public:
void f(){//对纯虚函数重写,从此派生类B脱离了抽象类的苦海
cout<<"B:f()"<<endl;
}
};
int main(){
B b;
b.g();
return 0;
}
运行结果
B:f()
抽象类作为一个类,连对象都不能创建,还有什么作用呢。其实,抽象类可以理解为对一大类的高度概括和抽象,他只能抽象出其中的一些特征但没有具体的实现方法,所以就定义为抽象类,再向下细分时,派生类就做到了具体实例化。就好比一个动物类,里面有一个函数输出动物的叫声,但是对于不同生物叫声千差万别,我们无法在动物类对其进行函数体的书写,这时就可以用抽象类先进行概括。