C++学习笔记DAY4

C++ 面向对象二 —— 继承 · 详细学习笔记

每天我就这样补补补,,,
继承在我看来有点像便捷的CV工程(当然还是本质设计不是这样的hhh)
实际上继承的核心目的只有一个,建模‘is-a'关系+多态替换,就有点像dog is animal而不是dog cpoies Animal
具体的我们后面再看吧


目录

  1. 继承的基本概念
  2. 权限修饰符详解
  3. 三种继承方式
  4. 继承中的对象模型
  5. 继承中的构造与析构顺序
  6. 继承中的同名成员处理
  7. 继承中的同名静态成员处理
  8. 多继承语法
  9. 菱形继承问题
  10. 虚继承——菱形继承的解决方案
  11. 虚继承内存模型深度剖析
  12. 综合案例:图形类
  13. 常见陷阱与要点
  14. 进阶拓展
  15. 速查表

1. 继承的基本概念

1.1 为什么需要继承

当多个类有相同的属性和方法时,会出现大量代码重复。继承可以提取共性到父类中,子类复用父类代码。

问题场景:

Dog 类           Cat 类
├ 属性:姓名       ├ 属性:姓名
├ 属性:年龄       ├ 属性:年龄
├ 属性:颜色       ├ 属性:品种
├ 行为:吃饭       ├ 行为:吃饭
├ 行为:睡觉       ├ 行为:睡觉
└ 行为:看家       └ 行为:抓老鼠

          重复!重复!重复!

解决方案:提取共性到 Animal 父类

         Animal(父类 / 基类 / 超类)
         ├ 属性:姓名
         ├ 属性:年龄
         ├ 行为:吃饭
         └ 行为:睡觉
              │
     ┌────────┴────────┐
     │                 │
   Dog(子类)        Cat(子类)
   ├ 属性:颜色        ├ 属性:品种
   └ 行为:看家        └ 行为:抓老鼠

   ← 复用 + 扩展 →   ← 复用 + 扩展 →

1.2 继承的定义

继承:允许一个类(派生类/子类)继承另一个类(基类/父类)的属性和行为,并可以添加自己的新成员。

基本语法:

class 子类 : 继承方式 父类 {
    // 子类新增的成员
};

完整入门案例:

#include <iostream>
#include <string>
using namespace std;

// ============ 父类(基类) ============
class Animal {
public:
    string m_Name;  // 姓名
    int m_Age;      // 年龄

    void eat() {
        cout << m_Name << " 在吃饭" << endl;
    }

    void sleep() {
        cout << m_Name << " 在睡觉" << endl;
    }
};

// ============ 子类1:Dog ============
class Dog : public Animal {
public:
    string m_Color;  // Dog 特有属性

    void watchHouse() {  // Dog 特有行为
        cout << m_Name << " 在看家" << endl;
    }
};

// ============ 子类2:Cat ============
class Cat : public Animal {
public:
    string m_Breed;  // Cat 特有属性

    void catchMouse() {  // Cat 特有行为
        cout << m_Name << " 在抓老鼠" << endl;
    }
};

int main() {
    Dog dog;
    dog.m_Name = "旺财";   // 从父类继承的属性
    dog.m_Age = 3;         // 从父类继承的属性
    dog.m_Color = "黄色";  // 子类自己的属性
    dog.eat();             // 从父类继承的方法
    dog.sleep();
    dog.watchHouse();      // 子类自己的方法

    cout << "---" << endl;

    Cat cat;
    cat.m_Name = "咪咪";
    cat.m_Age = 2;
    cat.m_Breed = "橘猫";
    cat.eat();
    cat.sleep();
    cat.catchMouse();

    return 0;
}

1.3 继承的作用

作用 说明
代码复用 子类自动获得父类的所有成员,避免重复编码
扩展性 子类可以添加新的属性和方法,或重写父类方法
层次化设计 类之间形成层次结构,便于理解和维护
多态基础 继承是实现多态的前提条件

1.4 继承使用场景

当多个类之间有相同的属性和方法时,将这些共同部分抽取到基类中,其他类通过继承复用。

案例分析:教师与学生

Person(父类)
├ 姓名
├ 年龄
├ 性别
├ 吃饭()
└ 睡觉()
     │
     ├── Teacher(子类)
     │   └ 工号、教书()
     │
     └── Student(子类)
         └ 学号、学习()
class Person {
public:
    string m_Name;
    int m_Age;
    string m_Gender;

    void eat()  { cout << m_Name << " 在吃饭" << endl; }
    void sleep() { cout << m_Name << " 在睡觉" << endl; }
};

class Teacher : public Person {
public:
    int m_TeacherId;
    void teach() { cout << m_Name << " 在教书" << endl; }
};

class Student : public Person {
public:
    int m_StudentId;
    void study() { cout << m_Name << " 在学习" << endl; }
};

1.5 继承的扩展——重写

当父类的功能不能满足子类需求时,子类可以重写父类的方法:

