C++ 继承篇

面向对象语言的三大特性:封装,继承和多态

根据目前学到的知识,对于封装的理解,大致有两层:

  1. 将数据和方法封装,不想让外面看到用private/protected修饰,想让外面看到用public修饰
  2. 类型的行为不满足我们的需求,将类型封装,自主规定类型的行为,比如list迭代器,反向迭代器

从现在开始,进入继承的学习

1. 继承的概念和定义


1.1 继承的概念

有这样的场景,你要完成一个学生管理系统,必然需要描述很多的对象,于是构建很多类,每个类代表不同的群体,比如学生类、老师类、宿管类…定义出来后,发现每个类中都有某些属性是相同的,比如大家都有名字、年龄、性别这样的属性,在每个类中都定义了一遍,显然代码冗余,于是就有了继承

将每个类的公共属性提取出来,单独作为一个类,称为父类/基类;每个群体中持有它们独有的属性,叫做子类/派生类,通过继承的方式,将父类继承给子类,这样子类就有父类的属性和自身独有的属性

在这里插入图片描述

继承是面向对象语言中代码复用的一种重要手段,它允许我们保持原有类的特性,增加新的功能,这样产生的类叫做派生类

1.2 继承的定义

在这里插入图片描述

三种的继承方式+访问限定符决定了在派生类中访问基类成员的方式

规律十分简单,如果是基类的private成员,那么不管何种继承方式,都不能在派生类中直接使用

其他情况,按照public > protected > private的顺序,基态成员在派生类中的修饰方式,按照继承方式和基态类成员修饰符当中最小值

类成员/继承方式public继承protected继承private继承
基类public成员派生类的public成员派生类的protected成员派生类的private成员
基类protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类private成员派生类中不可见派生类中不可见派生类中不可见

在实践中,一般不会对基类成员进行private修饰和private继承

struct的默认继承方式和限定符都是共有;class的默认继承方式和限定符都是私有

2. 基类和派生类赋值转换


C语言中,相关类型之间可以发生隐式类型转换,中间会产生临时变量,C++中延续了这种语法

不相关类型间不能隐式类型转换,但对于基类和派生类,可以发生赋值转换,中间不会产生临时变量,它由编译器特殊处理

对于public继承,每一个派生类对象都是一个特殊的基类对象,这种赋值转换也叫做切割/切片

在这里插入图片描述

  • 派生类对象可以赋值给基类对象/引用/指针
  • 基类对象不能赋值给派生类对象
int main()
{Student s;Person p = s;Person* ptr = &s;Person& ref = s;// 没有产生临时变量,因此可以不加constptr->_name += 'x';ref._age = 1;return 0;
}

在这里插入图片描述

3. 继承中的作用域


  • 继承体系中,基类和派生类有自身独立的作用域
  • 如果基类和派生类有同名成员变量,派生类中默认访问的是自身的,可以通过显示调用访问基类的;该同名变量构成隐藏,也叫重定义
  • 如果是成员函数的隐藏,只要函数名相同就构成隐藏
