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

第七章 继承

1. 什么是继承?继承的意义目的是什么?

  • 详细定义:继承是面向对象三大特性(封装、继承、多态)之一。它允许我们定义一个类(派生类/子类)去继承另一个类(基类/父类)的属性和方法。
  • 底层的意义与目的:
    1. 代码复用(实现继承):子类可以直接使用父类的成员变量和成员函数,避免重复造轮子。
    2. 建立抽象层次(接口继承):表达现实世界中“Is-a”(是一个)的关系(如:猫是动物)。通过继承,我们可以用父类指针统一操作不同的子类对象。
    3. 实现多态:基于继承和虚函数,实现“一个接口,多种形态”,这是 C++ 运行期多态的基础。
  • 注意:继承是一种强耦合关系。现代软件工程提倡“组合优于继承”(Composition over Inheritance),因为父类的修改往往会破坏子类的封装。

2. C++中是单继承还是多继承?

C++ 既支持单继承,也支持多继承。

  • 单继承:class Derived : public Base。结构简单,不易产生歧义。
  • 多继承:class Derived : public Base1, public Base2。一个子类可以有多个父类。
  • 对比:Java、C# 等语言只支持单继承(类),但支持多实现(接口)。C++ 的多继承虽然灵活,但引入了如菱形继承、二义性等复杂性。

3. public/ protected / private这3种权限效果分别是什么?

  • 类的内部访问:
    • public:类内、类外、子类均可访问。
    • protected:类内、子类可访问,类外(对象)不可访问。
    • private:仅类内(和友元)可访问,子类和类外均不可访问。
  • 继承权限的影响(重点):
    父类的 private 成员无论何种继承方式,子类都不可见(不可直接访问,但物理内存上包含)。
    • public 继承:父类 public -> 子类 public;父类 protected -> 子类 protected。
    • protected 继承:父类 public、protected -> 子类 protected。
    • private 继承:父类 public、protected -> 子类 private。
    • 补充:private 继承通常表示“根据某物实现出”(Is-implemented-in-terms-of),而不是“是一个”(Is-a)。在 C++11 中,推荐使用 final 关键字防止进一步继承。

4. 单继承下,子类对象的大小跟什么有关?

子类对象的大小(sizeof)由以下因素决定:

  1. 从父类继承的非静态成员变量(静态变量在全局/静态区,不占对象空间)。
  2. 子类自己新增的非静态成员变量。
  3. 虚函数指针(vptr):如果类中有虚函数(无论是继承的还是自己声明的),编译器会为该类生成一个虚函数表(vtable),并在对象中插入一个隐藏的 vptr(通常 8 字节,64位系统)。
  4. 内存对齐(Padding):按照成员中最大对齐数的整数倍对齐。
  5. 空基类优化(EBO):如果父类是空类(没有非静态成员变量和虚函数),编译器通常会优化,使其在子类中占用 0 字节(但独立空类占 1 字节)。

5. 单继承下,子类对象的创建和销毁过程是什么?

  • 创建过程(构造):
    1. 分配内存空间(在栈或堆上)。
    2. 调用父类的构造函数(初始化父类部分)。
    3. 初始化子类的虚函数表指针(vptr)指向子类的虚函数表(关键步骤,发生在构造函数体之前)。
    4. 调用子类成员变量的构造函数(按声明顺序,与初始化列表顺序无关)。
    5. 执行子类自身的构造函数体。
  • 销毁过程(析构):与构造完全相反。
    1. 执行子类自身的析构函数体。
    2. 调用子类成员变量的析构函数(按声明逆序)。
    3. 调用父类的析构函数。
    4. 释放内存。
  • 警告:绝不要在构造函数和析构函数中调用虚函数,因为此时 vptr 指向的是当前类(父类或子类)的虚表,无法触发多态,可能导致未定义行为。

6. 什么是隐藏(Name Hiding)?有什么效果?

注:图片中“隐藏oversee”大概率是笔误,C++中对应术语是 Name Hiding(名字隐藏),而非 Override。

  • 定义:子类中声明了与父类同名的成员(无论是函数还是变量),无论参数列表是否相同,父类的该名字在子类作用域内都会被隐藏。
  • 效果:
    1. 在子类中直接调用该名字,只会调用子类的版本。
    2. 如果想调用父类被隐藏的函数,必须加上作用域限定符 Base::func(),或者在子类中使用 using Base::func; 将其引入。
  • 对比重载、重写(覆盖)、隐藏:
    • 重载(Overload):同一作用域,同名不同参。
    • 重写/覆盖(Override):父子类中,函数签名完全相同,且父类函数是 virtual(触发多态)。
    • 隐藏(Hide):父子类同名,但不符合重写条件(参数不同,或父类非虚)。

