继承的详细介绍与理解,看了就懂

2023-11-05

继承的概念及定义

继承也是面向对象的三大特性之一,是为了代码能够复用的重要手段,它使得我们在原有的类特性的基础上进行扩展,产生新的功能,这样的类我们成为派生类,而原有的类则叫做基类。继承就和我们以前的函数复用一样,只是这次复用的是属于设计层次上的。

定义格式

例如:

class Person {
public:
	string _name = "mingzi";
};

class Student :public Person{
public:
	void print() {
		cout << "name" << _name << endl;
		cout << "stid" << _stid << endl;
	}
private:
	int _stid = 202238;//学号
};
int main() {
	Student stdt;
	stdt.print();
}

此时Student和Person就成为了父子类关系,Person被称为基类,也叫做父类,Students被称为派生类,也叫子类,而public则叫做继承方法。上面的代码我们可以发现子类复用了父类的成员,并且我们在子类里可以使用父类的成员变量,这就是因为用的是public的继承方式
继承方式和访问限定符都是一样的,分别为public,protected,private三种

继承基类成员访问方式的变化

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

此时我们可以发现,不管是什么类型的成员还是继承方式,最后子类获得的都是范围最小的那一种即public>protected>private,protect叫做保护成员限定符,是因继承才出现的。简单来讲,private和不可见的区别是,派生类不可见是子类无法使用父类的private成员,而private是属于类内可以使用,但类外无法使用。而protected,父类的protected成员子类可以使用,但类外无法使用。
不过我们实际运用中一般都是public

基类和派生类对象赋值转换

//同上一份类
int main(){
	Person p;
	Student s;
	//public继承
	p=s;//父类=子类
	s=p;//不可以
	Person* ptr=&s;//指针
	Person& ref=s;//引用
	return 0;
}

我们把子类对象赋值给父类对象/指针/引用的行为叫做切割,天然行为,不存在类型转换(没有const临时变量)。形象的来讲因为子类中有全部的父类成员,把多余(属于自己子类)的部分给切除,就可以把子类内的成员依次给父类赋值了

继承中的作用域

基类和派生类都有独立的作用域,当子类和父类中有同名成员时,子类成员会隐藏父类成员,这种情况叫隐藏,也叫重定义

class Person {
public:
	string _name = "mingzi";
	int _stid=111;
};

class Student :public Person{
public:
	void print() {
		cout << "name" << _name << endl;
		cout << "stid" << _stid << endl;
		//若想打印父类的可以使用 Person::_stid
	}
private:
	int _stid = 202238;//学号
};
int main() {
	Student stdt;
	stdt.print();
}

此时将会打印202238,因为子类的成员函数会优先调用自己的成员变量,将父类的隐藏(注:尽量不要重名,但是在虚函数中又不一样了,后面多态会讲)

class A{
public:
	void fun(){
		cout << "func" <<endl;
	}
};
class B : public A{
public:
	void fun(int i){}
};
int main(){
	B b;
	b.fun(10);
	//b.fun();
	b.A::fun();
	return 0;
}

此时A类和B类的两个fun函数构成隐藏关系(只要函数名相同,不管参数怎么样,就是隐藏关系),继承中函数名相同就是隐藏值得注意的是,重载的条件是在同一个作用域中。

派生类的默认成员函数

子类的构造函数——我们不写,编译器默认生成,此时
1.继承的父类成员作为一个整体——调用父类的默认构造函数初始化
2.自己的自定义类型成员 ——调用它的默认构造函数
3.自己的内置类型成员 ——不处理(除非声明时给了缺省值)

子类的拷贝构造函数也是同理——我们不写,编译器默认生成,此时
1、继承的父类成员作为一个整体 ——调用父类的拷贝构造
2、自己的自定义类型成员 —— 调用它的拷贝构造
3、自己的内置类型成员 —— 值拷贝

子类的拷贝赋值函数也是同理——我们不写,编译器默认生成

子类析构函数 – 我们不写,编译器默认生成 ——此时
1、继承的父类成员作为一个整体 – 调用父类的析构函数
2、自己的自定义类型成员 – 调用它的析构函数
3、自己的内置类型成员 – 不处理
子类析构函数和父类析构函数构成隐藏关系
因为编译器会对析构函数名做特殊处理,所有类的析构函数名都会被处理成统一名字destructor()
。编译器为什么要这么做呢,多态会讲到
子类的析构函数在执行结束会后,会自动调用父类的析构函数