class Person
{
protected:string _name;int _num = 111;
};class Student : public Person
{
public:void func(){cout << _num << endl;// 默认是自身的成员变量cout << Person::_num << endl;// 指定父类中的成员变量}protected:int _num = 222;
};int main()
{Student s;s.func();return 0;
}
class Person
{
public:void func(int i = 1){cout << "fun(int i)" << endl;}protected:string _name;int _num = 111;
};class Student : public Person
{
public:void func(){Person::func(10);cout << "fun()" << endl;}protected:int _num = 222;
};int main()
{Student s;s.func();// 调用子类中的func()s.Person::func();// 在子类中调用父类中的func()Person p;p.func();// 调用父类中的func()return 0;
}

4. 派生类的默认成员函数


派生类的成员变量分为两部分:

  1. 父类的成员(看作一个整体)
  2. 自身的内置类型和自定义类型按照跟以前一样的方式
  • 默认构造函数会调用父类的默认构造函数初始化父类的成员,如果父类没有默认构造函数,则必须在初始化列表中显示调用

    class Person
    {
    public:Person(const char* name):_name(name){cout << "Person()" << endl;}protected:string _name;
    };class Student : public Person
    {
    public:Student(const char* name, int num):Person(name),_num(num){}protected:int _num;
    };int main()
    {Student s("zhangsan", 4);return 0;
    }
    
  • 拷贝构造调用父类的拷贝构造完成父类成员的拷贝初始化

    class Person
    {
    public:Person(const char* name):_name(name){cout << "Person()" << endl;}Person(const Person& p):_name(p._name){cout << "Person(const Person& p)" << endl;}protected:string _name;
    };class Student : public Person
    {
    public:Student(const char* name, int num):Person(name),_num(num){cout << "Student()" << endl;}// s1(s2);Student(const Student& s):Person(s),_num(s._num){cout << "Student(const Student& s)" << endl;}protected:int _num;
    };int main()
    {Student s1("zhangsan", 4);Student s2(s1);return 0;
    }
    
  • 赋值重载调用父类的赋值重载完成父类成员的初始化

    class Person
    {
    public:Person(const char* name):_name(name){cout << "Person()" << endl;}Person(const Person& p):_name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){if (this != &p){_name = p._name;cout << "Person& operator=(const Person& p)" << endl;}return *this;}protected:string _name;
    };class Student : public Person
    {
    public:Student(const char* name = "xxxx", int num = 3):Person(name),_num(num){cout << "Student()" << endl;}// s1(s2);Student(const Student& s):Person(s),_num(s._num){cout << "Student(const Student& s)" << endl;}//s1 = s2Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_num = s._num;cout << "Student& operator=(const Student& s)" << endl;}return *this;}protected:int _num;
    };int main()
    {Student s1("zhangsan", 4);Student s2;s2 = s1;return 0;
    }
    
  • 析构函数调用时,会先析构子类的成员,再析构父类的成员;这是为了防止在子类的析构中访问父类的成员,如果先析构父类,就会造成访问非法空间的问题;因此,编译器在构造时,先构造父类,再构造子类;再析构时,会保证先析构子类,再析构父类

    class Person
    {
    public:Person(const char* name):_name(name){cout << "Person()" << endl;}Person(const Person& p):_name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){if (this != &p){_name = p._name;cout << "Person& operator=(const Person& p)" << endl;}return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name;
    };class Student : public Person
    {
    public:Student(const char* name = "xxxx", int num = 3):Person(name),_num(num){cout << "Student()" << endl;}// s1(s2);Student(const Student& s):Person(s),_num(s._num){cout << "Student(const Student& s)" << endl;}//s1 = s2Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_num = s._num;cout << "Student& operator=(const Student& s)" << endl;}return *this;}~Student(){Person::~Person();cout << "~Student()" << endl;}protected:int _num;
    };int main()
    {Student s1;return 0;
    }
    

5. 继承与友元


基类的友元函数不是派生类的友元函数,也就是说友元不能继承,基类友元函数不能访问派生类的私有和保护成员

class B;
class A
{
public:friend void func(const A& a, const B& b);protected:int _a;
};class B : public A
{
protected:int _b;
};void func(const A& a, const B& b)
{cout << a._a << endl;cout << b._b << endl;// 编译器报错
}int main()
{A a;B b;func(a, b);return 0;
}

6. 继承与静态成员


基类中的静态成员,不管该基类派生出多少个类,静态成员只有一份,所有派生类使用的都是同一个静态成员

可以根据这个特性,计算基类及其派生类一共创建的个数

class Person
{
public:static int _count;Person(){_count++;}protected:string _name;
};int Person::_count = 0;class Student : public Person
{
protected:int _stdid;
};class Other : public Student
{
protected:int _num;
};int main()
{Person p;Student s;Other o;cout << p._count << endl;// 3cout << s._count << endl;// 3cout << o._count << endl;// 3return 0;
}

