一、关于多态的基础概念
1. 什么是多态?
- 广义定义:多态(Polymorphism)意味着“多种形态”。在编程中,它指的是同一个接口(函数名),在不同的对象上表现出不同的行为。
- C++ 中的分类:
- 静态多态(编译期多态):通过**函数重载(Overload)和模板(Template)**实现。编译器在编译阶段就确定了调用哪个函数。
- 动态多态(运行期多态):通过**继承 + 虚函数(Virtual Function)**实现。程序在运行阶段,根据对象的实际类型来决定调用哪个函数。(本章复习的重点就是动态多态)。
- 意义与目的:实现接口复用和代码解耦。让程序员可以编写出处理基类的通用代码,而无需关心具体子类的实现细节。
2. 产生多态的条件有哪些?
必须同时满足以下三个条件,才能产生运行期多态:
- 存在继承关系:必须有父类和子类。
- 虚函数重写(Override):子类必须重写(覆盖)父类的虚函数。要求函数名、参数列表、返回类型(协变除外)完全相同,且父类函数必须有
virtual关键字。 - 父类指针或引用指向子类对象:必须通过父类的指针(
Base* ptr = new Derived();)或引用(Base& ref = derivedObj;)来调用虚函数。
- (补充:如果是直接通过对象赋值,如
Base b = derivedObj;,会发生对象切片,无法触发多态。)
3. 什么是虚函数?
- 定义:在类的成员函数声明前加上
virtual关键字的函数。 - 作用:告诉编译器这个函数需要动态绑定(Dynamic Binding)。当通过父类指针或引用调用该函数时,编译器不会在编译期固定调用父类的版本,而是生成代码在运行期去查找该对象实际类型对应的函数版本。
二、虚函数机制的底层原理(重难点)
4. 虚函数机制的原理是什么? 一个对象对应一个虚函数表,还是一个类对应一个虚函数表?
- 答案:一个类对应一个虚函数表(vtable),一个对象对应一个虚函数表指针(vptr)。
- 底层原理解析:
- 虚函数表(Virtual Table, vtable):如果一个类包含虚函数,编译器会在编译期为该类生成一张虚函数表。这是一个存放在只读数据段(.rodata)的数组,里面存放的是该类所有虚函数的入口地址。(注意:同一个类的所有对象共享同一张 vtable)。
- 虚函数表指针(Virtual Pointer, vptr):编译器会在包含虚函数的类的对象内存布局的最前面(通常是前 8 字节,64位系统),偷偷插入一个指针,即 vptr。这个指针在运行期(对象构造时)被初始化,指向该对象所属类的 vtable。
- 动态绑定的过程:当通过父类指针
ptr->func()调用虚函数时,底层汇编会执行以下步骤:- 取出
ptr指向对象中的 vptr。 - 通过 vptr 找到对应的 vtable。
- 在 vtable 中查找
func的偏移量,取出对应的函数地址。 - 跳转到该地址执行代码。
- 由于
ptr实际指向的是子类对象,其 vptr 指向的是子类的 vtable,因此调用的就是子类重写后的函数。
- 取出
三、概念辨析与进阶
5. oversea VS overload VS override 用自己的话描述
(注:图片中的 oversea 应该是笔误,正确术语是 Override(重写/覆盖)。还有另一个容易混淆的概念是 Hide(隐藏)。)
- Overload(重载):
- 范围:同一个作用域内(如同一个类中)。
- 条件:函数名相同,但参数列表不同(参数个数、类型、顺序不同)。与返回值无关。
- 本质:编译期多态。编译器根据传入的参数类型来决定调用哪个函数。
- Override(重写/覆盖):
- 范围:父子类之间(不同作用域)。
- 条件:子类定义了与父类完全相同的虚函数(函数名、参数列表、返回类型都相同)。
- 本质:运行期多态。子类覆盖了父类虚函数在 vtable 中的地址,从而触发动态绑定。
- C++11 建议:在子类重写的函数后加上
override关键字,让编译器帮你检查是否写错了参数,避免本该是重写却变成了隐藏。
- Hide(隐藏/遮蔽):
- 范围:父子类之间。
- 条件:子类定义了与父类同名的成员(变量或函数),但不符合重写条件(例如参数不同,或者父类函数没有
virtual)。 - 结果:父类的同名成员在子类作用域中被隐藏。需要在子类中调用父类被隐藏的函数时,必须加
Base::func()。
6. 什么是纯虚函数?
- 定义:在虚函数声明后加上
= 0,且没有函数体的虚函数。virtual void show() = 0; - 作用:纯虚函数是一种“接口规范”,它告诉子类“你必须实现这个功能,但我(父类)不知道怎么实现”。包含纯虚函数的类不能实例化对象。
7. 什么是抽象类?抽象类有什么特点?
- 定义:包含至少一个纯虚函数的类,称为抽象类(Abstract Class)。
- 特点:
- 不能实例化对象:
Base b;或new Base();都会编译报错。只能定义指向它的指针或引用。 - 只能作为基类:主要用于被继承,为派生类提供统一的接口规范(类似于 Java 中的
interface,但 C++ 抽象类还可以包含成员变量和非虚函数)。 - 子类的责任:如果子类没有实现父类所有的纯虚函数,那么这个子类也依然是抽象类,无法实例化。只有实现了所有纯虚函数,子类才能被实例化。
- 可以有构造函数:虽然抽象类不能直接实例化,但它的构造函数会在子类对象构造时被调用,用于初始化抽象类中的成员变量。
- 不能实例化对象:
四、多态相关的面试高频延伸(必看)
- 为什么父类的析构函数要写成虚函数(virtual destructor)?
- 原因:当用父类指针指向子类对象,并在最后
delete该指针时,如果父类析构函数不是虚函数,只会调用父类的析构函数,子类特有的资源(如动态内存)就不会被释放,造成内存泄漏。写成虚函数后,会先调用子类析构,再调用父类析构,保证资源完全释放。
- 原因:当用父类指针指向子类对象,并在最后
- 构造函数能不能是虚函数?
- 不能。因为虚函数的调用依赖于 vptr,而 vptr 是在构造函数执行阶段才被初始化的。在调用构造函数时,对象还不存在,vptr 还没设置好,无法实现动态绑定。
- 内联函数(inline)能不能是虚函数?
- 可以,但通常不会内联。当通过对象直接调用(非多态)时,编译器可能内联;但当通过指针或引用调用(多态)时,必须在运行期查表,此时内联失效。
- 静态成员函数能不能是虚函数?
- 不能。静态成员函数属于类,不属于对象,没有
this指针,无法访问 vptr,因此无法实现多态。
- 不能。静态成员函数属于类,不属于对象,没有