C++进阶之继承

news2024/11/28 14:41:28

文章目录

  • 前言
  • 一、继承的概念及定义
    • 1.继承概念
    • 2.继承格式与访问限定符
    • 3.继承基类与派生类的访问关系变化
    • 4.总结
  • 二、基类和派生类对象赋值转换
    • 基本概念与规则
  • 三、继承中的作用域
  • 四、派生类的默认成员函数
  • 五、继承与友元
  • 六、继承与静态成员
  • 六、复杂的菱形继承及菱形虚拟继承
  • 七、虚拟继承解决数据冗余和二义性的原理


在这里插入图片描述

前言

下面我们进入C++的重要部分,继承,面对对象的三大特性,封装,继承,多态,继承为其一大特性,可知继承的重要性

一、继承的概念及定义

1.继承概念

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

如以下代码,Student类和Teacher类从Person继承了name和age,从而可以使用name和age,同时Print和setage这两个函数也被继承了,所以在main里面可以使用这两个函数,从这里可以看出继承可以减轻程序员的重复书写,使得代码复用,减轻程序员的负担

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
	void setage()
	{
		_age = 20;
	}
private:
	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.setage();
	s.Print();
	t.Print();
	return 0;
}


在这里插入图片描述

2.继承格式与访问限定符

Person是父类,也称作基类。Student是子类,也称作派生类
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.继承基类与派生类的访问关系变化

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

列如上面的代码如果你在Person里没有提供Print和setage函数,那么在派生类Student和Teacher里age和name是不能被访问的,基类的private成员只能用基类提供的方法访问

4.总结

  • 基类private成员在派生类中无论以什么方式继承都是不可见的(但能通过基类提供的public方法访问)。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它

  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
    派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的、

  • 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
    成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected>private

  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
    在这里插入图片描述

  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
    使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
    面使用,实际中扩展维护性不强

这里放上可以通过改变继承方式来认识不同的访问限定符的访问关系的变化的代码

// 实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public :
void Print ()
{
	cout<<_name <<endl;
}
protected :
	string _name ; // 姓名
private :
	int _age ; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
	int _stunum ; // 学号
};

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

赋值转换是为继承专门设计的,是继承里非常重要的概念

基本概念与规则

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
    或者切割。寓意把派生类中父类那部分切来赋值过去
  • 基类对象不能赋值给派生类对象
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
    的指针是指向派生类对象时才是安全的。
class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};
void Test()
{
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;
	
	//sobj = pobj;//2.基类对象不能赋值给派生类对象
	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj;
	Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;
	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
	
	ps2->_No = 10;
}

三、继承中的作用域

  • 在继承体系中基类和派生类都有独立的作用域(作用域和访问限定符没有关系,访问限定符是对外而言的)
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
    也叫重定义
    。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏跟返回值和形参没有关系)。
  • 注意在实际中在继承体系里面最好不要定义同名的成员
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();
};

在这里插入图片描述

// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
	cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
	A::fun();
	cout << "func(int i)->" <<i<<endl;
}
};
void Test()
{
B b;
b.fun(10);
};

在这里插入图片描述

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

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
    的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派生类的operator=必须要调用基类的operator=完成基类的复制
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
    保证派生类对象先清理派生类成员再清理基类成员的顺序
  • 派生类对象初始化先调用基类构造再调派生类构造。
  • 派生类对象析构清理先调用派生类析构再调基类的析构
  • 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
    解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
    virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

在这里插入图片描述
在这里插入图片描述

具体的派生类和基类的默认函数的写法看以下代码

class Person
{
public:
	Person(const char* name = "peter")
		: _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;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		: Person(s)//会切片,所以能用派生类赋给基类
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);//会切片,所以能用派生类赋给基类
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};
void Test()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
}

int  main()
{
	Test();
	return 0;
}

五、继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员(父亲的朋友不是你的朋友)

class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

在这里插入图片描述

六、继承与静态成员

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

所以可以通过以下代码计算派生出了多少子类

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;
}

在这里插入图片描述

六、复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

在这里插入图片描述

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
在这里插入图片描述
菱形继承:菱形继承是多继承的一种特殊情况
在这里插入图片描述

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。
在这里插入图片描述

二义性可以通过作用域运算符来避免

