欢迎光临
我们一直在努力

【C++】C++继承入门(下):友元、静态成员与菱形继承的底层逻辑

📌 相关专栏

  • 【Linux专栏】
  • 【C语言专栏】
  • 【测试专栏】
  • 【MySQL专栏】
  • 【C++ 专栏】

📌 相关文章推荐

  • 【C++】C++继承入门(上):继承语法与基本特性详解
  • 【C++】模板进阶全解:非类型参数|全特化|偏特化|分离编译完全指南
  • 【C++】 深入容器适配器:从零实现stack与queue
  • 【C++】 C++ priority_queue 完全指南:优先队列的使用与手写实现

举爪爪打招呼

很高兴你点开这篇文章✨

这里会持续更新更多有用的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • 1. 友元与继承:友元关系不具有传递性
    • 1.1 理解
    • 1.2 代码示例
  • 2. 静态成员在继承体系中的共享性
    • 2.1 核心结论
    • 2.2 静态成员总结
  • 3. 单继承与多继承
    • 3.1 单继承
    • 3.2 多继承
  • 4. 菱形继承的问题:数据冗余与二义性
    • 4.1 什么是菱形继承?
    • 4.2 代码示例
    • 4.3 数据冗余
    • 4.4 二义性
  • 5. 虚继承:彻底解决菱形继承问题
    • 5.1 虚继承的语法
    • 5.2 虚继承后的内存布局
    • 5.3 虚继承的核心规则
    • 5.4 构造顺序验证
    • 虚继承下的构造顺序:
  • 6. 完整测试用例
    • 6.1 友元测试
    • 6.2 静态成员测试
    • 6.3 菱形继承问题测试
    • 6.4 虚继承测试
  • 7. 总结
  • 本文全部代码
    • 🐾 test.cpp

前言

在掌握了继承的基础语法后,我们需要深入理解继承体系中的一些进阶特性:

  • 友元关系是否会被继承?
  • 静态成员在派生类中如何共享?
  • 多继承会带来什么问题?
  • 菱形继承的数据冗余和二义性如何解决?

🐶 🐾 ✨ 🐾 🐶


1. 友元与继承:友元关系不具有传递性

1.1 理解

基类的友元函数/类无法直接访问派生类的私有/保护成员。

  • 可以类比为:你朋友的朋友不等同于你的朋友
  • 如果需要友元访问派生类成员,就必须在派生类中重新说明友元

1.2 代码示例

前置声明Student(为了让编译器走到友元函数时能向上查找到 Student):提前告诉编译器Student是一个类

// 前置声明:告诉编译器 Student 是一个类
class Student;

// 基类 Person
class Person
{
public:
// 声明 Display 是友元函数
friend void Display(const Person& p, const Student& s);
protected:
string _name = "千余";
};

// 派生类 Student
class Student : public Person
{
// 必须再次声明友元,否则 Display 无法访问 Student 的保护成员
friend void Display(const Person& p, const Student& s);
protected:
int _stuid = 888;//学号
};

// 友元函数实现
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; // Person 声明了友元,可访问
cout << s._stuid << endl; // Student 也声明了友元,才能访问
}

void Test1()
{
Person p;
Student s;
Display(p, s);
}

结论 :友元关系不具有继承性。如果需要友元访问派生类成员,必须在派生类中重新声明友元。

🐶 🐾 ✨ 🐾 🐶


2. 静态成员在继承体系中的共享性

2.1 核心结论

基类的静态成员在整个继承体系中仅存在一份,派生类和基类共享该成员,不会因为继承而产生多个副本。

  • 也就是说对于基类的静态成员而言,对其进行初始化则说所有派生类的该成员都会被初始化成相同值,并且一个类的静态成员进行修改也会影响其他所有相关的派生类和基类

class Person
{
public:
string _name; // 非静态:每个对象一份
static int _count; // 静态:整个类共用一份
};

// 静态成员必须在类外初始化
int Person::_count = 1;

class Student : public Person
{
protected:
int _stuid;
};

void Test2()
{
Person p;
Student s;

// 非静态成员:每个对象独立
cout << &p._name << endl; // 地址1
cout << &s._name << endl; // 地址2(不同)

// 静态成员:全家族共用一份
cout << &p._count << endl; // 地址A
cout << &s._count << endl; // 地址A(相同)
cout << &Person::_count << endl; // 地址A
cout << &Student::_count << endl; // 地址A

// 修改一个,全部改变
Person::_count++;
cout << p._count << endl; // 2
cout << s._count << endl; // 2
cout << Student::_count << endl; // 2
}

  • 公有的情况下,基类派生类指定类域都可以访问静态成员,静态成员不属于任何一个对象,是存在静态区的,可以把静态成员看成被类域限制的全局变量,所以要访问就需要使用域作用限定符::来突破类域访问

