C++(继承)


目录

前言:

正文:

1.继承的概念及定义

1.1继承的概念

1.2继承的本质

2.继承的定义

2.1继承格式

2.2继承关系和访问限定符

3 继承中的作用域

3.1隐藏

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

4.1切片

5 派生类中的默认成员函数

 5.1隐式调用

5.2显示调用

6 友元和继承

7 继承与静态成员

 8 菱形继承

8.1菱形继承的问题

8.2原因

8.3解决方案

9 补充



前言:

进入到c++面向对象的第二大板块继承,对此做一个复习巩固。所有的OO(面向对象)类型的语言都具备三大基本特征:封装-继承-多态,在累和对象复习篇章已经介绍了封装,今天主要复盘一下继承。


正文:

1.继承的概念及定义

1.1继承的概念

        继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,承是类设计层次的复用。

需要注意的是:

  • 被继承对象:父类/基类(base)
  • 继承方:子类/派生类(derive)

1.2继承的本质

        继承的本质就是 代码复用,只不过是类层次的复用,比如我定义了一个人的类,包含打印显示函数,作为学生我也有人的特征,我就可以继承这个类,作为教职工我也有人的特征,也可以继承这个类,这样就避免了代码的复写。比如:

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; //年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
//以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
	int _stuid; // 学号
};
class Teacher : public Person
{
protected:
	int _jobid; // 工号
};
int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}

  我们可以看到两个子类都具备父类中 public属性且相互不干扰。


 

2.继承的定义

2.1继承格式

         继承的格式很简单,子类:继承方式 父类,例如 上面那个例子 class Sdudent :public Person,需要注意的是 java中继承符号为 extern。

2.2继承关系和访问限定符

        继承方式有 共有继承(public)保护继承(protect)和私有继承(private)当然相应的访问限定符也是一样。如下图所示:

 需要注意的是:

权限大小:公有 > 保护 > 私有
保护 protected 比较特殊,只有在 继承 中才能体现它的价值,否则与 私有 作用一样

访问权限和继承方式各有三种,排列组合就是9种,如下所示:       
 

父类成员 / 继承权限 public protectedprivate
父类的 public 成员 外部可见,子类中可见外部不可见,子类中可见 外部不可见,子类中可见
父类的 protected 成员 外部不可见,子类中可见 外部不可见,子类中可见 外部不可见,子类中可见
父类的 private 成员 都不可见都不可见都不可见

总结:无论是哪种继承方式,父类中的 private 成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员时,依据 min(父类成员权限, 子类继承权限),只有最终权限为 public 时,外部才能访问。

假设不注明继承权限,class 默认为 privatestruct 默认为 public,最好是注明继承权限

小case:

实际使用中。权限是可以很好保护成员的,如何设计一个不能被继承的类呢?

  答:我们只要将我们不想被继承的类设置为私有,这样该类就不能被继承了,代码如下:

class base
{
private:
base();
~base();

};
class derived public base
{};

int main()
{
 derived d;
return 0;
}

 

3 继承中的作用域