class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	//a._name = "peter";未指明是Student还是Teacher的_name
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

在这里插入图片描述

虽然解决了二义性,但代码冗余未解决。所以cpp提出了一个虚继承同时解决二义性和代码冗余

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在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呢,看每个对象的第一个4字节有一个地址,这个地址所指向的位置是一个偏移量(我重新运行了,所一地址可能不一样,但偏移量是一样的,建议自己实验)
    在这里插入图片描述
    在这里插入图片描述

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

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

相关文章

图论试题2020

n-m 2 16 Pk(Kn)k(k-1)…(k-n1)。 C&#xff1a;A2对角线元素aii2等于对应顶点vi的度数&#xff0c;所以对角线元素之和等于边数的两倍。 A的所有特征值的平方和等于A2的对角线元素之和。 B 完全图没有顶点隔&#xff0c;实际上也只有以完全图为生成子图的图没有顶点隔。 连通…

Qt6 C++基础入门1 定时器与QTimer

定时器 定时器图片流水灯案例 实现效果&#xff1a;构建一个界面&#xff0c;点击开始按钮轮流播放文件夹下图片&#xff0c;点击停止按钮停止播放 构建页面&#xff0c;上部是一个没有内容的 label 下面是开始和暂停按钮&#xff0c;各自的名称分别为 startBtn 和 stopBtn 先保…

6.事件绑定

目录 1 事件对象的属性 2 事件绑定方式 3 在事件中赋值 4 事件传参 1 事件对象的属性 target是触发该事件源头的组件&#xff0c;currentTarget是当前事件所绑定的组件&#xff0c;比如现在有一个父组件包着子组件&#xff0c;你给父组件绑定事件&#xff0c;由于事件…

ps磨皮插件专用智能磨皮插件Portraiture4

Portraiture是一款智能磨皮插件&#xff0c;为Photoshop和Lightroom添加一键磨皮美化功能&#xff0c;快速对照片中皮肤、头发、眉毛等部位进行美化&#xff0c;无需手动调整&#xff0c;大大提高P图效率。全新4版本&#xff0c;升级AI算法&#xff0c;并独家支持多人及全身模式…

从0到1深入剖析微服务架构,阿里人十年经验浓缩成一份笔记

前言 数字化经济的快速发展和云计算给底层IT系统带来的巨大变革正是当下微服务架构快速发展的时代背景。Gartner预计&#xff0c;从2018年到2022年&#xff0c;PaaS将成为未来的主流平台交付模式&#xff0c;而PaaS平台需要更加灵活的云原生应用架构做技术支撑&#xff0c;微服…

图论与算法(3)图的深度优先遍历

1. 遍历的意义 1.1 图的遍历 图的遍历是指按照一定规则访问图中的所有顶点&#xff0c;以便获取图的信息或执行特定操作。常见的图遍历算法包括深度优先搜索&#xff08;DFS&#xff09;和广度优先搜索&#xff08;BFS&#xff09;。 深度优先搜索&#xff08;DFS&#xff0…

UART串口通信实验

不管是单片机开发还是嵌入式 Linux 开发&#xff0c;串口都是最常用到的外设。 可以通过串口将开发板与电脑相连&#xff0c;然后在电脑上通过串口调试助手来调试程序。 还有很多模块&#xff0c;比如蓝牙、GPS、GPRS等都使用串口与主控进行通信。 UART简介 串口全称串行接口…

vb6 Webview2微软Edge Chromium内核执行JS取网页数据测速

微软Edge Chromium内核执行JS获取网页数据测试 ExcuteScript eval(document.body.innerHTML) from : https://www.163.com 采集的网页HTM字符串占用字节空间1.2MB ExcuteScript回调事件中取得JS执行结果&#xff0c;用时 54 毫秒 其中JSON转字符13.5209毫秒 jSON数据长度: 增…

ChatGPT更新说明(20230524)

原文传送门&#xff1a;ChatGPT — Release Notes 更新说明&#xff08;5月24日&#xff09; 简要&#xff1a;iOS应用在更多国家可用&#xff0c;Alpha测试中的共享链接&#xff0c;Bing插件&#xff0c;iOS上的历史记录禁用 ChatGPT iOS应用在更多国家可用 好消息&#xf…

Elasticsearch:如何使用集群级别的分片分配过滤(不包括节点)安全地停用节点

