继承的引入

继承是面向对象类类的复用的重要手段。
继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。

场景:
现在有一个学生管理系统,里面有老师,同学,宿管阿姨他们三者都有 age 和 name 和 编号 这三个一样的元素这时如果 每个类都去定义这三个成员变量,那么就太亢余了这是我们 搞一个父类定义这三个变量,让老师类,同学类,阿姨类继承父类就可以了

// 父类(基类)
class Person {
protected:
    int age;
    string name;
    string id;  // 编号

public:
    // 构造函数
    Person(int a, string n, string i) : age(a), name(n), id(i) {}
    
    // 显示基本信息
    virtual void display() {
        cout << "编号: " << id << ", 姓名: " << name << ", 年龄: " << age;
    }
    
    // 虚析构函数
    virtual ~Person() {}
};

// 老师类
class Teacher : public Person {
private:
    string department;  // 所在部门

public:
    Teacher(int a, string n, string i, string dep) 
        : Person(a, n, i), department(dep) {}
    
    void display() override {
        Person::display();
        cout << ", 职位: 老师, 部门: " << department << endl;
    }
};

// 学生类
class Student : public Person {
private:
    string major;  // 专业

public:
    Student(int a, string n, string i, string m) 
        : Person(a, n, i), major(m) {}
    
    void display() override {
        Person::display();
        cout << ", 职位: 学生, 专业: " << major << endl;
    }
};

// 宿管阿姨类
class DormManager : public Person {
private:
    string building;  // 管理的楼栋

public:
    DormManager(int a, string n, string i, string bld) 
        : Person(a, n, i), building(bld) {}
    
    void display() override {
        Person::display();
        cout << ", 职位: 宿管, 管理楼栋: " << building << endl;
    }
};

其中继承别人的类叫做,派生类
被派生类继承的类叫基类

继承的种类与成员访问限定符

我们已经很了解public,private作为成员访问限定符的作用
现在该介绍第三种成员访问限定符了-------protected
protected专为继承而设计

有三种继承方式,public继承,protected继承,private继承
三种继承对于外界访问类成员,派生类访问基类成员关系如下

在这里插入图片描述

总结这张表 :
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3.访问权限计算规则:实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public >protected > private.
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承.

派生类和基类的默认成员函数

所有类都有默认成员函数,构造函数,析构函数,拷贝构造函数
operator = 赋值函数等,派生类和基类也不例外
那么派生类和基类的默认成员函数的关系应该是怎么样的呢?
答:核心原则,各管各的,派生类和基类都是各管各的成员初始化

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化 .
3.派生类的operator=必须要调用基类的operator=完成基类的复制。

第370和371行,就是派生类去初始化基类成员所以错了在这里插入图片描述

375行 ,这样子去调用person的构造函数 自己去初始化自己的部分才是对的
其他默认成员函数同理。
在这里插入图片描述

4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

#include <iostream>
using namespace std;

// 基类
class Base {
public:
    Base() {
        cout << "Base类构造函数被调用" << endl;
    }
    
    ~Base() {
        cout << "Base类析构函数被调用" << endl;
    }
};

// 派生类
class Derived : public Base {
public:
    Derived() {
        cout << "Derived类构造函数被调用" << endl;
    }
    
    ~Derived() {
        cout << "Derived类析构函数被调用" << endl;
        // 注意:这里没有显式调用基类析构函数
        // 但基类析构函数会自动被调用
    }
};

int main() {
    cout << "创建派生类对象:" << endl;
    Derived obj;
    
    cout << "\n派生类对象超出作用域,开始析构:" << endl;
    // obj超出作用域,析构函数被自动调用
    return 0;
}
//运行结果如下
创建派生类对象:
Base类构造函数被调用
Derived类构造函数被调用

派生类对象超出作用域,开始析构:
Derived类析构函数被调用
Base类析构函数被调用

面试题:如何设置一个无法被继承的类?答:private 这个类的构造函数,那么这个基类成员无法初始化

复杂的继承

多继承

多继承:一个派生类同时继承多个父类,这就是多继承
在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;

// 基类:Student(学生)
class Student {
protected:
    string studentId;   // 学生学号
    string studentName; // 学生姓名
public:
    Student(string id = "", string name = "") : studentId(id), studentName(name) {}
    void study() {
        cout << studentName << "(学生,学号:" << studentId << ")正在学习。" << endl;
    }
};

// 基类:teacher(教师)
class teacher {
protected:
    string teacherId;   // 教师工号
    string teacherName; // 教师姓名
public:
    teacher(string id = "", string name = "") : teacherId(id), teacherName(name) {}
    void teach() {
        cout << teacherName << "(教师,工号:" << teacherId << ")正在授课。" << endl;
    }
};

// 派生类:assistant(助教),多重继承自 Student 和 teacher
class assistant : public Student, public teacher {
private:
    string course; // 助教负责的课程
public:
    // 构造函数,同时初始化 Student、teacher 和自身成员
    assistant(string stuId, string stuName, string teaId, string teaName, string crs) 
        : Student(stuId, stuName), teacher(teaId, teaName), course(crs) {}
    
    // 助教特有的行为:辅助教学
    void assist() {
        cout << "助教 " << studentName << "(学生学号:" << studentId 
             << ",教师工号:" << teacherId << ")正在辅助课程《" << course << "》。" << endl;
    }
};

int main() {
    // 创建助教对象
    assistant assi("S1001", "张三", "T2001", "李老师", "数据结构");
    
    // 调用 Student 类的方法
    assi.study();
    // 调用 teacher 类的方法
    assi.teach();
    // 调用 assistant 自身的方法
    assi.assist();
    
    return 0;
}

多重继承特性:assistant同时从Student和teacher两个基类继承,因此同时拥有学生和教师的属性与行为(如学生的 “学号、学习”,教师的 “工号、授课”)。
角色融合性:模拟了 “助教” 的现实场景 —— 助教通常既具有 “学生” 身份(如攻读学位),又承担 “教师” 的部分职责(如辅助授课、指导),多重继承很好地体现了这种 “双重角色” 的融合。

菱形继承

在这里插入图片描述

问题: Assistant 中有两份 person 的内容造成代码亢余性和二义性

Student 和 Teacher 都继承了 Person 的成员(属性、方法等),因此 Assistant 通过 Student 会包含一份 Person 的成员拷贝,同时通过 Teacher 又会包含另一份 Person 的成员拷贝。

CPP语法坑

CPP历史悠久,有些语法的设计上因为历史原因,留下了很多语法坑,其中多继承就是CPP留下的语法坑
多继承是C++的一个缺陷,多继承->菱形继承->虚继承语法复杂,甚至性能受到影响所以:不到万不得已,不要设计多继承

继承和组合

除了继承,组合也是复用类的一种方法
组合的实现非常简单,只要一个类包含另一个类即可
在这里插入图片描述

两者的区别:
继承会破坏基类的封装性,基类的protected成员在派生类中暴露
组合就不会破坏封装性,该private不能用的到了外面就是不能用

破坏封装性:使逻辑内耦合度提高(A,B是两个类,不能说提高内聚)不破坏封装性:耦合度低(耦合度低代码维护性好

那么我们时候使用继承,什么时候使用组合呢?
答:当两个类关系非常紧密的时候用继承(is-a理论)关系非常紧密的时候都可以看作叫高内聚了(尽管是不同的类),所以用继承。
当两个类关系没有那么紧密的时候用组合(has-a理论)维持两个类的封装性

到这里,我们就完成了面向对象语言的第二架马车—继承的学习

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