C++:继承(这一篇就够了)

C++:继承(这一篇就够了)

  • 一、继承的概念及定义
    • 1.1 继承的概念
    • 1.2 继承定义
      • 1.2.1定义格式
      • 1.2.2 继承关系和访问限定符
      • 1.2.3 继承基类成员访问方式的变化
  • 二、基类和派生类对象赋值转换
  • 三、继承中的作用域
  • 四、派生类的默认成员函数
  • 五、继承与静态成员
  • 六、复杂的菱形继承及菱形虚拟继承
    • 6.1 菱形继承的问题
    • 6.2 二义性结局办法(1)
    • 6.3 二义性结局办法(2)
    • 6.4 虚拟继承解决数据冗余和二义性的原理
  • 七、继承和组合

一、继承的概念及定义

1.1 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
【实例】:(简单看一下如何使用,具体后面会详细介绍)

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
 		cout << "age:" << _age << endl;
 	}
protected:
 	string _name = "peter"; // 姓名
 	int _age = 18;  // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
//这里体现出了Student和Teacher复用了Person的成员。
class Student : public Person
{
protected:
 	int _stuid; // 学号
};

int main()
{
	 Student s;
	 s.Print();
	 return 0;
}

1.2 继承定义

1.2.1定义格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
在这里插入图片描述

1.2.2 继承关系和访问限定符

在这里插入图片描述

1.2.3 继承基类成员访问方式的变化

继承基类成员访问方式的所有变化:基类的私有成员在子类都是不可见;基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
表格如下:

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

小tips:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  4. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
    .

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

我们知道类型相似的对象之间支持相互转换,并且中间会生成一个临时变量。比如:int和double都表示数,只是精度的问题罢了,所以支持赋值转换。
【实例】:

int main()
{
//为什么i加引用,不加const会报错.
//原因在于赋值转化时,首先会将变量d转化为int类型并将结果存在一个临时变量里
//所以这里引用绑定的对象实际是这个临时变量,所以必须加const,否则会编译报错。
	double d = 1.2;
	//int& i = d; //error
	const int& i = d;
	return 0;
}

那对于基类和派生类对象赋值转换也是这样吗?我们来看看下面这段代码:

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

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

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

我们运行这段代码发现可以正常通过,也从侧面说明基类和派生类对象之间的赋值转换不会产生临时变量。
其实在public继承,父类和子类是一个is-a关系;子类对象赋值给父类对象/父类指针/父类的引用,我们认为这是天然的,中间不产生临时变量,这个叫做父子类赋值兼容规则(或切割、切片)

在这里插入图片描述
小tips:

  1. 基类对象不能赋值给派生类对象。
  2. 类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。

三、继承中的作用域

继承中的作用域主要就注意以下这几点就可以了。

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

【实例1】:

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
	int _num = 111; //身份证
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号
};

void Test()
{
	Student s1;
	s1.Print();
};

【实例2】:

// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};

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

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

四、派生类的默认成员函数

6个默认成员函数,即我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。


【实例】:(下面主要演示4个主流的默认构造函数,至于其他2个基本没用)

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:

	//按照声明顺序完成构造,先调用基类的默认构造,在初始化其他成员
	//由于基类Person中有默认构造,所以也可以不显示调用
	Student(const char* name, int id)
		:_id(id)
		, Person(name)
	{
		cout << "Student(const char* name, int id)" << endl;
	}

	//派生类拷贝构造,基类的成员变量的拷贝构造必须调用基类的拷贝构造
	Student(const Student& s)
		:Person(s)
		, _id(s._id)
	{
		cout << "Student(const Student& s)" << endl;
	}

	//派生类赋值重载中的基类成员必须调用基类的赋值重载函数
	//由于派生类和基类的赋值重载构成隐藏,所以我们必须显示指定类域调用对应的赋值重载
	Student& operator=(const Student& s)
	{
		if (&s != this)
		{
			Person::operator=(s);//显示指定调用
			_id = s._id;
		}

		cout << "Student& operator=(const Student& s)" << endl;
		return *this;
	}

	// 由于多态的原因,析构函数统一会被处理成destructor
	// 父子类的析构函数构成隐藏
	// 为了保证析构安全,先子后父
	// 父类析构函数不需要显示调用,子类析构函数结束时会自动调用父类析构
	// 保证先子后父
	~Student()
	{
		//Person::~Person();
		cout << "~Student()" << endl;
	}

protected:
	int _id;
};

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

	Student s3("李四", 19);
	s1 = s3;


	return 0;
}

