【C++】继承相关(基类与派生类的继承关系以及细节整理)

news2024/11/22 19:00:38

目录

00.引言

01.继承的定义

02.基类和派生类对象

03.继承中的作用域

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

05.友元、静态成员


00.引言

继承是面向对象编程中的一个重要概念,它的作用是创建一个新的类,该类可以从一个已存在的类(父类/基类)继承属性和方法。

通过下面一个例子可以理解继承的作用:

class teacher
{
private:
	char _sex;
	int _age;
	char _name;
private:
	int _jobid; // 工号
};

class student
{
private:
	char _sex;
	int _age;
	char _name;
private:
	int _stuid; // 学号
};

在创建学校学校成员信息的时候,需要定义不同的两个类分别用于存放老师和学生的信息种类,但是两者的信息类别是存在重复的(性别、年龄、姓名) ,定义两个类还好,但是如果定义需要更多的类呢,那样就会进行过多的重复工作。此时就可以用继承的方法,将同样的信息类别在父类中定义:

class people
{
public:
	char _sex;
	int _age;
	char _name;
};

使用正确的继承语法,继承父类person的属性:

class teacher : public people
{
public:
	teacher()
		: _sex('男') // 在成员初始化列表进行初始化 报错
		, _age(40)
		, _name('张')
	{}

private:
	int _jobid; // 工号
};

class student : public people
{
public:
	student()
	{
		_sex = '男';
		_age = 20;
		_name = '李';
	}
private:
	int _stuid; // 学号
};

这里注意:在定义默认构造函数时,不能够通过初始化列表初始化其父类的成员,因为初始化列表只能初始化其自身的成员,所以 teacher() 函数会报错。

01.继承的定义

如图:

我们可以看到,student是子类,也叫派生类,他继承的是父类people,也叫基类,而public是继承方式,继承方式也可以是protected、private,和访问限定符是一样的。

使用不同的继承方式继承的基类成员的访问权限是不一样的,具体关系可以看下面这张表:

可以看出基类的私有成员在派生类中是不可见的,而其他成员的访问方式取决于基类中成员的访问限定符和继承方式。通常情况下,使用 public 继承是最常见的方式,因为它保留了基类的接口,并且派生类可以访问基类的公共和受保护成员。 

注意:

使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。

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

02.基类和派生类对象

当一个派生类对象被赋值给一个基类对象时,如果使用的是赋值操作符 = 或者拷贝构造函数,那么只会复制派生类对象中基类部分的内容,而派生类特有的成员会被截断

这种行为称为对象切片,因为派生类对象被“切片”成了基类对象

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。

class people
{
public:
	char _sex;
	int _age;
	char _name;
};

class student : public people
{
public:
	student()
	{
		_sex = '男';
		_age = 20;
		_name = '李';
	}
public:
	int _stuid; // 学号
};

int main()
{
	student s1;
	s1._stuid; // 可以访问
	people p1 = s1;
	p1._stuid; // 无法访问
	
	return 0;
}

这里将派生类对象s1赋值给基类对象p1后, p1就无法访问s1中的_stuid元素,因为s1是被切片赋值给p1的,如图:

下面的示例用来解释基类的指针赋值:

class Person
{
    protected :
    string _name; // 姓名
    string _sex;  // 性别
    int _age;    // 年龄
};
class Student : public Person
{
public:
    int _No; // 学号
};
void Test ()
{
    Student sobj;
    // 1.子类对象可以赋值给父类对象/指针/引用
    Person pobj = sobj;
    Person* pp = &sobj;
    Person& rp = sobj;

    //2.基类对象不能赋值给派生类对象
    sobj = pobj;

    //3.基类的指针可以通过强制类型转换赋值给派生类的指针
    pp = &sobj
    Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
    ps1->_No = 10;
    pp = &pobj;
    Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问
    ps2->_No = 10;
}

03.继承中的作用域

在继承体系中,基类和派生类都有独立的作用域。

访问限定符决定了类的成员在类内部和外部的可见性可访问性。继承方法决定了基类的的成员继承到派生类之后的作用域。

