C++继承相关总结
文章目录
- 前言
- 1.继承的相关概念
- 1.继承概念
- 2.继承的相关语法
- 3.基类和派生类对象赋值转换(赋值兼容规则)
- 2.继承中的注意事项
- 1.继承中的作用域
- 2.派生类的默认成员函数
- 1.构造函数与拷贝构造
- 2.赋值重载与析构
- 3.友元关系与静态成员变量
- 3.多继承(菱形继承)
- 1.虚拟继承
- 2.虚拟继承的背后原理
- 4.继承总结与反思
- 1. 继承和组合
前言
本文主要是对C++三大特性之一的继承进行讲解。将会围绕继承的概念,继承的语法,继承方式,多继承中的菱形继承以及产生的问题进行介绍。
1.继承的相关概念
1.继承概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
2.继承的相关语法
在对继承语法进行介绍之前,我们先看看如下示例代码。
#include<iostream>
using namespace std;
class Person
{
public:void Print(){cout << _name <<" " << _age << endl;;}
private:string _name ="aaa";int _age=11;
};class Student :public Person
{private: int _id;
};
int main()
{Student s;s.Print();
}
通过上述简单示例我们不难看出继承就是一种代码复用的手段。students类被称为派生类,也被称为子类,Preson被称为父类也叫做基类。这里继承方式是public公有继承。
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这点和我们现实世界比较像比如儿子继承父亲的遗产。这里继承被分为3种方式,分别是public共有继承,protected保护继承,private私有继承。
这里我们可以看出如下几点:
1基类private成员在派生类中无论以什么方式继承都是不可见的。
这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它.通俗来说不可见就是不能用。
2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3.基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
其实我们这里可以看出
父类中的被private修饰的成员是用来防儿子的,protected修饰的成员是用来防外人的。
3.基类和派生类对象赋值转换(赋值兼容规则)
派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
但是要注意,基类对象不能赋值给派生类对象。也就是说父不能给给子,但是子可以给给父。可以抽象的理解为不能啃老。
这里发生赋值兼容的时候并没有发生隐式类型转换,没有产生临时变量,这里是天然支持的。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。
但是必须是基类的指针是指向派生类对象时才是安全的。
也就说基类的指针的是可以通过强制类型转化赋值给一个派生类指针,但是不建议这么做,最好是基类指针指向的一个派生类对象。因为派生类的对象的空间的会比基类大,这样做可能会引发越界访问的问题。
2.继承中的注意事项
1.继承中的作用域
在继承体系中基类和派生类都有独立的作用域。
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
(在子类成员函数中,可以使用 基类::基类成员 显示访问)需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
注意在实际中在继承体系里面最好不要定义同名的成员。
这里Student中有_name的成员变量,但是父类也有一个_name的成员变量。如果不指定的话默认是Studnet对象在调用Print函数时默认是打印Student类中的成员变量_name,这里是根据就近原则来的。
这里父类和子类都有一个函数Print这里子类在调用Print函数会调用子类自己的Print函数,这里就会构成函数隐藏。
这里不是函数重载,函数重载的前提条件是在同一作用域下的两个函数。子类和父类的函数名相同就会构成隐藏。
构成函数隐藏后如果想使用父类的成员函数必须指定类域,不然就会程序报错。
2.派生类的默认成员函数
1.构造函数与拷贝构造
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
从上述代码中不难看出无论如何子类从父类那里继承得来的变量都会先调用父类构造函数先初始化。
同样的子类的拷贝构造函数必须调用父类的拷贝构造完成继承下来的父类的成员变量拷贝初始化。
我们之前讲了父类与子类的赋值兼容规则,因此s虽然是子类类型的引用,但是通过切片处理依然可以调用父类的拷贝构造进行初始化。
2.赋值重载与析构
派生类的operator=也必须要调用基类的operator=完成基类的复制,但是有个点要注意:
因为父类与子类赋值重载的函数名是一样的,会构成隐藏,这样就得显示指定一下作用域。
析构函数就和之前的有所区别了,子类的析构函数不用我们显示写,子类对象在调用完析构函数后,编译器会自动调用父类的析构函数再去析构从父类那里继承下来的成员。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
因为对于子类对象来说,我们是先构造从父类那里继承下来的成员,之后在构造子类自己的成员。我们知道先构造的应该被后析构所以说这里是为了保证析构顺序。
3.友元关系与静态成员变量
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
抽象理解为:父亲的朋友不是儿子朋友。
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
静态成员变量对子类来说只有一份示例 对于父类来说也是只有一份实例 静态成员这份实例是被父类和子类共享的,静态成员变量是属于类的被所有对象共享。
补充一道很有意思的题目
如何设计一个不能被继承的类呢?
其实我们把该类的构造函数或者析构函数设为私有即可。
那可能会有人问我怎么创建销毁对象,这里我们可以提供一个静态方法就行了。比如我以构造函数为例。
这样设计出了一个不能被继承的类。
3.多继承(菱形继承)
以上都是单继承,继承还可以多继承。
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
多继承但是会引发一个特别的大的问题,就是菱形继承。菱形继承是一种特殊的多继承,即
两个派生类继承同一个基类,同时两个派生类又作为基类继承给另一个派生类。
菱形继承的问题:
从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
代码示例
#include<iostream>
using namespace std;
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;
};
int main()
{D d1;d1.B::_a = 1;d1.C::_a = 2;d1._b = 3;d1._c = 4;d1._d = 5;}
菱形继承的对象成员模型图
这里就看出来了D类中有两份a,这样就会数据冗余造成空间资源的浪费。同时也会造成二义性,我们访问_a的时候到底是访问哪个_a呢?
也许有人会说我们指定一下类域不就行了,但是空间资源的浪费怎么解决呢?而且正常来说我们肯定是想只有一份_a数据,就像我们只有只有唯一一个身份证号一样。
为了解决菱形继承引发的问题,C++中引入了虚拟继承的概念。
1.虚拟继承
我们先来看看虚拟继承的语法在来探究一下虚拟继承背后的原理。
我们看到D的父类B,父类C通过关键字virtual采用虚拟继承的方式去继承A类
,这样我们D类在继承BC的时候就不会有数据冗余和二义性的问题了。要注意的是虚拟继承不要在其他方去使用。
2.虚拟继承的背后原理
vs的监视窗口不能很好的观察,我将通过vs的内存窗口去观察一下这个D的对象模型。
从上图中我们可以看到D从b那里继承了_b外还有一个指针,这个指针被称为虚基指针,这个指针是指向一个虚基表这个表里存放相对于_a位置的偏移量,同样的对C来说也是如此。
虚拟继承的原理是在派生类中添加一个虚基指针(vbp),指向一个虚基表(vbt),虚基表中存放了基类的偏移量。当派生类的对象要访问基类的成员时,就可以通过虚基指针和虚基表找到基类的地址,从而访问基类的成员。
当D类的对象要访问A类的成员时,就可以通过虚基指针和虚基表找到A类的地址,从而访问A类的成员。
关于虚基类与虚基表的补充
上述例子中A是虚基类,因为它被B类和C类以虚拟继承的方式继承了。
虚基类是指在菱形继承结构中,被多个派生类以虚拟继承的方式共同继承的基类。
虚基表中存放的是虚基类相对于派生类对象的偏移量,虚基表不在虚基类或派生类里,而是在额外的空间中。虚拟继承确实会增加一些空间开销,因为每个虚拟继承的类都需要一个虚基指针和一个虚基表。但是,虚拟继承也可以避免菱形继承造成的数据冗余和二义性的问题,因为虚基类只会在最终的派生类中存在一份拷贝。
使用虚拟继承的派生类才有自己的虚基表。
例如,对于A类,它是一个虚基类,但它并没有自己的虚基表,也没有虚基指针。对于B类和C类,它们是使用虚拟继承的派生类,所以它们有自己的虚基表和虚基指针。对于D类,它是一个多重继承的派生类,它也有自己的虚基表和虚基指针,但是它只继承了B和C的虚基指针,而不是A的。
虚基指针除了指向虚基表外,还有一个作用是用于确定对象的类型。当一个对象被转换为一个虚拟继承的基类的指针时,它的虚基指针会被调整为指向该基类的虚基表。这样就可以通过虚基指针来判断对象的真实类型。D * d = new D();B * a = d;这个时候d的虚基指针会调整为指向B的虚基表。
补充一道面试题
class A
{
public:A(string s){cout << s << endl;}};
class B :virtual public A
{
public:B(string s1,string s2):A(s1){cout << s2 << endl;}
};
class C:virtual public A
{
public:C(string s1, string s2):A(s1){cout << s2 << endl;}};
class D :public B, public C
{
public:D(string s1, string s2, string s3, string s4):C(s1,s3),B(s1, s2),A(s1){cout<<s4<<endl;}
};
int main()
{D d1("classA","classB","classC","classD");
}
这里打印结果是什么呢?
实现是用菱形继承所以D构造的的时候对于s1 s2 s3 s4来说只会调用一次构造函数,也就是说s1会被A的构造函数调用打印,s2会被B的构造函数调用打印,s3会被C的构造函数调用打印,我们知道初始化列表的顺序是声明顺序。所以这里A先被继承之后再是BC被继承,所打印结果是classA clasB classC classD.
总结
在上述例子中B C虚拟继承A,B C中就会各自一个虚基指针指向各自的虚基表。D继承BC的时候会继承BC的虚基指针,也就是说D的虚基指针是BC虚基指针的组合,同样的D的虚基表也就是B C虚基表的组合。这样就解决了数据冗余问题和二义性。
4.继承总结与反思
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。
所以一般不建议设计出多继承,一定不要设计出菱形继承否则在复杂度及性能上都有问题。
多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
1. 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
简单来说,就是车是交通工具这就是is-a的关系,车里有发动机这就是has-a的关系。
优先使用对象组合,而不是类继承 。继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。
类之间的关系如果可以用继承,也可以用组合,首选用组合。
以上内容如有问题,欢迎指正!
相关文章:

C++继承相关总结
文章目录前言1.继承的相关概念1.继承概念2.继承的相关语法3.基类和派生类对象赋值转换(赋值兼容规则)2.继承中的注意事项1.继承中的作用域2.派生类的默认成员函数1.构造函数与拷贝构造2.赋值重载与析构3.友元关系与静态成员变量3.多继承(菱形继承)1.虚拟继承2.虚拟继…...

【从零开始学习 UVM】8.2、Reporting Infrastructure —— uvm_printer 详解
文章目录 老派风格在UVM中如何完成uvm 风格Table printerTree printerLine printerprint使用print使用条件使用konb更改print配置示例在一个随机验证环境中,数据对象不断地由不同的组件生成和操作,如果能够显示对象的内容,则调试会变得更加容易。 老派风格 传统上,这是通…...

Mybatis、TKMybatis对比
文章目录1.Mybatis(1)配置文件(2)实体类(3)Mapper(4)mybatis-config.xml2.TKMybatis(1)配置文件(2)实体类(3)M…...

37了解高可用技术方案,如冗余、容灾
高可用性技术方案是指在系统设计和架构中采用一系列措施来确保系统在遇到各种故障和问题时仍能保持持续的可用性,避免因单点故障而导致系统宕机、数据丢失等问题。其中包括冗余和容灾技术。 冗余技术: 冗余技术是指通过增加系统组件的冗余来提高系统可靠…...

