同名成员到底调用谁?C++ 隐藏规则你真的会吗?

同名成员到底调用谁?C++ 隐藏规则你真的会吗?

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

在这里插入图片描述

🌈say-fall:个人主页🚀专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》💪格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言:

对于c++来说,有三大核心特性,是面向对象编程(OOP)的经典三要素:封装、继承、多态。这三个特性是 C++ 区别于纯面向过程语言(如 C)的核心,也是理解 C++ 面向对象思想的关键。之前利用类和对象的思想和STL中的适配器:queue和stack了解过封装,本篇文章就详细介绍一下继承这个特性

文章目录


正文:

一、 什么是继承?

继承(inheritance) 机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类( 基类 )特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称 派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤( 模板 ),继承是类设计层次的复⽤。

下面我们设计了两个类:Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课。

classStudent{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 voididentity(){// ...}// 学习 voidstudy(){// ...}protected: string _name ="peter";// 姓名  string _address;// 地址  string _tel;// 电话 int _age =18;// 年龄 int _stuid;// 学号 };classTeacher{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 voididentity(){// ...}// 授课 voidteaching(){//...}protected: string _name ="张三";// 姓名 int _age =18;// 年龄  string _address;// 地址  string _tel;// 电话  string _title;// 职称 };intmain(){return0;}

显然,有大量的重复代码出现,我们将这些重复代码,或者说公共成员放入一个person类中,用继承的方法来处理,就不需要重复定义了

classPerson{public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 voididentity(){ cout <<"void identity()"<< _name << endl;}protected: string _name ="张三";// 姓名 int _age =18;// 年龄  string _address;// 地址  string _tel;// 电话 };classStudent:publicPerson{public:voidstudy(){ cout <<"void study()"<< endl;}protected:int _stuid;};classTeacher:publicPerson{public:voidtesching(){ cout <<"void tesching()"<< endl;}protected: string _title;};intmain(){ Student s; Teacher t; s.identity(); t.identity();return0;}

二、 继承的定义

定义的格式

下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)

在这里插入图片描述


其实子类和父类比较好理解,那么继承方式是什么呢?

在这里插入图片描述


在这里插入图片描述


继承方式如图,和访问限定符有点类似,都有三种,下面我们来看一下继承基类访问方式的变化:

在这里插入图片描述
  1. 看起来花里胡哨的,其实规则蛮简单的:
    public > protect > private 选其中小一点的
  2. 还有一些我们都知道的规则:使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
  3. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。

以上我们继承的Person是一个普通的类,继承不只能继承普通类,还能继承类模板

继承类模板

namespace say_fall {//template<class T>//class vector//{};// stack和vector的关系,既符合is-a,也符合has-a template<classT>classstack:public std::vector<T>{public:voidpush(const T& x){// 基类是类模板时,需要指定⼀下类域, // 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 vector<T>::push_back(x);//push_back(x);---->没有实例化,所以找不到}voidpop(){vector<T>::pop_back();}const T&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};}intmain(){ say_fall::stack<int> st; st.push(1); st.push(2); st.push(3);while(!st.empty()){ cout << st.top()<<" "; st.pop();}return0;}

注意到,这里用继承实现了一个stack类,而不是之前适配器的方法:用函数封装,这里就要解释一下,原来适配器实现用的是组合的方法,还可以用继承实现。

三、 基类和派生类之间的转换

继承就像是私有制极其严格的父子关系一样,有这么几条规则:

  1. public继承的派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
  2. 基类对象不能赋值给派⽣类对象。

基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)的dynamic_cast 来进⾏识别后进⾏安全转换。

在这里插入图片描述
  • 类比:父亲给孩子的财产父亲是有权使用的,而孩子不能直接使用父亲的财产,必须经过父亲允许才可以
classPerson{protected: string _name;// 姓名  string _sex;// 性别 int _age;// 年龄 };classStudent:publicPerson{public:int _No;// 学号 };intmain(){ Student sobj;// 1.派生类对象可以赋值给基类的指针/引用 Person* pp =&sobj; Person& rp = sobj;// 派生类对象可以赋值给基类的对象是通过调用基类的拷贝构造完成的  Person pobj = sobj;//2.基类对象不能赋值给派生类对象,这里会编译报错  sobj = pobj;//报错:没有与这些操作数匹配的 "=" 运算符return0;}

四、 继承的作用域

隐藏规则与重载比较

重载规则:

在同一作用域下的同名函数,在参数不同的情况下构成函数重载。

隐藏规则
  1. 在继承体系中基类和派⽣类都有 独⽴的作⽤域
  2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
    (在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆 classPerson{protected: string _name ="小李子";// 姓名 int _num =111;// ⾝份证号 };classStudent:publicPerson{public:voidPrint(){ cout <<" 姓名:"<< _name << endl; cout <<" ⾝份证号:"<< Person::_num << endl; cout <<" 学号:"<< _num << endl;}protected:int _num =999;// 学号 };intmain(){ Student s1; s1.Print();return0;};

相关选择题

classA{public:voidfun(){ cout <<"func()"<< endl;}};classB:publicA{public:voidfun(int i){ cout <<"func(int i)"<< i << endl;}};intmain(){ B b; b.fun(10); b.fun();return0;};
1. A和B类中的两个func构成什么关系()

A. 重载 B. 隐藏 C.没关系

  • 答案是 B ,回顾重载,其定义是一个作用域中的同名函数,而继承出来的子类和父类实际上是在两个作用域的,所以是隐藏
2. 上⾯程序的编译运⾏结果是什么()

A. 编译报错 B. 运⾏报错 C. 正常运⾏

  • 首先我们直到调用的func()是构成隐藏的,在调用b.fun();时候,却没有传入参数,很显然是编译错误。

五、 派生类默认成员函数

在这里插入图片描述


自己手动写一个类的过程中,我们了解过这6个默认成员函数,也就是即使我们自己不实现,编译器也会自己实现的,而继承的类中会自动生成吗?生成的规则又是什么样子的?

4个常⻅默认成员函数
在这里插入图片描述


这里说的四个默认构造函数是指构造、析构、赋值重载,拷贝构造:

  1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
  2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
  3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
  4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
  5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
  6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
  7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
classPerson{public:Person(constchar* 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;// 姓名 };classStudent:publicPerson{public:Student(constchar* 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);//operator=(s); 会触发递归,因为调用的是自己的operator=() _num = s._num;}return*this;}//赋值重载和拷贝构造是一样的~Student(){ cout <<"~Student()"<< endl;}//析构同样也和拷贝构造一样//有关于析构:析构子类时候会连带父类一起析构,所以父类不需要显式析构protected:int _num;//学号 };intmain(){ Student s1("jack",18); Student s2(s1); Student s3("rose",17); s1 = s3;return0;}

  • 本节完…

Read more

医疗送药机器人“空间拓扑优化+动态算法决策+多级容错控制”三重链式编程技术解析与应用

医疗送药机器人“空间拓扑优化+动态算法决策+多级容错控制”三重链式编程技术解析与应用

一、引言 1.1 研究背景与意义 在医疗体系中,高效精准的药品配送是保障医疗服务质量和患者安全的关键环节。随着医疗技术的不断进步和医疗需求的日益增长,传统的人工送药方式逐渐暴露出诸多弊端,如配送效率低下、易受人为因素干扰导致错误率上升、人力成本高昂等。特别是在大型综合医院,科室众多、布局复杂,药品配送路径长且需经过多个区域,这使得人工送药的难度和工作量大幅增加,进而影响医疗服务的及时性和准确性。 医疗送药机器人的出现为解决这些问题提供了新的途径。它能够在医院复杂的环境中自主导航,按照预设的路径和时间准确地将药品送达指定地点,极大地提高了药品配送的效率和准确性。通过自动化的配送流程,送药机器人可有效减少人为因素造成的错误,如拿错药、送错药等情况,从而保障患者的用药安全。同时,送药机器人的应用还能将药师和护士从繁琐的药品配送工作中解放出来,使其能够将更多的时间和精力投入到临床药学服务和患者护理工作中,提高医疗服务的整体质量。 “空间拓扑优化 + 动态算法决策 + 多级容错控制” 三重链式编程技术的提出,为医疗送药机器人性能的进一步提升带来了革命性的突破。空间拓扑优化技术能够对医院的

By Ne0inhk
【数据结构OJ】BFS算法的可视化:二叉树“层序遍历”

【数据结构OJ】BFS算法的可视化:二叉树“层序遍历”

今天我们来分享一道关于二叉树层序遍历的OJ算法题 目录 题目介绍: 1、核心定义 2、实现核心及思路 解题思路: 思路可视化: 代码实现: 代码测试: 题目介绍: 接口函数以及二叉树节点结构: /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right)

By Ne0inhk
【基础算法】算法的“预谋”:前缀和如何改变游戏规则

【基础算法】算法的“预谋”:前缀和如何改变游戏规则

🔭 个人主页:散峰而望 《C语言:从基础到进阶》《编程工具的下载和使用》《C语言刷题》《算法竞赛从入门到获奖》《人工智能》《AI Agent》 愿为出海月,不做归山云 🎬博主简介 【基础算法】算法的“预谋”:前缀和如何改变游戏规则 * 前言 * 前缀和 * 1.1 一维前缀和 * 1.1.1 前缀和 * 1.1.2 最大子段和 * 1.2 二维前缀和 * 1.2.1 二维前缀和 * 1.2.2 激光炸弹 * 结语 前言 在算法设计与优化中,前缀和是一种简单却强大的技巧,能够将复杂问题转化为高效计算。无论是处理一维数组的区间求和,还是解决二维矩阵的子矩阵问题,前缀和都能通过预处理将时间复杂度从线性降低到常数级别,彻底改变问题的解决方式。

By Ne0inhk
数据结构七大排序算法图解——选择排序动图演示

数据结构七大排序算法图解——选择排序动图演示

系列文章目录 四、选择排序 紧接上一篇交换排序 前言: 1、直接选择排序 思想: 例题: 代码部分: 性能分析 2、树形选择排序 思想: 例题一: 例题二: 性能分析 3、堆排序 定义: 方法: 如何“筛选”? 例题: 如何“建初始堆”? 例题: 代码部分 性能分析 4、总结 直接选择排序 树形排序 堆排序 前言: 选择排序的主要思想是每一趟从待排序列中选取一个关键字值最小的记录,也即第 1 趟从 n 个记录中选取关键字值最小的记录,在第 2 趟中,从剩下的 n-1 个记录中选取关键字值最小的记录,直到整个序列中的记录都选完位置。这样,由选取记录的顺序便可得到按关键字值有序的序列。

By Ne0inhk