写在前面:
多态是什么意思,比如你给你的女神发消息,发了一大串,她可能很久才回,或者压根就不回,而当她的男神给她发消息时,她基本都是秒回,答应我好吗,不要再做天狗了,爱会消失的。
好了言归正传哈,多态就是多种形态的意思,就是当一起去完成某个事情,不同的对象会触发不同的场景,例如做公交车,有投币就是两块,公交卡就是一块,而学生卡就是六折,还有老年卡等等,这就是不同的对象就会产生不同的情况。
那么说到底这个多态有什么作用呢?
多态性使编写程序变成了通用化编程,而不是特殊化编程,例如,像老师,学生,工人,如果为每一个人编写一个类,那么就会特别的繁琐,将他们抽象成为一整个类又会降低代码的复用性,而在继承性和多态性下,可以使代码的复用性变高,符合现代商业化的程序开发。
一、多态的构成条件
多态是在继承条件下,不同得对象调用同一个函数,产生不同的行为
例如:同一个函数Print() A类会打印全大写的HELLOWORLD!,而B类打印小写的helloworld!
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "HELLO WORLD!" << endl;
}
};
class B :public A
{
public:
void Print()
{
cout << "hello world!" << endl;
}
};
int main()
{
A a;
B b;
a.Print();
b.Print();
return 0;
}
也有人问,这不就是在继承中的,子类继承父类的成员函数,然后再将父类的重写覆盖吗?是的但是要注意这里为了防止菱形继承,应该设计出虚拟继承。
因此要构成多态要满足以下两个条件:
1.必须通过基类的引用或者指针调用虚函数
2.被调用的函数必须是虚函数,派生类必须重写基类的虚函数
这里为什么是,派生类要通过基类的引用或者指针来调用虚函数呢?我们知道,在继承中,是允许将派生类的引用,指针或者对象赋值给基类的但是基类的指针和引用却不能赋值给派生类,这是编译器不允许的操作,当基类想要调用派生类的成员函数也会产生错误,虽然说可以通过强制类型转换可以使基类访问派生类中基类没有的函数,但是这样会产生一定的风险;因此派生类想要调用基类中的函数,必须通过指向派生类的基类指针来获取多态行为。而且大家思考如果不通过这样得方式,假如通过他们各子的对象调用各自得函数那么这样就没多态啥事了,就无法体现出多态得思想了。
那么像上面的示例应该为:
#include<iostream>
using namespace std;
class A
{
public:
virtual void Print()//被调用的必须为虚函数
{
cout << "HELLO WORLD!" << endl;
}
};
class B :public A
{
public:
virtual void Print()//必须要进行虚函数重写
{
cout << "hello world!" << endl;
}
};
void Test(A& a)//使用引用调用虚函数
{
a.Print();
}
void Test1(A* a)//使用指针调用虚函数
{
a->Print();
}
int main()
{
A a;
B b;
Test(b);
Test1(&b);
return 0;
}
既然第二个条件是:派生类必须重写基类的虚函数,那么何为爱,何为生,咳咳 何为虚函数?
二、虚函数
C++中的虚函数的作用主要是实现了多态的机制。
那么虚函数就是通过 virtual 修饰的函数 例如:
virtual void print(){}
通过上述我们知道派生类通过基类的指针可以访问到基类中的虚函数,来实现多态性,那么这其中的原理究竟是什么呢?
虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
注意:重写是可以不加virtual关键字的,也可以构成虚函数的重写,但是这样的书写是不规范的,建议加上virtual。
另外还有虚函数的两个特殊的例子:
1.协变,虽然说虚函数重写必须要函数名,返回值,参数相同。但是这里有一种特殊的情况
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
例如:
class A
{
public:
virtual A* Print(){return new A}
};
class B:public A
{
public:
virtual B* Print(){return new B}
};
2.析构函数的重写
这是指当基类的析构函数是虚函数时,即使派生类和基类的析构函数名称不同,但是还是会构成虚函数的重写,虽然违背了,函数名相同这一重写的规则,但是这里时因为编译器做了一些特殊化的处理导致的
override 和 final关键字
为了防止一些重写时发生的种种错误,C++提出了两个关键字,来对虚函数进行检查,检查其是否重写。
1.final
被final修饰的虚函数,不能再被继承
例如:
2.override
检查派生类是否重写了基类的某个虚函数,如果没有重写则编译报错
多态的原理
1.虚函数表
顾名思义就是存放虚函数的表,但是这里并不是存放的虚函数,而应该是虚函数的地址,每个含有虚函数的类或者继承了有虚函数的类的子类都会有这个虚函数表,这样通过虚函数表来找到虚函数实现多态。
当然只有虚函数会被放入到这个虚函数表中,其他的成员变量或者成员函数是不会放入这个表中的。
virtual void print(){}
virtual void fun(){}
这样的两个虚函数,那么这两个虚函数的地址将会被放在虚函数表中:
而当派生类继承这个基类时,除了会继承里面的成员函数、变量等还会继承这个虚函数表
虚函数表的继承
例如下面这个类:
class A
{
public:
virtual void Print(){}
};
class B :public A
{
public:
virtual void Fun() {}
};
可以在VS 2017 开发人员命令提示符的监视窗口查看:
输入以下命令
cl /d1 reportSingleClassLayout类名 文件名
输入指令:
cl /d1 reportSingleClassLayout类名 文件名
例如我这里的类名是 B 文件名是Test 应该输入:
cl /d1 reportSingleClassLayoutB C:\Users\Dell\source\repos\VarTest\VarTest\Test.cpp
注意 这里的文件名最好是绝对路径,否则会显示找不到,文件名
按下回车后查看:
这里我们可以看到B类继承了A类之后,虚函数表里面存放了两个虚函数的地址,第一个是A类的虚函数地址,第二个是子类的虚函数地址
而当我们将A类的虚函数重写之后
class B :public A
{
public:
virtual void Print(){}
virtual void Fun() {}
};
这里可以看到,当重写了基类的虚函数之后,可以看到这里原来基类的虚函数被覆盖成了派生类的虚函数。
总结:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在 派生类中的声明次序增加到派生类虚表的最后。派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就 是存在部分的另一部分是自己的成员。
- 基类对象和派生类对象虚表是不一样的,虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的 叫法,覆盖是原理层的叫法。
- 继承下来的虚函数,放进了派生类的虚函数表,,不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
虚函数存在的位置,虚函数表存在哪里?
虚函数和普通的函数一样存在代码段里,但是虚函数表指针,指向的虚函数表存在哪里呢?
虚函数表其实是放在代码段的。
三、抽象类
当一个类里面的虚函数,如下形式时
virtual void Print()=0
{}
这种虚函数被称为纯虚函数,而含有这种纯虚函数的类被称为抽象类(也叫做接口类),这种类是不能实例化出对象的,继承这个类的派生类也不能实例化出对象,只有派生类对这个纯虚函数进行重写才可以实例化出对象。
例如:
class A
{
public:
virtual void Print()=0 {}
};
class B:public A
{
public:
virtual void Print() override{}
};
int main()
{
A a;
B b;
return 0;
}
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的 继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所 以如果不实现多态,不要把函数定义成虚函数。