多态的概念:
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
多态又分为静态多态和动态多态:
(1)静态多态,也称为静态绑定或前期绑定(早绑定):函数重载和函数模板实例化出多个函数(本质也是函数重载)。静态多态也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
(2)动态多态,也称为动态绑定或后期绑定(晚绑定):在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,即运行时的多态。在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
父类指针或引用指向父类,调用的就是父类的虚函数
父类指针或引用指向子类,调用的就是子类的虚函数
为什么动态多态无法在编译时就确定:
在编译的时候编译器并不知道用户选择的是哪种类型的对象。如果不是虚函数,则采用静态绑定,函数体与函数调用在程序运行之前(编译期间)就绑定了。
当函数声明为虚函数时,如果使用指针或引用的形式,那么由于指针指向的对象不确定是基类还是派生类,那么编译器就无法采用静态绑定,所以就只有通过动态绑定的方式。因此编译器通过创建一个虚函数表存放虚函数的地址,在运行时,编译器通过虚函数指针在虚函数表中找到正确的函数版本,然后调用。
多态的两个要求:
1、被调用的函数必须是虚函数(注意,只能调用虚函数),子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
2、父类指针或者引用去调用虚函数。
#include<iostream>
using namespace std;
//基类
class Print1
{
public:
//无参构造
Print1(){}
//有参构造
Print1(string name1) {
this->Name1 = name1;
}
//返回Name1
virtual string getName() {
return this->Name1;
}
//输出Print1
virtual void show() {
cout << "Print1的打印" << endl;
}
private:
string Name1;
};
//子类
class Print2:public Print1
{
public:
//无参构造
Print2() {};
//有参构造
Print2(string name2) {
this->Name2 = name2;
}
// 重写父类getName() 返回Name2
string getName() {
return this->Name2;
}
// 重写父类show() 输出Print2
void show() {
cout << "Print2的打印" << endl;
}
private:
string Name2;
};
//通过父类的引用调用虚函数
void PrintName(Print1 &P)
{
P.show();
}
int main()
{ //Name1 //Name2
Print1 P1("Name1"); Print2 P2("Name2");
Print1* p1 = &P1;
Print2* p2 = &P2;
//通过父类指针调用虚函数
cout << p1->getName() << endl; // P1.getName()被调用,返回Name1
p1 = p2;
cout << p1->getName() << endl; //P1.getName()被调用,返回Name1
//通过父类的引用调用虚函数
PrintName(P1);
PrintName(P2);
return 0;
}
根据C++11特性,子类重写父类虚函数的时候,我们可以在子类重写函数后面加入override,以防bug发生
override
关键字作用:
如果派生类在虚函数声明时使用了override描述符,
那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。
简单来说就是检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
注释掉父类函数,子类getName()之后加上override
此时会发生
抽象类:
虚函数后面加上=0就是纯虚函数,包含纯虚函数的类即为抽象类(接口类)。抽象类不能实例化出对象,派生类继承抽象类后若没有重写纯虚函数那么仍为抽象类,亦不能实例化出对象。纯虚函数规范了派生类必须重写虚函数,并且更加体现出了接口继承
子类没有重写抽象类
#include<iostream>
using namespace std;
//纯虚函数
class Base
{
public:
//纯虚函数
//只要有一个纯虚函数,这个类称为抽象类
//抽象类特点:
//1.无法实例化对象
//2.抽象类的子类 必须要重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
};
void test01()
{
//Base b; //抽象类是无法实例化对象
//new Base; //抽象类是无法实例化对象
//Son s;//子类必须重写父类中的纯虚函数,否则无法实例化对象
}
int main()
{
test01();
return 0;
}
子类重写抽象类
#include<iostream>
using namespace std;
//纯虚函数
class Base
{
public:
//纯虚函数
//只要有一个纯虚函数,这个类称为抽象类
//抽象类特点:
//1.无法实例化对象
//2.抽象类的子类 必须要重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
void func() //子类重写抽象类
{
cout << "func函数调用" << endl;
}
};
void test01()
{
//Base b; //抽象类是无法实例化对象
//new Base; //抽象类是无法实例化对象
Son s;//子类必须重写父类中的纯虚函数,否则无法实例化对象
Base* base = new Son;
base->func();
}
int main()
{
test01();
return 0;
}
析构函数的重写:虚析构和纯虚函数。
使用析构函数的重写,使得发生多态时,子类的析构函数也能调用
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A的构造" << endl;
}
~A()
{
cout << "A的析构" << endl;
}
//纯虚函数
virtual void show()=0;
};
class B:public A
{
public:
B(int num)
{
//将这个成员属性指向堆区
this->Num = new int(num);
cout << "B的构造" << endl;
}
//堆区数据手动开辟,手动释放
~B()
{
if (Num != NULL) {
delete Num;
Num = NULL;
}
cout << "B的析构" << endl;
}
void show()
{
cout << "num=" << *Num << endl;
}
//创建指针类型的成员属性
int* Num;
};
int main()
{
A* a = new B(10);
a->show();
//父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄漏
delete a;
return 0;
}
可以看到,父类指针或者引用指向派生类对象,父类指针在析构时候,只调用了父类析构,导致子类对象中有堆区属性,出现内存泄漏。
解决,父类析构使用虚析构函数或者纯析构函数。
虚析构
纯虚析构
这里注意,在类内声明纯虚析构函数时,类内声明,类外要初始化
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A的构造" << endl;
}
//虚析构函数
/*virtual~A()
{
cout << "A的析构" << endl;
}*/
//纯虚析构
virtual ~A() = 0;
//纯虚函数
virtual void show()=0;
};
//纯虚析构需要类内声明,类外初始化
A::~A() { cout << "A的析构" << endl; }
class B:public A
{
public:
B(int num)
{
//将这个成员属性指向堆区
this->Num = new int(num);
cout << "B的构造" << endl;
}
//堆区数据手动开辟,手动释放
~B()
{
if (Num != NULL) {
delete Num;
Num = NULL;
}
cout << "B的析构" << endl;
}
void show()
{
cout << "num=" << *Num << endl;
}
//创建指针类型的成员属性
int* Num;
};
int main()
{
A* a = new B(10);
a->show();
//父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄漏
delete a;
return 0;
}