C++ 继承篇

news2024/10/6 22:19:56

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

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

  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.coloradmin.cn/o/1653186.html

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

相关文章

Intel® Platform Firmware Resilience (Intel® PFR):英特尔® 平台固件恢复力(Intel® PFR)

为了降低与固件相关的安全风险&#xff0c;英特尔为服务器平台开发了英特尔平台固件恢复力&#xff08;Intel PFR&#xff09;。 此功能可保护关键固件在启动和运行时免受攻击。这可以被视为是 Cerberus 项目或 NIST SP800-193 的实现。 英特尔平台固件恢复力&#xff08;Int…

SQL 基础 | JOIN 操作介绍

在SQL中&#xff0c;JOIN是一种强大的功能&#xff0c;用于将两个或多个表中的行结合起来&#xff0c;基于相关的列之间的关系。 JOIN操作通常用在SELECT语句中&#xff0c;以便从多个表中检索数据。 以下是几种基本的JOIN类型以及它们的用法&#xff1a; INNER JOIN&#xff1…

探秘编程之旅:Baidu Comate 智能代码助手的魔法揭秘

目录 Baidu Comate智能代码助手1.场景需求2.安装步骤3.功能介绍3.1 /指令3.2 插件3.3 #知识 4.使用体验5.总结 Baidu Comate智能代码助手 智能编程助手的意义在于提升编程体验和效率&#xff0c;使开发人员能够更轻松、更快速地完成编码任务&#xff0c;是如今人工智能技术的一…

2024年颠覆商业模式《本草生活》项目,巧妙三招营销引流裂变套路

2024年颠覆商业模式《本草生活》项目&#xff0c;巧妙三招营销引流裂变套路 文丨微三云营销总监胡佳东&#xff0c;点击上方“关注”&#xff0c;为你分享市场商业模式电商干货。 - 引言&#xff1a;现如今流量枯竭、降本增效、红利不再已是线上营销的常态&#xff0c;互联网…

中金:如何把握不断轮动的资产“风口”

从比特币到日股&#xff0c;到黄金与铜再到当前的港股&#xff0c;每次超预期大涨后都透支回调。 今年以来资产的“风口”不断轮动&#xff0c;从比特币到日股&#xff0c;到黄金与铜&#xff0c;再到当前的港股&#xff0c;资产仿佛“接力”般交替领先&#xff0c;同时“风口”…

【北京迅为】《iTOP-3588开发板快速烧写手册》-第10章 多设备量产升级固件

RK3588是一款低功耗、高性能的处理器&#xff0c;适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用&#xff0c;RK3588支持8K视频编解码&#xff0c;内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP&…

分享6个免费下载电子书的网站

着急看书的宝子们看这里&#xff01; 收藏了一堆电子书网站终于能派上用场了~ 01/Z-Library https://zh.zlibrary-be.se/ 世界上最大的电子图书馆&#xff0c;拥有超千万的书籍和文章资源&#xff0c;99%的书籍资料都能在这里找到。 我给的这个网址现在还能正常打开使用&…

vue+sortablejs来实现列表拖拽——sortablejs的使用

sortablejs官网:https://sortablejs.com/ 最近在看form-builder组件&#xff0c;发现里面有用到sortablejs插件&#xff0c;用于实现拖拽效果。 但是这个官网中的配置&#xff0c;实在是看不懂&#xff0c;太简单又太复杂&#xff0c;不实用。 下面记录一下我的使用&#xff…

sklearn的make_blobs函数

make_blobs是一个用于生成随机数据点的实用函数&#xff0c; from sklearn.datasets import make_blobs X,Y make_blobs(n_samples2000,n_features2,centers12,cluster_std0.05,center_box[-5,5],random_state21)n_samples: 要生成的样本数量。centers: 要生成的簇&#xff0…

JetsonNano —— Windows下对Nano板卡烧录刷机(官方教程)

介绍 NVIDIA Jetson Nano™ 开发者套件是一款面向创客、学习者和开发人员的小型 AI 计算机。按照这个简短的指南&#xff0c;你就可以开始构建实用的 AI 应用程序、酷炫的 AI 机器人等了。 烧录刷机 1、下载 Jetson Nano开发者套件SD卡映像&#xff0c;并记下它在计算机上的保存…