五、继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。静态成员和成员函数一样只有一份,子类只是继承了使用权。
【实例】:(统计定义了多少个对象,都是继承关系)
【分析】:我们可以定义一个静态变量,由于是继承关系,所有对象的默认构造是都需要调用子类的默认构造来初始化对象中子类的成员。所以我们可以在最开始的类中显示的写默认构造函数 + 定义静态变量,同时每运行一次说明创建了一个对象,将静态变量++即可。
【代码】:

class Person
{
public:
	Person() 
	{
		++_count; 
	}
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;//定义

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


int main()
{
	Student s1;
	Person p1;
	cout << " 人数 :" << Person::_count << endl;
	Student s2;
	Student s3;
	cout << " 人数 :" << Person::_count << endl;

	return 0;
}

在这里插入图片描述

六、复杂的菱形继承及菱形虚拟继承

继承分为单继承和多继承。而菱形继承是多继承的一直特殊情况,虚拟继承则是为了解决其中的一些问题孕育而生。
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
在这里插入图片描述
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
在这里插入图片描述
菱形继承:菱形继承是多继承的一种特殊情况。

在这里插入图片描述

6.1 菱形继承的问题

我们来看看这个继承类:

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成员会有两份。
同时由于二义性,当我们直接通过a._name = "xxx"去修改数据时,我们没法确定访问的是哪一个。但这有两种解决方式:

6.2 二义性结局办法(1)

要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。

a.Student::_name = "xxx";
a.Teacher::_name = "yyy";

6.3 二义性结局办法(2)

第二种就是虚拟继承,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承(在继承方式前加virtual关键字),即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
【实例】:

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; // 主修课程
};

void Test()
{
	Assistant a;
	a._name = "peter";
}

6.4 虚拟继承解决数据冗余和二义性的原理

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
接下来所有分析都基于以下代码:

class A
{
public:
	int _a;
};

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

 class C : public A
//class C : virtual 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放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
在这里插入图片描述
下面是上面的Person关系菱形虚拟继承的原理解释:
在这里插入图片描述
总结:多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

七、继承和组合

  1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  2. 优先使用对象组合,而不是类继承
  3. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  4. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  5. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

竞赛保研 基于情感分析的网络舆情热点分析系统

文章目录 0 前言1 课题背景2 数据处理3 文本情感分析3.1 情感分析-词库搭建3.2 文本情感分析实现3.3 建立情感倾向性分析模型 4 数据可视化工具4.1 django框架介绍4.2 ECharts 5 Django使用echarts进行可视化展示5.1 修改setting.py连接mysql数据库5.2 导入数据5.3 使用echarts…

C++正则表达式全攻略:从基础到高级应用

C正则表达式全攻略&#xff1a;从基础到高级应用 一、基础知识二、正则表达式的基本匹配三、C中使用正则表达式四、高级正则表达式五、实践示例六、性能优化6.1、编译正则表达式6.2、避免过度使用回溯6.3、优化匹配算法 七、总结 一、基础知识 正则表达式是一种用于匹配、搜索…

【如何选择Mysql服务器的CPU核数及内存大小】

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容&#x1f4e2;文章总结&#x1f4e5;博主目标 &#x1f50a;博主介绍 &#x1f31f;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作…

【数据结构-单链表】(C语言版本)

今天分享的是数据结构有关单链表的操作和实践&#xff08;图解法&#xff0c;图变化更利于理解&#xff09; 记录宗旨&#x1f4dd;&#xff1a; 眼&#xff08;脑&#xff09;过千遍&#xff0c;不如手过一遍。 我们都知道单链表是一种常见的链表数据结构&#xff0c;由一系列…

关于标准那些事——第六篇 四象之“白虎”(要素的编写)

两仪生四象——东方青龙&#xff08;木&#xff09;、西方白虎&#xff08;金&#xff09;、南方朱雀&#xff08;火&#xff09;、北方玄武&#xff08;水&#xff09; 分别对应标准编写之四象——层次的编写、要素的编写、要素的表述、格式的编排。 今天来分享一下 要素的编…

【零基础入门TypeScript】TypeScript - 环境设置

目录 本地环境设置 文本编辑器 TypeScript 编译器 安装 Node.js 在 Windows 上安装 在 Mac OS X 上安装 IDE支持 视觉工作室代码 在 Windows 上安装 在 Mac OS X 上安装 在 Linux 上安装 括号 括号的 TypeScript 扩展 var message:string "Hello World"…

如何使用Node.js快速创建本地HTTP服务器并实现公网访问服务端

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

WPF+Halcon 培训项目实战(8-9):WPF+Halcon初次开发

文章目录 前言相关链接项目专栏运行环境匹配图片WPF Halcon组件HSmartWindowControlWPF绑定读取图片运行代码运行结果 抖动问题解决运行结果 绘制矩形绘制图像会消失 绘制对象绑定事件拖动事件 前言 为了更好地去学习WPFHalcon&#xff0c;我决定去报个班学一下。原因无非是想…

