【C++】你不得不爱的——继承

news2024/11/25 7:36:16

 凡是面向对象的语言,都有三大特性,继承,封装和多态,但并不是只有这三个特性,是因为者三个特性是最重要的特性,那今天我们一起来看继承!


目录

1.继承的概念及定义

1.概念

2.继承的定义

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

3.继承中的作用域

4.派生类的默认成员函数

1.构造和拷贝构造,赋值

2.析构函数的两怪!

5.继承与友元

6. 继承与静态成员

7.多继承

7.1继承分类

7.2 菱形继承 &&菱形虚拟继承

1.解决二义性的过程(指定作用域)

2.解决数据冗余(需要虚继承)

8.继承和组合(都是一种复用)

总结:


1.继承的概念及定义

1.概念

继承(inheritance)机制是面向对象程序设计 使代码可以复用的最重要的手段,它允许程序员在
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称 派生类。(子类)
继承 呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 承是类设计层次的复用。
那到底是什么意思呢?举个例子:
class Person
{
protected:
	char* _name;
	int _old;
};

class student :public Person
{
private:
	char* _id;
};
int main()
{
	student s;
}

当好多类都需要写Person类中的成员时 ,为了避免数据冗余,就可以使用类的继承,使代码复用,继承是让每一个派生类中,都有一份基类(父类)的成员。

2.继承的定义

继承的方式:(当然继承的目的就是为了让子类可以拥有父类的成员,并访问,所以一般情况下,我们只会进行公有继承:public:)

 那么我们来看一下,继承方式和访问之间的关系:

首先必须知道的一点是:基类中有私有成员时,子类中继承的父类的私有成员不可见。

不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。)

其次就是,基类成员的权限和继承方式的权限,谁的权限更小,在子类中继承的成员就是更小的那个权限。public > protected> private。

 如上图所示:


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

首先得回想起赋值转换这个过程:

不同的类型相互赋值时,中间会产生临时变量,通过临时变量进行赋值转换。

但是若子类和父类进行赋值交换时,并不产生中间的临时变量,而是天然的一个赋值。

(只能向上转换,即子类赋值给父类,字可以给父,父不可以给子)

看下面代码:

class Person
{
protected:
	string _name; // 姓名
	string _sex;  // 性别
public:
	int	_age;	 // 年龄
};

class Student : public Person
{
public:
	int _No; // 学号
};

int main()
{

	int i = 1;
	double d = 2.2;
	//中间会产生一个临时变量,临时变量具有常性,不可以改变。
    i = d;
	//所以此时ri是中间的临时变量的引用,而不是d的引用,如果不加const,就会放大权限
    const int& ri = d;


    //但父类和子类之间的赋值就不会产生中间的临时变量
    Person p;
	Student s;

	// 中间不存在类型转换,天然的一个赋值
	p = s;

	Person& rp = s;//对s的引用,可以访问和修改成员变量
	rp._age = 1;

	Person* ptrp = &s;
	ptrp->_age++;

	return 0;
}
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
切片的具体过程,我们画图来了解:

 

只能向上转换,即子类赋值给父类,字可以给父,父不可以给子


3.继承中的作用域

我们知道,一个类他就是一个域(作用域)。同一作用域不能定义同名的两个变量,但不同作用域它可以定义两个同名变量。所以父类,子类中都有同名的成员变量时,默认会自动访问子类的成员,因为就近原则,若想访问父类的成员,那就可以指定作用域!

同一作用域,定义两个同名函数,且参数不同叫做函数重载;

不同作用域,定义两个同名函数,叫做重定义;

1. 在继承体系中 基类派生类都有 独立的作用域
2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以 使用 基类::基类成员 显示访问
3. 需要注意的是如果是成员函数的隐藏, 只需要函数名相同就构成隐藏。
4. 注意在实际中在 继承体系里面最好 不要定义同名的成员

举例说明:

class A
{
public:
	void fun()
	{
		cout << "A::func()" << endl;
	}
};

class B : public A
{
public:
	void fun(int i)
	{
		cout << "B::func(int i)->" << i << endl;
	}
};

void Test()
{
	B b;
	b.fun(10);
};

这中情况,函数构成什么???函数重载?? 重定义(隐藏)??编译错误???

首先我们知道两个类中的同名函数,在不同作用域,这就构成了重定义(隐藏)!

若访问父类成员函数即:b.A::fun();


4.派生类的默认成员函数

1.构造和拷贝构造,赋值

先回顾一下,默认成员函数(无参,全缺省,编译器自己生成)

具体分析:

class Person  //父类
{
public:
	Person(const char* name) 
		: _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:
	Student(const char*name, int num)
//子类显示构造时,父类不可以直接访问进行初始化,必须调用父类自己的显示构造函数
		:Person(name)  
		, _num(num)
	{}

