C++:继承(protected、隐藏、不能被继承的类、)

目录

继承的概念

继承的使用

继承方式

protected

继承类模板

赋值兼容转换

隐藏 

子类的默认成员函数

构造函数

拷贝构造函数

赋值重载函数

析构函数

不能被继承的类

方法1:父类的构造函数私有

方法2:final

继承与友元

继承与静态成员

多继承

菱形继承

虚继承

继承和组合

is-a

has-a 


继承的概念

继承是面向对象语言程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类的特性基础上进行扩展,增加成员函数或者成员变量,这样产生新的类称为子类

继承的使用

首先我们要先有一个类,这个类是有需要被继承的属性,这个类就被叫做基类(父类)

class Person
{
public:void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "张三";string _address;string _tel;int _age = 18;
};class Student : public Person
{
public:void study(){// ...}
protected:int _stuid;int _major;
};

这里的Person就是基类,而下面还有一个Student类,将Person类给继承了,这个类就叫做派生类(子类)

 具体的使用继承的方法就是派生类 : 继承方式 基类

 这个时候的派生类里面就会有基类的成员变量和成员函数(二者都有)

那么这里的继承方式又是什么呢?

继承方式

和类里面一样,有三种继承方式:public继承、protected继承、private继承 

protected

在没学习继承的时候protected访问限定符的作用就和private一样,但现在就不一样了

在基类中protected限定的变量或函数只有派生类中能访问,在外界不能访问

而private是派生类和外界都不能访问!

例如上面Person类中的那四个成员变量都是protected限定,那么这四个成员变量只能在派生类Student中使用,在外界是不能使用的

继承方式相当于给基类里面的所有属性设置一个权限最大值

例如上面的继承方式是public,那么基类中的属性在派生类中可以是public、protected、private

但是如果继承方式是private,那么基类中的属性在派生类中就都是private,无论该属性在基类中是以什么访问限定符限定的

具体如下表所示:

总结:

1. 父类private成员在子类中无论以什么方式继承都是不可见的

这里的不可见指的是父类的私有成员被继承到了子类对象中,但是语法上限制了子类对象无论是在类内还是类外都无法访问它

2. 父类的其它成员在子类中的访问方式==Min(成员在父类中的访问限定符,继承方式),public > protected > private

3. 使用关键字class时默认的继承方式是private,使用struct关键字时默认的继承方式是public,最好显示写出继承方式

4. 在实际运用中建议使用public继承,因为protected和private继承下来的成员使用范围小,实际中扩展性不强

继承类模板

当需要继承类模板的时候,需要明确的指明类域

#include <iostream>
#include <vector>using namespace std;namespace lyw
{template<class T>class stack : public std::vector<T>{public:void push(const T& x){vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}int main()
{return 0;
}

这里使用继承也可以完成stack类的实现

但是我们在复用vector类模板里面的函数的时候,必须要明确的指明类域

因为在运行实例化stack的函数时,vector类模板里面的函数是还没有实例化的,所以我们需要指明类域依次能实例化出特定函数

赋值兼容转换

首先这里有两个类,Person(基类)和Student(派生类)

class Person
{
protected:string _name; string _sex; int _age;
};class Student : public Person
{
public:int _No;
};

派生类是可以赋值给基类的

int main()
{Student s;Person p;p = s;return 0;
}

但是基类不可以赋值给派生类!

int main()
{Student s;Person p;s = p;return 0;
}

父类的指针是可以指向子类的,引用也可以引用子类,如下:

int main()
{Student s;Person p;Person* pp = &s;Person& rp = s;return 0;
}

但是子类的指针却不能指向父类,引用也是

int main()
{Student s;Person p;Student* ps = &p;Student& rs = p;return 0;
}

但是如果我们稍加处理(强制类型转换),是可以的

int main()
{Student s;Person p;Student* ps = (Student*)&p;Student& rs = (Student&)p;return 0;
}

这里有一个说法叫做切片或者切割,它可以帮助我们很好的理解基类和派生类的赋值规则

因为子类中就有父类的属性,所以子类是可以赋值给父类的,但是父类缺没有子类的一些属性,所以不能赋值回去

总结:

public继承的子类对象可以赋值给父类的对象、指针、引用

父类对象不能赋值给子类对象,但是通过强转就可以实现指针和引用的赋值 

隐藏 

隐藏是当父类和子类有同名的成员,子类将屏蔽父类的成员(隐藏起来),我们可以通过访问限定符来访问父类的隐藏成员

class Person
{
protected:string _name = "张三";int _num = 111;
};
class Student : public Person
{
public:int GetNum(){return _num;}
protected:int _num = 999;
};int main()
{Student s;cout << s.GetNum() << endl;return 0;
}

若是我们在GetNum中加上类域

int GetNum()
{return Person::_num;
}

通过以上实验不难看出这里的Person确实被隐藏起来了,但是我们仍然能访问

总结:

1. 在继承中子类和父类都有独立的作用域

2. 当子类和父类中有同名成员(函数名或者变量名),那么它们将构成隐藏关系

3. 我们可以使用父类::父类成员显示访问

子类的默认成员函数

下面是Person类中的四个默认成员函数

class Person
{
public:Person(const char* 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;
};

那么我们将父类继承给子类之后,该怎么样调用父类的

class Student : public Person
{
public:
protected:int _num; 
};

构造函数

Student(int num): _num(num)
{cout << "Student()" << endl;
}

我们可以这样写

父类的那部分怎么初始化呢?

因为父类是有默认构造函数的,所以它会调用父类的默认构造函数,将子类的name构造为"peter"

若是没有这个"peter"会报错!

Person(const char* name): _name(name)
{cout << "Person()" << endl;
}

所以父类尽量有一个默认构造函数

但是如果就是没有默认构造函数怎么办?我们可以在初始化列表中显示调用父类的构造函数

Student(const char* 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;
}

和构造函数一样,我们需要在初始化列表中显示调用Person的构造函数

这样就可以使用Person的构造函数来构造从Person继承出来的那部分属性了

赋值重载函数

Student& operator=(const Student& s)
{cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator=(s);_num = s._num;}return *this;
}

 一样需要调用父类的赋值重载函数,但是这里由于函数名都相同,所以构造隐藏关系,既然构成隐藏关系,就需要类域限定符来指定Person类域访问Person的赋值重载函数

析构函数

~Student()
{cout << "~Student()" << endl;
}

析构函数只需要处理Student自己的属性即可,如果没有空间需要释放我们也可以什么都不做

因为子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员,因为只有这样才能保证子类对象先清理子类成员--->清理父类成员这样的顺序

这里父类的析构函数和子类的析构函数其实是构成隐藏关系

隐藏不是函数名相同吗?为什么会构成隐藏关系呢?

从代码层看起来它们的函数名不相同,但是从底层来看它们的名字都是一个叫destructor()的函数,这是编译器特殊处理的结果

所以它们是构成了隐藏关系的

不想要这个关系可以加上virtual关键字在父类析构上

virtual ~Person()
{cout << "~Person()" << endl;
}

总结:

1. 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员,如果父类没有默认构造函数那么子类需要显示调用父类的构造函数

2. 子类的拷贝构造函数必须调用父类的拷贝构造函数完成父类的拷贝初始化

3. 子类的operator=必须要调用父类的operator=完成父类的复制,需要注意的是它们两个构成隐藏关系,子类调用时需要指定父类作用域

4. 子类的析构函数会在被调用后自动调用父类的析构函数,确保顺序是子类析构--->父类析构

5. 子类和父类的析构函数由于编译器特殊处理的原因,它们也构成隐藏关系

具体处理流程图如下: 

不能被继承的类

如何实现不能被继承的类?

方法1:父类的构造函数私有

这是C++98时期常用的写法

当父类的构造函数私有时,子类的构成必须要调用父类的构造函数,但是私有化后,子类看不见,所以就不能调用了,子类就无法实例化出对象

class Base
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:Base(){}
};

方法2:final

C++11新增了一个final关键字,final修改父类,子类就不能继承父类了

class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private://Base()//{}
};

继承与友元

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

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;
}

 

