[c++高阶] 继承深度剖析

news2025/2/3 8:02:20

1.前言

   继承 是 面向对象三大特性之一(封装、继承、多态),所有的面向对象(OO)语言都具备这三个基本特征,封装相关概念已经在《类和对象》系列中介绍过了,今天主要学习的是 继承,即如何在父类的基础之上,构建出各种功能更加丰富的子类。

例:

在继承父类动物的基础上,展现出了自己的特性--即子类。

本章重点

本章将着重的讲解继承的概念和定义,父类和子类对象的赋值转换,继承的作用域,继承中父类和子类的默认成员函数,继承与友元,继承与静态成员以及菱形继承,和虚继承的概念。

2.继承的概念和定义

2.1 什么是继承

        继承(inheritance)机制是 ----面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有基类(父类)特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类(子类) 。

看官方解释:

这里就延伸了几个概念:

  • 被继承对象:父类 / 基类 (base
  • 继承方:子类 / 派生类 (derived

继承的本质就是 ------------ 复用代码 --避免重复的代码写多次

举一个例子:

假设我现在要设计一个校园师生管理系统,那么肯定会设计很多角色类,比如学生、老师之类的。 

设计好以后,我们发现,有些数据和方法是每个角色都有的,而有些则是每个角色独有的。 

为了复用代码、提高开发效率,可以从各种角色中选出共同点,组成基类,比如每个人都有姓名、年龄、联系方式等基本信息,而教职工与学生的区别就在于管理与被管理,因此可以在 基类 的基础上加一些特殊信息如教职工号表示教职工,加上学号表示学生。

这样就可以通过 继承 的方式,复用 基类 的代码,划分出各种 子类 。

基类:

class Person {
public:
	Person(string name,int age = 18, string tel = "123456789", string address = "njust")
		:_age(age)
		,_name(name)
		,_tel(tel)
		,_address(address)
	{}
	void Print()
	{
		std::cout << "我的姓名是:" << _name << endl;
		std::cout << "年龄是:" << _age << endl;
		std::cout << "电话号码是:" << _tel << endl;
		std::cout << "地址是:" << _address << endl;
	}
private:
	string _name;
	int _age;
	string _tel;
	string _address;
};

子类:Student 和Ter

class Student : public Person
{
public:
	Student(int stuid=123)
		:_stuId(stuid)
		,Person("张三",20,"1233445")
	{
		std::cout << "我是一个学生,我的信息如下" << endl;
		std::cout << "我的学号是:" << _stuId << endl;
	}
private:
	int _stuId;
};
class Ter :public Person
{
public:
	Ter(int workid=012)
		:_workid(workid)
		,Person("李四",45,"123234354")
	{
		std::cout << "我是一个老师,我的信息如下" << endl;
		std::cout << "我的学号是:" << _workid << endl;
	}
private:
	int _workid;
};

继承后父类的 Person 成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student 和 Teacher 复用了 Person

2.2 继承的定义

了解了什么是继承,那么就要开始使用继承了。

定义格式:

格式如下: Person 是 父类,也称作基类Student 是 子类,也称作派生类。 

格式为 :子类 : 继承方式 父类,比如 class a : public b 就表示 a 继承了 b,并且还是 公有继承 

2.3 继承权限

继承有权限的概念,分别为:公有继承(public)、保护继承(protected)、私有继承(private 。

那么看到这里有小伙伴就比较疑惑了哈,这不是跟我们之前学过的访问限定符一致嘛。没错,就是一致,只不过在这里我们把他当成继承的权限。

根据排列组合,我们可以有9种搭配方案

外部不可见---言外之意就是子类的对象要想访问基类的相关成员,要受到访问限定符的限制。

总结:

无论是哪种继承方式,父类中的 private 成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员时,依据 min(父类成员权限, 子类继承权限),只有最终权限为 public 时,外部才能访问
在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

至于如何证明相关结论,这里给出一段代码,可自行验证

// 父类
class A
{
public:
	int _a;
protected:
	int _b;
private:
	int _c;
};
 
// 子类
class B : public A
{
public:
	B()
	{
		cout << _a << endl;
		cout << _b << endl;
		cout << _c << endl;
	}
};
 
int main()
{
	// 外部(子类对象)
	B b;      
	b._a;
}

3.基类和派生类的赋值转换

在继承中,允许将 子类 对象直接赋值给 父类,但不允许 父类 对象赋值给 子类 

有以下四个结论:

1.子类对象可以给父类对象赋值

2.子类对象可以给父类指针赋值

3.子类对象可以给父类引用赋值

4.父类不可以给子类赋值

那么子类是如何把值赋给父类的呢?

根据切片的形式,即简单的理解为把父类的相关成员函数切割出来,然后赋值给父类的成员。

4.继承的作用域

在继承体系中 基类 和 派生类 都有独立的作用域,如果子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问,这种情况叫 隐藏,也叫重定义。

函数重载是在同一个作用域下面,才构成函数重载。而隐藏即重定义就是说在两个不同的作用域定义了相同的成员变量。

看现象:

// 基类
class Person
{
protected:
	string _name = "Edison"; // 姓名
	int _num = 555; // 身份证号
};
 
// 派生类
class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "学号:" << _num << endl;
	}
protected:
	int _num = 888; // 学号
};
 
int main()
{
	Student s1;
	s1.Print();
 
	return 0;
}

s1里面打印_num是会优先打印派生类的,如果想打印出基类的_num,那么就需要用 基类 :: 基类成员显示的去访问

结论:

1.在继承中,基类和派生类是拥有不同的作用域

2.如果基类和派生类中有相同的定义,那么如果想访问基类中的可以使用基类::基类成员的方式

3.在继承中,函数构成隐藏只需要函数名相同即可。

4.最好不要在基类和派生类中出现对同一变量的定义

5.子类中的默认成员函数

派生类(子类)也是 ,同样会生成 六个默认成员函数(用户未定义的情况下)

不同于单一的 子类 是在 父类 的基础之上创建的,因此它在进行相关操作时,需要为 父类 进行考虑

直接给出结论:

1.派生类的构造函数被调用时,会自动调用基类的构造函数初始化基类的那一部分成员,如果基类当中没有默认的构造函数,则必须在派生类构造函数的初始化列表当中显示调用基类的构造函数。
2.派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝构造。
3.派生类的赋值运算符重载函数必须显示调用基类的赋值运算符重载函数完成基类成员的赋值。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
5.派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。
6.派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。

这里解释一下:为什么在析构的时候,要析构子类,在析构父类

  • 和前面的默认成员函数不同,在实现派生类的析构时,基类的析构不能显式调用
  • 这是因为,如果显示调用了基类的析构,就会导致基类成员的资源先被清理,如果此时派生类成员还访问了基类成员指向的资源,就会导致野指针问题
  • 因此,必须保证析构顺序为先子后父,保证数据访问的安全

6.继承与友元

友元关系不能继承,也就是说 基类友元不能访问子类私有和保护成员,只能访问自己的私有和保护成员。 

下面代码中,Display 函数是基类 Person 的友元,但是 Display 函数不是派生类 Student 的友元,也就是说 Display 函数无法访。

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; // 无法访问
}
 
