继承、菱形继承与虚拟继承

news2024/10/7 6:48:50

继承、菱形继承与虚拟继承

  • 一、概念
  • 二、定义格式
  • 三、继承方式
  • 四、派生类继承基类成员访问方式的变化
  • 五、基类和派生类对象赋值转换
    • 1、概念
    • 2、示意图
    • 3、示例代码
    • 4、特点
  • 六、继承中的作用域
    • 1、概念
    • 2、示例代码
    • 3、运行结果
  • 七、派生类的默认成员函数
    • 1、调用方法
    • 2、示例代码
    • 3、运行结果
  • 八、友元关系不能继承
    • 1、代码
    • 2、注意
    • 3、运行结果
    • 4、错误代码与编译器报错
  • 九、继承类别
    • 1、单继承
      • (1)概念
      • (2)示意图
    • 2、多继承
      • (1)概念
      • (2)示意图
    • 3、菱形继承
      • (1)概念
      • (2)示意图
      • (3)缺点
      • (4)示例代码
      • (5)调试结果
      • (6)不显式指定时编译器报的错误
    • 4、虚拟继承
      • (1)定义与作用
      • (2)代码
      • (3)数据冗余代码
      • (4)调试内存窗口
      • (5)虚拟继承
      • (6)调试内存窗口
    • 5、总结

一、概念

  • 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,是C++三大特性(封装、继承、多态)的其中一种,同时也是类设计层次的复用。
  • 继承允许程序员在保持原有类特性的基础上进行扩展,增加功能。通过这种方法产生出的新类称为派生类,也叫子类;原有类称为基类,也叫父类。
  • 继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

二、定义格式

在这里插入图片描述

三、继承方式

在这里插入图片描述

  • 继承方式一般使用公有继承或者保护继承,不太会使用私有继承。

四、派生类继承基类成员访问方式的变化

在这里插入图片描述

  • 派生类无论以什么方式继承基类,基类中被private限定符限定的成员,虽然会被继承到派生类中,但派生类不管在类内还是类外都无法访问它。
  • 如果基类的成员不想在基类以外直接被访问,又想在继承它的派生类中能被访问,则该基类的成员就需要用protected限定符限定。
  • 基类的成员在子类的访问方式为成员在基类的访问限定符和继承方式中,最小权限的那个。访问权限从大到小分别为public、protected、private。
  • 虽然关键字class和struct分别有默认的继承方式private和public,不过最好显式地写出继承方式。
  • 因为用protected或者private继承方式继承下来的基类成员都只能在派生类的类里面使用,实际中扩展维护性不强。所以,在实际运用中一般使用的都是public继承方式继承,几乎很少和不提倡使用protected或者private继承方式继承。

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

1、概念

  • 派生类对象可以赋值给基类的对象、指针、引用。这种赋值方式可以形象地称为切片或者切割,即把派生类中父类具有的那部分切割出来并赋值过去或者指向那块空间。
  • 基类对象不能赋值给派生类对象,因为派生类中可能会有基类没有的成员,而用基类赋值给派生类时,那部分成员就没有被赋值或者说没被初始化。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但必须是基类的指针指向派生类对象时才是安全的。

2、示意图

在这里插入图片描述

3、示例代码

class Person
{
protected:
	string _name = "snowdragon";
	size_t _age = 18;
};

class Student :public Person
{
public:
	string _id;
};

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

	//s = p;

	Student* ps1 = (Student*)pp;
	ps1->_id = "123";
	cout << ps1->_id << endl;

	pp = &p;
	Student* ps2 = (Student*)pp;
	
	//下方为越界访问
	//ps2->_id = "123";
	//cout << ps2->_id << endl;

	return 0;
}

4、特点

  • 派生类对象可以赋值给基类对象以及基类对象的指针和引用。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针可以通过强制类型转换赋值给派生类的指针,如果基类指针指向的是派生类对象,访问不会发生错误;如果基类指针指向的是基类对象,将会存在越界访问的问题。因为派生类中可能有基类没有的成员,而用派生类的指针访问这些成员就是越界访问,在运行时编译器会奔溃。

