24-2 C++中的基本继承

既然我们已经从抽象层面讨论了继承的概念,接下来就来探讨它在C++中的具体应用。

C++中的继承发生在类之间。在继承(is-a)关系中,被继承的类称为父类(parent class)、基类(base class)或超类(superclass),而进行继承的类则称为子类(child class)、派生类(derived class)或子类( subclass)。

image

在上图中,Fruit 是父类,Apple 和 Banana 均为子类。

image

在此图中,三角形既是形状的子类,也是直角三角形的父类。

子类既继承父类的行为(成员函数),也继承其属性(成员变量),但需遵守某些访问限制(我们将在后续课程中讲解)。

这些变量和函数成为派生类的成员。

由于子类是完整的类,它们当然可以拥有特定于该类的成员。我们稍后将看到一个示例。


Person 类

以下是一个表示通用人的简单类:

#include <string>
#include <string_view>

class Person
{
// In this example, we're making our members public for simplicity
public:
    std::string m_name{};
    int m_age{};

    Person(std::string_view name = "", int age = 0)
        : m_name{ name }, m_age{ age }
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }

};

由于Person类旨在表示通用人物,我们仅定义了适用于任何类型人物的公共成员。每个人(无论性别、职业等)都有姓名和年龄,因此在此进行了体现。

请注意,本例中所有变量和函数均设为public。此举纯粹是为了保持示例的简洁性,通常我们会将变量设为private。本章后续将探讨访问控制机制及其与继承的交互关系。


棒球运动员类

假设我们要编写一个管理棒球运动员信息的程序。棒球运动员需要包含特定于棒球运动员的信息——例如,我们可能需要存储球员的打击率和击出的本垒打数量。

以下是我们尚未完成的棒球运动员类:

class BaseballPlayer
{
// In this example, we're making our members public for simplicity
public:
    double m_battingAverage{};
    int m_homeRuns{};

    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
    {
    }
};

现在,我们还想记录棒球运动员的姓名和年龄,而这些信息已作为Person类的一部分存在。

为BaseballPlayer添加姓名和年龄有三种方案:

  1. 直接将姓名和年龄作为成员添加到BaseballPlayer类中。这可能是最差的选择,因为我们重复了Person类中已有的代码。对Person的任何更新都必须在BaseballPlayer中同步修改。
  2. 通过组合将Person作为BaseballPlayer的成员。但需自问:“棒球运动员是否拥有个人身份?”答案是否定的,因此这不符合设计范式。
  3. 让BaseballPlayer从Person继承这些属性。请记住继承体现的是“是-一种”关系。棒球运动员是否属于人?答案是肯定的。因此继承在此处是合理选择。

使BaseballPlayer成为派生类

要让BaseballPlayer继承自Person类,语法相当简单。在BaseballPlayer类的声明之后,我们使用冒号、关键字“public”以及要继承的类名。这被称为公共继承。关于公共继承的具体含义,我们将在后续课程中详细探讨。

// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
    double m_battingAverage{};
    int m_homeRuns{};

    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
    {
    }
};

通过推导图,我们的继承关系如下所示:

image

当BaseballPlayer从Person继承时,BaseballPlayer获得了Person的成员函数和变量。此外,BaseballPlayer还定义了两个专属成员:m_battingAverage和m_homeRuns。这很合理,因为这些属性是BaseballPlayer特有的,而非Person的通用属性。

因此,棒球运动员对象将拥有4个成员变量:来自棒球运动员类的m_battingAverage和m_homeRuns,以及来自Person类的m_name和m_age。

这很容易证明:

#include <iostream>
#include <string>
#include <string_view>

class Person
{
public:
    std::string m_name{};
    int m_age{};

    Person(std::string_view name = "", int age = 0)
        : m_name{name}, m_age{age}
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }

};

// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
    double m_battingAverage{};
    int m_homeRuns{};

    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
    {
    }
};

int main()
{
    // Create a new BaseballPlayer object
    BaseballPlayer joe{};
    // Assign it a name (we can do this directly because m_name is public)
    joe.m_name = "Joe";
    // Print out the name
    std::cout << joe.getName() << '\n'; // use the getName() function we've acquired from the Person base class

    return 0;
}

哪个会输出该值:

image

这段代码能够编译并运行,是因为joe是BaseballPlayer类型,而所有BaseballPlayer对象都拥有m_name成员变量以及从Person类继承的getName()成员函数。


员工派生类

现在我们再编写另一个同样继承自Person的类。这次我们将创建一个Employee类。员工“是”人的一种,因此使用继承是恰当的:

// Employee publicly inherits from Person
class Employee: public Person
{
public:
    double m_hourlySalary{};
    long m_employeeID{};

    Employee(double hourlySalary = 0.0, long employeeID = 0)
        : m_hourlySalary{hourlySalary}, m_employeeID{employeeID}
    {
    }

    void printNameAndSalary() const
    {
        std::cout << m_name << ": " << m_hourlySalary << '\n';
    }
};

员工类继承自Person类,继承了m_name和m_age两个成员变量(以及两个访问函数),并新增了两个成员变量和一个专属成员函数。需注意printNameAndSalary()函数同时使用了所属类(Employee::m_hourlySalary)和父类(Person::m_name)的变量。

由此形成的派生关系图如下所示:

image

请注意,尽管员工和棒球运动员都继承自Person类,但它们之间并无直接关联。

以下是使用员工类的完整示例:

#include <iostream>
#include <string>
#include <string_view>

class Person
{
public:
    std::string m_name{};
    int m_age{};

    Person(std::string_view name = "", int age = 0)
        : m_name{name}, m_age{age}
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }

};

// Employee publicly inherits from Person
class Employee: public Person
{
public:
    double m_hourlySalary{};
    long m_employeeID{};

    Employee(double hourlySalary = 0.0, long employeeID = 0)
        : m_hourlySalary{hourlySalary}, m_employeeID{employeeID}
    {
    }

    void printNameAndSalary() const
    {
        std::cout << m_name << ": " << m_hourlySalary << '\n';
    }
};

int main()
{
    Employee frank{20.25, 12345};
    frank.m_name = "Frank"; // we can do this because m_name is public

    frank.printNameAndSalary();

    return 0;
}

这将输出:

image


继承链

可以从一个本身又从其他类派生的类继承。这样做并无特别之处——一切都如前例所示。

例如,我们来编写一个监督员类。监督员是员工,而员工是人。我们已经编写了员工类,因此将其作为监督员的基类:

class Supervisor: public Employee
{
public:
    // This Supervisor can oversee a max of 5 employees
    long m_overseesIDs[5]{};
};

现在我们的推导图如下所示:

image

所有主管对象都继承了员工和人员的函数与变量,并添加了专属的m_overseesIDs成员变量。

通过构建这样的继承链,我们可以创建一组可重用的类:在顶层具有高度通用性,并在每个继承层级逐步变得更具体。


为何此类继承模式如此实用?

从基类继承意味着我们无需在派生类中重新定义基类信息。通过继承机制,派生类自动获得基类的成员函数与成员变量,只需添加所需的额外功能即可。这不仅节省了工作量,还意味着当基类被更新或修改时(例如添加新功能或修复错误),所有派生类将自动继承这些变更!

例如,若在Person类中新增功能,则Employee、Supervisor和BaseballPlayer类将自动获得该功能。若在Employee中新增变量,Supervisor同样能使用该变量。这使我们能以简便、直观且低维护的方式构建新类!


总结

继承机制通过让其他类继承其成员实现类复用。后续课程我们将继续探索其运作原理。

posted @ 2026-01-31 19:08  游翔  阅读(1)  评论(0)    收藏  举报