	Student(const Student& s)
//子类显示拷贝构造时,父类不可以直接访问进行初始化,必须调用父类自己的显示拷贝构造函数
		:Person(s)
		, _num(s._num)
	{}

	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
//赋值的运算符重载,两个同名函数构成了隐藏,需要指定作用域
			Person::operator=(s);
			_num = s._num;
		}

		return *this;
	}

protected:
	int _num; //学号
};

int main()
{
	Student s1("张三", 18);
	Student s2(s1);

	Student s3("李四", 20);

	s1 = s3;
	return 0;
}

最重要的一句话:父类成员必须调用父类自己的构造函数,拷贝构造完成初始化或拷贝。

或者说:子类中的父类那部分成员由父类自己的构造或者拷贝构造实现初始化或者拷贝。

2.析构函数的两怪!

直接看代码:

class Person
{
public:
	Person(const char* name = " ")
		: _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;
		delete[] p;
	}

protected:
	string _name; // 姓名

	int* p = new int[10];
};

class Student : public Person
{
public:
	Student(const char* name)
		:Person(name)
		, _num(1)
	{}

	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);
			_num = s._num;
		}
		return *this;
	}

	// 第一怪:1、子类析构函数和父类析构函数构成隐藏关系。(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
	// 第二怪:子类先析构,父类再析构。子类析构函数不需要显示调用父类析构,子类析构后会自动调用父类析构
	~Student()
	{
		//Person::~Person();

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

protected:
	int _num; //学号
};

int main()
{
	Student s("张三");

	return 0;
}

1、子类析构函数和父类析构函数构成隐藏关系。(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
2.子类先析构,父类再析构。子类析构函数不需要显示调用父类析构,子类析构后会自动调用父类析构

构造顺序:先父类,再子类;析构顺序:先子类,再父类。


5.继承与友元

友元关系不能继承!

 若子类对象也想访问友元函数,那只能在子类中也加上友元!(但不建议使用友元,会破坏继承关系)


6. 继承与静态成员

子类继承父类,不是继承父类这个对象,而是会有一份父类的模型。父类有的成员变量,子类也会有一份,互不干扰。

但静态成员就不一样了,他们是同一份;静态成员属于整个类和类的所有对象。同时也属于所有派生类及派生类的对象。

class Person
{
public:
	//friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
public:
	static int _num;
};
int Person::_num = 0;

class Student : public Person
{
protected:
	int _stuNum; // 学号
};

void test()
{
	Student s;
	Person p;
	cout << p._num << endl;
	p._num++;
	cout << p._num << endl;
	s._num++;
	cout << s._num << endl;
	cout << &s._num << endl;
	cout << &p._num << endl;
}

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例
 重点:

	Person* ptr = nullptr;
	ptr->Print();
	cout << ptr->_num << endl;
	cout << ptr->_name << endl;
	cout << (*ptr)._num << endl;
	(*ptr).Print();

对象里面只存成员变量,成员函数在代码段中,所以以上代码哪个不对呢?

我们知道空指针不能解引用,解引用意思是,这里是去访问指针指向对象的内部成员,那看一看哪个访问了内部的成员呢?

函数不在内部,在代码段,可以!

_num为对象内部成员变量,不能解引用访问,不可以!

(*ptr)是解引用了吗?我们不能凭借解引用符号来判断是否解引用,我们需要看内部的访问情况,(*ptr)->Print();并没有访问内部成员,可以!

(*ptr)->_num;也可以,_num是静态成员,不在成员里面。


7.多继承

7.1继承分类

单继承:一个子类只有一个直接父类

多继承:一个子类有两个或两个以上的父类

菱形继承:是多继承的一种特殊情况,会产生数据冗余和二义性!

 (person类的中的成员,会在student和teacher中都有一份,assistant继承student和teacher时,assistant中会有两份person,造成了数据冗余和二义性)

解决方法:

解决二义性:

可以通过访问限定符来指定访问哪一个成员。

那如何解决二义性的问题呢?

此时虚继承就上线了!

虚继承在腰部继承,谁引发的数据冗余,谁就进行虚继承(解决冗余)

 由此可见,加上virtual,变为虚继承以后,确实解决了数据的冗余

 那么到底如何解决的呢??具体下面分析!

7.2 菱形继承 &&菱形虚拟继承

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助 内存窗口观察对象成
员的模型。

1.解决二义性的过程(指定作用域)

(菱形继承)
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 = 1;
	d._c = 2;
	d._d = 3;
	d.B::_a = 4;
	d.C::_a = 5;
}

2.解决数据冗余(需要虚继承)

(菱形虚拟继承)

class A
{
public:
	int _a;
};

