10_3、C++继承与派生:派生类成员访问

news2024/10/6 20:40:36

派生类成员访问

  • 作用域分辨符
  • 虚基类
    • 概念及用法
    • 虚基类派生类的构造函数
  • 赋值兼容规则

作用域分辨符

如果派生类中存在和基类中完全相同的函数,将发生同名覆盖。如果在派生类中存在一个和基类某数据成员同名的数据成员,或者和基类某成员函数的名称和参数表都相同的成员函数,则派生类中的新成员就覆盖了基类成员,不管是在派生类内还是在派生类外部只能通过成员名访问到派生类的成员,而访问不到基类成员。
如果需要在派生类中访问基类中的同名成员怎么办呢?
可以通过基类名和作用域分辨符来访问基类中的同名成员。作用域分辨符就是“::”。

  • 在派生类内部访问基类同名成员的语法形式是:
基类名::数据成员名;           // 数据成员
基类名::函数成员名(参数表);   // 函数成员
  • 在派生类外通过派生类对象访问的话,前面还要加上“派生类对象名.”:
派生类对象名.基类名::数据成员名;                  // 数据成员
派生类对象名.基类名::函数成员名(参数表);   // 函数成员

这里的基类名就限定了后面的成员属于哪个类。

如果多个基类具有同名成员,派生类也新增了同名成员,则派生类成员会覆盖所有基类中的同名成员。通过成员名只能访问到派生类的成员,要访问各基类的同名成员就需要使用作用域分辨符。而如果派生类中不存在同名成员,访问多个基类的同名成员也需要使用作用域分辨符,因为从不同基类继承过来的同名成员具有相同的作用域,通过成员名无法唯一标识成员,所以也需要作用域分辨符来分辨。

  1. 多继承情况下同名覆盖的例子
#include <iostream>
using namespace std;
class Base1                // 基类Base1的声明
{
public:
     int x;
     void show()        { cout<<"x of Base1: "<<x<<endl; } 
};
class Base2                // 基类Base2的声明
{
public:
     int x;
     void show()        { cout<<"x of Base2: "<<x<<endl; } 
};
class Child : public Base1, public Base2    // 派生类Child的声明
{
public:
    int x;
    void show()        { cout<<"x of Child: "<<x<<endl; } 
};
int main()
{
     Child child;
     child.x = 5;          // 访问派生类数据成员
     child.show();         // 调用派生类函数成员
     child.Base1::x = 7;   // 使用作用域分辨符访问基类Base1的数据成员
     child.Base1::show();  // 使用作用域分辨符调用基类Base1的函数成员
     child.Base2::x = 8;   // 使用作用域分辨符访问基类Base2的数据成员
     child.Base2::show();  // 使用作用域分辨符访问基类Base2的函数成员
     return 0;
}

在这里插入图片描述

  • 主函数main中声明了派生类Child的对象child,因为同名覆盖,所以通过成员名只能访问派生类Child的成员,要访问基类Base1和Base2的同名成员就需要像上面那样使用作用域分辨符访问。如果在派生类Child的成员函数show中访问基类Base1的同名成员,比如x,则可以将Child的show函数修改为:void show() { cout<<"x of Child: "<<Base1::x<<endl; }。
  1. 多继承情况下无同名覆盖的例子
    将上例中派生类Child的新增同名成员去掉,改为:
class Child : public Base1, public Base2
{
};

程序其余部分不变,则主函数main中的语句child.x = 5;和child.show();就会编译报错,因为这两个标识符具有二义性,系统无法唯一标识它们,不知道该访问哪个成员。只能通过作用域分辨符来访问。

  1. 如果派生类的全部或者部分基类有共同的基类,也就是说派生类的这些基类是从同一个基类派生出的,那么派生类的这些直接基类从上一级基类继承的成员都具有相同的名称,即都是同名成员,要访问它们就必须通过直接基类限定,使用作用域分辨符访问。
#include <iostream>
using namespace std;
class Base0                    // 基类Base0的声明
{
public:
     int x;
     void show()      { cout<<"x of Base0: "<<x<<endl; }
};
class Base1 : public Base0     // 由Base0派生的类Base1的声明
{
};
class Base2 : public Base0     // 由Base0派生的类Base2的声明
{
};
class Child : public Base1, public Base2
{
};
int main()
{
      Child child;
      child.Base1::x = 3;      // 通过直接基类Base1限定成员
      child.Base1::show();
      child.Base2::x = 5;      // 通过直接基类Base2限定成员
      child.Base2::show();
      return 0;
}

