【C++】继承---下(子类默认成员函数、虚继承对象模型的详解等)

news2024/11/25 4:06:08

前言:    

     上篇文章我们一起初步了解了继承的概念和使用,本章我们回家新一步深入探讨继承更深层次的内容。

    前文回顾——>继承---上

目录

(一)派生类的默认成员函数

(1)6个默认成员函数

 (2)派生类的默认成员函数使用规则

(3)实例化详解

 (4)应用:如何设计一个不能继承的类

(二) 继承与友元

(三)继承与静态成员

(四)多继承和菱形继承

(1)菱形继承的问题

(2)解决方法之虚拟继承

(3)虚拟继承的底层原理


(一)派生类的默认成员函数

在之前类和对象的学习中,我们详细学习了基类的默认成员函数

类和对象默认成员函数复习——>类和对象的默认成员函数

那么派生类的默认成员函数的使用规则是什么样的呢?

(1)6个默认成员函数

我们浅浅回顾一下~

我们6个成员函数对于一个普通的类适用,那么对于基类也适用

下面是我们6大默认成员函数

顾名思义,就是对于一些默认类型,我们自己不需要给出具体的成员函数,编译器可以自动生成。我们前面也详解过,对于自定义类型还是需要我们手动实现深拷贝,编译器自动生成的都是浅拷贝等等(遗忘的童鞋可以回顾之前的博文)

 (2)派生类的默认成员函数使用规则

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

(3)实例化详解

 我们这里引用一个Person父类和Student的子类来一一验证。

Person类:

#include<iostream>
using namespace std;
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; // 姓名
};

Student类:
 

class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}

	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);
			_num = s._num;
		}
		return *this;
	}

	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};

我们下面开始测试:

父类成员构造、拷贝构造、赋值、析构:

测试代码:

运行结果:

此时父类和普通类的默认成员函数使用规则一样。


派生类成员构造、拷贝构造、赋值、析构 :

测试代码:

运行结果:

 这里我们可以清楚观察到对于派生类对象的构造函数、拷贝构造函数和赋值重载:

继承的派生类对象都必须先调用基类的构造(拷贝构造、赋值重载)然后再完成自己这部分的构造(拷贝构造、赋值重载)

对于析构函数:

派生类对象析构清理先调用派生类析构再调基类的析构

因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

 (4)应用:如何设计一个不能继承的类

开门见山:

方法:把构造函数私有化。

class A
{
private:
	A()
	{}
};

class B : public A
{

};

int main()
{
	B b;
	
	return 0;
}
  • 父类A的构造函数私有化以后,B就无法构造对象
  • 因为规定了子类的成员必须调用父类的构造函数初始化

但是问题来了:

这时候就还有一个问题A类想单独构造对象也不行了

解决办法:(单例设计模式)


这时又有一个问题 —— 先有鸡还是先有蛋的问题:

  •  调用成员函数需要对象,对象创建需要调用成员函数,调用成员函数需要对象…

解决办法:

用一个静态成员函数就能很好的解决问题

(二) 继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

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

 报错:

  • 想两个都访问时,只要既变成父类的友元也变成子类的友元就可以了。
  • 不能说是父类的友元你就是子类的友元了。

(三)继承与静态成员

问题:

比如说父类有一个静态成员,那子类继承之后,子类会增加一个静态成员还是和父类共享一个静态成员呢? 

 验证代码:
 


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;
}
int main()
{
	TestPerson();
	return 0;
}

这里我们输出的会是3还是4呢?

通过输出我们可以发现:

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

(四)多继承和菱形继承

(1)菱形继承的问题

多继承:

一个子类有两个或以上直接父类时称这个继承关系为多继承

 菱形继承:

菱形继承是多继承的一种特殊情况。

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。

在Assistant的对象中Person成员会有两份。

 样例代码:


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; // 主修课程
};
void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "peter";
	
}

报错:

 我们可以指定作用域来解决二义性问题:

// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";

但是数据冗余的问题无法解决:

  • 数据冗余带来的问题就是空间的浪费
  • 当父类中的成员变量很大的时候

(2)解决方法之虚拟继承

