首页 > 编程语言 >C++ 继承( inheritance)

C++ 继承( inheritance)

时间:2024-03-17 14:31:41浏览次数:18  
标签:函数 inheritance 继承 成员 派生类 C++ 基类 变量

目录

一、简介:

二、继承

1.基础介绍:

1.1、

1.2 继承格式介绍

1.2.1

1.2.2

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

3.继承中的作用域

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

5.友元与继承

6.继承与静态成员变量

7.复杂的菱形继承及菱形虚拟继承

8.总结:


一、简介:

面向对象编程(Object-Oriented Programming,OOP)是一种程序设计范式,它基于以下三大特性:

  1. 封装(Encapsulation): 封装是将数据和方法(或函数)组合成一个单一的单元,并对外部隐藏其内部的实现细节的机制。通过封装,对象的内部状态和行为被保护起来,只有通过对象提供的公共接口才能访问和操作对象的数据。这样可以提高代码的可维护性和安全性,同时也方便了代码的复用和协作开发。

  2. 继承(Inheritance): 继承是一种机制,允许一个类(称为子类或派生类)基于另一个类(称为父类或基类)定义,并且在不改变原有类的情况下增加或修改其功能。子类继承了父类的属性和方法,并且可以在此基础上扩展新的属性和方法。继承使得代码的重用更为简单,同时也支持了代码的层次化组织和抽象概念的表示。

  3. 多态(Polymorphism): 多态是指同一个操作或函数在不同的对象上具有不同的行为或实现方式的能力。在面向对象编程中,多态允许一个函数根据调用时的对象类型以不同的方式进行响应,这样可以在不修改函数本身的情况下改变函数的行为。多态性提高了代码的灵活性和可扩展性,使得代码更加易于维护和扩展。

这三大特性共同构成了面向对象编程的基础,使得程序设计变得更加模块化、灵活和易于扩展。在许多现代编程语言中,如Java、C++、Python等,都支持面向对象编程,并且提供了丰富的语法和功能来支持封装、继承和多态。

二、继承

1.基础介绍:

1.1、

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

下面举个例子:

这里可以运行一下程序:

我们可以发现,这两个类执行的是同一个函数,侧面说明了父类是子类的成员,我们可以再通过调试看看是不是真的是这样。

如上图所示,t 这个类里面明显包含了person 这个类,说明前面的结论是对的;

1.2 继承格式介绍
1.2.1

1.2.2

继承关系与访问限定符:

用不同的方式继承,导致的效果会不一样。这里的表示继承方式的符号与访问限定符是一样的,

当我们以不同的方式继承时,派生类访问方式也会不同,具体如下表

这里如果强行去记,未免太过麻烦,这里我提供一个更好的记忆方式:由于访问限定符与继承方式符号时一样的,所以每次继承时,取两个符号中权限小的作为访问方式,权限默认 public >

protected > private, 比如基类的public 成员以protected 方式继承,那这个基类成员在派生类中就是protected 成员了。

这里总结一下:

<1> 基类中private 成员无论什么方式继承,在派生类都不可见,这里的不可见不是没有继承,而是继承了,但由于private访问限定符的原因,在基类外无法访问。

<2>如果我们想基类成员在类外不能被访问,但又在派生类内可以访问,我们就可以使用protected 这个访问限定符,这个限定符就是为了继承才诞生的。

<3>默认class关键字继承方式是private, 而struct 关键字的继承方式默认是public ,为了方便阅读,这里建议继承时最好表明继承方式。

<4>实际应用的时候一般用public 继承,主要是private 继承与protected 继承的代码只能在派生类中使用,实际应用中代码的拓展维护性不强。

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

这里先介绍一点其他知识

如上图所示,当我们以定义了一个 i 变量与d变量时,虽然两者的类型不同,但我们依然可以将i 变量赋值给 d 变量,这里很明显出现了类型转换,但是为什么下面的k 和 t 变量不可以呢?