Display函数中可以访问处于父类protected保护中的_name,因为Display是Person类的友元

但是却不能访问子类中protected保护中的_stuNum,因为它们不是友元,没有关系 

父类的朋友可不一定是你子类的朋友

继承与静态成员

若是父类定义了static静态成员,则整个继承体系中只有一个这样的成员,无论生出多少个子类,都只有一个static成员实例

class Person
{
public:string _name;static int _count; // 声明
};int Person::_count = 0; // 定义class Student : public Person
{
protected:int _stuNum;
};int main()
{Person p;Student s;cout << &p._count << endl;cout << &s._count << endl;return 0;
}

在X86的环境下我们打印出了父类Person的_count和子类Student继承下来的_count的地址

最终结果显而易见它们的地址是相同的

说明它们是同一个变量

所以证实了static成员只有一个!

多继承

继承的关系并不是只可以有父和子的,还可以有“爷孙”关系(多继承)

Student继承Person,PostGraduate继承Student,这就是一个简单的多继承关系 

 

Assistant同时继承了Student和Teacher,这也是一个简单的多继承关系 

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

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

多继承对象在内存中的模型是:先继承的父类在前面,后面继承的父类在后面,子类成员放到最后面 

菱形继承

菱形继承是多继承中的一种特殊的情况