当你想停用 Elasticsearch 中的节点时&#xff0c;通常的过程不是直接销毁节点。 如果你这样做&#xff0c;那么你就有数据丢失的风险&#xff0c;这不是你想要对应该是可靠的数据库做的事情。 这样做的问题是&#xff0c;节点很可能会通过 Elasticsearch 处理的恰当命名的分片…

Character.AI成为新晋AI聊天应用爆款;谷歌推出 Google Slides AI 图像生成

&#x1f989; AI新闻 &#x1f680; Character.AI&#xff1a;首周下载量超越ChatGPT&#xff0c;成为新晋AI聊天应用爆款 摘要&#xff1a;Character.AI是一款受欢迎的人工智能聊天应用&#xff0c;用户可以自由创建AI角色&#xff0c;并与它们聊天。该应用于2023年5月23日…

C#,码海拾贝(32)——计算“实对称三对角阵的全部特征值与特征向量的”之C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 矩阵类 /// 作者&#xff1a;周长发 /// 改进&#xff1a;深度混淆 /// https://blog.csdn.net/beijinghorn /// </summary> public partial class Matrix {…

【Mysql】 表的约束

文章目录 【Mysql】 表的约束空属性默认值列描述zerofill主键自增长唯一键外键综合案例 【Mysql】 表的约束 上一个博客记录的是mysql中的类型&#xff0c;这篇博客记录的是mysql中的表的约束&#xff1b;即列字段对插入数据的约束 空属性 俩个值&#xff1a; null (默认) 和…

Vue3 + ElementPlus实战学习——模拟简单的联系人列表管理后台

文章目录 &#x1f4cb;前言&#x1f3af;demo 介绍&#x1f3af;功能分析&#x1f9e9;数据的展示与分页功能&#x1f9e9;编辑功能&#x1f9e9;删除功能 &#x1f3af;部分代码分析&#x1f3af;完整代码&#x1f4dd;最后 &#x1f4cb;前言 这篇文章介绍一下基于 Vue3 和…

DataSpell第一次安装使用教程

官网&#xff1a; Download DataSpell: The IDE for Data Scientists (jetbrains.com) 双击.exe文件开始安装 安装过程就一直点击下一步就好&#xff0c;遇到方框需要勾选的全部勾上。 注意尽量别安装在C盘&#xff0c;我安装在了D盘。 获取jihuoma&#xff1a;(484条消息)…

MySql学习1:安装

前言 学习教程&#xff1a;黑马程序员 MySQL数据库入门到精通&#xff0c;从mysql安装到mysql高级、mysql优化全囊括 目前的打算是跟着教程学习基础部分&#xff0c;进阶和运维部分以后可能会学习。 安装 关于如何安装mysql可以跟着视频里的操作&#xff0c;但是对于我这种…

盘点一个AI你画我猜的小工具

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 寻声暗问弹者谁&#xff0c;琵琶声停欲语迟。 大家好&#xff0c;我是Python进阶者。 一、前言 前几天在【ChatGPT&AI破局俱乐部】知识星球发现了一…

【Python】Python系列教程-- Python3 字典(十四)

文章目录 前言创建空字典访问字典里的值修改字典删除字典元素字典键的特性字典内置函数&方法 前言 往期回顾&#xff1a; Python系列教程–Python3介绍&#xff08;一&#xff09;Python系列教程–Python3 环境搭建&#xff08;二&#xff09;Python系列教程–Python3 VS…

【Java】Java(四十七):单元测试

文章目录 1. 概述2. 特点3. 使用步骤4. 相关注解5. 疑惑: 有了main函数 为啥还要 单元测试6. 后记 1. 概述 JUnit是一个 Java 编程语言的单元测试工具。JUnit 是一个非常重要的测试工具 2. 特点 JUnit是一个开放源代码的测试工具。提供注解来识别测试方法。JUnit测试可以让…

在k8s平台部署个人博客(三)

先下载实战-在k8s平台部署个人博客-资源包 再K8s部署个人博客 实验步骤如下&#xff1a; [rootk8s-master]# kubectl create secret generic mysql-pass --from-literalpasswordYOUR_PASSWORD #把mysql.tar.gz和wordpress.tar.gz上传到K8s工作节点&#xff0c;手动解压即可…