22 继承与派生

1 对继承与派生概念的理解

面向对象思想一节中,我们使用 “敌人类” 派生出 “弓箭手类” 和 “剑客类” 两种敌人的例子,简单介绍了 继承与派生 的概念,请跳转查看。

2 特别说明

C++ 语言的继承与派生,功能是非常强大的。C++ 支持单继承、多继承,也支持公有继承、保护继承和私有继承。但对其他面向对象编程语言来说,很多只支持单继承和公有继承。因为多继承等方式会更容易使代码产生混乱。

这里不讨论多继承好坏。为了同学们在接触其他语言时,不容易产生混乱,也为了尽量简化本课程的内容, 本教程只介绍单继承和公有继承

3 派生语法

声明一个派生类的语法如下:

class 派生类名 : public 基类名
{
    派生类中新增的成员变量和成员函数
};

此处的 public 表示 “继承方式”“公有继承”。在公有继承中,来自父类的公有成员(包括属性和方法)的访问权限,如果 protected 或者 public ,那么在子类中,它们的访问权限也是 protected 或者 public ,不会改变;如果是 private ,那么在子类中,它们是子类无权访问的。

我们继续用学生类举例。所有学生都是人,所以如果把 “学生类” 看做一个派生类,它的父类就是 “人类” 。

“人类” 有 “名字” m_name 和 “年龄” m_age 两个属性, “学生类” 比 “人类” 多了 “成绩” m_score 这一属性。

由此,我们可以得到以下代码:

#include<iostream>

using namespace std;

//基类 People
class People {
public:
    void setName(string name);

    void setAge(int age);

    string getName();

    int getAge();

private:
    string m_name;
    int m_age;
};

void People::setName(string name) { m_name = name; }

void People::setAge(int age) { m_age = age; }

string People::getName() { return m_name; }

int People::getAge() { return m_age; }

//派生类 Student
class Student : public People {
public:
    void setScore(float score);

    float getScore();

private:
    float m_score;
};

void Student::setScore(float score) { m_score = score; }

float Student::getScore() { return m_score; }

int main() {
    Student stu;
    stu.setName("小明");
    stu.setAge(16);
    stu.setScore(95.5f);
    cout << stu.getName() << "的年龄是 " << stu.getAge() << ",成绩是 " << stu.getScore() << endl;

    return 0;
}

程序输出:小明的年龄是 16,成绩是 95.5

4 基类和派生类的构造函数

同学们有没有注意到,我们在上面的代码中,并没有使用构造函数的方式,去初始化 Student 类的对象。

在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。

但是,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。

这种矛盾在 C++ 继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。

下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:

#include<iostream>

using namespace std;

//基类 People
class People {
protected:
    string m_name;
    int m_age;
public:
    People(string name, int age) : m_name(name), m_age(age) {}
};

//派生类 Student
class Student : public People {
private:
    float m_score;
public:
    Student(string name, int age, float score) : People(name, age), m_score(score) {}

    void display() {
        cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << "。" << endl;
    }
};

int main() {
    Student stu("小明", 16, 90.5);
    stu.display();

    return 0;
}

程序输出:小明的年龄是16,成绩是90.5。

注意点:

  1. 因为我们需要使用 display() 方法来访问 m_namem_age 两个来自父类的成员变量,所以,这两个变量的访问权限就不可以是 private ,否则 Student 类是没有访问这两个变量的权限的。
  2. Student 类的构造函数的 初始化列表 中,我们先写了对父类构造函数的调用,后写的对派生类新增属性的初始化内容。但是不管它们的顺序如何,派生类构造函数总是先调用基类构造函数再执行其他代码(包括参数初始化表以及函数体中的代码)。
  3. 调用父类的构造函数,必须使用初始化列表,不能在构造函数的函数体中调用。
  4. 从上面的分析中可以看出,基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:A --> B --> C,那么创建 C 类对象时构造函数的执行顺序为:A类构造函数 --> B类构造函数 --> C类构造函数。还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。以上面的 A、B、C 类为例,C 是最终的派生类,B 就是 C 的直接基类,A 就是 C 的间接基类。
  5. 定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(或无参构造函数);如果没有默认构造函数,那么编译失败。

5 基类和派生类的析构函数

和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。
#include <iostream>
using namespace std;
class A{
public:
    A(){cout<<"A 构造函数"<<endl;}
    ~A(){cout<<"A 析构函数"<<endl;}
};
class B: public A{
public:
    B(){cout<<"B 构造函数"<<endl;}
    ~B(){cout<<"B 析构函数"<<endl;}
};
class C: public B{
public:
    C(){cout<<"C 构造函数"<<endl;}
    ~C(){cout<<"C 析构函数"<<endl;}
};
int main(){
    C test;
    return 0;
}

程序输出:
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor

这个例子这也说明了,如果没有显式调用父类构造函数,那么系统将自动调用父类的无参构造函数。

6 继承中的“支配规则”(同名覆盖规则)

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

本例中,基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成遮蔽。第 37 行代码中,stu 是 Student 类的对象,默认使用 Student 类的 show() 函数。

但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符,如第 39 行代码所示。

#include<iostream>
using namespace std;
//基类People
class People{
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show(){
    cout<<"嗨,大家好,我叫"<<m_name<<",今年"<<m_age<<"岁"<<endl;
}
//派生类Student
class Student: public People{
public:
    Student(char *name, int age, float score);
public:
    void show();  //遮蔽基类的show()
private:
    float m_score;
};
Student::Student(char *name, int age, float score){
    m_name = name;
    m_age = age;
    m_score = score;
}
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
int main(){
    Student stu("小明", 16, 90.5);
    //使用的是派生类新增的成员函数,而不是从基类继承的
    stu.show();
    //使用的是从基类继承来的成员函数
    stu.People::show();
    return 0;
}

7 将派生类赋值给基类(向上转型)

假设有 AB 两个类, AB 的父类。我们将 B 的示例,赋值给 A 类,这分为两种情况:

1. 赋值给 A 类对象

A a;
B b;
a = b;

此时,a 对象中的所有属性值,会被赋值为 b 对象中的对应属性值。

2. 赋值给 A 类引用或指针

B b;
A& a = b;

此时,A 类的引用或指针,指向的仍然是 b 这个对象,但通过 a ,将只能访问 A 类中,存在的成员。