当前位置: 首页 > news >正文

C++三大特性之继承,详细介绍

eea5eba81baa42f0a333f323d0825d11.jpeg

阿尼亚全程陪伴大家学习~

前言

每个程序员在开发新系统时,都希望能够利用已有的软件资源,以缩短开发周期,提高开发效率。 为了提高软件的可重用性(reusability),C++提供了类的继承机制。

1.继承的概念

继承: 指在现有类的基础上建立一个新的类。现有类称为基类或父类,新建类称为派生类或子类。对于父类与子类,人们也常说子类继承了父类,或者父类派生了子类。

语法:class 子类(派生类):继承方式 父类(基类)

现在我们一起来看一下具体的实现

#include<iostream>
using namespace std;
//基础界面
class BasePage
{void left(){cout << "Java,C++,Python,C...." << endl;}void right(){cout << "右界面" << endl;}void head(){cout << "头部界面" << endl;}void bottom(){cout << "底部界面" << endl;}
};
//C语言界面
class C 
{
public:void left(){cout << "Java,C++,Python,C...." << endl;}void right(){cout << "右界面" << endl;}void head(){cout << "头部界面" << endl;}void bottom(){cout << "底部界面" << endl;}void Linux(){cout << "Linux" << endl;}
};
//C语言界面
class C :public BasePage
{
public:void Linux(){cout << "Linux" << endl;}
};
//Java界面
class Java :public BasePage
{
public:void JavaSE(){cout << "JavaSE" << endl;}
};

这可以看做一个编程语言学习的主界面,左界面是各种不同语言的分类,这些不同编程语言界面都有主界面的部分,但是他们也有自己独特的部分,主界面是基类,而具体的编程语言界面则是派生类,派生类都继承了父类(基类)所有的成员函数。

2.继承的三种方式

公共(public)继承、保护(protected)继承、私有(private)继承

57b776b97a5a42bf9c40a65b14817fb2.png

下面我们一一介绍

2.1公共继承

#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
public:int _A = 10;void A_print(){cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;}
};
class B:public A
{
private:int _b = 2;
public:int _B = 20;void B_print(){cout << "_b=" << _b << endl;}
};
int main()
{B b;b.B_print();b.A_print();return 0;
}

72cd08d60b8e425db7cd1497424a84ff.png

我们发现通过公共继承的方式来继承A类,A类中的打印函数也被继承了,所以B类的对象可以调用A类的成员函数,接下来我们修改一下代码,在B类中访问A类的成员变量

9a012076321d43a9a05e0e9aa25d89ee.png

93a2f026724b48519b3e5c6244457f70.png

编译器告诉我们_a不可以被访问因为他是A类中的私有成员,而_A可以被访问因为他是A类中的公有成员,那有没有什么方法能让_a也能被访问呢?

有。一种是把_a设置为public成员,另外一种是把_a设置为protected成员(最好的做法),这三种又有什么区别呢?(重要!!!)

*公有(public)成员:

公有成员可以从任何地方被访问,包括类的内部、类的派生类以及类的外部。
把_a设置为公有成员意味着任何地方的代码都可以直接访问它,这通常不是一个好的做法,因为它破坏了封装性(封装性意味着隐藏对象的内部状态以防止它们被外部代码直接访问)。

*私有(private)成员:

私有成员只能在类的内部被访问。
编译器告诉我们_a不可以被访问,因为它被声明为私有成员。这意味着你不能从类的外部或派生类中直接访问它。

*保护(protected)成员:

保护成员可以在类的内部和派生类中被访问,但不能在类的外部被访问。
把_a设置为保护成员意味着你可以在其派生类中访问它,但不能在类的外部直接访问它。这提供了一种在派生类中重用和扩展基类功能的方式,同时保持对外部世界的封装性。

能明白三者的差异,也就很容易理解三种继承方式的差别了

#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
protected:int a = 100;
public:int _A = 10;void A_print()//A类打印函数{cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;cout << "a=" << a << endl;}
};
class B:public A
{
private:int _b = 2;
public:void B_print()//B类打印函数{cout << "_b=" << _b << endl;//cout << "_a=" << _a << endl;//A类私有成员不能被访问cout << "_A=" << _A << endl;//A类公有成员可以被访问cout << "a=" << a << endl;//A类保护成员可以被访问A_print();//A类公有成员可以被访问}
};
int main()
{B b;b.B_print();cout << endl;cout << b._A << endl;//public继承方式,在类的外部能访问类public成员b.A_print();//public继承方式,在类的外部能访问类public成员//cout << b.a << endl;//在类的外部不能访问类protected成员return 0;
}

2.2保护继承