jdb调试问题集锦
https://bbs.kanxue.com/thread-210049.htm蓝铁 1 2017-8-25 19:40 4 楼 0 根据提示,可知,出错的地方是,android.app.ActivityThread.handleBindApplication(), 行4,400 查看源码可以发现,代码中指向的是app.onCreate() …...

要和文心一言来一把你画我猜吗?
想和文心一言来一把你画我猜吗? ChatGPT的爆火,让AI对话模型再次走入大众视野。大家在感叹ChatGPT的智能程度时,总会忍不住想:如果我们也有自己的AI对话模型就好了。在社会的压力下,国内的厂商和研究机构也纷纷做出尝试…...

delete[] p->elems和free(p->elems)有什么区别?
delete[]和free()都是释放内存的函数,但它们具有不同的使用方法和适用情况。 delete[] 通常用于释放C中动态分配的数组空间。在使用new[]运算符分配内存时,应使用delete[]运算符来释放分配的内存。delete[] 运算符会调用每个数组元素的析构函数…...

CAS问题
CAS🔎什么是CAS🔎伪代码解析🔎CAS是如何实现原子性的🔎CAS的应用🌻实现原子类🌻实现自旋锁🔎ABA问题🌻ABA问题可能引起的BUG🌻ABA问题的解决方案🔎结尾&#…...

