从C向C++7——继承

一.继承

1.理解继承

C++中的继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如儿子继承父亲的财产。

继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。

在C++中,派生继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。

被继承的类称为父类类,继承的类称为子类派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。

派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。

以下是两种典型的使用继承的场景:

  1. 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。

  2. 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员。

继承的一般语法为:

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

继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。

现在我们知道,public、protected、private 三个关键字除了可以修饰类的成员,还可以指定继承方式。

2.继承方式

不同的继承方式会影响基类成员在派生类中的访问权限。

1) public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

2) protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

3) private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。

3.改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

//基类People
class People {
public:void show();
protected:char *m_name;int m_age;
};
void People::show() {cout << m_name << "的年龄是" << m_age << endl;
}//派生类Student
class Student : public People {
public:void learning();
public:using People::m_name;  //将protected改为publicusing People::m_age;  //将protected改为publicfloat m_score;
private:using People::show;  //将public改为private
};

我们可以看到在派生类中对应的权限下进行using的基类成员再声明,即可改变类成员在派生类中的访问权限。

二.同名遮蔽问题

1.遮蔽

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

#include<iostream>
using namespace std;class Base {
public:int m_a;void func() {cout << "Base的func()调用" << endl;}Base() {m_a = 100;}
};class Son :public Base {
public:int m_a;            //同名成员属性void func() {        //同名成员方法cout << "Son的func()调用" << endl;}Son() {m_a = 200;}
};//测试同名属性
void test01() {Son s;cout << "Son下的m_a为:" << s.m_a << endl;cout << "Base下的m_a为:" << s.Base::m_a << endl;
}//测试同名方法
void test02() {Son p;p.func();p.Base::func();
}
int main() {test01();test02();return 0;
}

还需要补充的是,即使你在基类中使用函数重载,而派生类只有同名函数而没有对应的重载,想要在派生类中调用即类的某个重载函数也是不可能的,因为派生类中只要出现同名,就会遮蔽即类中所有的同名函数,无论其是否构成重载。

总结:

  • 1.子类对象可以直接访问到子类中同名成员
  • 2.子类对象加作用域可以访问到父类同名成员
  • 3.当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

2.同名静态成员处理

首先复习一下静态成员知识点:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

上面我们知道了一般成员的同名遮蔽问题和怎么处理?那么静态成员的处理是不是一样的呢?

其实静态成员和非静态成员出现同名,处理方式一致:

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

三.继承知识点

1.继承中的对象模型

第二部分就说过,有继承关系时,派生类的内存模型可以看成是基类非静态成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。

那么存在成员变量遮蔽时的内存分布模型是怎么样的呢?通过开发人员工具我们知道,假设A类有属性m_a,B有m_b,且遮蔽m_a,那么总共应该有m_a、m_b、A::m_a三份数据。

#include<iostream>
using namespace std;class Base {
public:int m1;static int m0;  //静态变量
private:int m2;
};class Son :public Base {
public:int m1;//同名遮蔽int m3;//新增变量
};void test01() {cout << "基类的内存模型大小为:" << sizeof(Base) << endl;cout << "派生类的内存大小为:" << sizeof(Son) << endl;
}int main() {test01();return 0;
}

运行结果为:

基类的内存模型大小为:8
派生类的内存大小为:16

总结一下,计算类的大小时,首先排除静态变量,然后判断是否有继承,继承又是否有同名遮蔽问题。

2.构造和析构顺序

#include<iostream>
using namespace std;class Base {
public:Base() {cout << "Base的构造函数" << endl;}~Base() {cout << "Base的析构函数" << endl;}
};class Son :public Base {
public:Son() {cout << "Son的构造函数" << endl;}~Son() {cout << "Son的析构函数" << endl;}
};void test01() {Son s;
}int main() {test01();return 0;
}

运行结果:

Base的构造函数
Son的构造函数
Son的析构函数
Base的析构函数

可以很明显的看到当存在继承关系时,我们虽然自定义了一个派生类对象,但是它也调用了基类的构造函数与析构函数,构造函数的顺序是先调用基类,再调用派生类,而析构函数则与此相反。

注意:还记得我们之前的封闭类吗,注意区分二者调用构造与析构的顺序。

3.构造与析构

前面我们说基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。

在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。

这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。

事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。

和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

四.多继承

1.多继承

在前面的例子中,派生类都只有一个基类,称为单继承。除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类。

多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:

class D: public A, private B, protected C{//类D新增加的成员
}

D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。

二义性问题

​ 当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。

2.多继承的对象内存模型

、B 是基类,C 是派生类,假设 obj_c 的起始地址是 0X1000,那么 obj_c 的内存分布如下图所示:

在这里插入图片描述

基类对象的排列顺序和继承时声明的顺序相同。

五.虚继承

1.菱形继承

多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:

在这里插入图片描述

类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径。

菱形继承,也叫钻石继承,也就是典型的钻石问题。

2.虚继承

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

在继承方式前面加上 virtual 关键字就是虚继承,