#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
protected:int a = 100;
public:int _A = 10;void A_print()//A类打印函数{cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;cout << "a=" << a << endl;}
};
class B:protected A
{
private:int _b = 2;
public:void B_print()//B类打印函数{cout << "_b=" << _b << endl;//cout << "_a=" << _a << endl;//A类私有成员不能被访问cout << "_A=" << _A << endl;//A类公有成员可以被访问cout << "a=" << a << endl;//A类保护成员可以被访问A_print();//A类公有成员可以被访问}
};
int main()
{B b;b.B_print();//b.A_print();//protected继承方式,在类的外部不能访问类成员return 0;
}

2e7f28a743fa4e6d8669fa810f6e06ac.png

2.3私有继承

#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
protected:int a = 100;
public:int _A = 10;void A_print()//A类打印函数{cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;cout << "a=" << a << endl;}
};
class B:private A
{
private:int _b = 2;
public:void B_print()//B类打印函数{cout << "_b=" << _b << endl;//cout << "_a=" << _a << endl;//A类私有成员不能被访问cout << "_A=" << _A << endl;//A类公有成员可以被访问cout << "a=" << a << endl;//A类保护成员可以被访问//A_print();//A类公有成员可以被访问}
};
int main()
{B b;b.B_print();cout << endl;//cout << b._A << endl;//private继承方式,在类的外部不能访问类public成员//cout << b.a << endl;private继承方式,在类的外部不能访问类protected成员//b.A_print();//private继承方式,在类的外部不能访问类public成员return 0;
}

d7137cceb740440b8e3327a29eff7c2f.png

总结

在类的内部,派生类无论是以哪种方式继承基类,都不能访问基类的private成员,而基类的public成员、protected成员可以被派生类访问

在类的外部,首先需要明确的是无论是基类还是派生类的private成员和protected成员都是不能直接被访问的。而public继承的方式可以通过派生类的对象访问基类的public成员,而private继承的方式和protected继承的方式中,却不可以通过派生类的对象访问基类的public成员(可以这么理解此时基类的public成员分别成了派生类的私有成员和保护成员)

下图辅助理解

229a0b6d10214a0293e3da56fc85412c.png

61b3089280d344f7b93ebc88013aeebc.png

3.继承的对象模型

718f57d5558447ff9c44c839bc476a4c.png

我们发现B类的大小是16个字节,但是我们之前继承方式当中不是说基类的私有成员派生类不能访问吗?

其实父类中所有非静态成员都会被子类继承,父类中的私有成员属性其实是被编译器隐藏了,因此访问不到,但是确实被继承了下来,下面我们来验证一下

首先我们先打开这个工具(开发人员命令提示符)

613ae8d3be7d47468f5014d2bd15da21.png

指令输入步骤

c5e6a59d0159496aa4ac63dc2c785674.png

最终呈现结果

ca3ffc87bc904cbcab43d77949454264.png

经过验证是不是更可靠了呢

4.派生类的构造函数和析构函数

4.1构造函数

派生类不继承基类的构造函数,在声明派生类时一般应定义自己的构造函数db9b43127fe74ab58bb63cad00b80c4a.png

注意

派生类构造函数的总参数表中的参数,应当包括调用基类构造函数所需的参数 派生类构造函数的执行过程是,先调用基类构造函数初始化基类成员,然后对新增成员初始化

#include<iostream>
#include<string>
using namespace std;class Person//基类
{
protected:string _name;//姓名char _sex;//性别int _age;//年龄
public://Person构造函数Person(string name,char sex,int age):_name(name),_sex(sex),_age(age){}void PersonPrint(){cout << "name:" << _name << endl;cout << "sex:" << _sex << endl;cout << "age:" << _age << endl;}
};
//派生类
class Student :public Person
{
private:int _num;//学号
public://Student构造函数Student(string name,char sex,int age,int num):Person(name,sex,age),_num(num){}void StudentPrint(){PersonPrint();cout << "num:" << _num << endl;}
};
int main()
{Student s("liming", 'M', 18, 1001);s.StudentPrint();return 0;
}

0e4eca5fe1384ebfac822be5744e520e.png

4.1.1有子对象的派生类的构造函数

在学习结构体时,我们讲到一个结构体的成员还可以是个结构体变量。 派生类也可以有子对象(类类型的成员变量)

d022ad16d4204a1784e9c59e36e682c3.png

#include<iostream>
#include<string>
using namespace std;class Person//基类
{
protected:string _name;//姓名char _sex;//性别int _age;//年龄
public://Person构造函数Person(string name,char sex,int age):_name(name),_sex(sex),_age(age){}void PersonPrint(){cout << "name:" << _name << endl;cout << "sex:" << _sex << endl;cout << "age:" << _age << endl;}
};
//派生类
class Student :public Person
{
private:Person _teacher;//班主任(子对象)int _num;//学号
public://Student构造函数Student(string name,char sex,int age,int num,const Person& teacher):Person(name,sex,age),_num(num),_teacher(teacher){}void StudentPrint(){PersonPrint();//打印学生的信息cout << "num:" << _num << endl;_teacher.PersonPrint();//打印老师的信息}
};
int main()
{Person teacher("zhaoli", 'F', 38);//班主任Student s("liming", 'M', 18, 1001, teacher);//学生s.StudentPrint();return 0;
}

