【C++技能树】继承概念与解析

news2025/1/19 11:09:42

在这里插入图片描述
Halo,这里是Ppeua。平时主要更新C++,数据结构算法,Linux与ROS…感兴趣就关注我bua!

继承

  • 0. 继承概念
    • 0.1 继承访问限定符
  • 1. 基类和派生类对象赋值兼容转换
  • 2. 继承中的作用域
  • 3. 派生类中的默认成员函数
  • 4.友元
  • 5.继承中的静态成员
  • 6.菱形继承
  • 7.菱形虚拟继承
  • 总结
  • 总结

在这里插入图片描述

0. 继承概念

​ 设想一个场景,你需要设计学生、老师、教授…的类,除了每个身份中独有的信息,例如:学号,工号,教授身份号,但是他们都有一个共同的属性,就是人.所以我们可以先设计一个类:人.

每设计一个新的类都可以复用人这个类,增加了代码的复用性.这就是C++中的新特性:继承

我们之前接触的函数重载是函数层面的复用,继承则是类层面的复用

class Person{
public:
   void print()
   {
       cout<<age<<" "<<name<<endl;
   }
   int age=0;
   string name="Peter";
   string address;
   int tel;
};
class Student:public Person
{
public:
   int _stuid;
};

我们可以通过调用来看看其结构模型.

int main()
{
    Person p1;
    cout<<p1.name<<endl;
    Student s1;
    cout << s1._stuid;
}

image-20230823184212791

子类中可以共享父类中的变量,父类不可以访问子类的变量.

0.1 继承访问限定符

在类中有访问限定符,同样的,在继承方式上也有访问限定符.

24756bb84f210a146239f27a1e1494b

派生类(子类)可以通过以下方式来继承基类(父类):

image-20230823193458458

在这里新出现了一个权限符号,protected.它与private是类似的:

protected修饰的变量在类外与private类似,不能被访问.

但是在派生类中可以访问protected修饰的变量,而不能访问private修饰的变量

所以权限的大小的关系为:

public>protected>private

所以,在权限的继承中有一个最小原则.

  1. 以public来继承,可以继承的变量为:public,protected
  2. 以protected来继承.可以继承的变量为:protected
  3. 以private来继承,无可以继承的变量

通常情况下,我们一般用public来继承,protected/private的继承方式实用性不高

1. 基类和派生类对象赋值兼容转换

派生类可以转换为基类,而基类并不能转换为派生类.

例如上面Person与Student的例子:

可以实现
Person p1;
Student s1;
p1=s1;
不能实现
s1=p1;

这其实很好理解.父类中的属性往往比子类中的成员多,子类中的成员可以通过切割多余的成员转换到父类中.

通过这样的方式,这中间不涉及强制类型转换.我们可以通过以下这个例子来看.

我们知道,强制类型转换会产生一个临时变量.例如:

int a=10;
double b=a;

这当中会产生一个临时变量double a,来赋值给b.

Person &p1=s1;

如果产生了临时变量,这个赋值是不可以的.但通过编译器验证,我们发现这样是可以的.侧面的说明这并不是引用.

但对于p1的成员进行修改,s1也会同样被修改.

Student s1;
Person p1 = s1;
p1.name = "H";

Before:

image-20230823231534645

After:

image-20230823231912045

虽然这种限制(子类可以转父类,父类不可转子类)可以通过指针直接访问内存的方法解除

原来指向的是student对象,现在强制转换为student指针是可以的.

Student s1;
Person *p1 = &s1;
Student* sp1 = (Student*)p1;
sp1->age = 10;

原来指向的是person对象,现在强制转换为student指针则会发生越界

Student s1;
Person pp1;
Person *pp1 = &s1;
Student* sp1 = (Student*)pp1;
sp1->age = 10;

2. 继承中的作用域

一个{}是一个作用域,所以在基类和派生类中,都有自己的作用域.

所以当在派生类中定义与基类相同的名的变量的时候就会构成隐藏:隐藏父类的相关变量

