编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

      • README
      • Cpp简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针引用
      • 类和对象
      • 继承多态
        • 10.1 继承
          • 10.1.1 继承概念
          • 10.1.2 继承语法
          • 10.1.3 继承访问控制
          • 10.1.4 继承中对象模型
          • 10.1.5 继承中构造和析构顺序
          • 10.1.6 继承同名成员处理方式
          • 10.1.7 继承同名静态成员处理
          • 10.1.8 综合案例与思考
        • 10.2 多重继承
          • 10.2.1 多重继承概念
          • 10.2.2 多重继承语法
          • 10.2.3 多重继承示例
          • 10.2.4 多重继承二义性
          • 10.2.5 菱形继承
          • 10.2.6 虚继承
          • 10.2.7 注意事项
          • 10.2.8 多重继承场景
          • 10.2.9 综合案例与思考
        • 10.3 多态
          • 10.3.1 多态概念
          • 10.3.2 多态语法
          • 10.3.3 多态案例
          • 10.3.4 多态的分类
          • 10.3.5 多态和虚函数
          • 10.3.4 多态和析构函数
          • 10.3.6 综合案例与思考
        • 10.4 抽象类
          • 10.4.1 抽象类概念
          • 10.4.2 抽象类定义
          • 10.4.3 抽象类特点
          • 10.4.4 纯虚函数
          • 10.4.5 派生类实现抽象类
          • 10.4.6 使用抽象类
          • 10.4.7 抽象类与接口
          • 10.4.8 看个案例
          • 10.4.9 综合案例与思考
        • 10.5 综合案例
          • 10.5.1 计算器类
          • 10.5.2 制作饮品
          • 10.5.3 组装电脑
          • 10.6.2 虚函数表与动态绑定
          • 10.6.3 虚继承的底层实现
          • 10.6.4 RTTI与dynamic_cast的代价
        • 10.7 继承和多态训练题
        • 10.8 综合思考题
        • 10.Y 新手陷阱 Top 5
      • 内存模型
      • 动态内存
      • IO和文件
      • 异常处理
      • 线程和锁
      • STL模版
      • 预处理器
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 入门教程
杨充
2026-05-07
目录

继承多态

# 第 10 章 C++ 继承多态

# 目录介绍

  • 10.1 继承
    • 10.1.1 继承概念
    • 10.1.2 继承语法
    • 10.1.3 继承访问控制
    • 10.1.4 继承中对象模型
    • 10.1.5 继承中构造和析构顺序
    • 10.1.6 继承同名成员处理方式
    • 10.1.7 继承同名静态成员处理
    • 10.1.8 综合案例与思考
  • 10.2 多重继承
    • 10.2.1 多重继承概念
    • 10.2.2 多重继承语法
    • 10.2.3 多重继承示例
    • 10.2.4 多重继承二义性
    • 10.2.5 菱形继承
    • 10.2.6 虚继承
    • 10.2.7 注意事项
    • 10.2.8 多重继承场景
    • 10.2.9 综合案例与思考
  • 10.3 多态
    • 10.3.1 多态概念
    • 10.3.2 多态语法
    • 10.3.3 多态案例
    • 10.3.4 多态的分类
    • 10.3.5 多态和虚函数
    • 10.3.6 多态和析构函数
    • 10.3.6 综合案例与思考
  • 10.4 抽象类
    • 10.4.1 抽象类概念
    • 10.4.2 抽象类定义
    • 10.4.3 抽象类特点
    • 10.4.4 纯虚函数
    • 10.4.5 派生类实现抽象类
    • 10.4.6 使用抽象类
    • 10.4.7 抽象类与接口
    • 10.4.8 看个案例
    • 10.4.9 综合案例与思考
  • 10.5 综合案例
    • 10.5.1 计算器类
    • 10.5.2 制作饮品
    • 10.5.3 组装电脑
  • 10.6 继承和多态底层原理
    • 10.6.1 继承的内存模型
    • 10.6.2 虚函数表与动态绑定
    • 10.6.3 虚继承的底层实现
    • 10.6.4 RTTI与dynamic_cast的代价
  • 10.7 继承和多态训练题
  • 10.8 综合思考题

# 10.1 继承

# 10.1.1 继承概念

继承 是面向对象编程(OOP)的核心特性之一,它允许一个类(派生类)基于另一个类(基类)创建,从而复用基类的成员并扩展其功能。

  • 基类(父类):被继承的类。
  • 派生类(子类):继承基类的类。

派生类可以访问基类的成员(根据访问权限),并可以添加新的成员或重写基类的成员函数。

# 10.1.2 继承语法

class BaseClass {
    // 基类成员
};

class DerivedClass : access-specifier BaseClass {
    // 派生类成员
};

访问修饰符:可以是 public、protected 或 private,决定基类成员在派生类中的访问权限。

access-specifier:访问修饰符,可以是 public、protected 或 private,决定基类成员在派生类中的访问权限。

class 派生类名 : 访问修饰符 基类名 {
    // 派生类的成员
};

# 10.1.3 继承访问控制

继承后的可访问性是指派生类(子类)对基类(父类)成员的访问权限(public,protected,private)。

继承方式 public 成员在派生类中 protected 成员在派生类中 private 成员在派生类中
公有继承 public public protected 不可访问
保护继承 protected protected protected 不可访问
私有继承 private private private 不可访问

总结

  • 保护继承将基类的 public 和 protected 成员在派生类中变为 protected 成员。
  • 保护继承适用于需要限制基类成员访问权限的场景。
  • 在实际开发中,保护继承的使用较少,通常优先考虑公有继承或组合(Composition)。

# 10.1.4 继承中对象模型

问题:从父类继承过来的成员,哪些属于子类对象中?

class Base {
public:
    int a;
protected:
    int b;
private:
    int c; //私有成员只是被隐藏了,但是还是会继承下去
};

//公共继承
class Son : public Base {
public:
    int d;
};

void test01() {
    cout << "sizeof int = " << sizeof(int) << endl;
    cout << "sizeof Son = " << sizeof(Son) << endl;
}

int main() {
    test01();
    return 0;
}

然后打印一下结果,如下:

sizeof int = 4
sizeof Son = 16

由结果可知:单继承中,派生类对象包含基类部分和派生类部分。

# 10.1.5 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

问题:父类和子类的构造和析构顺序是谁先谁后?

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

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

void test01() {
    //继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
    Son s;
}

int main() {
    test01();
    return 0;
}

打印结果如下所示:

Base构造函数!
Son构造函数!
Son析构函数!
Base析构函数!

总结:继承中,先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。

# 10.1.6 继承同名成员处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域
class Base {
public:
    Base() {
        a = 100;
    }
    void func() {
        cout << "Base - func()调用" << a << endl;
    }
    void func(int a) {
        cout << "Base - func(int a)调用" << a << endl;
    }
public:
    int a;
};


class Son : public Base {
public:
    Son() {
        a = 200;
    }
    //当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
    //如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
    void func() {
        cout << "Son - func()调用" << a << endl;
    }

public:
    int a;
};

void test01() {
    Son s;
    cout << "Son下的a = " << s.a << endl;
    cout << "Base下的a = " << s.Base::a << endl;
    s.func();
    s.Base::func();
    s.Base::func(10);
}

int main() {
    test01();
    return EXIT_SUCCESS;
}

打印结果如下所示:

Son下的a = 200
Base下的a = 100
Son - func()调用200
Base - func()调用100
Base - func(int a)调用10

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

# 10.1.7 继承同名静态成员处理

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  1. 访问子类同名成员 直接访问即可
  2. 访问父类同名成员 需要加作用域
class Base {
public:
    static void func() {
        cout << "Base - static void func()" << a << endl;
    }
    static void func(int a) {
        cout << "Base - static void func(int a)" << a << endl;
    }
    static int a;
};

int Base::a = 100;

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

int Son::a = 200;

//同名成员属性
void test01() {
    //通过对象访问
    cout << "通过对象访问: " << endl;
    Son s;
    cout << "Son  下 a = " << s.a << endl;
    cout << "Base 下 a = " << s.Base::a << endl;

    //通过类名访问
    cout << "通过类名访问: " << endl;
    cout << "Son  下 a = " << Son::a << endl;
    cout << "Base 下 a = " << Son::Base::a << endl;
}

//同名成员函数
void test02() {
    //通过对象访问
    cout << "通过对象访问: " << endl;
    Son s;
    s.func();
    s.Base::func();

    cout << "通过类名访问: " << endl;
    Son::func();
    Son::Base::func();
    //出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
    Son::Base::func(100);
}

int main() {
    //test01();
    test02();
    return 0;
}

# 10.1.8 综合案例与思考

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

class Person {
public:
    string name;
    int age;
    Person(const string& n, int a) : name(n), age(a) {
        cout << "Person构造: " << name << endl;
    }
    ~Person() { cout << "Person析构: " << name << endl; }
    
    void introduce() { cout << "我是" << name << ", " << age << "岁" << endl; }
    void introduce(const string& title) {
        cout << title << name << ", " << age << "岁" << endl;
    }
protected:
    string idCard = "身份证号保护中";
private:
    string password = "密码私有";
};

class Student : public Person {
public:
    string school;
    int grade;
    
    Student(const string& n, int a, const string& s, int g)
        : Person(n, a), school(s), grade(g) {
        cout << "Student构造: " << name << endl;
    }
    ~Student() { cout << "Student析构: " << name << endl; }
    
    // 同名成员函数,隐藏了父类的 introduce
    void introduce() {
        cout << school << grade << "年级学生 " << name << endl;
        // 访问 protected 成员
        cout << "  [保护信息] " << idCard << endl;
        // cout << password << endl;  // 错误!private 不可访问
    }
    
    void callParentIntroduce() {
        Person::introduce();           // 访问父类同名函数
        Person::introduce("同学: ");   // 访问父类重载版本
    }
};

// 静态成员继承示例
class Base {
public:
    static int count;
    static void showCount() { cout << "Base::count = " << count << endl; }
};
int Base::count = 100;

class Derived : public Base {
public:
    static int count;  // 同名静态成员
    static void showCount() { cout << "Derived::count = " << count << endl; }
};
int Derived::count = 200;

int main() {
    cout << "=== 构造析构顺序 ===" << endl;
    Student stu("张三", 15, "清华附中", 9);
    
    cout << "\n=== 继承中对象模型 ===" << endl;
    cout << "sizeof(Person) = " << sizeof(Person) << endl;
    cout << "sizeof(Student) = " << sizeof(Student) << endl;
    
    cout << "\n=== 同名成员处理 ===" << endl;
    stu.introduce();             // 子类版本
    stu.callParentIntroduce();   // 通过作用域访问父类版本
    
    cout << "\n=== 同名静态成员 ===" << endl;
    Derived::showCount();         // Derived的
    Derived::Base::showCount();   // 通过作用域访问Base的
    
    cout << "\n=== 析构顺序(与构造相反)===" << endl;
    return 0;
}

案例知识融合:本案例将继承的核心知识融为一体:继承语法与访问控制(public/protected/private成员在子类中的可见性)、继承中的对象模型(子类包含父类所有成员,sizeof验证)、构造析构顺序(先父后子构造,先子后父析构)、同名成员处理(子类隐藏父类同名函数,通过作用域访问父类)以及同名静态成员的处理方式。

思考题:

  1. 如果把 class Student : public Person 改为 class Student : private Person,外部还能通过 stu.name 访问吗?protected 继承呢?
  2. 子类 introduce() 隐藏了父类所有同名函数(包括重载版本),这是设计还是缺陷?如何用 using 声明解决?
  3. 父类的 private 成员在子类中是"不可访问"还是"不存在"?通过 sizeof 如何验证?

# 10.2 多重继承

# 10.2.1 多重继承概念

多重继承是指一个类可以从多个基类继承属性和行为。与单继承(一个类只能继承一个基类)不同,多重继承允许一个类同时继承多个基类,从而组合多个类的功能。

# 10.2.2 多重继承语法

class DerivedClass : access-specifier BaseClass1, access-specifier BaseClass2, ... {
    // 派生类的成员
};
  • DerivedClass:派生类。
  • BaseClass1, BaseClass2, ...:多个基类。
  • access-specifier:访问修饰符(public、protected 或 private)。

多继承可能会引发父类中有同名成员出现,需要加作用域区分。

# 10.2.3 多重继承示例

#include <iostream>

// 基类1
class Animal {
public:
    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};

// 基类2
class Mammal {
public:
    void breathe() {
        std::cout << "Mammal is breathing." << std::endl;
    }
};

// 派生类,继承自 Animal 和 Mammal
class Dog : public Animal, public Mammal {
public:
    void bark() {
        std::cout << "Dog is barking." << std::endl;
    }
};

int main() {
    Dog dog;
    dog.eat();      // 继承自 Animal
    dog.breathe();  // 继承自 Mammal
    dog.bark();     // 派生类自己的方法

    return 0;
}

输出:

Animal is eating.
Mammal is breathing.
Dog is barking.

多重继承特点

  1. 组合多个类的功能:派生类可以继承多个基类的成员函数和成员变量。
  2. 访问控制:通过访问修饰符(public、protected、private)控制基类成员的访问权限。
  3. 构造函数调用顺序:基类的构造函数按照继承顺序调用,析构函数按照相反顺序调用。
  4. 可能引发问题:多重继承可能导致菱形继承问题和命名冲突。

# 10.2.4 多重继承二义性

在C++的多重继承中,当派生类从多个基类中继承相同的成员函数或成员变量时,可能会导致二义性问题。这种情况下,编译器无法确定应该使用哪个基类的成员,从而导致编译错误。

二义性问题主要有两种情况:

  1. 成员函数二义性:当派生类从多个基类中继承了相同的成员函数时,如果派生类直接调用该成员函数,编译器无法确定应该使用哪个基类的成员函数。这种情况下,可以使用作用域解析运算符(::)来指定要调用的基类成员函数。
  2. 成员变量二义性:当派生类从多个基类中继承了相同的成员变量时,如果派生类直接访问该成员变量,编译器无法确定应该使用哪个基类的成员变量。这种情况下,可以使用作用域解析运算符(::)来指定要访问的基类成员变量。
class Base1 {
public:
    void display() {
        std::cout << "Base1 display()" << std::endl;
    }
    Base1() {
        std::cout << "Base1 constructor" << std::endl;
    }
};

class Base2 {
public:
    void display() {
        std::cout << "Base2 display()" << std::endl;
    }
    Base2() {
        std::cout << "Base2 constructor" << std::endl;
    }
};