//间接基类A
class A{
protected:int m_a;
};
//直接基类B
class B: virtual public A{  //虚继承
protected:int m_b;
};
//直接基类C
class C: virtual public A{  //虚继承
protected:int m_c;
};
//派生类D
class D: public B, public C{
public:void seta(int a){ m_a = a; }  //正确void setb(int b){ m_b = b; }  //正确void setc(int c){ m_c = c; }  //正确void setd(int d){ m_d = d; }  //正确
private:int m_d;
};
int main(){D d;return 0;
}

这里B和C继承A的方式就叫虚继承,而A类称为虚基类。

这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。

3.虚继承的构造函数

​ 在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

​ 另外需要关注的是构造函数的执行顺序。虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。

4.虚继承的内存模型

对于普通继承,基类子对象始终位于派生类对象的前面(也即基类成员变量始终在派生类成员变量的前面),而且不管继承层次有多深,它相对于派生类对象顶部的偏移量是固定的。

前面我们说过,编译器在知道对象首地址的情况下,通过计算偏移来存取成员变量。对于普通继承,基类成员变量的偏移是固定的,不会随着继承层级的增加而改变,存取起来非常方便。

而对于虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量。

六.向上转型

在 C/C++中经常会发生数据类型的转换,例如将 int 类型的数据赋值给 float 类型的变量时,编译器会先把 int 类型的数据转换为 float 类型再赋值;反过来,float 类型的数据在经过类型转换后也可以赋值给 int 类型的变量。

类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型。相应地,将基类赋值给派生类称为向下转型。****

向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。运行结果也有力地证明了这一点,虽然有a=b;这样的赋值过程,但是 a.show() 始终调用的都是 A 类的 display() 函数。换句话说,对象之间的赋值不会影响成员函数,也不会影响 this 指针。

派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”,以发现,即使将派生类对象赋值给基类对象,基类对象也不会包含派生类的成员,所以依然不同通过基类对象来访问派生类的成员。

这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值。

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

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

相关文章

Codeforces Round 924(Div.2) A~E

A.Rectangle Cutting (模拟) 题意&#xff1a; 给出一个长方形&#xff0c;通过平行于原始矩形的一条边进行切割&#xff0c;将该矩形切割成两个边长为整数的矩形。询问是否能通过旋转和移动这两个矩形&#xff0c;得到新的矩形。 分析&#xff1a; 可以发现拼成的新长方形…

Python算法探索:从经典到现代(三)

一、引言 随着信息技术的飞速发展&#xff0c;数据已经成为现代社会不可或缺的资源。Python&#xff0c;作为数据处理和分析的利器&#xff0c;为我们提供了大量强大的库和工具&#xff0c;用于从经典到现代的各种算法探索。本文将带你领略Python在算法领域的魅力&#xff0c;从…

COW AI接入到微信 保姆教程 (部署在服务器,插件安装)

此文章不涉及国外的AI模型&#xff0c;也无需翻墙&#xff0c;跟某AI模型无关&#xff0c;审核大哥别弄错了 最近的AI开始越开越火了&#xff0c;开始介入到我们生活中的方方面面。就有人好奇AI是否能接入到微信吗&#xff1f;我在GitHub上搜索的时候还真有除了对话外还可以通…

MySQL 基础知识(五)之数据增删改

目录 1 插入数据 2 删除数据 3 更改数据 创建 goods 表 drop table if exists goods; create table goods ( id int(10) primary key auto_increment, name varchar(14) unique, stockdate date )charsetutf8; 1 插入数据 当要插入的数据为日期/时间类型时&#xff0c;如果…

Spring Boot 笔记 021 项目部署

1.1 引入坐标&#xff0c;并双击package打包成jar包 1.2 在服务器上运行jar包 1.3 使用postman测试 2.1 运行配置 2.1.1 命令更改端口 java -jar big-event-1.0-SNAPSHOT.jar --server.port7777 2.1.2 环境变量更新&#xff08;略&#xff09; 2.1.3 外部配置文件&#xff0c…

日期类运算符重载以及const成员详细解析

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言进阶 数据结构初阶 Linux C初阶 算法 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂 目录 一.前言 二.运算符重载 2.1概念 2.2比较的符号重载 2.2.1…

ZBX_NOTSUPPORTED: Unsupported item key. 原因排查

进行自定义监控项时&#xff0c;在zabbix-agent端测试没问题&#xff0c;却在zabbix-server进行测试时&#xff0c;出现 ZBX_NOTSUPPORTED: Unsupported item key. 1.在zabbix-agent测试没问题&#xff0c;证明自定义监控项的脚本没问题&#xff0c;却在zabbix-server端测试出…

点击侧边栏菜单时只切换 <router-view> 中的内容,而不是进行整个页面的路由跳转(动态路由)

解决方法&#xff1a;在 <el-menu> 的 select 事件中调用了 handleMenuSelect 方法来处理菜单项的选择。你可以在 handleMenuSelect 方法中根据菜单项的 index 来执行相应的操作&#xff0c;例如更新组件内的数据或者切换组件。由于整个页面的路由路径并没有改变&#xf…

