Menu
CoddyTech

虚继承

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 54 课,共 104 课。

当一个类继承自两个共享同一个基类的类时,就会出现菱形问题。如果不进行特殊处理,派生类最终会包含两份公共基类的副本,导致歧义并浪费内存。

class Animal {
public:
    int age;
};

class Mammal : public Animal {};
class Bird : public Animal {};

class Bat : public Mammal, public Bird {};

Bat b;
b.age = 5;  // 错误:存在歧义,指的是哪个 'age'?

Bat 类包含两个独立的 Animal 子对象:一个通过 Mammal,另一个通过 Bird。这形成了菱形继承图,这个问题也因此得名。

虚继承通过确保公共基类只有一份来解决这个问题。从共享基类继承时,添加 virtual 关键字:

class Animal {
public:
    int age;
    Animal(int a = 0) : age(a) {}
};

class Mammal : virtual public Animal {
public:
    Mammal(int a = 0) : Animal(a) {}
};

class Bird : virtual public Animal {
public:
    Bird(int a = 0) : Animal(a) {}
};

class Bat : public Mammal, public Bird {
public:
    Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};

Bat b(5);
b.age = 10;  // 可以正常运行!只有一个 'age'。

请注意,Bat 必须在其构造函数中直接初始化 Animal。使用虚继承时,无论中间类是什么,最派生类都负责构造虚基类。

challenge icon

挑战

简单

让我们构建一个员工管理系统,展示虚拟继承如何解决菱形问题。你将创建一个继承层级,其中 TeamLead 同时继承自 Developer 和 Manager,而这两个类都共享同一个基类 Employee。

你需要将代码分为 4 个文件:

  • Employee.h:定义公共基类 Employee,其中包含:
    • 受保护的 std::string name 和 int id
    • 一个接收这两个值并打印以下内容的构造函数:Employee [<name>] hired with ID <id>
    • 一个公共 getInfo() 方法,打印以下内容:Employee: <name> (ID: <id>)
    • 一个虚析构函数,打印以下内容:Employee [<name>] record closed
  • Developer.h:定义一个通过虚公共继承自 Employee 的 Developer 类:
    • 一个受保护的 std::string language 成员
    • 一个接收 name、id 和 language 的构造函数:将 name 和 id 传递给 Employee,保存 language,并打印以下内容:Developer [<name>] specializes in <language>
    • 一个公共 code() 方法,打印以下内容:<name> is coding in <language>
    • 一个析构函数,打印以下内容:Developer [<name>] signed off
  • Manager.h:定义一个通过虚公共继承自 Employee 的 Manager 类:
    • 一个受保护的 int teamSize 成员
    • 一个接收 name、id 和 team size 的构造函数:将 name 和 id 传递给 Employee,保存 team size,并打印以下内容:Manager [<name>] leads a team of <teamSize>
    • 一个公共 manage() 方法,打印以下内容:<name> is managing <teamSize> people
    • 一个析构函数,打印以下内容:Manager [<name>] stepped down
  • main.cpp:读取 4 个输入(每个输入单独占一行):
    1. 姓名(字符串)
    2. 员工 ID(整数)
    3. 编程语言(字符串)
    4. 团队人数(整数)

    定义一个公共继承自 Developer 和 Manager 的 TeamLead 类:

    • 一个接收全部 4 个参数的构造函数,必须先直接初始化 Employee(虚基类),然后初始化 Developer 和 Manager
    • 构造函数应打印:TeamLead [<name>] ready to lead and code!
    • 一个按顺序调用 getInfo()、code() 和 manage() 的 showRole() 方法
    • 一个析构函数,打印以下内容:TeamLead [<name>] promoted out

    在一个代码块作用域内创建一个 TeamLead 对象,调用 showRole(),然后让它离开作用域。代码块结束后,打印:Organization restructured!

例如,输入 Alice、101、C++ 和 5 时:

Employee [Alice] hired with ID 101
Developer [Alice] specializes in C++
Manager [Alice] leads a team of 5
TeamLead [Alice] ready to lead and code!
Employee: Alice (ID: 101)
Alice is coding in C++
Alice is managing 5 people
TeamLead [Alice] promoted out
Manager [Alice] stepped down
Developer [Alice] signed off
Employee [Alice] record closed
Organization restructured!

注意,Employee 构造函数只调用一次,Employee 析构函数也只调用一次:虚拟继承确保共享基类只有一个副本。TeamLead 必须直接初始化 Employee,因为在虚拟继承中,最派生类负责构造虚基类。

亲手试试

#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"

using namespace std;

// TODO: 定义 TeamLead 类,公开继承 Developer 和 Manager
// 记住:使用虚继承时,TeamLead 必须直接初始化 Employee(虚基类)
class TeamLead : public Developer, public Manager {
public:
    // TODO: 实现接收 name、id、language 和 teamSize 的构造函数
    // 必须先初始化 Employee(虚基类),再初始化 Developer,最后初始化 Manager
    // 应输出:TeamLead [<name>] ready to lead and code!
    TeamLead(const std::string& name, int id, const std::string& language, int teamSize)
        : Employee(name, id),
          Developer(name, id, language),
          Manager(name, id, teamSize) {
        // TODO: 输出构造函数消息
    }

    // TODO: 实现 showRole() 方法
    // 应依次调用 getInfo()、code() 和 manage()
    void showRole() {
        // TODO: 调用这三个方法
    }

    // TODO: 实现析构函数
    // 应输出:TeamLead [<name>] promoted out
    ~TeamLead() {
        // TODO: 输出析构函数消息
    }
};

int main() {
    // 读取输入
    string name;
    int id;
    string language;
    int teamSize;

    getline(cin, name);
    cin >> id;
    cin.ignore();
    getline(cin, language);
    cin >> teamSize;

    // TODO:在块作用域内创建一个 TeamLead 对象
    // 调用 showRole(),然后让对象离开作用域
    {
        // TODO:创建 TeamLead 对象并调用 showRole()
    }

    // 在代码块结束后输出最终消息
    cout << "Organization restructured!" << endl;

    return 0;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器