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;// 没有产生临时变量,因此可以不加const

	ptr->_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 = s2
    	Student& 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 = s2
    	Student& 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;// 3
	cout << s._count << endl;// 3
	cout << o._count << endl;// 3

	return 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中的_name
		cout << _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.mfbz.cn/a/608153.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mybatis 源码分析

《黑马架构师_源码系列-主流框架&中间件》-- MyBatis &#xff08;讲师&#xff1a;子慕&#xff09; * 手写持久层框架-仿写mybatis * Mybatis架构设计&主要组件 * Mybatis如何完成的初始化? * Mybatis如何完成的sql解析及执行? * Mybatis如何设置的参数? * Mybat…

容器集群管理系统Kubernetes(K8S)

目录 一、前言 1.1什么是云原生&#xff1f; 1.2云要素的四要素&#xff1f; 1.2.1微服务 1.2.2容器化 1.2.3DevOps 1.2.4持续交付 1.3云平台有哪些&#xff1f; 1.4SRE 二、Kubernetes 概述 2.1K8S 是什么 2.2K8S作用 2.3K8S版本 2.4为什么要用 K8S 2.5K8S 的特…

【论文阅读】Spectral–Spatial Attention Network for Hyperspectral Image Classification

Spectral–Spatial Attention Network for Hyperspectral Image Classification 论文地址摘要&#xff1a;1. 简介1.1.动机1.2.贡献 2. 相关作品2.1.双向递归网络RNN2.2.CNN2.3. Attention Mechanism 3. 方法3.1 Attention with RNN for Spectral Classification3.2&#xff0e…

【镜像仿真篇】磁盘镜像仿真常见错误

【镜像仿真篇】磁盘镜像仿真常见错误 记系统镜像仿真常见错误集—【蘇小沐】 1、实验环境 2023AFS39.E01&#xff08;Windows11系统镜像&#xff09;Arsenal Image Mounter&#xff0c;[v3.10.262]‍Vmware Workstation 17 Pro&#xff0c;[v17.5.1]Windows 11 专业工作站版…

单片机——直流电机

1 .关于4线直流电机 两根12v供电线&#xff0c;通入12v&#xff0c;风扇以最高转速工作。 一根测速线&#xff0c;电机工作时输出测速信号&#xff0c;提供转速反馈。一根PWM控制信号线&#xff0c;电机工作时控制器输入PWM控制信号&#xff0c;以控制风扇转速(通常为占空比可…

性能测试瓶颈:CPU 问题的深度分析和调优!

性能测试是评估系统、应用程序或服务的性能和稳定性的过程。在进行性能测试时&#xff0c;我们经常会发现一些瓶颈&#xff0c;其中之一就是与CPU相关的问题。CPU是计算机系统中最重要的组件之一&#xff0c;对系统的整体性能起着至关重要的作用。本文将从零开始详细介绍如何分…

【有趣的透镜】1.透镜初相识

1.透镜的外形和材料 (1)透镜由玻璃或者塑料制成&#xff1b; (2)透镜一般为圆型&#xff0c;其单面或双面为球面&#xff1b; 2.透镜的类型和折射 (1)球面外凸为凸透镜(聚光)&#xff0c;球面内凹为凹透镜(散光)&#xff1b; (2)透镜是基于光的折射&#xff0c;只要光从一…

第一批00后已经开始做家政了!2024年轻人的机会在哪里?2024创业小项目。

当代年轻人的路子到底有多野&#xff1f;江苏无锡的00后女生冯佳佳&#xff0c;觉得未来家政行业很有前景&#xff0c;便毅然决然的辞去了幼儿园老师的工作&#xff0c;和男朋友一起成立了家政公司。、 诚然&#xff0c;现在的家政收费是及其昂贵赚钱的&#xff0c;开锁师傅开一…

点击导航栏选项后,导航栏高亮该选项

