C++ 继承

在实际开发中,我们经常需要定义多个相似的类。比如同时定义 Student(学生)和 Teacher(老师)两个类,它们都包含姓名属性,也都需要做自我介绍。如果在每个类里都把相同的成员重复写一遍,不仅代码啰嗦,后期维护起来也非常麻烦。

在 C++ 中,我们可以把多个类共同的部分提取出来,单独定义成一个公共类,再让其他类去 “继承(Inheritance)” 它。这样在设计类与对象时,新类既能直接复用已有的代码,又能在此基础上添加自己特有的新成员。

C++ 继承的基本用法

在 C++ 中,被继承的那个原有的类称为 “基类(Base Class)”,平时也常叫做 “父类”。通过继承创建出来的新类则称为 “派生类(Derived Class)”,平时也常叫做 “子类”。

语法:

class 派生类名 : public 基类名 {
    // 派生类新增的成员
};

说明:

想要让派生类继承基类的成员,我们只需要在派生类名后面加上一个英文冒号(:),再写上 public 基类名 就可以了。

C++ 共有 3 种继承方式:public(公有继承)、protected(保护继承)和 private(私有继承)。在实际开发中,大多数情况下我们使用的都是 public 继承,另外两种方式这里简单了解一下即可。

示例 1:继承的基本用法

#include <iostream>
#include <string>

// 定义基类
class Person {
public:
    std::string name;

    void introduce() {
        std::cout << "姓名:" << name << '\n';
    }
};

// 定义派生类
class Student : public Person {
public:
    int score;

    void showScore() {
        std::cout << "成绩:" << score << '\n';
    }
};

int main() {
    Student stu;
    stu.name = "小明";
    stu.score = 95;
    stu.introduce();
    stu.showScore();
    return 0;
}

运行结果如下。

姓名:小明
成绩:95

分析:

在这个例子中,我们先定义了一个基类 Person,再通过 class Student : public Person 定义了派生类 Student,并在内部新增了 score 成员变量和 showScore() 成员函数。

在 main() 函数中,我们只创建了一个 Student 对象 stu,却可以直接读写 stu.name 并调用 stu.introduce()。这是因为它们都是从基类继承过来的,同时 stu 也可以使用自己新增的成员。

注意 使用 class 定义派生类时,如果不写 public,C++ 默认会采用 private 继承,这会导致基类原本公开的成员在派生类外部全部无法访问。因此在实际开发中,小伙伴们一定要记得把 public 明确写出来,不要省略。

当然了,我们也可以让多个不同的派生类继承同一个基类,请看下面例子。

示例 2:多个派生类继承同一基类

#include <iostream>
#include <string>

// 定义基类
class Person {
public:
    std::string name;

    void introduce() {
        std::cout << "大家好,我是" << name << '\n';
    }
};

// 定义学生类
class Student : public Person {
public:
    int score;

    void study() {
        std::cout << name << "正在学习,成绩:" << score << '\n';
    }
};

// 定义老师类
class Teacher : public Person {
public:
    std::string course;

    void teach() {
        std::cout << name << "正在讲授:" << course << '\n';
    }
};

int main() {
    Student stu;
    stu.name = "小明";
    stu.score = 95;
    stu.introduce();
    stu.study();

    Teacher tch;
    tch.name = "林老师";
    tch.course = "C++";
    tch.introduce();
    tch.teach();
    return 0;
}

运行结果如下。

大家好,我是小明
小明正在学习,成绩:95
大家好,我是林老师
林老师正在讲授:C++

分析:

这里 Student 和 Teacher 两个派生类都公有继承了 Person 基类。因此它们创建出来的对象 stu 和 tch 都拥有基类的 name 成员变量和 introduce() 成员函数,不需要在两个类里重复编写相同的代码。

与此同时,Student 拥有特有的 study() 函数,Teacher 拥有特有的 teach() 函数,既复用了公共代码,又保留了各自专属的功能。

继承中的成员访问权限

在前面的章节中我们学过,类里面的成员可以通过 public、protected 和 private 这 3 种访问修饰符来控制权限。当派生类采用 public 方式继承基类后,基类中的不同成员在派生类里能不能直接访问呢?具体规则如下表所示。

public 继承访问权限
基类中的成员权限 派生类成员函数内部 类的外部(通过对象)
public(公有成员) 可以直接访问 可以直接访问
protected(受保护成员) 可以直接访问 不能直接访问
private(私有成员) 不能直接访问 不能直接访问

对于上表中的规则,小伙伴们不要误解了:class Student : public Person 中的 public 只是继承方式,并不会把基类原有的 private 成员变成公有成员。也就是说,基类的私有成员在派生类中依然不能直接访问。

而 protected(受保护成员)刚好处在两者之间:在类的外部不能通过对象直接访问,但在派生类的成员函数内部却可以直接读写。如果我们不希望基类的某个成员对外公开,同时又希望留给子类直接使用,就可以将其定义为 protected。

示例 3:访问基类的不同成员

#include <iostream>
#include <string>

class Person {
public:
    std::string name;

    std::string getIdCard() {
        return idCard;
    }

protected:
    int age;

private:
    std::string idCard = "440100";
};

class Student : public Person {
public:
    void setInfo() {
        name = "小明";    // 可访问基类 public 成员
        age = 18;         // 可访问基类 protected 成员
    }

    void showInfo() {
        std::cout << "姓名:" << name << ",年龄:" << age << '\n';
        std::cout << "证件号:" << getIdCard() << '\n';
    }
};