class Animal {
public:
    virtual void speak() {
        cout << "动物在叫" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {  // 重写父类方法
        cout << "旺旺叫!" << endl;
    }
};

注意:重写是多态的前提。这里用到了虚函数,仅占位,后面还会详细展开


2. 权限修饰符详解

2.1 三种权限修饰符

C++ 有三种访问权限修饰符,控制类成员的访问范围:

修饰符 本类 子孙类(派生类) 外部类(类外) 权限大小
public 最高
protected 中等
private 最低

权限大小:private < protected < public

私藏自用,保护传家,公有共享

2.2 代码验证

#include <iostream>
using namespace std;

class Person {
public:
    int m_A;       // 公共权限——任意位置可访问

protected:
    int m_B;       // 保护权限——类内和子类可访问

private:
    int m_C;       // 私有权限——仅类内可访问

public:
    void func() {
        m_A = 10;  // 本类内可以访问 public
        m_B = 20;  // 本类内可以访问 protected
        m_C = 30;  // 本类内可以访问 private
    }
};

// 子类
class Student : public Person {
public:
    void test() {
        m_A = 100;  // ✓ 子类可以访问父类的 public
        m_B = 200;  // ✓ 子类可以访问父类的 protected
        // m_C = 300;  // ✗ 子类不能访问父类的 private
    }
};

int main() {
    Person p;
    p.m_A = 1;    // ✓ 类外可以访问 public
    // p.m_B = 2;  // ✗ 类外不能访问 protected
    // p.m_C = 3;  // ✗ 类外不能访问 private

    return 0;
}

2.3 protected 的特殊价值

protected 是继承体系中最重要的权限修饰符:

┌─────────────────────────────────────────────────┐
│              protected 的意义                      │
├─────────────────────────────────────────────────┤
│  对子类可见(像 public)                            │
│  对外部不可见(像 private)                          │
│                                                   │
│  → 专门为继承设计!                                 │
│  → 父类中希望子类能访问但不希望外部访问的成员        │
│    用 protected 修饰                               │
└─────────────────────────────────────────────────┘

实际应用:

class BankAccount {
public:
    void deposit(double amount) {  // 存款——外部可用
        m_Balance += amount;
    }
    double getBalance() {  // 查询余额——外部可用
        return m_Balance;
    }

protected:
    double m_Balance;  // 余额——子类可用,外部不可直接修改
    void applyInterest() {  // 计算利息——子类可用
        m_Balance *= 1.03;
    }

private:
    int m_AccountPassword;  // 密码——只有本类可用
};

class SavingsAccount : public BankAccount {
public:
    void yearlyUpdate() {
        applyInterest();  // ✓ 子类可以调用 protected 方法
        // m_AccountPassword = 123;  // ✗ 子类不能访问 private
    }
};

int main() {
    SavingsAccount sa;
    sa.deposit(1000);         // ✓ public
    // sa.m_Balance = 0;      // ✗ protected
    // sa.applyInterest();    // ✗ protected
    return 0;
}

2.4 struct vs class 的默认权限

关键字 默认权限(不写修饰符时)
class private
struct public
class A {
    int x;  // 默认 private
};

struct B {
    int x;  // 默认 public
};

// 继承的默认也是一样的:
class C : A {};      // 默认 private 继承
struct D : B {};      // 默认 public 继承

最佳实践:永远显式写明权限修饰符,不依赖默认值。


3. 三种继承方式

3.1 继承方式概述

C++ 支持三种继承方式,它们的区别在于:父类成员在子类中的最高访问权限是什么

继承方式 父类 public 成员 父类 protected 成员 父类 private 成员 效果
public 继承 子类中仍为 public 子类中仍为 protected 子类中不可访问 权限不变(最常用)
protected 继承 子类中降为 protected 子类中仍为 protected 子类中不可访问 public → protected
private 继承 子类中降为 private 子类中降为 private 子类中不可访问 全部降为 private

3.2 核心规则——权限降级

继承方式决定了「降级上限」:

  public 继承    → 权限不变(最宽松)
  protected 继承 → public 降为 protected,其余不变
  private 继承   → 全部降为 private(最严格)

记忆口诀:「取小值」
  父类成员权限 和 继承方式,取两者中更严格的那个。

权限降级图解:

=== public 继承(全部保持)===

父类成员           子类中
public    ──→     public     (不变)
protected ──→     protected  (不变)
private   ──→     不可访问    (隐藏)


=== protected 继承(public 降级)===

父类成员           子类中
public    ──→     protected  (降级!)
protected ──→     protected  (不变)
private   ──→     不可访问    (隐藏)


=== private 继承(全部降级)===

父类成员           子类中
public    ──→     private    (降级!)
protected ──→     private    (降级!)
private   ──→     不可访问    (隐藏)

这个逻辑应该挺好理顺的

3.3 公有继承(public)

class Base {
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

class Son : public Base {
public:
    void test() {
        m_A = 10;  // public 成员,继承后仍为 public
        m_B = 20;  // protected 成员,继承后仍为 protected
        m_C = 30;  // 这里会报错,private 成员,子类不可访问
    }
};

int main() {
    Son s;
    s.m_A = 1;    // 子类中 m_A 仍是 public,类外可访问
    s.m_B = 2;  // protected,类外不可访问
    return 0;
}

公有继承表示 "is-a" 关系:子类就是一种父类。这是最常用的继承方式。

3.4 保护继承(protected)

class Son : protected Base {
public:
    void test() {
        m_A = 10;  // ✓ public 成员,继承后降为 protected
        m_B = 20;  // ✓ protected 成员,继承后仍为 protected
        // m_C = 30;  // ✗ private,不可访问
    }
};

int main() {
    Son s;
    // s.m_A = 1;  // ✗ m_A 降为 protected,类外不可访问了!
    return 0;
}

3.5 私有继承(private)

class Son : private Base {
public:
    void test() {
        m_A = 10;  // ✓ public 成员,继承后降为 private
        m_B = 20;  // ✓ protected 成员,继承后降为 private
        // m_C = 30;  // ✗ private,不可访问
    }
};

// 再往下继承一层
class GrandSon : public Son {
public:
    void test() {
        // m_A = 10;  // ✗ m_A 已降为 private,孙类不可访问
        // m_B = 20;  // ✗ m_B 已降为 private,孙类不可访问
    }
};

私有继承表示 "has-a" / "用...实现" 关系:子类用父类来实现自己的功能,但不暴露继承关系。

3.6 三种继承方式对比总结

┌──────────────────────────────────────────────────────────────┐
│                    继承方式对比速查表                          │
├──────────┬──────────┬──────────────┬──────────────┬──────────┤
│ 继承方式 │ 语义关系  │ public→子类  │ protected→子类 │ 使用频率 │
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
│ public   │ is-a     │ public       │ protected    │ ★★★★★   │
│ protected│ 实现继承  │ protected    │ protected    │ ★☆☆☆☆   │
│ private  │ has-a    │ private      │ private      │ ★★☆☆☆   │
└──────────┴──────────┴──────────────┴──────────────┴──────────┘

最佳实践:95% 以上的场景使用 public 继承。只有在需要限制接口暴露时才考虑 protected/private 继承。


4. 继承中的对象模型

4.1 子类对象的内存布局

子类对象包含了从父类继承的所有成员(包括 private),加上自己的新增成员。

class Base {
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

class Son : public Base {
public:
    int m_D;
};

内存布局:

Son 对象内存:
┌─────────────┐
│  m_A (4字节)  │  ← 从 Base 继承(public)
├─────────────┤
│  m_B (4字节)  │  ← 从 Base 继承(protected)
├─────────────┤
│  m_C (4字节)  │  ← 从 Base 继承(private,隐藏但存在!)
├─────────────┤
│  m_D (4字节)  │  ← Son 自己的成员
└─────────────┘

sizeof(Son) = 16(4 个 int)

4.2 关键结论:private 成员也被继承了

#include <iostream>
using namespace std;

class Base {
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

class Son : public Base {
public:
    int m_D;
};

int main() {
    cout << "sizeof(Base) = " << sizeof(Base) << endl;  // 12
    cout << "sizeof(Son)  = " << sizeof(Son) << endl;   // 16

    // Son 中包含了 m_A + m_B + m_C + m_D = 16 字节
    // 说明 m_C(private)也被继承了!只是编译器隐藏了访问
    return 0;
}

输出:

sizeof(Base) = 12
sizeof(Son)  = 16

重要结论:父类中私有成员也是被子类继承下去了,只是由编译器隐藏后访问不到。它在内存中确实存在,只是编译器在语法层面禁止访问。

4.3 内存对齐

编译器可能会在成员之间插入填充字节,使成员地址对齐到自然边界(通常是其大小的倍数):

class Base {
    char m_C;    // 1字节,但会填充到 sizeof(int)
    int m_A;      // 4字节
};

class Son : public Base {
    double m_D;  // 8字节
};

// sizeof(Son) 可能不是简单的 1+4+8=13
// 而是由于对齐变成更大的值

内存对齐规则因编译器和平台而异,使用 sizeof 运算符获取实际大小。

4.4 查看类内存布局的工具

使用编译器命令查看类的内存布局(以 GCC/Clang 为例):

# GCC
g++ -fdump-lang-class=layout.txt main.cpp

# Clang
clang++ -Xclang -fdump-record-layouts main.cpp

# MSVC (Visual Studio)
# 项目属性 → C/C++ → 命令行 → 添加 /d1reportSingleClassLayoutSon

5. 继承中的构造与析构顺序

5.1 构造与析构顺序

子类对象创建时,会先调用父类的构造函数,再调用子类的构造函数。析构顺序与构造相反。

#include <iostream>
using namespace std;

class Base {
public:
    Base()  { cout << "Base 构造函数!" << endl; }
    ~Base() { cout << "Base 析构函数!" << endl; }
};

class Son : public Base {
public:
    Son()  { cout << "Son 构造函数!" << endl; }
    ~Son() { cout << "Son 析构函数!" << endl; }
};

int main() {
    cout << "=== 创建 Son 对象 ===" << endl;
    Son s;
    cout << "=== main 函数执行中 ===" << endl;
    cout << "=== Son 对象即将销毁 ===" << endl;
    return 0;
}

输出:

=== 创建 Son 对象 ===
Base 构造函数!    ← 先构造父类
Son 构造函数!     ← 再构造子类
=== main 函数执行中 ===
=== Son 对象即将销毁 ===
Son 析构函数!     ← 先析构子类
Base 析构函数!    ← 再析构父类

5.2 顺序图解

构造顺序(创建对象时):

    Base() ──→ Son()
    父类构造      子类构造
    (先)        (后)


析构顺序(销毁对象时):