菱形虚拟继承解决了数据冗余和二义性的问题。

先看代码:


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";
	cout << a._name << endl;
}
int main()
{
	Test();
}

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用

(3)虚拟继承的底层原理

样例代码:

class A
{
public:
	int _a;
};

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

class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d._a = 0;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

运行调试我们发现虚继承之后无论有没有指定作用域,各个作用域_a的值是一样的:

我们再调用内存管理来更深入的探究:

 

我们&d后找到d的内存,大致能看出 _a,_b,_c,_d的存放位置,按照常理来说,_a按照顺序应该是在前面存放,但事实上确是在最后,那第一个位置存放的地址中到底是什么呢?

我们访问第一个位置存放的地址:

我们发现存放了00 00 00 00 然后有一个14 00 00 00,14是十六进制换算十进制就是16+4=20,20个字节正好够存放五个地址 ,我们惊奇的发现02 00 00 00和这个地址的偏移量就恰好是20个字节(5个地址),那么我们可以假设第一个地址中存放的是_a的偏移量。

而_c上面的地址54 7b b1 00 我们同样方法访问可以得到:

 0c转换成十六进制就是12,也就是12个字节,而_a正好也和它相差3个地址的偏移量,我们更可以确信这是存放偏移量的地址。


模型总结:

菱形虚拟继承调整了对象的模型。

  • 我们发现B和C对象的开头都存了一个指针, 这种对象模型是省了四个字节(_a),却又增加了两个指针(八个字节),反而变大了四个字节。
  • 但是如果A很大的情况下,剩下来的空间和这两个指针(八个字节)相比
  • 整体空间是节省了不少的空间了

 而且B和C虚继承后第一个地址经过我们分析存放的是偏移量,用来找到存储_a的地址。

为什么要搞这个偏移量呢?

  • 场景一:
  • 在赋值转换 —— 切片的时候就能用得到
  • 假设 D d;B b;b = d;此时就要切片
  • 切片切割的时候,能找到_b,但是要通过偏移量计算出A的位置

  • 场景二:
  • B * ptrb = &d; 这里也是切片,ptrb->_a = 1;
  • B的指针能找到_b,但是找_a是要通过偏移量来算出A的位置、

 我们发现找偏移量的时候,第一个位置存放的总是00 00 00 00。

为什么偏移量存储在第二个位置,而不是存在第一个位置

  • 第一个位置是预留的,可能其他地方要用

菱形虚拟继承的缺点:

  • 对于编译器和人们的理解都变复杂了
  • 虽然将数据冗余和二义性一概解决了,但是付出了很大的代价 — 多了两层间接
  • 代价就是这个存储模型,该模型也一定程度影响了访问数据的效率
     

模型的优点:

因为不同的编译的设计的不同,A对象存储的位置也会不一样,但是只要有指针去找偏移量,再通过偏移量去找A就能找到,这是通用的方法,统一模型

  • 这个表也叫做 —— 虚基表
  •  A叫做虚基类
  •  该指针叫做虚基表指针

 

补充:

  • A对象只初始化一次

图解:


一道练习题:

class A 
{
public:
	A(const char* s)
	{ 
		cout << s << endl; 
	}

	~A() 
	{}
};

class B : virtual public A
{
public:
	B(const char* s1, const char* s2)
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};

class C : virtual public A
{
public:
	C(const char* s1, const char* s2)
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};

class D : public B, public C
{
public:
	D(const char* s1, const  char* s2, const char* s3, const char* s4) 
		:B(s1, s2)
		,C(s1, s3)
		,A(s1)
	{
		cout << s4 << endl;
	}
};

int main() 
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;

	return 0;
}

输出是什么?

原因:

  • 结合初始化列表初始化的顺序和声明的顺序有关系
  • 子类在调用构造函数前会调用父类的构造函数


总结

  • 很少有人设计菱形继承,但是C++标准库中就有菱形继承,IO流的类就是菱形继承。
  • 继承的意义是用子类去复用父类
  • 实际当中可以设计多继承,但是尽量不要设计菱形继承,更不要设计菱形虚拟继承,太复杂了!还有一定程度的效率损失。
  • 我们正常使用的时候要尽量避开继承中语法的坑