class DerivedYc : public Base1, public Base2 {
public:
    void display() {
        std::cout << "Derived display()" << std::endl;
    }
    DerivedYc() {
        std::cout << "Derived constructor" << std::endl;
    }
};

void test() {
    DerivedYc d;
    d.display();  // 编译错误,二义性调用
    d.Base1::display();  // 使用作用域解析运算符调用 Base1 类中的 display() 函数
    d.Base2::display();  // 使用作用域解析运算符调用 Base2 类中的 display() 函数
}

int main() {
    test();
    return 0;
}

在这个示例中,Derived类从Base1和Base2类中继承了相同的display()函数。

当通过Derived类的对象调用display()函数时,会导致编译错误,因为编译器无法确定应该使用哪个基类的成员函数。

为了解决这个问题,可以使用作用域解析运算符来指定要调用的基类成员函数。

# 10.2.5 菱形继承

菱形继承概念: 两个派生类继承同一个基类,又有某个类同时继承者两个派生类,导致某个类中包含多个相同的基类子对象。这种继承被称为菱形继承,或者钻石继承。

菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

这个时候类 Animal 中的成员变量和成员函数继承到类 SheepTuo 中变成了两份,一份来自 Animal-->Sheep-->SheepTuo 这条路径,另一份来自 Animal-->Tuo-->SheepTuo 这条路径。

示例:

class Animal {
public:
    int age;
};

//此时公共的父类Animal称为虚基类
class Sheep : public Animal {};
class Tuo   : public Animal {};
class SheepTuo : public Sheep, public Tuo {};

int main() {
    SheepTuo st;
    st.Sheep::age = 100;
    st.Tuo::age = 200;

    cout << "st.Sheep::age = " << st.Sheep::age << endl;
    cout << "st.Tuo::age = " <<  st.Tuo::age << endl;
    //cout << "st.age = " << st.age << endl; //因为SheepTuo的父类Sheep和Tuo都有age,编译器不知道选用哪一个,所以产生了歧义。
    return 0;
}

打印结果如下所示:

st.Sheep::age = 100
st.Tuo::age = 200

总结:菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义。并且调用变量时存在二义性(编译器不知道选择那个)。

# 10.2.6 虚继承

虚继承(Virtual Inheritance)是一种继承方式,用于解决多继承中的菱形继承问题(Diamond Inheritance Problem)。

菱形继承问题是指当一个派生类同时继承自两个或多个基类,而这些基类又共同继承自同一个基类时,派生类中会存在多个对同一个基类成员的拷贝,导致二义性和冗余。

虚继承通过在继承关系中使用virtual关键字来解决菱形继承问题。在虚继承中,派生类对共同基类的继承是虚拟的,只会保留一个共同基类的实例,从而避免了多个拷贝和二义性。

class Animal {
public:
    int age;
};

//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo   : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

int main() {
    SheepTuo st;
    st.Sheep::age = 100;
    st.Tuo::age = 200;

    cout << "st.Sheep::age = " << st.Sheep::age << endl;
    cout << "st.Tuo::age = " <<  st.Tuo::age << endl;
    cout << "st.age = " << st.age << endl;
    return 0;
}

打印结果如下所示:

st.Sheep::age = 200
st.Tuo::age = 200
st.age = 200

# 10.2.7 注意事项

  1. 命名冲突:如果多个基类中有同名成员,派生类需要使用作用域解析运算符 :: 来指定访问哪个基类的成员。
    class Base1 {
    public:
        void print() { std::cout << "Base1" << std::endl; }
    };
    
    class Base2 {
    public:
        void print() { std::cout << "Base2" << std::endl; }
    };
    
    class Derived : public Base1, public Base2 {
    public:
        void show() {
            Base1::print(); // 调用 Base1 的 print
            Base2::print(); // 调用 Base2 的 print
        }
    };
    
  2. 设计复杂性:多重继承会增加代码的复杂性,建议谨慎使用。
  3. 优先使用组合:如果多重继承导致设计复杂,可以考虑使用组合(将类作为成员变量)代替继承。

# 10.2.8 多重继承场景

  1. 接口实现:一个类可以实现多个接口(抽象类)。
  2. 功能组合:将多个类的功能组合到一个类中。
  3. 框架设计:在框架设计中,多重继承可以用于扩展功能。

# 10.2.9 综合案例与思考

#include <iostream>
using namespace std;

// 虚基类
class Device {
public:
    int id;
    Device(int i) : id(i) { cout << "Device构造 id=" << id << endl; }
    virtual ~Device() { cout << "Device析构" << endl; }
    void powerOn() { cout << "设备" << id << "开机" << endl; }
};

// 虚继承 —— 解决菱形继承
class Printer : virtual public Device {
public:
    Printer(int i) : Device(i) { cout << "Printer构造" << endl; }
    void print() { cout << "打印文档" << endl; }
};

class Scanner : virtual public Device {
public:
    Scanner(int i) : Device(i) { cout << "Scanner构造" << endl; }
    void scan() { cout << "扫描文档" << endl; }
};

// 多重继承 + 虚继承
class MultiFunctionDevice : public Printer, public Scanner {
public:
    // 虚继承时,最终派生类负责初始化虚基类
    MultiFunctionDevice(int i) : Device(i), Printer(i), Scanner(i) {
        cout << "MultiFunctionDevice构造" << endl;
    }
    
    void copy() {
        scan();
        print();
        cout << "复印完成" << endl;
    }
};

int main() {
    cout << "=== 创建多功能设备 ===" << endl;
    MultiFunctionDevice mfd(1001);
    
    cout << "\n=== 使用功能 ===" << endl;
    mfd.powerOn();  // 只有一份 Device,无二义性
    mfd.print();
    mfd.scan();
    mfd.copy();
    
    cout << "\n=== 验证只有一份基类数据 ===" << endl;
    mfd.Printer::id = 100;
    cout << "通过Scanner访问id: " << mfd.Scanner::id << endl;  // 同一个id
    cout << "直接访问id: " << mfd.id << endl;
    
    cout << "\n=== 析构 ===" << endl;
    return 0;
}

案例知识融合:本案例通过多功能办公设备模拟了多重继承的典型场景:MultiFunctionDevice 同时继承 Printer 和 Scanner,它们共同继承自 Device,形成菱形继承。使用 virtual 虚继承确保只有一份 Device 基类实例,消除二义性。验证了通过不同路径访问同一个 id 变量的结果一致性,以及虚继承中由最终派生类负责初始化虚基类的规则。

思考题:

  1. 如果去掉 virtual 关键字,mfd.id 的访问会出现什么错误?为什么?
  2. 虚继承有性能开销吗?底层是通过什么机制(虚基类指针/虚基类表)实现的?
  3. 实际开发中,除了虚继承,还有什么设计方式可以避免菱形继承问题?

# 10.3 多态

# 10.3.1 多态概念

多态是指通过基类的指针或引用调用派生类的重写函数,实现“一个接口,多种实现”。多态分为两种:

  1. 编译时多态:通过函数重载和运算符重载实现。
  2. 运行时多态:通过虚函数和继承实现。

面向对象程序设计语言有封装、继承和多态三种机制,这三种机制能够有效提高程序的可读性、可扩充性和可重用性。

多态(polymorphism):同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

