深入浅出继承

news2024/11/27 15:45:57

目录

一、继承的概念

二、继承的定义

2.1 继承格式

2.2 继承方式与访问限定符

2.3 继承方式和访问限定符

2.4 默认继承方式

三、基类与派生类对象赋值转换

四、继承中的作用域

六、派生类默认成员函数

七、继承与友元

八、继承与静态成员


一、继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,其允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类被称为派生类
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。之前接触的复用都是函数复用,而继承便是类设计层次的复用

以下代码中Student类和Teacher类就继承了Person类:

#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
	void Print() {
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "bjy";
	int _age = 18;
};
class Student : public Person
{
protected:
	int _stuid = 04;
};
class Teacher : public Person
{
protected:
	int _jobid = 05;
};

继承后,父类Person的成员(成员函数和成员变量),都会变成子类的一部分。即子类Student和Teacher复用了父类Person成员

二、继承的定义

2.1 继承格式

在继承中,父类也称为基类,子类是由基类派生而来的,所以子类又称为派生类

2.2 继承方式与访问限定符

访问限定符有以下三种:

  1. public访问
  2. protected访问
  3. private访问

继承的方式有三种:

  1. public继承
  2. protected继承
  3. private继承

2.3 继承方式和访问限定符

基类中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类中后,该成员最终在派生类中的访问方式将会发生变化

  1. 在基类中的访问方式为public或protected的成员,在派生类中的访问方式变为:Min(成员在基类的访问方式,继承方式)
  2. 在基类中的访问方式为private的成员,在派生类中都是不可见的

基类的private成员在派生类中不可见是什么意思?

即无法在派生类中访问基类的private成员。如:虽然Student类继承了Person类,但无法在Student类中访问Person类中的private成员_name

基类的private成员无论以什么方式继承,在派生类中都是不可见的,这里的不可见是指基类的私有成员虽然被继承到了派生类对象中,但是语法上限制派生类对象不管在类内还是类外都不能访问

基类的private成员在派生类中是不能被访问的,若基类成员不想在类外直接被访问,但要在派生类中能访问,就需要定义为protected

注意: 在实际运用中一般使用的都是public继承,几乎很少使用protected和private继承,也不提倡使用protected和private继承,因为使用protected和private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

2.4 默认继承方式

在使用继承时可以不指定继承方式,使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public

//基类
class Person
{
public:
	string _name = "张三"; //姓名
};
//派生类
class Student : Person //默认为private继承
{
protected:
	int _stuid;   //学号
};
//基类
class Person
{
public:
	string _name = "张三"; //姓名
};
//派生类
struct Student : Person //默认为public继承
{
protected:
	int _stuid;   //学号
};

三、基类与派生类对象赋值转换

派生类对象可以赋值给基类的对象或引用,派生类的地址可以赋值给基类的指针。即赋值兼容规则,在这个过程中会发生基类和派生类对象间的赋值转换

//基类
class Person
{
protected:
	string _name; //姓名
	string _sex;  //性别
	int _age;     //年龄
};
//派生类
class Student : public Person
{
protected:
	int _stuid;   //学号
};
Student s;
Person p = s;     //派生类对象赋值给基类对象
Person* ptr = &s; //派生类对象的地址赋值给基类指针
Person& ref = s;  //派生类对象赋值给基类引用

这种做法被称为切片/切割,即为将派生类中基类那部分切来赋值过去

派生类对象赋值给基类对象图示

派生类对象赋值给基类指针图示

派生类对象赋值给基类引用图示

注意:基类对象不能赋值给派生类对象,基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但是基类的指针或引用必须是管理的派生类对象才是安全的

四、继承中的作用域

在继承体系中的基类和派生类都有独立的作用域。若子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问。这种情况被称为隐藏,也称为重定义

#include <iostream>
#include <string>
using namespace std;
//父类
class Person
{
protected:
	int _num = 111;
};
//子类
class Student : public Person
{
public:
	void fun()
	{
		cout << _num << endl;
	}
protected:
	int _num = 999;
};
int main()
{
	Student s;
	s.fun(); //999
	return 0;
}

此时若要访问父类中的_num成员,可以使用作用域限定符进行指定访问

void fun() {
	cout << Person::_num << endl; //指定访问父类作用域中的_num成员
}