2.2 静态成员总结

  • 存储位置:在静态区,不属于任何对象
  • 初始化: 必须在类外进行
  • 访问方式 :可通过对象、类名(类名::)访问
  • 继承特性: 所有派生类共享同一份静态成员
  • 静态函数 :只能访问静态成员,不能访问非静态成员

🐶 🐾 ✨ 🐾 🐶


3. 单继承与多继承

3.1 单继承

单继承 :一个派生类只有⼀个直接基类的关系

🐾单继承模型

在这里插入图片描述


3.2 多继承

多继承 :一个派生类有两个或以上直接基类的继承关系

class Student { /* … */ };
class Teacher { /* … */ };

class Assistant : public Student, public Teacher
{
// 继承了 Student 和 Teacher 的所有成员
};


🐾多继承模型

在这里插入图片描述

🐶 🐾 ✨ 🐾 🐶


4. 菱形继承的问题:数据冗余与二义性

4.1 什么是菱形继承?

菱形继承是指一个派生类同时继承两个基类,而这两个基类又共同继承自一个顶层基类的结构 :

🐾模型:

在这里插入图片描述


4.2 代码示例

lass Person
{
public:
string _name; //会被Assistant继承两次
};

// 中间基类1
class Student : public Person
{
protected:
int _num; //学号
};

// 中间基类2
class Teacher : public Person
{
protected:
int _id; //职工编号
};

class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};


4.3 数据冗余

Assistant 对象中包含两份 Person 的 _name

在这里插入图片描述

4.4 二义性

查看二义性:

1.在“Assistant a;”处打断点,按F5开始调试,调试->窗口->监视;

2.点开a前面的小箭头:
+a Assistant
+Student // 第一个基类部分
+Person // 里面有一个 _name
_num
+Teacher // 第二个基类部分
+Person // 里面又有一个 _name
_id
_majorCourse

3.再分别展开Student和Teacher:
展开 Student → 里面有个 Person → 有一个 _name
展开 Teacher → 里面也有个 Person → 有另一个 _name

在这里插入图片描述

void Test4()
{
Assistant a;

// 错误:_name 访问不明确
// a._name = "张三";

// 二义性:两个都叫_name,编译器不知道要的是哪个
//必须显式指定访问哪个基类的 _name
a.Student::_name = "李四";
a.Teacher::_name = "王五";
}

结论 :菱形继承会导致数据冗余(两份 Person)和访问二义性(不知道用哪个 _name)。

🐶 🐾 ✨ 🐾 🐶


5. 虚继承:彻底解决菱形继承问题