多态可以分为编译时的多态和运行时的多态。

  1. 前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;
  2. 后者则和继承、虚函数等概念有关。

# 10.3.2 多态语法

  • 虚函数是在基类中声明为 virtual 的函数,允许派生类重写(Override)该函数。
  • 通过基类指针或引用调用虚函数时,实际调用的是派生类的重写函数。
class BaseClass {
public:
    virtual void function() {
        // 基类实现
    }
};

class DerivedClass : public BaseClass {
public:
    void function() override {
        // 派生类实现
    }
};

# 10.3.3 多态案例

多态满足条件:1.有继承关系;2.子类重写父类中的虚函数

多态使用条件:父类指针或引用指向子类对象

class Animal {
public:
    void eat() {
        cout << "动物在吃东西" << endl;
    }
    //Speak函数就是虚函数
    //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
    virtual void speak() {
        cout << "动物在说话" << endl;
    }
};

class Cat : public Animal {
public:
    void eat() {
        cout << "小猫吃鱼" << endl;
    }
    void speak() {
        cout << "小猫在说话" << endl;
    }
};

class Dog : public Animal {
public:
    void eat() {
        cout << "小狗吃屎" << endl;
    }
    void speak() {
        cout << "小狗在说话" << endl;
    }
};

//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal){
    animal.eat();
    animal.speak();
}

int main() {
    Cat cat;
    DoSpeak(cat);

    Dog dog;
    DoSpeak(dog);
    return 0;
}

打印结果如下所示:

动物在吃东西
小猫在说话
动物在吃东西
小狗在说话

# 10.3.4 多态的分类

静态多态和动态多态区别:

  1. 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  2. 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
#include <iostream>
using namespace std;

class Base {
public:
    // 普通函数 - 静态绑定
    void normalFunc() {
        cout << "Base::normalFunc()" << endl;
    }
    
    // 虚函数 - 动态绑定  
    virtual void virtualFunc() {
        cout << "Base::virtualFunc()" << endl;
    }
};

class Derived : public Base {
public:
    void normalFunc() {
        cout << "Derived::normalFunc()" << endl;
    }
    
    void virtualFunc() override {
        cout << "Derived::virtualFunc()" << endl;
    }
};

void demonstrateBinding() {
    cout << "=== 绑定机制演示 ===" << endl;
    Derived derived;
    Base* basePtr = &derived;
    
    // 静态绑定 - 编译时确定
    cout << "静态绑定结果: ";
    basePtr->normalFunc();  // 输出: Base::normalFunc()
    
    // 动态绑定 - 运行时确定  
    cout << "动态绑定结果: ";
    basePtr->virtualFunc(); // 输出: Derived::virtualFunc()
    
    cout << "\n=== 对象直接调用 ===" << endl;
    derived.normalFunc();   // 输出: Derived::normalFunc()
    derived.virtualFunc();  // 输出: Derived::virtualFunc()
}

两种多态区别

特性 静态多态 动态多态
绑定时间 编译时绑定(早绑定) 运行时绑定(晚绑定)
实现机制 函数重载、运算符重载、模板 虚函数、继承
性能 高效,无运行时开销 有轻微性能开销(虚表查找)
灵活性 相对固定 高度灵活,支持运行时决策
关键字 不需要特殊关键字 需要virtual关键字

# 10.3.5 多态和虚函数

虚函数:基类中提供默认实现,派生类可以选择性地重写(override)该函数。

#include <iostream>

class Animal {
public:
    virtual void speak() {
        std::cout << "Animal speaks." << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog barks." << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Cat meows." << std::endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->speak(); // 输出: Dog barks.
    animal2->speak(); // 输出: Cat meows.

    delete animal1;
    delete animal2;
    return 0;
}

# 10.3.4 多态和析构函数

# 10.3.6 综合案例与思考

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// 基类:图形
class Shape {
public:
    virtual double area() const = 0;      // 纯虚函数
    virtual string name() const = 0;
    virtual void draw() const {
        cout << "绘制 " << name() << " 面积=" << area() << endl;
    }
    // 虚析构函数(多态基类必须)
    virtual ~Shape() {
        cout << name() << " 析构" << endl;
    }
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
    string name() const override { return "圆"; }
};

class Rectangle : public Shape {
    double w, h;
public:
    Rectangle(double w, double h) : w(w), h(h) {}
    double area() const override { return w * h; }
    string name() const override { return "矩形"; }
};

class Triangle : public Shape {
    double base, height;
public:
    Triangle(double b, double h) : base(b), height(h) {}
    double area() const override { return 0.5 * base * height; }
    string name() const override { return "三角形"; }
};

// 多态调用:统一接口,不同行为
void printShapeInfo(const Shape& shape) {
    shape.draw();  // 运行时决定调用哪个版本
}

int main() {
    // 使用基类指针管理不同派生类
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>(5.0));
    shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
    shapes.push_back(make_unique<Triangle>(3.0, 8.0));
    
    cout << "=== 多态遍历 ===" << endl;
    double totalArea = 0;
    for (const auto& shape : shapes) {
        printShapeInfo(*shape);
        totalArea += shape->area();
    }
    cout << "总面积: " << totalArea << endl;
    
    // 验证静态绑定 vs 动态绑定
    cout << "\n=== 绑定方式对比 ===" << endl;
    Circle c(10);
    Shape* ptr = &c;
    Shape& ref = c;
    ptr->draw();  // 动态绑定 -> Circle::draw()
    ref.draw();   // 动态绑定 -> Circle::draw()
    
    cout << "\n=== 虚析构演示 ===" << endl;
    Shape* p = new Circle(1);
    delete p;  // 虚析构确保正确调用 Circle 的析构函数
    
    cout << "\n=== shapes 容器销毁 ===" << endl;
    return 0;
}

案例知识融合:本案例通过图形系统综合展示了多态的核心:纯虚函数定义接口(Shape是抽象类不可实例化)、派生类用 override 重写虚函数、基类指针/引用调用派生类函数实现运行时多态、虚析构函数确保通过基类指针 delete 时正确释放派生类资源。使用 unique_ptr 管理多态对象生命周期是现代C++的最佳实践。

思考题:

  1. 如果 Shape 的析构函数不是虚函数,delete p 会发生什么?为什么多态基类必须有虚析构?
  2. override 关键字的作用是什么?如果派生类的函数签名和基类不一致(如忘了 const),不加 override 会怎样?
  3. 虚函数的底层实现机制是什么(虚函数表vptr/vtable)?每个对象的 vptr 占多少空间?

# 10.4 抽象类

# 10.4.1 抽象类概念

抽象类是一种特殊的类,它不能被实例化,只能作为其他类的基类。抽象类的主要目的是为派生类提供一个统一的接口或行为规范。

抽象类通常包含至少一个纯虚函数,这使得它无法直接创建对象。

# 10.4.2 抽象类定义

抽象类通过声明纯虚函数来定义。纯虚函数是一个没有实现的虚函数,其声明以 = 0 结尾。

class AbstractClass {
public:
    // 纯虚函数
    virtual void pureVirtualFunction() = 0;

    // 普通成员函数
    void normalFunction() {
        std::cout << "This is a normal function." << std::endl;
    }
};