六、继承中的作用域

1、概念

  • 在继承体系中基类和派生类都有各自独立的作用域。
  • 派生类和基类中有同名成员函数时,派生类中的该成员函数将屏蔽基类对该同名成员函数的直接访问,这种情况称为隐藏,也称为重定义。但在派生类成员函数中,可以使用基类::基类成员显式地进行访问。
  • 如果是成员函数的隐藏,只需要函数名相同就行。

2、示例代码

class Person
{
public:
	void Func()
	{
		cout << "Person::void Func()" << endl;
	}
protected:
	string _name = "snowdragon";
	size_t _age = 18;
};

class Student :public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "年龄:" << _age << endl;
		cout << "id:" << _id << endl;
	}

	void Func()
	{
		Person::Func();
		cout << "Student::void Func()" << endl;
	}
protected:
	string _name = "snow";
	string _id = "123";
};

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

3、运行结果

在这里插入图片描述

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

1、调用方法

  • 派生类的构造函数必须调用基类的构造函数初始化基类所具有的成员,即派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式地调用基类的构造函数。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成对象中基类所具有的成员的拷贝初始化。
  • 派生类的赋值运算符重载(operator=)必须调用基类的赋值运算符重载(operator=)完成对象中基类所具有的成员的赋值。
  • 为了保证派生类对象先清理派生类成员再清理基类成员,派生类的析构函数会在被调用完成后自动调用基类的析构函数去清理对应的基类成员,即派生类对象进行析构清理时,会先调用派生类的析构函数再调用基类的析构函数。

2、示例代码

class Person
{
public:
	Person(const char* name = "snowdragon")
		:_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 = "snow", const char* id = "123")
		:Person(name)
		,_id(id)
	{
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		:Person(s)
		,_id(s._id)							//不能在函数体内赋值
	{
		cout << "Student(const Student& s)" << endl;
	}
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& p)" << endl;
		if (this != &s)
		{
			Person::operator=(s);
			_id = s._id;
		}
		return *this;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	string _id;
};

int main()
{
	Student s1("dragon", "18");
	cout << endl;

	Student s2(s1);
	cout << endl;

	Student s3("snowdragon", "19");
	cout << endl;

	s1 = s3;
	cout << endl;

	return 0;
}

3、运行结果

在这里插入图片描述

八、友元关系不能继承

1、代码

class Student;
class Person
{
public:
	friend void Print(const Person& p, const Student& s);
protected:
	string _name = "snowdragon";
private:
	string _sex = "man";
};

class Student :public Person
{
public:
	string _hairColor = "black";
protected:
	string _id = "123";
private:
	size_t _age = 18;
};
void Print(const Person& p, const Student& s)
{
	cout << "姓名:" << p._name << endl;
	cout << "性别:" << p._sex << endl;
	cout << "头发颜色:" << s._hairColor << endl;
	/*cout << "id:" << s._id << endl;
	cout << "年龄:" << s._age << endl;*/
}
int main()
{
	Person p;
	Student s;
	Print(p, s);
	return 0;
}

2、注意

  • 基类友元函数不能访问子类用私有和保护限定符限定的成员,但因为它是基类的友元,所以它能访问基类的成员。
  • 因为上方代码中基类友元函数的参数要用到派生类Student,而派生类在友元函数声明语句的后面才定义,所以需要在友元函数的声明之前,即基类前声明派生类。

3、运行结果

在这里插入图片描述

4、错误代码与编译器报错

在这里插入图片描述

九、继承类别

1、单继承

(1)概念

对于继承关系,一个派生类只有一个直接的基类。

(2)示意图

在这里插入图片描述

2、多继承

(1)概念

对于继承关系,一个派生类同时继承多个(两个或以上)基类。

(2)示意图

在这里插入图片描述

3、菱形继承

(1)概念

属于多继承的一种特殊情况,两个不同的派生类继承于同一个基类,又有一个派生类继承于这个两个派生类。