从上图就可以看出菱形继承有数据冗余和二义性问题

在Assistant的对象中Person的成员会有两份 

只要支持多继承就一定会有菱形继承的问题

在Java中就不支持多继承,直接的规避掉了这里的问题,这里也是C++比Java语法相对难一些的地方

数据冗余:Person的成员在Assistant中有两份

二义性:Person的成员在Assistant中的两份代表的含义是什么?怎么能有两个?

具体示例代码如下:

class Person
{
public:string _name;
};class Student : public Person
{
protected:int _num; 
};class Teacher : public Person
{
protected:int _id;
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse;
};

若是我们直接在Assistant的对象中直接访问从Person继承下来的成员,编译就会报错

int main()
{Assistant a;a._name = "peter";return 0;
}

 

这就是二义性的问题

二义性的问题我们可以依靠指定类域来解决

int main()
{Assistant a;a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}

但是数据冗余的问题我们依然无法解决,a对象的名字怎么能有两个?依照我们前面类中的设定name是只能有一个的!

那多继承的二义性和数据冗余到底该如何彻底解决呢?

虚继承

虚继承相当于让菱形继承下来的那个类只能有一个相同的父类属性

class Person
{
public:string _name; 
};class Student : virtual public Person
{
protected:int _num; 
};class Teacher : virtual public Person
{
protected:int _id; 
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; 
};

使用方法是在最前面的父类继承下了的两个子类的继承方式前面加一个virtual关键字

 

这样当我们再次在Assistant对象中访问继承下了的_name的时候就不会有二义性和数据冗余的问题了!

int main()
{Assistant a;a._name = "peter";return 0;
}

 

继承和组合

is-a

public继承是一种is-a的关系

template<class T>
class stack : public vector<T>
{};

也就是说每个子类对象都是一个父类对象

has-a 

组合是一种has-a的关系

template<class T>
class stack
{
public:vector<T> _v;
};

若B组合A,那么每个B对象中都有一个A对象

继承是根据父类的实现来定义子类的实现

这种通过生成子类的复用通常被称为白箱复用

白箱:父类内部细节对子类可见(公开透明) 

继承⼀定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度高,这不是一个很好的方式

对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或者组合对象来获得

这种复用风格被称为黑箱复用

