需要预先学习:


1. 继承

继承(inheritance)让一个新类可以获得另一个类的成员,从而复用代码,并表达"是一种"(is-a)的关系。

学生、教师都是人,都有姓名、年龄,都会打招呼。把这些共性提取到 Person 类里,让 Student、Teacher 从它继承,各自只需写独有的部分——不必把姓名、年龄在每个类里重复一遍。

  • 基类(base class,也叫父类):被继承的类
  • 派生类(derived class,也叫子类):继承别人的类

“学生是一个人”——这种 is-a 关系是继承的核心:派生类对象可以当作基类对象来用。


1.1 继承的语法

继承写在类名后面的冒号处:

class Student : public Person      // Student 公开继承 Person
{
    // 这里只需写 Student 独有的成员
};
#include <iostream>
#include <string>

class Person
{
public:
    std::string m_name;
    int m_age = 0;
    void say_hello() { std::cout << "你好,我是" << m_name << std::endl; }
};

class Student : public Person
{
public:
    std::string m_school;
};

int main()
{
    Student s;
    s.m_name = "张三";               // 继承自 Person 的成员变量
    s.m_age = 15;
    s.m_school = "第一中学";          // Student 自己的成员变量
    s.say_hello();                   // 继承自 Person 的成员函数
    std::cout << "就读于" << s.m_school << std::endl;

    return 0;
}

编译并运行代码后,看到这样的输出:

你好,我是张三
就读于第一中学

Student 的对象同时拥有 Person 的成员和 Student 自己的成员,可以直接使用继承来的成员,就像它们本来就属于 Student 一样。


1.2 继承方式与访问权限

继承时同样要写一个访问权限限定符,它决定基类成员在派生类中的访问权限:

基类成员 public 继承 protected 继承 private 继承
public 仍是 public 变为 protected 变为 private
protected 仍是 protected 仍是 protected 变为 private
private 不可直接访问 不可直接访问 不可直接访问
  • 无论哪种继承方式,基类的 private 成员在派生类中都不可直接访问——但它们仍然存在于派生类对象里,只是必须通过基类的公开接口间接使用
  • public 继承最常用,也最符合 is-a 的语义;protected、private 继承较少使用,本章只以 public 继承为主

protected 与 private 的区别

在 封装 里两者看起来一样(类外部都不可访问),差别在于派生类:

  • private:只有本类的成员函数能访问
  • protected:本类和派生类的成员函数都能访问
#include <iostream>
#include <string>

class Animal
{
public:
    void eat() { std::cout << "吃东西" << std::endl; }

protected:
    std::string m_kind = "动物";     // 派生类可以访问

private:
    int m_weight = 10;               // 派生类也不可访问
};

class Dog : public Animal
{
public:
    void show()
    {
        std::cout << "我是一只" << m_kind << std::endl;     // protected 成员,派生类可以访问
        std::cout << m_weight << std::endl;                // 错误!private 成员,派生类不可访问
    }
};

int main()
{
    Dog d;
    d.show();

    return 0;
}

编译时报错:

error: 'int Animal::m_weight' is private within this context

所以设计基类时,把"希望派生类直接使用、但不希望外界碰"的成员声明为 protected;只希望本类自己使用的成员声明为 private。


1.3 继承中的构造函数与析构函数

派生类对象在创建时,会先构造其中的基类部分,再构造派生类自己的部分——就像盖楼先打地基、再建上层。销毁时的顺序正好相反。

#include <iostream>
#include <string>

class Person
{
public:
    Person(const std::string& name) : m_name(name)
    {
        std::cout << "Person 构造: " << m_name << std::endl;
    }

    ~Person()
    {
        std::cout << "Person 析构: " << m_name << std::endl;
    }

private:
    std::string m_name;
};

class Student : public Person
{
public:
    Student(const std::string& name, const std::string& school)
        : Person(name), m_school(school)         // 在初始化列表中调用基类构造函数
    {
        std::cout << "Student 构造: " << m_school << std::endl;
    }

    ~Student()
    {
        std::cout << "Student 析构: " << m_school << std::endl;
    }

private:
    std::string m_school;
};

int main()
{
    Student s("张三", "第一中学");

    return 0;
}

编译并运行代码后,看到这样的输出:

Person 构造: 张三
Student 构造: 第一中学
Student 析构: 第一中学
Person 析构: 张三

规则:

  1. 创建:先基类构造函数,后派生类构造函数;销毁:先派生类析构函数,后基类析构函数
  2. 派生类构造函数可以在初始化列表中调用基类构造函数,把参数传过去
  3. 如果基类没有默认构造函数,派生类构造函数必须显式调用基类的某个构造函数,否则编译报错
  4. 派生类不继承基类的构造函数;如果不显式写,编译器会自动生成一个默认构造函数,它先调用基类的默认构造函数

1.4 名字隐藏

如果派生类定义了与基类同名的函数,基类的所有同名重载都会被隐藏(name hiding)——即使参数列表不同:

#include <iostream>

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

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

int main()
{
    Derived d;
    d.func();
    d.func(1);           // 错误!Base 的 func(int) 被隐藏了

    return 0;
}

编译时报错:

error: no matching function for call to 'Derived::func(int)'

编译器在派生类里找到 func 这个名字后就不再向上查找,于是基类的重载统统看不见了。有两种解决办法:

  • 用类名限定调用:d.Base::func(1);
  • 在派生类里用 using Base::func; 把基类的所有同名重载都引入进来

1.5 派生类对象的内存布局

派生类对象在内存中,先存放基类部分(基类子对象),再存放派生类新增的成员:

Student 对象的内存布局
+------------------------+
|  Person 部分            |  基类子对象:m_name、m_age
+------------------------+
|  m_school              |  Student 新增成员
+------------------------+

把派生类对象的地址赋给基类指针时,指针指向的正是最前面这段基类子对象。这个布局在 多态 里讨论虚函数表时还会用到。