C++---继承

继承

  • 前言
  • 继承的概念及定义
    • 继承的概念
    • 继承定义
      • 继承关系和访问限定符
  • 基类和派生类对象赋值转换
  • 继承中的作用域
  • 派生类的默认成员函数
  • 继承与友元
  • 继承与静态成员
  • **多重继承**
    • 多继承下的类作用域
    • 菱形继承
    • 虚继承
      • 使用虚基类
    • 支持向基类的常规类型转换

前言

在需要写Father类和Mother类的时候,需要给这两个类写一些属性,像 名字,性别,年龄,爱好,电话,家庭地址等,这两个类中会有一些共同的属性,把这些公共的属性进行提取,封装成一个Person类,Father和Mother继承Person,就不需要在写共同的属性了。这就是本章要说的继承

继承的概念及定义

继承的概念

继承机制是面向对象程序设计是代码可以复用的最重要的手段,它允许程序猿在保持原有类特性的基础上进行扩展,增加功能,这样产生的新的类,称为派生类。继承呈现了面向对象程序设计的层次结构。层次结构的根部有一个基类,派生类就是直接或间接的从基类继承而来。基类负责定义在层次结构中所有类的共同拥有的成员,而每个派生类定义各自特有的成员。

class Person
{
public:void Print(){cout << "name>>" << _name << endl;cout << "age>>" << _age << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
protected:int _stuid = 123;
};int main()
{Student s;s.Print();return 0;
}

在这里插入图片描述

通过调试可以看出,派生类中有基类的成员,也有自己定义的特殊的成员。

继承定义

	   派生类   继承方式  基类
class Student : public Person

继承关系和访问限定符

在这里插入图片描述


继承基类的时候,访问限定符的不同,成员访问也会变化。

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类的不可见在派生类中不可见
  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。。

  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。

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

  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强 。

基类和派生类对象赋值转换

通常情况下,如果我们想把引用活指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。存在继承关系的类是也给重要的列外:我们可以将基类的指针或引用绑定到派生类对象上。 见上面代码将Student对象的地址赋值给Person*

在这里插入图片描述

之所以存在派生类向基类的类型转换是因为每个派生类对象都包含了一个基类的部分,而基类的引用或者指针可以绑定到该基类的部分上。

Student s;
Person* p1 = &s;
Person& p2 = s;

p1的过程中派生类会将父类的那一部分切出来拷贝过去,p2的过程则是子类中父类的部分的别名。只有派生类对象中的基类部分会被拷贝,移动或者赋值,它的派生类部分将被忽略掉。

	s.Print();p1->_name = "B";p1->Print();p2._name = "C";p2.Print();

在这里插入图片描述

编译器在编译时无法确定某个特定的转换在运行时是否安全,这是因为编译器只能通过检查指针或者引用的静态类型来推断该转换是否合法。如果在基类中含有一个或多个虚函数,我们可以使用 dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行。

