在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。
举个例子:有一个父类叫做人,子类有学生,有军人。分别用父类,子类,实例出三个对象:普通人,学生A,军人B。这三个对象去做同一件事->买票,拿到的结果不一样,普通人是成人票,学生是学生票,军人是优先买票。这就是多态,同一件事,不同的对象去做,会有不同的结果(也就是调用不同的函数)。
多态有两大类:
构成多态需要满足两个条件:
需要了解:
(1) 虚函数
在成员函数前加上关键字virtual
,就为虚函数。
(2) 重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类
型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写需要满足:
注意
:重写的两个例外
class person
{
public:
virtual person* buy_ticekt()
{
cout << "买的票,是全价" << endl;
return this;
}
};
class student : public person
{
public:
virtual student* buy_ticekt()
{
cout << "买的票,是半价" << endl;
return this;
}
};
析构函数的格式为:~类名(),那么基类和派生类的类名一定不同,所以它们可以对析构函数重写吗?答案是可以重写的。这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。也就是说,基类和派生类的析构函数名称,编译时会按照同名处理。
class person
{
public:
virtual ~person()
{
cout << "~person()" << endl;
}
};
class student : public person
{
public:
virtual ~student()
{
cout << "~student()" << endl;
}
};
int main()
{
person* p1 = new person;
person* p2 = new student;
delete p1;
delete p2;
}
很明显,基类对象调用基类析构;派生类对象调用派生类析构,派生类中继承的基类数据由基类析构。
还有一点需要强调
:
只在基类将想要实现多态的函数设为虚函数就可以了,子类重写的函数前可以不加virtual,但是这种写法不够规范,我建议只要是重写那么就设为虚函数,什么情况下,可以这样使用呢?那就是析构函数的重写。
有一个父类叫做人,子类有学生。去完成一件事:买票,人去买票是全价,学生去买票是半价。
#include
using namespace std;
class person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是全价" << endl;
}
};
class student : public person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是半价" << endl;
}
};
int main()
{
person common;
student A;
// 基类指针去调用
person* ptr1 = &common;
person* ptr2 = &A;
ptr1->buy_ticekt();
ptr2->buy_ticekt();
// 基类引用去调用
person& y1 = common;
person& y2 = A;
y1.buy_ticekt();
y2.buy_ticekt();
return 0;
}
上面的完全是满足多态的条件的,我们来看看运行结果: 有人可能对重写还是不太理解,后面讲原理时会细讲的。
从上面重写的学习可以看出,c++对于重写的要求还是很高的。所以引入这两个关键字,进一步的规范重写。
(1) override :检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
它是用于检查,派生类是否对某个虚函数进行了重写,只需要在子类的重写函数后面加上 override。
父类:
virtual void buy_ticekt()
{
cout << "买的票,是全价" << endl;
}
子类:
virtual void buy_ticekt(int a=1) override
{
cout << "买的票,是半价" << endl;
}
很明显以上,并没有完成对父类成员函数的重写,但是我加上了关键字 override,来看看运行情况:
报错了:
(2) final :放在类后,表示该类不能被继承 / 放在虚函数后,表示该虚函数不能再被重写
所以引入了一个关键字: final
,只需要在类名后加上 final ,此类就不能被继承了。
class person final
{
}
如果还要继承此类,毫无疑问会报错:
基类:
virtual void buy_ticekt() final
{
cout << "买的票,是全价" << endl;
}
派生类:
virtual void buy_ticekt()
{
cout << "买的票,是半价" << endl;
}
基类的虚函数后已经加上了关键字:final,但是子类依旧对其重写,毫无疑问会报错:
抽象类:包括纯虚函数的类,纯虚函数:虚函数的后面加上 =0
,纯虚函数不需要定义,只声明就好了。这种类被称为抽象类。
抽象类有什么作用?它是一种接口类,不需要实例化出对象,它内部的纯虚函数,需要被其子类重写后才能有价值,否则没有意义。
比如:我定义一个抽象类->car,其中的纯虚函数是显示车的品牌。昂,车的品牌多了去了,一个抽象类car,能够显示出其车牌吗?所以只需要声明此函数,没必要实现。
#include
using namespace std;
class car
{
public:
virtual void Car_brand() = 0;
};
class BMW : public car
{
public:
virtual void Car_brand()
{
cout << "my_car_brand is BMW" << endl;
}
};
class Benz : public car
{
public:
virtual void Car_brand()
{
cout << "my_car_brand is Benz" << endl;
}
};
int main()
{
car* ptr1 = new BMW;
car* ptr2 = new Benz;
ptr1->Car_brand();
ptr2->Car_brand();
}
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的
继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所
以如果不实现多态,不要把函数定义成虚函数。
抽象类,更加体现了接口继承,它的内部直接搞一个纯虚函数,只有被子类重写的命,如果不被重写那么其毫无存在意义。
纯虚函数强制要求子类对其重写,如果不被重写,那么直接报错:因为不能实例抽象类
这一点有点像刚讲的override ,不过override是语法层面上的检查。上面那个是本质强烈要求。
通过上面的学习,我们认识了多态,并且简单的使用了多态,那么现在我们来讲讲多态的实现原理。上车了,同学们坐稳扶好。
先出一道迷惑的小题:
求:下面的类的大小
class base
{
public:
virtual void test()
{
cout << "how are you" << endl;
}
private:
int _a;
char _b;
int _c;
};
一看这题简单呀,类的成员函数在代码区,所以只看成员变量,利用学过的内存对齐知识,很轻松的得到答案:12字节。好的,我们用sizeof()来 验证一下吧:
答案是:16 字节。昂,怎么回事?通过调试,来看看base 对象里都有什么?
下面的三个成员变量:_a,_b,_c不用说,主要是那个_vfptr是什么?它就是虚函数表,存的是虚函数的地址,可以将其理解成一个函数指针数组,所以它的类型是 void ** 一个二级指针,内部存的是 void *。
重写又被称为覆盖,什么是覆盖?覆盖的是谁?如何覆盖?我们来通过虚函数表,来理解重写。
举例:我可以定义一个基类,派生类对基类中的一个虚函数进行重写
class base
{
public:
virtual void fun1()
{
cout << "how are you" << endl;
}
virtual void fun2()
{
cout << "i am fine" << endl;
}
virtual void fun3()
{
cout << "thanks" << endl;
}
protected:
int _a;
};
class derive : public base
{
public:
virtual void fun1()
{
cout << "are you ok" << endl;
}
};
int main()
{
base b;
derive d;
}
可以看到,基类总共有三个虚函数,派生类对第一个虚函数进行了重写。通过调试查看重写是怎么肥事?
派生类对fun1(),进行了重写,所以虚函数表的第一个函数指针的值是不同的,也就是说派生类并没有继承父类的fun1(),对此fun1()进行了覆盖,本质上,子类的fun1()已经是一个新的函数,子类的虚函数表的第一个函数指针不再指向从基类继承的函数位置,而是指向了子类重写的虚函数位置。
我们再来看虚函数表的下面俩个位置,发现基类和派生类指向的虚函数位置是一样的。
所以总结:派生类 拷贝 基类的虚函数表 -> 没有重写的虚函数指向同一函数地址,进行重写的虚函数,派生类会指向它重写的函数的地址。
讲到这里我提一个问题:虚函数存在哪里?虚函数表存在哪里?
虚函数和普通成员函数一样都存在代码段,只不过虚函数的指针会存在虚函数表中;虚函数表存在具体的对象中,每个有虚函数的对象都有虚函数表。
注意
:
int main()
{
derive d;
derive d1;
}
class derive : public base
{
public:
virtual void fun1()
{
cout << "are you ok" << endl;
}
virtual void fun4()
{
cout << "not ok" << endl;
}
};
我在派生类新增了一个虚函数fun4(),通过调试查看一下:
关谷神奇发现:没有显示的看到新增的虚函数在虚表中,阿尤头大了,到底存没存进去虚函数的地址,上面不是说过虚函数表中一定会有虚函数的地址吗?这是编译器在搞怪,其实是存进去了,通过内存可以看一下,虚函数表末尾存的是空指针嘛。
毫无疑问:红圈的地方就是我们新存的虚函数地址,我上面画到的青色框,内存从后往前读,可发现两个框的内容是一致的。用内存验证了,虚函数表存的虚函数的地址。
有了上面知识的铺垫,我们来正式说道多态的实现原理:多态就是不同类型的对象去做同一件事,拿到不同的结果。满足多态需要有俩个条件:基类的指针或者引用去调用虚函数,虚函数必须被派生类重写。为什么要满足这两个条件?
多态的实现靠的就是虚函数表,通过虚函数表找到,重写的虚函数,从而实现不同于基类的功能。
我们回到最开始举的例子->不同人买票,这样大家就更懂了:
class person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是全价" << endl;
}
};
class student : public person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是半价" << endl;
}
};
int main()
{
person common;
student A;
// 基类指针去调用
person* ptr1 = &common;
person* ptr2 = &A;
ptr1->buy_ticekt();
ptr2->buy_ticekt();
// 基类引用去调用
person& y1 = common;
person& y2 = A;
y1.buy_ticekt();
y2.buy_ticekt();
}
通过调试,来看看多态的实现过程:
(1) 重写后,也可以看到,重写的虚函数也显示的标明出了类域
(2) 不同的对象会通过虚表,从而调到不同的函数
因为是用的基类的指针或引用,所以派生类和基类的指针或引用都可以使用多态,编译器一看是基类的指针或引用,根本不管你是基类赋值的指针,还是派生类赋值的指针,我一视同仁,都只看基类继承下来的部分。
画图讲一下吧,干讲难理解:
ptr1指向的是基类对象,编译器管你是啥对象,你就是个person指针,你只能以person的视角来调用,要实现多态,我就去你指向的对象的虚函数表中找到对应的虚函数地址:
ptr2指向的派生类对象,编译器同样不管你是啥对象,依旧看出person指针,只能以person视角来调用,要实现多态,我就去你指向的对象的虚函数表中找到对应的虚函数地址:
然后有了函数地址,当然就会调用对应的函数
讲到这里,不知道大家看出来没,动态多态的实现是运行时,才完成的,它是运行时才完成的对应调用。
多继承的多态,啧啧,多继承我就有点头大了,里面的菱形继承更是头大,你还搞一个多继承的多态,啊,没关系,简单的讲讲就ok了,都得要稳稳的幸福,知识不能有太大的缺口。
我举一个例子:
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
base1中有func1(),base2中也有func1(),并且多继承的派生类对func1(),进行了重写,是对从哪个继承下来的fun1()进行的重写呢?派生类中没有进行重写的继承下来的虚函数如何存储呢?还有派生类中新生的虚函数又是如何存在那呢?
对base1以及base2中的func1()都进行了重写,而且重写后的虚函数是同一个。
看base1,base2中虚函数表,第一个位置就是我们重写后的func1(),发现地址尽然不同,但是它俩通过跳转最后是跳转到了同一个函数地址上,所以我们可以得到一个结论:虚函数表里存的不一定是虚函数的地址,它可能会存jmp跳转前的地址。
继承下来但没有进行重写的虚函数,会存放在继承下来对应的虚函数表中。
这个问题解决得比较简单。
派生类新增的虚函数会放在第一个继承的基类的虚函数表中,不过是最后一个虚函数指针罢了。这个我们可以通过内存来看,虚函数表的默认是null,内存就是00000,这个上面讲过。
对于多继承的多态,掌握这些也不少了,但如果,还是很好奇各种多继承的多态,我建议可以查阅相关的C++文献,要理解菱形继承的多态,不是一件容易的事。大家感兴趣可以自己再去研究,不过一般情况下用的少。
结尾语: 以上就是多态的相关知识,大家有问题可以评论或者私信,还有欢迎大佬来此斧正。