感谢您的阅读,祝您学业有成!

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

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

相关文章

Pytorch全连接神经网络实现手写数字识别

问题Mnist手写数字识别数据集作为一个常见数据集&#xff0c;包含10个类别&#xff0c;在此次深度学习的过程中&#xff0c;我们通过pytorch提供的库函数&#xff0c;运用全连接神经网络实现手写数字的识别方法设置参数input_size 784hidden_size 500output_size 10num_epoc…

JavaScript对象类型之function

目录 一、Function 定义函数 调用函数 默认参数 匿名函数 箭头函数 二、函数是对象 三、函数作用域 四、闭包 五、let、var与作用域 一、Function 定义函数 function 函数名(参数) {// 函数体return 结果; } 例如&#xff1a; function add(a, b) {return a b; …

应届生通过Java培训班转行IT有前途吗?

借用邓小平同志曾说过的一句话&#xff1a;科学技术是第一生产力。IT行业作为科技行业中的一员&#xff0c;不管是在自身的发展&#xff0c;还是支持其他行业的发展中都扮演了不可或缺的角色&#xff0c;“互联网”是社会发展的趋势&#xff0c;前途是无限的。而计算机语言是目…

dolphinscheduler之hivecli 任务

hivecli 任务 Hivecli任务说明 dolphinscheduler的hivecli任务是专门执行hivesql的任务类型。其中子类型分为FROM_SCRIPT和FROM_FILE。 FROM_SCRIPT 执行的脚本可以直接在文本框中编写 执行的底层采用-e参数执行 hive -e "show databases;show tables"FROM_FILE…

建造者模式解读

目录 话题引进 传统方式解决盖房需求 传统方式的问题分析 建造者模式基本介绍 基本介绍 四个角色 原理类图 ​编辑 应用实例 改进代码 建造者模式在 JDK 的应用和源码分析 建造者模式的注意事项和细节 抽象工厂模式 VS 建造者模式 话题引进 1) 需要建房子&#xff1a;…

剑指 Offer (第 2 版)

&#xff08;简单&#xff09;剑指 Offer 03. 数组中重复的数字 找出数组中重复的数字。 在一个长度为 n 的数组 nums 里的所有数字都在 0&#xff5e;n-1 的范围内。数组中某些数字是重复的&#xff0c;但不知道有几个数字重复了&#xff0c;也不知道每个数字重复了几次。请…

Python实现采集某二手房源数据并做数据可视化展示

目录环境介绍&#xff1a;模块使用:实现爬虫思路&#xff1a;代码环境介绍&#xff1a; Python 3.8Pycharm 模块使用: requests >>> pip install requests 数据请求模块 parsel >>> pip install parsel 数据解析模块 csv 内置模块 实现爬虫思路&#x…

如何搭建自己的V Rising自建服务器,以及常见的V Rising服务器问题解决方案

V rising官方服务器经常无法连接&#xff0c;无法和小伙伴玩耍&#xff1b;如何搭建自己的V rising服务器呢&#xff1f;还可以修改掉落倍率&#xff0c;加快游戏进度&#xff0c;搭建自己的私人服务器。 前言 最近V rising这个游戏很火呀&#xff0c;迫不及待地和小伙伴一起…

基于粒子群优化算法的面向综合能源园区的三方市场主体非合作交易方法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【JSP学习笔记】4.JSP 隐式对象及客户端请求

前言 本章介绍JSP的隐式对象及客户端请求。 JSP 隐式对象 JSP隐式对象是JSP容器为每个页面提供的Java对象&#xff0c;开发者可以直接使用它们而不用显式声明。JSP隐式对象也被称为预定义变量。 JSP所支持的九大隐式对象&#xff1a; 对象描述requestHttpServletRequest 接…

一文吃透Arthas常用命令!

Arthas 常用命令 简介 Arthas 是Alibaba开源的Java诊断工具&#xff0c;动态跟踪Java代码&#xff1b;实时监控JVM状态&#xff0c;可以在不中断程序执行的情况下轻松完成JVM相关问题排查工作 。支持JDK 6&#xff0c;支持Linux/Mac/Windows。这个工具真的很好用&#xff0c;…