# 10.4.3 抽象类特点

  1. 不能实例化:抽象类不能直接创建对象。例如:
    AbstractClass obj; // 错误:无法实例化抽象类
    
  2. 必须被继承:抽象类通常作为基类,派生类必须实现所有的纯虚函数。
  3. 可以包含普通成员函数:抽象类可以包含普通成员函数和成员变量。
  4. 可以包含虚函数:抽象类可以包含虚函数,这些虚函数可以有默认实现。

# 10.4.4 纯虚函数

纯虚函数是抽象类的核心特性。它没有函数体,派生类必须重写它。

virtual 返回类型 函数名(参数列表) = 0;

示例

class Shape {
public:
    // 纯虚函数
    virtual void draw() = 0;
};

虚函数是"可以重写"的函数(有默认实现),纯虚函数是"必须重写"的函数(无实现,定义接口规范)。

特性 虚函数 (Virtual Function) 纯虚函数 (Pure Virtual Function)
语法 virtual void func(); virtual void func() = 0;
实现 基类中有函数体 基类中无函数体(=0)
重写要求 派生类可选重写 派生类必须重写
类性质 可以是具体类 使类成为抽象类
实例化 可以创建对象 不能创建抽象类对象

# 10.4.5 派生类实现抽象类

派生类必须实现抽象类中的所有纯虚函数,否则派生类也会成为抽象类。

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a square." << std::endl;
    }
};

# 10.4.6 使用抽象类

抽象类通常用于多态场景,通过基类指针或引用调用派生类的实现。

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Square();

    shape1->draw(); // 输出: Drawing a circle.
    shape2->draw(); // 输出: Drawing a square.

    delete shape1;
    delete shape2;

    return 0;
}

# 10.4.7 抽象类与接口

在 C++ 中,没有专门的 interface 关键字,但可以通过抽象类实现接口的功能。通常,接口是一个只包含纯虚函数的抽象类。

class IPrintable {
public:
    virtual void print() = 0;
    virtual ~IPrintable() = default; // 虚析构函数
};

# 10.4.8 看个案例

这里面三种写法有什么区别?当然实际情况只可以用其中一种。这里只是用来分析。

class IPollStrategy {
public:
  virtual ~IPollStrategy(){};
  virtual ~IPollStrategy();
  virtual ~IPollStrategy() = 0;
};

1. virtual ~IPollStrategy(){};

含义: 这是一个 有实现的虚析构函数。它被声明为 virtual,表示这是一个虚函数,允许派生类重写它。它有一个空的函数体 {},表示这是一个具体的实现。

作用:确保派生类对象在销毁时能够正确调用析构函数。允许 IPollStrategy 类被实例化(即可以创建 IPollStrategy 的对象)。

class IPollStrategy {
public:
    virtual ~IPollStrategy(){};
};

class Derived : public IPollStrategy {
public:
    ~Derived() override {
        // 可以选择是否重写析构函数
    }
};

int main() {
    IPollStrategy* obj = new Derived();
    delete obj; // 正确调用析构函数
    return 0;
}

2. virtual ~IPollStrategy();

含义: 这是一个 纯虚析构函数的声明。它被声明为 virtual,表示这是一个虚函数,允许派生类重写它。它没有函数体,表示这是一个纯虚函数。

作用: 使 IPollStrategy 类成为 抽象类,不能直接实例化。强制派生类实现析构函数。

注意事项:纯虚析构函数 必须在类外提供实现,否则会导致链接错误。

class IPollStrategy {
public:
    virtual ~IPollStrategy() = 0; // 纯虚析构函数
};

// 在类外提供纯虚析构函数的实现
IPollStrategy::~IPollStrategy() {}

class Derived : public IPollStrategy {
public:
    ~Derived() override {
        // 必须实现析构函数
    }
};

int main() {
    // IPollStrategy* obj = new IPollStrategy(); // 错误:不能实例化抽象类
    IPollStrategy* obj = new Derived();
    delete obj; // 正确调用析构函数
    return 0;
}

3. virtual ~IPollStrategy() = 0;

含义: 这是一个 纯虚析构函数的定义。它被声明为 virtual,表示这是一个虚函数,允许派生类重写它。= 0 表示这是一个纯虚函数。

作用: 使 IPollStrategy 类成为 抽象类,不能直接实例化。强制派生类实现析构函数。

注意事项: 纯虚析构函数 必须在类外提供实现,否则会导致链接错误。

class IPollStrategy {
public:
    virtual ~IPollStrategy() = 0; // 纯虚析构函数
};

// 在类外提供纯虚析构函数的实现
IPollStrategy::~IPollStrategy() {}

class Derived : public IPollStrategy {
public:
    ~Derived() override {
        // 必须实现析构函数
    }
};

int main() {
    // IPollStrategy* obj = new IPollStrategy(); // 错误:不能实例化抽象类
    IPollStrategy* obj = new Derived();
    delete obj; // 正确调用析构函数
    return 0;
}

三种写法的区别

特性 virtual ~IPollStrategy(){}; virtual ~IPollStrategy(); virtual ~IPollStrategy() = 0;
是否有实现 有实现(空函数体 {}) 无实现(纯虚函数) 无实现(纯虚函数)
是否为抽象类 不是抽象类,可以实例化 是抽象类,不能实例化 是抽象类,不能实例化
派生类是否需要实现 派生类可以选择是否重写析构函数 派生类必须实现析构函数 派生类必须实现析构函数
是否需要在类外实现 不需要 需要 需要

virtual ~IPollStrategy(); 和 virtual ~IPollStrategy() = 0;:两者本质上是相同的,但 = 0 是更标准的写法,明确表示纯虚函数。

在实际开发中,通常使用 virtual ~IPollStrategy() = 0; 来定义抽象类,因为它更清晰且符合 C++ 的语法规范。

# 10.4.9 综合案例与思考

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// 接口(纯抽象类):可序列化
class ISerializable {
public:
    virtual string serialize() const = 0;
    virtual ~ISerializable() = default;
};

// 接口:可打印
class IPrintable {
public:
    virtual void print() const = 0;
    virtual ~IPrintable() = default;
};

// 抽象基类:包含部分实现
class Document : public ISerializable, public IPrintable {
protected:
    string title;
    string author;
public:
    Document(const string& t, const string& a) : title(t), author(a) {}
    
    // 实现 IPrintable(通用部分)
    void print() const override {
        cout << "[" << getType() << "] " << title << " by " << author << endl;
    }
    
    // 留给子类实现的纯虚函数
    virtual string getType() const = 0;
    
    virtual ~Document() = default;
};

class PDFDocument : public Document {
    int pages;
public:
    PDFDocument(const string& t, const string& a, int p)
        : Document(t, a), pages(p) {}
    
    string getType() const override { return "PDF"; }
    
    string serialize() const override {
        return "PDF|" + title + "|" + author + "|pages=" + to_string(pages);
    }
};

class WordDocument : public Document {
    bool hasMacros;
public:
    WordDocument(const string& t, const string& a, bool m)
        : Document(t, a), hasMacros(m) {}
    
    string getType() const override { return "Word"; }
    
    string serialize() const override {
        return "Word|" + title + "|" + author + "|macros=" + (hasMacros ? "yes" : "no");
    }
};