黑箱:对象的内部细节是不可见的,对象只以“黑箱”的方式出现(不公开) 

 组合之间没有很强的依赖关系,耦合度低

建议优先使用组合,而不是继承,为什么?

因为组合的耦合度低,代码维护性好

当然还是需要看两者属于什么关系,如果是is-a则使用继承,是has-a则使用组合

另外,如果想要实现多态也得使用继承  

小tips:

C++最常用的iostream库中使用的也是菱形继承

是由istream和ostream继承而来的 


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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis 键值型数据库

一、Redis是什么 Redis&#xff1a;REmote DIctionary Server&#xff08;远程字典服务器&#xff09; 是完全开源免费的&#xff0c;用C语言编写的&#xff0c;遵守BSD协议&#xff0c;是一个高性能的&#xff08;Key/Value&#xff09;分布式内存数据 库&#xff0c;基于内存…

tcp 和udp通信

一.recvfrom recvfrom函数是一个系统调用&#xff0c;用于从套接字接收数据。该函数通常与无连接的数据报服务&#xff08;如 UDP&#xff09;一起使用&#xff0c;但也可以与其他类型的套接字使用。与简单的 recv() 函数不同&#xff0c;recvfrom() 可以返回数据来源的地址信息…

FreeRTOS学习:内存管理

FreeRTOS内存管理简介 在使用 FreeRTOS 创建任务、队列、信号量等对象的时候&#xff0c; FreeRTOS 一般都提供了两种方法&#xff0c; 动态方法创建&#xff1a;自动地从 FreeRTOS 管理的内存堆中申请所创建对象所需的内存&#xff0c;在对象被删除后&#xff0c;又可以将这…

Vue3开始

1.创建 Vue3 工程 1.基于 vue-cli创建 2.基于vite创建工程 Home | Vite中文网 (vitejs.cn) nodejs 环境可以 使用 nvm 来管理 nodejs 的版本 3.开始创建 4.目录结构 5.创建完 执行 安装依赖 npm i 6.运行项目 为什么命令是 dev 就是从这里配置的 npm run dev

大白话社融中M1和M2的学习笔记

一、背景 为什么我看社融数据呢&#xff1f;因为作为一个码农&#xff0c;我不清楚当前个人所处的样本情况是否真实。我所处的环境是在深圳南山&#xff0c;身边一些同事有不少找工作不怎么顺利&#xff0c;我所在的公司今年也没招大学生了&#xff0c;人员也只出不进为主&…

本地生活服务商系统如何利用本地推获得更多曝光?

随着本地生活赛道中的竞争愈演愈烈&#xff0c;越来越多的本地生活服务商和本地生活商家开始计划着通过在本地推等平台投放相关信息&#xff0c;以提高品牌店铺的曝光量和知名度。不过&#xff0c;就目前的情况来看&#xff0c;绝大多数人都陷入了一种“投入多&#xff0c;转化…

教育部-华为产学合作协同育人项目 | 仓颉编程语言专项

为响应《教育部高等教育司关于调整产学合作协同育人项目运行模式及征集2024年产学合作协同育人项目的通知》号召&#xff0c;华为公司2024年第二批70个项目已发布&#xff0c;其中仓颉编程语言领域共计10个项目&#xff0c;如下所示&#xff0c;通过新工科建设项目&#xff0c;…

U盘安装Ubuntu24.04,乌邦图,UltralISO

文章目录 前言通过UltraISO&#xff0c;制作启动U盘下载镜像制作工具UltraISO(软碟通)下载ubuntu镜像文件制作启动U盘 安装ubuntu设置root密码&#xff0c;并登陆root 前言 在Ubuntu作为主流的linux系统&#xff0c;有时候使用VMware安装使用&#xff0c;总归有一定的性能损耗…

力扣每日一题 数组最后一个元素的最小值 位运算

