C++继承探究

news2025/1/19 23:24:39

文章目录

  • 一、继承相关概念
    • 1、 基本概念
    • 2、继承方式
    • 3、如何构造基类
    • 4、基类和派生类对象赋值转换
    • 5、继承中的作用域
  • 二、菱形继承的问题及解决方案
  • 三、虚继承的原理
  • 四、继承 VS 组合

一、继承相关概念

1、 基本概念

代码复用是编程语言设计的核心,对于一个函数,其实就是函数级别的代码复用,对于一个类,代码复用的方式有两种:继承和组合,继承是一种is-a的关系,而组合是一种a part of的关系。

继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。
成员变量继承一份给自己,成员函数和父类共用。
被继承的类称为基类或父类,继承的类称为派生类或子类。
继承和派生是一个概念,只是站的角度不同。
派生类除了拥有基类的成员,还可以定义新的成员,以增强其功能。

语法:
class 派生类名:[继承方式]基类名
{
    派生类新增加的成员
}; 

使用继承的场景:

  1. 如果新创建的类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承。
  2. 当需要创建多个类时,如果它们拥有很多相似的成员变量或成员函数,可以将这些类共同的成员提取出来,定义为基类,然后从基类继承。

2、继承方式

类成员的访问权限由高到低依次为:public --> protected -->private,public成员在类外可以访问,private成员只能在类的成员函数中访问。

如果不考虑继承关系,protected成员和private成员一样,类外不能访问。但是,当存在继承关系时,protected和private就不一样了。基类中的protected成员可以在派生类中访问,而基类中的
private成员不能在派生类中访问。

继承方式有三种:public(公有的)、protected(受保护的)和private(私有的)。它是可选的,继承方式如果不写,对于class默认为private,对于struct,默认为public。不同的继承方式决定了在派生类中成员函数中访问基类成员的权限。如图表示了继承到派生类中成员变量的访问权限在这里插入图片描述

继承规则:

  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限

例如,当继承方式为protected时,那么基类成员在派生类中的访问权限最高也为protected,高于protected的会降级为protected,但低于protected不会升级。再如,当继承方式为public时,那么基类成员在派生类中的访问权限将保持不变。也就是说,继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的。

  1. 不管继承方式如何,基类中的private成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。

注意:不能使用不代表没有继承,派生类是完完整整的继承了基类的所有成员的,其内存结构如下:
在这里插入图片描述
基类是作为一个对象完整继承在派生类中的,即使派生类对其不可访问

3.如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为public 或protected;只有那些不希望在派生类中使用的成员才声明为private。

4.如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

  1. 如果一个类不希望被继承,有两种方法:

C++98:把类的构造函数私有化。
C++11:在类定义时在类名后添加关键字:final

3、如何构造基类

  • 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
  • 如果没以指定基类构造函数,将使用基类的默认构造函数。
  • 可以用初始化列表指明要使用的基类构造函数。
  • 基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。
  • 派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
  • 实例化一个派生类对象时,先构造基类,才会构造派生类;析构时,先调用派生类的析构,才会调用基类的析构函数(先进先出的栈模型,如果是先调用基类的析构,会出问题,如:如果析构基类后,派生类析构之前用到了基类的成员呢?所以直接从设计上杜绝了这种问题的发生)

如果基类的拷贝构造、赋值重载这些函数特殊(深拷贝),那么派生类也需要写特定的函数对应,而且对于基类的成员部分初始化要显示调用基类的成员函数

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。
               
class A {        // 基类
public:
    int m_a;
private:
    int m_b;
public:
    A() : m_a(0) , m_b(0)                     // 基类的默认构造函数。
    { 
        cout << "调用了基类的默认构造函数A()。\n";  
    }
    A(int a,int b) : m_a(a) , m_b(b)     // 基类有两个参数的构造函数。
    { 
        cout << "调用了基类的构造函数A(int a,int b)。\n";  
    }
    A(const A &a) : m_a(a.m_a+1) , m_b(a.m_b+1)   // 基类的拷贝构造函数。
    {
        cout << "调用了基类的拷贝构造函数A(const A &a)。\n";
    }
               