这里是因为在类型转换的时候会出现一个临时变量,而且这个临时变量具有常性,所以第二个赋值就无法成立,需要在double& 前加一个const,使其具有常性才不会报错(如下图)。

这里解释一下为什么我们需要临时变量这个东西,举个例子:在一些比较中我们需要将字符与整型进行对比,但是这两个的类型是不一样的,所以一般都会对字符进行整型提升,将其变为和整型一样的类型才可以与其比较。假设没有临时变量,此时我们的字符就变成了整型了,而我们的目的是比较这两者的大小,并不希望改变字符,所以我们就需要另外一个变量来代替字符进行比较,确保原来字符不变。

那我们再看下面的例子

这里的teacher 转换为 person类型时为什么没有报错呢?其实这里的做了特殊处理(也就是为了语法体系逻辑自洽而设计的规定),这里的特殊处理就涉及到了基类和派生类对象赋值转换,俗称切片,

这里的变量最终都是指向派生类中的基类部分,也就是说,前面两张图中的t 、b、ptr 指向的或代表的都是teacher这个类中继承person的那一部分,这个就是赋值转换。

这里要说明几个点:

1、子类对象可以赋值给父类的指针、引用、或变量;

2、父类对象是不能赋值给子类对象(注意与第三条区别);

3.  基类的指针可以通过强制类型转换赋值给派生类的指针(不过这个要看情况,不能随便转换,要不然会有越界行为的发生);

示例:

3.继承中的作用域

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

我们可以举个例子:

比如上面这个类,_num再没有指定特定的作用域之前,是默认优先选择派生类里的同名变量(或函数),这就构成了隐藏,如果确实要访问基类里的成员就用上述所说的指明是哪一个基类即可。这里特别说明一下函数,若基类与子类的两个成员函数同名,则是构成隐藏,而不是重载,重载要在同一作用域内。

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

1.构造函数:

<1>如果我们没有写,由编译器生成默认的构造函数,则对派生类成员来说,就是跟普通的默认构造没什么区别,内置类型和自定义类型分别处理。而对派生类里的基类成员来说,它们会返回去调基类的默认构造(把基类比作爹,把派生类比作儿子,那这个过程就是相当于爹活只能爹干,儿子的活只能儿子干)。

<2>如果我们要显示的写构造函数,直接在初始化列表里面初始化是会报错的

像这张图一样,我们是没法在B 这个类里面初始name这个基类变量,所以在派生类里头我们一般是不用特地初始化基类成员变量的函数,因为基类的成员会自己调自己的默认构造函数。如果非要在派生类里初始化,只能将在初始化列表里直接加上基类的构造函数(基类的构造此时要自己写,不然会报错,因为如果不写编译器会找不到对应的构造函数)。

那么这里的初始化顺序是怎么样的呢?

答案其实显而易见,在学习默认构造函数时,我们就知道初始化的顺序跟初始化列表写的顺序没有关系,只和声明的顺序相关,在这里基类肯定比派生类要先声明,所以这里基类成员肯定会先初始化。

2.析构函数

其实析构函数大体跟构造函数差不多,只不过在一些地方有些差异,这里将介绍一些差异。

先看这张图:

我们可以发现,在对B这个类进行析构时,里面想调A 的析构函数是调不到的,这是因为隐藏的缘故,或许你会疑惑为什么函数名不同都能构成隐藏,这里其实跟多态的知识相关联,这里的函数名都会被处理成destructor(),这里也就变成了隐藏关系,由于牵扯到后面知识,这里就不在赘述。

如果我们一定在B这个派生类里调A这个基类的析构函数怎么办呢,在函数名前加上作用域即可(如下图),

在程序运行以后,我们会发现一个问题,那就是~A 函数调用了两次,这是为什么呢?

其实这是编译器为了保证析构时的顺序时先子后父,再派生类的析构函数结束后,编译器会自动调用基类的析构函数,这里说明一下为什么析构时要按照先子后父。

