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

Flutter 组件 simplify 的适配 鸿蒙Harmony 实战 - 驾驭路径精简算法、实现鸿蒙端高性能地理足迹渲染与矢量图形优化方案

Flutter 组件 simplify 的适配 鸿蒙Harmony 实战 - 驾驭路径精简算法、实现鸿蒙端高性能地理足迹渲染与矢量图形优化方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 simplify 的适配 鸿蒙Harmony 实战 - 驾驭路径精简算法、实现鸿蒙端高性能地理足迹渲染与矢量图形优化方案 前言 在鸿蒙(OpenHarmony)生态的运动健康轨迹展示、高精度室内导航以及大规模矢量地图看板开发中,“路径性能”是决定用户滑动流畅度的核心红线。面对用户运动 1 小时产生的包含数万个(X, Y)坐标点的原始 GPS 序列。如果直接将其交给鸿蒙端的渲染层进行绘制,不仅会引发由于顶点(Vertices)过多导致的 GPU 负载饱和。更会由于频繁的坐标点内存申请(Memory Allocation),产生严重的 UI 掉帧与功耗飙升。 我们需要一种“去重存精、视觉无损”的几何精简艺术。 simplify 是一套专注于极致性能的 Douglas-Peucker 及其增强算法实现。它能瞬间将冗余的、

By Ne0inhk
动态规划 线性 DP 五大经典模型:LIS、LCS、合唱队形、编辑距离 详解与模板

动态规划 线性 DP 五大经典模型:LIS、LCS、合唱队形、编辑距离 详解与模板

文章目录 * 最长上升子序列 * 【模板】最长上升子序列 * 合唱队形 * 牛可乐和最长公共子序列 * 编辑距离 经典线性 dp 问题有两个:最⻓上升⼦序列(简称:LIS)以及最⻓公共⼦序列(简称:LCS),这两道题⽬的很多⽅⾯都是可以作为经验,运⽤到别的题⽬中。⽐如:解题思路,定义状态表⽰的⽅式,推到状态转移⽅程的技巧等等。 因此,这两道经典问题是需要我们重点掌握的。 最长上升子序列 题目描述 题目解析 本题介绍最长上升子序列的一般解法,当数据量不大时用这种解法。 在此之前,小编先区分一下子数组和子序列,子数组需要是连续的,而子序列可以是间断的。 1、状态表示 dp[i]表示以i结尾的所有子序列中,最长的上升子序列。

By Ne0inhk

LeetCode 3379.转换数组:下标取模

【LetMeFly】3379.转换数组:下标取模 力扣题目链接:https://leetcode.cn/problems/transformed-array/ 给你一个整数数组 nums,它表示一个循环数组。请你遵循以下规则创建一个大小 相同 的新数组 result : 对于每个下标  i(其中 0 <= i < nums.length),独立执行以下操作: * 如果 nums[i] > 0:从下标 i 开始,向 右 移动 nums[i] 步,在循环数组中落脚的下标对应的值赋给 result[i]。 * 如果 nums[i] < 0:从下标

By Ne0inhk

EasyOCR用法全攻略:Python开源OCR工具快速上手,图文识别零门槛

在日常开发与办公场景中,图文识别(OCR)需求无处不在——比如提取图片中的文字、识别身份证/发票信息、批量处理扫描件等。传统OCR工具要么收费高昂,要么配置复杂,而 EasyOCR 作为Python开源OCR库,凭借“安装简单、支持多语言、识别精度高”的优势,成为入门级OCR开发的首选工具。 本文将从核心特性、环境搭建、基础用法到实战场景,全方位解析EasyOCR的使用技巧,帮你快速实现图文识别功能,无需深厚的计算机视觉知识。 一、为什么选择EasyOCR? 在众多OCR工具中,EasyOCR的核心优势的在于“轻量化+高性价比”,具体体现在: 1. 零门槛上手:API设计简洁,一行代码即可实现文字识别,无需复杂配置; 2. 多语言支持:默认支持80+种语言(中文、英文、日文、韩文等),可通过参数灵活切换; 3. 识别精度高:基于深度学习模型(CNN+

By Ne0inhk