在这里插入图片描述
我们先声明一个基类Base0,Base0中有数据成员x和函数成员show,再声明类Base1和Base2,它们都由Base0公有继承而来,最后从Base1和Base2共同派生出类Child。这时Base0的成员经过到Base1和Base2再到Child的两次派生过程,出现在Child类中时,实际上Base0的数据成员x已经是两个不同的成员,只是名称相同但是在内存中是两份,函数成员show也是两个不同的成员,只是名称相同但是函数体可能不同。这就需要使用作用域分辨符访问了,但是不能用基类Base0来限定,因为这样还是不能说明成员是从Base1还是Base2继承而来,所以必须使用直接基类Base1或者Base2来限定,达到唯一标识成员的目的。
上面的主函数main中定义了派生类Child的对象child,如果只通过成员名访问成员x和show,系统就不能确定访问哪个x和哪个show,这就需要使用直接基类Base1或者Base2和作用域分辨符来访问它们。
数据成员x在内存中有两份拷贝,可以存放不同的数值,但是一般我们只需要一个这样的拷贝,那多出来的那个就是对内存的浪费。解决这个问题就需要后面讲的虚基类技术。

虚基类

概念及用法

  • 场景:如果派生类的全部或者部分基类有共同的基类,那么派生类的这些直接基类从上一级基类继承的成员都具有相同的名称,定义了派生类的对象后,同名数据成员就会在内存中有多份拷贝,同名函数也会有多个映射。
  • 解决方案:访问这些同名成员时,为了唯一标识它们可以使用作用域分辨符,也可以使用虚基类技术

虚基类:将派生类直接基类的共同基类声明为虚基类后,派生类从不同的直接基类继承来的同名数据成员在内存中就会只有一份拷贝,同名函数也会只有一个映射,这样不仅实现了唯一标识同名成员,而且也节省了内存空间。(赋值时只能赋一个值,若不用虚基类则占用多个内存,可以赋多个值)
虚基类声明的语法形式为:

class 派生类名:virtual 继承方式 基类名
  • 这里关键字virtual跟继承方式一样,只限定紧跟在它后面的基类。比如,声明了类A为虚基类,类B为A的派生类,类C也是A的派生类,类D是由类B和C共同继承而来,则类B和类C从A继承的同名数据成员在类D的对象中只有一份拷贝,同名函数成员也只有一个函数体。(声明了那个基类是虚基类,那么和其有关的派生类对象调用该基类中的数据成员和函数成员时只会在内存中拷贝一份,不管有多少重继承关系和什么继承途径)
#include <iostream>
using namespace std;
class Base0                    // 基类Base0的声明
{
public:
     int x;
     void show()      { cout<<"x of Base0: "<<x<<endl; }
};
class Base1 : virtual public Base0     // Base0为虚基类,公有派生Base1类
{
};
class Base2 : virtual public Base0     // Base0为虚基类,公有派生Base2类
{
};
class Child : public Base1, public Base2
{
};
int main()
{
    Child child;
    child.x = 5;
    child.show();
    return 0;
}

在这里插入图片描述
声明虚基类只需要在它的派生类声明时使用关键字virtual修饰。

我们对作用域分辨符和虚基类技术进行对比分析可知,使用作用域分辨符唯一标识同名成员时,派生类中有同名成员的多个拷贝,可以存放不同的数据,进行不同的操作,而使用虚基类时派生类的同名成员只有一份拷贝,更节省内存。我们在软件开发中可以根据实际情况自己做出选择。

虚基类派生类的构造函数

如果虚基类定义了带参数表的非默认构造函数,没有定义默认形式的构造函数,那么情况会有些复杂。因为由虚基类直接或间接继承的所有派生类,都必须在构造函数的成员初始化列表中给出对虚基类成员的初始化。

#include <iostream>
using namespace std;
class Base0                    // 基类Base0的声明
{
public:
      Base0(int y)     { x=y; }
      int x;
      void show()      { cout<<"x of Base0: "<<x<<endl; }
};
class Base1 : virtual public Base0     // Base0为虚基类,公有派生Base1类
{
public:
      Base1(int y):Base0(y)    { }
};
class Base2 : virtual public Base0     // Base0为虚基类,公有派生Base2类
{
public:
      Base2(int y):Base0(y)    { }  
};
class Child : public Base1, public Base2
{
public:
      Child(int y):Base0(y),Base1(y),Base2(y)   { }
};
int main()
{
    Child child(3);
    child.show();
    return 0;
}