int main()
{
	Person p;
	Student s;
	Display(p, s);
 
	return 0;
}

7.继承与静态成员

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

8.菱形继承

8.1 单继承

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

8.2 多继承

 一个子类有两个或以上直接父类时称这个继承关系为多继承

8.3 菱形继承

 C++ 支持多继承,即支持一个子类继承多个父类,使其基础信息更为丰富,但凡事都有双面性多继承 在带来巨大便捷性的同时,也带来了个巨大的坑:菱形继承问题

会出现如下情况:

此时会有一个问题,类D的实例化对象中有类B和类C,然而B类和C类都有A类,所以说D类对象中的A类成员就重复了!

具体示例如下:

class A
{
public:
	int _a = 1;
};

class B :public A
{
public:
	int _b = 2;
};

class C :public A
{
public:
	int _c = 3;
};

class D :public B, public C
{
public:
	int _d = 4;
};

int main()
{
	D dem;
	return 0;
}

现象如下:

在一个dem里面出现了两份_a,这就导致了数据冗余和二义性的问题。

修改代码如下:

int main()
{
	D dem;
	cout << dem._a << endl;
	return 0;
}

这是因为有两个_a,当你没有明确指定使用哪一个时,编译器也无法确定。

解决办法:通过::限制访问域即可

修改代码如下:

int main()
{
	D dem;
	cout << dem.A::_a << endl;
	return 0;
}

就不会报错了。能够精准的知道是访问那个_a。但是这只是解决了二义性的问题,没有解决数据冗余的问题,即一个对象里面存放了两个不同的_a

那么真正解决这个问题的是:虚继承。

ps: 虚继承是专门用来解决 菱形继承 问题的,与多态中的虚函数没有直接关系

虚继承:在菱形继承的腰部继承父类时,加上 virtual 关键字修饰被继承的父类 

修改代码如下

class A
{
public:
	int _a = 1;
};

class B :virtual public A
{
public:
	int _b = 2;
};

class C :virtual public A
{
public:
	int _c = 3;
};

