<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.rhkb.cn/news/202611.html

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

相关文章

什么是高防IP?有什么优势?怎么选择高防IP?

在当今的互联网环境中&#xff0c;分布式拒绝服务&#xff08;DDoS&#xff09;攻击已经成为一种常见的安全威胁。这种攻击通过向目标服务器发送大量的无效流量&#xff0c;使其无法处理正常的请求&#xff0c;从而达到迫使服务中断的目的。作为一个用户&#xff0c;你是否曾遇…

【Web】PhpBypassTrick相关例题wp

目录 ①[NSSCTF 2022 Spring Recruit]babyphp ②[鹤城杯 2021]Middle magic ③[WUSTCTF 2020]朴实无华 ④[SWPUCTF 2022 新生赛]funny_php 明天中期考&#xff0c;先整理些小知识点冷静一下 ①[NSSCTF 2022 Spring Recruit]babyphp payload: a[]1&b1[]1&b2[]2&…

Go 语言中的 Switch 语句详解

switch语句 使用switch语句来选择要执行的多个代码块中的一个。 在Go中的switch语句类似于C、C、Java、JavaScript和PHP中的switch语句。不同之处在于它只执行匹配的case&#xff0c;因此不需要使用break语句。 单一case的switch语法 switch 表达式 { case x:// 代码块 cas…

【深度学习】如何选择神经网络的超参数

1. 神经网络的超参数分类 神经网路中的超参数主要包括: 1. 学习率 η 2. 正则化参数 λ 3. 神经网络的层数 L 4. 每一个隐层中神经元的个数 j 5. 学习的回合数Epoch 6. 小批量数据 minibatch 的大小 7. 输出神经元的编码方式 8. 代价函数的选择 9. 权重初始化的方法 …

2023.11.24 海豚调度,postgres库使用

目录 海豚调度架构dolphinscheduler DAG(Directed Acyclic Graph)&#xff0c; 个人自用启动服务 DS的架构(海豚调度) 海豚调度架构dolphinscheduler 注:需要先开启zookeeper服务,才能进行以下操作 通过UI进行工作流的配置操作, 配置完成后, 将其提交执行, 此时执行请求会被…

【数据分享】我国12.5米分辨率的DEM地形数据(免费获取/地理坐标系)

DEM地形数据是我们在各种研究和设计中经常使用的数据&#xff01;之前我们分享过500米分辨率的DEM地形数据、90米分辨率的DEM地形数据、30米分辨率的DEM地形数据&#xff08;均可查看之前的文章获悉详情&#xff09;。 本次我们为大家带来的是分辨率为12.5m的DEM地形数据&#…

【反射】简述反射的构造方法,成员变量成员方法

&#x1f38a;专栏【JavaSE】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【如愿】 &#x1f970;欢迎并且感谢大家指出我的问题 文章目录 &#x1f384;什么是反射&#x1f384;获取class对象的三种方式⭐代码实现 &#x1f3…

【黑马甄选离线数仓day04_维度域开发】

1. 维度主题表数据导出 1.1 PostgreSQL介绍 PostgreSQL 是一个功能强大的开源对象关系数据库系统&#xff0c;它使用和扩展了 SQL 语言&#xff0c;并结合了许多安全存储和扩展最复杂数据工作负载的功能。 官方网址&#xff1a;PostgreSQL: The worlds most advanced open s…

Java Web——XML

1. XML概述 XML是EXtensible Markup Language的缩写&#xff0c;翻译过来就是可扩展标记语言。XML是一种用于存储和传输数据的语言&#xff0c;它使用标签来标记数据&#xff0c;以便于计算机处理和我们人来阅读。 “可扩展”三个字表明XML可以根据需要进行扩展和定制。这意味…

【理解ARM架构】不同方式点灯 | ARM架构简介 | 常见汇编指令 | C与汇编

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《理解ARM架构》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 目录 &#x1f3c0;直接操作寄存器点亮LED灯&#x1f3c0;地址空间&#x1f3c0;ARM内部的寄存…