7. 多继承下,子类对象的大小和什么有关?

  • 各个父类的非静态成员变量大小之和 + 子类自身成员变量大小 + 内存对齐。
  • 多个虚函数指针(vptr):如果多个父类都有虚函数,子类对象中会包含多个 vptr,分别指向对应父类子对象的虚函数表。例如 Derived : public Base1, public Base2,如果两者都有虚函数,Derived 对象内存中会有两个 vptr。
  • 虚继承的额外开销:如果使用了虚继承(解决菱形继承),对象中会增加虚基类表指针(vbptr),导致对象变大。

8. 多继承下,子类对象的创建和销毁过程是什么?执行顺序和什么有关?

  • 创建过程:
    1. 分配内存。
    2. 按照继承声明的顺序(从左到右)依次调用各个父类的构造函数。
    3. 如果存在虚继承,最底层的派生类负责初始化虚基类(虚基类在最早阶段被构造)。
    4. 初始化子类自身的 vptr(如果有多个基类,可能会被多次赋值)。
    5. 按成员声明顺序调用成员变量的构造函数。
    6. 执行子类构造函数体。
  • 销毁过程:严格与创建过程相反。
  • 顺序决定因素:完全取决于继承声明时的顺序和成员变量声明的顺序,与构造函数初始化列表的书写顺序无关。

9. 什么是菱形继承?如何解决?

  • 菱形继承(Diamond Inheritance):假设 Base 是基类,B 和 C 都继承自 Base,而 D 同时继承 B 和 C。
  • 问题:
    1. 数据冗余:D 对象中会包含两份 Base 的成员变量。
    2. 二义性:访问 Base 的成员时,编译器不知道走 B 那条路还是 C 那条路。
  • 解决方法:使用虚继承(Virtual Inheritance)。
    令 class B : virtual public Base 和 class C : virtual public Base。此时 Base 变为虚基类,D 中只会保留一份 Base 的实例。
  • 底层原理:虚继承通过虚基类表(vbtable)和虚基类指针(vbptr)计算偏移量来访问虚基类成员。虚基类由最底层的派生类(D)负责初始化。

10. 父子类型之间发生转换,转换的是什么?

  • 如果是指针或引用的转换:转换的是指针/引用的类型(即编译器如何看待这块内存)。指针的值(地址)可能会发生偏移(在多继承中,父类指针指向子类对象时,地址可能需要调整)。
  • 如果是对象直接赋值(Base b = derivedObj;):发生的是对象切片(Object Slicing)。派生类对象中的派生类特有部分会被丢弃,只保留基类部分,赋值给基类对象。这通常是不被期望的,因此多态通常通过指针或引用来实现。

11. 什么是向上转型?什么是向下转型?是否需要额外的语法?

  • 向上转型(Upcasting):子类指针/引用 -> 父类指针/引用。天然安全(子类包含父类),属于隐式转换,不需要额外语法。
  • 向下转型(Downcasting):父类指针/引用 -> 子类指针/引用。不安全(父类不一定包含子类特有部分),必须显式转换。
    • static_cast:编译期转换,不提供运行期安全检查。如果转型失败,属于未定义行为(UB)。
    • dynamic_cast:运行期转换,依赖 RTTI(运行期类型信息)。如果转型失败,指针返回 nullptr,引用抛出 std::bad_cast 异常。要求父类必须有虚函数(多态类型)。

12. 向下转型时,什么时候能成功,什么时候不能成功?(用自己的话描述)

向下转型能否成功,完全取决于父类指针/引用真正指向的对象到底是什么类型。

  • 成功的情况:如果父类指针实际上指向的是一个子类对象(或者该子类的进一步派生类对象),那么向下转型就是安全的,能够成功。这就像你手里拿着一个“动物”的标签,但实际上牵着的是一只“猫”,你把它强行当成“猫”来看待,是没问题的。
  • 失败的情况:
    1. 如果父类指针实际上指向的是一个纯粹的父类对象(它本来就不是子类),你非要把它当子类看,就会失败。
    2. 如果父类指针实际上指向的是另一个不相关的兄弟子类对象(比如你拿着“动物”标签,实际牵着“狗”,却非要转型成“猫”),也会失败。
  • 通俗比喻:向上转型是“把猫当成动物看”(绝对安全);向下转型是“把动物当成猫看”(只有当这只动物真的是猫时才能成功,如果是狗就会出问题)。
  • 使用 dynamic_cast 时,如果转型失败,它会返回空指针(或抛异常)来告诉你“这只动物不是猫”;而 static_cast 不会做运行期检查,如果转型错误,程序可能会直接崩溃或产生乱码(未定义行为)。