如果没有 virtual 虚继承, Assistant 继承 Student 和 Teacher ,而两者又都继承 Person ,会导致 :

  • Person 子对象在 Assistant 里存在两份,造成数据冗余; ​
  • 访问 a._name 时会产生二义性,编译器不知道该用哪一 份 Person 的 _name
  • 🐾 因此,虚继承的作用是 :让顶层基类只存在一份,不再重复


    5.1 虚继承的语法

    在中间基类的继承方式前添加 virtual 关键字 :

    //顶层基类
    class Person
    {
    public:
    Person(const char* name)
    :_name(name)
    {
    }
    public:
    string _name; // 姓名
    };

    // 中间基类1:虚继承Person(添加virtual)
    //virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Student)加
    class Student : virtual public Person
    {
    public:
    Student(const char* name, int num)
    :Person(name)// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
    , _num(num)// 只会初始化自己的成员变量
    {
    }
    protected:
    int _num; //学号
    };

    // 中间基2:虚继承Person(添加virtual)
    //virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Teacher)加
    class Teacher : virtual public Person
    {
    public:
    Teacher(const char* name, int id)
    :Person(name)// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
    , _id(id)// 只会初始化自己的成员变量
    {
    }
    protected:
    int _id; // 职工编号
    };

    // 最终派生类:菱形继承(Person成员会被合并成仅一份)
    class Assistant : public Student, public Teacher
    {
    public:

    // 关键:虚继承下,顶层基类的构造由最终派生类显式调用
    Assistant(const char* name1, const char* name2, const char* name3)
    :Person(name1)//只有这个会执行,下面两个初始化不会进去
    , Student(name2, 1)
    , Teacher(name3, 2)
    , _majorCourse("计算机")
    {
    }

    protected:
    string _majorCourse; // 主修课程
    };



    5.2 虚继承后的内存布局

    没有虚继承时 :构造的顺序是 Person->Student->Person->Teacher->Assistant,会造成两次Person

    在这里插入图片描述


    5.3 虚继承的核心规则

    • 中间类 Student 、 Teacher 里的 Person(name) 会被编译器直接忽略、不执行 ​
    • 只有最终派生类 Assistant 才能初始化 Person

    5.4 构造顺序验证

    void Test5()
    {
    // 虽然有三次 Person(name) 调用
    // 但实际只执行 Assistant 中的 Person(name1)
    // 其他两次会被编译器忽略
    Assistant a("张三", "李四", "王五");

    // a 的 _name = "张三"
    }

    🐾 验证

    在这里插入图片描述

    虚继承下的构造顺序:

  • 先构造顶层基类 Person(由最终派生类调用)
  • 再按声明顺序构造中间基类 Student、Teacher
  • 最后构造最终派生类 Assistant
  • 🐶 🐾 ✨ 🐾 🐶


    6. 完整测试用例

    6.1 友元测试

    void Test1()
    {
    Person p;
    Student s;
    Display(p, s); // 输出:千余\\n888
    }

    在这里插入图片描述


    6.2 静态成员测试

    void Test2()
    {
    Person p;
    Student s;

    //非静态成员:每个对象独立
    //输出两个"不同"的地址,p和s是两个不同对象,各自有各自的_name
    cout << &p._name << endl;
    cout << &s._name << endl;
    cout << endl;

    //静态成员:全家族共用一份
    //输出"同一个"地址,静态成员在静态区,不属于任何对象,Person+所有派生类Student共用这一个_count
    cout << &p._count << endl;
    cout << &s._count << endl;
    cout << endl;

    //修改一个,全部变
    cout << p._count << endl;// 1
    cout << s._count << endl;// 1
    cout << endl;
    Person::_count++;

    // 公有的情况下,基类派生类指定类域都可以访问静态成员
    // 静态成员不属于任何一个对象,是存在静态区的
    // 可以把静态成员看成被类域限制的全局变量,所以要访问就需要使用域作用限定符::来突破类域访问
    cout << Person::_count << endl;// 2
    cout << Student::_count << endl;// 2
    cout << endl;
    }

    在这里插入图片描述


    6.3 菱形继承问题测试

    void Test4()
    {
    Assistant a;
    a.Student::_name = "李四";
    a.Teacher::_name = "王五";

    cout << a.Student::_name << endl; // 李四
    cout << a.Teacher::_name << endl; // 王五(两个不同的 _name)
    }

    在这里插入图片描述


    6.4 虚继承测试

    void Test5()
    {
    Assistant a("张三", "李四", "王五");
    // a 中只有一份 _name,值为 "张三"
    }

    🐶 🐾 ✨ 🐾 🐶


    7. 总结

    C++ 继承的进阶特性:

    • 友元与继承: 友元关系不具有继承性,需在派生类中重新声明
    • 静态成员 :整个继承体系共享一份静态成员
    • 单继承: 一个派生类只有一个直接基类
    • 多继承: 一个派生类有多个直接基类
    • 菱形继承问题: 数据冗余 + 二义性
    • 虚继承 :通过 virtual 解决菱形继承问题

    菱形继承解决方案对比

    方案数据冗余是否存在二义性的问题使用便利性
    普通多继承 存在 存在 需显式指定类域
    指定类域访问 存在 解决 麻烦,仍有冗余
    虚继承 解决 解决 便捷,自动共享

    谁被继承多次,谁就在那些子类加 virtual

    🐾 Person 被 Student 和 Teacher 继承

    • class Student : virtual public Person
    • class Teacher : virtual public Person

    🐶 🐾 ✨ 🐾 🐶


    本文全部代码

    🐾 test.cpp

    #define _CRT_SECURE_NO_WARNINGS 1
    #include<iostream>
    #include<vector>
    using namespace std;
    //每使用一段代码都要把其他的代码注释掉

    //1.友元——友元不能被继承
    /*基类的友元函数/类无法直接访问派生类的私有成员,可以类比成你的朋友的朋友不等同于是你的朋友,
    因此友元关系不具有继承性,如果需要友元访问派生类成员,就必须在派生类中重新说明友元*/

    //前置声明Student(为了让编译器走到友元函数时能向上查找到 Student):提前告诉编译器Student是一个类
    class Student;

    //基类—Person定义
    class Person
    {
    public:

    //声明Display是友元函数,使其能访问Person的保护成员_name
    friend void Display(const Person& p, const Student& s);

    protected:
    string _name = "千余"; //姓名
    };

    //派生类Student的定义—继承Person的成员,但基类的友元不会自动生成派生类的友元
    class Student : public Person
    {
    //如果这里不再声明一次友元,Display就无法访问Student的保护成员
    friend void Display(const Person& p, const Student& s);

    protected:
    int _stuid = 888; //学号
    };

    //友元函数的实现
    void Display(const Person& p, const Student& s)
    {
    cout << p._name << endl;//正常访问,因为Person声明了Display为友元
    cout << s._stuid << endl;//只有Student也声明友元,才能正常使用

    //验证了:基类的友元不能直接访问派生类的私有/保护成员,必须要在派生类中单独声明
    }

    void Test1()
    {
    Person p;
    Student s;
    Display(p, s);
    }

    int main()
    {
    Test1();
    return 0;
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /* 2.静态成员在继承体系中具有共享性:
    基类的静态成员(静态变量/静态函数)在整个继承体系中仅存在一份,派生类和基类共享该成员,不会因为继承而产生多个。
    这与非静态成员不同 —— 非静态成员每个对象独一份。
    也就是说对于基类的静态成员而言,对其进行初始化则说所有派生类的该成员都会被初始化成相同值,
    并且一个类的静态成员进行修改也会影响其他所有相关的派生类和基类。*/

    class Student;
    //静态成员
    class Person
    {
    public:
    string _name;//非静态:每个对象一份

    static int _count;//静态:整个类共用一份
    };

    //静态成员必须在类外初始化
    int Person::_count = 1;

    class Student : public Person
    {
    protected:
    int _stuid;
    };

    void Test2()
    {
    Person p;
    Student s;

    //非静态成员:每个对象独立
    //输出两个不同的地址,p和s是两个不同对象,各自有各自的_name
    cout << &p._name << endl;
    cout << &s._name << endl;
    cout << endl;

    //静态成员:全家族共用一份
    //输出同一个地址,静态成员在静态区,不属于任何对象,Person+所有派生类Student共用这一个_count
    cout << &p._count << endl;
    cout << &s._count << endl;
    cout << endl;

    //修改一个,全部变
    cout << p._count << endl;// 1
    cout << s._count << endl;// 1
    cout << endl;
    Person::_count++;

    // 公有的情况下,基类派生类指定类域都可以访问静态成员
    // 静态成员不属于任何一个对象,是存在静态区的
    // 可以把静态成员看成被类域限制的全局变量,所以要访问就需要使用域作用限定符::来突破类域访问
    cout << Person::_count << endl;// 2
    cout << Student::_count << endl;// 2
    cout << endl;
    }
    int main()
    {
    Test2();
    return 0;
    }

    /*结论:
    静态成员变量必须在类外初始化,否则会触发链接错误;

    静态成员函数只能访问静态成员变量,无法访问非静态成员;

    公有的情况下,基类派生类指定类域也可以访问静态成员;

    继承体系中所有类(基类,派生类)共享同一份静态成员,修改一处会影响全局。

    */

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    /*
    单继承与多继承
    单继承:一个派生类只有⼀个直接基类时称这个继承关系为单继承
    多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承

    模型
    单继承:class Person
    |
    class Student:publilc Person
    |
    class PostGraduate:public Student

    多继承:class Studentclass Teacher
    \\/
    class AssistanT:public Student,public Teacher
    */

    //多继承
    class Student
    {
    protected:
    string _name; //姓名
    };

    class Teacher
    {
    protected:
    int _id = 123; //职工编号
    };

    class Assistant : public Student, public Teacher
    {
    protected:
    string _majorCourse; //主修课程
    };

    void Test3()
    {
    Assistant a;

    }

    int main()
    {
    Test3();
    return 0;
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /*
    菱形继承:虚继承解决“数据冗余”与“二义性”
    菱形继承是指“一个派生类同时继承两个基类,而这两个基类又共同继承自一个顶层基类”的结构(并非一定是个菱形结构的图)

    模型:
    class Person
    / \\
    class Student:public Personclass Teacher:public Person
    \\ /
    class Assistant:public Student,public Teacher

    */

    // 顶层基类

    class Person
    {
    public:
    string _name; //会被Assistant继承两次
    };

    // 中间基类1
    class Student : public Person
    {
    protected:
    int _num; //学号
    };

    // 中间基类2
    class Teacher : public Person
    {
    protected:
    int _id; //职工编号
    };

    class Assistant : public Student, public Teacher
    {
    protected:
    string _majorCourse; //主修课程
    };

    /*
    查看二义性:

    1.在“Assistant a;”处打断点,按F5开始调试,调试->窗口->监视;

    2.点开a前面的小箭头:
    +a Assistant
    +Student // 第一个基类部分
    +Person // 里面有一个 _name
    _num
    +Teacher // 第二个基类部分
    +Person // 里面又有一个 _name
    _id
    _majorCourse

    3.再分别展开Student和Teacher:
    展开 Student → 里面有个 Person → 有一个 _name
    展开 Teacher → 里面也有个 Person → 有另一个 _name
    */

    void Test4()
    {
    Assistant a;
    //a._name = "张三"; //error C2385: 对“_name”的访问不明确(到底是Student::_name还是Teacher::_name呢?)

    //二义性:两个都叫_name,编译器不知道要的是哪个
    a.Student::_name = "李四";
    a.Teacher::_name = "王五";

    // 只能显式指定,但数据冗余仍存在,没有解决
    cout << a.Student::_name << endl; // 输出李四
    cout << a.Teacher::_name << endl; // 输出王五
    }
    int main()
    {
    Test4();
    return 0;
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    //虚继承—-彻底解决菱形继承问题—让顶层基类只存在一份,不再重复

    /*
    如果没有 virtual 虚继承, Assistant 继承 Student 和 Teacher ,而两者又都继承 Person ,会导致:

    1. Person 子对象在 Assistant 里存在两份,造成数据冗余;

    2. 访问 a._name 时会产生二义性,编译器不知道该用哪一份 Person 的 _name

    */

    //没有虚继承时:构造的顺序是 Person->Student->Person->Teacher->Assistant,会造成两次Person

    /*
    虚继承规则: – 中间类 Student 、 Teacher 里的 Person(name) 会被编译器直接忽略、不执行
    ​ – 只有最终派生类 Assistant 才能初始化 Person
    */

    //顶层基类
    class Person
    {
    public:
    Person(const char* name)
    :_name(name)
    {
    }
    public:
    string _name; // 姓名
    };

    // 中间基类1:虚继承Person(添加virtual)
    //virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Student)加
    class Student : virtual public Person
    {
    public:
    Student(const char* name, int num)
    :Person(name)// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
    , _num(num)// 只会初始化自己的成员变量
    {
    }
    protected:
    int _num; //学号
    };

    // 中间基2:虚继承Person(添加virtual)
    //virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Teacher)加
    class Teacher : virtual public Person
    {
    public:
    Teacher(const char* name, int id)
    :Person(name)// 在虚继承下,中间基类会暂时不初始化顶层基类,所以这行会被跳过
    , _id(id)// 只会初始化自己的成员变量
    {
    }
    protected:
    int _id; // 职工编号
    };

    // 最终派生类:菱形继承(Person成员会被合并成仅一份)
    class Assistant : public Student, public Teacher
    {
    public:

    // 关键:虚继承下,顶层基类的构造由最终派生类显式调用
    Assistant(const char* name1, const char* name2, const char* name3)
    :Person(name1)//只有这个会执行,下面两个初始化不会进去
    , Student(name2, 1)
    , Teacher(name3, 2)
    , _majorCourse("计算机")
    {
    }

    protected:
    string _majorCourse; // 主修课程
    };

    /*
    验证:
    1.断点打在“Assistant a("张三", "李四", "王五");”

    2.按F5->窗口->监视->展开a:
    +a Assistant
    +Person // 只有一份!
    _name:张三
    +Student
    _num:1
    +Teacher
    _id:2
    _majorCourse:计算机

    */
    void Test5()
    {
    Assistant a("张三", "李四", "王五");

    //上面有三次Person(name),但其实就只有在Assistant里一次,其它两次会跳过。
    //所以是张三
    }

    int main()
    {
    Test5();
    return 0;
    }

    🐶 🐾 ✨ 🐾 🐶


  • 欢迎留言交流 ​
  • 期待你的评论与建议 ​
  • 留下你的想法吧

  • 举爪爪求关注

    谢谢你看到这里呀

    如果喜欢这篇内容,点个关注,下次更新不迷路✨

    👍 点赞 ⭐ 收藏 💬 评论

    赞(0)
    未经允许不得转载:171主机测评 » 【C++】C++继承入门(下):友元、静态成员与菱形继承的底层逻辑
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址