<C++> 继承

目录

前言

一、继承概念

1. 继承概念

2. 继承定义格式

3. 继承关系和访问限定符 

4. 继承基类成员访问方式的变化

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

三、继承中的作用域

四、派生类的默认成员函数

五、继承与友元

六、继承与静态成员

七、菱形继承及菱形虚拟继承

1. 菱形继承

2. 虚继承

总结

前言

        在代码编写中,如果一段代码重复多次 被调用,那么我们会将其封装为一个函数,提高代码复用性,例如交换函数swap;同样的,对于类的成员函数或成员变量,如果在多个类中重复出现,那么我们可以提取公共数据,封装为一个基类,使其它类来继承基类。 


一、继承概念

1. 继承概念

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

2. 继承定义格式

例: 

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};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;
}

3. 继承关系和访问限定符 

4. 继承基类成员访问方式的变化

类成员 / 继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

 不可见:在语法上限制访问,类里面和类外面都不能使用 (父类的私有成员不管什么继承都不可以使用) 。它跟private不同,private在类外不能使用,类里面可以使用。

子类继承父类的成员变量和成员函数,但是因为成员函数不在类内部,这似乎也叫不了继承

总结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected可以看出保护成员限定符是因继承才出现的
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)public > protected > private
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过最好显示的写出继承方式
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

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

        我们知道,不同类型的变量直接进行赋值会发生类型转换,类型转换有强制类型转换和隐式类型转换。

        同理,父类和子类之间是不是也可以进行相互转换呢?

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
protected:int _stuid; // 学号
};class Teacher : public Person
{
protected:int _jobid; // 工号
};int main()
{int i = 0;double d = i;Person p;Student s;p = s;//s = p;  父不能给子,因为子有的变量可能多于父,变量数量都不一致,不能赋值//语法方面禁止了父向子传递
}

        父类不能类型转换赋值给子类(称为向下转换),因为子类有的变量可能多于父类,变量数量不一致,不能完成赋值。如果显示的强制类型转换也不可以,在这里C++语法方面直接禁止了父向子的传递 ,只允许子

        对于内置类型,类型转换时会产生临时变量而对于父类与子类之间,它们的类型转换不产生临时变量,这种类型转换被称为赋值兼容(切片,切割),因为子一定含有父的特征,将子类中父类的那一部分切下拷贝赋值给父类变量即可

问:如何证明不产生中间变量?

        用引用!如果有临时变量,那么需要使用const修饰的引用

	int i = 0;double& d = i;    错误Student s;Person& p = s;    正确

        此时父类p是子类s中父类那一部分切片的别名

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)dynamic_cast 来进行识别后进行安全转换。
	Student sobj;// 1.子类对象可以赋值给父类对象/指针/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//2.基类对象不能赋值给派生类对象sobj = pobj;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;Student * ps1 = (Student*)pp; // 这种情况转换时可以的。ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_No = 10;

