C++---继承

news2024/11/19 1:41:27

继承

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

前言

在需要写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.coloradmin.cn/o/1018021.html

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

相关文章

嵌入式Linux驱动开发(I2C专题)(五)

I2C系统驱动程序模型 参考资料&#xff1a; Linux内核文档: Documentation\i2c\instantiating-devices.rstDocumentation\i2c\writing-clients.rst Linux内核驱动程序示例: drivers/eeprom/at24.c 1. I2C驱动程序的层次 I2C Core就是I2C核心层&#xff0c;它的作用&#xf…

20230916在WIN10的资源管理器里关闭最右边的预览窗口

20230916在WIN10的资源管理器里关闭最右边的预览窗口 百度搜索&#xff1a;WIN10 干掉预览模式 https://zhidao.baidu.com/question/1807564480928861867.html win10计算机预览窗口怎么关  我来答 首页 用户 合伙人 商城 法律 手机答题 我的 win10计算机预览窗口怎么关  …

知识库管理工具哪个好?我建议你可以试一下这个!

对于很多企业/用户来说&#xff0c;在职业成长和个人发展的过程中&#xff0c;是需要借助知识库管理工具来进行知识内容沉淀的。 随着工具市场的发展&#xff0c;各种知识库管理工具层出不穷&#xff0c;今天我就结合数据安全、知识管理体系、简单实用三个方面出发&#xff0c;…

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

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

day23集合02

1.泛型 1.1泛型概述 泛型的介绍 ​ 泛型是JDK5中引入的特性&#xff0c;它提供了编译时类型安全检测机制 泛型的好处 把运行时期的问题提前到了编译期间避免了强制类型转换 泛型的定义格式 <类型>: 指定一种类型的格式.尖括号里面可以任意书写,一般只写一个字母.例如:…

多输入多输出 | MATLAB实现PSO-BP粒子群优化BP神经网络多输入多输出

多输入多输出 | MATLAB实现PSO-BP粒子群优化BP神经网络多输入多输出 目录 多输入多输出 | MATLAB实现PSO-BP粒子群优化BP神经网络多输入多输出预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 Matlab实现PSO-BP粒子群优化BP神经网络多输入多输出预测 1.data为数据…

小程序引入vant-Weapp保姆级教程及安装过程的问题解决

小知识&#xff0c;大挑战&#xff01;本文正在参与“程序员必备小知识”创作活动。 本文同时参与 「掘力星计划」&#xff0c;赢取创作大礼包&#xff0c;挑战创作激励金 当你想在小程序里引入vant时&#xff0c;第一步&#xff1a;打开官方文档&#xff0c;第二步&#xff…

Linux C/C++实现SSL的应用层VPN (MiniVPN)

SSL协议和VPN&#xff08;虚拟私人网络&#xff09;原理是网络安全领域中的两个重要概念。 SSL协议&#xff0c;全称安全套接层&#xff08;Secure Sockets Layer&#xff09;&#xff0c;是一种广泛应用于互联网的安全协议&#xff0c;主要在两个通信端点之间建立安全连接&am…

深度解剖数据在栈中的应用

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大一&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 望小伙伴们点赞&#x1f44d;收藏✨加关注哟&#x1f495;&#x1…

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

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

红心向阳 百鸟朝凤

背景 最近在玩 folium 模块&#xff0c;基于使用过程中的一些个人体验&#xff0c;对 folium 进行了二次封装&#xff0c;开源在 GpsAndMap.在使用的过程中&#xff0c;发现在地图上打图标是可以进行旋转的。遇到就发现了一些有意思的玩法。 隔海的相望 下面的代码在地图 厦…

GIS跟踪监管系统信息查询

GIS跟踪监管系统信息查询 GIS跟踪监管系统&#xff08;1&#xff09;物资查询与展示。① 几何查询。代码说明&#xff1a;② 物资定位。• 单个物资定位&#xff1a;• 多个物资定位&#xff1a;③ 物资统计。&#xff08;2&#xff09;物资信息更新① 新增物资。 GIS跟踪监管系…

【项目经验】:elementui表格中数字汉字排序问题及字符串方法localeCompare()

一.需求 表格中数字汉字排序&#xff0c;数字按大小排列&#xff0c;汉字按拼音首字母&#xff08;A-Z&#xff09;排序。 二.用到的方法 第一步&#xff1a;把el-table-column上加上sortable"custom" <el-table-column prop"date" label"序号…

第七章 查找 一、查找的基本概念

一、基本概念 查找——在数据集合中寻找满足某种条件的数据元素的过程称为查找。 查找表(查找结构)——用于查找的数据集合称为查找表&#xff0c;它由同一类型的数据元素(或记录)组成。 关键字——数据元素中唯一标识该元素的某个数据项的值&#xff0c;使用基于关键字的查…

2023年的深度学习入门指南(27) - CUDA的汇编语言PTX与SASS

通过前面的学习&#xff0c;我们了解了在深度学习和大模型中&#xff0c;GPU的广泛应用。可以说&#xff0c;不用说没有GPU&#xff0c;就算是没有大显存和足够先进的架构&#xff0c;也没法开发大模型。 有的同学表示GPU很神秘&#xff0c;不知道它是怎么工作的。其实&#x…

Vue2.7 封装 Router@4 的 hook

1、问题 在 Vue2.7 中&#xff0c;尤大大是支持大部分 Vue3 的功能&#xff0c;并且支持使用 CompositionAPI 的写法&#xff0c;也支持 script setup 的便捷语法&#xff0c;但是 Vue2 对应的 Vue-router3 库并没有提供 hook 对应的支持&#xff0c;所以需要我们自行封装 Vue…

代码对比工具,都在这了

Git Diff Git是一个流行的分布式版本控制系统&#xff0c;它内置了代码对比功能。使用git diff命令可以比较两个不同版本的代码文件&#xff0c;也可以使用图形化的Git客户端进行可视化对比。 git diff 命令 | 菜鸟教程www.runoob.com/git/git-diff.html Diff diff是一个Un…

NAND价格第4季度回暖,现在是SSD入手时机吗?

这两天有粉丝后台在咨询购买SSD相关的问题。小编也好奇的搜下当前业内SSD品牌。不搜不知道&#xff0c;一搜吓一跳&#xff0c;将近200多个品牌。 那么&#xff0c;买SSD应该买什么品牌&#xff1f;现在是否可以入手SSD呢&#xff1f; 1.固态硬盘SSD的原理 我们首先了解下固态…

兄弟DCP-7080激光打印机硒鼓清零方法

兄弟DCP-708打印机清零方法?兄弟DCP-7080打印机的硒鼓计数器是用来记录硒鼓使用寿命的&#xff0c;当硒鼓使用寿命达到一定程度时&#xff0c;打印机会提示更换硒鼓。如果用户更换了硒鼓&#xff0c;但打印机仍提示需要更换&#xff0c;这时需要进行清零操作&#xff0c;详细请…

xen-gic初始化流程

xen-gic初始化流程 调试平台使用的是gic-600&#xff0c;建议参考下面的文档来阅读代码&#xff0c;搞清楚相关寄存器的功能。 《corelink_gic600_generic_interrupt_controller_technical_reference_manual_100336_0106_00_en》 《IHI0069H_gic_architecture_specification》…