网络编程socket(下)
目录 一、TCP网络程序 1.1 服务端初始化 1.1.1 创建套接字 1.1.2 服务端绑定 1.1.3 服务端监听 1.2 服务端启动 1.2.1 服务端获取连接 1.2.2 服务端处理请求 1.3 客户端初始化 1.4 客户端启动 1.4.1 发起连接 1.4.2 发起请求 1.5 网络测试 1.6 单执行流服务端的…...

华为OD机试题【打折买水果】用 C++ 编码,速通
最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧本篇题解:打折买水果 题目 有 m m m…...

JSON 数据类型
JSON 数据类型 JSON 格式支持以下数据类型: 类型描述数字型(Number)JavaScript 中的双精度浮点型格式字符串型(String)双引号包裹的 Unicode 字符和反斜杠转义字符布尔型(Boolean)true 或 fal…...

Python函数简介
Python是一种高级编程语言,它的函数是其中一个非常重要的特性。在程序中,函数是一段被命名的代码块,它可以接受输入并且返回输出。本篇文章将介绍Python函数的基本概念、定义、调用和参数。 基本概念 在Python中,函数是由def关键…...

一文读懂 mysql 为什么要两阶段提交以及两阶段提交原理
文章目录 为什么要两阶段提交redo log与binlog两份日志之间的逻辑不一致,会出现什么问题?两阶段提交是怎么保证逻辑一致的呢?当 binlog 写完,redo log 还没 commit 前发生 crash,那崩溃恢复后 MySQL 如何处理?redo 与 binlog 的刷盘时机MySQL 的双 1 配置能否只用 redo l…...

启动Hadoop报错【Error: JAVA_HOME is not set and could not be found.】
当用了一下午从0安装上Hadoop兴奋的启动的时候! Error: JAVA_HOME is not set and could not be found. 他告诉我JAVA_HOME 没被找到? 我明明安装了java的,为什么找不到? java -version看了下发现是没问题的 解决: 后…...

《MySQL系列-InnoDB引擎35》索引与算法-B+树索引的使用
B树索引的使用 1 不同应用中B树索引的使用 在OLTP中,B树索引建立后,对该索引的使用应该只是通过该索引取得表中少部分的数据。这时建立B树索引才是有意义的,否则即使建立了,优化器也可能不选择使用索引。 在OLAP中,…...

【EHub_tx1_tx2_E100】不止科技NVISTAR ROC 300激光雷达Ubuntu18.04+ROS1ROS2 评测
介绍NVISTAR 的二维DTOF激光雷达 ROC 300在EHub_tx1_tx2_E100载板,TX1核心模块环境(Ubuntu18.04)下测试ROS1驱动和ROS2的驱动,打开使用RVIZ 查看点云数据,本文的前提条件是你的TX1里已经安装了ROS1版本:Mel…...

C语言函数大全--d开头的函数
C语言函数大全 本篇介绍C语言函数大全–d开头的函数 1. detectgraph 1.1 函数说明 函数声明函数功能void detectgraph(int *graphdriver, int *graphmode);通过检测硬件确定图形驱动程序和模式 1.2 演示示例 #include <graphics.h> #include <stdlib.h> #incl…...

基于springboot实现福聚苑社区团购演示【项目源码】
基于springboot实现福聚苑社区团购演示开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包&#…...