class Person
{
public:
	Person(const char* name = "peter")
	//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)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
	//int _age;
};
class Student : public Person
{
public:
	// 我们要自己实现子类构造函数
	// 要注意的父类成是作为一个整体,调用父类的构造函数进行初始化
	Student(const char* name)
		:Person(name)//不可以单独给_name进行赋值
		, _id(id)
		, _address(address)
	{}
	//当我们子写了拷贝构造函数
	//一般情况没必要写子类的拷贝构造,除非子类里的成员变量有深浅拷贝问题,才会需要
	Student(const Student& s)
		:_id(s._id)
		, _address(s._address)//⭐值得注意的是,这里address用的是自定义类型的拷贝
		//构造,即string的拷贝构造,因为在析构时并没有发生崩溃,说明是深拷贝
		, Person(s)//这里我们可以直接传子类对象,给父类引用,这里发生切片
	{}
	//⭐若子类的拷贝构造没去调用父类的拷贝构造(即没有Person(s)),拷贝构造也是构造函
	//数,构造函数规定,如果你不调用自定义类型,那会去调用它的默认构造,(和编译器生成的不同)
	//当我们子写了拷贝赋值函数
	Student& operator=(const Student& s){
		if (this != &s){
			_id = s._id;
			_address = s._address;
			Person::operator=(s); // 我们显示调用=的函数,⭐此时父子类都有=的重载,
			//构成隐藏关系,否则自己调自己了,所以我们需要指明类域,切片
		}
		return *this;
	}
	
	~Student(){
		//Person::~Person();Student和父类析构构成隐藏
		// 清理自己的资源
	} // 会自动调用父类的析构函数
private:
	int _id;
	string _address;
};
int main(){
	Student s1("张三", 1, "西安市");
	Student s2(s1);
	Student s3("张思", 2, "北京市");
	s1 = s3;  //此时会打印Person()
			  //        Person(const Person& p)
			  //        Person()
			  //        Person operator=(const Person& p)
			  //		~Person()
			  //        ~Person()
			  //        ~Person()   在有子类析构情况下,顺序父构造子构造子析构父析构
}

当父类的构造函数是无参或者给了全缺省的默认构造函数时,子类的构造函数也可以是默认构造函数(也可以不是),来调用父类的默认构造函数。但是当父类不是默认构造函数时(子类也不能使用默认构造函数),子类的构造函数必须对父类一个整体,进行初始化(如:Person(name),就像缺省值一样)
⭐⭐声明的顺序,才是初始化顺序,所以子类的初始化列表里(列表里出现的顺序不重要,重要的是声明的顺序),无论父类Person在哪个位置,都是先初始化父类的

PS:这里比较啰嗦,代码里的注释没看懂的话可以看这。

拷贝构造:⭐值得注意的是,这里address用的是自定义类型的拷贝构造,即string的拷贝构造,因为在析构时并没有发生崩溃,说明是深拷贝

继承与友元

Display函数声明为友元,所以Display函数可以使用类里的成员变量,但对子类来说,友元不能继承,即基类友元不能访问子类私有和保护成员