5f8537b22c4d4a9685933a5698270833.png

基类构造函数和子对象的书写顺序可以任意

这里有子对象的派生类的构造函数还有另外两种写法

写法一

这种不如引用传参更安全,效率高

e5f8dd2afd1d4aee805a8f580180ecd6.png

写法二

这种写法相对比较麻烦,写的形参更多了

919d454d00094125ad877a5ca1a79b77.png

4.1.2派生类构造函数的执行顺序

结论:

先调用基类构造函数,对基类数据成员初始化

再调用子对象类的构造函数,对子对象的数据成员初始化

最后执行派生类构造函数体中的语句,对派生类新增数据成员初始化

验证

4a11cf824b454f17b58aa4f654f5c23b.png

4.2析构函数

1.派生类不继承基类的析构函数,在声明派生类时,应当定义自己的析构函数

2.派生类的析构函数只对新增成员进行清理工作,基类、子对象的清理工作仍由它们各自的析构函数负责。

3.在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,分别对基类和子对象进行清理

4.析构函数的执行顺序与构造函数正好相反 先执行派生类自己的析构函数,对派生类新增成员进行清理; 然后调用子对象的析构函数,对子对象进行清理; 最后调用基类的析构函数,对基类进行清理。

析构顺序验证

3c98b196292840a9b1421176e8e4185f.png

我们发现Person类只构造了两次,居然析构了三次,这是为什么呢?

其实是因为默认的Person类拷贝构造函数,现在我们显示写一下他的拷贝构造函数来验证一下

#include<iostream>
#include<string>
using namespace std;
class Person//基类
{
protected:string _name;//姓名char _sex;//性别int _age;//年龄
public:void PersonPrint(){cout << "name:" << _name << endl;cout << "sex:" << _sex << endl;cout << "age:" << _age << endl;}//Person构造函数Person(string name,char sex,int age) :_name(name), _sex(sex), _age(age){cout << "Person构造" << endl;}//Person拷贝构造Person(const Person& p){_name = p._name;_sex = p._sex;_age = p._age;cout << "Person拷贝构造" << endl;}~Person(){cout << "~Person析构" << endl;}
};
class Student :public Person
{
private:Person _teacher;//班主任(子对象)int _num;//学号
public://Student构造函数Student(string name, char sex, int age, int num, const Person& teacher):Person(name, sex, age), _num(num), _teacher(teacher){cout << "Student构造" << endl;}void StudentPrint(){PersonPrint();//打印学生的信息cout << "num:" << _num << endl;_teacher.PersonPrint();//打印老师的信息}~Student(){cout << "~Student析构" << endl;}
};
int main()
{Person teacher("zhaoli", 'F', 38);//班主任cout << "*******************************" << endl;Student s("liming", 'M', 18, 1001, teacher);//学生//s.StudentPrint();return 0;
}

c0dbabeb58454ec597c6c888bc388ff3.png

那么谁是拷贝构造的呢?

第一个构造的是Person类的班主任对象,第二个则是调用基类的构造,第三个构造的是子对象是拷贝构造

5.同名成员的处理

假如在A类(基类)和B类(派生类)中有同名数据成员m,同名函数print(),那在类外面访问他们的时候会如果我们想调用A类中的函数print()和访问A类数据成员m,可以通过创建一个A类的对象,通过对象来访问。

但是既然A类被B类通过公共继承的方式继承了,那么A类的公有数据成员m和函数print(),也被继承了,但是B类(派生类)中有同名数据成员m,同名函数print()。此时如果我们建一个B类的对象,访问数据成员m和调用函数print(),会产生二义性吗(冲突)

一起来看一下

4f8b69051908420a8dd0f250449d3442.png

证明了同名成员也被继承

3d01202794044f4db0de8503b6f8be8e.png

我们发现B类的对象访问的都是B类中的数据成员m、函数print(),A类的数据成员m、函数print()并没有被访问,也没有产生二义性,其实派生类会隐藏基类的同名成员,那我们怎么样才能通过派生类的对象,访问基类成员和成员函数呢?

其实很简单只需要加类名::即可

eef60cdeb43240a3aa4f6e57f49053ee.png

总结: 
1.子类对象可以直接访问到子类中同名成员
2.子类对象加作用域可以访问到父类同名成员
3.当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

补充

继承同名静态成员处理方式


问:继承中同名的静态成员在子类对象上如何进行访问?