7.棱形继承


单继承:一个子类只有一个直接父类时称这种继承关系为单继承

在这里插入图片描述

多继承:一个子类有两个及以上的直接父类时称这种继承关系为多继承

在这里插入图片描述

多继承一般用于一个对象同时是两种类别,比如西红柿,它既是水果,又是蔬菜,继承两个父类是很合理的

但是,有多继承就意味着会出现棱形继承

在这里插入图片描述

棱形继承会导致派生类包含了Other包含了两份Person,产生数据冗余和二义性的问题

在这里插入图片描述

class Person
{
protected:string _name;
};class Student : public Person
{
protected:int _stdid;
};class Teacher : public Person
{
protected:int _jobid;
};class Other : public Student, public Teacher
{
public:void func(){cout << _name << endl;// 编译器报错,不知道访问的是Student还是Teacher中的_namecout << _other << endl;}protected:int _other;
};int main()
{Other o;o.func();return 0;
}

C++早期设计时,认为多继承很合理,但在后续使用中就出现了棱形继承的问题,该如何解决呢?

使用虚拟继承,让基类的第一级的派生类继承时加上virtual关键字,表示虚拟继承

class Person
{
public:int _name;
};class Student : virtual public Person
{
public:int _stdid;
};class Teacher : virtual public Person
{
public:int _jobid;
};class Other : public Student, public Teacher
{
public:int _other;
};int main()
{Other o;o.Student::_name = 6;o.Teacher::_name = 7;o._stdid = 1;o._jobid = 2;o._other = 3;return 0;
}

在这里插入图片描述

使用棱形虚拟继承,在内存中,基类被放到了最下面,变成公共的,同时第一级的派生类中多了指针,该指针指向一个数,表示该类到基类的偏移量

棱形虚拟继承中,基类被叫做虚基类,派生类中的指针叫做虚基表指针,指向一个虚基表,里面存放着基类的偏移量

发生切割/切片时,会有指针偏移,指针指向自身的对象

8.继承总结


关于多继承的面试题:

  1. C++有多继承,为什么java没有?

    C++比java先设计,在当时,多继承看起来十分合理,于是就设计了出来,但是没想到出现了很多问题,导致解决非常麻烦,而且生活中也很少使用;而java吸收了这个教训,在设计时就舍弃了多继承

  2. 多继承的问题是什么?

    多继承本身没有任何问题,但有多继承就可能会写出棱形继承

  3. 棱形继承的问题?如何解决?

    数据冗余,二义性;对第一级的派生类使用虚拟继承

  4. 底层角度是如何解决数据冗余和二义性的?

    将基类放到第一级派生类的后面,派生类中加入虚函数指针,指向虚函数表,存放基类的偏移量

继承和组合:

public继承是一种is-a的关系,比如学生和人,学生是人

组合是一种has-a的关系,比如汽车和轮胎,汽车有轮胎

如果使用继承,基类对象对于派生类是可见的,一定程度上破坏了基类的封装,导致基类和派生类耦合度高

而对于组合,自定义对象成员在其他对象中不可见,类和类之间耦合度低

开发软件时,尽量做到类和类之间低耦合,高内聚,因此如果一个对象既能使用继承描述,又能使用组合组合描述,优先使用组合

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言自定义类型——枚举

枚举 枚举定义枚举 与 #define使用写一个简易计算器的程序。 枚举定义 格式&#xff1a; enum name(枚举什么类型) {//数据 ... };枚举&#xff0c;顾名思义一 一 列举。 在生活当中有很多可以列举的东西。 如&#xff1a; //电脑桌面上的软件 enum App {QQ ,WeChat,CCtalk,…

厂家自定义 Android Ant编译流程源码分析

0、Ant安装 Windows下安装Ant&#xff1a; ant 官网可下载 http://ant.apache.org ant 环境配置&#xff1a; 解压ant的包到本地目录。 在环境变量中设置ANT_HOME&#xff0c;值为你的安装目录。 把ANT_HOME/bin加到你系统环境的path。 Ubuntu下安装Ant&#xff1a; sudo apt…

AcWing 4993 FEB

4993. FEB - AcWing题库 大佬亲笔 将原串分成三段&#xff1a; FFF|E.....B|FFF 先合并中间段&#xff0c;再合并两边的段 #include <iostream> #include <cstring> #include <algorithm> #include <string> #include <queue&g…

【热门话题】如何通过AI技术提升内容生产的效率与质量

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 如何通过AI技术提升内容生产的效率与质量引言一、自然语言处理&#xff08;NLP&…

常见的一些RELAXED MODEL CONCEPTS

释放一致性(release consistency, RC) RC的核心观点是&#xff1a;使用 FENCE 围绕所有同步操作是多余的 同步获取 (acquire) 只需要一个后续的 FENCE&#xff0c;同步释放 (release) 只需要一个前面的 FENCE。 对于表 5.4 的临界区示例&#xff0c;可以省略 FENCE F11、F14…

extern关键字的使用。keil中编译时,出现error:identifier xxx is undefined

问题 编译时&#xff0c;出现error&#xff1a; identifier “Reg_Flag” is undefined extern Reg_Flag reg_flag; 很奇怪&#xff0c;我明明已经定义了。无非就是定义是在extern的下面&#xff0c;会不会是这个原因&#xff1f; 解决 果然&#xff0c;把extern的部分放到…

【热门话题】如何构建具有高度扩展性的系统

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 如何构建具有高度扩展性的系统引言一、理解扩展性1.1 扩展性的定义1.2 扩展性的…

私人健身教练预约管理小程序开发源码现成案例(小程序+APP+H5 源码部署)

一、私人健身教练预约管理系统-环境介绍 1.1 私人健身教练预约管理系统-运行环境 开发语言&#xff1a;PHP 数据库&#xff1a;MySQL 系统架构&#xff1a;TP 后端&#xff1a;SpringBoot 前端&#xff1a;Vue 2. 私人健身教练预约管理系统-系统介绍。 2.1私人健身教练预约管…

TL-WN826N无线网卡连接电脑蓝屏,提示rtl8188gu.sys

TL-WN826N无线网卡插电脑就蓝屏&#xff0c;提示rtl8188gu.sys 处理方法&#xff1a; 设备管理器中卸载其他的2.0无线网卡程序和功能中卸载网卡驱动TPlink官网下载 TL-WN826N V1.0_1.0.0&#xff08;https://www.tp-link.com.cn/product_572.html?vdownload&#xff09;&…

已经有 Prometheus 了,还需要夜莺?

谈起当下监控&#xff0c;Prometheus 无疑是最火的项目&#xff0c;如果只是监控机器、网络设备&#xff0c;Zabbix 尚可一战&#xff0c;如果既要监控设备又要监控应用程序、Kubernetes 等基础设施&#xff0c;Prometheus 就是最佳选择。甚至有些开源项目&#xff0c;已经内置…

部署 Sentinel 控制台:实现流量管理和监控

序言 Sentinel 是阿里巴巴开源的一款流量防护与监控平台&#xff0c;它可以帮助开发者有效地管理微服务的流量&#xff0c;实现流量控制、熔断降级、系统负载保护等功能。本文将介绍如何在项目中部署和配置 Sentinel 控制台&#xff0c;实现微服务的流量防护和监控。 一、Sen…

BurpSuite软件的介绍以及下载

BurpSuite是一个用于攻击web应用程序的集成平台&#xff0c;它包含了许多工具&#xff0c;这些工具可以协同工作&#xff0c;有效地分享信息&#xff0c;并支持以某种工具中的信息为基础供另一种工具使用的方式发起攻击。这些工具包括但不限于&#xff1a; Proxy&#xff08;代…