若成员函数的隐藏,只需函数名相同就构成隐藏

对于以下代码,调用成员函数fun时将直接调用子类中的fun,若想调用父类中的fun,则需使用作用域限定符指定类域

#include <iostream>
#include <string>
using namespace std;
//父类
class Person
{
public:
	void fun(int x)
	{
		cout << x << endl;
	}
};
//子类
class Student : public Person
{
public:
	void fun(double x)
	{
		cout << x << endl;
	}
};
int main()
{
	Student s;
	s.fun(3.14);       //直接调用子类中的成员函数fun
	s.Person::fun(20); //指定调用父类中的成员函数fun
	return 0;
}

注意:该代码中,父类中的fun和子类中的fun不构成函数重载,函数重载要求两个函数在同一作用域,而此时这两个fun函数并不在同一作用域。为了避免类似问题,实际在继承体系中最好不要定义同名的成员

六、派生类默认成员函数

默认成员函数,即程序员不写编译器也会自动生成的函数,类中的默认成员函数有以下六个

以下面这个Person类为基类

//基类
class Person
{
public:
	//构造函数
	Person(const string& 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;
	}
private:
	string _name; //姓名
};

用Person基类派生出Student类,Student类中的默认成员函数的基本逻辑如下

//派生类
class Student : public Person
{
public:
	//构造函数
	Student(const string& name, int id)
		: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& s)" << endl;
		if (this != &s) {
			Person::operator=(s); //调用基类的operator=完成基类成员的赋值
			_id = s._id; //完成派生类成员的赋值
		}
		return *this;
	}
	//析构函数
	~Student() {
		cout << "~Student()" << endl;
		//派生类的析构函数会在被调用完成后自动调用基类的析构函数
	}
private:
	int _id; //学号
};

派生类与普通类的默认成员函数的不同之处概括为以下几点:

  1. 派生类的构造函数被调用时,会自动调用基类的构造函数初始化基类的那一部分成员。若基类中没有默认构造函数,则须在派生类构造函数的初始化列表中显示调用基类构造函数
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝构造
  3. 派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数完成基类成员的赋值
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员
  5. 派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数
  6. 派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数

在编写派生类的默认成员函数时,需要注意以下几点:

  1. 派生类和基类的赋值运算符重载函数因为函数名相同构成隐藏,因此在派生类中调用基类的赋值运算符重载函数时,需使用作用域限定符进行指定调用
  2. 由于多态的某些原因,任何类的析构函数名都会被统一处理为destructor();。因此,派生类和基类的析构函数也会因为函数名相同构成隐藏,若需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用
  3. 在 派生类的拷贝构造函数和operator=函数 中调用 基类的拷贝构造函数和operator=函数 的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用

说明:

  • 基类的构造函数、拷贝构造函数、赋值运算符重载函数都可以在派生类当中自行进行调用,而基类的析构函数是当派生类的析构函数被调用后由编译器自动调用的,若是自行调用基类的构造函数就会导致基类被析构多次的问题
  • 创建派生类对象时是先创建的基类成员再创建的派生类成员,编译器为了保证析构时先析构派生类成员再析构基类成员的顺序,所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数

七、继承与友元

友元关系不能继承,也就是说基类的友元可以访问基类的私有和保护成员,但是不能访问派生类的私有和保护成员(友元关系不可被继承)

以下代码中Display函数是基类Person的友元,但是Display函数不是派生类Student的友元,即Display函数无法访问派生类Student中的私有和保护成员

#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public:
	//声明Display是Person的友元
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; //姓名
};
class Student : public Person
{
protected:
	int _id; //学号
};
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;
}

若想让Display函数也能够访问派生类Student的私有和保护成员,只能在派生类Student中进行友元声明

class Student : public Person
{
public:
	//声明Display是Student的友元
	friend void Display(const Person& p, const Student& s);
protected:
	int _id; //学号
};

八、继承与静态成员

若基类中定义了一个static静态成员变量,则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类,都只有一个static成员实例

如:在基类Person中定义了静态成员变量_count,尽管Person又被派生类Student和Graduate继承,但在整个继承体系里面只有一个该静态成员
若是在基类Person的构造函数和拷贝构造函数中设置_count进行自增,那么就可以随时通过_count来获取该时刻已经实例化的Person、Student以及Graduate对象的总个数