在这里插入图片描述

  • 如果不是虚基类,那么Base0这个基类的构造函数将被调用3次,这里采用虚基类,编译器在遇到这种情况时会进行特殊处理:如果构造的对象中有从虚基类继承来的成员,那么虚基类成员的初始化由而且只由最远派生类的构造函数调用虚基类的构造函数来完成。
  • 最远派生类就是声明对象时指定的类,上面例子中构造对象child时,类Child就是最远派生类。除了最远派生类,它的其他基类对虚基类构造函数的调用会被忽略。上例中就只会由Child类的构造函数调用虚基类Base0的构造函数完成成员x的初始化,而Child类的基类Base1和Base2对虚基类Base0构造函数的调用会被忽略。

赋值兼容规则

派生类如果是从基类公有继承的,则它会包含基类中除构造函数和析构函数外的所有成员,基类的公有成员也成为派生类的公有成员,又因为对象只能访问类的公有成员,所以基类对象具有的功能,派生类对象都有。这样就引出了赋值兼容规则
***赋值兼容规则就是指在基类对象可以使用的地方都可以用公有派生类对象来代替。***注意必须是公有派生类。
赋值兼容规则中的代替有三种方式:

  1. 派生类的对象可以赋值给基类的对象。
  2. 派生类对象的地址可以赋值给基类类型的指针。
  3. 派生类对象可以用来初始化基类的引用。
class Base
{
       ...
};
class Child : public Base
{
    ...
};
Base base, *pBase;
Child child;

base = child;     //赋值
pBase = &child;  //指针
Base &b = child;  //引用

作用:
因为有了赋值兼容规则,有了上述三种赋值方式,所以函数的参数中有基类对象或者基类指针又或者基类引用时,我们可以直接传入派生类对象或者派生类对象的地址作为实参来执行相同的操作。这样的好处是什么呢?那就是我们想对基类及派生类的对象做相同的操作时,只要定义一个函数就行了,它的参数为基类对象或者基类指针也或者是基类引用。这样就大大提高了软件开发的效率。
公有派生类对象可以代替基类对象使用,但是我们只能使用它从基类继承的成员,而无法使用它的新添成员。

#include <iostream>
using namespace std;
class Base           // 基类Base的声明
{
public:
     void show()    { cout << "Base::show()" << endl; }      // 公有成员函数show
};
class Child0 : public Base     // 类Base的公有派生类Child0的声明
{
public:
     void show()    { cout << "Child0::show()" << endl; }    // 公有成员函数show
};
class Child1 : public Child0   // 类Child0的公有派生类Child1的声明
{
public:
     void show()    { cout << "Child1::show()" << endl; }    // 公有成员函数show
};
void CallShow(Base *pBase)     // 一般函数,参数为基类指针
{
     pBase->show();
}
int main()
{
     Base base;                 // 声明Base类的对象
     Base *pBase;             // 声明Base类的指针
     Child0 ch0;                 // 声明Child0类的对象
     Child1 ch1;                 // 声明Child1类的对象
     pBase = &base;        // 将Base类对象base的地址赋值给Base类指针pBase
     CallShow(pBase);
     pBase = &ch0;            // 将Child0类对象ch0的地址赋值给Base类指针pBase
     CallShow(pBase);
     pBase = &ch1;            // 将Child1类对象ch1的地址赋值给Base类指针pBase
     CallShow(pBase);
     return 0;
}

在这里插入图片描述

  • 针对基类中函数的操作,可以直接从公有派生类的对象进行赋值来操作。不用再对派生类单独创建操作函数
  • 我们首先定义了一个函数CallShow,其参数pBase为基类Base类型的指针,根据赋值兼容规则,我们可以用公有派生类对象的地址为基类指针赋值,那么CallShow函数就可以处理这个类族的所有对象。在主函数中我们就分别把基类对象base的地址、派生类对象ch0的地址和派生类对象ch1的地址赋值给基类指针pBase,然后将pBase作为实参调用CallShow,在CallShow中调用了成员函数show。
  • 将派生类对象的地址赋值给pBase以后,通过pBase只能访问派生类从基类继承的成员。所以即使指针pBase指向的是派生类对象ch0或者ch1,在CallShow中通过pBase也只能调用从基类Base继承的成员函数show,而不会调用Child0类或者Child1类的成员函数show。因此主函数中三次调用CallShow函数,都是访问的基类Base的成员函数show,输出都是Base::show()。