docker部署小试

一 1.1 需求&#xff1a;根据docker部署nginx并且实现https 1.2 前期准备 准备一台装备好的docker-ce虚拟机&#xff0c;容量至少满足4G/2C&#xff0c;同时做好关闭防火墙的操作 systemctl stop firewalld setenforce 0 1.3 实验部署 1.3.1 创建并进入文件夹 1.3.2 编辑run脚本…

XSKY SDS 6.4 重磅更新:NFS 性能飙升 3 倍,对象多站点等 10 多项功能强势升级

近日&#xff0c;XSKY星辰天合发布了 XSKY SDS V6.4 新版本&#xff0c;该版本在文件的性能提升、对象容灾能力完善方面改进异常显著&#xff0c;同时也大幅提高了存储系统的安全特性&#xff0c;适配更多的信创软硬件生态。 近来&#xff0c;软件定义存储&#xff08;SDS&…

pymysql用法整理--python实现mysql数据库操作

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文着重整理pymsql的常用方法 不专门讲解MySQL数据库的相关知识 常用基本语法汇总 import pymysql#连接数据库 connpymysql.connect(host127.0.0.1,port3306,userroot,password123456,charsetutf8,db"expe…

洛谷 P4148:简单题 ← KD-Tree模板题

【题目来源】https://www.luogu.com.cn/problem/P4148【题目描述】 你有一个 NN 的棋盘&#xff0c;每个格子内有一个整数&#xff0c;初始时的时候全部为 0&#xff0c;现在需要维护两种操作&#xff1a; ● 1 x y A → 1≤x,y≤N&#xff0c;A 是正整数。将格子 (x,y) 里的数…

韩国站群服务器提供高级安全防护以确保数据和网站的安全性

韩国站群服务器提供高级安全防护以确保数据和网站的安全性 在当今数字化时代&#xff0c;网站的安全性和数据保护已成为企业和个人不可忽视的重要议题。韩国站群服务器作为一个拥有发达科技和互联网基础设施的国家&#xff0c;通过提供高级安全防护措施&#xff0c;为用户的数…

安卓surfaceview的使用方式

1. 什么是surfaceview surfaceview内部机制和外部层次结构 在安卓开发中&#xff0c;我们经常会遇到一些需要高性能、高帧率、高画质的应用场景&#xff0c;例如视频播放、游戏开发、相机预览等。这些场景中&#xff0c;我们需要直接操作图像数据&#xff0c;并且实时地显示到…

后端常用技能:解决java项目前后端传输数据中文出现乱码、问号问题

0. 问题背景 最近做一个解析数据的小工具&#xff0c;本地运行时都正常&#xff0c;发布到服务器上后在导出文件数据时发现中文全部变成了问号&#xff0c;特此记录下问题解决的思路和过程 1. 环境 java 1.8 springboot 2.6.13 额外引入了fastjson&#xff0c;commons-csv等…

##07 从线性回归到神经网络:PyTorch实战解析

文章目录 前言线性回归基础理论背景实现步骤神经网络介绍理论背景实现步骤从线性回归到神经网络结论前言 在深度学习的浩瀚宇宙中,线性回归和神经网络是两个基本但极其重要的概念。线性回归模型是统计学和机器学习的基石之一,而神经网络则是深度学习技术的核心。本文旨在通过…

【日志革新】在ThinkPHP5中实现高效TraceId集成,打造可靠的日志追踪系统

问题背景 最近接手了一个骨灰级的项目&#xff0c;然而在项目中遇到了一个普遍的挑战&#xff1a;由于公司采用 ELK&#xff08;Elasticsearch、Logstash、Kibana&#xff09;作为日志收集和分析工具&#xff0c;追踪生产问题成为了一大难题。尽管 ELK 提供了强大的日志分析功…

在时间同步应用上节省大量时间!德思特GNSS模拟器是怎么做到的?

​ 作者介绍 德思特Safran GNSS模拟器是一款综合解决方案&#xff0c;专为精确的PNT&#xff08;位置、导航和时间&#xff09;仿真与测试设计。它超越了传统GNSS定位导航仿真&#xff0c;也能提供极高的授时精度。 这款模拟器对于评估和提升GNSS接收机及同步系统的整体性能至…