#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
	Person() { _count++; }
	Person(const Person& p) { _count++; }
protected:
	string _name; //姓名
public:
	static int _count; //统计人的个数
};
int Person::_count = 0; //静态成员变量在类外进行初始化

//派生类
class Student : public Person
{
protected:
	int _stuNum; //学号
};
//派生类
class Graduate : public Person
{
protected:
	string _seminarCourse; //研究科目
};

int main()
{
	Student s1;
	Student s2(s1);
	Student s3;
	Graduate s4;
	cout << Person::_count << endl; //4
	cout << Student::_count << endl; //4
	return 0;
}

也可以通过打印Person类和Student类中静态成员_count的地址来证明就是同一个变量

cout << &Person::_count << endl; //00F1F320
cout << &Student::_count << endl; //00F1F320

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

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

相关文章

【c++】c++类的大小的计算和this指针

文章目录 1.类的大小如何计算&#xff1f;2.类内部的this指针3.this指针的特性 本文为作者关于c类学习过程中的小小总结 1.类的大小如何计算&#xff1f; c的类由成员变量和成员函数等组成&#xff0c;不同于c中的结构体只有成员变量&#xff0c;但类大小的计算方法和结构体的…

Redis中的Set类型

目录 set的相关命令 sadd smembers sismember scard spop smove srem 操作集合间的命令 sinter sinterstore sunion sunionstore sdiff sdiffstore 内部编码 set类型的应用场景 redis中的集合类型是保存多个字符串类型的元素的. 作为集合,有两个关键的特性:1…

CSS实现鼠标移至图片上显示遮罩层及文字效果

效果图&#xff1a; 1、将遮罩层html代码与图片放在一个div 我是放在 .proBK里。 <div class"proBK"><img src"../../assets/image/taskPro.png" class"proImg"><div class"imgText"><h5>用户在线发布任务&l…

C++ 动态规划。。。

#include <iostream> #include <algorithm> using namespace std; // 定义一个常量&#xff0c;表示无穷大 const int INF 1e9; int dp[1000 2];// 定义一个函数&#xff0c;计算数组中某个区间的和 int sum(int arr[], int start, int end) {int s 0;for (int …

【C/C++】什么是POD(Plain Old Data)类型

2023年11月6日&#xff0c;周一下午 目录 POD类型的定义标量类型POD类型的特点POD类型的例子整数类型&#xff1a;C 风格的结构体&#xff1a;数组&#xff1a;C 风格的字符串&#xff1a;std::array:使用 memcpy 对 POD 类型进行复制把POD类型存储到文件中&#xff0c;并从文…

干货丨Linux终端常见用法总结(收藏)

一、前言 熟悉Linux终端的基础用法和常见技巧可以极大提高运维及开发人员的工作效率&#xff0c;笔者结合自身学习实践&#xff0c;总结以下终端用法供同行交流学习。 二、常见用法 1.快捷键 1.1.Alt. 在光标位置插入上一次执行命令的最后一个参数。 1.2.CtrlR 模糊搜索历…

jenkins gitlab CI/CD

jenkins的安装教程就不说了&#xff1a;Jenkins docker 一键发布 (一)_jenkins 一键发布-CSDN博客 最近打算从svn切换到gitlab&#xff0c;所以配置了一下jenkins的git 很简单&#xff0c;直接上图 1 选择 Git 2 录入gitlab的http地址&#xff08;由于我的git地址不是22端口&…

数据结构与算法之美代码:排序算法3

目录 算法原理桶排序 代码实现桶排序代码实现计数排序代码实现 算法原理 桶排序 核心思想是将要排序的数据分到几个有序的桶里&#xff0c;每个桶里的数据再单独进行排序。桶内排完序之后&#xff0c;再把每个桶里的数据按照顺序依次取出&#xff0c;组成的序列就是有序的了。…

【小白专用】PHP中的JSON转换操作指南 23.11.06

一、JSON的基础知识 1.1JSON数据格式 JSON数据格式是一组键值对的集合&#xff0c;通过逗号分隔。键值对由“键”和“值”组成&#xff0c;中间使用冒号分隔。JSON数据格式可以嵌套&#xff0c;而且可以使用数组 二、PHP中的JSON函数 JSON的操作需要使用编程语言进行处理&am…