int main() {
    // Document doc("test", "yc");  // 错误!抽象类不能实例化
    
    vector<unique_ptr<Document>> docs;
    docs.push_back(make_unique<PDFDocument>("C++入门", "张三", 300));
    docs.push_back(make_unique<WordDocument>("设计文档", "李四", true));
    
    cout << "=== 多态打印 ===" << endl;
    for (const auto& doc : docs) {
        doc->print();
    }
    
    cout << "\n=== 序列化 ===" << endl;
    for (const auto& doc : docs) {
        cout << doc->serialize() << endl;
    }
    
    // 通过接口类型使用
    cout << "\n=== 接口多态 ===" << endl;
    ISerializable* serializable = docs[0].get();
    cout << serializable->serialize() << endl;
    
    return 0;
}

案例知识融合:本案例展示了抽象类与接口在实际设计中的应用。ISerializable 和 IPrintable 是纯接口(只含纯虚函数),Document 是抽象基类(含部分实现+纯虚函数),PDFDocument 和 WordDocument 是具体实现类。这种设计遵循了"依赖抽象而非具体"的原则,新增文档类型只需继承 Document 并实现纯虚函数,无需修改已有代码(开闭原则)。

思考题:

  1. 如果 PDFDocument 没有实现 serialize(),会发生什么?它自己能被实例化吗?
  2. C++ 没有 interface 关键字,但可以用纯抽象类模拟接口。与 Java 的 interface 相比,C++ 的方式有什么优势和劣势?
  3. 虚析构函数写成 virtual ~ISerializable() = default; 和写成 virtual ~ISerializable() {} 有什么区别?

# 10.5 综合案例

# 10.5.1 计算器类

案例描述:分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:1.代码组织结构清晰;2.可读性强;3.利于前期和后期的扩展以及维护

示例:普通写法

class Calculator {
public:
    int getResult(string oper) {
        if (oper == "+") {
            return num1 + num2;
        } else if (oper == "-") {
            return num1 - num2;
        } else if (oper == "*") {
            return num1 * num2;
        }
        //如果要提供新的运算,需要修改源码
        return 0;
    }
public:
    int num1;
    int num2;
};

void test01() {
    //普通实现测试
    Calculator c;
    c.num1 = 10;
    c.num2 = 10;
    cout << c.num1 << " + " << c.num2 << " = " << c.getResult("+") << endl;
    cout << c.num1 << " - " << c.num2 << " = " << c.getResult("-") << endl;
    cout << c.num1 << " * " << c.num2 << " = " << c.getResult("*") << endl;
}

示例:多态技术

//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator {
public :
    virtual int getResult() {
        return 0;
    }

    int num1;
    int num2;
};

//加法计算器
class AddCalculator : public AbstractCalculator {
public:
    int getResult() {
        return num1 + num2;
    }
};

//减法计算器
class SubCalculator : public AbstractCalculator {
public:
    int getResult() {
        return num1 - num2;
    }
};

//乘法计算器
class MulCalculator : public AbstractCalculator {
public:
    int getResult() {
        return num1 * num2;
    }
};

void test02() {
    //创建加法计算器
    AbstractCalculator *abc = new AddCalculator;
    abc->num1 = 10;
    abc->num2 = 10;
    cout << abc->num1 << " + " << abc->num2 << " = " << abc->getResult() << endl;
    delete abc;  //用完了记得销毁

    //创建减法计算器
    abc = new SubCalculator;
    abc->num1 = 10;
    abc->num2 = 10;
    cout << abc->num1 << " - " << abc->num2 << " = " << abc->getResult() << endl;
    delete abc;

    //创建乘法计算器
    abc = new MulCalculator;
    abc->num1 = 10;
    abc->num2 = 10;
    cout << abc->num1 << " * " << abc->num2 << " = " << abc->getResult() << endl;
    delete abc;
}

# 10.5.2 制作饮品

案例描述:制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡,茶叶,奶茶等。

//抽象制作饮品
class AbstractDrinking {
public:
    //烧水
    virtual void Boil() = 0;
    //冲泡
    virtual void Brew() = 0;
    //倒入杯中
    virtual void PourInCup() = 0;
    //加入辅料
    virtual void PutSomething() = 0;
    //规定流程
    void MakeDrink() {
        Boil();
        Brew();
        PourInCup();
        PutSomething();
    }
};

//制作咖啡
class Coffee : public AbstractDrinking {
public:
    //烧水
    virtual void Boil() {
        cout << "煮农夫山泉!" << endl;
    }
    //冲泡
    virtual void Brew() {
        cout << "冲泡咖啡!" << endl;
    }
    //倒入杯中
    virtual void PourInCup() {
        cout << "将咖啡倒入杯中!" << endl;
    }
    //加入辅料
    virtual void PutSomething() {
        cout << "加入牛奶!" << endl;
    }
};

//制作茶水
class Tea : public AbstractDrinking {
public:
    //烧水
    virtual void Boil() {
        cout << "煮自来水!" << endl;
    }
    //冲泡
    virtual void Brew() {
        cout << "冲泡茶叶!" << endl;
    }
    //倒入杯中
    virtual void PourInCup() {
        cout << "将茶水倒入杯中!" << endl;
    }
    //加入辅料
    virtual void PutSomething() {
        cout << "加入枸杞!" << endl;
    }
};

//业务函数
void DoWork(AbstractDrinking *drink) {
    drink->MakeDrink();
    //delete drink; //报错, delete called on 'AbstractDrinking' that is abstract but has non-virtual destructor
}

void test() {
    DoWork(new Coffee);
    cout << "--------------" << endl;
    DoWork(new Tea);
}

int main() {
    test();
    return 0;
}

打印结果如下所示:

煮农夫山泉!
冲泡咖啡!
将咖啡倒入杯中!
加入牛奶!
--------------
煮自来水!
冲泡茶叶!
将茶水倒入杯中!
加入枸杞!

# 10.5.3 组装电脑

案例描述:

  1. 电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)
  2. 将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
  3. 创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
  4. 测试时组装三台不同的电脑进行工作
#include<iostream>
using namespace std;

//抽象CPU类
class CPU
{
public:
	//抽象的计算函数
	virtual void calculate() = 0;
};

//抽象显卡类
class VideoCard
{
public:
	//抽象的显示函数
	virtual void display() = 0;
};

//抽象内存条类
class Memory
{
public:
	//抽象的存储函数
	virtual void storage() = 0;
};

//电脑类
class Computer
{
public:
	Computer(CPU * cpu, VideoCard * vc, Memory * mem)
	{
		m_cpu = cpu;
		m_vc = vc;
		m_mem = mem;
	}

	//提供工作的函数
	void work()
	{
		//让零件工作起来,调用接口
		m_cpu->calculate();

		m_vc->display();

		m_mem->storage();
	}

	//提供析构函数 释放3个电脑零件
	~Computer()
	{

		//释放CPU零件
		if (m_cpu != NULL)
		{
			delete m_cpu;
			m_cpu = NULL;
		}

		//释放显卡零件
		if (m_vc != NULL)
		{
			delete m_vc;
			m_vc = NULL;
		}

		//释放内存条零件
		if (m_mem != NULL)
		{
			delete m_mem;
			m_mem = NULL;
		}
	}

private:

	CPU * m_cpu; //CPU的零件指针
	VideoCard * m_vc; //显卡零件指针
	Memory * m_mem; //内存条零件指针
};

//具体厂商
//Intel厂商
class IntelCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Intel的CPU开始计算了!" << endl;
	}
};

class IntelVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Intel的显卡开始显示了!" << endl;
	}
};

class IntelMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Intel的内存条开始存储了!" << endl;
	}
};

//Lenovo厂商
class LenovoCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Lenovo的CPU开始计算了!" << endl;
	}
};

class LenovoVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Lenovo的显卡开始显示了!" << endl;
	}
};

class LenovoMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Lenovo的内存条开始存储了!" << endl;
	}
};


void test01()
{
	//第一台电脑零件
	CPU * intelCpu = new IntelCPU;
	VideoCard * intelCard = new IntelVideoCard;
	Memory * intelMem = new IntelMemory;

	cout << "第一台电脑开始工作:" << endl;
	//创建第一台电脑
	Computer * computer1 = new Computer(intelCpu, intelCard, intelMem);
	computer1->work();
	delete computer1;

	cout << "-----------------------" << endl;
	cout << "第二台电脑开始工作:" << endl;
	//第二台电脑组装
	Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);;
	computer2->work();
	delete computer2;

	cout << "-----------------------" << endl;
	cout << "第三台电脑开始工作:" << endl;
	//第三台电脑组装
	Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);;
	computer3->work();
	delete computer3;

}


## 10.6 继承和多态底层原理

### 10.6.1 继承的内存模型

**派生类对象在内存中包含完整的基类子对象**:

```cpp
class Base {
    int x;      // 4字节
    double y;   // 8字节
};

class Derived : public Base {
    int z;      // 4字节
};

内存布局:

Derived对象的内存:
偏移量    内容            说明
0x00     x (int)         来自Base,4字节
0x04     padding         对齐到8字节边界
0x08     y (double)      来自Base,8字节
0x10     z (int)         Derived自己的,4字节
0x14     padding         对齐到8字节(sizeof = 24)

关键点:基类子对象在前,派生类新增成员在后。这保证了将Derived*转换为Base*时,指针值不需要调整(对于单继承)。

多重继承的内存布局:

class A { int a; };           // sizeof = 4
class B { double b; };        // sizeof = 8
class C : public A, public B { int c; };

// C的内存布局:
// [A子对象: a(4)] [padding(4)] [B子对象: b(8)] [c(4)] [padding(4)]
// 总大小 = 24

多重继承中,将C*转换为B*时,编译器需要调整指针偏移:

C obj;
B* bp = &obj;  // bp = &obj + sizeof(A) + padding = &obj + 8
// bp 和 &obj 的值不同!

这就是为什么多重继承下的指针转换不是简单的类型转换,编译器必须计算正确的偏移量。

# 10.6.2 虚函数表与动态绑定

虚函数的底层——vptr和vtable:

class Animal {
public:
    virtual void speak() { cout << "..." << endl; }
    virtual void eat() { cout << "eating" << endl; }
    int age;
};

class Cat : public Animal {
public:
    void speak() override { cout << "meow" << endl; }
    // eat() 没有重写,继承基类版本
};

编译器生成的数据结构:

Animal的vtable(虚函数表,编译期生成,存储在只读数据段):
+--------+----------------------------+
| slot 0 | &Animal::speak()           |
| slot 1 | &Animal::eat()             |
+--------+----------------------------+

Cat的vtable:
+--------+----------------------------+
| slot 0 | &Cat::speak()    ← 重写了   |
| slot 1 | &Animal::eat()   ← 继承的   |
+--------+----------------------------+

Animal对象的内存布局:
+--------+----------------------------+
| vptr   | → 指向Animal的vtable        | 8字节
| age    | 具体值                       | 4字节
+--------+----------------------------+

Cat对象的内存布局:
+--------+----------------------------+
| vptr   | → 指向Cat的vtable           | 8字节(构造时设置)
| age    | 具体值                       | 4字节
+--------+----------------------------+

虚函数调用的汇编过程:

Animal* p = new Cat();
p->speak();  // 调用哪个speak?
; p->speak() 的汇编:
mov    rax, QWORD PTR [rdi]       ; 1. 从对象开头读取vptr
mov    rax, QWORD PTR [rax]       ; 2. 从vtable中读取第0个函数指针
call   rax                         ; 3. 间接调用

这就是动态绑定的三步:①读vptr → ②查vtable → ③间接调用。相比直接调用(call 函数地址),多了两次内存访问,这就是虚函数的运行时开销。

构造函数中设置vptr:

Cat() {
    // 编译器在构造函数开头插入:
    this->vptr = &Cat_vtable;
}

继承链上每个构造函数都会设置vptr。Base的构造函数先将vptr设为Base_vtable,然后Cat的构造函数再改为Cat_vtable。这就是为什么在构造函数中调用虚函数不会触发多态——此时vptr还指向当前类的vtable。

# 10.6.3 虚继承的底层实现

普通菱形继承的内存问题:

class A { int a; };
class B : public A { int b; };
class C : public A { int c; };
class D : public B, public C { int d; };

// D的内存布局(无虚继承):
// [B子对象: [A子对象: a] b] [C子对象: [A子对象: a] c] [d]
// a出现了两次!

虚继承通过虚基类指针(vbptr)解决:

class B : virtual public A { int b; };
class C : virtual public A { int c; };
class D : public B, public C { int d; };

// D的内存布局(虚继承):
// [B子对象: vbptr_B, b] [C子对象: vbptr_C, c] [d] [共享的A子对象: a]
  • vbptr(Virtual Base Pointer)指向一个虚基类表(vbtable)
  • 虚基类表中存储了从当前位置到虚基类子对象的偏移量
  • 虚基类子对象被放在对象末尾,所有继承路径共享这一份
  • 访问虚基类成员时,需要通过vbptr查表计算偏移,有额外开销

这就是为什么虚继承有性能代价:每次访问虚基类成员都需要额外的间接寻址。

# 10.6.4 RTTI与dynamic_cast的代价

RTTI(Run-Time Type Information):

C++在vtable中存储了类型信息(type_info对象),typeid和dynamic_cast依赖于此。

Animal* p = new Cat();
cout << typeid(*p).name() << endl;  // 输出Cat的类型名

Cat* cp = dynamic_cast<Cat*>(p);  // 安全向下转换

dynamic_cast的底层过程:

  1. 通过vptr找到vtable
  2. 从vtable中获取type_info
  3. 沿继承链遍历,检查目标类型是否是实际类型的基类或本身
  4. 如果是多重继承,还需要计算指针偏移

dynamic_cast的时间复杂度与继承链深度相关,在性能关键路径上应该避免使用。替代方案包括:

  • 虚函数(让多态自然工作,不需要判断类型)
  • static_cast(如果你确定类型,零开销但不安全)
  • 访问者模式(双重分发,避免类型判断)

# 10.7 继承和多态训练题

训练题1:实现图形继承体系

要求:设计一个图形继承体系,基类Shape提供面积和周长的纯虚函数接口,实现Circle、Rectangle、Triangle三个派生类,并用多态遍历计算总面积。

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

class Shape {
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    virtual string type() const = 0;
    virtual ~Shape() = default;

    void describe() const {
        cout << type() << ": 面积=" << area()
             << " 周长=" << perimeter() << endl;
    }
};

class Circle : public Shape {
    double r_;
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return M_PI * r_ * r_; }
    double perimeter() const override { return 2 * M_PI * r_; }
    string type() const override { return "圆"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    double perimeter() const override { return 2 * (w_ + h_); }
    string type() const override { return "矩形"; }
};

class Triangle : public Shape {
    double a_, b_, c_;
public:
    Triangle(double a, double b, double c) : a_(a), b_(b), c_(c) {}
    double area() const override {
        double s = (a_ + b_ + c_) / 2;
        return sqrt(s * (s - a_) * (s - b_) * (s - c_));
    }
    double perimeter() const override { return a_ + b_ + c_; }
    string type() const override { return "三角形"; }
};

int main() {
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>(5));
    shapes.push_back(make_unique<Rectangle>(4, 6));
    shapes.push_back(make_unique<Triangle>(3, 4, 5));

    double totalArea = 0;
    for (const auto& s : shapes) {
        s->describe();
        totalArea += s->area();
    }
    cout << "总面积: " << totalArea << endl;

    return 0;
}

练习重点:纯虚函数定义接口、override重写、unique_ptr管理多态对象、虚析构函数。


训练题2:观察虚函数表

要求:编写代码验证vptr和vtable的存在,观察虚函数调用的间接性。

#include <iostream>
using namespace std;

class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    int data = 42;
};

class Derived : public Base {
public:
    void func1() override { cout << "Derived::func1" << endl; }
    virtual void func3() { cout << "Derived::func3" << endl; }
};

int main() {
    Base b;
    Derived d;

    // 1. vptr在对象开头
    cout << "=== vptr位置 ===" << endl;
    cout << "Base对象地址:   " << &b << endl;
    cout << "Base::data地址: " << &b.data << endl;
    cout << "vptr占据了前" << ((char*)&b.data - (char*)&b) << "字节" << endl;

    // 2. 不同类的vptr不同
    cout << "\n=== vptr值(vtable地址)===" << endl;
    void* vptr_base = *(void**)&b;
    void* vptr_derived = *(void**)&d;
    cout << "Base的vtable:    " << vptr_base << endl;
    cout << "Derived的vtable: " << vptr_derived << endl;
    cout << "vtable不同? " << (vptr_base != vptr_derived ? "是" : "否") << endl;

    // 3. 通过vtable手动调用虚函数
    cout << "\n=== 手动通过vtable调用 ===" << endl;
    using FuncPtr = void(*)(void*);
    void** vtable = *(void***)&d;
    FuncPtr f1 = (FuncPtr)vtable[0];  // func1
    FuncPtr f2 = (FuncPtr)vtable[1];  // func2
    f1(&d);  // Derived::func1
    f2(&d);  // Base::func2(未重写)

    // 4. sizeof验证vptr占用空间
    cout << "\n=== sizeof验证 ===" << endl;
    cout << "sizeof(Base) = " << sizeof(Base) << " (vptr8 + data4 + pad4 = 16)" << endl;
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;

    return 0;
}

练习重点:理解vptr在对象内存中的位置、不同类有不同的vtable、通过vtable指针手动调用虚函数验证动态绑定机制。


训练题3:模板方法模式实践

要求:使用继承和多态实现模板方法设计模式——基类定义算法框架,派生类实现具体步骤。

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

// 模板方法基类:数据处理流水线
class DataProcessor {
public:
    // 模板方法:定义算法框架(不可重写)
    void process(const string& input) {
        cout << "=== 开始处理 ===" << endl;
        string data = readData(input);
        string parsed = parseData(data);
        string result = transformData(parsed);
        outputResult(result);
        cout << "=== 处理完成 ===" << endl;
    }

    virtual ~DataProcessor() = default;

protected:
    // 这些步骤由派生类实现
    virtual string readData(const string& source) = 0;
    virtual string parseData(const string& raw) = 0;
    virtual string transformData(const string& parsed) = 0;

    // 钩子方法:有默认实现,派生类可选择重写
    virtual void outputResult(const string& result) {
        cout << "输出: " << result << endl;
    }
};

class CsvProcessor : public DataProcessor {
protected:
    string readData(const string& source) override {
        cout << "读取CSV: " << source << endl;
        return "name,age\nAlice,30\nBob,25";
    }

    string parseData(const string& raw) override {
        cout << "解析CSV为结构化数据" << endl;
        return "[{name:Alice,age:30},{name:Bob,age:25}]";
    }

    string transformData(const string& parsed) override {
        cout << "过滤age>28的记录" << endl;
        return "[{name:Alice,age:30}]";
    }
};

class JsonProcessor : public DataProcessor {
protected:
    string readData(const string& source) override {
        cout << "读取JSON: " << source << endl;
        return R"({"users": [{"name": "Charlie"}]})";
    }

    string parseData(const string& raw) override {
        cout << "解析JSON" << endl;
        return "[Charlie]";
    }

    string transformData(const string& parsed) override {
        cout << "转换为大写" << endl;
        return "[CHARLIE]";
    }

    void outputResult(const string& result) override {
        cout << "[JSON格式输出] " << result << endl;
    }
};

int main() {
    CsvProcessor csv;
    csv.process("data.csv");

    cout << endl;

    JsonProcessor json;
    json.process("data.json");

    // 多态调用
    cout << "\n=== 多态调用 ===" << endl;
    DataProcessor* processors[] = {&csv, &json};
    for (auto* p : processors) {
        p->process("source");
        cout << endl;
    }

    return 0;
}

练习重点:模板方法模式(基类定义框架+派生类实现步骤)、纯虚函数与钩子方法的区别、多态在设计模式中的应用。

# 10.8 综合思考题

  1. 虚函数的性能代价:虚函数调用比普通函数调用多了两次内存间接访问(读vptr + 读vtable slot)。在现代CPU上,这通常只有1-2个时钟周期的额外开销,但虚函数调用无法被内联,这才是真正的性能瓶颈。请思考:为什么虚函数不能内联?编译器的"去虚拟化(devirtualization)"优化是如何在某些场景下消除虚函数开销的?

  2. CRTP(奇异递归模板模式)——静态多态:CRTP通过模板实现编译期多态,避免虚函数开销:template<class T> class Base { void interface() { static_cast<T*>(this)->impl(); } }。请对比CRTP和虚函数多态的优缺点。CRTP适合什么场景?为什么标准库的std::enable_shared_from_this使用CRTP?

  3. C++20的Concepts vs 虚函数:C++20引入的Concepts可以约束模板参数必须满足某些接口,类似于"编译期接口"。请思考:Concepts能否完全替代虚函数?它们分别适用于哪些场景?"编译期多态"和"运行期多态"的根本区别是什么?

  4. 多重继承争议:Java和C#只支持单继承(接口除外),Go只支持组合,而C++支持多重继承。请分析多重继承的利弊。为什么现代语言倾向于避免多重继承?C++中有什么最佳实践来安全使用多重继承?(提示:只从接口类多重继承,最多一个实现类)

# 10.Y 新手陷阱 Top 5

# 陷阱 说明
1 基类析构非 virtual delete base_ptr; 不调用派生类析构 → 资源泄漏
2 构造/析构里调用虚函数 调到的是当前类版本,不是子类(对象未完全构造)
3 切片(slicing) Base b = derived; 派生类信息丢失;用引用/指针传
4 多重继承的菱形继承 用虚继承 virtual public,且最派生类负责构造虚基类
5 不写 override 基类签名变化时编译器无法帮你检查
上次更新: 2026/06/28, 17:55:19
类和对象
内存模型

← 类和对象 内存模型→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式