    // 显示基类A全部的成员。
    void showA() { cout << "m_a=" << m_a << ",m_b=" << m_b << endl; }
};
               
class B :public A        // 派生类
{        
public:
    int m_c;
    B() : m_c(0) , A()             // 派生类的默认构造函数,指明用基类的默认构造函数(不指明也无所谓)。
    {
        cout << "调用了派生类的默认构造函数B()。\n";
    }
    B(int a, int b, int c) : A(a, b), m_c(c)           // 指明用基类的有两个参数的构造函数。
    {
        cout << "调用了派生类的构造函数B(int a,int b,int c)。\n";
    }
    B(const A& a, int c) :A(a), m_c(c)              // 指明用基类的拷贝构造函数。
    {
        cout << "调用了派生类的构造函数B(const A &a,int c) 。\n";
    }
           
    // 显示派生类B全部的成员。
    void showB() { cout << "m_c=" << m_c << endl << endl; }
};          
             
int main()
{
    B b1;                 // 将调用基类默认的构造函数。
    b1.showA();     b1.showB();
       
    B b2(1, 2, 3);      // 将调用基类有两个参数的构造函数。
    b2.showA();     b2.showB();
            
    A a(10, 20);      // 创建基类对象。
    B b3(a, 30);      // 将调用基类的拷贝造函数。
    b3.showA();     b3.showB();
}     

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

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。基类对象不能赋值给派生类对象。(如果赋值了,那么派生类多出来的成员变量怎么办?)
在这里插入图片描述

5、继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,而不是函数重载,都不在一个作用域中 (在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
 string _name = "小李子"; // 姓名
 int _num = 111;   // 身份证号
};
class Student : public Person
{
public:
 void Print()
 {
 cout<<" 姓名:"<<_name<< endl;
 cout<<" 身份证号:"<<Person::_num<< endl;
 cout<<" 学号:"<<_num<<endl;
 }
protected:
 int _num = 999; // 学号
};
void Test()
{
 Student s1;
 s1.Print();
};

二、菱形继承的问题及解决方案

C++中的继承有多种形态,单继承,多继承,菱形继承,其中菱形继承是多继承的特殊形态

单继承:一个子类只有一个直接父类时称这个继承关系为单继承
在这里插入图片描述
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
在这里插入图片描述
菱形继承:菱形继承是多继承的一种特殊情况。
在这里插入图片描述
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。

在这里插入图片描述

针对此问题,C++的设计者通过引入虚继承的概念来解决,如上面的继承关系,在Student和
Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用,没有意义

class Person
{
public :
 string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
 int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
 int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 Assistant a ;
 a._name = "peter";
}

三、虚继承的原理

虚拟继承解决数据冗余和二义性的原理 为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。

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

通过内存窗口和对成员变量的赋值,可以看到各个成员变量和对象的内存位置:
在这里插入图片描述

蓝色框的是B类、红色框的是C类,绿色框的是A类;可以看到,通过使用虚继承,会把造成数据冗余的类抽出来,这样就没有了数据冗余和二义性问题了。但是,抽出来单独存放后,怎么找到A类呢?还有B、C类中里面除了成员变量,存的那玩意是啥呢?(e0 7b d1 00 …)

在这里插入图片描述

B和C虚继承后,不再对象内存储A,里面反而多了个指针,这个指针指向指向的一张表。这张表记录了虚继承的基类成员的偏移量。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A。

四、继承 VS 组合

继承:类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。继承的操作权限更大,一定程度的破坏了父类的封装性,且父子类之间的耦合度较高。

组合:对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。各个类的封装性得以保持,耦合度低。

继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。

但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。

对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。
  这导出了我们的面向对象设计的一个原则:
  两者关系在is-a和a part of都相差无几的情况下,优先使用对象组合,而不是类继承。

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

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

相关文章

数学之-曼德勃罗