【C++】模板进阶--非类型模板参数模板特化及分离编译

文章目录一、非类型模板参数二、模板的特化1.模板特化的概念2.函数模板的特化3.类模板的特化3.1 全特化3.2 偏特化4.类模板特化应用示例三、模板的分离编译四、模板总结一、非类型模板参数 模板参数分为类型形参与非类型形参&#xff0c;其中&#xff0c;类型形参即出现在模板…

MBD-PMSM闭环控制模型(FOC算法)

目录 前面 Speed_and_Position_Estimator 获取HALL信号 HALL状态更新 计算转速 位置判断 ADC相电流/总线电流电压 获取AD值 计算实际值 低速高速切换 SlowLoopControl FastLoopControl 最后 前面 前面分析了BLDC的开环与闭环&#xff0c;接下来分析PMSM或者说FOC…

MySQL 异步复制、半同步复制、增强半同步复制(史上最全)

背景&#xff1a;来自于小伙伴问题 小伙伴的难题&#xff1a; mysql主从同步的时候&#xff0c;半同步和增强半同步是怎样的一个概念&#xff0c;我看网上说的有点不明不白的&#xff0c;也没找到合适的解释。 这里尼恩给大家做一下系统化、体系化的梳理。也一并把这个题目以…

【每天学习一点点】RocketMQ的架构、写数据、高效的数据查询索引、负载均衡

Rocket一、学习目标二、RocketMQ的架构运行图2.1、NameServer2.1.1 为什么需要NameServer2.1.1.1 不可以没有nameserver吗&#xff1f;2.1.2 NameServer需要单独部署吗2.1.3 Nameserver可以动态注册和注销Broker、Topic和Consume 是什么意思2.1.4 可以使用nacos的配置中心替代N…

成本与体验的“非零和博弈”

随着移动互联网和智能终端的普及&#xff0c;越来越多的海内外互联网企业开始发力短视频业务。在短视频用户全球化&#xff0c;短视频产品及内容消费井喷式增长的今天&#xff0c;用户开始逐渐对体验有了越来越高的要求。为了更清晰更流畅地播放&#xff0c;用户播放成本也随着…

TensorFlow GPU不可用,WSL2安装

这个帖子写给23年刚买电脑、系统是win11&#xff0c;tensorflow版本是2.10以上的兄弟们。不符合的可以去看其他答案了。 这是以我三天来的安装经历来写的&#xff0c;希望能给后来的兄弟们减少时间的浪费。 win11&#xff0c;安装的tensorflow的版本都是2.12的&#xff0c;但…

(二)Cmd Markdown 编辑阅读器的使用效果 | 以 Cmd Markdown 编辑阅读器为例

Cmd Markdown 编辑阅读器使用指南 &#xff08;一&#xff09;Cmd Markdown 编辑阅读器的使用示例 | 以 Cmd Markdown 编辑阅读器为例&#xff08;二&#xff09;Cmd Markdown 编辑阅读器的使用效果 | 以 Cmd Markdown 编辑阅读器为例 在 Cmd Markdown 编辑阅读器&#xff08; …

C语言实现Allan方差计算

Allan方差专有概念解释 1.量化噪声 量化噪声是一切量化操作所固有的噪声,只要进行数字量化编码采样,传感器输出的理想值与量化值之间就必然会存在微小的差别,量化噪声代表了传感器检测的最小分辨率水平。 2.角度随机游走 角度随机游走是宽带角速率白噪声积分的结果,即陀螺…

金三银四,大伙冲刺,SpringCloud 的 25 个面试考点

今天给大家分享SpringCloud高频面试题。 Spring Cloud核心知识总结 下面是一张Spring Cloud核心组件关系图&#xff1a; 从这张图中&#xff0c;其实我们是可以获取很多信息的&#xff0c;希望大家细细品尝。 话不多说&#xff0c;我们直接开始 Spring Cloud 连环炮。 连环炮…