接口测试及接口测试工具

首先&#xff0c;什么是接口呢&#xff1f; 接口一般来说有两种&#xff0c;一种是程序内部的接口&#xff0c;一种是系统对外的接口。 系统对外的接口&#xff1a;比如你要从别的网站或服务器上获取资源或信息&#xff0c;别人肯定不会把数据库共享给你&#xff0c;他只能给你…

【NI-DAQmx入门】外部采样时钟相关

1.时钟的作用 时钟在几乎所有测量系统中都起着至关重要的作用。通过硬件定时测量&#xff0c;时钟控制采样或更新的发生时间。与依赖软件计时测量相比&#xff0c;您可以选择硬件定时测量来实现采样或更新之间更一致的时间间隔。以数模转换器特性分析为例。该应用由三个基本部分…

阿里云安全恶意程序检测(速通一)

阿里云安全恶意程序检测 赛题理解赛题介绍赛题说明数据说明评测指标 赛题分析数据特征解题思路 数据探索数据特征类型数据分布箱型图 变量取值分布缺失值异常值分析训练集的tid特征标签分布测试集数据探索同上 数据集联合分析file_id分析API分析 特征工程与基线模型构造特征与特…

大厂面试题-innoDB如何解决幻读

从三个方面来回答&#xff1a; 1、Mysql的事务隔离级别 Mysql有四种事务隔离级别&#xff0c;这四种隔离级别代表当存在多个事务并发冲突时&#xff0c;可能出现的脏读、不可重复读、幻读的问题。 其中InnoDB在RR的隔离级别下&#xff0c;解决了幻读的问题。 2、什么是幻读&…

Java手动引入Maven依赖的Jar包

&#x1f648;作者简介&#xff1a;练习时长两年半的Java up主 &#x1f649;个人主页&#xff1a;程序员老茶 &#x1f64a; ps:点赞&#x1f44d;是免费的&#xff0c;却可以让写博客的作者开心好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全栈&#xff0c;…

65从零开始学Java之初学者必会的几个常用Java类

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 我们在解决实际问题时&#xff0c;除了经常需要对数字、日期、时间进行操作之外&#xff0c;有时候还…

js原型链

什么叫原型链 原型链是js中的核心&#xff0c;原型链将各个属性链接起来&#xff0c;在原型链上面定义&#xff0c;原型链上的其他属性能够使用&#xff0c;原型链就是保证继承 原型链区分 原型链分为显式原型和隐式原型 显式原型&#xff1a;只有函数和构建函数才有显式原型…

【ARM AMBA AXI 入门 12 -- AXI协议中的 WLAST 与 RLAST】

文章目录 AXI协议中的 WLAST 与 RLAST AXI协议中的 WLAST 与 RLAST AMBA AXI协议是由ARM公司定义的一种高性能&#xff0c;高频率的总线协议。总线协议中的 WLAST 信号是一个重要的信号&#xff0c;它在 AXI 协议中用来标识一个突发&#xff08;Burst&#xff09;传输的最后一…

requests库编写的爬虫程序没有那么难!

下文是用requests库编写的爬虫程序&#xff0c;用于爬取toutiao上的图片。程序使用了代理服务器&#xff0c;代理服务器的地址为duoip&#xff0c;端口号为8000。 import requests from bs4 import BeautifulSoup# 设置代理服务器 proxy_host duoip proxy_port 8000 proxy {…

STM32创建工程步骤

以创建led工程为例&#xff1a; 新建一个led文件夹 新建一个以led命名的工程&#xff08;用keil_uVision5&#xff09;并添加三个组。 Library文件夹里放置库函数文件。 User&#xff1a; 点亮led灯的程序&#xff1b; 直接给寄存器赋值 调用库函数。 #include "stm…

网络运维Day05

文章目录 实验环境用户与组概述用户账号创建查看用户-查创建用户-增修改用户属性-改删除用户-删 用户密码管理交互式修改密码非交互式修改密码 用户初始配置文件基本权限和归属基本权限的类别权限适用对象(归属)查看权限 修改权限设置基本权限如何判断用户对某目录所具备怎样的…