动静态库的制作
文章目录:什么是程序库?动态链接和静态链接动静态库的认识静态库的创建与使用创建使用动态库的创建与使用创建使用什么是程序库? 程序库:一般是软件作者为了发布方便、替换方便或二次开发目的,而发布的一组可以单独与应…...

QMS-云质-质量软件-客诉,为什么应该用两段式来处理
-云质QMS原创文章,转载请注明来源- 客户满意度是决定企业是否能够基业长青的关键因素之一。 如果客诉处理的不好,会极大影响客户的满意程度。 通常处理客诉分为两个阶段。 第一个阶段是快反遏制,想方设法快速答复和解决客户提出的问题&…...

JS:关于邮箱的正则表达式及规则
常用正则表达式—邮箱(Email) 要验证一个字符串是否为邮箱的话,首先要了解邮箱账号的格式。我尝试过在网上找出一个标准的格式,但是很遗憾我没有找到。我也尝试使用RFC标准来判断邮箱的格式,但是也没有结果。网上些博…...

两句话,ChatGPT帮我写一个打飞机的游戏
大家好,我是全村的希望今天的主题是让 chatGPT 来帮我们写一个打飞机的游戏记得我刚学 Python 的时候,看的那本很经典的入门书《Python 编程:从入门到实践》,里面就有小项目就是教你编写一个打飞机的游戏我那时候是对着书一个一个…...

计算机图形学14:三维图形的投影变换
作者:非妃是公主 专栏:《计算机图形学》 博客地址:https://blog.csdn.net/myf_666 个性签:顺境不惰,逆境不馁,以心制境,万事可成。——曾国藩 文章目录专栏推荐专栏系列文章序一、三维图形的投…...

【ChatGPT4】王老师零基础《NLP》(自然语言处理)第二课
我的已经在起、点开了书《王老师带我成为救世主》,那个更新及时 (1)---------------------------------------------------------------------------------------- 我: 1我/喜欢/吃/苹果,因为/它/们/很/好吃。 2 Th…...

设计模式之中介者模式在前端的应用
文章目录中介者模式在前端的应用场景1.实现组件之间的松耦合2. 实现异步请求的协同3. 实现事件驱动的编程模型4. 实现复杂交互的协调总结中介者模式在前端的应用场景 中介者模式是一种常见的设计模式,它可以将对象之间的通信集中处理,从而提高系统的可维…...

2023年还能入行程序员吗?工作3年以上的黑马老学员怎么说?
很多人觉得,毕业3年,不过是毕业第1年的重复,键盘Ctrl、C和V键磨损更严重了。妥妥属于光涨年龄,不涨经验;只涨体重,不涨工资…… 他们不理解,为什么同样的起跑线,有人发展神速&#…...

接收机的噪声来源与噪声分析
噪声分类 射频接收机中的噪声主要可以分为两类:内部噪声和外部噪声。 内部噪声 内部噪声主要来自于接收机内部的放大器、混频器、本振等元件所产生的噪声。根据不同的产生机制,内部噪声可以分为以下几类: a. 电感噪声:由于电感…...

Android FrameWork——SystemServer
Android系统在启动的时候有两个非常重要的进程,一个是Zygote,另一个就是system_server。SystemServer是系统用来启动service的入口,比如我们常用的AMS,WMS,PMS等等都是由它创建的。 system_server进程的启动 system_…...

婴儿推车ASTMF883测试
1.cpc认证是总称,cpc认证下边有很多的标准,常见的有ASTM F963(铅含量)、CPSIA(邻苯8P)、ASTM F833(婴儿车)等; 2.婴儿车ASTM认证是什么 2019年8月2日,美国消…...

射频接收机概述
接收机架构 射频接收机架构是指电子设备中用于接收无线电信号的部分。它通常由前置放大器、中频放大器、混频器、局部振荡器和带通滤波器等组成。以下是一个基本的射频接收机架构: 前置放大器:前置放大器的作用是放大接收天线接收到的微弱无线电信号&am…...