静态成员和非静态成员出现同名,处理方式一致
·访问子类同名成员直接访问即可
·访问父类同名成员需要加作用域

6.多继承

6.1语法

C++允许一个类继承多个类
语法:class子类:继承方式 父类1,继承方式 父类2......
多继承可能会引发父类中有同名成员出现,需要加作用域区分

0694e9b2077c4bb39a5f2eb3791f1850.png

6.2多继承派生类的构造函数

2389e86628e247aca239d1d149b5a01d.png

注意:

⑴初始化表中基类构造函数的排列顺序任意;

⑵派生类D的构造函数的执行顺序是先调用基类的构造函数,再执行派生类构造函数的函数体;

⑶调用基类的构造函数的顺序是按照声明派生类时基类出现的顺序。

示例:

032aa8f72e29449a84869b1e7b1c72a9.png7182b9776b0b446a957b5de5a82d9aad.png

7.菱形继承

如图,先声明A类,然后由它派生出B1类、B2类,D类同时继承了B1类、B2类

当菱形继承,两个父类拥有相同数据,需要加以作用域区分

c0c0dcd56a1c4e959608ee01693d3b55.png

2f312277cb71472a9f1cfd226f1f48fb.png

da2652fcb68d4381bca5f6444867e01e.png

A类中的数据成员a这份数据我们知道只有有一份就可以,菱形继承导致数据有两份,造成了资源浪费,该如何解决呢?

利用虚继承可以解决,在继承方式之前 加上关键字virtual即可,此时的基类称为虚基类

f198952b5063472793091b605fc54f5e.png

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Yolov9比其他yolo版本的改进
  • 设计与实现完整的余额充值系统
  • MySQL之多表查询—列子查询
  • python后端结合uniapp与uview组件tabs,实现自定义导航按钮与小标签颜色控制
  • 谷歌google play上架
  • 淘宝扭蛋机小程序,扭蛋市场创新模式
  • 【recast-navigation-js】使用three.js辅助绘制Agent寻路路径
  • php质量工具系列之PHPCPD
  • 论文中eps格式图片制作
  • 如何利用langchian调用百度大模型API
  • c++进阶——继承
  • 数据库管理工具——DBeaver简介
  • 51单片机独立按键控制LED灯,按键按一次亮,再按一次灭
  • Jira的原理及应用详解(五)
  • 基于Python+Flask框架实现的新冠疫情可视化的设计与实现
  • javascript数组去重/查找/插入/删除
  • Linux快速复制或删除大量小文件
  • ucore操作系统实验笔记 - 重新理解中断
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • 闭包--闭包作用之保存(一)
  • 聊聊spring cloud的LoadBalancerAutoConfiguration
  • 扑朔迷离的属性和特性【彻底弄清】
  • 实现菜单下拉伸展折叠效果demo
  • 小而合理的前端理论:rscss和rsjs
  • 一个项目push到多个远程Git仓库
  • 自制字幕遮挡器
  • AI又要和人类“对打”,Deepmind宣布《星战Ⅱ》即将开始 ...
  • Spring第一个helloWorld
  • ​【数据结构与算法】冒泡排序:简单易懂的排序算法解析
  • ​什么是bug?bug的源头在哪里?
  • (C#)一个最简单的链表类
  • (八)Spring源码解析:Spring MVC
  • (附源码)springboot掌上博客系统 毕业设计063131
  • (十一)手动添加用户和文件的特殊权限
  • (一)使用IDEA创建Maven项目和Maven使用入门(配图详解)
  • (转)nsfocus-绿盟科技笔试题目
  • (转)用.Net的File控件上传文件的解决方案
  • (转载)从 Java 代码到 Java 堆
  • .NET MVC、 WebAPI、 WebService【ws】、NVVM、WCF、Remoting
  • .Net MVC4 上传大文件,并保存表单
  • .NET/C# 利用 Walterlv.WeakEvents 高性能地中转一个自定义的弱事件(可让任意 CLR 事件成为弱事件)
  • .NET/C# 推荐一个我设计的缓存类型(适合缓存反射等耗性能的操作,附用法)
  • .NET_WebForm_layui控件使用及与webform联合使用
  • .net反编译的九款神器
  • .Net面试题4
  • .NET项目中存在多个web.config文件时的加载顺序
  • .NET值类型变量“活”在哪?
  • .NET中分布式服务
  • @NotNull、@NotEmpty 和 @NotBlank 区别
  • [2019/05/17]解决springboot测试List接口时JSON传参异常
  • [2021ICPC济南 L] Strange Series (Bell 数 多项式exp)
  • [AAuto]给百宝箱增加娱乐功能
  • [AIGC] Java List接口详解
  • [BZOJ2208][Jsoi2010]连通数
  • [C#]DataTable常用操作总结【转】