1. 在继承体系中 基类 派生类 都有 独立的作用域
2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。 (在子类成员函数中,可以 使用 基类 :: 基类成员 显示访问
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在 继承体系里 面最好 不要定义同名的成员

3.1隐藏

隐藏也叫重定义,也可叫做重写(覆盖) 。子类中只要出现和父类中同名的成员就叫重定义不考虑返回值,参数。假设出现同名函数时,默认会将父类的同名函数隐藏调,进而执行子类的同名函数,

//父类
class Base
{
public:
	void func() { cout << "Base val: " << val << endl; }
protected:
	int val = 123;
};

//子类
class Derived : public Base
{
public:
	int func() 
	{ 
		cout << "Derived val: " << val << endl;
		return 0;
	}
private:
	int val = 668;
};

int main()
{
	Derived d;
	d.func();
	return 0;
}

执行结果:

此时,父子类中的方法和成员均被隐藏,执行的是子类方法,输出的是子类成员

 只修改子列方法名为funa

int funA() 
{ 
	cout << "Derived val: " << val << endl;
	return 0;
}

 执行结果:

函数名不同,不构成隐藏,结果是 父类方法+子类成员。

只修改子类成员为 num

 

int num = 668;

执行结果:

隐藏也消失了,执行结果:子类方法+父类成员

综上所述,当子类中的方法出现 隐藏 行为时,优先执行 子类 中的方法;当子类中的成员出现 隐藏 行为时,优先选择当前作用域中的成员(局部优先)

这已经证明了 父子类中的作用域是独立存在的

如何显式的使用父类的方法或成员?

 

  • 利用域作用限定符 :: 进行访问范围的限制 


 

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

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。

4.1切片

 将 父类对象 看作一个结构体,子类对象 看作结构体Plus 版

将 子类对象 中多余的部分去除,留下 父类对象 可接收的成员,最后再将 对象 的指向进行改变就完成了 切片


 

5 派生类中的默认成员函数

 派生类(子类)也是 ,同样会生成 六个默认成员函数(用户未定义的情况下)

不同于单一的 子类 是在 父类 的基础之上创建的,因此它在进行相关操作时,需要为 父类 进行考虑。

 5.1隐式调用

子类在继承父类后,构建子类对象时 会自动调用父类的 默认构造函数,子类对象销毁前,还会自动调用父类的 析构函数。

class Person
{
public:
	Person() { cout << "Person()" << endl; }
	~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
	Student() { cout << "Student()" << endl; }
	~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Student s;
	return 0;
}

注意: 自动调用是由编译器完成的,前提是父类存在对应的默认成员函数;如果不存在,会报错 

 

5.2显示调用

 因为存在 隐藏 的现象,当父子类中的函数重名时,子类无法再自动调用父类的默认成员函数,此时会引发 浅拷贝 相关问题

class Person
{
public:
	Person() { cout << "Person()" << endl; }
	void operator=(const Person& P) { cout << "Person::operator=()" << endl; }
	~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
	Student() { cout << "Student()" << endl; }
	void operator=(const Student&) { cout << "Student::operator=()" << endl; }
	~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Student s1;
	cout << "================" << endl;
	Student s2;
	s1 = s2;
	return 0;
}

 

 此时可用通过 域作用限定符 :: 显式调用父类中的函数

 

总的来说,子类中的默认成员函数调用规则可以概况为以下几点:

1.子类的构造函数必须调用父类的构造函数,初始化属于父类的那一部分内容;如果没有默认构造函数,则需要显式调用。
2.子类的拷贝构造、赋值重载函数必须要显式调用父类的,否则会造成重复析构问题
3.父类的析构函数在子类对象销毁后,会自动调用,然后销毁父类的那一部分
注意:

子类对象初始化前,必须先初始化父类那一部分
*子类对象销毁后,必须销毁父类那一部分
不能显式的调用父类的析构函数(因为这不符合栈区的规则),父子类析构函数为同名函数 destructor,构成隐藏,如果想要满足我们的析构需求,就需要将其变为虚函数,构成重写
析构函数必须设为 虚函数,这是一个高频面试题,同时也是 多态 中的相关知识
 


 

6 友元和继承

友元关系不能被继承

场景:友元函数 Print 可以访问父类中的私有成员,但子类继承父类后,友元函数无法访问子类中的私有成员

 

class Base
{
	friend void Print();
private:
	static const int a = 10;
};

class Derived : public Base
{
private:
	static const int b = 20;
};

void Print()
{
	cout << Base::a << endl;
	cout << Derived::b << endl;
}

int main()
{
	Print();
	return 0;
}

爸爸的朋友不可以是儿子的朋友,如果要想成为儿子的朋友,需要在儿子家里声明为友元。

 总结:友元关系不能被继承

7 继承与静态成员

         静态成员是唯一存在的,无论是否被继承

                静态变量为于静态区,不同于普通的堆栈区,静态变量的声明周期很长,通常是程序运行结束后才会被销毁,因此 假设父类中存在一个静态变量,那么子类在继承后,可以共享此变量

用计数的demo证明一下:

class Base
{
	friend void Print();
public:
	Base() { num++; }
	static int num;	//静态变量
};

int Base::num = 0;	//初始化静态变量

class Derived : public Base
{
public:
	Derived() { num++; }
};

void Print()
{
	cout << Base::num << endl;
}

int main()
{
	Derived d1;
	Derived d2;
	Derived d3;
	Print();
	return 0;
}

由此可见:静态成员是唯一存在的,并且被子类共享 


 

 8 菱形继承

单继承:一个子类只能继承一个父类
多继承:一个子类可以继承多个父类(两个及以上)

C++ 支持多继承,即支持一个子类继承多个父类,使其基础信息更为丰富,但凡事都有双面性,多继承 在带来巨大便捷性的同时,也带来了个巨大的坑:菱形继承问题

注:其他面向对象的高级语言为了避免出现此问题,直接规定了不允许出现多继承

8.1菱形继承的问题

菱形继承的问题:我们通过代码演示问题
class Person
{
public:
	string _name;	//姓名
};

//本科生
class Undergraduate : public Person
{};

//研究生
class Postgraduate : public Person
{};

//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};

int main()
{
	Graduate g1;
	g1._name = "zhangsan";

	return 0;
}

8.2原因

Undergraduate 中继承了 Person 的 _namePostgraduate 也继承了 Person 的 _name

Graduate 多继承 Undergraduate Postgraduate 后,同时拥有了两个 _name,使用时,无法区分

 

8.3解决方案

方法一:通过域访问限制符 :: 

Graduate g1;
g1.Undergraduate::_name = "zhangsan";
cout << g1.Undergraduate::_name << endl;
 

 这种方法并没有从本质上解决数据冗余的问题

方法二:虚继承

虚继承是专门用来解决 菱形继承 问题的,与多态中的虚函数没有直接关系

虚继承:在菱形继承的腰部继承父类时,加上 virtual 关键字修饰被继承的父类

 

class Person
{
public:
	string _name;	//姓名
};

//本科生
class Undergraduate : virtual public Person
{};

//研究生
class Postgraduate : virtual public Person
{};

//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};

int main()
{
	Graduate g1;
	g1._name = "zhangsan";
	cout << g1._name << endl;

	return 0;
}

 

虚继承是如何解决菱形继承问题的?

  • 利用 虚基表 将冗余的数据存储起来,此时冗余的数据合并为一份
  • 原来存储 冗余数据 的位置,现在用来存储 虚基表指针

 

虚继承底层是如何解决菱形继承问题的?

对于冗余的数据位,改存指针,该指针指向相对距离
对于冗余的成员,合并为一个,放置后面,假设想使用公共的成员(冗余成员),可以通过相对距离(偏移量)进行访问
这样就解决了数据冗余和二义性问题
为何在冗余处存指针?

指针指向空间有预留一个位置,可以用于多态
因此虚继承用的是第二个位置
新建对象进行兼容赋值时,对象指向指针处

该指针(偏移量)指向的目标位置不定
无论最终位置在何处,最终汇编指令都一样(得益于偏移量的设计模式)
虚函数是否会造成空间浪费?

不会,指针大小固定为 4/8 字节
指针所指向的空间(虚基表)是否浪费空间?

可以忽略不计,所有对象共享
假设存在多个共享成员,需要新增指针(偏移量),因为这些成员都是连续的,找到第一个,即可找到其他

即使涉及内存对齐问题,编译器也会根据规则做出调整
 

注意:为了解决 菱形继承 问题,想出了 虚继承 这种绝妙设计,但在实际使用中,要尽量避免出现 菱形继承问题


 

9 补充

继承是面向对象三大特性之一,非常重要,需要对各种特性进行学习

关于多继承时,哪个父类先被初始化的问题

谁先被声明,谁就会先被初始化,与继承顺序无关
除了可以通过继承使用父类中的成员外,还可以通过 组合 的方式进行使用

公有继承:is-a —> 高耦合,可以直接使用父类成员
组合:has-a —> 低耦合,可以间接使用父类成员
实际项目中,更推荐使用 组合 的方式,这样可以做到 解耦,避免因父类的改动而直接影响到子类

当然,使用哪种方式还要取决于具体场景,具体问题具体分析
 

//父类
class A {};

//继承
class B : public A
{
    //直接继承,直接使用
};

//组合
class C
{
private:
    A _aa;  //创建 A 对象,使用成员及方法
}

可能有的人问 继承 到底有什么用?答案很简单,为后面的 多态 实现铺路,也就是说,多态的实现离不开继承!

关于之前的 适配器 模式,除了可以使用 组合 的方式进行适配外,还可以通过 继承 的方式进行适配

  • queue -> dequelist
  • reverse_iterator -> iterator

在通过后者实现前者时,可以通过 组合,也可以通过 继承

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

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

相关文章

JDK17 SpringBoot3 整合常见依赖

JDK版本:17 SpringBoot 整合Mybatis Plus 、Redis等 依赖文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xs…

【MySQL】数据库和表的操作

数据库和表的操作 一、数据库的操作1. 创建数据库2. 字符集和校验规则&#xff08;1&#xff09;查看系统默认字符集以及校验规则&#xff08;2&#xff09;查看数据库支持的字符集&#xff08;3&#xff09;查看数据库支持的字符集校验规则&#xff08;4&#xff09;校验规则对…

代码随想录算法训练营 | day56 动态规划 583.两个字符串的删除操作,72.编辑距离

刷题 583.两个字符串的删除操作 题目链接 | 文章讲解 | 视频讲解 题目&#xff1a;给定两个单词 word1 和 word2&#xff0c;找到使得 word1 和 word2 相同所需的最小步数&#xff0c;每步可以删除任意一个字符串中的一个字符。 示例&#xff1a; 输入: "sea", &…

一键修复找不到msvcp140.dll无法继续执行代码的办法,有效修复

电脑出现“找不到msvcp140.dll无法继续执行代码”是什么情况&#xff1f;如果系统中没有这个文件或文件发生损坏&#xff0c;那么在启动某些应用程序或游戏时&#xff0c;可能会遇到错误消息&#xff0c;如“程序无法启动因为msvcp140.dll丢失在您的计算机上”或“找不到msvcp1…

Java方法重写及@Override注解(学习推荐版)

定义 所有类都继承了object类&#xff0c;例如重写object的toString()方法 规则 就近原则 重写的规则总结&#xff1a; ①子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同。 ②子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符 …

Java接收并解析HL7协议数据

一、前言 HL7协议相信医疗行业的IT人员都不陌生&#xff0c;笔者由于接触时间比较短&#xff0c;乍一听“协议”还是比较懵&#xff0c;不自觉就把它和“HTTP”、"SOAP”之类的网络协议挂上关联&#xff0c;可事实上这个HL7只是一种数据格式&#xff0c;传输方式也可以使用…

【基础知识】大数据组件YARN简述

YARN是一个分布式的资源管理系统。 YARN是Hadoop系统的核心组件&#xff0c;主要功能包括负责在Hadoop集群中的资源管理&#xff0c;负责对作业进行调度运行以及监控。 ResourceManager 负责集群的资源管理与调度&#xff0c;为运行在YARN上的各种类型作业分配资源。 非HA集…

FreeRTOS-07任务切换 对vPortSVCHandler和xPortPendSVHandler的理解

任务的基本单位是TCB块&#xff0c;相当于任务的身份证&#xff0c;其基本成员包括栈指针&#xff0c;栈所在地址&#xff0c;链表节点地址&#xff0c;如下&#xff1a; FreeRTOS.h typedef struct tskTaskControlBlock {volatile StackType_t *pxTopOfStack; /* 栈顶…

前缀和数组、差分数组、树状数组在Leetcode中的应用

文章目录 前缀和数组、差分数组、树状数组知识简单回顾Leetcode 1109. 航班预订统计Leetcode 307. 区域和检索-数组可修改LeetCode 面试题10.10. 数字流的秩LeetCode 1310. 子数组异或查询LeetCode 1409. 查询带键的排列 前缀和数组、差分数组、树状数组知识简单回顾 之前的文…

揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

c语言编写http服务器(Linux下运行)

参考文章&#xff1a;https://blog.csdn.net/baixingyubxy/article/details/125964986?spm1001.2014.3001.5506 上面是详细讲解&#xff0c;我这篇是总结了他的代码&#xff0c;因为他没给整体代码 所有代码&#xff1a; #include <stdio.h> #include <stdlib.h&g…

Python Django Jet:优化 Django 后台管理

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天分享 Python 中的 Django Jet 库。 Github项目地址&#xff1a;https://github.com/geex-arts/django-jet Django Jet 是一个强大的 Django 后台管理界面扩展&#xff0c;旨在提供更现代…

人工智能125个常用名词解释

1 什么是人工智能 人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;是指计算机系统通过模拟人类的思维和行为来完成特定任务的技术和方法。人工智能的研究涉及多个学科&#xff0c;包括计算机科学、数学、心理学、哲学等领域。 人工智能可以被分为…

SVM —— 理论推导

SVM 支持向量线性可分最大间隔超平面最大间隔超平面的推导支持向量分类间隔的推导最优化问题 对偶问题拉格朗日乘子法强对偶性 SVM 优化软间隔解决问题优化目标及求解 核函数线性不可分核函数的作用常见核函数 SVM 算法优缺点 支持向量机&#xff08;Support Vector Machine&am…

Collecting package metadata (current_repodata.json): failed(解决方案)

如果有重装过anaconda&#xff0c;在C盘的用户目录下&#xff0c;会有一个名叫.condarc的文件会自动生成。 当使用conda install和conda create命令会出现下面的问题&#xff1a;Collecting package metadata (current_repodata.json): failed 解决方案&#xff1a; 1.打开Anac…

Leetcod面试经典150题刷题记录 —— 双指针篇

双指针篇 1. 验证回文串Python3 2. 判断子序列Python3双指针 3. 两数之和 II - 输入有序数组Python3 4. 盛最多水的容器Python3双指针 5. 三数之和 1. 验证回文串 题目链接&#xff1a;验证回文串 - leetcode 题目描述&#xff1a; 如果在将所有大写字符转换为小写字符、并移除…

Spring Cloud + Vue前后端分离-第6章 通用代码生成器开发

Spring Cloud Vue前后端分离-第6章 通用代码生成器开发 6-1 代码生成器原理介绍 1.增加generator模块&#xff0c;用于代码生成 2.集成freemarker 通用代码生成器开发 FreeMarker 是一款模版引擎&#xff0c;通过模板生成文件&#xff0c;包括html页面&#xff0c;excel …

基于vue+element-plus+echarts制作动态绘图页面(柱状图,饼图和折线图)

前言 我们知道echarts是一个非常强大的绘图库&#xff0c;基于这个库&#xff0c;我们可以绘制出精美的图表。对于一张图来说&#xff0c;其实比较重要的就是配置项&#xff0c;填入不同的配置内容就可以呈现出不同的效果。 当然配置项中除了样式之外&#xff0c;最重要的就是…

腾讯云debian服务器的连接与初始化

目录 1. 远程连接2. 软件下载3. 设置开机自启动 1. 远程连接 腾讯云给的服务器在安装好系统之后&#xff0c;只需要在防火墙里面添加一个白名单&#xff08;ip 或者域名&#xff09;就能访问了。 防火墙添加本机WLAN的IPv4白名单&#xff0c;本地用一个远程工具连接&#xff…

C++设计模式之——命令模式

命令模式 概念创建步骤示例示例一代码实现运行结果 示例二代码实现运行结果 示例三示例代码运行结果 示例四代码实现运行结果 应用场景 概念 命令模式是一种行为型设计模式&#xff0c;它允许将请求封装为一个对象&#xff0c;从而使得可以参数化客户端请求、将请求排队或者记…