一文搞懂C++运行时多态:virtual、override、final与虚函数表

发布时间:2026/9/3 10:51:08
一文搞懂C++运行时多态:virtual、override、final与虚函数表 一文搞懂C运行时多态virtual、override、final与虚函数表引言首先先说明虽然多态的学习成本较继承而言低很多但由于其与继承高度绑定请熟练理解继承后再对多态进行学习多态是C的最后一个重要特性它分为编译时多态静态多态和运行时多态动态多态两部分编译时多态指的是在编译期间对同一个类或函数产生了不同的效果主要就是指函数重载类模板函数模板运算符重载等情况而运行时多态就是今天要重点学习的部分因此以下会将运行时多态简写为多态多态与继承紧密相关接下来将围绕多态的性质语法及其衍生语法用途经典习题以及实现多态的初步原理等角度进行介绍1. 多态的概念多态顾名思义就是指多种形态运行时多态即“在运行时根据某种方式时某个解决方案如函数的行为有所差异是其具备多种形态”具体来说它主要指通过函数重写的方式实现同一个父类的不同派生类之间或父类与其派生类之间通过父类指针/引用调用同一个函数时能够产生不同效果的场景举例来说对于基类Animal与其派生类Dog和Cat可以通过多态的方式使三个类中都具备的函数animal_sound()在使用Animal指针/引用进行调用的情况下运行时产生不同的效果代码示例classAnimal{public:virtualvoidanimal_sound(){cout根据动物区分叫声endl;}};classDog:publicAnimal{public:virtualvoidanimal_sound(){cout汪汪endl;}};classCat:publicAnimal{public:virtualvoidanimal_sound(){cout喵喵endl;}};voidtest1(){Animal s1;Dog s2;Cat s3;Animalrs1s1;Animalrs2s2;Animalrs3s3;rs1.animal_sound();//根据动物区分叫声rs2.animal_sound();//汪汪rs3.animal_sound();//喵喵}intmain(){test1();return0;}通过上述代码及其运行结果可以观察到同样是Animal类型的变量调用了animal_sound()函数但运行时却能够根据变量中存储的原始类型来进行函数不同行为的区分这就是多态的神奇之处2. 多态的语法观察上述代码应该能发现多态是在继承的基础上进行了更改具体来讲需要统一使用基类的指针或引用进行构成多态的函数的调用需要在派生类中对指定函数进行函数重写函数重写指在派生类中对父类的某个虚函数进行重建函数体的过程在上述示例中Dog类与Cat类中的animal_sound函数即Animal类中animal_sound函数的函数重写衍生规则由于需要使用父类的指针或引用来进行函数的调用所以必须支持赋值兼容转换也就是说想使用基类指针或引用触发多态时必须以public方式继承函数重写的触发条件如下将需要触发多态的成员函数父类与子类中都需要使用virtual修饰为虚函数重写函数要满足三相同函数名返回值类型参数列表注函数重写的本质是重写函数体实际上不会重写参数列表也就是说不会重写缺省参数可以发现多态的语法较为复杂严苛接下来示例将仍旧使用上述代码同时将增添语法的相关注释以便学习classAnimal//创建父类Animal{public:virtualvoidanimal_sound()//使用virtual关键字修饰需要实现多态的成员函数animal_sound使其成为虚函数为接下来的函数重写操作奠基{cout根据动物区分叫声endl;}};classDog:publicAnimal//派生类Dog继承父类Animal{public:virtualvoidanimal_sound()//将重写函数animal_sound在派生类中声明为虚函数同时在保证三相同的情况下进行基类中对应成员函数的重写{cout汪汪endl;}};classCat:publicAnimal//派生类Cat与Dog相同{public:virtualvoidanimal_sound(){cout喵喵endl;}};voidtest1(){Animal s1;Dog s2;Cat s3;Animalrs1s1;Animalrs2s2;//将派生类实例化出的成员赋值给基类的引用或指针以触发多态Animalrs3s3;rs1.animal_sound();//根据动物区分叫声rs2.animal_sound();//汪汪rs3.animal_sound();//喵喵}intmain(){test1();return0;}3. 重写函数的三相同的特殊情况形参名与缺省参数与协变关于重写函数中三相同的要求其实存在一些特殊情况及细节但难度不大且只做了解即可3.1 重写函数的形参名可以不同三相同中有提到参数列表要相同其实参数列表相同仅仅指参数的类型及类型顺序要与源函数相同对参数名没有要求究其原因是重写函数只重写函数体不会对重写的参数列表做处理稍做验证的代码示例classA{public:virtualvoidfunc(inta0){coutvirtual void func(int a)endl;}};classB:publicA{public:virtualvoidfunc(intb0){coutvirtual void func(int b)endl;}};voidtest2(){A*pnewB;p-func();//virtual void func(int b)}可以观察到即使派生类B中的func形参名与A中不同也仍旧触发了多态也就意味着函数重写成功即三同中的参数列表相同不包括形参名3.2 三相同中参数列表的缺省参数可以不同与形参名类似由于函数重写不会重写参数列表因此三相同中的缺省参数也可以不同最终会按照父类中的缺省参数进行初始化操作简单代码示例classA{public:virtualvoidfunc(inta0){coutvirtual void func(int a): aendl;}};classB:publicA{public:virtualvoidfunc(intb1){coutvirtual void func(int b): bendl;}};voidtest2(){A*pnewB;p-func();//virtual void func(int b): 0}观察运行结果可知b的默认值为0而非重写函数中的缺省值1也就证明了函数重写不会重写参数列表的缺省参数3.3 协变协变是重写函数的一个特殊规则正常重写函数都需要满足三相同参数列表函数名返回值但协变规定重写函数可以不满足返回值类型必须相同但与此同时也对返回值不同时进行了规定父类中重写函数的返回值必须是一个父类类型可以不是自己的引用或指针而子类也必须是一个对应父类返回值的子类类型可以不是自己的引用或指针注必须同样是或同样是*两者不能混用-》也就是说对于两组分别满足继承关系的A父类和B子类与C父类和D子类协变的存在可以使A中的重写函数返回值为A/A*此时B中的返回值就需要为B/B*也可以A中重写函数返回值为C/C*B中为D/D*代码示例//协变举例classC{};classD:publicC{};classB;//声明BclassA{public://父类A使用父类A的引用或指针做返回值时virtualAfunc1(){coutvirtual A func1()endl;A a;returna;}virtualA*func2(){coutvirtual A* func2()endl;returnnullptr;}//父类A使用父类C的引用或指针做返回值时virtualCfunc3(){coutvirtual C func3()endl;C c;returnc;}virtualC*func4(){coutvirtual C* func4()endl;returnnullptr;}//验证返回值不可以*混用/*virtual A* func5() { cout virtual A* func5() endl; return nullptr; }*///验证返回值必须父类对应父类子类对应子类virtualBfunc6(){coutvirtual B func6()endl;B b;returnb;}};classB:publicA{public://子类B使用子类B的引用或指针做返回值时virtualBfunc1(){coutvirtual B func1()endl;B b;returnb;}virtualB*func2(){coutvirtual B* func2()endl;returnnullptr;}//子类B使用子类D的引用或指针做返回值时virtualDfunc3(){coutvirtual D func3()endl;D d;returnd;}virtualD*func4(){coutvirtual D* func4()endl;returnnullptr;}//验证返回值不可以*混用/*virtual B func5() { cout virtual B func5() endl; B b; return b; }*///验证返回值必须父类对应父类子类对应子类virtualAfunc6(){coutvirtual A func6()endl;A a;returna;}};voidtest(){//协变正确示例B s;Arss;rs.func1();//virtual B func1()rs.func2();//virtual B* func2()rs.func3();//virtual D func3()rs.func4();//virtual D* func4()//-可以发现上述四句代码均触发了多态由此验证了协变的规则//协变的错误使用示例//rs.func5();//由于*混用所以编译失败报错“返回类型既不相同也不协变”rs.func6();//编译失败报错“返回类型既不相同也不协变”}intmain(){test();return0;}3.4 函数重写的一道经典例题//以下程序的运行结果是什么( C )//A. A: 0 B. A: 1 C. B: 0 D. B: 1classA{public:virtualvoidfunc(intx0){coutA: xendl;}voidtest(){func();}};classB:publicA{public:virtualvoidfunc(intx1){coutB: xendl;}};voidtest1(){B().test();}intmain(){test1();//B: 0return0;}解析test1函数中使用B的匿名对象调用了B类中继承的A中的test函数然后test函数再调用func函数-》这里会产生一个疑问此时test参数中隐含的this指针是const A类型还是const B类型呢事实上this指针遵循“最终成员在哪个类中找到其this指针就是哪个类型的”也就是说虽然test是B类中继承的A类的成员函数但实际上由于test是A的成员函数所以其this指针也是const A*类型的-》也就是说最终传给func的this指针的类型是const A*此时也符合多态的触发条件在派生类内通过this指针调用了继承下来的父类成员函数且该成员函数与派生类构成函数重写所以最终会调用B类中重写过的func函数也就会运行cout B: x endl;那么为什么x的值是0呢这是因为重写函数只会重写函数体不会重写缺省参数等信息也就是说此时x所使用的缺省值仍是A类中func函数的0-最终运行结果为B: 04. 多态中的final与override关键字首先要明确override与fina关键字在多态中的作用是在C11后添加的接下来将对两者在多态中的行为进行说明及代码示例4.1 override关键字override在多态中的作用是帮助检查函数是否正确重写使用时要加在重写函数的参数列表后代码示例classA{public:virtualvoidfunc(){coutA:virtual void func()endl;}};classB:publicA{public://virtual void funs() override//由于funs与A中函数名不同这里override关键字检查出不可构成函数重写编译错误//virtual void func(int x 1) override//由于参数列表不同所以编译失败/*virtual*/voidfunc()override//正确且由于子类中重写函数的virtual的作用是为了写明该函数为重写函数//加入override关键字后就可以不用在子类重写函数中添加virtual关键字了{coutB:virtual void func()endl;}};voidtest1(){A*psnew(B);ps-func();}4.2 final关键字final关键字在继承中已经见过了其作用是将类标记为最终类是其不可作为父类而在多态中其作用是使一个函数不可被重写使用时要将final关键字添加到函数的参数列表后代码示例usingnamespacestd;classC{public:virtualvoidfunc()final{coutC:virtual void func()endl;}};classD:publicC{public:voidfunc()override//编译失败-》由于C中func函数被声明为final不能被重写{coutB:virtual void func()endl;}};voidtest2(){C*psnew(D);ps-func();}intmain(){//test1();test2();return0;}5. 纯虚函数由于多态的使用场景在多数情况下是不需要使用父类的譬如父类为Animal想通过多态来实现不同动物的叫声时Animal中的animal_sound函数本身是不需要被调用的。此时重点其实在于Animal中animal_sound函数在子类中的重写函数如子类Dog中animal_sound函数体中的输出“汪汪”等为了更加契合使用情景同时也为避免程序员忘记对父类虚函数进行重写导致问题便设计了纯虚函数与抽象类语法。纯虚函数与抽象类纯虚函数是指在虚函数参数列表后添加0的函数而抽象类指的是含有纯虚函数的类继承的父类的纯虚函数的子类同样也是抽象类纯虚函数的作用就是将类变为抽象类而抽象类的特性是不能使用抽象类实例化对象但可以通过重写纯虚函数的方式使继承父类纯虚函数的子类变为可以正常实例化对象的类换言之纯虚函数与抽象类的作用其实就是“促使继承纯虚函数的父类重写该函数”与此同时由于不能实例化含有纯虚函数的类也就没有必要为纯虚函数创建函数体了当然也可以选择创建只不过这样做的现实意义不大//应用举例classAnimal{public://virtual void animal_sound() 0;//在虚函数后添加“0”使其成为纯虚函数virtualvoidanimal_sound(){cout根据动物区分叫声endl;}};classDog:publicAnimal{public:virtualvoidanimal_sound(){cout汪汪endl;}};voidtest1(){//Dog s;////当没有在Benz类中重写func函数时会编译报错无法实例化抽象类Dog s;}intmain(){test1();return0;}6. 初步了解多态的底层实现原理先来一道练习题作为引入//计算下面类A的大小classA{public:virtualvoidfunc(){coutvirtual void func()endl;}int_a;char_b;};voidtest1(){coutsizeof(A)endl;//x86环境下结果为12}题目解析与主题引入前面已经提到过类的大小计算方式与结构体大小的计算方式一致那这里A的大小按道理来说应该是int类型占0~3char类型占4共5然后4 * 2 5 4 * 1所以A的大小应该是8但为什么打印出的结果为12呢问题就出现在A中的虚函数实际上存在虚函数的类比普通的类多了一个隐藏变量——“虚函数表指针”而多态的底层实现也是依赖“虚函数表指针”指向的“虚函数表”注类中存放的是“虚表指针(__vfptr)”而非“虚函数表”所以类的大小不会在已有虚函数的情况下再因增加虚函数而扩大关于虚函数表虚函数表本质是一个函数指针数组其中存放了类中继承的虚函数地址若有且未被重写、重写的继承的虚函数地址、自身的虚函数地址三部分子类继承父类的虚函数且未进行重写时子类中的虚函数表对应虚函数的地址与父类的虚函数的地址一致同一个类实例化的对象都共享同一个虚函数表如同类中的成员函数般其不属于某个对象而是属于该类的所有对象虚函数与普通函数一样都存放在代码区而虚函数表中存放的只不过是虚函数的指针并未存放虚函数本身虚函数表的存储位置并没有被C标准规定由编译器决定但通常都存放在代码段的常量区VS编译环境对虚函数表函数指针数组的尾部使用0x00000000做了特殊标记其作用类似字符串的\0但g一般不会放//虚表性质验证classB{public:virtualvoidfunc(){coutB:virtual void func()endl;}virtualvoidfuncs(){coutB:virtual void funcs()endl;}voidfunct(){coutC:virtual void funct()endl;}};classC:publicB{public:virtualvoidfunc(){coutC:virtual void func()endl;}};voidtest2(){coutsizeof(B)endl;//x86下结果是4-尽管B中存放了两个虚函数但其大小仍然是4说明类中存放的是“虚表指针”而非“虚函数表”B s1;B s2;//通过调试发现s1s2的虚表指针值相同-同一个类实例化出的对象共享同一个虚函数表也就共享同一个虚表指针C s3;//调试发现s3虚表指针的值与s1s2不同-不同类实例化出的虚函数表不同所以虚表指针不相同//但同时也发现s3虚函数表中未重写的函数funcs的虚函数指针与s1s2中的funcs指针一致//而重写了的func函数的指针与s1s2的func的地址不一致-》若子类未重写虚函数则虚函数表中会存放父类中虚函数的地址若重写则存放重写后的新地址}//补充//1.同一个类实例化出的对象为什么共用同一个虚函数表呢//答如同相同类实例化出的对象的函数般由于函数不会随着对象中数据的改变而改变//所以相同类的对象调用同一个函数即可没必要为每个对象都重新分配一个函数//2.为什么子类实例化出的对象中继承的父类不能与父类对象共享同一个虚函数表呢//答子类可能对父类中的虚函数进行重写若重写则虚函数表中对应的虚函数地址就会发生变化所以无法共享//同时这点也是“多态实现的底层原理”的核心步骤//验证虚函数以及虚函数表在VS2022中的存储位置//由于程序中每个区域管理的内存都有明显划分所以可以通过以下方式验证观察虚表指针与虚函数离哪个区域的数据地址更近voidtest3(){inti0;staticintj1;int*p1newint;constchar*p2xxxxxxxx;printf(栈:%p\n,i);printf(静态区:%p\n,j);printf(堆:%p\n,p1);printf(常量区:%p\n,p2);B b;C d;B*p3b;C*p4d;printf(Person虚表地址:%p\n,*(int*)p3);//将p3的类型强转为int*类型解引用结果即为x86环境下该对象中虚函数表的地址printf(Student虚表地址:%p\n,*(int*)p4);printf(虚函数地址:%p\n,B::func);printf(普通函数地址:%p\n,B::funct);//验证发现//1.VS2022中虚函数存放在代码区//2.VS2022中虚函数表存放在代码段的常量区}intmain(){//test1();//test2();test3();return0;}7. 结语以上便是多态相关的基础语法了多态整体的学习难度较继承而言算是偏低的但为了较熟练掌握理解其思想仍旧需要及时复习……至此C三大特性的学习就结束了接下来将有更加复杂的智能指针等知识等着我们但也不要因此气馁加油再坚持一下就可以完成C基础语法的学习了