class B:virtual public A
{
public:
	int _b;
};

class C:virtual public A
{
public:
	int _c;
};

class D:public B,public C
{
public:
	int _d;
};

int main()
{
	D d;
	d._b = 1;
	d._c = 2;
	d._d = 3;
	d.B::_a = 4;
	d.C::_a = 5;
}

 那如果遇到这种情况呢???父子类的赋值转换(切片)

class A
{
public:
	int _a;
};

class B:virtual public A
{
public:
	int _b;
};

class C:virtual public A
{
public:
	int _c;
};

class D:public B,public C
{
public:
	int _d;
};

int main()
{
	D d;
	d._b = 1;
	d._c = 2;
	d._d = 3;
	d._a = 4;
	d._a = 5;

	B b;
	b._a = 1;
	b._b = 3;
	B* ptr = &b;
	ptr->_a = 2;

	ptr = &d;
	ptr->_a = 6;
}

从b对象可以看的出来,只要是虚继承以后,就会把虚基类放到最下面;

就像切片这种情况,ptr指向不同,那么距离虚基类的距离就不同,所以就必须要有虚基表的地址,来访问虚基表继而找到偏移量,然后访问到虚基类! 

我们通常使用下,很忌讳出现菱形继承,但可以多继承。

可以看得出,虚继承在时间上确实有损耗,过程比较复杂,但是如果虚基类比较大时,就可以很大程度上节省内存。


8.继承和组合(都是一种复用)

public继承是一种 is-a(是一个)的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种 has-a(有一个)的关系。假设B组合了A,每个B对象中都有一个A对象。
我们会说低耦合高内聚有,意思就是相互的联系比较小,不会因为改动一个,而很大的影响另一个;
在组合中,两个类中的成员变量一般都是私有,那么就无法访问,那么修改也不会相互影响到;
在继承中,因为要继承,所以父类成员一般子类都可以访问的,那么修改的话,彼此相互影响就比较大!
那么组合其实就是很好的低耦合。
就比如我们平时举例说到的:person,student,这就是继承关系,学生是一个人;
那再举一个,头有一双眼睛(这就是组合)
事实上,哪个适合就用哪个,都适合就先用组合!

总结:

一口气说了这么多,你学会了吗?细节还是比较多的,我们应该下去多多自己琢磨,反复调试,去感受过程,从而理解的更深刻!下期再见!

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

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

相关文章

Linux进程学习【进程地址】

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f38a;每篇一句&#xff1a; 图片来源 &#x1f383;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 Perseverance is not a long race; it is many short races one after another…

Dynabook笔记本电脑无法开机怎么重装新系统?

Dynabook笔记本电脑无法开机怎么重装新系统&#xff1f;有用户使用Dynabook笔记本电脑出现了无法正常开机的情况。遇到这样的问题是我们的电脑系统出现了损坏&#xff0c;可以尝试进行系统修复。如果无法修复的话&#xff0c;就需要进行系统重装了。以下为大家带来Dynabook笔记…

SQLMap安装教程

注意&#xff1a;在python3环境下安装sqlmap的时候会提示需要在python2的环境下才能安装&#xff0c;其实在python3.6以后也都支持sqlmap了。 sqlmap安装步骤&#xff1a; 一、下载python&#xff1b; 下载地址 https://www.python.org/downloads/ 下载教程参考&#xff08…

通过反射获取注解的属性值(内含源代码)

通过反射获取注解的属性值&#xff08;内含源代码&#xff09; 源代码下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87554543 目录通过反射获取注解的属性值&#xff08;内含源代码&#xff09;源代码下载链接地址&#xff1a;[https://downl…

做互联网自媒体创业的月薪收入真的能过万吗?

搞自媒体创业有前途吗&#xff1f;收入月薪过万是真的吗&#xff1f; 自媒体创业是一种新兴的创业方法&#xff0c;它的远景十分广阔。自媒体创业能够让人们在自己的兴趣爱好和专业范畴上发挥自己的才能&#xff0c;一起也能够获得不错的收入。可是&#xff0c;月薪过万并不是…

ArangoDB——AQL编辑器

AQL 编辑器 ArangoDB 的查询语言称为 AQL。AQL与关系数据库管理系统 (RDBMS)区别在于其更像一种编程语言&#xff0c;更自然地适合无模式模型&#xff0c;并使查询语言非常强大&#xff0c;同时保持易于读写。数据建模概念 数据库是集合的集合。集合存储记录&#xff0c;称为文…

三维人脸实践:基于Face3D的人脸生成、渲染与三维重建 <三>

face3d: Python tools for processing 3D face git code: https://github.com/yfeng95/face3d paper list: PaperWithCode 基于BFM模型&#xff0c;估计3DMM的参数&#xff0c;可以实现线性的人脸表征&#xff0c;该方法可用于基于关键点的人脸生成、位姿检测以及渲染等。推荐…