即使派生类对象代替了基类对象,它也只能产生基类的功能,自己的新功能无法体现。要想在代替以后同样能够实现自己的功能,就要用到面向对象设计的另一个特性–多态性。

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

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

相关文章

采用PHP语言(医院安全不良事件上报系统源码)医院不良事件 各类事件分析、分类、处理流程

医疗安全不容忽视&#xff01; 医疗安全&#xff08;不良&#xff09;事件是指在临床诊疗活动中以及医院运行过程中&#xff0c;任何可能影响患者的诊疗结果、增加患者的痛苦和负担并可能引发医疗纠纷或医疗事故&#xff0c;以及影响医疗工作的正常运行和医务人员人身安全的因…

我给KTV服务生讲解防抖,他竟然听懂了

端午节三天假期&#xff0c;的最后一天&#xff0c;我和朋友闲来无事&#xff0c;想着去唱会儿歌吧&#xff0c;好久不唱了&#xff0c;于是吃了午饭&#xff0c;石景山就近找了一家KTV&#xff0c;我们团好了卷就过去了。 装修还算不错&#xff0c;很快找到服务生&#xff0c…

【创建SpringBoot项目常见问题】保姆级教程(踩过的坑)

文章目录 特别提醒无效目标发行版 18类文件具有错误的版本 61.0, 应为 52.0Spring 项目运行,控制台乱码Spring 配置文件乱码引入插件&#xff0c;idea找不到 在创建第一个SpringBoot项目时&#xff0c;我出现了很多的配置错误&#xff0c;接下来与大家分享一下解决方法。希望我…

讯方技术与华为终端签署鸿蒙合作协议,将为企业助培百万鸿蒙人才

1月18日&#xff0c;鸿蒙生态千帆启航仪式在深圳举行&#xff0c;华为宣布HarmonyOS NEXT鸿蒙星河版开发者预览面向开发者开放申请&#xff0c;这意味着鸿蒙生态进入第二阶段&#xff0c;将加速千行百业的应用鸿蒙化。讯方技术总裁刘国锋、副总经理刘铭皓应邀出席启航仪式&…

基于esp8266_点灯blinker_智能家居

文章目录 一 实现思路1 项目简介2 项目构成3 代码实现4 外壳部分 二 效果展示UI图片 一 实现思路 摘要&#xff1a;esp8266&#xff0c;mixly&#xff0c;点灯blinker&#xff0c;物联网&#xff0c;智能家居&#xff0c;3donecut 1 项目简介 1 项目效果 通过手机blinker app…

17- Redis 中的 quicklist 数据结构

在 Redis 3.0 之前&#xff0c;List 对象的底层数据结构是双向链表或者压缩列表&#xff0c;然后在 Redis 3.2 的时候&#xff0c;List 对象的底层改由 quicklist 数据结构实现。 其实 quicklist 就是【双向链表 压缩列表】组合&#xff0c;因为一个 quicklist 就是一个链表&…

解锁 DevOps 精通:成功的综合指南

在动态的软件开发领域&#xff0c;要掌握 DevOps&#xff0c;需要对其核心原则有细致的了解&#xff0c;并采取战略性实施方法。DevOps 是一种协作方法&#xff0c;它将软件开发 (Dev) 和 IT 运营 (Ops) 结合起来&#xff0c;以自动化和简化软件交付流程。它旨在缩短开发周期、…

双模蓝牙芯片TD5165A功能介绍—拓达半导体

拓达芯片TD5165A是一颗支持U盘&TF卡的双模蓝牙芯片&#xff0c;此颗芯片的亮点在于同时支持音频蓝牙与BLE数传&#xff0c;芯片在支持蓝牙无损音乐播放的同时&#xff0c;还支持 APP和小程序&#xff0c;通过BLE通道对芯片进行控制&#xff0c;同时也支持通过蓝牙串口透传数…

抖动的评估(TJ 和 TIE 的关系)

TIE&#xff1a;时间间隔误差(Time Interval Error,简称TIE)抖动&#xff0c;即在很长的一串波形中&#xff0c;每次边缘的位置相对理想clk 的抖动。 TJBER &#xff1a;TJ&#xff08;Total Jitter&#xff09;总体抖动&#xff0c;为某误码率&#xff08;Bit Error Ratio&am…

