浮世尘弦个人主页⭐个人专栏《C语言》、《数据结构与算法》、《C》非淡薄无以明志非宁静无以致远前言学完继承很多人会觉得自己已经摸到了 C 面向对象的门槛。直到遇见多态。多态值得单独写一篇不只是因为它被面试反复提起更因为它是继承和虚函数真正活过来的地方。在这里你会撞见一些看上去自相矛盾的现象同样是virtual写进构造函数就失去意义基类析构函数少写一个virtual程序可能在退出时悄悄出错还有三个长得几乎一样的词——重载、重写、隐藏——它们决定了那次调用究竟落到谁身上。这篇会从虚函数讲起顺着重写规则、多态成立的两个条件、析构函数的重写一路走到纯虚函数和抽象类最后一章把视角沉进内存看看虚函数表指针与虚函数表做了什么。我想让你读完能自己回答一个问题在编译器沉默的那段时间里它究竟把什么决定留给了运行期。让我们开始今天的学习吧目录前言一多态的定义和实现1认识虚函数2虚函数的重写/覆盖3多态(动态)的两个条件⭐4多态练习题易错5,协变了解二析构函数的重写⭐1重写析构函数2override和final关键字3重载重写隐藏的对比三纯虚函数和抽象类四多态的原理⭐1,虚函数表指针2多态的原理3动态绑定和静态绑定4虚函数表结尾一多态的定义和实现1认识虚函数在认识多态前我们先要知道虚函数以及虚函数重写。虚函数类成员函数前面加virtual修饰那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。核心功能实现运行时多态。声明虚函数和虚继承使用的同样的关键字但是两者没有什么关系。2虚函数的重写/覆盖虚函数的重写/覆盖派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表类型和个数完全相同(三同))称派⽣类的虚函数重写了基类的虚函数。注意点只要基类写了virtual即使子类不写也是虚函数。一般建议写上3多态(动态)的两个条件⭐先简单了解一下多态的概念多态就是一个继承关系下的类对象调用同一个接口函数名在对象上表现出不同的行为。通俗来说就是“一个接口多种实现”。多态分为两大类编译时多态(静态多态)和运行时多态(动态多态。编译时多态(静态多态)就是函数模版和函数重载。特点在编译期间就已经确定了具体调用哪个函数。效率极高因为地址在编译期就绑定好了静态绑定运行时多态(动态多态)就是虚函数继承特点在运行期间根据指针/引用所指向的实际对象类型来决定调用哪个类的函数动态绑定。效率比静态多态稍慢因为需要在运行时查表虚函数表接下来结合一个代码样例来了解一下多态吧class Person { public: virtual void BuyTicket() { cout 旅游-全价 endl; } }; class Student :public Person { public: void BuyTicket()//子类不加由于继承也是虚函数 { cout 旅游-半价 endl; } }; //这里使用Person是因为只有父类指针才能同时接收父类和子类 void Func(Person ptr) { ptr.BuyTicket();//实际调用时和类型无关和ptr指向的对象有关 } int main() { Person p1; Student s1; Func(p1); Func(s1); return 0; }运行结果上面一个继承关系下的类对象子类继承父类的虚函数。通过父类对象的指针调用了不同对象中的虚函数。由此我们引出构成多态的两个条件1,必须是基类的指针或者引用调用了虚函数。原因只有基类的指针和引用才能即指向基类对象又指向派生类对象。2派生类必须完成了对父类虚函数的重写。原因只有完成了重写才能有不同的函数形态用来进行调用。4多态练习题易错下面这段代码的运行结果是什么A-0 B: B-1 C: A-1 D: B-0 E: 编译出错 F: 以上都不正确分析这里B完成了对test()函数的继承但是没有完成对其的重写。B中实际继承的A::test()因此p-test会去调用父类的test()在里面发现func()函数就根据this的类型A*去查找在A中找到了func()。发现func()实际是一个虚函数实际的对象是B。this-func()就构成了多态。去B中找到func()进行调用。最后一个坑子类虚函数的默认参数取决于“调用时指针/引用的静态类型”。这里编译器在编译A::test()时只知道A::func的默认参数是1。所以不会用到下面的参数。最终打印的结果就是B-1直接进行调用时B::func()或者p-func()这种场景下才会使用val 0,打印出B-0C 标准委员会早就发现了这个坑所以极力推荐绝对不要在子类重写虚函数时重新定义默认参数结果B:B-15,协变了解派⽣类重写基类的虚函数时与基类的虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或者引用不一定是自己的派⽣类虚函数返回派⽣类对象的指针或者引用时称为协变。协变也是多态。////协变 class A{}; class B : public A {}; class Person { public: //virtual A* BuyTicket() //可以不是自己的 virtual Person* BuyTicket() { cout 旅游-原价 endl; return nullptr; } }; class Student : public Person { public: //virtual B* BuyTicket() virtual Student* BuyTicket() { cout 旅游-半价 endl; return nullptr; } }; void Func2(Person* p1) { p1-BuyTicket(); } int main() { Person p1; Student s1; Func2(p1); Func2(s1); return 0; }运行结果二析构函数的重写⭐1重写析构函数只要基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写。虽然看起来名字不同不能构成重写但是实际上编译器对析构函数的名称做了特殊处理编译后析构函数的名称统⼀处理成destructor//析构函数的重写 class A { public: virtual ~A() { cout ~A() endl; } }; class B : public A { public: //名字被处理成destructor,构成隐藏 ~B() //不加virtual,名字不同也构成虚函数重写 { cout ~B() p1 endl;//子类会自动调用父类的析构 delete p1; } protected: int* p1 new int[10]; }; int main() { A* p1 new A; A* p2 new B; delete p1; //p2-dectructor()operator delete //先调用子类的析构后调用父类的析构 delete p2; return 0; }运行结果为什么基类中的析构函数建议设计为虚函数因为要构成多态如果基类指针指向子类对象如p2当delete p2时如果基类析构不是虚函数就只会调用基类的析构导致子类申请的额外资源无法被释放从而造成内存泄漏。所以这里析构的名字被处理成了destrctor构成了多态关系 同时基类不加virtual两者构成了隐藏关系。2override和final关键字override关键字上面的虚函数重写要三个相同如果写错了就无法构成重写这种错误无法在编译期间报错。在运行时得不到想要的结果时才会进行更正。因此C11提供了override关键字可以帮助我们检查是否构成了重写。final关键字如果不想让子类重写基类的虚函数可以在基类当中的虚函数后加上一个final演示一下3重载重写隐藏的对比三纯虚函数和抽象类纯虚函数的定义在虚函数的声明后面写上 0这个函数就成为了纯虚函数。纯虚函数的实现纯虚函数通常不需要定义实现因为它的意义在于被派生类重写父类给出实现通常没有实际意义只要声明即可。不过C 语法上是允许纯虚函数拥有实现的。抽象类的定义包含纯虚函数的类被称为抽象类。抽象类的限制抽象类不能实例化出对象。派生类的约束如果一个派生类继承自抽象类但没有重写父类的纯虚函数那么这个派生类依然也是抽象类依然无法实例化。设计意义纯虚函数某种程度上强制了派生类必须重写该虚函数因为不重写就无法实例化出对象。代码演示//纯虚函数和抽象类 class fruit //抽象类 { public: virtual void Diver() 0; //纯虚函数声明即可 }; class apple : public fruit { public: virtual void Diver()//重写纯虚函数不属于抽象类 { cout apple-sweet endl; } }; class orange : public fruit { public: virtual void Diver() { cout orange-sour endl; } }; int main() { //父类不能实例化出对象但是可以使用父类的指针和引用 fruit* a1 new apple; fruit* o1 new orange; a1-Diver(); o1-Diver(); return 0; }运行结果四多态的原理✨1,虚函数表指针先来看一个例题下面这段代码在32位下的运行结果是什么A8 B12 C编译报错 D运行报错分析运行结果为12字节明明只有两个变量应该为 8个字节才对这是为什么呢打开监视窗口就可以发现端倪这里的b有三个部分构成除去两个成员新增加了一个_vfptr放在对象的前面这是一个指针32位下刚好是4个字节因此结果为12字节称为虚函数表指针。一个含有虚函数的类中都至少含有一个虚函数表指针。因为一个类当中的虚函数的地址都要放到一个函数指针数组当中这个函数指针数组就是虚函数表虚函数表又可以称为虚表。结果B2多态的原理下面结合一组案例来理解多态的原理class Person { public: virtual void BuyTicket() { cout 旅游-全价 endl; } string _name; }; class Student :public Person { public: virtual void BuyTicket()//子类不加由于继承也是虚函数 { cout 旅游-半价 endl; } int id; }; class OldMan : public Person { public: virtual void BuyTicket() { cout 旅游-优先 endl; } int age; }; void Func3(Person* p1) { p1-BuyTicket(); } int main() { Person* p new Person; Person* s new Student; Person* o new OldMan; Func3(p); Func3(s); Func3(o); return 0; }从底层调用角度Func3()函数中的p1-BuyTicket()是如何作为p1指向Person对象调用Person::BuyTicket()?p1是如何调用Student::BuyTicket的呢可以通过下图得知满足多态条件后编译器不再是编译时通过调用对象确定函数地址而是运行时去调用对象的虚表中确定虚函数的地址。这样就实现了指针或者引用指向基类的对象就调用基类的虚函数指向派生类就调用派生类的虚函数。3动态绑定和静态绑定静态绑定不满足多态条件的函数调用是在编译时绑定的也就是编译期确定调用函数的地址动态绑定满足多态条件的函数调用是在运行时绑定的也就是在运行时到指向对象的虚函数表中确定调用函数的地址下面从汇编层面了解一下两种绑定的差异从上图可知静态绑定只需要少量指令运行速度比动态绑定要快的多。4虚函数表1基类对象的虚函数表中存着基类所有虚函数的地址。同类型的对象共用一张虚表不同类型的对象各用各的虚表。基类和子类各用各的虚表。代码测试class Base { public: virtual void Func1() { cout Func1 endl; } virtual void Func2() { cout Func2 endl; } void Func3() { cout Func3 endl; } protected: int a 1; }; int main() { Base b1; Base b2; return 0; }监视窗口2派生类有两部分构成继承父类的部分和自己成员部分。一般只要父类当中有虚表指针那么子类继承下来的就不需要再生成虚表指针。注意这个继承下来的虚表指针和父类当中的虚表指针不同。3子类重写父类的虚函数那么子类中从父类继承下来的的虚表中对应的虚函数地址也要更换成重写函数的地址。4派生类的虚表中的函数地址可以分成三部分(1)父类的虚函数地址。(2)子类重写覆盖父类的虚函数地址。(3)子类自己的虚函数地址。5虚函数存放在哪里虚函数代码存放的位置和普通函数一样编译后都是一段指令都存在代码段中。6虚函数表指针(vptr)存放对象的内存空间中栈堆或全局区。下面这段代码可以检测出存放的位置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; /*cout 和上面的各个区域的地址进行对比,找到接近的位于同区域 endl;*/ printf(Person虚表地址:%p\n, *(int*)p3); printf(Student虚表地址:%p\n, *(int*)p4); //成员函数取地址加上一个 printf(虚函数地址:%p\n, Base::Func1);//代码段 printf(普通函数地址:%p\n, Base::Func3); return 0; }结尾本篇文章中所涉及到的代码gitee链接多态以上就是本文的全部内容了如果对你有所帮助的话请大佬不妨给博主来个“一键三连”这对我是大大的支持也能让博主产出更优质的内容。往期回顾【C】从基类到派生类关于C继承的务实梳理总结本文系统讲解C多态机制涵盖虚函数、虚函数重写、多态的两个条件基类指针/引用调用虚函数、派生类重写基类虚函数并通过代码实例演示多态实现原理。深入剖析虚函数表vtable与虚函数表指针vptr的作用阐明动态绑定与静态绑定的区别强调析构函数设为虚函数的重要性以避免内存泄漏。同时介绍协变、纯虚函数、抽象类、override与final关键字等进阶特性帮助读者全面掌握运行时多态的核心机制。⚡把上面的内容吃透就休息一下吧⚡