参考: 分形系统介绍以及代码实现 - 知乎 Java实现高分辨率曼德伯罗特集 - 知乎 Mandelbrot集的最新变化形态一览——MandelBox&#xff0c;Mandelbulb&#xff0c;Burning Ship&#xff0c;NebulaBrot-CSDN博客

VL53L5CX驱动开发(5)----运动阈值检测

VL53L5CX驱动开发----5.运动阈值检测 概述视频教学样品申请源码下载生成STM32CUBEMX选择MCU串口配置IIC配置 INT设置配置使能与复位X-CUBE-TOF1串口重定向代码配置检测流程TOF代码配置主程序演示结果 概述 本章目的是展示如何充分利用VL53L5CX传感器的高级特性&#xff0c;通过…

【LLM微调范式1】Prefix-Tuning: Optimizing Continuous Prompts for Generation

论文标题&#xff1a;Prefix-Tuning: Optimizing Continuous Prompts for Generation 论文作者&#xff1a;Xiang Lisa Li, Percy Liang 论文原文&#xff1a;https://arxiv.org/abs/2101.00190 论文出处&#xff1a;ACL 2021 论文被引&#xff1a;1588&#xff08;2023/10/14&…

C# 图解教程 第5版 —— 第6章 方法

文章目录 6.1 方法的结构6.2 方法体内部的代码执行6.3 局部变量6.3.1 类型推断和 var 关键字6.3.2 嵌套块中的局部变量 6.4 局部常量6.5 控制流6.6 方法调用&#xff08;*&#xff09;6.7 返回值&#xff08;*&#xff09;6.8 返回语句和 void 方法6.9 局部函数6.10 参数&#…

关于数据链路层(初步)

以太网帧格式&#xff1a; 源地址和目的地址是指网卡的硬件地址&#xff08;也叫MAC地址&#xff09;&#xff0c;长度是48位&#xff0c;是在网卡出厂时固 化的&#xff1b; 帧协议类型字段有三种值&#xff0c;分别对应载荷的形式&#xff0c;有IP、ARP、RARP&#xff1b; …

Go-Python-Java-C-LeetCode高分解法-第十周合集

前言 本题解Go语言部分基于 LeetCode-Go 其他部分基于本人实践学习 个人题解GitHub连接&#xff1a;LeetCode-Go-Python-Java-C 欢迎订阅CSDN专栏&#xff0c;每日一题&#xff0c;和博主一起进步 LeetCode专栏 我搜集到了50道精选题&#xff0c;适合速成概览大部分常用算法 突…

LuaJit交叉编译移植到ARM Linux

简述 Lua与LuaJit的主要区别在于LuaJIT是基于JIT&#xff08;Just-In-Time&#xff09;技术开发的&#xff0c;可以实现动态编译和执行代码&#xff0c;从而提高了程序的运行效率。而Lua是基于解释器技术开发的&#xff0c;不能像LuaJIT那样进行代码的即时编译和执行。因此&…

手摸手Redis7配置哨兵模式(一主二从三哨兵)

安装redis #安装gcc yum -y install gcc gcc-c #安装net-tools yum -y install net-tools #官网https://redis.io/ cd /opt/ wget http://download.redis.io/releases/redis-7.0.4.tar.gz 解压至/opt/目录下 tar -zxvf redis-7.0.4.tar.gz -C /opt/ #编译安装 make make ins…

解决“本地计算机上的 mysql 服务启动后停止,某些服务在未由其他服务或程序使用时将自动停止”

电脑在服务中启动mysql报 如果你之前没有修改过数据库相关文件那么执行以下步骤 1.在数据库的根目录删除data文件&#xff08;删除前最好先备份一下&#xff09; 2&#xff0c;然后重新创建一个data文件夹 3.点击进入bin目录&#xff0c;点击上面的路径 4.点击后上面路径变蓝…

MATLAB——BP神经网络信号拟合程序

欢迎关注公众号“电击小子程高兴的MATLAB小屋” %% 学习目标&#xff1a;BP神经网络 %% 函数逼近 数据压缩 模式识别 %% 考虑要素&#xff1a;网络层数 输入层的节点数 输出层的节点数 隐含层的节点数 %% 传输函数 训练方法 %% 对信号曲线进行拟合 clear all; cle…

