一、继承方式

1. 单继承

一个派生类只有一个直接基类的时候称这个继承为单继承 Person ↓ Teacher ↓ Student (单链)

 

代码语言:javascript

AI代码解释

// 基类:人
class Person
{
public:
    string name;
    int age;

    void ShowPerson()
    {
        cout << "姓名: " << name << ",年龄: " << age << endl;
    }
};

// 派生类:老师(继承人)
class Teacher : public Person
{
public:
    string subject;

    void ShowTeacher()
    {
        cout << "姓名: " << name
            << ",年龄: " << age
            << ",教授科目: " << subject << endl;
    }
};

// 派生类:学生(继承老师)
class Student : public Teacher
{
public:
    string school;

    void ShowStudent()
    {
        cout << "姓名: " << name
            << ",年龄: " << age
            << ",教授科目: " << subject
            << ",学校: " << school << endl;
    }
};

int main()
{
    Student s;
    s.name = "呆头";       // 来自 Person
    s.age = 20;            // 来自 Person
    s.subject = "C++";     // 来自 Teacher
    s.school = "野鸡大学";  // 来自 Student

    s.ShowStudent();

    return 0;
}

运行结果

 

在这里插入图片描述

多链

 

在这里插入图片描述

代码语言:javascript

AI代码解释

// 基类:人
class Person
{
public:
    string name;
    int age;

    void ShowPerson()
    {
        cout << "姓名: " << name << ",年龄: " << age << endl;
    }
};

// 派生类:老师
class Teacher : public Person
{
public:
    string subject;

    void Teach()
    {
        cout << name << " 正在教授 " << subject << " 课程。" << endl;
    }
};

// 派生类:学生
class Student : public Person
{
public:
    string school;

    void Study()
    {
        cout << name << " 在 " << school << " 学习。" << endl;
    }
};

int main()
{
    Teacher t;
    t.name = "张老师";
    t.age = 35;
    t.subject = "C++";
    t.ShowPerson();
    t.Teach();

    cout << "------------------" << endl;

    Student s;
    s.name = "李华";
    s.age = 20;
    s.school = "宁夏大学";
    s.ShowPerson();
    s.Study();

    return 0;
}

2. 多继承

一个派生类有两个或者以上的直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是先继承的基类在前面,后继承的基类在后面,派生类成员放在最后面。 多继承的使用方法是在子类的位置对多个父类使用逗号,进行间隔,其余方式public形式不变,进行继承

代码语言:javascript

AI代码解释

 Person       Teacher       Athlete
     \          |          /
      \         |         /
         ---->  Student

代码语言:javascript

AI代码解释

// 基类:人
class Person
{
public:
    string name;
    int age;
};

// 老师类
class Teacher
{
public:
    string subject;
    void Teach() { cout << "正在教授 " << subject << " 课程。" << endl; }
};

// 运动员类
class Athlete
{
public:
    string sport;
    void Train() { cout << "正在训练 " << sport << " 项目。" << endl; }
};

// 学生类(多继承)
class Student : public Person, public Teacher, public Athlete
{
public:
    string school;
    void Show()
    {
        cout << "姓名: " << name
             << ",年龄: " << age
             << ",学校: " << school
             << ",教授科目: " << subject
             << ",运动项目: " << sport << endl;
    }
};

int main()
{
    Student s;
    s.name = "李华";
    s.age = 20;
    s.school = "宁夏大学";
    s.subject = "C++";
    s.sport = "800米";

    s.Show();
    s.Teach();
    s.Train();
    return 0;
}

运行结果

 

在这里插入图片描述

3. 菱形继承

菱形继承是多继承的⼀种特殊情况。菱形继承的问题有数据冗余和⼆义性的问题。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

 

在这里插入图片描述

代码语言:javascript

AI代码解释

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

// 基类:人
class Person
{
public:
    string name;
    int age;
};

// 老师继承人
class Teacher : public Person
{
public:
    string subject;
    void Teach()
    {
        cout << name << " 正在教授 " << subject << endl;
    }
};

// 运动员继承人
class Athlete : public Person
{
public:
    string sport;
    void Train()
    {
        cout << name << " 正在训练项目:" << sport << endl;
    }
};

// 学生同时继承老师与运动员
class Student : public Teacher, public Athlete
{
public:
    string school;