(2)示意图

在这里插入图片描述

(3)缺点

  • 使用菱形继承有数据冗余和二义性的缺点,即最后一个派生类C会继承两份基类sd的成员。
  • 显式指定访问基类的成员可以解决二义性问题,但数据冗余问题无法解决。

(4)示例代码

class Person
{
public:
	string _p = "snowdragon";
};
class PerDer1 :public Person
{
protected:
	string _pd1 = "snow";
};
class PerDer2 :public Person
{
protected:
	string _pd2 = "dragon";
};
class Derive :public PerDer1, public PerDer2
{
protected:
	string _d = "snowdragon";
};

int main()
{
	Derive d;
	//d._p = "sd";
	d.PerDer1::_p = "snow sd";
	d.PerDer2::_p = "sd dragon";
	return 0;
}

(5)调试结果

在这里插入图片描述

(6)不显式指定时编译器报的错误

在这里插入图片描述

4、虚拟继承

(1)定义与作用

  • 在派生类继承时,在继承方式前添加virtual即为虚继承,作用为解决菱形继承的二义性和数据冗余问题。
  • 当用继承关系中最后的派生类访问最初的基类的成员时,所有对象访问它时,访问到的都是同一个,即任一对象访问并修改该成员时,该成员的数据都会改变。

(2)代码

class Person
{
public:
	string _p = "snowdragon";
};
class PerDer1 :virtual public Person
{
protected:
	string _pd1 = "snow";
};
class PerDer2 :virtual public Person
{
protected:
	string _pd2 = "dragon";
};
class Derive :public PerDer1, public PerDer2
{
protected:
	string _d = "snowdragon";
};

int main()
{
	Derive d;
	d._p = "sd";
	d.PerDer1::_p = "snow sd";
	d.PerDer2::_p = "sd dragon";
	return 0;
}

(3)数据冗余代码

class Person
{
public:
	int _p;
};

class PerDer1 :public Person
//class PerDer1 :virtual public Person
{
public:
	int _pd1;
};

class PerDer2 :public Person
//class PerDer2 :virtual public Person
{
public:
	int _pd2;
};

class Derive :public PerDer1, public PerDer2
{
public:
	int _d;
};

int main()
{
	Derive d;
	d.PerDer1::_p = 1;
	d.PerDer2::_p = 2;
	d._pd1 = 3;
	d._pd2 = 4;
	d._d = 5;

	return 0;
}

(4)调试内存窗口

在这里插入图片描述

  • 地址后面的框中的数字为对象d的地址。
  • 基类成员_p有两份,第一份为类PerDer1 继承的,第二份为类PerDer2继承的。

(5)虚拟继承

  • 下方为上方数据冗余代码的注释部分,即将类名部分的注释,再将注释部分展开。
  • Derive 对象中将Person成员放到的了对象组成的最下面,而Person的成员同时属于PerDer1 和PerDer2。
  • PerDer1 和PerDer2 通过各自的虚基表指针,指向各自的虚基表。虚基表中存有偏移量,通过这个偏移量就可以从当前位置找到下面Person的成员。

(6)调试内存窗口

在这里插入图片描述

5、总结

  • 因为C++有多继承,进而就有了菱形继承,而有了菱形继承就会有菱形虚拟继承。使用它们会使底层实现变得很复杂。所以一般不建议设计出多继承,否则在复杂度和性能上都会有问题。
  • public继承是一种is-a的关系。即每个派生类对象都是一个基类对象或者基类扩展后的对象,而在基类中改动被protected和private限定符限定的成员时,可能会影响派生类,即耦合度高。
  • 组合是一种has-a的关系。如果B组合了A,则每个B对象中都会有一个A对象。简单点说就是在B类中定义了一个A类的对象。而在基类中改动被protected和private限定符限定的成员时,可能不会影响到派生类,即耦合度低。
  • 继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。虽然在继承方式中,基类的内部细节对子类可见,但也在一定程度上破坏了基类的封装。所以,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类的封装。
  • 因为要遵循高内聚低耦合的设计理念,所以,类之间的关系可以用组合时,就用组合;不能用组合时,再考虑用继承。因为组合的耦合度低,代码维护性好。