比如用public方法继承基类的成员,那么就在派生类的内部和外部都可以调用基类的成员,但是如果派生类内部也定义了一个和基类名字相同的成员呢?就像这样:

class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111;
	// 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号
};

此时派生类Student和基类Person都定义了成员变量_num,此时在派生类内部调用_num,会默认调用派生类本身的,而基类的_num会被隐藏掉,因为,当子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。但是在子类成员函数中,也可以使用 基类::基类成员 显示访问。

同名成员的定义降低了代码的可读性,所以我们在实际继承体系中最好不要定义同名的成员,上述的代码就可以这么修改:

class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _IDnum = 111;
	// 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << _IDnum << endl;
		cout << " 学号:" << _STnum << endl;
	}
protected:
	int _STnum = 999; // 学号
};

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

 首先我们要知道默认成员函数是什么:默认成员函数提供默认行为,包括默认构造函数、拷贝构造函数、运算符重载等,这样即使我们没有显式地编写这些函数,类仍然可以正常地进行对象的创建、复制和赋值等操作。那么在派生类中,这几个成员函数是如何生成的呢?

1.默认构造函数

由于派生类继承了基类的成员,在调用派生类的默认构造函数时必须调用基类的默认构造函数初始化基类的那一部分成员:


class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}
protected:
	int _num; //学号
};
int main()
{
	Student s1("jack", 18);
    return 0;
}

运行结果:

Person()
Student()

可以看出,编译器先调用基类的默认构造函数后再调用的派生类的默认构造函数。

2.析构函数

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。如此一来保证了先清理派生类成员再清理基类成员的顺序

class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};
int main()
{
	Student s1("jack", 18);
    return 0;
}

运行结果:

Person()
Student()

~Student()
~Person()

因为子类是对基类的继承与延伸,子类受基类的影响,但是基类并不受子类影响,所以在销毁子类对象时,首先释放子类特有的资源,然后再释放基类的资源。

3.拷贝构造函数

派生类的拷贝构造函数需要对两部分进行拷贝构造,一个是派生类自身的成员变量,一个是基类的成员变量,所以派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

class Person
{
public:
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
protected:
	int _num; //学号
};

int main()
{
    Student s1("jack", 18);
	Student s2(s1);
    return 0;
}

运行结果:

Person(const Person& p)
Student(const Student& s)

可以看出,也是先调用了基类的拷贝构造函数再调用派生类的。 

4.赋值运算符重载

与拷贝构造类似,派生类的operator=也必须要调用基类的operator=完成基类的复制

class Person
{
public:
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num; //学号
};

int main()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
    return 0;
}

运行结果:

Student& operator= (const Student& s)
Person operator=(const Person& p)

因为 s3Student 类型的对象。因此,会先输出 Student& operator= (const Student& s)。然后在 Student 类的赋值运算符内部,通过 Person::operator=(s); 调用了基类 Person 的赋值运算符重载函数。

05.友元、静态成员

友元关系不能继承,基类的友元不能访问子类私有和保护成员,但是可以通过将派生类定义成友元的方式,使得派生类也可以访问基类的私有成员

#include <iostream>

class Shape {
private:
    double _width;
    double _height;

public:
    Shape(double width, double height) : _width(width), _height(height) {}

    friend class Rectangle; // 将Rectangle类声明为Shape类的友元
};

class Rectangle : public Shape {
public:
    Rectangle(double width, double height) : Shape(width, height) {}

    void displayArea() {
        double area = _width * _height; // 可以直接访问基类的私有成员
        std::cout << "Area of rectangle: " << area << std::endl;
    }
};

int main() {
    Rectangle rect(5.0, 3.0);
    rect.displayArea();
    return 0;
}

在这个例子中,Rectangle 类通过将 Shape 类声明为友元,可以直接访问 Shape 类的私有成员 _width_height

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

class Person
{
public:
    Person () {++ _count ;}
protected:
    string _name ; // 姓名
public:
     static int _count; // 统计人的个数。
};
    int Person :: _count = 0;