平时积累的FPGA知识点(7)

平时在FPGA群聊等积累的FPGA知识点&#xff0c;第七期&#xff1a; 11 描述扇出的xilinx官方文档是&#xff1f; 解释&#xff1a;ug949 12 在BD中如何指定某个IP用global&#xff0c;其他的用OOC模式&#xff1f;因为某个模块引用的IP带着XPM&#xff0c;综合不了 解释&am…

代码随想录Day50 | 70. 爬楼梯 322. 零钱兑换 279.完全平方数

代码随想录Day50 | 70. 爬楼梯 322. 零钱兑换 279.完全平方数 70.爬楼梯322.零钱兑换279.完全平方数 70.爬楼梯 文档讲解&#xff1a;代码随想录 视频讲解&#xff1a; 状态 dp数组 dp[j]表示爬上第j阶台阶需要的方法数递推公式&#xff0c;由于在每一阶台阶可以向上走1~m阶&a…

代码随想录算法训练营Day57|647. 回文子串、516.最长回文子序列、动态规划总结

目录 647. 回文子串 前言 思路 算法实现 516.最长回文子序列 前言 思路 算法实现 动态规划总结 动规五部曲回顾 动规各小专题问题 647. 回文子串 题目链接 文章链接 前言 本题利用动态规划求解时&#xff0c;dp数组的定义与前面的就有些不同了&#xff0c;是难点之…

随机过程及应用学习笔记(三)几种重要的随机过程

介绍独立过程和独立增量过程。重点介绍两种独立增量过程-—维纳过程和泊松过程。 目录 前言 一、独立过程和独立增量过程 1、独立过程&#xff08;Independent Process&#xff09; 2、独立增量过程&#xff08;Independent Increment Process&#xff09; 二、正态过程&am…

debug - 打补丁 - 浮点数加法

文章目录 debug - 打补丁 - 浮点数加法概述笔记demo用CE查看汇编(x64debug)main()update_info()快捷键 - CE中查看代码时的导航打补丁的时机 - 浮点数加法补丁代码补丁效果浮点数寄存器组的保存END debug - 打补丁 - 浮点数加法 概述 在cm中, UI上显示的数值仅仅用来显示, 改…

力扣---通配符匹配

题目描述&#xff1a; 给你一个输入字符串 (s) 和一个字符模式 (p) &#xff0c;请你实现一个支持 ? 和 * 匹配规则的通配符匹配&#xff1a; ? 可以匹配任何单个字符。 * 可以匹配任意字符序列&#xff08;包括空字符序列&#xff09;。 判定匹配成功的充要条件是&#xff…

华为23年9月笔试原题,巨详细题解,附有LeetCode测试链接

文章目录 前言思路主要思路关于f函数的剖析Code就到这&#xff0c;铁子们下期见&#xff01;&#xff01;&#xff01;&#xff01; 前言 铁子们好啊&#xff01;今天阿辉又给大家来更新新一道好题&#xff0c;下面链接是23年9月27的华为笔试原题&#xff0c;LeetCode上面的ha…

开源≠不赚钱,开源软件盈利的7大模式。

开源不是目的&#xff0c;目的是圈用户&#xff0c;留住用户&#xff0c;盈利自然不成问题。 开源系统可以通过多种方式赚钱&#xff0c;以下是其中几种常见的方式&#xff1a; 提供付费支持&#xff1a; 开源系统可以提供付费的技术支持服务&#xff0c;包括安装、配置、维…

mysql5.6安装---windows版本

安装包下载 链接&#xff1a;https://pan.baidu.com/s/1L4ONMw-40HhAeWrE6kluXQ 提取码&#xff1a;977q 安装视频 1.解压完成之后将其放到你喜欢的地址当中去&#xff0c;这里我默认放在了D盘&#xff0c;这是我的根目录 2.配置环境变量 我的电脑->属性->高级->环境…

阿里云增加数据库访问白名单

阿里云增加数据库访问白名单 概况 我们希望在外网访问数据库时&#xff0c;可能会遇到无法连接的问题&#xff0c;这有可能是被拦截了。这时就需要去查看自己的ip有没有在白名单里面&#xff0c;没有的话就把ip加入到白名单。 路径 阿里云控制台-搜索RDS-进入RDS管理控制台…

宋小黑的生财知识资料库第1011期

大家好&#xff0c;我是小黑&#xff0c;在当前经济和职场环境中&#xff0c;拥有一份副业已经成为很多人增加财务安全感和提升个人能力的重要手段。通过投身于副业&#xff0c;你不仅可以为自己创造一个额外的收入来源&#xff0c;减少对主职工作的经济依赖&#xff0c;还可以…

【C++】类和对象(四)

前言&#xff1a;在类和对象中&#xff0c;我们走过了十分漫长的道路&#xff0c;今天我们将进一步学习类和对象&#xff0c;类和对象这块荆棘地很长&#xff0c;各位一起加油呀。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质量&a…