本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请务必点赞、收藏加关注💕💕💕

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

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

相关文章

【LeetCode刷题-排序】--147.对链表进行插入排序

147.对链表进行插入排序 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; }…

必须收藏:IPv6核心知识梳理!!(原理+基础配置)

一、概述 由于NAT技术的应用&#xff0c;缓解了IPv4地址不足产生的问题&#xff0c;但是部署IPv6是解决IPv4地址不足的最终方案。当前世界上不同地区对部署IPv6的需求强烈程度不一&#xff0c;且当前IPv4网络仍然占主流地位&#xff0c;因此短时间内IPv6和IPv4将会共存。 IPv4网…

深入理解网络IO复用并发模型

本文主要介绍服务端对于网络并发模型以及Linux系统下常见的网络IO复用并发模型。文章内容一共分为两个部分。 第一部分主要介绍网络并发中的一些基本概念以及我们Linux下常见的原生IO复用系统调用&#xff08;epoll/select&#xff09;等。第二部分主要介绍并发场景下常见的网…

opencv dnn模块 示例(21) 目标检测 object_detection 之 yolov6

文章目录 1、YOLOv6介绍1.1、概述1.2、关键技术1.2.0、网络结构1.2.1、表征能力更强的 RepBi-PAN Neck 网络1.2.2、全新的锚点辅助训练&#xff08;Anchor-Aided Training&#xff09;策略1.2.3、无痛涨点的 DLD 解耦定位蒸馏策略 1.3、总结 2、测试2.1、官方项目测试2.2、open…

View绘制流程

在子线程中不能更新UI的前提是不触发 checkThread ,逐步委托给mParent检查线程 onCreate加载contentView 进行draw onStart onResume 可能也没有完成测量流程 setContentView: public abstract void setContentView(LayoutRes int resId); Activity 是由ActivityThread类中…

Linux友人帐之网络编程基础DNS服务器

一、DNS服务器 1.1概述 DNS&#xff08;Domain Name System&#xff09;是一种分布式系统&#xff0c;用于将域名映射到IP地址。它是互联网上的基础设施之一&#xff0c;作为一种网络协议&#xff0c;它将域名转换为对应的IP地址。DNS的主要功能是将易于记忆的域名转换为计算机…

Brave Game(博弈论巴什博弈)

Problem - 1846 #include<bits/stdc.h> using namespace std; int t,n,m; signed main(){scanf("%d",&t);while(t--){scanf("%d%d",&n,&m);if(n%(m1)0) puts("second");else puts("first");}return 0; }

【带头学C++】----- 三、指针章 ---- 3.7 数组指针

3.7 数组指针 1.数组指针的概述 数组指针是一个指向数组的指针变量&#xff0c;是用来保存数组元素的地址。在C/C中&#xff0c;数组名代表了数组的首地址&#xff0c;可以被解释为一个指向数组第一个元素的指针。因此&#xff0c;一个指向数组的指针可以通过数组名来获…

安达发|APS智能优化排产软件的优先级应用

在制造业中&#xff0c;订单排产是一个重要的环节&#xff0c;它直接影响到生产效率、交货期和客户满意度。为了提高订单排产的效率和准确性&#xff0c;许多企业开始采用APS&#xff08;高级计划与排产&#xff09;智能优化排产软件。APS软件可以根据企业的生产资源、订单需求…

人大女王大学金融硕士项目——披星戴月走过的路,一定可以繁花满地

道路是曲折的&#xff0c;前途是光明的&#xff0c;在路上多花点时间到达才更有意义&#xff0c;作为身经百炼的职场人士&#xff0c;也难免遇到瓶颈期。这个时候&#xff0c;如果不改变自己&#xff0c;就很容易陷入焦虑。而在职读研便是一个很好的方式&#xff0c;一遍学习&a…