Problem: 3133. 数组最后一个元素的最小值 &#x1f468;‍&#x1f3eb; 灵神题解 class Solution {public long minEnd(int n, int x) {n--; // 先把 n 减一&#xff0c;这样下面讨论的 n 就是原来的 n-1long ans x;int i 0;int j 0;while((n >> j) > 0){// …

springboot集成海康sdk,针对视频流获取某一点的实时温度

直接上代码吧: 前端页面专递点的x和y的坐标及其设备的ip @RequestMapping(value = "/getRealTemperatureByPoint") public float getRealTemperatureByPoint(HttpServletRequest request) {Map<String, Object> params = ParamUtil.getParams(request);Strin…

小区社区超市商城停车场管理系统-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…

Linux 信号 signal,sigaction,sigqueue,kill,相关函数

驱动与应用的结合&#xff0c;参考我的这篇&#xff1a;https://blog.csdn.net/rjszcb/article/details/113573517 一、 什么是信号 信号是软中断,用于通知进程某个事件已经发生。进程可以选择如何响应信号:忽略、默认处理、自定义处理等。 常见信号有:SIGINT(键盘中断)、SIGK…

C语言程序设计-练习篇

华夏波澜壮阔&#xff0c;少年仍需前行。 十&#xff0c;实现一个函数&#xff0c;打印乘法口诀表&#xff0c;口诀表的行数和列数自己指定 #include <stdio.h> //实现一个函数&#xff0c;打印乘法口诀表&#xff0c;口诀表的行数和列数自己指定 void print_table(int …

启动docker镜像

1、运行容器 2、当前运行的进程 3、当前位置和启动时间 4、cat/etc/redhat-release查看版本 5.镜像是模版&#xff0c;容器是实例 6.容器中没有命令运 7.容器总是能轻易获取 8.配置yum 9.安装http 10.修改index⽂件 11.httpd -k start 12.访问 13.退出就没有服务了 14…

Redis配置及idea部分操作

配置Redis远程访问 修改访问IP地址 #跳转到redis安装目录 cd /usr/local/redis-6.2.1 #修改redis.conf配置文件 vi redis.conf #注释redis.conf第69行的配置项 #bind 127.0.0.1 设置登录密码 找到下面这一行并去除注释&#xff0c;并添加密…

【微服务】SpringCloud Alibaba 10-14章

10 SpringCloud Alibaba入门简介 10.1 是什么 诞生 2018.10.31&#xff0c;Spring Cloud Alibaba 正式入驻了 Spring Cloud 官方孵化器&#xff0c;并在 Maven 中央库发布了第一个版本。 Spring Cloud Alibaba 介绍 10.2 能干嘛 https://github.com/alibaba/spring-cloud-al…

git常用操作合集

1 撤销 1.1 适用场景 如果在git上提交了commit&#xff0c;但是当前提交的代码有问题&#xff0c;需要回退到上个版本 1.2 操作命令 1、git log 查看历史提交记录及对应的commit id 找到需要回退的commit id 2、执行git reset回退到之前的状态 git reset --hard <commi…

基于Springboot和BS架构的宠物健康咨询系统pf

TOC springboot509基于Springboot和BS架构的宠物健康咨询系统pf 第一章 课题背景及研究内容 1.1 课题背景 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#x…

day37动态规划+三.Github链接本地仓库

一.动态规划 474.一和零 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 请你找出并返回 strs 的最大子集的长度&#xff0c;该子集中 最多 有 m 个 0 和 n 个 1 。 如果 x 的所有元素也是 y 的元素&#xff0c;集合 x 是集合 y 的 子集 。 思路:这道题更像是另一种的0-…

C++ 设计模式——外观模式

外观模式 C 设计模式——外观模式主要组成部分1. 外观类&#xff08;Facade&#xff09;2. 子系统类&#xff08;Subsystem&#xff09;3. 客户端&#xff08;Client&#xff09; 例一&#xff1a;工作流程示例1. 外观类&#xff08;Facade&#xff09;2. 子系统类&#xff08;…