1. 什么是继承?继承的意义目的是什么?
- 详细定义:继承是面向对象三大特性(封装、继承、多态)之一。它允许我们定义一个类(派生类/子类)去继承另一个类(基类/父类)的属性和方法。
- 底层的意义与目的:
- 代码复用(实现继承):子类可以直接使用父类的成员变量和成员函数,避免重复造轮子。
- 建立抽象层次(接口继承):表达现实世界中“Is-a”(是一个)的关系(如:猫是动物)。通过继承,我们可以用父类指针统一操作不同的子类对象。
- 实现多态:基于继承和虚函数,实现“一个接口,多种形态”,这是 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)由以下因素决定:
- 从父类继承的非静态成员变量(静态变量在全局/静态区,不占对象空间)。
- 子类自己新增的非静态成员变量。
- 虚函数指针(vptr):如果类中有虚函数(无论是继承的还是自己声明的),编译器会为该类生成一个虚函数表(vtable),并在对象中插入一个隐藏的 vptr(通常 8 字节,64位系统)。
- 内存对齐(Padding):按照成员中最大对齐数的整数倍对齐。
- 空基类优化(EBO):如果父类是空类(没有非静态成员变量和虚函数),编译器通常会优化,使其在子类中占用 0 字节(但独立空类占 1 字节)。
5. 单继承下,子类对象的创建和销毁过程是什么?
- 创建过程(构造):
- 分配内存空间(在栈或堆上)。
- 调用父类的构造函数(初始化父类部分)。
- 初始化子类的虚函数表指针(vptr)指向子类的虚函数表(关键步骤,发生在构造函数体之前)。
- 调用子类成员变量的构造函数(按声明顺序,与初始化列表顺序无关)。
- 执行子类自身的构造函数体。
- 销毁过程(析构):与构造完全相反。
- 执行子类自身的析构函数体。
- 调用子类成员变量的析构函数(按声明逆序)。
- 调用父类的析构函数。
- 释放内存。
- 警告:绝不要在构造函数和析构函数中调用虚函数,因为此时 vptr 指向的是当前类(父类或子类)的虚表,无法触发多态,可能导致未定义行为。
6. 什么是隐藏(Name Hiding)?有什么效果?
注:图片中“隐藏oversee”大概率是笔误,C++中对应术语是 Name Hiding(名字隐藏),而非 Override。
- 定义:子类中声明了与父类同名的成员(无论是函数还是变量),无论参数列表是否相同,父类的该名字在子类作用域内都会被隐藏。
- 效果:
- 在子类中直接调用该名字,只会调用子类的版本。
- 如果想调用父类被隐藏的函数,必须加上作用域限定符
Base::func(),或者在子类中使用using Base::func;将其引入。
- 对比重载、重写(覆盖)、隐藏:
- 重载(Overload):同一作用域,同名不同参。
- 重写/覆盖(Override):父子类中,函数签名完全相同,且父类函数是
virtual(触发多态)。 - 隐藏(Hide):父子类同名,但不符合重写条件(参数不同,或父类非虚)。
7. 多继承下,子类对象的大小和什么有关?
- 各个父类的非静态成员变量大小之和 + 子类自身成员变量大小 + 内存对齐。
- 多个虚函数指针(vptr):如果多个父类都有虚函数,子类对象中会包含多个 vptr,分别指向对应父类子对象的虚函数表。例如
Derived : public Base1, public Base2,如果两者都有虚函数,Derived对象内存中会有两个 vptr。 - 虚继承的额外开销:如果使用了虚继承(解决菱形继承),对象中会增加虚基类表指针(vbptr),导致对象变大。
8. 多继承下,子类对象的创建和销毁过程是什么?执行顺序和什么有关?
- 创建过程:
- 分配内存。
- 按照继承声明的顺序(从左到右)依次调用各个父类的构造函数。
- 如果存在虚继承,最底层的派生类负责初始化虚基类(虚基类在最早阶段被构造)。
- 初始化子类自身的 vptr(如果有多个基类,可能会被多次赋值)。
- 按成员声明顺序调用成员变量的构造函数。
- 执行子类构造函数体。
- 销毁过程:严格与创建过程相反。
- 顺序决定因素:完全取决于继承声明时的顺序和成员变量声明的顺序,与构造函数初始化列表的书写顺序无关。
9. 什么是菱形继承?如何解决?
- 菱形继承(Diamond Inheritance):假设
Base是基类,B和C都继承自Base,而D同时继承B和C。 - 问题:
- 数据冗余:
D对象中会包含两份Base的成员变量。 - 二义性:访问
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. 向下转型时,什么时候能成功,什么时候不能成功?(用自己的话描述)
向下转型能否成功,完全取决于父类指针/引用真正指向的对象到底是什么类型。
- 成功的情况:如果父类指针实际上指向的是一个子类对象(或者该子类的进一步派生类对象),那么向下转型就是安全的,能够成功。这就像你手里拿着一个“动物”的标签,但实际上牵着的是一只“猫”,你把它强行当成“猫”来看待,是没问题的。
- 失败的情况:
- 如果父类指针实际上指向的是一个纯粹的父类对象(它本来就不是子类),你非要把它当子类看,就会失败。
- 如果父类指针实际上指向的是另一个不相关的兄弟子类对象(比如你拿着“动物”标签,实际牵着“狗”,却非要转型成“猫”),也会失败。
- 通俗比喻:向上转型是“把猫当成动物看”(绝对安全);向下转型是“把动物当成猫看”(只有当这只动物真的是猫时才能成功,如果是狗就会出问题)。
- 使用
dynamic_cast时,如果转型失败,它会返回空指针(或抛异常)来告诉你“这只动物不是猫”;而static_cast不会做运行期检查,如果转型错误,程序可能会直接崩溃或产生乱码(未定义行为)。