试用了所有热门的报表工具,终于找到这款好用的报表工具,太赞了

经常有朋友向我咨询&#xff0c;有没有哪个报表工具既简单易上手&#xff0c;又适合绝大多数普通职场人使用。经过我的一番研究&#xff0c;我发现大家在选择报表工具时&#xff0c;主要关注以下3点&#xff1a; 1. 简单易上手&#xff1a;大家希望报表工具的学习门槛低&#…

怎样才能找到合适的产品说明书模板?方法献上

制作一份专业而吸引人的产品手册对于企业来说至关重要。然而&#xff0c;对于许多企业和个人而言&#xff0c;制作产品手册可能是一个挑战&#xff0c;因为需要一定的设计和排版能力。为了帮助大家更轻松地制作出优质的产品手册&#xff0c;下面将向大家推荐三款优秀的产品手册…

杭州默医宠物医院:猫咪应激,铲屎官一定要重视!

“猫咪应激”铲屎官们都略有耳闻&#xff0c;甚至自家猫主子也有出现过&#xff0c;但很多铲屎官对猫咪应激不重视。猫咪应激的程度可能远超出我们的想象。 一、猫应激的原因有以下方面&#xff1a; ①外出 ②搬家 ③洗澡 ④猫群不合 ⑤强迫猫咪做某些行为 ⑥主人的一惊…

jenkins+pytest+allure

jenkinspytestallure allure下载地址 Releases allure-framework/allure2 GitHub allure环境变量配置 allure --version 查看版本(确定是否配置完成) python安装allure插件 pip install allure-pytest pytest的运行指令 pytest -sv test_demo.py 开发完毕后将代码上传到…

算法训练day56|动态规划part16

583. 两个字符串的删除操作 逆向思路&#xff1a;求最长公共子序列&#xff0c;在用总长度-2*公共子序列长度 正向思路&#xff1a;删除多少 1. dp数组&#xff08;dp table&#xff09;以及下标的含义 dp[i][j]&#xff1a;以i-1为结尾的字符串word1&#xff0c;和以j-1位结…

重读VIT:深入探索细节与影响

1. 模型架构 提起一个新模型&#xff0c;我想大家最关心的事就是&#xff1a;它到底长什么样&#xff1f;输入输出是什么&#xff1f;我要怎么用&#xff1f;所以&#xff0c;我们先来看模型架构。 1.1 Bert结构 前面说过&#xff0c;VIT几乎和Bert一致&#xff0c;我们来速扫…

隐退三年,身价4800亿元的Google创始人出面只为给他写代码?

随着 Google 发布迄今为止最强大、最通用、最灵活的大模型 Gemini 以此正面叫板 ChatGPT 之际&#xff0c;近日不少人开始细细研读 Gemini 背后的技术详解论文&#xff08;https://arxiv.org/abs/2312.11805&#xff09;&#xff0c;这一看不打紧&#xff0c;竟有许多令人意外的…

mac中excel条件格式找到每一列的最大值并标红

假设现在excel有A1:R24组数据&#xff0c;最终效果如下 先选择要处理数据的第一列&#xff0c;然后点击【条件格式】-【新建规则】 style选择【classic】以及【Use a formula to determine which cells to format】&#xff0c;输入规则【C3MAX(C$3:C$24)】 注意这里C$3前面没…

ARM CCA机密计算硬件架构之内存管理

实施了TrustZone安全扩展的Arm A-profile处理器呈现两个物理地址空间(PAS): 非安全物理地址空间安全物理地址空间Realm管理扩展增加了两个PAS: Realm物理地址空间Root物理地址空间下图显示了这些物理地址空间以及如何在工作系统中实施这些空间: 正如表格所示,根状态能够访…

干洗店洗鞋店小程序核心功能有哪些?

在繁忙的生活中&#xff0c;我们的鞋子常常承载着风尘仆仆的故事。而洗鞋小程序&#xff0c;就是那个让您的鞋子焕然一新的魔法师。通过这个小程序&#xff0c;您可以在线预约、支付&#xff0c;查询洗鞋订单&#xff0c;并与洗鞋店铺进行互动&#xff0c;轻松享受专业的洗鞋服…

【嵌入式学习笔记-01】什么是UC,操作系统历史介绍,计算机系统分层,环境变量(PATH),错误

【嵌入式学习笔记】什么是UC&#xff0c;操作系统历史介绍&#xff0c;计算机系统分层&#xff0c;环境变量&#xff08;PATH&#xff09;&#xff0c;错误 文章目录 什么是UC?计算机系统分层什么是操作系统&#xff1f; 环境变量什么是环境变量&#xff1f;环境变量的添加&am…