    void Show()
    {
        // ⚠️ 错误:编译器不知道 name 是哪个基类的
        // cout << "姓名: " << name << endl; // ❌ 二义性

        cout << "Teacher::name = " << Teacher::name << endl;
        cout << "Athlete::name = " << Athlete::name << endl;
        cout << "学校: " << school << endl;
    }
};

int main()
{
    Student s;
    s.Teacher::name = "张老师";
    s.Athlete::name = "李同学"; // 各自一份 name
    s.subject = "C++";
    s.sport = "800米";
    s.school = "宁夏大学";

    s.Show();

    // 访问需要显式指定路径
    s.Teacher::Teach();
    s.Athlete::Train();

    return 0;
}

Student ├── Teacher::Person │ ├── name │ └── age ├── Athlete::Person │ ├── name │ └── age └── school

 

在这里插入图片描述

 

在这里插入图片描述

 

在这里插入图片描述

3.1 菱形虚拟继承

如何解决菱形继承带来的弊端?——》使用虚拟继承! 以上述对象模型为例,在 Teacher 和 Athlete 继承 Person 时采用虚拟继承,即可避免 Student 同时拥有两份 Person 子对象。

  • 虚拟继承是一种专门用于解决菱形继承问题的机制,在其它普通继承场景下不建议使用。
  • 在 C++ 中,虚拟继承的使用方式是:当多个派生类继承自同一个公共基类,并且这些派生类又被同一个子类多继承时,应当在这些“中间层”父类继承公共基类的地方,使用 virtual 关键字进行修饰。(上述示例就应该在老师和运动处使用virtual进行修饰)

代码语言:javascript

AI代码解释

class Person
{
public:
    string name;
    int age;
};

// 老师虚继承人
class Teacher : virtual public Person
{
public:
    string subject;
};

// 运动员虚继承人
class Athlete : virtual public Person
{
public:
    string sport;
};

// 学生多继承
class Student : public Teacher, public Athlete
{
public:
    string school;

    void Show()
    {
        cout << "姓名: " << name << endl;  // ✅ 不再二义性
        cout << "学校: " << school << endl;
    }
};

3.2 菱形虚拟继承的原理

回顾我们上述示例是这样的继承关系:

代码语言:javascript

AI代码解释

      Person
     /      \
 Teacher     Athlete
     \      /
     Student

 

  • 如果Student同时继承Teacher和Athlete,他就会有两份Person成员(一次从Teacher一次从Athlete),这样会造成数据冗余
  • 调用Student的Person的成员函数或者访问成员变量的时候,编译器就不知道选择Teacher中的还是Athlete中的那一份,就造成了二义性

3.2.1 内存布局

虚拟继承会让共享的基类在内存中只有一份,并且派生类通过 虚基表(vbt/vptr类似) 来找到这份共享的基类。

代码语言:javascript

AI代码解释

Student 对象布局(虚继承):
[ Student 部分 ][ Teacher 部分 ][ Athlete 部分 ][ Person(共享) ]

注意 Person 只存在一份。

  • 每个通过虚继承的中间类(TeacherAthlete)内部不直接包含 Person 成员,而是 包含一个指向共享 Person 的指针(实际上是偏移量)。
  • Student 被实例化时,最终会在对象末尾放置一份 Person 数据。
  • 所有通过虚继承的类访问 Person 时,通过偏移量找到唯一的 Person

示意图:

代码语言:javascript

AI代码解释

Student 内存:
[ Student fields ]
[ Teacher fields | pointer_to_shared_Person ]
[ Athlete fields | pointer_to_shared_Person ]
[ Shared Person fields ]

3.2.2 虚表(vtable/vptr)调整

虚继承通常涉及两个表:

普通虚表:用于多态函数调用。 虚基表(VBT):用于找到虚继承基类的偏移量。

访问虚基类成员时:

  • 编译器生成代码不是 this + offset_of_Person_in_Teacher
  • 而是 this + vbt_offset → 找到唯一 Person

举例:

代码语言:javascript

AI代码解释

Student s;
s.Teacher::Person::name = "Alice"; // 编译器查找 Teacher 的虚基表 -> 定位到 Student 中唯一的 Person

底层逻辑thisStudent 的起始地址 → 查找虚基表中的 Person 偏移量 → 访问共享 Person


3.2.3 构造函数调用顺序

虚继承的构造顺序也不同:

  1. 最顶层虚基类(Person)先构造。
  2. 然后是非虚中间类(TeacherAthlete)。
  3. 最后是最派生类(Student)。

 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