【C++第十六章】多态
定义🧐
多态通俗来说,就是多种形态,我们去完成某个行为时,不同对象完成时会产生出不同的状态。比如游乐园中,1.2米以上买票就需要买成人票,1.2米以下就可以买儿童票。
多态是在不同继承关系的类对象,去调用同一函数,产生不同行为。我们通过下面代码来学习:
#include <iostream> using namespace std;class Person { public:virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public:virtual void BuyTicket() { cout << "买票-半价" << endl; } }; //多态条件 //1.虚函数重写 //2.父类的指针或者引用去调用虚函数//虚函数重写要求 //父子继承关系的两个虚函数,要求同函数名、参数、返回//virtual只能修饰成员 //三同的例外:协变->返回类型可以不同,但必须是父子类关系的指针或者引用 //派生类重写的虚函数可以不加virtual void Func(Person& p) {p.BuyTicket(); } int main() {Person ps;Student st;Func(ps);Func(st);return 0; }
student类继承了person类,并拥有同一函数BuyTicket,此时我们使用**父类的指针或者引用去调用该函数**就可以形成多态。
总结一下,多态形成的条件为:1.虚函数的重写(由于一些特性,子类虚重写的虚函数可以不加virtual) 2.需要父类指针或者引用去调用该函数。而虚函数重写需要满足三同——函数名、参数、返回类型(除协变)都要相同。普通函数的继承是实现继承,虚函数的继承是接口继承。
协变🔎
协变是三同的例外,协变的返回类型可以不同,但必须是父子类关系的指针或者引用。
子类virtual🔎
子类重写的虚函数可以不加virtual,但父类必须加上virtual
原因在于,父类指针只会调用父类析构,但是我们可能将父类指针指向子类,而delete由两部分构成——destructor(析构的统一处理,继承章节提到过)和operator delete,析构函数的名称由于多态被统一处理了,所以delete时会先调用析构再调用operator delete,在该代码中,我们想要p指向谁就调用谁的析构,此时就需要用到虚函数,而person和student满足父子关系,也有统一函数名destructor,此时只缺少virtual关键字,所以我们在析构加上virtual就可以变为多态,实现指向谁析构谁。
不过在设计时,为了不让我们忘记给子类加上virtual而导致内存泄漏,所以统一设计,即使子类不写virtual也可以重写。
#include <iostream> using namespace std;class Person { public:~Person(){cout << "~Person()" << endl;} }; class Student : public Person { public:~Student(){cout << "~Student()" << endl;} };int main() {Person* p = new Person;delete p;p = new Student;delete p;return 0; }
重载、重写(覆盖)、重定义(隐藏)🔎
小试牛刀🔎
B类创建了一个指针,该指针指向test函数,而test是A类的成员,所以test的参数为A* this,内部为this->fucn(),而this是B,B与A是父子关系,满足虚函数重写,所以是多态调用,但虚函数重写是父类的实现,用的是父类的接口,所以val还是父类的值,则选B。
final和override🧐
final用于修饰虚函数,被修饰的虚函数不能被重写。
override修饰派生类的虚函数,可以检查是否完成重写,没有重写则会报错。
抽象类🧐
在虚函数后面加上"=0",则这个函数为纯虚函数,含有纯虚函数的类叫做抽象类(接口类),抽象类不能实例出对象,派生类继承后必须要重写纯虚函数,才能实例化对象。
纯虚函数规范了抽象类必须要重写,也体现出接口继承。抽象类一般用于不需要实例化的对象,比如person类,我们没有赋予属性前这个类就可以看做是抽象的,当我们继承person类后,重写它的各种属性(身高年龄职业等),让其变为一个具体的对象再进行实例化。
虚函数表🧐
C++会把虚函数存到虚函数表(_vfptr)中,所以会多开一个指针指向该表(本篇博客代码在32位环境下运行),虚函数编译后也存在代码段中,只不过会把虚函数的地址单独拿出来放在表中。
虚函数的重写,也叫虚函数的覆盖,虚函数表也会进行覆盖。所以当我们传子类对象时,实际上是父类对子类的切片,通过子类虚函数表找到虚函数地址。
#include <iostream> using namespace std; class Person { public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void func(){}private:int a = 0; };class Student : public Person { public:virtual void BuyTicket() { cout << "买票-半价" << endl; }private:int b = 1; };void Func(Person ptr) {ptr.BuyTicket(); }int main() {Person p;Student s;Func(p);Func(s);return 0; }
那么为什么可以用指针和引用,而不能用对象呢?首先引用的底层也是指针,所以它俩都是能指向子类对象中切割出来的父类。用对象也是子类切割出来的父类,成员拷贝给父类,但是不会拷贝虚函数表的指针,原因在于当出现父类=子类的情况时,不一定能调用到父类的虚函数。
如果父类写了虚函数,子类没写虚函数,虚函数表的内容一样,但是存储在不同的位置,因为多开一个虚函数表不会耗费太多资源,从安全性考虑新开一个表更为保险。
同一个类可以共用一张虚表。
我们将虚表地址打印出来,发现虚表更靠近代码段,所以我们认为虚表是存在代码段中的。
并且,所有的虚函数一定会被放进类的虚函数表中,我们以下面代码来说明:
#include <iostream> using namespace std;class Base { public:virtual void func1(){cout << "Base::func1" << endl;}virtual void func2(){cout << "Base::func1" << endl;} private:int _a; };class Derive : public Base { public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}virtual void func4(){cout << "Derive::func4" << endl;}void func5(){cout << "Derive::func5" << endl;} private:int _b; }; class X : public Derive { public:virtual void func3(){cout << "X::func3" << endl;} }; //打印虚函数 typedef void (*VFUNC)(); void PrintVFT(VFUNC* a) {for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];(*f)();}printf("\n"); }int main() {Base b;PrintVFT((VFUNC*)(*((int*)&b)));Derive d;PrintVFT((VFUNC*)(*((int*)&d)));X x;PrintVFT((VFUNC*)(*((int*)&x)));return 0; }
我们用监视窗口发现只有两个虚函数。
但实际上用函数指针数组打印出虚函数地址,发现实际有四个,不过监视窗口给我们隐藏了。
我们打印出虚函数地址的原理为,先取到Derive对象的地址,然后强转成int*只取前4个字节,也就是取到虚函数表,解引用拿到虚函数的地址,最后强转一下传过去。
在多继承下,会有多个虚表存在。
#include <iostream> using namespace std;class Base1 { public:virtual void func1(){cout << "Base1::func1" << endl;}virtual void func2(){cout << "Base1::func2" << endl;} private:int _a1; };class Base2 { public:virtual void func1(){cout << "Base2::func1" << endl;}virtual void func2(){cout << "Base2::func2" << endl;} private:int _a2; };class Derive : public Base1, public Base2 { public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}private:int _b; }; typedef void (*VFUNC)(); void PrintVFT(VFUNC* a) {for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];(*f)();}printf("\n"); }int main() {Derive d;PrintVFT((VFUNC*)(*((int*)&d)));Base2* ptr = &d; //自动切片PrintVFT((VFUNC*)*(int*)ptr);return 0; }
我们打印发现,重写的两个fun1地址不一样。
我们用下面代码讲解:
#include <iostream> using namespace std;class Base1 { public:virtual void func1(){cout << "Base1::func1" << endl;}virtual void func2(){cout << "Base1::func2" << endl;} private:int _a1; };class Base2 { public:virtual void func1(){cout << "Base2::func1" << endl;}virtual void func2(){cout << "Base2::func2" << endl;} private:int _a2; };class Derive : public Base1, public Base2 { public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}private:int _b; }; typedef void (*VFUNC)(); void PrintVFT(VFUNC* a) {for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];(*f)();}printf("\n"); }int main() {Derive d;Base1* p1 = &d;p1->func1();Base1* p2 = &d;p2->func1();return 0; }
我们从p1的汇编指令来看,首先p1所call的不是真正的地址,而是call到jmp,再由jmp跳到真正的地址,开始建立fun1的栈帧,而p2发现call和jmp以及func1的地址也不一样,并且会进行多段跳。
原因在于func1所接受的是Derive* this,而this应该能够访问整个对象,所以我们需要修正p2让它指向Derive,如图2的ecx-8就是在修正p2,让其指向Derive对象,而p1恰好与this重叠,所以没有多段跳。
结尾👍
以上便是多态的全部内容,如果有疑问或者建议都可以私信笔者交流,大家互相学习,互相进步!🌹