原因:正常情况下,我们不显示调父类(基类)的析构时,派生类里的基类成员会调自己基类构造函数。假设是基类成员先析构,如果在派生类析构函数里,是有权限调度基类的成员,如果此时派生类的析构对基类成员进行访问,就有可能出现野指针的情况,那程序就有可能会崩溃。所以在平常写的时候不建议显示地写基类的析构函数。

小结一下:其实这些默认成员函数规则其实和普通类是差不多的,只不过在继承这一部分,多了父类的成员,父类的那一部分将有父类自己的函数完成。

5.友元与继承

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员 (简单理解就是,你父亲的朋友不是你的朋友),如下图所示。

6.继承与静态成员变量

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。(无论你指定基类还派生类的作用域访问的都是同一个静态变量)

7.复杂的菱形继承及菱形虚拟继承

继承分为单继承,多继承与菱形继承,单继承就是子类只有一个直接父类(如下图)

多继承就是子类有两个及以上的父类(如下图)

单继承与多继承其实理解起来难度没什么大,最主要的菱形继承,这个继承相对复杂,这里着重介绍。

如上图,这里的继承关系就是菱形继承,这里会产生一些歧义,下面举个例子

在上面的者个例子中我们可以看见,A 被 B 与 C 所继承,同时D 又继承了B 与 C。 此时我们访问name 这个变量时,就会出现歧义,到底是访问B 中 A的变量,还是C 中 A 的变量呢?此时编译器无法确定,就会报错。简单的解决办法就是在name 前指明是哪个类,比如我要调B 里面继承的name , 那就在name 加上 "B :: "即可。当然,这个解决方式只是暂时解决了二义性(有歧义的意思)的问题,并没有解决数据冗余的问题,在D这个类中我们只要有一个name 变量即可。

这里介绍一下官方的解决办法

在B 和 C 这两个继承方式符号前加一个virtual 关键字

此时问题就得到解决了,我们可以打开监视窗口看看name 到底是不是只有一个。

打开测试窗口,我们会发现,所有的name都变成了666,说明在D 这个类里只有一个name变量,如果你还不相信,可以打印name的地址,这里不在赘述。

下面介绍一下这个方法的原理

这里我们先把继承方式符号前的virtual 去掉,然后依次设定变量的值,在内存中观察他们,

正常在内存中,变量的存储方式与上图一致,加上virtual后我们再观察一下有什么变化

可以看到,被virtual 修饰后,A 的成员变量已经跑到最下面来了。通过观察我们会发现B 和 C 这个类里面有存了一串数字(16进制),推测它可能是地址,我们可以通过内存窗口观察一下到底这个地址里面到底有什么东西。 

我们可以发现这两个地址里面都存了一个数字,分别是0x00000014 和 0x0000000c 转换成十进制,就是20 跟12 。此时我们如果将C 类所在的地址加12 和 B类的地址加20 我们就会发现它们的和刚好就是A 的地址,这里的指针指向的地址就叫作虚基表,里面存的是相对于A 的偏移量,或许你会疑惑为什么不能在虚基表里的第一个位置存偏移量呢?,其实这里还要存储其他内容,由于牵扯后面的内容,所以这里先卖个关子。需要注意的是,所有D 类的对象中的虚基表指向的都是同一个位置,不会因为对象不同而改变。

另外,如果在虚继承时,发生了赋值转换(切片),此时访问基类时通过虚机表里面的偏移量来实现的,而这就降低了访问速度,所以为了解决这个问题,代价很大。

补充一点,我们在编译时大多数变量的地址其实就确定了,但虚继承的对象成员(基类)要在运行时才能有确定的地址。

8.总结:

1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有会问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的面向对象语言都没有多继承,如Java(因为实在太坑了)。
3. 继承和组合
<1>public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
<2>组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
<3>优先使用对象组合,而不是类继承 。
<4>继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
<5>对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
<6>组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。(总结引自比特课件)

感谢各位读者的阅读,如有错误之处还望各位大佬指出,谢谢!!!