class Student : public Person
{
protected :
    int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
    string _seminarCourse ; // 研究科目
};
void TestPerson()
{
    Student s1 ;
    Student s2 ;
    Student s3 ;
    Graduate s4 ;
    cout <<" 人数 :"<< Person ::_count << endl;
    Student ::_count = 0;
    cout <<" 人数 :"<< Person ::_count << endl;
}

以上就是继承相关的知识的整理了,欢迎在评论区留言,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

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

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

相关文章

sipeed 的 MaixCam显示图片

WiFi联网后&#xff0c;把固件升级到最新 一根tpyc-c连接线为MaixCam供电&#xff0c;点击液晶屏settings 在WiFi中设置确保联网&#xff0c;在更新MaixPy中升级固件 可以选择国内源加速&#xff0c;将固件升级到最新版 MaixVision的操作 1&#xff0c;在MaixVision左下角…

谷歌Gboard应用的语言模型创新:提升打字体验的隐私保护技术

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

C语言 | Leetcode C语言题解之第87题扰乱字符串

题目&#xff1a; 题解&#xff1a; struct HashTable {int key;int val;UT_hash_handle hh; };void modifyHashTable(struct HashTable** hashTable, int x, int inc) {struct HashTable* tmp;HASH_FIND_INT(*hashTable, &x, tmp);if (tmp NULL) {tmp malloc(sizeof(st…

【数据结构与算法 刷题系列】合并两个有序链表

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;数据结构与算法刷题系列&#xff08;C语言&#xff09; 目录 一、问题描述 二、解题思路详解 合并两个有序链表的思路 解题的步…

HTML飘落的花瓣

目录 写在前面 HTML​​​​​​​简介 完整代码 代码分析 系列推荐 写在最后 写在前面 本期小编给大家推荐HTML实现的飘落的花瓣&#xff0c;无需安装软件&#xff0c;直接下载即可打开~ HTML​​​​​​​简介 HTML&#xff08;Hypertext Markup Language&#xff…

【Linux】文件描述符和重定向

目录 一、回顾C文件 二、系统文件I/O 2.1 系统调用 open 2.2 标志位传参 2.3 系统调用 write 2.4 文件描述符fd 2.5 struct file 2.6 fd的分配规则 2.7 重定向 2.7.1 基本原理&#xff1a; 2.7.2 系统调用 dup2 2.8 标准错误 一、回顾C文件 文件 内容 属性 对…

阿里云OSS配置跨域及域名访问

1、配置跨域 进入对象存储OSS–>OSS存储桶–>数据安全–>跨域设置–>创建规则 2、配置跨域 Etag x-oss-request-id3、配置结果如下 4、数据源配置 切换到数据管理–>静态页面 配置根页面 保存结果如下 5、配置域名访问 绑定域名 添加txt记录 验证绑定 …

【CSP CCF记录】202109-2 非零段划分

题目 过程 思路 参考&#xff1a;http://t.csdnimg.cn/XRKTm STL库用法 unique用法 unique是STL中很实用的函数之一&#xff0c;需要#include&#xff08;感谢各位提醒&#xff09;&#xff0c;下面来简单介绍一下它的作用。 unique的作用是“去掉”容器中相邻元素的重复…

手机配置在线检测工具微信小程序源码

手机配置在线检测工具微信小程序源码&#xff0c;这是一款升级版检测工具&#xff0c;自动检测手机真伪,序列号等。另外还可以给手机检测各项功能是否正常。 由于能检测的项目太多,所以大家到时候自行研究吧。另外支持多做流量主模式,还有外卖CPS,和友情小程序推荐等&#xff…

Unity自定义动画-Animation动画数据-How is “fileIDToRecycleName“ generated

一般美术和程序分工明确的项目 fbx确实是和动画一一对应的&#xff1b; 但一些独立&#xff0c;或者小工作室的项目&#xff0c;就没法保证了&#xff0c;关键还是在于 Unity的 .meta 目录 查找和对比了一下 .fbx 和 .meta&#xff1a; 缓存和不缓存Animation 具体的Animat…

天诚AIoT无线联网智能门锁即将亮相成都安博会、永康门博会

