Limesn
Limesn
发布于 2026-09-28 / 0 阅读
0

第八章 多态

一、关于多态的基础概念

1. 什么是多态?

  • 广义定义:多态(Polymorphism)意味着“多种形态”。在编程中,它指的是同一个接口(函数名),在不同的对象上表现出不同的行为。
  • C++ 中的分类:
    1. 静态多态(编译期多态):通过**函数重载(Overload)和模板(Template)**实现。编译器在编译阶段就确定了调用哪个函数。
    2. 动态多态(运行期多态):通过**继承 + 虚函数(Virtual Function)**实现。程序在运行阶段,根据对象的实际类型来决定调用哪个函数。(本章复习的重点就是动态多态)。
  • 意义与目的:实现接口复用和代码解耦。让程序员可以编写出处理基类的通用代码,而无需关心具体子类的实现细节。

2. 产生多态的条件有哪些?

必须同时满足以下三个条件,才能产生运行期多态:

  1. 存在继承关系:必须有父类和子类。
  2. 虚函数重写(Override):子类必须重写(覆盖)父类的虚函数。要求函数名、参数列表、返回类型(协变除外)完全相同,且父类函数必须有 virtual 关键字。
  3. 父类指针或引用指向子类对象:必须通过父类的指针(Base* ptr = new Derived();)或引用(Base& ref = derivedObj;)来调用虚函数。
  • (补充:如果是直接通过对象赋值,如 Base b = derivedObj;,会发生对象切片,无法触发多态。)

3. 什么是虚函数?

  • 定义:在类的成员函数声明前加上 virtual 关键字的函数。
  • 作用:告诉编译器这个函数需要动态绑定(Dynamic Binding)。当通过父类指针或引用调用该函数时,编译器不会在编译期固定调用父类的版本,而是生成代码在运行期去查找该对象实际类型对应的函数版本。

二、虚函数机制的底层原理(重难点)

4. 虚函数机制的原理是什么? 一个对象对应一个虚函数表,还是一个类对应一个虚函数表?

  • 答案:一个类对应一个虚函数表(vtable),一个对象对应一个虚函数表指针(vptr)。
  • 底层原理解析:
    1. 虚函数表(Virtual Table, vtable):如果一个类包含虚函数,编译器会在编译期为该类生成一张虚函数表。这是一个存放在只读数据段(.rodata)的数组,里面存放的是该类所有虚函数的入口地址。(注意:同一个类的所有对象共享同一张 vtable)。
    2. 虚函数表指针(Virtual Pointer, vptr):编译器会在包含虚函数的类的对象内存布局的最前面(通常是前 8 字节,64位系统),偷偷插入一个指针,即 vptr。这个指针在运行期(对象构造时)被初始化,指向该对象所属类的 vtable。
    3. 动态绑定的过程:当通过父类指针 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)。
  • 特点:
    1. 不能实例化对象:Base b; 或 new Base(); 都会编译报错。只能定义指向它的指针或引用。
    2. 只能作为基类:主要用于被继承,为派生类提供统一的接口规范(类似于 Java 中的 interface,但 C++ 抽象类还可以包含成员变量和非虚函数)。
    3. 子类的责任:如果子类没有实现父类所有的纯虚函数,那么这个子类也依然是抽象类,无法实例化。只有实现了所有纯虚函数,子类才能被实例化。
    4. 可以有构造函数:虽然抽象类不能直接实例化,但它的构造函数会在子类对象构造时被调用,用于初始化抽象类中的成员变量。

四、多态相关的面试高频延伸(必看)

  1. 为什么父类的析构函数要写成虚函数(virtual destructor)?
    • 原因:当用父类指针指向子类对象,并在最后 delete 该指针时,如果父类析构函数不是虚函数,只会调用父类的析构函数,子类特有的资源(如动态内存)就不会被释放,造成内存泄漏。写成虚函数后,会先调用子类析构,再调用父类析构,保证资源完全释放。
  2. 构造函数能不能是虚函数?
    • 不能。因为虚函数的调用依赖于 vptr,而 vptr 是在构造函数执行阶段才被初始化的。在调用构造函数时,对象还不存在,vptr 还没设置好,无法实现动态绑定。
  3. 内联函数(inline)能不能是虚函数?
    • 可以,但通常不会内联。当通过对象直接调用(非多态)时,编译器可能内联;但当通过指针或引用调用(多态)时,必须在运行期查表,此时内联失效。
  4. 静态成员函数能不能是虚函数?
    • 不能。静态成员函数属于类,不属于对象,没有 this 指针,无法访问 vptr,因此无法实现多态。