信息收集之搜索引擎

Google Hacking 也可以用百度&#xff0c;不过谷歌的搜索引擎更强大 site 功能&#xff1a;搜索指定域名的网页内容&#xff0c;可以用来搜索子域名、跟此域名相关的内容 示例&#xff1a; site:zhihu.com 搜索跟zhihu.com相关的网页“web安全” site:zhihu.com 搜索zhihu…

提升学习 Prompt 总结

NLP现有的四个阶段&#xff1a; 完全有监督机器学习完全有监督深度学习预训练&#xff1a;预训练 -> 微调 -> 预测提示学习&#xff1a;预训练 -> 提示 -> 预测 阶段1&#xff0c;word的本质是特征&#xff0c;即特征的选取、衍生、侧重上的针对性工程。 阶段2&…

C++核心编程

一、内存分区模型概述&#xff1a;C程序在执行时&#xff0c;将内存划分为4个区域程序运行前&#xff1a;代码区&#xff1a;存放函数体的二进制代码&#xff0c;由操作系统管理①共享。共享的目的是对于频繁被执行的程序&#xff0c;在内存中只需有一份代码即可②只读。使其只…

组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比

组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比 目录 组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比预测效果基本介绍模型描述程序设计参考资料预测效果 基本介绍 MATLAB实现EMD-KP

传输层协议 TCP UDP

目录 协议前菜 端口号 ​编辑端口号范围划分 认识知名端口号(Well-Know Port Number) netstat pidof 传输层协议 UDP协议 UDP协议端格式 UDP的特点 面向数据报 UDP的缓冲区 UDP使用注意事项 基于UDP的应用层协议 TCP协议 TCP协议概念 TCP协议段格式 标志…

深度分析中国高端投教市场第一股“九方财富”的投资价值

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;九方财富&#xff08;09636&#xff09;已于3月10在港交所成功IPO上市&#xff0c;并成为了“中国在线高端投教市场第一股”。 作为中国领先的在线投资决策方案提供商&#xff0c;九方财富…

一起来学习配置Combo接口吧!

Combo接口是一个光电复用的逻辑接口&#xff0c;一个Combo接口对应设备面板上一个GE电接口和一个GE光接口。电接口与其对应的光接口是光电复用关系&#xff0c;两者不能同时工作&#xff08;当激活其中一个接口时&#xff0c;另一个接口就自动处于禁用状态&#xff09;&#xf…

常用存储芯片-笔记本上固态硬盘PTS11系列推荐

在存储领域中&#xff0c;除了存储颗粒之外&#xff0c;还有一种极其重要的芯片&#xff1a;存储控制芯片。存储控制芯片是CPU与存储器之间数据交换的中介&#xff0c;决定了存储器最大容量、存取速度等多个重要参数。特别是在AI、5G、自动驾驶时代&#xff0c;对于数据处理及存…

2.HTML页面组成

html页面组成html简介-基础-元素html属性-标题-段落html链接-头部-图像html表格-列表-区块html表单-框架-颜色html字符实体-url前言&#xff1a; 在学习爬虫前&#xff0c;我们还需要了解HTML页面&#xff0c;学习它的组成部分以及各部分的意思和使用方法&#xff0c;代码我放在…

C++回顾(十八)—— 文件操作

18.1 I/O流概念和流类库结构 1 概念 程序的输入指的是从输入文件将数据传送给程序&#xff0c;程序的输出指的是从程序将数据传送给输出文件。 C输入输出包含以下三个方面的内容&#xff1a; &#xff08;1&#xff09;对系统指定的标准设备的输入和输出。即从键盘输入数据&am…

前端初学者 sdk 的使用

目录结构&#xff1a; package.json {"name": "ivanfor666","version": "1.0.0","description": "","main": "dist/index.cjs.js","module": "dist/index.esm.js",&quo…

Kafka 分区机制

Kafka 分区机制分区策略轮询策略随机策略按消息键保序策略基于地理位置的分区策略主题 (Topic) &#xff1a;承载真实数据的逻辑容器&#xff0c;主题下还分 n 个分区 Kafka 消息的三级结构&#xff1a; 主题 - 分区 - 消息主题下的每条消息只会保存在某个分区中&#xff0c;…

案例06-复用思想的接口和SQL

目录 一&#xff1a;背景介绍 二&#xff1a;思路&方案 三&#xff1a;过程 1.Controller层接口的复用 2.Mapper层sql语句的复用 四&#xff1a;总结 一&#xff1a;背景介绍 我们在开发项目的过程中非常容易出现的一种现象就是用什么我就直接写什么&#xff0c;就像我…