Godot

前言 为什么要研究开源引擎 主要原因有&#xff1a; 可以享受“信创”政策的红利&#xff0c;非常有利于承接政府项目。中美脱钩背景下&#xff0c;国家提出了“信创”政策。这个政策的核心就是&#xff0c;核心技术上自主可控。涉及的产业包括&#xff1a;芯片、操作系统、数据…

Tiktok小店如何入驻?注册流程与资料全解

作为国内成功的出海App之一&#xff0c;Tiktok的特色就是社交平台兴趣电商&#xff0c;已然成为当前跨境电商的一大趋势。数据显示&#xff0c;目前Tiktok全球月活跃用户已接近16亿&#xff0c;正是红海一片。非常值得跨境电商玩家入局&#xff01;今天就来给大家整理一份tk小店…

uniapp IOS从打包到上架流程(详细简单) 原创

​ 1.登入苹果开发者网站&#xff0c;打开App Store Connect ​ 2.新App的创建 点击我的App可以进入App管理界面&#xff0c;在右上角点击➕新建App 即可创建新的App&#xff0c;如下图&#xff1a; ​ 3.app基本信息填写 新建完App后&#xff0c;需要填写App的基本信息&…

uniapp开发的微信小程序进行代码质量控制,分包+压缩js+组件按需注入等

小程序代码分包的操作请看另外一篇文章&#xff1a;uniapp分包优化&#xff0c;包括分包路由跳转规则-CSDN博客 JS文件压缩&#xff1a;在工具「详情」-「本地设置」中开启「上传代码时自动压缩脚本文件」的设置 代码包&#xff1a;组件 > 启用组件按需注入解决办法 在小程…

黑马React18: Redux

黑马React: Redux Date: November 19, 2023 Sum: Redux基础、Redux工具、调试、美团案例 Redux介绍 Redux 是React最常用的集中状态管理工具&#xff0c;类似于Vue中的Pinia&#xff08;Vuex&#xff09;&#xff0c;可以独立于框架运行 作用&#xff1a;通过集中管理的方式管…

【MySQL】宝塔面板结合内网穿透实现公网远程访问

文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar3.2 创建HTTP隧道4.远程连接5.固定TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单几步,通过宝塔面板cpo…

想问问各位大佬,网络安全这个专业普通人学习会有前景吗?

网络安全是一个非常广泛的领域&#xff0c;涉及到许多不同的岗位。这些岗位包括安全服务、安全运维、渗透测试、web安全、安全开发和安全售前等。每个岗位都有自己的要求和特点&#xff0c;您可以根据自己的兴趣和能力来选择最适合您的岗位。 渗透测试/Web安全工程师主要负责模…

STM32:基本定时器原理和定时程序

一、初识定时器TIM 定时器就是计数器&#xff0c;定时器的作用就是设置一个时间&#xff0c;然后时间到后就会通过中断等方式通知STM32执行某些程序。定时器除了可以实现普通的定时功能&#xff0c;还可以实现捕获脉冲宽度&#xff0c;计算PWM占空比&#xff0c;输出PWM波形&am…

Android开发从0开始(服务)

Android后台运行的解决方案&#xff0c;不需要交互&#xff0c;长期运行。 服务基础框架&#xff1a; public class MyService extends Service { public MyService() { } Override public IBinder onBind(Intent intent) { //activity与service交互&#xff08;需要继…

解决:javax.websocket.server.ServerContainer not available 报错问题

原因&#xff1a; 用于扫描带有 ServerEndpoint 的注解成为 websocket&#xff0c;该方法是 服务器端点出口&#xff0c;当进行 SpringBoot 单元测试时&#xff0c;并没有启动服务器&#xff0c;所以当加载到这个bean时会报错。 解决方法&#xff1a; 加上这个注解内容 Spr…