02.多态1. 多态的概念多态(polymorphism)的概念通俗来说就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)这⾥我们重点讲运⾏时多态编译时多态(静态多态)和运⾏时多态(动态多态)。编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板他们传不同类型的参数就可以调⽤不同的函数通过参数不同达到多种形态之所以叫编译时多态是因为他们实参传给形参的参数匹配是在编译时完成的我们把编译时⼀般归为静态运⾏时归为动态。运⾏时多态具体点就是去完成某个⾏为(函数)可以传不同的对象就会完成不同的⾏为就达到多种形态。⽐如买票这个⾏为当普通⼈买票时是全价买票学⽣买票时是优惠买票(5折或75折)军⼈买票时是优先买票。再⽐如同样是动物叫的⼀个⾏为(函数)传猫对象过去就是”(^ω^)喵“传狗对象过去就是汪汪。多态就和我们买票时的情形相似具有多种选择2. 多态的定义及实现2.1 多态的构成条件多态是⼀个继承关系的下的类对象去调⽤同⼀函数产⽣了不同的⾏为。⽐如Student继承了Person。Person对象买票全价Student对象优惠买票。2.1.1 实现多态还有两个必须重要条件• 必须是基类的指针或者引⽤调⽤虚函数• 被调⽤的函数必须是虚函数并且完成了虚函数重写/覆盖。说明要实现多态效果第⼀必须是基类的指针或引⽤因为只有基类的指针或引⽤才能既指向基类对象⼜指向派⽣类对象第⼆派⽣类必须对基类的虚函数完成重写/覆盖重写或者覆盖了基类和派⽣类之间才能有不同的函数多态的不同形态效果才能达到。- 指针 / 引用本身**不代表类型是基类对象实体**它只是 “一个标签可以挂靠不同真实对象”- 普通对象是**一块实实在在独立内存**定义是什么类型这块内存就固定是什么类型。这里就是典型的用基类的引用完成的一个多态编译时产生的是静态多态。2.1.2 虚函数类成员函数前⾯加virtual修饰那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。class Person 2 { 3 public: 4 virtual void BuyTicket() { cout 买票-全价 endl;} 5 };2.1.3 虚函数的重写/覆盖虚函数的重写/覆盖派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称派⽣类的虚函数重写了基类的虚函数。注意在重写基类虚函数时派⽣类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性)但是该种写法不是很规范不建议这样使⽤不过在考试选择题中经常会故意买这个坑让你判断是否构成多态。class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout 买票-打折 endl; } }; void Func(Person* ptr) { // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // 但是跟ptr没关系⽽是由ptr指向的对象决定的。 ptr-BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }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; }1. B 类没有重写test()所以调用继承来的 A::test()2. 进入 A::test()里面是 this-func();- this 在 A::test() 内部**静态类型是 A***编译器在编译这个函数的时候就认定 this 是 A*- 运行时this 实际指向 B 对象 → 查虚表选中 B::func多态生效调用 B 版本函数3. 编译期处理func()不传参编译器看 this 的静态类型 A*去找 A 版本 func 的默认参数 → val14. 所以实际调用B::func(1)✅ 输出B-1情况二A* p new B;最经典对比1. p 的**静态类型A***p 指向的真实对象B2. p-func()func 是虚函数 → 运行时查虚表调用 B::func3. 编译期p 的静态类型是 A*不传参取 A::func 默认参数 val14. 实际调用B::func(1)✅ 输出B-1情况三B* p new B; p-func();1. p 静态类型B*真实对象 B2. p-func()虚函数运行时调用 B::func3. 编译期p 是 B*编译器拿 B 类 func 的默认参数 val04. 实际调用B::func(0)输出B-0这就是差别来源这里直接用 B*调用 func静态类型是 B*默认参数取 B 的 0而前面两种编译时识别静态类型是 A*默认参数拿 A 的 1。再补充一个反向例子帮你巩固A* p new A; p-func();静态类型 A*对象也是 A调用 A::func参数 1 → A-1不管在哪里默认参数都是在编译器就确定的只不过在多态会产生问题 默认参数**永远都是编译期在调用点确定、由编译器补实参**这条规则对所有函数都生效 只有搭配多态虚函数动态绑定的时候两套规则不一致才会出现反直觉的 bug。拆开两层再巩固1. 通用规则全局成立跟 virtual 无关只要写 func() 不传参编译器就看**这一行调用处的指针 / 变量静态类型**把对应的默认值直接塞进去变成 func(xxx)。这件事编译阶段就做完了运行时根本不知道 “默认参数” 这回事。2. 多态场景下的冲突来源- 虚函数**运行时**看对象真实类型决定调用哪个函数- 默认参数**编译时**看调用点静态类型决定传入数值。两个决策时机不一样就会出现**调用的是子类虚函数但传进去的是基类的默认值**。如果不是虚函数静态绑定函数选哪个、默认参数取哪个全都看静态类型两者是同步的你就感受不到异常。举个极简对照A* p new B; p-func();- func 不是虚函数编译期直接选 A::func A 的默认值 1 → 行为统一不奇怪- func 是虚函数运行期选 B::func编译期填 A 的默认值 1 → 割裂就是那个经典坑2.1.5 虚函数重写的⼀些其他问题协变(了解)派⽣类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤派⽣类虚函数返回派⽣类对象的指针或者引⽤时称为协变。协变的实际意义并不⼤所以我们了解⼀下即可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; } }; void Func(Person* ptr) { ptr-BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0;析构函数的重写基类的析构函数为虚函数此时派⽣类析构函数只要定义⽆论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则实际上编译器对析构函数的名称做了特殊处理编译后析构函数的名称统⼀处理成destructor所以基类的析构函数加了vialtual修饰派⽣类的析构函数就构成重写。下⾯的代码我们可以看到如果~A()不加virtual那么delete p2时只调⽤的A的析构函数没有调⽤B的析构函数就会导致内存泄漏问题因为~B()中在释放资源。注意这个问题⾯试中经常考察⼤家⼀定要结合类似下⾯的样例才能讲清楚为什么基类中的析构函数建议设计为虚函数。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]; }; // 只有派⽣类Student的析构函数重写了Person的析构函数下⾯的delete对象调⽤析构函数才能 构成多态才能保证p1和p2指向的对象正确的调⽤析构函数。 int main() { A* p1 new A; A* p2 new B; delete p1; delete p2; return 0; }~A() ~B()-delete:0x... ~A()这个试运行的结果## delete p1; p1 指向 A 对象- p1 是 A*指向 A 对象- 虚函数查找调用 ~A()输出~A()## 2. delete p2; p2 是 A*指向 B 对象核心多态场景1. 因为 ~A() 是虚析构触发多态运行时识别真实类型是 B2. 先执行 ~B()打印 ~B()-delete:地址释放 _p 指向的堆内存3. ** 派生类析构结束之后编译器自动调用基类的析构A ()**这一步是编译器强制插入不是手动写的 所以接着打印~A()2.1.6 override 和 final关键字从上⾯可以看出C对虚函数重写的要求⽐较严格但是有些情况下由于疏忽⽐如函数名写错参数写错等导致⽆法构成重写⽽这种错误在编译期间是不会报出的只有在程序运⾏时没有得到预期结果才来debug会得不偿失因此C11提供了override可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数那么可以⽤final去修饰。// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法 class Car { public: virtual void Dirve() {} }; class Benz :public Car { public: virtual void Drive() override { cout Benz-舒适 endl; } }; int main() { return 0; } // error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写 class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() { cout Benz-舒适 endl; } }; int main() { return 0;这是两个可以让编译器进行检查的关键字2.1.7 重载/重写/隐藏的对⽐注意这个概念对⽐经常考⼤家得理解记忆⼀下3. 纯虚函数和抽象类在虚函数的后⾯写上 0 则这个函数为纯虚函数纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写但是语法上可以实现)只要声明即可。包含纯虚函数的类叫做抽象类抽象类不能实例化出对象如果派⽣类继承后不重写纯虚函数那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数因为不重写实例化不出对象class Car { public: virtual void Drive() 0; }; class Benz :public Car { public: virtual void Drive() { cout Benz-舒适 endl; } }; class BMW :public Car { public: virtual void Drive() { cout BMW-操控 endl; } }; int main() { // 编译报错error C2259: “Car”: ⽆法实例化抽象类 Car car; Car* pBenz new Benz; pBenz-Drive(); Car* pBMW new BMW; pBMW-Drive(); return 0;纯虚函数在实践上面没有实际的意义只是强制派生类重写函数4. 多态的原理4.1 虚函数表指针下⾯编译为32位程序的运⾏结果是什么A. 编译报错 B. 运⾏报错 C. 8 D. 12class Base { public: virtual void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; int main() { 比特就业课 Base b; cout sizeof(b) endl; return 0;}上⾯题⽬运⾏结果12bytes除了_b和_ch成员还多⼀个__vfptr放在对象的前⾯(注意有些平台可能会放到对象的最后⾯这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中虚函数表也简称虚表。这是我们监视窗口下所看到的成员组成还给虚函数表指针开了一份空间4.2 多态的原理4.2.1 多态是如何实现的从底层的⻆度Func函数中ptr-BuyTicket()是如何作为ptr指向Person对象调⽤Person::BuyTicketptr指向Student对象调⽤Student::BuyTicket的呢通过下图我们可以看到满⾜多态条件后底层不再是编译时通过调⽤对象确定函数的地址⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址这样就实现了指针或引⽤指向基类就调⽤基类的虚函数指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图ptr指向的Person对象调⽤的是Person的虚函数第⼆张图ptr指向的Student对象调⽤的是Student的虚函数。这里可以看出我们调用所构成的虚函数时ptr的类型没有太大关系和他所指向的类型是有关系的class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } private: string _name; }; class Student : public Person { public: virtual void BuyTicket() { cout 买票-打折 endl; } private: string _id; }; class Soldier: public Person { public: virtual void BuyTicket() { cout 买票-优先 endl; } private: string _codename; }; void Func(Person* ptr) { // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // 但是跟ptr没关系⽽是由ptr指向的对象决定的。 ptr-BuyTicket(); } int main() { // 其次多态不仅仅发⽣在派⽣类对象之间多个派⽣类继承基类重写虚函数后 // 多态也会发⽣在多个派⽣类之间。 Person ps; Student st; Soldier sr; Func(ps); Func(st); Func(sr); return 0;4.2.2 动态绑定与静态绑定• 对不满⾜多态条件(指针或者引⽤调⽤虚函数)的函数调⽤是在编译时绑定也就是编译时确定调⽤函数的地址叫做静态绑定。• 满⾜多态条件的函数调⽤是在运⾏时绑定也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址也就做动态绑定。// ptr是指针BuyTicket是虚函数满⾜多态条件。 // 这⾥就是动态绑定编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址 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 eax // BuyTicket不是虚函数不满⾜多态条件。 // 这⾥就是静态绑定编译器直接确定调⽤函数地址 ptr-BuyTicket(); 00EA2C91 mov ecx,dword ptr [ptr] 00EA2C94 call Student::Student (0EA153Ch) 1静态绑定是在编译期间就确定的动态绑定是在运行期间通过指向的对象来确定调用的是哪个函数4.2.3 虚函数表基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表不同类型的对象各⾃有独⽴的虚表所以基类和派⽣类有各⾃独⽴的虚表。比特就业课• 派⽣类由两部分构成继承下来的基类和⾃⼰的成员⼀般情况下继承下来的基类中有虚函数表指针⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。• 派⽣类中重写的基类的虚函数派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。• 派⽣类的虚函数表中包含(1)基类的虚函数地址(2)派⽣类重写的虚函数地址完成覆盖派⽣类⾃⼰的虚函数地址三个部分。• 虚函数表本质是⼀个存虚函数指针的指针数组⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C并没有进⾏规定各个编译器⾃⾏定义的vs系列编译器会再后⾯放个0x00000000标记g系列编译不会放)• 虚函数存在哪的虚函数和普通函数⼀样的编译好后是⼀段指令都是存在代码段的只是虚函数的地址⼜存到了虚表中。• 虚函数表存在哪的这个问题严格说并没有标准答案C标准并没有规定我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)class Base { public: virtual void func1() { cout Base::func1 endl; } virtual void func2() { cout Base::func2 endl; } void func5() { cout Base::func5 endl; } protected: int a 1; }; class Derive : public Base { public: // 重写基类的func1 virtual void func1() { cout Derive::func1 endl; } virtual void func3() { cout Derive::func1 endl; } void func4() { cout Derive::func4 endl; } protected: int b 2; }; int main() { Base b; Derive d; return 0; } int main() { int i 0; static int j 1; int* p1 new int; const char* p2 xxxxxxxx; printf(栈:%p\n, i); printf(静态区:%p\n, j); printf(堆:%p\n, p1); printf(常量区:%p\n, p2); Base b; Derive d; Base* p3 b; Derive* p4 d; printf(Person虚表地址:%p\n, *(int*)p3); printf(Student虚表地址:%p\n, *(int*)p4); printf(虚函数地址:%p\n, Base::func1); printf(普通函数地址:%p\n, Base::func5); return 0; } 运⾏结果 栈:010FF954 静态区:0071D000 堆:0126D740 常量区:0071ABA4 Person虚表地址:0071AB44 Student虚表地址:0071AB84 虚函数地址:00711488 普通函数地址:007114BF 31. 虚表是**属于类**不是对象同类所有对象共享一张虚表每个对象自带 vfptr虚指针2. 派生类重写虚函数虚表对应位置覆盖成自己函数地址新增虚函数追加到虚表后面3. 普通成员函数**不进虚表**全部在代码段静态绑定4. 内存地址区间区分- 局部变量 i栈- static j静态区- new 出来堆- 字符串字面量常量区静态区的只读部分- 函数不管虚 / 普通代码段- 虚表静态只读数据区5. *(int*)p3把 Base 指针 p3 强制转成 int*解引用拿到对象开头的 vfptr也就是虚表首地址就是代码打印虚表地址的原理