5月上旬&#xff0c;对于江苏新巢天诚智能技术有限公司&#xff08;以下简称“天诚”&#xff09;而言&#xff0c;依旧忙得如火如荼。随着各地人才公寓、公租房、智慧校园类智慧通行与租住新项目的实施、落地与服务&#xff0c;天诚也不忘初心&#xff0c;携全新升级的AIoT全场…

DEV--C++小游戏(吃星星(0.5))

目录 吃星星&#xff08;0.5&#xff09; 该版本简介 DEV--C小游戏(吃星星(0.1)) DEV--C小游戏(吃星星(0.2)) 分部代码 头文件 命名空间变量&#xff08;增&#xff09; 副函数&#xff08;新&#xff0c;增&#xff09; 清屏函数 打印地图函数&#xff08;增&…

d18(169-174)-勇敢开始Java,咖啡拯救人生

目录 特殊文件 .properties 属性文件 读取属性文件 写出属性文件 .xml XML文件 读取XML文件 ​编辑 写出XML文件 约束XML文件 日志技术 Logback 日志级别 特殊文件 .properties 属性文件 每行都是一个键值对 键不能重复 文件后缀一般是.properties 读取属性文件 …

记录一下 log4j的漏洞

目录 背景 bug的产生 bug复现 JNDI 网络安全学习路线 &#xff08;2024最新整理&#xff09; 学习资料的推荐 1.视频教程 2.SRC技术文档&PDF书籍 3.大厂面试题 特别声明&#xff1a; 背景 log4j这次的bug&#xff0c;我相信大家都已经知道了&#xff0c;仅以…

OpenSSL自签证书并基于Express搭建Web服务进行SSL/TLS协议分析

OpenSSL自签证书并基于Express搭建Web服务进行SSL/TLS协议分析 起因 最近在学习安全协议&#xff0c;大多数实验都是基于Windows下IIS&#xff0c;或者Linux下nginx搭建的Web服务&#xff0c;搭建环境和编写配置文件比较麻烦。而且我有多个不同环境的设备&#xff0c;折腾起来…

使用Dockerfile配置Springboot应用服务发布Docker镜像-16

创建Docker镜像 springboot-docker模块 这个应用可以随便找一个即可&#xff0c;这里不做详细描述了。 pom.xml 依赖版本可参考 springbootSeries 模块中pom.xml文件中的版本定义 <dependencies><dependency><groupId>com.alibaba.cloud</groupId>…

EasyExcel导出Excel文件——合并单元格多层级数据导出

合并单元格多层数据导出 思维脑图 代码实现 /*** 导出所有信息** param request 请求体*/ Override public void getWilliamExportList(WilliamReqVo request, HttpServletResponse response) throws Exception {List<SysDictData> dataByType dictDataService.getDic…

添砖Java之路(其五)——封装,String,StringBuilder类。

封装&#xff1a; 封装意义&#xff1a;更好的维护数据&#xff0c;让使用者无需关心如何使用&#xff0c;只需要知道怎么使用。 Java Bean&#xff1a; 然后我们要知道Java Bean(实体类)标准。 1.对于这个类的成员都需要设为私有&#xff0c;而且要对外提供相应Get,Set的接…

WWW服务器搭建(1)——HTTP协议原理篇

目录 一、WWW的相关概念 1.1 WWW的定义 1.2 超文本标记语言HTML 1.3 统一资源定位符URL 1.4 超文本传输协议HTTP 二、HTTP协议工作过程 2.1 DNS解析 2.2 TCP连接过程 2.3 HTTP 请求与响应 2.4 TCP连接断开 三、HTTP请求报文格式 3.1 请求行 3.2 请求头 3.3 空行 …

大语言模型的数据预处理

文章目录 质量过滤敏感内容过滤数据去重 当收集了丰富的文本数据之后&#xff0c;为了确保数据的质量和效用&#xff0c;还需要对数据进行预处理&#xff0c;从而消除低质量、冗余、无关甚可能有害的数据。一般来说&#xff0c;需要构建并使用系统化的数据处理框架&#xff08;…