C++ 多态:概念,定义及实现以及原理

发布时间:2026/8/7 7:43:00
C++ 多态:概念,定义及实现以及原理 目录一.多态的概念1.1静多态1.2动多态重点二.多态的定义及实现2.1 多态的构成条件2.1.1 虚函数2.1.2 实现多态还有两个必须重要条件2.1.3 虚函数的重写/覆盖2.1.4 多态场景的一个选择题腾讯面试题2.1.5 虚函数重写的一些其他问题2.1.6override和final关键字2.1.7重载/重写/隐藏的对比面试考点一.多态的概念多态polymorphism的概念通俗来说就是多种形态。多态分为编译时多态静态多态和运行时多态动态多态这里我们重点讲运行时多态编译时多态静态多态和运行时多态动态多态。编译时多态静态多态主要就是我前面文章讲的函数重载和函数模板他们传不同类型的参数就可以调用不同的函数通过参数不同达到多种形态之所以叫编译时多态是因为他们实参传给形参的参数匹配是在编译时完成的我们把编译时一般归为静态运行时归为动态。运行时多态具体点就是去完成某个行为函数可以传不同的对象就会完成不同的行为就达到多种形态。比如买票这个行为当普通人买票时是全价买票学生买票时是优惠买票5折或75折)军人买票时是优先买票。再比如同样是动物叫的一个行为函数传猫对象过去就是”( ^ w ^ 喵“传狗对象过去就是”汪汪”。1.1静多态1. 核心定义在代码编译阶段编译器就已经确定好了要执行哪一个函数地址在编译期绑定也叫静态绑定、早绑定。 程序还没运行调用关系就已经敲定。2. 实现方式函数重载最典型 同一个作用域内函数名一致参数列表个数、类型、顺序不同编译器依靠参数区分调用哪个函数。运算符重载C3. 特点绑定时机编译期执行效率高运行时无额外开销不依赖继承、重写仅依靠函数签名区分。1.2动多态重点1. 核心定义编译阶段无法确定具体调用哪个函数必须等到程序运行起来根据当前对象的真实类型动态匹配对应的函数又称动态绑定、晚绑定。 也是面向对象里大家常说的狭义上的 “多态”。2. 必备条件C/Java 通用逻辑父子类存在继承关系父类声明虚函数C/ 允许重写Java子类重写父类的虚函数使用父类指针 / 引用指向子类对象。3. 特点绑定时机运行期需要继承 重写 虚函数支撑运行时有查表开销效率略低于静态多态同一个调用语句传入不同子类对象表现不同行为是多态最核心价值。对比项静态多态动态多态绑定时机编译期运行期别称早绑定、静态绑定晚绑定、动态绑定实现手段函数重载、运算符重载继承 虚函数重写、父类引用 / 指针是否需要继承不需要必须依靠继承体系运行开销几乎无开销虚表查找轻微运行开销典型场景同一类中同名不同参函数调用统一父类接口不同子类差异化实现二.多态的定义及实现2.1 多态的构成条件多态是一个继承关系的下的类对象去调用同一函数产生了不同的行为。比如Student继承了Person。Person对象买票全价Student对象优惠买票2.1.1 虚函数类成员函数前面加virtual修饰那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰注意虚函数和虚继承虽然用的是同一个关键字virtual但是并没有什么关联关系对比维度虚函数 virtual 函数虚继承 virtual 继承修饰对象类的成员函数继承方式父类继承核心目标实现运行时多态动态绑定函数解决菱形多重继承的数据二义性、多份基类副本依赖场景单继承、普通继承都能用仅限多重菱形继承场景底层结构虚函数表 vtable 虚指针 vptr虚基表 虚基指针 vbptr作用对象控制成员函数调用逻辑调整对象内存布局、共享基类成员变量是否影响数据存储不改变成员变量布局大幅改变继承体系的内存布局典型问题解决父类指针精准调用子类重写方法d.A::x分不清是 B 里的 A 还是 C 里的 A内存开销对象增加 1 个 vptr 指针子类增加 vbptr 虚基指针2.1.2 实现多态还有两个必须重要条件被调用的函数必须是虚函数并且完成了虚函数重写/覆盖若父类没有用虚函数则不会构成多态调用的时候只会调用父类基类不开virtual编译时认准指针类型是Animal*固定调用父类方法基类开启virtual运行时查看指针指向的真实对象类型自动匹配子类重写的函数。func(a)→ Animal::talkfunc(c)→ 依然 Animal::talkfunc(d)→ 依然 Animal::talk必须是基类的指针或者引用调用虚函数2.1.3 虚函数的重写/覆盖虚函数的重写/覆盖派生类中有一个跟基类完全相同的虚函数即派生类虚函数与基类虚函数的返回值、函数名字、参数列表完全相同称派生类的虚函数重写了基类的虚函数注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写因为C 规定基类中被virtual修饰的成员函数其 “虚函数” 的特性会被派生类继承。只要派生类定义的函数与基类虚函数的签名完全一致函数名、参数列表、const/volatile修饰符、返回类型都匹配那么这个派生类函数会自动成为虚函数无需显式加virtual关键字自然也能构成对基类虚函数的重写但是该种写法不是很规范不建议这样使用不过在考试选择题中经常会故意留这个坑让你判断是否构成多态主包之前在腾讯的笔试题中见过非常恶心只能说相关出题人用心险恶。2.1.4 多态场景的一个选择题腾讯面试题代码分析#include iostream using namespace std; // 父类基类A class A { public: // 虚函数func允许子类重写默认参数val1静态绑定的关键 virtual void func(int val 1) { cout A- val endl; } // 虚函数test内部调用func()B未重写此方法继承复用的关键 virtual void test() { func(); // 等价于 this-func()this是隐藏指针 } }; // 子类派生类Bpublic继承A继承的核心 class B : public A { public: // 重写overrideA的虚函数func函数签名和父类一致多态的前提 // 默认参数改为val0但默认参数不参与多态 void func(int val 0) { cout B- val endl; } // 关键B没有重写test() → 完全继承A的test()逻辑 }; int main() { B* p new B; // 静态类型B*动态类型B* p-test(); // 核心调用1重点分析 p-func(); // 核心调用2对比分析 delete p; // 补充避免内存泄漏 return 0; }2.1.5 虚函数重写的一些其他问题协变了解派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或引用时称为协变。协变的实际意义并不大所以了解一下即可但是若AB类没有继承关系就会有报错就是因为没有满足协变的要求如下图析构函数的重写笔试常见基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加 virtual 关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同看起来不符合重写的规则实际上编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成 destructor 所以基类的析构函数加了 virtual 修饰派生类的析构函数就构成重写。下面结合代码说明一下某些情况下若析构函数不加 virtual 会出现的问题为什么基类中的析构函数建议设计为虚函数这个问题在面试中经常考察一定要结合样例才可以讲清楚总结基类和派生类的析构函数在编译器的作用下定义为相同的名称在main函数中如果用基类的指针或引用来指向基类或派生类对象时若调用delete则只会调用基类的析构不会调用派生类的析构所以我们要加上virtual来构成多态指向谁调用谁这里为什么调用两次~Person1.Person* p1 new Person; delete p1;真实对象就是Person虚析构触发自身析构函数仅执行~Person()→ 控制台第一行输出~Person()2.Person* p2 new Student; delete p2;因为基类Person析构加了virtual虚析构开启动态绑定完整的析构流程运行时识别真实类型是Student优先调用子类析构~Student()C 语法强制规则子类析构执行完毕后编译器会自动调用直接父类的析构函数不需要手动写代码 → 依次打印~Student()~Person()合在一起三行输出视觉上看到两次~Person()。2.1.6override和final关键字从上面可以看出C对虚函数重写的要求比较严格但是有些情况下由于疏忽比如函数名写错参数写错等导致无法构成重写而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来debug灰得不偿失因此C11提供了 override 可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数那么可以用final修饰一、override关键字1、作用显式声明当前函数意图重写父类的虚函数编译器强制校验 必须确实正确重写了基类的虚函数否则直接编译报错杜绝手误导致 “本该重写实际变成新建重载 / 隐藏函数”。2、使用规则只能写在子类重写虚函数的形参列表末尾只能修饰成员函数不能修饰普通函数、构造 / 析构、变量基类必须是virtual虚函数否则加override编译报错。这里我们可以看出不加override编译器不会报错当我们加上override后直接就会检查出来二.final关键字2.1.7重载/重写/隐藏的对比面试考点