class Student;
class Person{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person{
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s){
	cout << p._name << endl;
	cout << s._stuNum << endl;
}

void main(){
	Person p;
	Student s;
	Display(p, s);
}

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,子类不会有这样的static的成员,并且我们可以用类名去访问静态成员(由该类所有对象共享),类名::变量名

⭐复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称整个继承关系为单继承
多继承:一个子类有两个或者以上父类时整个继承关系称为多继承
菱形继承:时多继承的一种特殊情况
在这里插入图片描述

class A{
public:
	int _a;
};
class B : public A{
public:
	int _b;
};
class C : public A{
public:
	int _c;
};
class D : public B, public C{
public:
	int _d;//类中产生两个A类的a成员变量
};
int main(){
	D x;
	//x._a=0;//此时会报错,因为这里产生了二义性
	x.A::_a=0
	x.B::_a=0;//这样可以暂时解决问题,需要显示指定访问

那这种情况下我们应该如何去解决这样的问题呢?
首先我们知道了D类中用拥有两个_a的成员变量,那么此时我们可以通过调试,使用内存可以看到他们的数据,首先我们先取d的地址
在这里插入图片描述
我们可以看到d内存和监视的地址是相同的
在这里插入图片描述
我们可以发现在B类和C类空间中都各有一份_a成员
此时,我们可以通过virtual,虚继承去解决二义性和数据冗余

//...略
class B : virtual public A
class C : virtual public A
class D : virtual public B, public C
//..略
int main(){
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	d._a = 0;
}

此时我们在到内存中查看

在这里插入图片描述
这时候我们可以发现BC空间中没有了_a的数据,在D类的空间中出现了2,但BC中产生了两个地址

在这里插入图片描述
在这里插入图片描述

我们进入到两个地址时,发现了14和0c两个数据
那么这是什么呢?
实际上这里两个值叫做偏移量,0x14=20,0x0c=12,这时候我们可以进行计算,D空间地址-B空间地址=20,D空间地址-C空间地址=12,所以我们可以通过存储偏移量,这样我们就可以只存1份_a了。
⭐02是A类成员变量的存储的空间,05是D类成员变量存储的空间
上面两个表叫做虚基表,我们通过B和C的两个指针指向一张表,指针叫做虚基表指针
但实际上因为我们需要格外增加指针寻找变量,所以效率降低了,更复杂

B b=d;
B* p=&d;
B& r=&d;//这样也可以获取到_a

总结

在多继承中我们可以感受到C++的复杂性,所以我们一般不建议设计出多继承,一定不要有菱形继承
我们应该优先使用组合,而不是类继承,继承中基类的内部细节对子类可见,继承一定成都破坏了基类的封装,使得基类和派生类耦合度很高
所以我们尽量使用组合,降低耦合度,组合被叫做黑箱复用,内部不可见。而继承叫白箱复用
最后感谢各位看到这里噢!!喜欢的可以点个赞噢!

想要了解多态的同学可以点击这里噢,简单易懂:【多态】多态的详细介绍,简单易懂

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

继承的详细介绍与理解,看了就懂 的相关文章

随机推荐

  • sqlite3中的execute与executemany

    预备知识 数据查询语言DQL 数据操纵语言DML 数据定义语言DDL 数据控制语言DCL 其中DQL为 select语句 数据操纵语言DML主要有三种形式 1 插入 INSERT 2 更新 UPDATE 3 删除 DELETE 使用exec
  • tensorflow使用

    tf reduce mean tf reduce mean 函数用于计算张量tensor沿着指定的数轴 tensor的某一维度 上的的平均值 主要用作降维或者计算tensor 图像 的平均值 reduce mean input tensor
  • 权限管理+安全框架shiro+密码加密器

    目录 1 权限管理 1 1 什么是权限管理 1 2 什么是身份认证 1 3 什么是授权 2 shiro框架 2 1什么是shiro 2 2 为什么使用shiro 简单 安全 2 3 shiro的核心组件 2 4 使用shiro完成认证 in
  • linux下Source /etc/profile不生效

    在linux下开发时 我们会经常安装很多环境 因为环境多 我们需要将其配置成全局命令 这样才好操作 配置成全局命令时 有一种方式是比较常用的 vim etc profile 增加配置信息 然后source etc profile是之生效 的
  • Java参数-Xms和-Xmx的区别

    java内存堆栈不够用时我们会寻求java参数 Xms和 Xmx的帮助 网上也有许多前辈给出了例子 但很多人喜欢把 Xms和 Xmx的值设置成一样的 甚至我还见过有吧 Xms设的比 Xmx还要大 Xms是最小值 Xmx是最大值 一开始我也不
  • Java中的网络编程

    文章目录 Java中的网络编程 一 网络编程 1 1网络编程的概念 1 2网络编程面临的问题 1 3网络编程的要素 二 IP 2 1IP的概念 2 2IP地址 2 3IP地址的分类 2 4测试IP地址 2 5java中测试IP的常用方法 三
  • EDA课程期末考试题(通信信工)

    eda没上过考试也能过 题目 1 图2中的7400 在我的博客EDA第一次课 1117电路图的绘制 中 已经大致讲过器件库如何加器件了 然后我再写下这几个图 我们现在创建一个PCB文件 点击file new project PCBproje
  • 【数据库创建与管理】【基本操作】

    文章目录 创建数据库 使用Studio创建 使用T SQL语言创建 管理数据库 使用SSMS 修改数据库存储容量 改名 删除 分离 附加 使用T SQL 修改 删除 分离 附加 创建数据库 使用Studio创建 右键 数据库 gt 新建数据
  • Java中类的方法

    目录 1 类的方法 1 1 方法的语法结构 1 2 方法的返回值 1 3 类的方法调用 2 成员变量和局部变量 2 1 变量的作用域 2 2 成员变量和局部变量 4 带参数的方法 4 1 定义 4 2 示例 5 包 5 1 包概述 5 2
  • C#刷新控件的几种方法

    Control Update 方法 https msdn microsoft com zh cn library 9dc1yh37 v vs 100 aspx 执行所有对绘制的挂起请求 可通过以下两种方法重绘窗体及其内容 您可以将 Inva
  • @Auto-Annotation自定义注解——接口限流篇

    Auto Annotation自定义注解 接口限流篇 自定义通用注解连更系列 连载中 首页介绍 点这里 前言 在访问高峰期为保证应用服务稳定运行 需要对高并发场景下接口进行接口限流处理 通对接口流量的访问限制能够在一定程度上防止接口被恶意调
  • 一、Python基础---计算机基本概念

    一 Python基础 计算机基本概念 1 计算机是什么 2 计算机的组成 3 计算机语言概述 4 计算机语言的发展 5 解释型语言和编译性语言的差别 6 交互方式 7 DOS命令 8 文本文件和字符集 8 1文本文件 8 2 常见字符集 9
  • unity3d-血条的设计

    任务目标 完成血条的预制设计 任务要求 分别使用 IMGUI 和 UGUI 实现 使用 UGUI 血条是游戏对象的一个子元素 任何时候需要面对主摄像机 分析两种实现的优缺点 给出预制的使用方法 实现过程 使用IMGUI实现 创建一个空对象
  • linux系统下常用的激活命令总结

    linux系统下常用的激活命令总结 作为一个刚入门linux的小白 很多的命令用了之后又会忘记 所以记录一下 方便后面回头查询 1 退出base环境 在terminal或者 bashrc文件中把conda自动启动设置为 false cond
  • Redis可视化客户端

    Redis是一个超精简的基于内存的键值对数据库 key value 一般对并发有一定要求的应用都用其储存session 乃至整个数据库 redis的可视化客户端目前较流行的有三个 Redis Client Redis Desktop Man
  • Vue 3 中的 Suspense 是什么?如何使用它

    Vue 3 中的 Suspense 是什么 如何使用它 介绍 Vue 3 是 Vue js 的最新版本 引入了一些令人兴奋的新功能和改进 其中之一是 Suspense 中文翻译为 暂停 机制 Suspense 是一种用于处理异步组件和延迟加
  • 教你App如何上架应用宝----腾讯开放开发平台

    上架app视频 http v youku com v show id XMTU0NTM1MTczNg html from y1 7 1 2 paction app在腾讯的 应用宝 市场 输入 czg学习演示 可以下载 注意 上架app视频的
  • 冒泡排序算法的Python实现(头歌实践教学平台)

    第1关 冒泡排序的实现 任务描述 本关任务 编写代码实现冒泡排序 相关知识 为了完成本关任务 你需要掌握 1 如何实现冒泡排序 2 冒泡排序的算法分析 冒泡排序 冒泡排序又称起泡排序 它的算法思路在于对无序表进行多趟比较交换 每趟都包括了多
  • ERROR: cannot launch node of type [turtlesim/turtlesim_node]

    这个之前路径是正确的 没有文档里说的那个BUG 为什么后续运行roslaunch rename01 node start turtle launch 的时候还是会报错呢 还是会出现 ERROR cannot launch node of t
  • 继承的详细介绍与理解,看了就懂

    继承的介绍 继承的概念及定义 定义格式 继承基类成员访问方式的变化 基类和派生类对象赋值转换 继承中的作用域 派生类的默认成员函数 继承与友元 继承与静态成员 复杂的菱形继承及菱形虚拟继承 总结 继承的概念及定义 继承也是面向对象的三大特性