多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

多态:同一接口,万千形态

多态:同一接口,万千形态 一.多态的概念多态的概念通俗来说就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态)在这里我们重点来说运行时多态。对于编译时多态(静态多态)主要就是我们所说的函数重载和函数模板他们传不同类型的参数就可以调用不同的函数通过参数不同达到多种形态之所以叫编译时多态是因为他们实参传给形参的参数匹配是在编译时完成的我们把编译时⼀般归为静态运行时归为动态。运行时多态可以理解为在进行某一个动作时呈现的不同状态例如叫声狗与猫同时进行叫声这个动作狗是汪猫是喵进行同一个动作结果却不同.二.多态的定义及实现2.1 多态的构成条件多态是⼀个继承关系的下的类对象去调用同⼀函数产生了不同的⾏为。比如Student继承了 Person。Person对象买票全价Student对象优惠买票。2.2 实现多态还有两个必须重要条件必须为基类的指针或引用来调用虚函数被调用的函数必须是虚函数并且完成了虚函数的重写/覆盖.如下class Person { public: virtual void BuyTicket() { cout 买票全价 endl; } }; class Student:public Person { public: virtual void BuyTicket() { cout 买票半价 endl; } }; void Func(Person people) { people.BuyTicket(); } void test() { Person Mike; Func(Mike); Student Johnson; Func(Johnson); }在Func函数中我们发现ticket都是由p基类的引用来调用(满足第一个条件)在Person与Student中两个BuyTicket都是虚函数且完成了虚函数的重写(满足第二个条件)调用结果如下2.3虚函数类成员函数前⾯加virtual修饰那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修 饰如静态成员函数与友元函数class Person { public: virtual void BuyTicket() { cout 买票-全价 endl;} };2.4 虚函数的重写/覆盖虚函数的重写/覆盖派生类中有⼀个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称派生类的虚函数重写了基类的虚函数。在派生类中虚函数可以不加virtual基类中的virtual会被继承下来也会构成重写但这样的写法不规范不建议使用了解即可.class Animal { public: virtual void talk() const {} }; class Dog : public Animal { public: virtual void talk() const { std::cout 汪汪 std::endl; } }; class Cat : public Animal { public: virtual void talk() const { std::cout 喵喵 std::endl; } }; void letsHear(const Animal animal) { animal.talk(); } int main() { Cat cat; Dog dog; letsHear(cat); letsHear(dog); return 0; }可以通过该代码详细观察虚函数的重写与覆盖可以通过下面一道题来更好的理解多态的原理与进行.2.1.4 多态场景的⼀个选择题以下程序输出结果是什么A:A-0 B:B-1 C:A-1 D:B-0 E:编译出错 F:以上都不正确class A { public: virtual void func(int val 1){ std::coutA- val std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val 0){ std::coutB- val std::endl; } }; int main(int argc ,char* argv[]) { B*p new B; p-test(); return 0; }结果为B.首先我们需要知道虚函数的重写可以理解为virtual 基类的返回类型 函数名(基类的参数) { 派生类的函数内容 }因此缺省值是基类中的缺省值2.5 override和final关键字从上面可以看出C对虚函数重写的要求比较严格但是有些情况下由于疏忽比如函数名写错参数写错等导致无法构成重写而这种错误在编译期间是不会报出的只有在程序运性时没有得到预期结 果才来debug会得不偿失因此C11提供了override可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数那么可以用final去修饰.2.5.1 overrideclass Car { public: virtual void Dirve() {} }; class Benz :public Car { public: virtual void Drive() override { cout Benz-舒适 endl; } };例如该代码在派生类中虚函数为Dirve基类中为Drive因此两者不构成重写此时因为加上override编译就会报错.如下因此我们可以将override可以理解为一个检查虚函数是否重写成功的关键字.2.5.2 final作用一如果不想让一个成员函数重写我们便可以使用final来修饰该成员函数class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() { cout Benz-舒适 endl; } };当基类中的函数加上final后此成员函数则不可以被重写如果重写编译器则会报错作用二如果一个类我们不想让其向下继承我们可以在类名后加上finalclass Car final { public: virtual void Drive() {} }; class Benz :public Car { public: virtual void Drive() { cout Benz-舒适 endl; } };当基类中的类名后加上final后此类则不可以被继承如果继承编译器则会报错2.6 虚函数重写的⼀些其他问题2.6.1协变了解即可派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变.class Person { public: virtual Person* BuyTicket() { cout 买票全价 endl; return nullptr; } }; class Student:public Person { public: virtual Student* BuyTicket() { cout 买票半价 endl; return nullptr; } }; int main() { Person p; Student s; return 0; }此外除了自身的基类与派生类外其他类的派生类与基类也可如下class A { }; class B :public A { }; class Person { public: virtual A* BuyTicket() { cout 买票全价 endl; return nullptr; } }; class Student :public Person { public: virtual B* BuyTicket() { cout 买票半价 endl; return nullptr; } };如上也是正确的.2.6.2析构函数的重写基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同看起来不符合重写的规则实际上编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor所以基类的析构函数加了 vialtual修饰派⽣类的析构函数就构成重写为什么要构成多态看如下代码class A { public: virtual ~A() { cout ~A() endl; } }; class B : public A { public: ~B() { cout ~B()-delete: _p endl; delete _p; } protected: int* _p new int[10]; }; int main() { A* p new B; delete p; return 0; }如果不构成多态该段代码如果去A中析构则会出现内存泄漏的问题.三.纯虚函数和抽象类在虚函数后加上0则这个函数为纯虚函数,如下Drive()便是纯虚函数class Car { public: virtual void Drive() 0; };虚函数不需要定义实现(实现没啥意义因为要被派生类重写但是语法上可以实现)只要声明即可.因此包含纯虚函数的类叫做抽象类因此Car即为抽象类抽象类不能实例化出对象如果派生类继承后不重写纯虚函数那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数因为不重写实例化不出对象。演示抽象类不能实例化出对象:class Car { public: virtual void Drive() 0; }; int main() { //编译报错error C2259: “Car”: ⽆法实例化抽象类 Car c; return 0; }如果派生类继承后不重写纯虚函数那么派生类也是抽象类:class Car { public: virtual void Drive() 0; }; class Benz :public Car { public: }; int main() { Benz b; return 0; };四.多态的原理4.1虚函数表指针首先我们看下方代码class Base { public: virtual void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; class Base1 { public: void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; int main() { Base b; Base1 b1; return 0; }在Base中有虚函数在Base1中没有虚函数实例化对象b与b1成员不一样如下除了_b和_ch成员在b对象中还多⼀个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后⾯这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有⼀个虚函数表指针因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中虚函数表也简称虚表.4.2.多态是如何实现的class Person { public: virtual void BuyTicket() { cout 买票全价 endl; } private: string _name; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票半价 endl; } private: string _id; }; void Func(Person people) { people.BuyTicket(); } int main() { Person p; Student s; Func(p); Func(s); return 0; }对于对象p与s都会存在一个虚表指针详细解释如下可以简单理解为有虚函数的基类会产生一个虚表指针存储虚函数如果子类对该虚函数的指针进行了重写则会覆盖该虚函数的指针因此在调用时产生了不同的效果此外每一个类无论实例化出多少个对象都共用一个虚表指针.int main() { Person p; Person b; return 0; }我们可以看到虚表指针的地址是相同的.为什么不可以通过子类对象来直接拷贝给父类对象从而触发多态一个类的虚表在创建时已经被编译器固定了拷贝实际就是将子类中的数值给父类让其复制一份在内存中重新生成一个新的对象且该对象为父类对象虚表依旧为该对象固定的虚表.基类对子类进行引用与指针指针指向了整个子类对象本体但只能使用父类对象中的内容并没有重新开辟空间对象的 vptr 仍是子类的调用虚函数时走子类的 vtable因此可以使同一个函数出现不同的结果。4.3动态绑定与静态绑定对不满足多态条件(指针或者引⽤调⽤虚函数)的函数调用是在编译时绑定也就是编译时确定调用函数的地址叫做静态绑定.(即普通成员函数的调用)ptr-BuyTicket(); 00EA2C91 mov ecx,dword ptr [ptr] 00EA2C94 call Student::Student (0EA153Ch)例如上方是在汇编下调用普通函数如何调用编译器直接确定调用函数地址.满⾜多态条件的函数调用是在运行时绑定也就是在运行时到指向对象的虚函数表中找到调用函数的地址也就做动态绑定.(即为多态的调用)ptr-BuyTicket(); 00EF2001 mov eax,dword ptr [ptr] 00EF2004 mov edx,dword ptr [eax] 00EF2006 mov esi,esp 00EF2008 mov ecx,dword ptr [ptr] 00EF200B mov eax,dword ptr [edx] 00EF200D call eax4.4虚函数表基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同⼀张虚表不同类型的对象各自有独立的虚表所以基类和派生类有各自独立的虚表.class Person { public: virtual void BuyTicket() { cout 买票全价 endl; } virtual void func() { cout virtual void func() endl; } private: string _name; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票半价 endl; } virtual void func() { cout virtual func() endl; } private: string _id; }; int main() { Person p; Person e; Student s; Student t; return 0; }此时基类中有两个虚函数因此虚表指针中也会有两个虚函数的地址.通过下方两张图我们可以看出对于同种类型的对象共用一张表不同类型的对象有各自的虚表派生类由两部分构成继承下来的基类和自己的成员⼀般情况下继承下来的基类中有虚函数表指针自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个就像基类对象的成员和派生类对象中的基类对象成员也独立的.派生类中重写的基类的虚函数派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址class Person { public: virtual void BuyTicket() { cout 买票全价 endl; } virtual void func() { cout virtual void func() endl; } }; class Student :public Person { public: virtual void BuyTicket() { cout 买票半价 endl; } }; int main() { Person p; Student s; return 0; }通过对比在第一个虚函数重写的情况下虚表第一个位置的地址被覆盖换成新的地址而未重写的则继续为基类的地址虚函数表本质是⼀个存虚函数指针的指针数组一般情况这个数组最后面放了⼀0x00000000标记。(这个C并没有进行规定各个编译器自行定义的vs系列编译器会再后面放个0x00000000标记g系列编译不会放)在32位的情况下一个指针占4个字节.虚函数存在哪的虚函数和普通函数⼀样的编译好后是⼀段指令都是存在代码段的只是虚函数的地址又存到了虚表中.虚函数表存在哪的这个问题严格说并没有标准答案C标准并没有规定我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)
返回列表