04_led灯闪烁

创建新的项目&#xff0c;步骤和教程2一样&#xff0c;项目结构和创建后的代码如下所示 具体代码如下所示&#xff1a;使用16进制加延迟的方式控制led的亮灭0表示亮1表示灭 #include <REGX52.H> #include <INTRINS.H> void Delay500ms() //11.0592MHz {unsigne…

网页在线打开PDF_网站中在线查看PDF之pdf.js

一、pdf.js简介 PDF.js 是一个使用 HTML5 构建的便携式文档格式查看器。 pdf.js 是社区驱动的&#xff0c;并由 Mozilla 支持。我们的目标是为解析和呈现 PDF 创建一个通用的、基于 Web 标准的平台。 pdf.js 将 PDF 文档转换为 HTML5 Canvas 元素&#xff0c;并使用 JavaScr…

ASAN地址消毒+GCOV覆盖率分析

安全之安全(security)博客目录导读 覆盖率分析汇总 目录 一、代码示例 二、代码编译及运行 三、ASAN地址消毒&#xff08;找到溢出&泄露点&#xff09; 四、GCOV覆盖率分析 ASAN相关详见ASAN(AddressSanitizer)地址消毒动态代码分析 GCOV相关详见GCOV覆盖率分析 现…

Doris入门了解

微信公众号&#xff1a;大数据高性能计算 大数据存储与分析入门学习文档&#xff1a;深入了解 Doris 大数据技术已成为现代数据处理的核心组成部分&#xff0c;为企业提供了更多洞察和决策支持。Doris&#xff08;以前称为Palo&#xff09;是一种用于大规模数据存储和分析的开…

【LeetCode刷题(数据结构与算法)】:完全二叉树的节点个数

完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层&#xff0c;则该层包含 1~ 2h 个节点 输入&#xff1a;r…

Megatron-LM GPT 源码分析(二) Sequence Parallel分析

引用 本文基于开源代码 https://github.com/NVIDIA/Megatron-LM &#xff0c;延续上一篇Megatron-LM GPT 源码分析&#xff08;一&#xff09; Tensor Parallel分析 通过对GPT的模型运行示例&#xff0c;从三个维度 - 模型结构、代码运行、代码逻辑说明 对其源码做深入的分析。…

Spring framework Day15:@lmport注解使用

前言 在编程中&#xff0c;import注解通常用于导入外部的类、接口或其他资源&#xff0c;以便在当前代码文件中使用。它可以提供一种简洁、方便的方式来引入外部依赖&#xff0c;并且有以下几个主要的应用场景和好处&#xff1a; 引入外部类/接口&#xff1a;使用import注解可…

2023年最受好评的五款项目计划工具排名揭晓!

近年来&#xff0c;项目计划工具已经成为项目管理中不可或缺的一部分。正确的项目计划工具将帮助您更有效地管理项目&#xff0c;从而改善结果。随着技术的进步&#xff0c;现在有许多强大而通用的项目计划工具可用。展望2023年&#xff0c;以下是你应该考虑的深受好评的五款项…

如何在Linux上安装Tomcat

安装Tomcat的前提是安装好JDK 使用yum安装JDK Liunx的包管理器就如同手机上的应用商城一样&#xff0c;可以在其中下载软件 Linux中的包管理器有很多&#xff1a;yum、apt、pacman...其中yum是centos自带的包管理器 获取与jdk有关的数据包 请注意&#xff1a;i686后缀的为32位操…

DarkGate恶意软件通过消息服务传播

导语 近日&#xff0c;一种名为DarkGate的恶意软件通过消息服务平台如Skype和Microsoft Teams进行传播。它冒充PDF文件&#xff0c;利用用户的好奇心诱使其打开&#xff0c;进而下载并执行恶意代码。这种攻击手段使用了Visual Basic for Applications&#xff08;VBA&#xff0…