int main() {
    Student stu;
    stu.setInfo();
    stu.showInfo();
    return 0;
}

运行结果如下。

姓名:小明,年龄:18
证件号:440100

分析:

在 Student 类的成员函数内部,我们可以直接读写基类的 public 成员 name 和 protected 成员 age。

而基类的 private 成员 idCard 虽然也包含在 stu 对象中,但不能在派生类内部直接访问,只能通过基类公开的 getIdCard() 函数来间接获取。

到了类的外部(如 main() 函数中),只有 public 成员才能通过对象直接访问。如果我们在 main() 中尝试直接访问 protected 或 private 成员,编译器都会直接报错:

Student stu;

// 正确方式
stu.name = "小明";

// 错误方式
stu.age = 18;
stu.idCard = "1001";

继承中的构造与析构

由于派生类对象是在基类的基础上构建出来的,因此在创建和销毁派生类对象时,基类与派生类的构造函数和析构函数都有固定的执行顺序与传参规则。

1. 构造与析构的执行顺序

当我们创建一个派生类对象时,程序会先调用基类的构造函数把基类部分初始化好,然后再执行派生类自己的构造函数。而在销毁派生类对象时,顺序刚好相反:程序会先执行派生类的析构函数,最后再执行基类的析构函数。

这就像平时盖房子一样:盖房子时要先打好底下的地基(基类),再往上盖楼层(派生类)。拆房子时则要先拆上面的楼层,最后再清理底下的地基,如下图所示。

基类与派生类的构造和析构顺序对比

示例 4:构造与析构的执行顺序

#include <iostream>

class Person {
public:
    Person() {
        std::cout << "1. Person 基类构造\n";
    }

    ~Person() {
        std::cout << "4. Person 基类析构\n";
    }
};

class Student : public Person {
public:
    Student() {
        std::cout << "2. Student 派生类构造\n";
    }

    ~Student() {
        std::cout << "3. Student 派生类析构\n";
    }
};

int main() {
    Student stu;
    return 0;
}

运行结果如下。

1. Person 基类构造
2. Student 派生类构造
3. Student 派生类析构
4. Person 基类析构

分析:

从运行结果可以清楚地看到:创建派生类对象 stu 时,基类的构造函数 Person() 最先执行,随后才执行派生类自己的构造函数 Student()。

当 main() 函数结束、局部对象 stu 被销毁时,顺序完全反过来,先执行 Student 的析构函数,再执行 Person 的析构函数。对于这一点,小伙伴们只需要记住一句话:先构造基类,再构造派生类;先析构派生类,再析构基类。

2. 调用基类的带参构造函数

在上面的例子中,基类 Person 使用的是无参构造函数,创建派生类对象时编译器会自动调用它。

但在实际开发中,基类往往定义了带参数的构造函数,比如创建 Person 时必须传入姓名。此时,我们就必须在派生类构造函数的初始化列表中,显式调用基类的构造函数并把参数传过去。

语法:

派生类名(参数列表) : 基类名(基类实参), 派生类成员(成员初值) {
    // 派生类构造函数体
}

说明:

这里的冒号后面写的是 基类名(基类实参),表示在初始化派生类自己的成员变量之前,先将参数传给基类的构造函数,完成基类部分的初始化。

示例 5:调用基类的带参构造函数

#include <iostream>
#include <string>

class Person {
public:
    std::string name;

    // 基类的带参构造函数
    Person(std::string n) : name(n) {}
};

class Student : public Person {
public:
    int score;

    // 显式调用基类构造函数
    Student(std::string n, int s) : Person(n), score(s) {}

    void showInfo() {
        std::cout << "姓名:" << name << ",成绩:" << score << '\n';
    }
};

int main() {
    Student stu("小明", 95);
    stu.showInfo();
    return 0;
}

运行结果如下。

姓名:小明,成绩:95

分析:

Student(std::string n, int s) : Person(n), score(s) {} 这句代码通过 Person(n) 把实参 "小明" 传给基类构造函数来初始化 name,再通过 score(s) 初始化派生类自己的 score。

这里小伙伴们要注意:初始化从基类继承来的成员时,冒号后面必须写基类名 Person(n),不能直接写基类的成员变量 name(n)。另外,不管把 Person(n) 排在初始化列表的前面还是后面,程序都会先执行基类构造函数,平时固定写在第一项最直观。

一旦基类只定义了带参构造函数,编译器就不会再生成默认的无参构造函数了。此时如果派生类漏写了 : Person(n),编译器就会因为找不到无参构造函数 Person() 而直接报错:

// 正确方式
Student(std::string n, int s) : Person(n), score(s) {}

// 错误方式
Student(std::string n, int s) : score(s) {}

继承与对象组合(is-a 与 has-a)

刚接触继承时,有些小伙伴可能会觉得:只要两个类有一些相同的成员,让其中一个去继承另一个不就能少写代码了吗?其实除了继承之外,把一个类的对象作为另一个类的成员变量(也就是 “对象组合”),同样可以用来复用代码。

在实际开发中,我们可以根据两个类之间的逻辑关系来判断应该选哪一种:

is-a(是一种)关系:如果说 “B 是一种 A” 在逻辑上自然成立,就使用继承。比如 “学生(Student)是一种人(Person)”,让 Student 继承 Person 就非常合适。

has-a(有一个)关系:如果更自然的说法是 “B 有一个 A”,就应该使用对象组合。比如汽车(Car)和发动机(Engine),我们只能说 “汽车有一个发动机”,此时应该把 Engine 作为 Car 的成员变量。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号