同名成员到底调用谁?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

无人机和地面站能够达到的多远的通信距离?无人机需要飞多高?附在线计算网页

无人机和地面站能够达到的多远的通信距离?无人机需要飞多高?附在线计算网页

无人机和地面站能够达到的多远的通信距离?无人机需要飞多高?附在线计算网页 在无人机组网通信中,如何估算无人机与地面站之间的稳定通信距离是一个常见的问题。本文将从地球曲率和菲涅尔区两个方面,详细探讨如何计算无人机与地面站的通信距离,并提供一个在线计算网页以方便读者进行实际计算。 经常有朋友会问到这个问题, * 无人机组网通信中,如果已经知道了无人机的飞行高度、地面站天线的高度,那么无人机和地面站稳定通信距离是多少km? * 无人机组网通信中,如果已经知道了地面站天线高度、期望的稳定通信距离,无人机需要飞多高才能满足期望的通信距离? 解答这个问题,需要从以下两个方面来考虑: 1. 无线通信距离受到地球曲率的影响 2. 无线通信距离受到空间传输通道的影响。 注意:本文不讨论由于发射端EIRP不够,链路余量不足引起的通信距离不足的问题,所有的计算和分析都是假设发射端EIRP足够,链线余量足够,仅仅考虑地球曲率和空间传输通道的影响。 地球曲率(无线信号传输的视距模型):解决能否看见的问题 无人机与地面电台之间的最远通讯距离受地球曲率限制,通常采用考虑大气折射的无线电视距

By Ne0inhk

一文吃透SBUS协议:从原理到实战(无人机/航模/机器人适用)

在无人机、航模、机器人等精密控制领域,“稳定、快速、可靠”是控制信号传输的核心诉求。传统的PWM信号虽然简单直观,但存在通道数有限、抗干扰能力弱、布线复杂等痛点。而SBUS(Serial Bus)协议——由FUTABA公司专为遥控设备设计的串行数字通信协议,凭借单线传输多通道数据、抗干扰强、延迟低的核心优势,逐渐成为行业主流。 本文将从“是什么-怎么工作-协议细节-厂家产品-接口设计-代码实现-实战技巧-常见问题”八个维度,用最通俗的语言+大量对比表格,全面拆解SBUS协议。无论你是刚入门的电子爱好者,还是需要落地项目的工程师,都能从本文中找到所需的实用信息。 一、SBUS协议基础认知:核心定位与优势对比 在深入技术细节前,我们先通过对比和基础定义,快速建立对SBUS的认知。很多人会把SBUS和常见的UART、PWM等混淆,这里先明确其核心定位:SBUS是基于反向电平UART的“应用层控制协议”,专门用于遥控器与接收机、接收机与飞控/执行器之间的控制信号传输。 1.1 为什么需要SBUS?传统方案的痛点 在SBUS出现之前,航模和早期无人机主要使用PWM或PPM协议传输控

By Ne0inhk
深入解析OpenClaw Skills:从原理到实战,打造专属机器人技能

深入解析OpenClaw Skills:从原理到实战,打造专属机器人技能

一、OpenClaw Skills:机器人行为的“最小执行单元” 1.1 什么是OpenClaw Skills? OpenClaw是面向开源机械爪/小型机器人的控制框架(核心仓库:openclaw/openclaw),旨在降低机器人行为开发的门槛。而Skills(技能) 是OpenClaw框架中对机器人“单一可执行行为”的封装模块——它将机器人完成某一特定动作的逻辑(如“夹取物体”“释放物体”“移动到指定坐标”)抽象为独立、可复用、可组合的代码单元。 简单来说: * 粒度:一个Skill对应一个“原子行为”(如“单指闭合”)或“组合行为”(如“夹取→移动→释放”); * 特性:跨硬件兼容(适配不同型号机械爪)、可插拔(直接集成到OpenClaw主框架)、可扩展(支持自定义参数); * 核心价值:避免重复开发,让开发者聚焦“

By Ne0inhk

Flutter 三方库 angular_bloc 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致响应、工业级的 AngularDart 与 BLoC 协同架构实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 angular_bloc 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致响应、工业级的 AngularDart 与 BLoC 协同架构实战 在鸿蒙(OpenHarmony)系统的桌面级协同(如分布式办公网页版)、后台管理终端或高度复杂的 Web 仪表盘开发中,如何将经典的 BLoC 状态管理应用于 AngularDart 环境?angular_bloc 为开发者提供了一套天衣无缝的组件化连接器。本文将实战演示其在鸿蒙 Web 生态中的深度应用。 前言 什么是 Angular BLoC?它是一套专门为 AngularDart 框架设计的 BLoC 实现。通过指令(Directives)和管道(Pipes),它实现了由于数据流变化触发的 UI

By Ne0inhk