  • 从派生类向基类的类型转换只对指针或者引用类型有效
  • 基类向派生类不存在隐式类型转换
  • 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行

自动类型转换只对指针或者引用有效,但是继承体系中的大多数类仍然(显示或隐式的)定义了拷贝控制成员。因此,我们通常能够将一个派生类对象拷贝,移动,或者赋值给一个基类对象。不过这种操作只处理派生类中的基类部分。

继承中的作用域

每个类定义自己的作用域,这个作用域内我们定义的成员,当存在继承关系时,派生类的作用域嵌套,在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析时,编译器将继续在外层的基类作用域中寻找改名字的定义。


和其他作用域一样,派生类也能重用定义在其直接基类或简介基类中的名字,此时定义在内层作用域(既派生类)的名字将隐藏定义在外层作用域(既基类)的名字。

class Person
{
public:void Print(){cout << "name>>" << _name << endl;cout << "age>>" << _age << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
public:void Print(){cout << "age>>" << _age << endl;cout << "Person.age>>" << Person::_age << endl;}protected:int _stuid = 123;int _age = 20;
};int main()
{Student s;s.Print();return 0;
}

输出结果是 20 18

派生类的成员将隐藏同名的基类成员。
如果想要访问基类中的成员,可以使用作用域运算符来使用隐藏的成员


class Person
{
public:void Print(){cout << "Person::Print()" << endl;}string _name = "A";int _age = 18;
};class Student : public Person
{
public:void Print(int i){cout << "Student::Print()" << endl;}
protected:int _stuid = 123;int _age = 20;
};

代码中的两个Print是什么关系呢?

隐藏

为什么不是重载呢?同名函数,参数不同,构成重载很合理。

但是重载有一个限定,同一个作用域。

派生类的默认成员函数

class Person
{
public:Person(const char* name = "A"):_name(name){}protected:string _name;int _age = 18;
};class Student : public Person
{
public:Student(const char* name = "B"):_id(0),_name(name){}
protected:int _id;
};

在父类中写了构造函数后,想在子类中给继承而来的name成员进行初始化,但是Student中的构造函数给name成员初始化的操作却报错了。

因为派生类的构造只能构造在派生类中新增的成员,要想调用基类的构造函数可以在派生类的初始化列表中加上 Person(name)

	Student(const char* name = "B"):_id(0),Person(name)

如果不写 Person(name)的话,派生类在执行构造函数的时候,会自动的去调用基类的构造函数(要有默认参数,不然会报错).


	Student(const Student& s):Person(s) // 将基类 = 派生类,会有切片操作,将属于基类的部分进行切割。,_id(s._id){}

拷贝构造中也要调用父类Person才能完成。

如果在拷贝构造中不写Person(s),就会去调用默认拷贝构造函数。