三、继承中的作用域

  • 在继承体系中基类派生类都有独立的作用域
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义(在子类成员函数中,可以使用 基类::基类成员 显示访问
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中继承体系里最好不要定义同名的成员
class Person
{
public:void fun(){cout << "Person::func()" << endl;}protected:string _name = "小李子"; // 姓名int _num = 111; 	   // 身份证号
};// 隐藏/重定义:子类和父类有同名成员,子类的成员隐藏了父类的成员
class Student : public Person
{
public:void fun(){cout << "Student::func()" << endl;}void Print(){cout << " 姓名:" << _name << endl;cout << _num << endl;如果要使用父类变量,就指定类域cout << Person::_num << endl;}
protected:int _num = 999; // 学号
};

        若在函数内输出变量,编译器优先在函数内寻找、其次是类成员变量、如果有继承就在父类成员找、最后是全局

        重载要在同一个作用域,底层使用了函数名修饰规则,不然找地址的时候区分不开函数

隐藏是在父子类域中,只要函数名相同就形成隐藏

四、派生类的默认成员函数

class Person
{
public://Person(const char* name = "peter")Person(const char* name): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;delete _pstr;}
protected:string _name; // 姓名};class Student : public Person
{
public:// 先父后子Student(const char* name = "张三", int id = 0):_name(name)    报错,_id(0){}protected:int _id;
};
//输出
Person()
~Person()

语法规定:

  • 派生类不能在初始化列表初始化从基类继承的成员变量(初始化列表初始化顺序和编写顺序无关,只和成员变量声明顺序有关,由于继承的变量在子类成员变量之前,所以先初始化继承的变量)
  • 派生类会在初始化列表自动调用基类的默认构造函数,如果基类没有默认构造,那么就会报错,我们可以显示调用基类的构造函数解决问题,编写语规则就像定义了匿名对象 
	Student(const char* name = "张三", int id = 0):Person(name)    //最好写前面,_id(0){}
  • 对于派生类的拷贝构造,如果不写父类的拷贝构造,会默认调用父类的默认构造,如果没有默认构造就会报错,所以需要显示的调用
  • 析构函数特殊,由于多态的原因,析构函数的函数名被特殊处理了,统一处理成destructor,所以派生类的析构隐藏了基类的析构,所以在调用父类的析构时还需要加上类名::
  • 由于子类实例化对象时,先调用父类的构造函数,再调用子类的构造,那么在析构时,要先析构子类,再析构父类,因为子类可能会用到父类。 显示调用父类析构,无法保证先子后父,所以子类析构函数完成后,自动调用父类析构,这样就保证了析构先子后父
例如此情况,先析构父再析构子就发生错误了,因为_pstr是父类的~Student(){Person::~Person();cout << *_pstr << endl;delete _ptr;}
class Person
{
public://Person(const char* name = "peter")Person(const char* name): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;delete _pstr;}
protected:string _name; // 姓名string* _pstr = new string("111111111");
};class Student : public Person
{
public:// 先父后子Student(const char* name = "张三", int id = 0):Person(name),_id(0){}Student(const Student& s):Person(s)    这里传子类s是可以的,上转型为p,_id(s._id){}// 10:45继续Student& operator=(const Student& s){if (this != &s){这里如果写为operator=会发生隐藏,造成死循环Person::operator=(s);    _id = s._id;}return *this;}~Student(){//Person::~Person();cout << *_pstr << endl;delete _ptr;}
protected:int _id;int* _ptr = new int;
};

总结:

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函

数,则必须在派生类构造函数的初始化列表阶段显示调用。

2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

3. 派生类的operator=必须要调用基类的operator=完成基类的复制。

4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类

对象先清理派生类成员再清理基类成员的顺序。

5. 派生类对象初始化先调用基类构造再调派生类构造。

6. 派生类对象析构清理先调用派生类析构再调基类的析构

五、继承与友元

        友元关系不能继承,即父类的友元不能被子类继承

        如果也想使用父类声明的友元,那么再子类也声明以此即可

六、继承与静态成员

        基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例

        静态成员属于父类和派生类,在派生类中不会单独拷贝一份,派生类继承的是使用权


class Person
{
public:Person() 
{}
//protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};int main()
{Person p;Student s;cout << &p._name << endl;cout << &s._name << endl;cout << &p._count<< endl;cout << &s._count << endl;cout << &Person::_count << endl;cout << &Student::_count << endl;return 0;
}

举例:求父类和子类总共实例化多少对象

        子类构造函数默认生成,在默认生成的构造函数中又默认调用父类默认构造,所以不需要写子类的构造函数

class Person
{
public:Person() { ++_count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};
int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;cout << " 人数 :" << Person::_count << endl;Student::_count = 0;cout << " 人数 :" << Person::_count << endl;
}

七、菱形继承及菱形虚拟继承

1. 菱形继承

        多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,用 ',' 分割

         有多继承就会出现菱形继承,菱形继承是多继承的一种特殊情况(不规则也属于菱形继承,只要有公共的父类,三角形、五边形等等)

        继承的变量所在空间地址是相邻的

         那么菱形继承就会引起一些问题,即数据冗余,例如Student类继承了Person的_name,而Teacher也继承了Person的_name,最终Assistant继承了两类的_name,这不仅会造成数据冗余,还会造成二义性(可以指定类域访问,但是数据冗余问题无法解决)

class Person
{
public:string _name; // 姓名int _age;
};class Student : public Person
{
protected:int _num; //学号
};class Teacher : public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};int main()
{Assistant as;as.Student::_age = 18;as.Teacher::_age = 30;as._age = 19;	错误,因为二义性无法明确知道访问的是哪一个return 0;
}

2. 虚继承

        C++3.0对于菱形继承的二义性,提出了虚继承的解决方案

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 d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

        使用虚拟继承:

class A
{
public:int _a;
};class B : virtual public A
{
public:int _b;
};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类的D类内部增加一个变量空间用来专门存储_a,原本的B、C类存_a的空间改为存储一个指针信息,该指针指向一个表,称为虚基表,表的内容是单个或多个偏移量,是存放指针空间地址与D内部新增的_a空间地址之间的便宜量。

        虚基表可以减小D类对象所占的内存空间,且可以存储多个偏移量信息。

问:有的同学可能认为直接在存指针的地方存偏移量不就好了吗?

答:其实这是格局小了,如果需要存两个偏移量,那么B、C类每一处都要写两个偏移量,而如果我们将偏移量写进表内,那么当D实例化多个对象,我们只需要使每个对象的指针指向的虚基表地址相同,因为类相同,那么偏移量也是相同的,所以可以公用虚基表,这就高效的利用了空间

问:为什么要有偏移量,不能直接到D类内存最后一块直接访问吗?

答:是为了统一上转型对象以及本类对象访问_a的方式,这就是都存偏移量的意义

        首先,B、C类在虚继承之后内存结构也会发生改变,内存结构与D类一致,即首地址存虚基表地址,在B类内存最后存放_a

        这种情况是为了保障上转型对象能够访问_a的情况

B类指针
B* ptr = &b;
ptr->_a++;上转型指针
ptr = &d;
ptr->_a++;

        在这种情况编译器区分不了ptr是什么类的指针,编译器做的是根据首地址处存储的地址找到偏移量,再根据当前位置的地址加上偏移量去找_a

汇编指令:

根据当前位置加上偏移量,取出值进行++,再放回去

总结

        面向对象三大特性之一的继承内容基本不难,依赖类和对象阶段基本知识(如六大默认构造函数),下节我们学习面向对象三大特性的最后一个——多态。

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

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

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

相关文章

万字解析设计模式之模板方法与解释器模式

一、模板方法模式 1.1概述 定义一个操作中算法的框架&#xff0c;而将一些步骤延迟到子类中&#xff0c;模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 例如&#xff0c;去银行办理业务一般要经过以下4个流程&#xff1a;取号、排队、办理具体业…

HTTP/2:多路复用、服务器推送和首部压缩的革命

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

系统优化软件Bitsum Process Lasso Pro v12.4,供大家学习研究参考

1、自动或手动调整进程优先级;将不需要抑制的进程添加到排除列表; 2、设置动态提升前台运行的进程/线程的优先级 3、设置进程黑名单,禁止无用进程(机制为启动即结束,而非拦截其启动)。 4、优化I/O优先级以及电源模式自动化。 5、ProBalance功能。翻译成中文是“进程平衡…

【从删库到跑路】MySQL数据库 — E-R图 | 关系模型

&#x1f38a;专栏【MySQL】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【如愿】 大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 文章目录 &#x1f339;简述什么是E-R图⭐核心概念 &#x1f339;E-R图…

Windows power shell for循环

有时候需要重复执行某个shell命令 for($i1;$i -lt 10;$i$i1){echo $i}如果是cmd for /l %i in (1,1,5) do echo %i

typora中的快捷键shift enter 和 enter的交换

1 问题&#xff1a; 我最近在用 typora 进行写作&#xff0c;但是在合格 typora 的 markdown 编辑器很奇怪&#xff0c;它的一个回车符是两次换行&#xff0c;而用 shfit ent 找了半天都不知道怎么解决的这个问题&#xff0c;然后我就去了这个 typora 在 github 开源的问题仓库…

vitis AIE graph

https://zhuanlan.zhihu.com/p/661255763 在前文中,我们首先认识了 Vitis™ 2022.1 统一软件平台内适用于 Versal™ 的 AI 引擎 (AIE) 应用。 我们认识了 Vitis IDE 2022.1 中 AIE 应用工程的结构,还了解了用于计算图初始化、运行和终止的一些 API。在本文中,我们将进一步深…

忘记跟进客户?CRM系统来帮您

销售人员要同时跟进多个客户&#xff0c;经常容易忘记跟进客户&#xff0c;导致客户流失。这就代表您的企业需要使用CRM系统了。下面我们就来对这个问题聊聊&#xff0c;销售总忘记跟进客户&#xff1f;CRM客户管理了解一下。 CRM如何帮助销售跟进转化客户&#xff1a; 管理客…

【华为数通HCIP | 网络工程师】821-IGP高频题、易错题之OSPF(6)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

0基础能不能转行做网络安全?网络安全人才发展路线

最近有同学在后台留言&#xff0c;0基础怎么学网络安全&#xff1f;0基础可以转行做网络安全吗&#xff1f;以前也碰到过类似的问题&#xff0c;想了想&#xff0c;今天简单写一下。 我的回答是先了解&#xff0c;再入行。 具体怎么做呢&#xff1f; 首先&#xff0c;你要确…

单链表的反转?太细了哥们!细到离谱!

单链表的反转&#xff08;面试常出&#xff09;&#xff1a; ​ 单链表的反转&#xff0c;可以通过很多种方法实现。包括迭代法&#xff0c;递归法&#xff0c; 迭代法&#xff1a; 定义三个指针&#xff1a;prev、current和next&#xff0c;它们分别表示前一个节点、当前节点…

【开源】基于Vue.js的城市桥梁道路管理系统的设计和实现

项目编号&#xff1a; S 025 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S025&#xff0c;文末获取源码。} 项目编号&#xff1a;S025&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询城市桥…