网络流常用示意图及基本概念

【网络流简介】 ● 网络流基本概念网络&#xff1a;网络是一个有向有权图&#xff0c;包含一个源点和一个汇点&#xff0c;没有反平行边。网络流&#xff1a;是定义在网络边集上的一个非负函数&#xff0c;表示边上的流量。网络最大流&#xff1a;在满足容量约束和流量守恒的前…

..\USER\stm32f10x.h(298): error: #67: expected a “}“

原keil4的示例工程在用keil5打开之后出现报错&#xff1a; ..\USER\stm32f10x.h(298): error: #67: expected a "}" 在去掉手动添加的一个宏定义STM32F10X_HD后即可正常编译&#xff0c;因为KEIL5已经自动添加了

VR 大厦巡检机器人:开启智能化巡检新时代

在现代城市的高楼大厦中&#xff0c;保障建筑物的安全和功能正常运作是至关重要的。随着建筑结构日益复杂&#xff0c;隐蔽角落和繁杂管道线路的存在使得传统人工巡检面临诸多挑战和局限。电路老化、狭窄通道、拐角等潜在安全隐患&#xff0c;往往难以通过人工巡检完全覆盖&…

【STM32HAL库学习】定时器功能、时钟以及各种模式理解

一、文章目的 记录自己从学习了定时器理论->代码实现使用定时->查询数据手册&#xff0c;加深了对定时器的理解以及该过程遇到了的一些不清楚的知识。 上图为参考手册里通用定时器框图&#xff0c;关于定时器各种情况的工作都在上面了&#xff0c;在理论学习和实际应用后…

spring常用注解(八)@Async

一、介绍 1、介绍 二、原理 三、集成与使用 1、集成方法 &#xff08;1&#xff09;开启 使用以下注解开启 EnableAsync &#xff08;2&#xff09;使用 在需要异步处理的方法上加上 Async 2、返回值 Async注解的方法返回值只能为void或者Future<T>。 &…

轻松实现App推广代理结算,Xinstall超级渠道功能助您一臂之力!

在App推广的广阔天地中&#xff0c;与渠道方建立合作关系&#xff0c;共同实现用户增长和品牌提升&#xff0c;已成为众多开发者和广告主的共识。然而&#xff0c;如何高效管理这些渠道、监测推广效果、实现代理结算&#xff0c;一直是困扰大家的难题。今天&#xff0c;我们就来…

比较器 XD393 XINLUDA(信路达) DIP-8 2.5mA 模拟比较器 双路差动

XD393是一款比较器集成电路&#xff0c;适用于各种电子设备中的信号比较和处理。它的应用领域可能包括但不限于以下几个方面&#xff1a; 1. 电源管理&#xff1a;在电源管理系统中&#xff0c;XD393可以用来监控电压水平&#xff0c;确保系统稳定运行&#xff0c;或者触发某…

振动分析-2-信号频域分析以及频率分辨率的理解

参考2023-11-26 什么是频率&#xff1f; 参考2016-10-31什么是固有频率&#xff1f; 参考2024-01-28什么是频率分辨率&#xff1f; 参考2024-01-07香农采样定理有两种描述&#xff0c;哪个正确&#xff1f; 1 什么是频率 1.1 频率的定义 我们把振动发生完成一个完整往复循环…

利用 AI 深度学习,实现化合物配比最优化解决方案

为什么需要化合物配比的优化&#xff1f; 在化合物制造行业中&#xff0c;化合物的配比是产品质量控制的关键环节。 化合物制造流程 目前&#xff0c;这一过程高度依赖于材料专家和工程技术人员的经验&#xff0c;通过反复试验来验证产品性能&#xff0c;确保其满足市场和客户的…

Redis实现分布式锁有哪些方案?

Redis实现分布式锁有哪些方案&#xff1f; 在这里分享六种Redis分布式锁的正确使用方式&#xff0c;由易到难。 本文已收录到Java面试网站 方案一&#xff1a;SETNXEXPIRE 方案二&#xff1a;SETNXvalue值(系统时间过期时间) 方案三&#xff1a;使用Lua脚本(包含SETNXEXPIRE…

谷粒商城实战(033 业务-秒杀功能4-高并发问题解决方案sentinel 2)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第332p-第p335的内容 熔断降级 开启对Feign远程服务的熔断保护机制 feign.sentinel.enabletrue 这里我们只是调用方加就行 被调用方不用加 正常…