class D :public B, public C
{
public:
	int _d = 4;
};

int main()
{
	D dem;
	cout << dem._a << endl;
	return 0;
}

这是打印出的结果就是1.

那么虚继承是如何解决这个问题的呢?

  • 利用 虚基表 将冗余的数据存储起来,此时冗余的数据合并为一份
  • 原来存储 冗余数据 的位置,现在用来存储 虚基表指针

此时无论这个 冗余 的数据存储在何处,都能通过 基地址 + 偏移量 的方式进行访问 

看现象:

虚基表里面存放的都是一些偏移量,当访问这个公共的数据时,先访问续集表指针,再通过续集表指针里面的地址来找到该值。

9.总结

继承就说到这里了,继承在cpp中是非常关键的一个知识点,在后续的多态学习中非常重要,也是面试里面常被问到的一个知识点,希望各位阅读文章的小伙伴能够打好基础。

如有兴趣的小伙伴还想了解一下is-a和has-a的关系的话,可以阅读下面文章

【C++】继承最全解析(什么是继承?继承有什么用?)_类的继承有什么用-CSDN博客

本篇文章的撰写也参考了上述文章的相关知识与结构。

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

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

相关文章

RTX4060+ubuntu22.04+cuda11.8.0+cuDNN8.6.0 如何根据显卡型号和系统配置cuda和cuDNN所需的安装环境

文章目录 &#x1f315;电脑原配置&#x1f315;安装cuda和cuDNN前的环境选择&#x1f319;cuDNN与CUDA tookit和nvidia driver的对应关系&#x1f319;cuda版本选择⭐查看自己的nvidia driver版本和最大支持的CUDA版本⭐最小支持版本 &#x1f319;查看11.8.0版本的cuda和ubun…

前端学习——CSS——李白代表作品页面(3)

上传资源的地方&#xff1a;http://download.csdn.net/ 项目要求部分&#xff08;1&#xff09; &#xff1a; 支撑知识点&#xff1a; 1.CSS附加方式——外部样式表&#xff1a; --->链接式外部样式表 语法&#xff1a; 在head标签里边写link单标签&#xff0c;其中再…

认知杂谈99《打工人 警惕 画饼》

内容摘要&#xff1a; 领导心理游戏是指领导者利用甜言蜜语和虚假承诺来操控员工情感&#xff0c;使员工产生依赖和盲目信任的行为。他们常以美好未来的描绘来吸引员工&#xff0c;但这些承诺往往难以实现。 员工之所以容易陷入这种心理游戏&#xff0c;是因为他们渴望得到情感…

Golang | Leetcode Golang题解之第455题分发饼干

题目&#xff1a; 题解&#xff1a; func findContentChildren(g []int, s []int) (ans int) {sort.Ints(g)sort.Ints(s)m, n : len(g), len(s)for i, j : 0, 0; i < m && j < n; i {for j < n && g[i] > s[j] {j}if j < n {ansj}}return }

进阶岛第4关:InternVL 多模态模型部署微调实践

准备InternVL模型 我们使用InternVL2-2B模型。该模型已在share文件夹下挂载好&#xff0c;现在让我们把移动出来。 mkdir -p /root/project/joke/modelcp -r /root/share/new_models/OpenGVLab/InternVL2-2B /root/project/joke/model # 不用ln -s 准备环境 这里我们来手动配…

k8s 中微服务之 MetailLB 搭配 ingress-nginx 实现七层负载

目录 1 MetailLB 搭建 1.1 MetalLB 的作用和原理 1.2 MetalLB功能 1.3 部署 MetalLB 1.3.1 创建deployment控制器和创建一个服务 1.3.2 下载MealLB清单文件 1.3.3 使用 docker 对镜像进行拉取 1.3.4 将镜像上传至私人仓库 1.3.5 将官方仓库地址修改为本地私人地址 1.3.6 运行清…

【24最新亲试】ubuntu下载go最新版本

系列综述&#xff1a; &#x1f49e;目的&#xff1a;本系列是个人整理为了工具配置的&#xff0c;整理期间苛求每个知识点&#xff0c;平衡理解简易度与深入程度。 &#x1f970;来源&#xff1a;材料主要源于Ubuntu 升级 golang 版本完美步骤进行的&#xff0c;每个知识点的修…

算法笔记(十一)——优先级队列(堆)

文章目录 最后一块石头的重量数据流中的第 K 大元素前K个高频单词数据流的中位数 优先级队列是一种特殊的队列&#xff0c;元素按照优先级从高到低&#xff08;或从低到高&#xff09;排列&#xff0c;高优先级的元素先出队&#xff0c;可以用 堆来实现 堆是一种二叉树的结构&…