    ~Son() ──→ ~Base()
    子类析构     父类析构
    (先)        (后)

总结:析构顺序与构造完全相反

5.3 多层继承的构造析构顺序

class Grandpa {
public:
    Grandpa()  { cout << "Grandpa 构造" << endl; }
    ~Grandpa() { cout << "Grandpa 析构" << endl; }
};

class Father : public Grandpa {
public:
    Father()  { cout << "Father 构造" << endl; }
    ~Father() { cout << "Father 析构" << endl; }
};

class Son : public Father {
public:
    Son()  { cout << "Son 构造" << endl; }
    ~Son() { cout << "Son 析构" << endl; }
};

int main() {
    Son s;
    return 0;
}

输出:

Grandpa 构造    ← 最顶层父类先构造
Father 构造     ← 中间层
Son 构造        ← 最底层子类最后构造
Son 析构        ← 最底层子类先析构
Father 析构     ← 中间层
Grandpa 析构    ← 最顶层父类最后析构

规律:构造从最顶层父类开始,逐层向下到子类;析构从子类开始,逐层向上到最顶层父类。

5.4 为什么先构造父类?

子类对象的完整性依赖于父类部分先被正确初始化。子类构造函数可能需要使用父类的成员,如果父类还没构造,子类就可能访问到未初始化的数据。

这里也能感受到看到可以在子类修改父类public的内容

比喻:
  建房子时,必须先打地基(父类),再建框架(子类)
  拆房子时,必须先拆上层(子类),再拆地基(父类)

5.5 子类构造函数中调用父类构造函数

class Base {
public:
    int m_A;
    Base(int a) : m_A(a) {  // 父类有参构造
        cout << "Base(" << a << ") 构造" << endl;
    }
};

class Son : public Base {
public:
    int m_B;
    // 通过初始化列表调用父类有参构造
    Son(int a, int b) : Base(a), m_B(b) {
        cout << "Son(" << a << ", " << b << ") 构造" << endl;
    }
};

int main() {
    Son s(10, 20);
    cout << "m_A = " << s.m_A << ", m_B = " << s.m_B << endl;
    return 0;
}

注意:如果父类没有默认构造函数(无参构造),子类构造函数必须在初始化列表中显式调用父类的有参构造。


6. 继承中的同名成员处理

6.1 同名成员问题

当子类与父类出现同名成员变量或同名成员函数时,子类会隐藏父类的同名成员。

6.2 同名成员变量

#include <iostream>
using namespace std;

class Base {
public:
    int m_A = 100;  // 父类的 m_A
};

class Son : public Base {
public:
    int m_A = 200;  // 子类的 m_A(同名!隐藏了父类的 m_A)
};

int main() {
    Son s;

    // 直接访问 → 访问子类的同名成员
    cout << "Son 中的 m_A = " << s.m_A << endl;    // 200

    // 加父类作用域 → 访问父类的同名成员
    cout << "Base 中的 m_A = " << s.Base::m_A << endl;  // 100

    return 0;
}

输出:

Son 中的 m_A = 200
Base 中的 m_A = 100

6.3 同名成员函数

#include <iostream>
using namespace std;

class Base {
public:
    void func() {
        cout << "Base::func() 调用" << endl;
    }
    void func(int x) {  // 重载版本
        cout << "Base::func(int) 调用, x = " << x << endl;
    }
};

class Son : public Base {
public:
    void func() {  // 同名!隐藏了父类所有 func 版本
        cout << "Son::func() 调用" << endl;
    }
};

int main() {
    Son s;

    // 直接调用 → 调用子类的同名函数
    s.func();           // Son::func() 调用

    // 加父类作用域 → 调用父类的同名函数
    s.Base::func();     // Base::func() 调用

    // 注意:子类的同名函数会隐藏父类所有同名函数(包括重载版本)
    // s.func(10);       // 编译错误!父类的 func(int) 被隐藏了

    // 需要加作用域才能调用被隐藏的父类重载版本
    s.Base::func(10);   // Base::func(int) 调用, x = 10

    return 0;
}

6.4 同名成员处理总结

┌──────────────────────────────────────────────┐
│          继承中同名成员访问规则                │
├──────────────────────────────────────────────┤
│  访问子类同名成员:                           │
│    对象名.成员名          (直接访问)         │
│                                              │
│  访问父类同名成员:                           │
│    对象名.父类名::成员名   (加作用域          │
└──────────────────────────────────────────────┘

6.5 using 声明恢复被隐藏的父类成员

如果子类同名函数隐藏了父类的所有重载版本,可以用 using 声明恢复:

class Base {
public:
    void func()       { cout << "Base::func()" << endl; }
    void func(int x)  { cout << "Base::func(" << x << ")" << endl; }
};

class Son : public Base {
public:
    using Base::func;  // 恢复父类所有 func 重载版本!

    void func() {      // 子类自己的版本
        cout << "Son::func()" << endl;
    }
};

int main() {
    Son s;
    s.func();       // Son::func()
    s.func(10);     // Base::func(10)  ← using 恢复了!
    s.Base::func(); // Base::func()
    return 0;
}

7. 继承中的同名静态成员处理

7.1 静态成员的特点

  • 静态成员变量在类内声明,类外初始化
  • 静态成员属于类级别,所有对象共享
  • 静态成员存储在全局区(静态区),不属于某个具体对象

这里定义的地方必须是全局作用域。当然C++17可以使用inline static在类内直接定义与初始化,这里涉及到多线程安全的问题

7.2 同名静态成员的访问

处理方式和非静态成员一样:直接访问子类的,加作用域访问父类的。但静态成员多了类名访问的方式。

#include <iostream>
using namespace std;

class Base {
public:
    static int m_A;  // 静态成员变量——类内声明
};
int Base::m_A = 100; // 静态成员变量——类外初始化

class Son : public Base {
public:
    static int m_A;  // 同名静态成员
};
int Son::m_A = 200;  // 类外初始化

int main() {
    Son s;

    // ===== 方式一:通过对象访问 =====
    cout << "通过对象访问:" << endl;
    cout << "Son 中的 m_A  = " << s.m_A << endl;         // 200
    cout << "Base 中的 m_A = " << s.Base::m_A << endl;   // 100

    // ===== 方式二:通过类名访问 =====
    cout << "通过类名访问:" << endl;
    cout << "Son 中的 m_A  = " << Son::m_A << endl;       // 200
    cout << "Base 中的 m_A = " << Son::Base::m_A << endl; // 100
    // 也可以:
    cout << "Base 中的 m_A = " << Base::m_A << endl;     // 100

    return 0;
}

7.3 同名静态成员函数

class Base {
public:
    static void func() {
        cout << "Base::static func()" << endl;
    }
};

class Son : public Base {
public:
    static void func() {
        cout << "Son::static func()" << endl;
    }
};

int main() {
    // 通过对象名访问
    Son s;
    s.func();           // Son::static func()
    s.Base::func();     // Base::static func()

    // 通过类名访问
    Son::func();              // Son::static func()
    Son::Base::func();        // Base::static func()

    return 0;
}

7.4 同名静态成员总结

同名静态成员处理方式和非静态完全一样:
  访问子类:直接访问
  访问父类:加作用域

唯一区别:静态成员可以用「类名::成员名」方式访问
  子类类名::成员名        → 子类的
  子类类名::父类名::成员名  → 父类的
  父类名::成员名          → 父类的

8. 多继承语法

8.1 多继承概念

C++ 允许一个类同时继承多个父类。

class 子类 : 继承方式 父类1, 继承方式 父类2, ... {
    // 子类成员
};

注意:C++ 实际开发中不建议频繁使用多继承,因为容易产生二义性问题。

8.2 多继承示例

#include <iostream>
using namespace std;

class Base1 {
public:
    int m_A = 10;
    void func1() { cout << "Base1::func1()" << endl; }
};

class Base2 {
public:
    int m_B = 20;
    int m_A = 30;  // 注意:和 Base1 有同名成员!
    void func2() { cout << "Base2::func2()" << endl; }
};

// 多继承
class Son : public Base1, public Base2 {
public:
    int m_C = 40;
};

int main() {
    Son s;
    cout << "sizeof(Son) = " << sizeof(Son) << endl;  // 16(4个int)

    // 访问不同父类的成员
    cout << "m_A (Base1) = " << s.Base1::m_A << endl;  // 10
    cout << "m_B (Base2) = " << s.m_B << endl;          // 20
    cout << "m_A (Base2) = " << s.Base2::m_A << endl;   // 30
    cout << "m_C (Son)  = " << s.m_C << endl;           // 40

    // 调用不同父类的方法
    s.func1();
    s.func2();

    return 0;
}

8.3 多继承中的同名问题

当多个父类中有同名成员时,子类使用时必须加作用域区分:

class Base1 {
public:
    int m_A = 10;
};

class Base2 {
public:
    int m_A = 20;  // 和 Base1 同名
};

class Son : public Base1, public Base2 {};

int main() {
    Son s;
    // cout << s.m_A << endl;      // 编译错误!二义性!
    cout << s.Base1::m_A << endl;  // 10
    cout << s.Base2::m_A << endl;  // 20
    return 0;
}

8.4 多继承的构造顺序

class Base1 {
public:
    Base1() { cout << "Base1 构造" << endl; }
};

class Base2 {
public:
    Base2() { cout << "Base2 构造" << endl; }
};

// 继承顺序决定构造顺序
class Son : public Base1, public Base2 {
public:
    Son() { cout << "Son 构造" << endl; }
};

int main() {
    Son s;
    return 0;
}

输出:

Base1 构造    ← 按继承列表顺序
Base2 构造
Son 构造

构造顺序由继承列表中父类的出现顺序决定(从左到右),与初始化列表中的顺序无关。

8.5 多继承的争议

多继承的问题:
  1. 二义性 —— 多个父类有同名成员时需要加作用域
  2. 菱形继承 —— 两个父类继承自同一个祖父类,导致数据重复
  3. 复杂性 —— 继承关系难以理解和维护

Java 和 C# 的解决方案:
  → 不支持多继承(类层面),只支持多实现(接口层面)

C++ 的解决方案:
  → 允许多继承,但用虚继承解决菱形问题
  → 实际开发中,更推荐用「组合」替代多继承

9. 菱形继承问题

9.1 什么是菱形继承

菱形继承(Diamond Inheritance),又称钻石继承,是指:

  1. 两个派生类继承同一个基类
  2. 又有某个类同时继承这两个派生类
        Animal(祖父类)
        /    \
       /      \
    Sheep    Tuo    (两个中间类)
       \      /
        \    /
      SheepTuo  (最终子类)

         ◇ 菱形结构

9.2 代码示例

#include <iostream>
using namespace std;

// 祖父类
class Animal {
public:
    int m_Age;  // 年龄
};

// 父类1:羊
class Sheep : public Animal {};

// 父类2:驼
class Tuo : public Animal {};

// 子类:羊驼(多继承)
class SheepTuo : public Sheep, public Tuo {};

int main() {
    SheepTuo st;

    // 问题1:二义性!
    // st.m_Age = 10;  // 编译错误!不确定是 Sheep 的 m_Age 还是 Tuo 的 m_Age

    // 只能加作用域访问
    st.Sheep::m_Age = 8;   // 羊的年龄 = 8
    st.Tuo::m_Age = 28;    // 驼的年龄 = 28

    cout << "Sheep 的 m_Age = " << st.Sheep::m_Age << endl;  // 8
    cout << "Tuo 的 m_Age   = " << st.Tuo::m_Age << endl;    // 28

    // 问题2:数据冗余!
    // 羊驼只需要一个年龄,但继承了两份!
    // sizeof(SheepTuo) = 8(两份 m_Age)
    cout << "sizeof(SheepTuo) = " << sizeof(SheepTuo) << endl;

    return 0;
}

9.3 菱形继承的两大问题

问题一:二义性(Ambiguity)
  SheepTuo 继承了 Sheep 的 m_Age 和 Tuo 的 m_Age
  访问 st.m_Age 时编译器不知道选哪个
  → 编译错误!

问题二:数据冗余(Redundancy)
  羊驼只需要一个年龄属性
  但继承了两份 m_Age(分别来自 Sheep 和 Tuo)
  → 浪费内存,且两个值可能不一致

9.4 菱形继承的内存模型

=== 普通菱形继承的内存结构 ===

SheepTuo 对象:
┌─────────────────────────┐
│  Sheep 部分               │
│    └ m_Age (4字节)        │  ← 从 Animal 继承,第一份
├─────────────────────────┤
│  Tuo 部分                 │
│    └ m_Age (4字节)        │  ← 从 Animal 继承,第二份
├─────────────────────────┤
│  SheepTuo 自己的成员       │
│    (无)                  │
└─────────────────────────┘

sizeof(SheepTuo) = 8
两份 m_Age → 二义性 + 数据冗余!

10. 虚继承——菱形继承的解决方案

10.1 虚继承语法

在中间类(Sheep 和 Tuo)继承 Animal 时,加上 virtual 关键字:

// 祖父类
class Animal {
public:
    int m_Age;
};

// 父类1:虚继承 Animal
class Sheep : virtual public Animal {};

// 父类2:虚继承 Animal
class Tuo : virtual public Animal {};

// 子类:羊驼
class SheepTuo : public Sheep, public Tuo {};

10.2 虚继承后的效果

int main() {
    SheepTuo st;

    // 不再有二义性!直接访问即可
    st.m_Age = 10;
    cout << "m_Age = " << st.m_Age << endl;             // 10
    cout << "Sheep::m_Age = " << st.Sheep::m_Age << endl; // 10(同一份数据)
    cout << "Tuo::m_Age   = " << st.Tuo::m_Age << endl;   // 10(同一份数据)

    cout << "sizeof(SheepTuo) = " << sizeof(SheepTuo) << endl;

    return 0;
}

10.3 虚继承原理

虚继承通过 虚基类指针(vbptr)虚基类表(vbtable) 实现:

vbptr — Virtual Base Pointer(虚基类指针)
  → 每个虚继承的类对象中都有一个
  → 指向虚基类表

vbtable — Virtual Base Table(虚基类表)
  → 记录虚基类成员相对于对象起始地址的偏移量
  → 通过偏移量找到共享的那一份数据

10.4 虚继承后的内存模型

=== 虚继承后的内存结构(64位系统)===

SheepTuo 对象:
┌──────────────────────────────────┐
│ Sheep 部分                        │
│   └ vbptr_Sheep (8字节)           │  ← 虚基类指针
├──────────────────────────────────┤
│ Tuo 部分                          │
│   └ vbptr_Tuo (8字节)            │  ← 虚基类指针
├──────────────────────────────────┤
│ Animal 部分(共享!只有一份!)     │
│   └ m_Age (4字节)                 │  ← 只有一份!
└──────────────────────────────────┘
  内存对齐后: sizeof(SheepTuo) ≈ 24字节

vbptr_Sheep → vbtable → 偏移量 → 找到 m_Age
vbptr_Tuo   → vbtable → 偏移量 → 找到同一个 m_Age

  ┌─────────────────────┐
  │ 两个 vbptr 指向的是  │
  │ 同一份 Animal::m_Age │
  │ → 不再二义!不再冗余!│
  └─────────────────────┘

10.5 虚继承完整代码

#include <iostream>
using namespace std;

// 祖父类
class Animal {
public:
    int m_Age;
    Animal() : m_Age(0) {}
};

// 虚继承!
class Sheep : virtual public Animal {};

// 虚继承!
class Tuo : virtual public Animal {};

class SheepTuo : public Sheep, public Tuo {
public:
    void setAge(int age) {
        m_Age = age;  // 直接访问,不再有二义性
    }
    int getAge() {
        return m_Age;
    }
};

int main() {
    SheepTuo st;
    st.m_Age = 10;  // 直接访问!没有二义性!

    cout << "st.m_Age         = " << st.m_Age << endl;
    cout << "st.Sheep::m_Age  = " << st.Sheep::m_Age << endl;  // 同一份
    cout << "st.Tuo::m_Age    = " << st.Tuo::m_Age << endl;    // 同一份
    cout << "sizeof(SheepTuo) = " << sizeof(SheepTuo) << endl;

    return 0;
}

11. 虚继承内存模型深度剖析

11.1 各类内存大小对比

#include <iostream>
using namespace std;

class Animal {
public:
    int m_Age;     // 4字节
};

class Sheep : virtual public Animal {};
class Tuo   : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

int main() {
    cout << "sizeof(Animal)    = " << sizeof(Animal) << endl;
    cout << "sizeof(Sheep)     = " << sizeof(Sheep) << endl;
    cout << "sizeof(Tuo)       = " << sizeof(Tuo) << endl;
    cout << "sizeof(SheepTuo)  = " << sizeof(SheepTuo) << endl;
    return 0;
}

输出(64位系统,GCC编译器):

sizeof(Animal)    = 4
sizeof(Sheep)     = 16
sizeof(Tuo)       = 16
sizeof(SheepTuo)  = 24

11.2 内存大小分析

Animal:4字节
┌──────────┐
│  m_Age   │  4字节
└──────────┘

Sheep:16字节
┌──────────────┐
│ vbptr_Sheep  │  8字节(虚基类指针)
├──────────────┤
│  m_Age       │  4字节(从 Animal 继承)
├──────────────┤
│  对齐填充    │  4字节(对齐到8的倍数)
└──────────────┘
  总计:16字节

SheepTuo:24字节
┌──────────────┐
│ vbptr_Sheep  │  8字节(Sheep 的虚基类指针)
├──────────────┤
│ vbptr_Tuo    │  8字节(Tuo 的虚基类指针)
├──────────────┤
│  m_Age       │  4字节(只有一份!共享的 Animal 数据)
├──────────────┤
│  对齐填充    │  4字节
└──────────────┘
  总计:24字节

11.3 虚基类指针工作原理

SheepTuo 对象起始地址:0x1000

┌──────────────┐ 0x1000
│ vbptr_Sheep  │ ──→ vbtable_Sheep
├──────────────┤ 0x1008     │ 偏移量 = 16 │
│ vbptr_Tuo    │ ──→ vbtable_Tuo
├──────────────┤ 0x1010     │ 偏移量 = 8  │
│  m_Age       │ ←── 两个 vbptr 通过偏移量都指向这里!
└──────────────┘ 0x1014

vbptr_Sheep 查表得到偏移量 16 → 0x1000 + 16 = 0x1010 → m_Age
vbptr_Tuo   查表得到偏移量 8  → 0x1008 + 8  = 0x1010 → m_Age

→ 同一个 m_Age!不会二义,不会冗余!

11.4 虚继承 vs 普通继承内存对比

=== 普通菱形继承 ===              === 虚继承 ===

SheepTuo (8字节)                 SheepTuo (24字节)
┌────────────┐                   ┌──────────────┐
│ Sheep::m_Age│ 4字节             │ vbptr_Sheep  │ 8字节
├────────────┤                   ├──────────────┤
│ Tuo::m_Age  │ 4字节             │ vbptr_Tuo    │ 8字节
└────────────┘                   ├──────────────┤
  两份 m_Age                      │ m_Age        │ 4字节
  二义 + 冗余                      └──────────────┘
                                   一份 m_Age
                                   无二义 无冗余
                                   但多了指针开销
对比项 普通菱形继承 虚继承
m_Age 份数 2份 1份
二义性
数据冗余
额外开销 vbptr 指针
内存大小 较小(8字节) 较大(24字节)
访问方式 需加作用域 直接访问

虚继承的作用是:在多继承体系中,确保共享的基类子对象只有一份,从而解决菱形继承带来的数据冗余和访问歧义;但它会引入额外的内存和运行成本,因此在业务代码中应尽量避免,优先使用组合或接口继承。

12. 综合案例:图形类

12.1 案例描述

编写图形类 Shape,有方法 draw() 画出图形、area() 计算面积;编写具体图形类 Circle(圆)和 Square(正方形),均继承自 Shape,分别实现自己的 draw()area()

12.2 完整代码

#include <iostream>
#include <cmath>
using namespace std;

// ============ 图形基类(抽象类) ============
class Shape {
public:
    // 画图形
    virtual void draw() {
        cout << "  画一个抽象图形" << endl;
    }

    // 计算面积
    virtual double area() {
        return 0.0;
    }

    // 虚析构
    virtual ~Shape() {}
};

// ============ 圆形 ============
class Circle : public Shape {
private:
    double m_Radius;

public:
    Circle(double r) : m_Radius(r) {}

    void draw() override {
        cout << "  画一个圆形(半径=" << m_Radius << ")" << endl;
        cout << "      ***      " << endl;
        cout << "    *     *    " << endl;
        cout << "   *       *   " << endl;
        cout << "    *     *    " << endl;
        cout << "      ***      " << endl;
    }

    double area() override {
        return 3.14159 * m_Radius * m_Radius;
    }
};

// ============ 正方形 ============
class Square : public Shape {
private:
    double m_Side;

public:
    Square(double s) : m_Side(s) {}

    void draw() override {
        cout << "  画一个正方形(边长=" << m_Side << ")" << endl;
        cout << "  +-------+" << endl;
        cout << "  |       |" << endl;
        cout << "  |       |" << endl;
        cout << "  +-------+" << endl;
    }

    double area() override {
        return m_Side * m_Side;
    }
};

// ============ 拓展:三角形 ============
class Triangle : public Shape {
private:
    double m_Base;
    double m_Height;

public:
    Triangle(double b, double h) : m_Base(b), m_Height(h) {}

    void draw() override {
        cout << "  画一个三角形(底=" << m_Base << ", 高=" << m_Height << ")" << endl;
        cout << "      /\\      " << endl;
        cout << "     /  \\     " << endl;
        cout << "    /    \\    " << endl;
        cout << "   /______\\   " << endl;
    }

    double area() override {
        return 0.5 * m_Base * m_Height;
    }
};

// ============ 多态展示函数 ============
void showShape(Shape &shape) {
    shape.draw();
    cout << "  面积 = " << shape.area() << endl;
    cout << "  ----------" << endl;
}

int main() {
    Circle c(5.0);
    Square sq(4.0);
    Triangle t(6.0, 3.0);

    cout << "===== 图形展示 =====" << endl;
    showShape(c);
    showShape(sq);
    showShape(t);

    return 0;
}

12.3 拓展:图形类层次设计

              Shape(抽象基类)
              ├── draw()    = 0  (纯虚函数)
              ├── area()    = 0  (纯虚函数)
              └── ~Shape()  virtual
                    │
       ┌────────────┼────────────┐
       │            │            │
    Circle       Square      Triangle
    ├── m_Radius  ├── m_Side   ├── m_Base
    ├── draw()    ├── draw()    ├── m_Height
    └── area()    └── area()    ├── draw()
                                └── area()

13. 常见陷阱与要点

13.1 继承的常见陷阱

陷阱1:误以为 private 成员不被继承

class Base {
private:
    int m_A = 10;
};

class Son : public Base {
public:
    void test() {
        // m_A = 20;  // 编译错误,不可访问
        // 但 m_A 在内存中确实存在!sizeof(Son) 包含了它
    }
};
// sizeof(Son) = 4  →  m_A 被继承了,只是访问不到

陷阱2:子类同名函数隐藏父类所有重载版本

class Base {
public:
    void func()       { cout << "func()" << endl; }
    void func(int x)  { cout << "func(int)" << endl; }
};

class Son : public Base {
public:
    void func() { cout << "Son::func()" << endl; }
};

Son s;
s.func();       // Son::func()
// s.func(10); // 编译错误!父类 func(int) 被隐藏了!

陷阱3:构造函数不能被继承

class Base {
public:
    Base(int x) { cout << x << endl; }
};

class Son : public Base {
    // Son 不继承 Base(int) 构造函数!
    // 如果 Base 没有默认构造,Son 必须在初始化列表中调用
public:
    Son() : Base(0) {}  // 必须显式调用
};

陷阱4:友元不能被继承

class Base {
    friend void friendFunc(Base &b);  // 友元函数
};

class Son : public Base {
    // friendFunc 不能直接访问 Son 的私有成员!
    // 友元关系不被继承
};

陷阱5:静态成员属于类,不属于对象

class Base {
public:
    static int s_Count;
};
int Base::s_Count = 0;

class Son : public Base {};

Son s1, s2;
s1.s_Count = 10;
cout << s2.s_Count << endl;     // 10,所有对象共享同一份
cout << Base::s_Count << endl;  // 10
cout << Son::s_Count << endl;   // 10

13.2 高频问题

Q1: 继承的优缺点?

优点 缺点
代码复用 父类变化影响所有子类(耦合)
扩展性强 继承层次过深难以理解
多态的基础 破坏封装(protected 暴露细节)
层次化设计 编译期绑定,灵活性不如组合

Q2: 继承 vs 组合,如何选择?

继承(is-a):Dog is an Animal        → 使用继承
组合(has-a):Car has an Engine      → 使用组合

优先使用组合,而非继承(Effective C++ 条款 32)

class Engine { ... };
class Car {
private:
    Engine m_Engine;  // 组合!Car has an Engine
};

Q3: 菱形继承怎么解决?

虚继承:在中间层类继承顶层基类时加 virtual 关键字,使最终子类只持有一份顶层基类的数据。

Q4: 虚继承的原理是什么?

虚继承通过虚基类指针(vbptr)和虚基类表(vbtable)实现。vbptr 存储在对象中,指向 vbtable,vbtable 记录虚基类成员相对于对象起始地址的偏移量,通过偏移量访问共享的虚基类成员。

Q5: 什么是对象切片?

class Base { public: virtual void f() {} };
class Derived : public Base { public: int m_Extra; };

Derived d;
Base b = d;  // 对象切片!Derived 部分被截断
// b.m_Extra;  // 不存在

值传递时,子类特有的部分会被"切掉",只保留父类部分。


14. 进阶拓展

14.1 继承与组合的对比

// ===== 继承方式(is-a)=====
class Stack : public vector<int> {
    // Stack 是一种 vector → 语义上不太准确
    // 而且 vector 的所有 public 方法都暴露了
public:
    void push(int val) { push_back(val); }
    int pop() { int v = back(); pop_back(); return v; }
};

// ===== 组合方式(has-a)—— 更好的设计 =====
class Stack {
private:
    vector<int> m_Data;  // Stack 有一个 vector
public:
    void push(int val) { m_Data.push_back(val); }
    int pop() {
        int v = m_Data.back();
        m_Data.pop_back();
        return v;
    }
    bool empty() const { return m_Data.empty(); }
    // 只暴露需要的方法,不暴露 vector 的所有接口
};

14.2 私有继承实现组合

私有继承在语义上等同于组合,但不如直接使用成员变量直观:

// 方式一:私有继承(has-a 的另一种实现)
class Stack : private vector<int> {
public:
    void push(int val) { push_back(val); }
    int pop() { int v = back(); pop_back(); return v; }
    using vector<int>::empty;  // 有选择地暴露基类方法
};

// 方式二:成员变量(推荐)
class Stack {
private:
    vector<int> m_Data;
public:
    void push(int val) { m_Data.push_back(val); }
    int pop() { int v = m_Data.back(); m_Data.pop_back(); return v; }
};

原则:能用组合就用组合,只有需要访问基类的 protected 成员或需要虚函数时才考虑私有继承。

14.3 using 改变继承的访问权限(C++11)

class Base {
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

class Son : private Base {  // 私有继承
public:
    using Base::m_A;  // 将 m_A 恢复为 public
    // using Base::m_B;  // 可以恢复为 public(原来是 protected)
    // using Base::m_C;  // 编译错误!private 成员不能用 using 恢复
};

int main() {
    Son s;
    s.m_A = 10;  // ✓ m_A 被恢复为 public
    return 0;
}

14.4 final 禁止继承(C++11)

class FinalClass final {  // 这个类不能被继承
public:
    void func() {}
};

// class Derived : public FinalClass {};  // 编译错误!

14.5 继承中的默认构造函数

class Base {
public:
    Base() = default;            // C++11,让编译器生成默认构造
    Base(int x) : m_X(x) {}
    int m_X;
};

// C++11: using 继承父类构造函数
class Son : public Base {
public:
    using Base::Base;  // 继承父类所有构造函数!
};

Son s1;        // 调用 Base()
Son s2(10);    // 调用 Base(int)

14.6 CRTP — 奇异递归模板模式

// CRTP:编译期多态(静态多态)
template <typename Derived>
class ShapeBase {
public:
    void draw() {
        static_cast<Derived*>(this)->drawImpl();
    }
    double area() {
        return static_cast<Derived*>(this)->areaImpl();
    }
};

class Circle : public ShapeBase<Circle> {
public:
    void drawImpl() { cout << "画圆" << endl; }
    double areaImpl() { return 3.14 * 5 * 5; }
};

// 零开销抽象,编译期确定调用,无需虚函数表

15. 速查表

15.1 继承核心知识速查

知识点 关键内容
继承语法 class 子类 : 继承方式 父类 {}
三种继承方式 public / protected / private
常用继承 public(95%以上场景)
构造顺序 父类 → 子类(从上到下)
析构顺序 子类 → 父类(从下到上,与构造相反)
同名成员 子类隐藏父类,加作用域访问父类
静态同名 同上,多了类名访问方式
多继承 class C : public A, public B {}
菱形继承 两个子类继承同一父类,再被同一孙类继承
虚继承 class Sheep : virtual public Animal {}
虚继承原理 vbptr + vbtable,共享一份数据

15.2 权限修饰符速查

修饰符 本类 子类 外部
public
protected
private

15.3 三种继承方式速查

继承方式 public 成员→子类 protected 成员→子类 private 成员→子类
public public protected 不可访问
protected protected protected 不可访问
private private private 不可访问

15.4 同名成员访问速查

同名非静态成员:
  访问子类:  对象名.成员名
  访问父类:  对象名.父类名::成员名

同名静态成员:
  对象名访问:
    访问子类:  对象名.成员名
    访问父类:  对象名.父类名::成员名
  类名访问:
    访问子类:  子类名::成员名
    访问父类:  子类名::父类名::成员名 或 父类名::成员名

15.5 菱形继承速查

问题:
  Animal ← Sheep ← SheepTuo
  Animal ← Tuo    ← SheepTuo
  → SheepTuo 有两份 Animal 数据 → 二义性 + 冗余

解决:
  class Sheep : virtual public Animal {}  ← 虚继承
  class Tuo   : virtual public Animal {}  ← 虚继承
  → SheepTuo 只有一份 Animal 数据 → 无二义 无冗余

原理:
  vbptr(虚基类指针)→ vbtable(虚基类表)→ 偏移量 → 共享数据

内存开销:
  普通菱形继承:8字节(两份 m_Age)
  虚继承:     24字节(2个 vbptr + 1份 m_Age + 对齐)
  → 空间换正确性

15.6 继承 vs 组合

继承 组合
关系 is-a(是一种) has-a(有一个)
耦合 高(编译期绑定) 低(运行时可替换)
灵活性
封装性 弱(暴露 protected) 强(只暴露需要的方法)
推荐度 在 is-a 时使用 优先使用

15.7 构造析构顺序速查

单继承:
  构造:父 → 子
  析构:子 → 父

多继承:
  构造:按继承列表从左到右
  析构:从右到左

多层继承:
  构造:Grandpa → Father → Son
  析构:Son → Father → Grandpa
posted @ 2026-08-02 18:36  小怪兽zzy  阅读(2)  评论(0)    收藏  举报