面向对象(二):继承

原理

派生类内存布局

派生类对象 = 基类子对象在前 + 派生类新增成员在后。基类子对象位于派生类对象的起始位置——这意味着向上转型(派生类指针→基类指针)是零开销的(地址值相同)。

class Base {
    int baseVal;     // 4B
    char baseChar;   // 1B + pad 3B
};
class Derived : public Base {
    int derivedVal;  // 4B
    double derivedDbl; // 8B
};
// 布局: [baseVal 4B][baseChar+pad 4B][derivedVal+pad 8B][derivedDbl 8B]
// sizeof(Derived) = 24

构造链的汇编实现

编译器在每个派生类构造函数体的最前面自动插入对基类构造函数的调用:

; Derived::Derived(int a, int b, const string& s)
; 阶段1: 基类构造(编译器插入)
    mov    rdi, [this]        ; rdi = 派生类对象首地址 = 基类子对象首地址
    mov    esi, [a]
    call   Base::Base(int)    ; 基类构造——零偏移
; 阶段2: 成员初始化
    mov    [this + 8], esi     ; derivedVal(基础类型,直接写)
    lea    rdi, [this + 16]   ; name(string 子对象)
    call   string::string()   ; 成员构造函数
; 阶段3: 构造函数体

如果初始化列表中未显式调用基类构造函数,编译器自动插入基类的默认构造函数。若基类没有默认构造函数 → 编译错误。

析构链完全逆序:派生类析构函数体 → 成员按声明逆序析构 → 编译器自动插入基类析构调用。

向上转型与虚析构

向上转型是零开销的——基类指针和派生类指针的值相同。但通过基类指针 delete 派生类对象时,若基类析构函数不是 virtual,则只调用基类析构,派生类的资源泄漏。

Base* p = new Derived();
delete p;  // ~Base() 非虚 → ~Derived() 不会被调用!

原理:非虚析构使用静态绑定——编译器根据指针的静态类型决定调用哪个析构函数。虚析构通过 vtable 进行动态绑定,运行时根据对象实际类型调用正确的析构函数。

三种继承方式的访问影响

继承方式基类 public 成员基类 protected 成员基类 private 成员
public保持 public保持 protected不可访问
protected变为 protected保持 protected不可访问
private变为 private变为 private不可访问

private/protected 继承会阻断类外的向上转型——只能在本类内部进行转换。

名称隐藏(Name Hiding)

派生类中定义与基类同名的非虚函数,会隐藏基类的所有同名重载版本。这不是重写——通过基类指针调用时走的仍是基类版本。使用 using Base::func; 可将基类重载版本导入派生类作用域。

多重继承的内存布局

多重继承的派生类对象包含所有基类子对象,按继承声明顺序排列:

[Base1 子对象][Base2 子对象][Derived 新增成员]

向上转型到不同基类时,this 指针可能需要偏移调整——编译器自动插入修正代码。

菱形继承与虚继承

非虚继承的菱形结构中,最终派生类包含多份共同祖先的子对象(每路径一份),导致名空间歧义。虚继承保证整个层级中只有一份虚基类子对象:

  • 中间类不再内嵌虚基类子对象,改为存储 vbptr(Virtual Base Pointer,8 字节)
  • 唯一的虚基类子对象放在最终对象的末尾
  • 虚基类的构造函数由最派生类直接调用,中间类不参与
  • 每次访问虚基类成员需要经过 vbptr → 虚基类表 → 偏移量 → 实际地址,多一次间接寻址

vbptr 指向虚基类表,表中记录从当前子对象到虚基类子对象的偏移量。无论走哪条继承路径,最终都收敛到同一虚基类实例。


语法

继承声明

class Derived : public Base { };      // public 继承
class Derived : protected Base { };   // protected 继承
class Derived : private Base { };     // private 继承
struct Derived : Base { };            // struct 默认 public 继承
class Derived : Base { };            // class 默认 private 继承

派生类构造函数

class Derived : public Base {
    Derived(int a, int b)
        : Base(a), member(b) { }   // 必须调用基类构造
};

using 提升访问级别

class Derived : private Base {
public:
    using Base::publicFunc;  // 恢复为 public
    using Base::value;       // 恢复为 public
};

多重继承

class Duck : public Flyable, public Swimmable { };

虚继承

class Mammal : virtual public Animal { };
class Bird : virtual public Animal { };
class Bat : public Mammal, public Bird { };
// Bat 中只有一份 Animal 子对象

final 阻止继承(C++11)

class FinalClass final { };  // 不能作为基类
void method() final;          // 虚函数不能被重写

实践

力扣题目:力扣链式结构题(继承关系的链式结构),力扣扩展逻辑题(用继承扩展评分逻辑)。

AI 自检:画出以下继承体系的内存布局,标出每个成员的偏移量:

class A { int a; };
class B : public A { double b; virtual void f(); };
class C : public B { char c; };
// 分析 sizeof(C) 并说明 vptr 的位置

建议先阅读05_面向对象(一)类与对象基础 — 对象内存布局和 this 指针基础;07_面向对象(三)多态与虚函数 — 虚函数表与动态绑定的完整机制。