Python 语言学习——应用1.2 数字图像处理(第二节,变换)

目录 1.基础知识 1.图像几何变换概念 2.图像几何变换方式 3.插值运算 4.几何变换步骤 2.各类变换 1.位置变换 2.形状变换 3.代数运算 3.实战演练 1.基础知识 1.图像几何变换概念 在图像处理过程中&#xff0c;为了观测需要&#xff0c;常常需要对 图像进行几何变换&am…

EXCEL_光标百分比

Public Sub InitCells()Dim iSheet As LongFor iSheet Sheets.Count To 1 Step -1Sheets(iSheet).ActivateActiveWindow.Zoom 85ActiveWindow.ScrollRow 1ActiveWindow.ScrollColumn 1Sheets(iSheet).Range("A1").ActivateNext iSheetEnd Sub对日项目中的文档满天…

Python | Leetcode Python题解之第459题重复的子字符串

题目&#xff1a; 题解&#xff1a; class Solution:def repeatedSubstringPattern(self, s: str) -> bool:def kmp(query: str, pattern: str) -> bool:n, m len(query), len(pattern)fail [-1] * mfor i in range(1, m):j fail[i - 1]while j ! -1 and pattern[j …

深度学习中的结构化概率模型 - 结构化概率模型的深度学习方法篇

序言 在深度学习的广阔领域中&#xff0c;结构化概率模型&#xff08; Structured Probabilistic Model \text{Structured Probabilistic Model} Structured Probabilistic Model&#xff09;扮演着至关重要的角色。这类模型利用图论中的图结构来表示概率分布中随机变量之间的…

普渡PUDU MT1:AI赋能,破解大面积场景清洁新挑战

普渡AI智能扫地机器人PUDU MT1:破解大面积场景清洁难题的新利器 在仓储物流、工业车间、交通枢纽、大型商场等大面积场景中,清洁难题一直是管理者们头疼的问题。这些区域面积广阔,清洁任务繁重,传统清洁方式难以胜任。然而,普渡机器人最新推出的AI智能扫地机器人PUDU MT1…

【目标检测】工程机械车辆数据集2690张4类VOC+YOLO格式

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2694 标注数量(xml文件个数)&#xff1a;2694 标注数量(txt文件个数)&#xff1a;2694 标注…

付费计量系统数据元素(Data elements)

See also Clause 4 for a discussion on general concepts.Data elements are the instruments used to keep record of information on the status of the system and changes over time. 参见条目 4 关于一般概念的讨论。数据元素是仪器使用保留关于系统状态和随时间…

【Kubernetes】常见面试题汇总(四十五)

目录 102.使用 Kubernetes 时可以采取的最佳安全措施是什么&#xff1f; 103.什么是联合集群&#xff1f; 特别说明&#xff1a; 题目 1-68 属于【Kubernetes】的常规概念题&#xff0c;即 “ 汇总&#xff08;一&#xff09;~&#xff08;二十二&#xff09;” 。 题目…

1688商品详情关键词数据-API

要利用 Python 爬虫采集 1688 商品详情数据&#xff0c;需要先了解 1688 网站的页面结构和数据请求方式。一般使用 requests 库请求网站的数据&#xff0c;使用 BeautifulSoup 库解析网页中的数据。 以下是一个简单的 Python 爬虫采集 1688 商品详情数据的示例代码&#xff1a…

二、变量与基本类型

变量与基本类型 变量定义声明和使用 基本类型数字类型介绍运算算术运算符位运算符赋值运算符运算符优先级 布尔类型字符类型字符串类型 变量 定义 变量&#xff0c;指值可以变的量。变量以非数字的符号来表达&#xff0c;一般用拉丁字母。变量的用处在于能一般化描述指令的方式…

Java_Se 泛型

泛型基本概念&#xff1a; 泛型是JDK5.0以后增加的新特性。泛型的本质就是“数据类型的参数化”&#xff0c;处理的数据类型不是固定的&#xff0c;而是可以作为参数传入。我们可以把“泛型”理解为数据类型的一个占位符(类似&#xff1a;形式参数)&#xff0c;即告诉编译器&am…

如何搭建自己的域名邮箱服务器?Poste.io邮箱服务器搭建教程,Linux+Docker搭建邮件服务器的教程

Linux系统Docker搭建Poste.io电子邮件服务器&#xff0c;搭建属于自己的域名邮箱服务器&#xff0c;可以无限收发电子邮件&#xff08;Email&#xff09;&#xff01; 视频教程&#xff1a;https://www.bilibili.com/video/BV11p1mYaEpM/ 前言 什么是域名邮箱&#xff1f; …