未来架构:无服务器计算和容器的融合

文章目录 无服务器计算&#xff1a;构建和扩展应用的新方式优势&#xff1a;示例代码&#xff1a; 容器技术&#xff1a;实现跨环境一致性优势&#xff1a;示例代码&#xff1a; 无服务器与容器的融合优势&#xff1a;示例代码&#xff1a; 未来架构的挑战和展望结论 &#x1f…

全球250米年度城区范围产品数据

简介 全球250米年度城区范围产品&#xff08;MGUP&#xff09;基于现有的城区产品CCI-LC、MCD12Q1和GlobeCover产品经样本清洗自动化得到可靠的多时序城区样本。前言 – 人工智能教程 将全球划分为5格网使用随机森林分类器对2001-2018年进行全球城区范围制图。经时空后处理后&…

记一次,kettle执行JOB的一个BUG,linux下环境表输入(查询)卡住问题,windows环境下执行正常。

问题 采集数据&#xff0c;linux下执行JOB时。 发现表输入卡在&#xff0c;查询卡住&#xff0c;如图&#xff0c;11:37:19、11:37:42、 11:42:56 分别在40000、60000行的时候卡住&#xff0c;23s、5分14s。 拿出sql到pl/sql上查询&#xff0c;并查看执行计划&#xff0c;均…

盘点10月Sui生态发展,了解Sui的近期成长历程!

10月是Web3的Uptober&#xff0c;整个行业在经历了深度的低潮期后迎来了一些回暖。Sui也紧跟行业脚步&#xff0c;举办了各类生态活动&#xff0c;包括Quset 3游戏主题的奖励活动、DeFi和zklogin专题Workshop & AMA、多平台发布的线上教育内容以及持续的科普文章输出。此外…

SPSS二项分布检验

前言&#xff1a; 本专栏参考教材为《SPSS22.0从入门到精通》&#xff0c;由于软件版本原因&#xff0c;部分内容有所改变&#xff0c;为适应软件版本的变化&#xff0c;特此创作此专栏便于大家学习。本专栏使用软件为&#xff1a;SPSS25.0 本专栏所有的数据文件请点击此链接下…

多测师肖sir___app测试_001

app测试 一、app测试分为两大类 app手工测试&#xff08;讲&#xff09; app自动化测试&#xff08;讲&#xff09; &#xff08;1&#xff09;手工app测试&#xff1f; 就是通过手点击app上的应用&#xff0c;cs架构上 &#xff08;2&#xff09;app自动化测试&#xff1f; 通…

*LEEDCODE 73矩阵置零

![在这里插入代码片](https://img-blog.csdnimg.cn/ab1d7d4b9d5046d8900de430249be3bf.png)1 0 0 替换两个列表 2 记录时 0 0 已经是半改好的状态

整理10个地推拉新app接单平台,免费一手推广渠道平台干货分享

1. 聚量推客&#xff1a; “聚量推客”汇聚了众多市场上有的和没有的地推网推拉新接单项目&#xff0c;目前比较火热&#xff0c;我们做地推和网推从业者如果长期在这行业去做推广可以使用这个平台&#xff0c;价格高数据也好&#xff0c;大部分拉新项目也都是官签一手资源 一…

nodejs express uniapp 图书借阅管理系统源码

开发环境及工具&#xff1a; nodejs&#xff0c;mysql5.7&#xff0c;HBuilder X&#xff0c;vscode&#xff08;webstorm&#xff09; 技术说明&#xff1a; nodejs express vue elementui uniapp 功能介绍&#xff1a; 用户端&#xff1a; 登录注册 首页显示轮播图&am…

shell脚本的一些测试和笔记总结

目录 1、cat <<EOF2、echo -e3、$#、 $、 $* 、 $?4、测试的demo 1、cat <<EOF cat命令表示查看&#xff0c;而cat <<EOF命令表示将进行输入&#xff0c;直到以EOF终止符来结束输入&#xff08;最后的新行&#xff09;。EOF必须写在一行的头部&#xff0c;…