如图所示&#xff0c;点击“流浪猫客栈”时&#xff0c;会一直高亮显示&#xff0c;表示现在看的是这个选项的页面。 Cat.jsp上写&#xff1a; <!--header--> <jsp:include page"header.jsp"><jsp:param name"flag" value"3">…

上位机图像处理和嵌入式模块部署(树莓派4b和c++新版本的问题)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 自己读书的时候是03年&#xff0c;学习c也是差不多04年开始&#xff0c;到现在基本上20年了。这20年过程当中&#xff0c;其实c的语言版本一直是在…

SSRF漏洞学习

1.ssrf漏洞简介 ssrf&#xff08;服务器端请求伪造&#xff09;&#xff0c;它是由攻击者构造形成的由服务端发起的一个较为安全的漏洞。 它攻击的目标是从外网无法访问的内部系统&#xff0c;因为它是从服务端发起的&#xff0c;所以它能够请求到与它相连并且与外网隔离的内部…

PXE批量部署,一键安装配置多台Linux系统

目录 一、PXE批量部署的优点 二、搭建PXE远程安装服务器 1. 实验初始化设置 2. 一键安装软件包 3. 复制 vmlinuz、initrd.img、pxelinux.0文件 4. 配置PE启动菜单配置文件 5. 修改配置文件&#xff0c; 启动各个软件服务 6. kickstart自动应答文件修改启动菜单配置文件…

【用文本生成歌声】Learn2Sing 2.0——歌声转换算法即梅尔频谱详解

一. 频谱图与梅尔谱图的介绍 频谱图&#xff1a;频谱图可以理解为一堆垂直堆叠在一起的快速傅里叶变换结果。 1.1 信号 在进入频谱图模块之前&#xff0c;首先我们需要了解信号是什么。 信号就是某一特定量随时间变化&#xff0c;对于音频来说&#xff0c;这个特定的变化量就…

java.net.SocketInputStream.socketRead0 卡死导致 tomcat 线程池打满的问题

0 TL;DR; 问题与原因&#xff1a;某些特定条件下 java.net.SocketInputStream.socketRead0 方法会卡死&#xff0c;导致运行线程一直被占用导致泄露采用的方案&#xff1a;使用监控线程异步监控卡死事件&#xff0c;如果发生直接关闭网络连接释放链接以及对应的线程 1. 问题 …

pytest教程-42-钩子函数-pytest_runtest_makereport

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节我们学习了pytest_runtest_teardown钩子函数的使用方法&#xff0c;本小节我们讲解一下pytest_runtest_makereport钩子函数的使用方法。 pytest_runtest_makereport 钩子函数在 pytest 为每个测试生成报…

修改表空间的状态

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 表空间有4种状态:联机、脱机、只读和读写。修改其中某一种状态的语句如下所示 设置表空间 tspace 为联机状态。 SQL>ALTER TABLESPACE space ONLINE: 设置表空间 tspa…

Python运维之多线程!!

一、多线程 二、多线程编程之threading模块 2.1、使用threading进行多线程操作有两种方法&#xff1a; 三、多线程同步之Lock&#xff08;互斥锁&#xff09; 四、多线程同步之Semaphore&#xff08;信号量&#xff09; 五、多线程同步之Condition 六、多线程同步之Event…

开发Web3 ETF的技术难点

开发Web3 ETF&#xff08;Exchange-Traded Fund&#xff0c;交易所交易基金&#xff09;软件时&#xff0c;需要注意以下几个关键问题。开发Web3 ETF软件是一个复杂的过程&#xff0c;涉及到金融、法律和技术多个领域的专业知识。开发团队需要综合考虑上述问题&#xff0c;以确…

kubernate 基本概念

一 K8S 是什么&#xff1f; K8S 全称&#xff1a;Kubernetes 作用&#xff1a; 用于自动部署、扩展和管理“容器化&#xff08;containerized&#xff09;应用程序”的开源系统。 可以理解成 K8S 是负责自动化运维管理多个容器化程序&#xff08;比如 Docker&#xff09;的…
最新文章