【C++】从0到1讲继承|复杂的菱形继承

news2025/1/11 2:31:50

 

 

个人主页:🍝在肯德基吃麻辣烫

我的gitee:gitee仓库
分享一句喜欢的话:热烈的火焰,冰封在最沉默的火山深处。


前言

本文主要讲述的是继承的概念,以及基类和派生类和衍生出的各种东西,还有多继承,菱形继承等,从0到1讲解继承。


一、什么是继承?

与日常生活中的人的继承相关,你可以继承你父亲的财富,继承你父亲的房产等等。

二、继承的语法表示

1.基类和派生类

基类也叫父类,派生类也叫子类,子类通过继承方式,继承父类。

2.继承的方式

继承方式有三种:public,protected,private。

不同的继承对应着不同的访问方式的变化,变化如下:

 其中,我们最经常使用的是公有继承。

有需要注意的点:
1.基类的private一旦被继承,就不可见。这里的不可见是在派生类中无法被访问,而不是没有继承。

2.只推荐使用公有继承,其他的继承方式不推荐使用。

3.class默认的继承方式是私有继承,struct默认的继承方式是公有继承,但是推荐显式写出继承方式。

三、基类和派生类的对象赋值转换

  • 1.子类对象可以直接赋值给基类对象/基类的指针/基类的引用但是基类对象不能赋值给子类。因为编译器认为基类的对象类型不完全包含子类。在这里也叫做切片。
  • 2.基类对象本身不能赋值给子类对象。
  • 3.基类的指针/引用可以赋值通过强制类型转换赋值给子类的指针/引用,但必须是基类的指针指向子类才安全。(了解即可)

 四、继承中的新概念——隐藏(重定义)

  • 1.在子类继承父类中,子类的作用域和父类的作用域是独立的。
  • 2.如果子类和父类有同名成员变量,子类成员会将父类的同名成员变量隐藏起来,可以理解成父类的成员变量被揣进裤兜里了。
  • // Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
    class Person
    {
    protected :
        string _name = "小李子"; // 姓名
        int _num = 111;  // 身份证号
    };
    
    class Student : public Person
    {
    public:
        void Print()
        {
            cout<<" 姓名:"<<_name<< endl;
            cout<<" 身份证号:"<<Person::_num<< endl;
            cout<<" 学号:"<<_num<<endl;
        }
    
    protected:
        int _num = 999; // 学号
    };
    
    void Test()
    {
        Student s1;
        s1.Print();
    };

    上面代码的情况就符合隐藏,虽然代码能跑,不过不容易进行区分。

  • 3.如果子类和父类有同名的成员函数,同样也会隐藏起来,这个也叫做重定义。
  • // B中的fun和A中的fun不是构成重载,因为不是在同一作用域
    // B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
    
    class A
    {
    public:
    
        void fun()
        {
            cout << "func()" << endl;
        }
    
    };
    
    class B : public A
    {
    public:
    
        void fun(int i)
        {
            A::fun();
            cout << "func(int i)->" <<i<<endl;
        }
    
    };
    
    void Test()
    {
        B b;
        b.fun(10);
    };

    对于类成员函数来说,只要同名就构成隐藏。

  • 4.实际中最好不要定义重名成员。

五、派生类的默认成员函数

  • 1.构造函数

在子类的构造函数中会调用父类的构造函数。所以如果父类中没有默认构造函数,则在子类的构造函数的初始化列表中必须显式地调用父类的构造函数来完成父类那部分成员的初始化。

  • 并且在子类的构造函数的初始化列表中,会按照声明出现的顺序依次初始化,所以应该先调用父类的构造函数,再初始化子类的成员。
  • 总结:构造保证先付猴子
  • 2.拷贝构造

在子类的拷贝构造中,必须显式地调用父类的拷贝构造,否则编译器会自动调用父类的默认构造,而不是调用父类的拷贝构造。

拷贝构造也是构造,最后作用域结束会调用父类的析构对父类成员进行释放。

  • 3.赋值

在子类的赋值运算符重载同样需要显式地调用父类的赋值运算符重载。

  • 4.析构

析构函数就不同了,不能显式调用父类的析构函数。

原因如下:
1.在构造函数中是先构造父类再构造子类,析构的顺序应该是先析构子类再析构父类。如果显式调用就会改变顺序,不合理。

2.有可能在子类会使用父类的成员,如果父类先析构,可能会造成非法访问。

六、继承和友元