	Student& operator= (const Student& t){cout << "Student& operator= (const Student& t)" << endl;if (this != &t){Person::operator=(t);// 不写Person:: 会造成隐藏_id = t._id;}return *this;}
	Person& operator= (const Person& n){cout << "Person& operator= (const Person& n)" << endl;if (this != &n){_name = n._name;}return *this;}

在基类和派生类中分别写一个赋值运算符,派生类也需要调用父类的赋值运算符。


在析构函数体执行完后,对象的成员会被隐式销毁,类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源。

class Person
{
public:Person(const char* name = "A"):_name(name){cout << "Person" << endl;}Person& operator= (const Person& n){cout << "Person& operator= (const Person& n)" << endl;if (this != &n){_name = n._name;}return *this;}~Person(){cout << "~Person" << endl;}protected:string _name;
};class Student : public Person
{
public:Student(const char* name = "B"):_id(0),Person(name){cout << "Student(const char* name = 'B')" << endl;}Student& operator= (const Student& t){cout << "Student& operator= (const Student& t)" << endl;if (this != &t){Person::operator=(t);_id = t._id;}return *this;}~Student(){Person::~Person();cout << "~Student" << endl;}Student(const Student& s):Person(s),_id(s._id){}protected:int _id;
};int main()
{Student s("张三");return 0;
}

后面由于多态的原因,析构函数的函数名被特殊处理了,统一处理成destructor。

在执行代码后

在这里插入图片描述

先给基类初始化,然后派生类初始化,在调用析构发现,基类的析构多调用了一次。

对象销毁的顺序正好与创建的顺序相反,创建对象的时候是先调用父类的构造函数,这里子类析构函数首先执行,然后是父类的析构函数,以此类推,沿着继承体系的反方向直至最后。

继承与友元

就像友元关系不能传递一样,有缘关系同样也不能继承,基类的友元在访问派生类成员时不具有特殊属性,类似的,派生类的友元也不能随意访问基类的成员。

class Student;
class  Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name = "S"
};class Student : public Person
{
protected:int _id = 1;
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._id << endl;
}int main()
{Person p;Student s;Display(p, s);return 0;
}

不能继承友元关系,每个类负责控制各自成员的访问权限。

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系只存在该成员的唯一定义。不管有多少个派生类,静态成员只有一个实例。

class Student;
class  Person
{
public:string _name = "S";static int num;
};int Person::num = 0;class Student : public Person
{
protected:int _id = 1;
};int main()
{Person p;Student s;cout << &s.num << endl;cout << &p.num << endl;return 0;
}

输出结果会发现,这两个地址是一样的。

静态成员遵循通用的访问控制规则。

多重继承

一个子类只有一个直接父类时称这个继承为单继承。

在这里插入图片描述

多重继承是指一个子类有两个或以上直接父类,多继承的派生类继承了所有父类的属性。从概念上看不是很难,但是多个基类相互交织就会产生一些特殊的问题。

在这里插入图片描述

构造一个派生类的对象将同时构造并初始化它的所有基类子对象。与从一个基类进行的派生一样,多继承的派生类的构造函数初始值也只能初始化它的直接基类。

多继承下的类作用域

在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中。查找过程沿着继承体系自底向上进行,直到找到所需要的名字,派生类的名字将隐藏基类的同名成员。

在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到了,则对该名字的使用将具有二义性。

菱形继承

在这里插入图片描述

一个学生,它继承了A,也继承了B,A和B都继承了Person,前面也说过,派生类会继承基类的所有属性,如果A中存在age,B中也存在age,在Student中访问age就会导致二义性(不知道访问哪个),无法明确知道访问的是哪一个。

class Person
{
public:string _name;
};class A : public Person
{
public:string _id;
};class B : public Person
{
public:string _num;
};class Student : public A, public B
{
public:string _sex;
};int main()
{Student s;s._name = "张三";return 0;
}

在这里插入图片描述

当然,可以通过指定访问哪个父类的成员可以解决二义性的问题,但是数据冗余(浪费空间)的问题无法解决。

	Student s;s.B::_name = "张三";s.A::_name = "李四";cout << s.B::_name << endl;cout << s.A::_name << endl;

在这里插入图片描述

当一个类拥有多个基类,有可能出现派生类从两个或更多基类中继承同名成员的情况,此时,不加前缀限定符直接使用该名字会引发二义性。

虚继承

在默认情况下,派生类中含有继承链上的每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。也就是上面的菱形继承造成的二义性和数据冗余问题。

C++给出了虚继承的机制来解决这两个问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

class Person
{
public:string _name;
};class A : virtual public Person
{
public:string _id;
};class B :virtual public Person
{
public:string _num;
};class Student : public A, public B
{
public:string _sex;
};int main()
{Student s;s._name = "张三";cout << s._name << endl;return 0;
}

在这里插入图片描述


#include <iostream>using namespace std;class A
{
public:int _a;
};class B : public A
{
public:int _b;
};class C : public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在这里插入图片描述

在调试的时候,查看内存,可以发现,这些数据都是挨着放的。

在这里插入图片描述

这是通过前缀限定符解决二义性问题的,可以看出,数据冗余问题还存在。


下面看通过虚继承的方式。

在代码最后添上了 d._a = 0;

在这里插入图片描述

在这里插入图片描述

这样,就没有数据冗余和二义性了。但是BC类中多了两个东西。

在这里插入图片描述

把这两个地址查看一下发现,指向的都是0,但是这个地址的下一个位置存的都有数据。其实通过计算可以发现,1到6的偏移量刚好是20,3到6的偏移量刚好是12.所以BC中多出来的地址存的是距离A的偏移量(相对距离)。

存找基类偏移量的表叫虚机表

为什么要把偏移量给存下来呢?上面说的切片操作,基类以引用或者指针的方式,把派生类=基类。

使用虚基类

我们指定虚基类的方式是添加关键字 virtual

class B :virtual public Person

通过上面的代码,我们将Person 定义为 B的虚基类。

vitrual说明符表示一种愿望,即在后续的派生类当中共享虚基类的同一份实例。

支持向基类的常规类型转换

不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。

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

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

相关文章

征战开发板从无到有(三)

接上一篇&#xff0c;翘首已盼的PCB板子做好了&#xff0c;管脚约束信息都在PCB板上体现出来了&#xff0c;很满意&#xff0c;会不会成为爆款呢&#xff0c;嘿嘿&#xff0c;来&#xff0c;先看看PCB裸板美图 由于征战开发板电路功能兼容小梅哥ACX720&#xff0c;大家可以直…

【MySQL】 MySQL数据库基础

文章目录 &#x1f431;‍&#x1f453;数据库的操作&#x1f4cc;显示当前的数据库&#x1f4cc;创建数据库&#x1f388;语法&#xff1a;&#x1f388;语法说明&#x1f388;示例&#xff1a; &#x1f334;使用数据库&#x1f38b;删除数据库&#x1f431;‍&#x1f3cd;语…

Stable DIffusion 炫酷应用 | AI嵌入艺术字+光影光效

目录 1 生成AI艺术字基本流程 1.1 生成黑白图 1.2 启用ControlNet 参数设置 1.3 选择大模型 写提示词 2 不同效果组合 2.1 更改提示词 2.2 更改ControlNet 2.2.1 更改模型或者预处理器 2.2.2 更改参数 3. 其他应用 3.1 AI光影字 本节需要用到ControlNet&#xff0c;可…

flutter聊天界面-TextField输入框buildTextSpan实现@功能展示高亮功能

flutter聊天界面-TextField输入框buildTextSpan实现功能展示高亮功能 最近有位朋友讨论的时候&#xff0c;提到了输入框的高亮展示。在flutter TextField中需要插入特殊样式的标签&#xff0c;比如&#xff1a;“请 张三 回答一下”&#xff0c;这一串字符在TextField中输入&a…

【PowerQuery】Excel的PowerQuery按需刷新

将数据通过PowerQuery 导入进来后,这里将进行数据分组运算,最终的数据计算结果将保存在Excel 表格中,图为销售统计结果。 在Excel中,如果我们希望进行销售统计的手动更新可以使用几种不同的方法来进行刷新: 刷新单一数据连接如果仅仅需要刷新单一数据连接的话我们可以通过…

DETR:End-to-End Object Detection with Transformers

代码&#xff1a;https://github.com/HuKai97/detr-annotations 论文&#xff1a;https://arxiv.org/pdf/2005.12872.pdf 参考视频&#xff1a;DETR 论文精读【论文精读】_哔哩哔哩_bilibili 团队&#xff1a;Meta AI 摘要 DETR 做目标检测任务既不需要proposal&#xff0…

每日一题~中序后序遍历构造二叉树

原题链接&#xff1a;106. 从中序与后序遍历序列构造二叉树 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 思路分析&#xff1a; 后序遍历分析图 中序遍历分析图 不难看出后序遍历的结果中的最后一个元素就是根节点&#xff0c;倒数第二个元素则是根节点的…

VUE build:gulp打包:测试、正式环境

目录 项目结构 Gulp VUE使用Gulp Vue安装Gulp Vue定义Gulp.js package.json build文件夹 config文件夹 static-config文件夹 项目结构 Gulp Gulp是一个自动化构建工具&#xff0c;可以帮助前端开发者通过自动化任务来管理工作流程。Gulp使用Node.js的代码编写&#xff…

【牛客网】BC146 添加逗号

一.题目描述 牛客网题目链接:添加逗号_牛客题霸_牛客网 描述: 对于一个较大的整数 N(1<N<2,000,000,000) 比如 980364535&#xff0c;我们常常需要一位一位数这个数字是几位数&#xff0c;但是如果在这 个数字每三位加一个逗号&#xff0c;它会变得更加易于朗读。 因此&a…

linux查看进程对应的线程(数)

首先&#xff0c;top或ps查看进程列表&#xff0c;确定要查看的进程pid&#xff0c;如下面40698 查看进程的线程情况 查看进程&#xff1a;top -p 40698 查看线程&#xff1a;top -p 40698 -d 3 -H 其中-d是刷新频率 可看到此进程共211个线程&#xff0c;运行中的是211个。…

虹科案例 | Zuellig Pharma和ELPRO通过符合GDP标准的温度监测和高效的温度数据管理为未来发展奠定基础

在本案例研究中&#xff0c;您将了解Zuellig Pharma 实施了温度监测解决方案&#xff0c;以一致的数据结构获取各国和各种运输方式的数据; 通过将温度数据上传到其数据库管理系统&#xff0c;显著提高了其效率; 并建立了为未来管理决策提供数据增值使用的基础。 项目合作伙伴 …

使用终端MobaXterm连接Centos

1. 下载MobaXterm 官网&#xff1a; https://mobaxterm.mobatek.net/download.html 2. MobaXterm连接Linux 1 、查看刚才安装的 Linux 的 IP 地址 2、连接 3. Linux自带了JRE 由于javac指令不能运行&#xff0c;所以Linux只自带了JRE&#xff01;&#xff01;&#xff01;

Vue 2 组件间的通信方式总结

引言 组件间的关系有父子关系、兄弟关系、祖孙关系和远亲关系。 不同的关系间&#xff0c;组件的通信有不同的方式。 一、prop 和 $emit prop向下传递&#xff0c;emit向上传递。 父组件使用 prop 向子组件传递信息。 ParentComponent.vue <template><div><…

VSCODE 使用技巧

vscode批量去掉代码中空行的方法 1、在vscode中使用ctrl f组合快捷键打开替换窗口. 2、输入下面的正则表达式 ^\s*(?\r?$)\n https://mp.weixin.qq.com/s/ZKV2sZWszxBLNTNLEWhsng 你的代码够安全吗&#xff1f;推荐5个VS Code代码安全插件 VSCode&#xff1a;人生苦短&…

C++项目实战——基于多设计模式下的同步异步日志系统-⑤-实用工具类设计

文章目录 专栏导读获取系统时间time介绍 getTime函数设计判断文件是否存在stat介绍exists函数设计 获取文件所在路径find_last_of介绍path函数设计 创建文件所在目录mkdir介绍find_first_of介绍函数createDirectory设计 实用工具类整理 专栏导读 &#x1f338;作者简介&#xf…

VS code 下 makefile 【缺少分隔符 停下来】 报错解决方法

首先来看报错的makefile源码 再来看报错的信息&#xff1a; 第5行缺少分隔符&#xff0c;其实不止是第5行&#xff0c;只要是前面需要加tab留白的行都会报这个错误&#xff0c;比如说第7行第11行 编译的时候&#xff0c;前面的留白必须是按tab键生成的 但是&#xff01;&…

【JavaSE笔记】抽象类与接口

一、抽象类 1、概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是反过来&#xff0c;并不是所有的类都是用来描绘对象的&#xff0c;如果一个类中没有包含足够的信息来描绘一个具体的对象&#xff0c;这样的类就是抽象类。 package demo2…

Json-Jackson和FastJson

狂神&#xff1a; 测试Jackson 纯Java解决日期格式化 设置ObjectMapper FastJson&#xff1a; 知乎&#xff1a;Jackson使用指南 1、常见配置 方式一&#xff1a;yml配置 spring.jackson.date-format指定日期格式&#xff0c;比如yyyy-MM-dd HH:mm:ss&#xff0c;或者具体的…

ArmSoM-W3之RK3588 Debian11详解

1. 简介 RK3588从入门到精通Debian 是⼀种完全⾃由开放并⼴泛⽤于各种设备的 Linux 操作系统。Rockchip在官⽅Debian发⾏版的基础上构建和适配了相关硬件功能 2. 环境介绍 硬件环境&#xff1a; ArmSoM-W3 RK3588开发板 软件版本&#xff1a; OS&#xff1a;ArmSoM-W3 Debia…

WPF 如何让xmal的属性换行显示 格式化

WPF 如何让UI的xmal 按照下面的格式化显示 首先格式化显示在VS中的快捷键是 Ctrl &#xff2b;D 然后需要配置&#xff0c;工具 选项 -文本编辑器 -xmal -格式化-间距 更改成如下就可以了