当在派生类中定义与基类相同的函数时(只需要名字相同),就会构成重定义(隐藏):重定义父类相关函数

如果需要访问父类被隐藏的属性,需要在前加类域限定符才能访问

#include<iostream>
using namespace std;
class Person {
public:
    void print()
    {
        cout <<"Person:" << age << endl;
    }
    int age = 0;
};
class Student :public Person
{
public:
    void print(int i)
    {
        cout <<"Student:" << age << endl;
    }
    int age=10;

};
int main()
{
    Student s1;
    cout << s1.age; //10
    s1.print(1);// student:10
    cout << s1.Person::age;//0
    s1.Person::print();//person:0
    return 0;
}

3. 派生类中的默认成员函数

总的来说,派生类中的所有涉及父类的行为都要从父类当中去寻找相关方法论:

  1. 派生类初始化的时候会先调用父类的初始化函数,在调用自己的.若父类没有默认构造函数则需要在派生类中的初始化列表中调用父类构造函数传入参数.(**为什么需要在初始化列表中调用不在函数体里调用呢?**自定义类型成员(且该类没有默认构造函数时)在初始化列表中调用相关文章:初始化列表

    class Person{
    public:
        Person(int sage)
        :age(sage)
        {
    
        }
       void print()
       {
           cout<<age<<" "<<name<<endl;
       }
       int age=0;
       string name="Peter";
       string address;
       int tel;
    private:
        int s=0;
    };
    class Student:public Person
    {
    public:
       Student(int _age)
        :Person(_age)
        {
        
        }
       int _stuid;
    };
    
  2. 拷贝构造与赋值运算符重载需要通过显式调用父类中的方法来完成.

    class person
    {
    public:
        person(const char *name="peter")
        :_name(name)
        {
            cout<<"person()";
        }
        person(const person&p1)
        :_name(p1._name)
        {
            cout<<"person(const Person& P)"<<endl;
        }
        person& operator=(const person&p)
        {
            cout<<"person operator=(const person&p)"<<endl;
            if(this!=&p)
                _name=p._name;
            return *this;
        }
    
        string _name;
         
    };
    class student:public person
    {
    public:
        student(const char*name="zhangsan",int id=0)
        :person(name),
        _id(id)
        {
            cout<<"student()"<<endl;
        }
    
        student(const student&s1)
        :person(s1),
        _id(s1._id)
        {}
    
        student& operator=(const student& s1)
        {
            if(this!=&s1)
            {
                //出现隐藏,想要调用父类的=
                person::operator=(s1);
                _id=s1._id;
            }
            return *this;
        }
        void print()
        {
            cout<<_id<<" "<<_name<<endl;
        }
    private:
        int _id;
    };
    
  3. 析构函数不需要显式调用父类(也不能),编译器会自己调用完派生类的析构函数,在调用基类的析构函数

    其实这也很好理解.从函数栈帧方面:先创建父类再创建子类,自然先析构子类再析构父类.

    从内存保护方面:在子类中有可能调用了父类的成员对象,如果先消除父类,会导致子类中出现野指针的情况

16375783a643a74a0431af96eefa36c

4.友元

父类的友元不能访问子类的成员变量。(父亲的朋友不是孩子的朋友

class B;
class A{
friend void print(const A& a1,const B& b1);
private:
    int a=10;

};
class B:public A{
private:
    int b=100;
};
void print(const A& a1,const B&b1)
{
    cout<<a1.a<<endl; //right
    cout<<b1.a<<endl; //right
    cout<<b1.b<<endl; //error
}

在上面的例子中可以看到:print函数可以访问A的private,而不能访问B中的private

5.继承中的静态成员

静态成员只会存在一份.在父类当中,子类中可以继承静态成员.但是继承的是访问权,只能访问不能修改

且其是存在类当中,也就是无论几个对象,访问的都是同一个静态成员

class B;
class A{
friend void print(const A& a1,const B& b1);
public:
    static int count;
private:
    int a=10;
};
int A::count=10;
class B:public A{
public:
    void print()
    {
        cout<<count<<" "<<endl;
    }
   
private:
    int b=100;
   
};



void print(const A& a1,const B&b1)
{
    cout<<a1.count<<endl;
    cout<<b1.count<<endl;
    cout<<A::count;
}

int main()
{
    A a1;
    B b1;
    print(a1, b1);//10
    A::count++;
    print(a1, b1);//11
    
}

6.菱形继承

在c++中,多继承的结构模型是这样的,使用不当时会导致出现菱形继承的情况.导致内存中会重复出现一些变量.也会导致二义性

cdd3d5768cf48df625009ef25537898

例如,在person中有一个表示年龄的age,在student与teacher中各有表示年龄的age,当professor继承student与teacher时,就会有两个age.这在现实环境中显然是不合理的

image-20230824171105761

#include<iostream>
using namespace std;
class Person {
public:
    int age = 10;
};

class Student :public Person
{
public:
    int stuid = 1;

};
class Teacher :public Person
{
    int teaid = 2;
};
class Professor:public Student,public Teacher
{
    int profeid = 3;
};
int main()
{
    Professor p1;
    p1.Student::age = 100;
    p1.Teacher::age = 200;

}

其在内存中的模型为:

94c65b2a5cf3f4035fcc7ea7cd1efe6

可以看到此时出现数据冗余二义性.

C++解决这个问题的方法则是:菱形虚拟继承

7.菱形虚拟继承

一个新的关键字:virtual,在之后用到很多,但每个地方的含义都不大相同.

在继承方面,我们用virtual来修饰基类.也就是在上方结构模型的腰部

class Person {
public:
    int age = 10;
};

class Student :virtual public Person
{
public:
    int stuid = 1;

};
class Teacher :virtual public Person
{
    int teaid = 2;
};
class Professor:public Student,public Teacher
{
    int profeid = 3;
};
int main()
{
    Professor p1;
    p1.Student::age = 100;
    p1.Teacher::age = 200;
    cout<<sizeof(p1);
}

此时的内存模型为

b991e55de33001344c16e25553e8e45

结构模型为:

image-20230824174055432

我们发现,重复出现的变量age修饰完只出现了一个.

观察内存模型,我们发现原来存age的地方,变成了一个指针.而age被放在了整个对象的最后一个位置.

f65d42e29d4e453b35e1d5ff4da514c

打开内存模型,我们发现,这个指针指向了一块内存空间.叫虚基表.其中第一个位置为:0(其存放的为虚表偏移量) 第二个位置存着该指针相较于age的偏移量

为什么要这样设计呢:

  1. 解决了数据冗余

  2. 相同的对象可以调用同一个虚基表

  3. 存放偏移量让切割成为了可能

    这里可以这样理解,当我创建了一个teacher的对象,将professor传入,则完成了切割,数据从teacher的指针开始访问,若我此时想要访问a,我直接读取偏移量即可.

总结

继承使C++底层变得复杂了起来,在日常使用中,需要避免出现菱形继承的问题.

更推荐使用组合:在一个类中调用另一个封装完的类,此时被调用的类的细节对调用类来说是不可见的.

相较于继承,更推荐使用组合的方式.高内聚低耦合一直是我们设计程序的原则

型,我们发现原来存age的地方,变成了一个指针.而age被放在了整个对象的最后一个位置.

[外链图片转存中…(img-WM0k8lWU-1692871122130)]

打开内存模型,我们发现,这个指针指向了一块内存空间.叫虚基表.其中第一个位置为:0(其存放的为虚表偏移量) 第二个位置存着该指针相较于age的偏移量

为什么要这样设计呢:

  1. 解决了数据冗余

  2. 相同的对象可以调用同一个虚基表

  3. 存放偏移量让切割成为了可能

    这里可以这样理解,当我创建了一个teacher的对象,将professor传入,则完成了切割,数据从teacher的指针开始访问,若我此时想要访问a,我直接读取偏移量即可.

总结

继承使C++底层变得复杂了起来,在日常使用中,需要避免出现菱形继承的问题.

更推荐使用组合:在一个类中调用另一个封装完的类,此时被调用的类的细节对调用类来说是不可见的.

相较于继承,更推荐使用组合的方式.高内聚低耦合一直是我们设计程序的原则

相关文章:优先使用对象组合,而不是类继承

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

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

相关文章

lenovo联想笔记本小新 潮7000-14IKBR 2018款(81GA)原装出厂Windows10系统镜像

自带所有驱动、出厂主题壁纸LOGO、Office办公软件、联想电脑管家等预装程序 链接&#xff1a;https://pan.baidu.com/s/1ynP4d5z7MPF9l5U5lCjDzQ?pwdhjvj 提取码&#xff1a;hjvj 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1a;ISO 文件大小&#x…

2.4 关系数据库

思维导图&#xff1a; 前言&#xff1a; 这段话描述了“关系数据库”及其背后的理论基础。首先&#xff0c;我们来拆分这段话并逐步解释每部分。 关系数据库是采用关系模型作为数据组织方式的数据库。 这句话的关键是“关系模型”。关系模型是一种表示和操作数据库的理论模型…

软考备考-程序员-考试介绍和考试大纲

软考程序员-考试介绍和考试大纲 全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试 计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;以下简称计算机软件资格考试&#xff09;是原中国计算机软件专业技术资格和水平考试&#xff0…

第三方系统测试怎么做?有些什么流程

系统测试 软件系统研发出来以后&#xff0c;产品的各个模板功能是否能够正常运转需要进行软件系统测试。而对于普通的互联网企业来说&#xff0c;软件测试的各项工作已经超出开发工作内容&#xff0c;由开发人员进行系统测试不仅需要耗费软件团队精力&#xff0c;可能测试效果…

Pyecharts数据可视化(三)

目录 1.绘制词云图 2.绘制桑基图 3.绘制平行坐标图 4.绘制结点图 5.绘制地图 本文主要介绍了如何利用Pyecharts绘制词云图、桑基图、平行坐标图、节点图和地图&#xff0c;虽然这些图平时不是很常用&#xff0c;但是看起来还是比较好看的&#xff0c;如果放在论文当中&am…

leetcode793. 阶乘函数后 K 个零(java)

阶乘函数后 K 个零 题目描述二分法代码模拟 题目描述 难度 - 困难 阶乘函数后 K 个零 f(x) 是 x! 末尾是 0 的数量。回想一下 x! 1 * 2 * 3 * … * x&#xff0c;且 0! 1 。 例如&#xff0c; f(3) 0 &#xff0c;因为 3! 6 的末尾没有 0 &#xff1b;而 f(11) 2 &#xf…

Bito使用手册

第一步&#xff1a;输入网站 https://alpha.bito.co/bitoai/ 第二步&#xff1a;填写邮箱 第三步&#xff1a;登录邮箱&#xff0c;获取验证码 第四步&#xff1a;填写验证码 第五步&#xff1a;完成

Scala的函数式编程与高阶函数,匿名函数,偏函数,函数的闭包、柯里化,抽象控制,懒加载等

Scala的函数式编程 函数式编程 解决问题时&#xff0c;将问题分解成一个一个的步骤&#xff0c;将每个步骤进行封装&#xff08;函数&#xff09;&#xff0c;通过调用这些封装好的步骤&#xff0c;解决问题。 例如&#xff1a;请求->用户名、密码->连接 JDBC->读取…

zabbix监控平台部署

目录 前言 一、zabbix的基本概述 &#xff08;一&#xff09;、zabbix的工作流程 &#xff08;二&#xff09;、zabbix的构成 &#xff08;三&#xff09;、zabbix的监控对象 &#xff08;四&#xff09;、zabbix的常用术语 &#xff08;五&#xff09;、zabbix进程详解…

植物根系基因组与数据分析

1.背景 这段内容主要是关于植物对干旱胁迫的反应&#xff0c;并介绍了生活在植物体内外以及根际的真菌和细菌的作用。然而&#xff0c;目前对这些真菌和细菌的稳定性了解甚少。作者通过调查微生物群落组成和微生物相关性的方法&#xff0c;对农业系统中真菌和细菌对干旱的抗性…

windows 2012服务器配置nginx后无法访问域名的问题

环境&#xff1a;Windows 2012 R2 Nginx 问题&#xff1a;确认域名解析到服务器ip已生效&#xff08;通过ping域名地址确认域名已指向该ip&#xff09;&#xff0c;确认nginx配置无误&#xff08;绑定域名、配置端口、配置网站文件目录&#xff09;&#xff0c;但无法从外网访…

6年打工人,我的所见、所想、所感。

咪哥杂谈 本篇阅读时间约为 7 分钟。 1 前言 本篇文章全部基于个人心得总结&#xff0c;欢迎大家讨论&#xff0c;无论是赞同还是反对&#xff0c;所有观点均接受。 原本是 5 周年打工人的心得&#xff0c;愣是让我拖了一年变成了 6 周年心得。 最近一年多一直投身于区块链行业…

vue的第2篇 开发环境vscode的安装以及创建项目空间

一 环境的搭建 1.1常见前端开发ide 1.2 安装vs.code 1.下载地址&#xff1a;Visual Studio Code - Code Editing. Redefined 2.进行安装 1.2.1 vscode的中文插件安装 1.在搜索框输入“chinese” 2.安装完成重启&#xff0c;如下变成中文 1.2.2 修改工作区的颜色 选中[浅色]…

回复:c#的Winform如何让ComboBox不显示下拉框?https://bbs.csdn.net/topics/392565412

组合框.Parent this;组合框.Items.AddRange(new object[] { "111", "222", "333", "444" });组合框.DropDownHeight 1;组合框.SelectedIndex 0;//组合框.DropDownStyle ComboBoxStyle.Simple; ComboBox 组合框 new ComboBox();Li…

编写中间件以用于 Express 应用程序

概述 中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。 中间件函数可以执行以下任务&#xff1a; 执行任何代码。对请求和响应对象进行更改。结束请求/响应循环。调用堆…

忘记了zip密码,怎么解压文件?

Zip压缩包设置了密码&#xff0c;解压的时候就需要输入正确对密码才能顺利解压出文件&#xff0c;正常当我们解压文件或者删除密码的时候&#xff0c;虽然方法多&#xff0c;但是都需要输入正确的密码才能完成。忘记密码就无法进行操作。 那么&#xff0c;忘记了zip压缩包的密…

SpringMVC_基本使用

一、JavaWEB 1.回顾 JavaWEB 1.1新建项目结构 新建 javaweb 项目目录结构 1.2导入依赖 依赖 <dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>…

1776_树莓派简介视频学习小结

全部学习汇总&#xff1a; GitHub - GreyZhang/little_bits_of_raspberry_pi: my hacking trip about raspberry pi. 卖树莓派的时候赠送了部分视频资料&#xff0c;今天看了一段&#xff0c;主要是对树莓派进行一个简单的介绍的视频。挑我自己感兴趣的大致总结如下&#xff1a…

第五章 树与二叉树 二、二叉树的定义和常考考点,WPL的算法

一、定义 二叉树可以用以下方式详细定义&#xff1a; 二叉树是由节点构成的树形结构&#xff0c;每个节点最多可以有两个子节点。每个节点有以下几个属性&#xff1a; 值&#xff1a;存储该节点的数据。左子节点&#xff1a;有一个左子节点&#xff0c;如果没有则为空。右子节…

【包过滤防火墙——iptables静态防火墙】的简单使用

文章目录 规则链的分类--五链处理的动作iptables常用参数和作用 防火墙就是堵和通的作用 iptables &#xff1a;包过滤防火墙&#xff0c;是内核防火墙netfilter的管理工具 核心&#xff1a;四表五链 规则链的分类–五链 在进行路由选择前处理的数据包&#xff1a;PREROUTIN…