标签:函数,inheritance,继承,成员,派生类,C++,基类,变量
From: https://blog.csdn.net/2302_79538079/article/details/136630189

相关文章

  • 突破编程_C++_C++11新特性(智能指针与内存管理(1))
    1内存管理基础1.1什么是内存管理在C++中,内存管理是一个核心概念,它涉及到如何在程序执行过程中分配、使用和释放内存。由于C++允许程序员直接管理内存,因此内存管理在C++中显得尤为重要。合理的内存管理可以确保程序的正确运行,避免内存泄漏、野指针等问题,提高程序的......
  • 16.【CPP】详解继承
    继承方式如图注意点1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它2.基类private成员在派生类中是不能被访问,如果基类成员不......
  • LeetCode精选101刷题必备(C++)-附详细分类及解体说明
    分享一本leetcode刷题必备,互联网就业必备的免费书,非常好,值得推荐。感谢作者高畅无私整理和免费分享。本书介绍    本书分为算法和数据结构两大部分,又细分了十五个章节,详细讲解了刷LeetCode时常用的技巧。我把题目精简到了101道,一是呼应了本书的标题,二是不想让读......
  • java核心技术卷1 第五章:继承
    学习重要的是出,而不是入,此前一直埋头向前学,忽视了复习的重要性。写一个博客作为自己的学习笔记,也可作为以后查漏补缺的资料,温故而知新。类,超类和子类一个继承另一个类,父类也称为超类,基类。"超类"中的超来自于集合理论,指的是父类,与之后的super关键字对应java中,类的继承默认为pu......
  • c++中 int, long long, double 等数据类型的长度及范围整理
    原文链接:https://blog.csdn.net/mmk27_word/article/details/84378346byte:字节bit:位短整型short:所占内存大小:2byte=16bit;所能表示范围:-3276832767;(即-2^152^15-1)整型int:所占内存大小:4byte=32bit;所能表示范围:-21474836482147483647;(即-2^312^31-1)unsigned:所......
  • [C++] C++生成随机数
    一、简介在C语言中常使用srand()+random()的方式生成随机数,该方式并不是一个很好的随据说生成方法,一方面是因为其生成的随机数质量较低,另一方面其随机数范围也有所限制。在C++11中推荐使用随机数引擎的方式生成随机数。如何高效得生成高质量得随机数(甚至需要满足指定分布)是一个......
  • C++发布订阅者模式:实现简单消息传递系统
     概述:这个C++示例演示了发布者-订阅者模式的基本实现。通过`Event`类,发布者`Publisher`发送数据,而订阅者`Subscriber`订阅并处理数据。通过简单的回调机制,实现了组件间松散耦合的消息传递。好的,我将为你提供一个简单的C++实例,演示如何使用发布者-订阅者模式。在这个例......
  • C++文件操作实战:创建、写入、读取、修改文件一应俱全
     概述:此C++示例详解文件操作:创建、删除、判断存在、写入、读取和修改文件内容。清晰演示了常见文件处理方法及源代码实现。以下是一个简单的C++实例,演示如何进行文件操作,包括创建文件、删除文件、判断文件是否存在、向文件写入内容、读取文件内容以及修改文件内容。#include......
  • C++ 简单使用Json库与muduo网络库
    C++简单使用Json库与muduo网络库C++使用Json库测试代码均在Ubuntu20上运行首先下载json.hpp的代码链接然后和你的测试代码放在同一目录下面导入方式#include"json.hpp"usingjson=nlohmann::json;json序列化代码测试1voidtest1(){jsonjs;js["id"]={1......
  • UG NX二次开发(C++)-创建样条曲线(二)-UF_MODL_create_spline使用
    系列文章目录第一章、UGNX二次开发(C++)-创建样条曲线(一)-UF_CURVE_create_spline使用第二章、UGNX二次开发(C++)-创建样条曲线(二)-UF_MODL_create_spline使用提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录系列文章目录第一章、[UGN......