友元关系不能继承,也就是说基类的友元不能访问子类的私有成员和保护成员。

举个简单的例子:我父亲的朋友不是我的朋友。

如果想要父类的友元也变成子类的友元,则需要在子类中声明该函数为友元。

七、继承和静态成员

在继承中,你可以认为静态成员继承了,也可以认为没有继承。

因为对于静态成员,子类只继承了使用权。

在整个继承体系中,静态成员只有一份,子类和父类都可以共同使用。

用下面一段代码可以证明:
 

class Person
{
public :

    Person () {++ _count ;}
protected :

    string _name ; // 姓名
public :

    static int _count; // 统计人的个数。
};

int Person :: _count = 0;

class Student : public Person
{
protected :

    int _stuNum ; // 学号

};

class Graduate : public Student
{
protected :
    string _seminarCourse ; // 研究科目
};

void TestPerson()
{
    Student s1 ;
    Student s2 ;
    Student s3 ;
    Graduate s4 ;

    cout <<" 人数 :"<< Person ::_count << endl;

    Student ::_count = 0;

    cout <<" 人数 :"<< Person ::_count << endl;
}

这段代码计算整个继承体系一共创建了多少个类对象,包括父类和子类。

八、菱形继承和菱形虚拟继承

继承可以分为单继承和多继承.

下面这样的情况就是多继承。

 


而菱形继承就是多继承的一种特例。

8.1 菱形继承的问题

对于菱形继承来说,真正出问题的是上图的Assistant。

1.在它的成员中有两份重复的Person的成员,出现了数据冗余的情况。

2.如果想在Assistant中调用Person的成员变量/成员函数,编译器就无法确定到底该调用Teacher继承下来的还是调用Student继承下来的。

在上面的继承体系中,内存关系如下图:

 按照各个类声明出现的顺序依次继承,内存从上到下放置。

为了解决菱形继承的问题,我们可以使用菱形虚拟继承来解决。

我们在菱形继承体系的腰部加上两个virtual,让Student和Teacher继承Person是虚拟继承。 

 

用虚拟继承可以解决菱形继承的原因:
 

 我们重新定义一个菱形继承:
 

class A
{
public:
	int _a;
};

// class B : public A
class B : virtual public A
{
public:
	int _b;
};

// class C : public A
class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main(){
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

在这个菱形继承中,我们通过调试观察可以发现,D对象的内存地址如下:

 可以看到,前两个地址是B对象的地址,接着的是C对象的地址,再下来的地址是D对象中的成员_d的地址,而最后一个地址,其实是A对象中的成员_a的地址。

使用了菱形虚拟继承后,继承的对象在内存中会放到内存的最下面,在B和C对象的第一个地址中,存的是一个虚基表的地址,在虚基表中存的又是该对象相对于A对象的偏移量。

所以菱形虚拟继承可以通过查找到对象对应的虚基表的偏移量来获取对象A的地址,进而访问对象A的成员。这样就不用再在每个继承对象中都存一份A对象,并且在D子类中只有一份A对象,解决了数据冗余和二义性的问题。

A对象越大,越能够节省空间,因为在B和C对象中,只存了一个指针,指向虚基表,只有4字节。如果是存一个很大的数组,则需要花费巨大的空间。

总结:菱形虚拟继承是在对象中存一个指针,该指针指向一个虚基表,在虚基表中存着该对象相对于父类对象的偏移量,而父类在内存中是存储在整个继承体系内存的下面,能够通过偏移量找到父类对象的地址,进而访问父类对象的成员。

九、继承和组合

继承:白盒测试,每一部分细节都展示,需要测试每一部分代码的功能。

继承中每一个子类对象都是一个父类对象。

组合:黑盒测试,隐藏了细节,只暴露接口,用接口进行测试。 

组合中每一个子类对象都有一个父类对象。

十、常见笔试面试题

1. 什么是菱形继承?菱形继承的问题是什么?
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
3. 继承和组合的区别?什么时候用继承?什么时候用组合?

1.菱形继承是多继承中的一种,假如有一个父类对象A,子类对象B继承A,C也继承A,同时有一个子类对象D同时继承了B和C,这样的继承关系就是菱形继承。菱形继承的问题是在B类和C类中都有一份A类的成员,造成数据冗余,并且如果用D类对象访问A类的成员时,会出现二义性,也就是不知道该访问谁。

2、在B类继承A类和C类继承A类时加上一个virtual,就是菱形虚拟继承。菱形虚拟继承是在B类和C类中存储一个指针,该指针指向一个叫做虚基表的表,表中存着该对象和父类对象的地址偏移量,可以通过自己相对父类的偏移量找到父类的地址,进而访问父类成员。在上述的菱形继承案例中,A类的成员在整个继承体系中只有一份,就解决了二义性问题。而在B类和C类中只存储一个虚基表指针,可以解决数据冗余的问题。

3.继承是子类继承父类,可以使用父类的所有属性和方法,组合是将已存在的类作为新的类的成员,两者无上下级的关系。当我们只需要用一个类的接口函数时,用组合;其他情况用继承。

总结

本文讲解了C++继承的众多概念。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/787336.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

js实现css样式变换的实训

js实现css样式变换的实训 一、需求二、效果展示1.效果展示 三、实现四、其他1.其它系统 一、需求 完成以下功能&#xff1a; 1.掌控板三颗RGB灯初始所有RGB灯为红色 2.当掌控板P被触摸时&#xff0c;第一颗灯为白色&#xff0c;其他为红色&#xff1b;当掌控板Y被触摸时&…

C# GDI+编程之Graphics类

最近需要使用到C#DrawLine绘制直线这个功能&#xff0c;对这个了解的不多&#xff0c;记录一下使用的时候遇到的问题。 绘制线的基础部分&#xff0c;这个之前在《C#自学笔记&#xff08;四十&#xff09;之Windows绘图》就写过&#xff0c;有兴趣的可以看下 我这里主要说下Gra…

选择最佳安全文件传输方法的重要性

在数字化时代&#xff0c;文件的传输是商务、教育、科研、医学等领域不可或缺的工作流程。为了保障数据安全&#xff0c;选择最佳安全文件传输方法非常关键。在本文中&#xff0c;我们将探讨选择最佳安全文件传输方法的重要性。 第一、最佳安全文件传输方法可以保证文件内容不被…

【C++进阶】可变模版参数

一、前言 我们在之前Linux的学习中了解过命令行参数&#xff0c;可以让我们在命令行中传入多个参数&#xff0c;并且之前在学习printf&#xff0c;scanf等接口时&#xff0c;接触过可变模版参数&#xff1a; 而今天学习的可变参数模板和普通模板的语义是一样的&#xff0c;只…

Mac 系统钥匙串证书不受信任

Mac 系统钥匙串证书不受信任 解决办法 通过尝试安装 Apple PKI 的 Worldwide Developer Relations - G4 (Expiring 12/10/2030 00:00:00 UTC) 解决该异常问题 以上便是此次分享的全部内容&#xff0c;希望能对大家有所帮助!

[USACO14OPEN] Odometer S

洛谷[USACO14OPEN] Odometer S 题目大意 当一个数的每一位中有至少一半的数字相同&#xff0c;那么这个数就是一个有趣的数。求区间 [ L , R ] [L,R] [L,R]中有多少个有趣的数。 100 ≤ L ≤ R ≤ 1 0 18 100\leq L\leq R\leq 10^{18} 100≤L≤R≤1018 题解 这道题很容易能想…

AcWing242. 一个简单的整数问题

输入样例&#xff1a; 10 5 1 2 3 4 5 6 7 8 9 10 Q 4 Q 1 Q 2 C 1 6 3 Q 2输出样例&#xff1a; 4 1 2 5 #include<bits/stdc.h> using namespace std; const int N1e55; int n,m,a[N],c[N],x,y,d; char ch; int lowbit(int x){return x&-x; } void add(int x,int…

pytest--allure报告中添加用例详情

前言 前面介绍了如何生成allure的报告&#xff0c;看着allure的页面非常好看&#xff0c;但是感觉少了一些内容&#xff0c;allure还可以增加一些用例详情内容&#xff0c;这样让我们的报告看着更加绚丽。 allure增加用例详情 我们可以在报告测试套件中增加用例详情内容。 …

Spring初识(四)

文章目录 前言一.Bean的作用域1.1 作用域例子1.2 Bean的作用域类型 二.Bean的生命周期 前言 在前面我们学习了spring简单的读取和存储对象之后,Spring 中 Bean 是最核心的操作资源&#xff0c;我们接下来会介绍Bean对象. 一.Bean的作用域 什么是Bean作用域呢? 限定程序中变…

[DDPM] Denoising Diffusion Probabilistic Models

直接看paper云里雾里&#xff0c;一些推荐的讲解&#xff1a; The Annotated Diffusion Model 生成扩散模型漫谈&#xff08;一&#xff09;&#xff1a;DDPM 拆楼 建楼 生成扩散模型漫谈&#xff08;二&#xff09;&#xff1a;DDPM 自回归式VAE 生成扩散模型漫谈&#xff…

SQL-每日一题【619.只出现一次的最大数字】

题目 MyNumbers 表&#xff1a; 单一数字 是在 MyNumbers 表中只出现一次的数字。 请你编写一个 SQL 查询来报告最大的 单一数字 。如果不存在 单一数字 &#xff0c;查询需报告 null 。 查询结果如下例所示。 示例 1&#xff1a; 示例 2&#xff1a; 解题思路 1.题目要求我…

MyBatis---多表查询,动态sql的详细介绍

目录 1.命名规则 1.resultMap&#xff08;对应类属性名称与数据库字段名称&#xff09; 2.多表查询&#xff08;ResultMap&#xff09; 1.创建articleInfo类 2.创建ArticleMapper.xml配置文件和ArticleMapper接口 ①&#xff1a;不建议使用 &#xff08;文件之间耦合严重…

十大排序算法详解

目录 1. 冒泡排序 a. 思路 b. code 2. 插入排序 a. 思路 b. code 3. 希尔排序【插入排序plus】 a. 思路 b. code 4. 选择排序 a. 思路 b. code 5. 基数排序 a. 前置知识 b. 思路 c. code 6. 计数排序 a. 思路 b. code 7. 桶排序&#xff08;计数排序plus &…

怎么在shell中查看python版本以及降低anaconda的python版本

输入命令 python --version 电脑anaconda的python版本为3.11&#xff0c;如何降低版本 &#xff0c;输入命令 conda install python3.9 当安装完anaconda后&#xff0c;ubuntu系统的shell命令行最前面会出现base字样&#xff0c;此时要退出&#xff0c;就输入命令 conda deacti…

如何开启QQ邮件的SMTP服务以及如何使用Python发送邮件

如何开启QQ邮件的SMTP服务以及如何使用Python发送邮件 &#x1f607;博主简介&#xff1a;我是一名正在攻读研究生学位的人工智能专业学生&#xff0c;我可以为计算机、人工智能相关本科生和研究生提供排忧解惑的服务。如果您有任何问题或困惑&#xff0c;欢迎随时来交流哦&…

Qt应用开发——Drag and Drop

目录 一、前言 二、相关事件和类 三、实例 拖动文件到编辑框 一、前言 在实际场景中&#xff0c;经常会有导入文件的需求。导入文件一般两种方式&#xff0c;第一种QFileDialog显示一个文件选择窗口&#xff0c;选择后处理和显示。第二种就是使用拖放机制实现。还有在一些制…

.faust加密勒索数据库恢复---惜分飞

有客户的win服务器被勒索病毒加密,里面运行有用友系统的Oracle数据库&#xff0c;加密提示为&#xff08;camry2020aol.com&#xff09;&#xff1a; 加密的数据文件类似&#xff08;.DBF.id[0E564ACA-3493].[camry2020aol.com].faust&#xff09;: 通过工具检测发现少量bl…

(2)前端控制器的扩展配置, 视图解析器类型以及MVC执行流程的概述

SpringMVC入门程序的扩展说明 注册前端控制器的细节 在web.xml文件注册SpringMVC的前端控制器DispatcherServlet时使用url-pattern标签中使用/和/*的区别 /可以匹配.html或.js或.css等方式的请求路径,但不匹配*.jsp的请求路径/*可以匹配所有请求(包括.jsp请求), 例如在过滤器…

Kotlin空安全类型之平台类型

前言 众所周知,Kotlin的空安全很方便,能避免绝大部分的空安全问题 一般类型分为: 可空类型如 String? 不可空类型 String其中可空类型的String?可以赋值null或String类型的值,在使用时必须显式的进行空处理 而不可空类型的String,只能赋值String类型,无法向其赋值null,使用时…

JAVA基础-Stream流

引言 Java 8 版本新增的Stream&#xff0c;配合同版本出现的Lambda &#xff0c;给我们操作集合&#xff08;Collection&#xff09;提供了极大的 便利。Stream流是JDK8新增的成员&#xff0c;允许以声明性方式处理数据集合&#xff0c;可以把Stream流看作是遍历数据集 合的一个…