C++普通类,派生类,虚基类的成员构造顺序以及构造函数调用顺序详解

news2024/11/14 20:28:57

目录

    • 前言
    • 普通类构造析构顺序
      • 解析
      • 依赖关系产生的错误
    • 派生类构造析构顺序
      • 解析
      • 扩展菱形多继承场景
    • 含虚基类的派生类构造析构顺序
      • 解析
      • 扩展菱形多继承场景(引入虚继承)

前言

C++规定“对象的析构过程必须与其构造过程相反”这一语法规则。

因此我们研究透彻了构造过程,那么析构过程自然就是相反的;

(因为一个类的成员初始化和析构,类似一个压栈与弹栈的过程)

普通类构造析构顺序

  • 按照成员变量生命的顺序构造
#include<iostream>

int i = 1;

using namespace std;
class A 
{
public:
	A():a(i++),b(i++) {} //i为全局变量,初始值为1
	int b;
	int a;
};
int main()
{
	A x;
	cout << x.a <<' '<< x.b << endl; //运行结果2  1 ;
	return 0;
}

可以看到,虽然我们在类型A默认构造中的初始化列表里指定了构造顺序: 先构造a成员,再构造b成员;

事实上还是按照成员变量的声明顺序,先构造了b成员为1,再构造了a成员为2!

解析

对象的析构过程必须与其构造过程相反

一个类有不止一个构造函数,但是只有一个析构函数!

如果这些构造函数对成员的初始化顺序各不相同,那么在析构这个类的对象时,应当遵循什么样的成员析构顺序呢?难到要对构造改类对象时的初始化顺序进行某种方式的记录吗?

实际上,析构函数是不会关心这个类的对象是使用哪个构造函数构造出来的,但同时又要匹配一个确定的析构顺序,那么显然编译器只能根据类声明中成员变量的出现顺序来制定成员变量的初始化顺序紧接着相反的析构的顺序也就确定了;

依赖关系产生的错误

若成员的初始化存在某种依赖时应当在类声明中给出注释以进行说明,而不是将正确顺序写在某个构造函数里,应当着重注意声明顺序,避免不必要的错误。
在这里插入图片描述

初始化定义的顺序是先a后b,但编译器实际上是按照声明顺序先b后a的,所以b(a)先被初始化为随机值,随后a才被初始化为1;

派生类构造析构顺序

  • 派生类构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的

  • 类的构造,析构函数不能直接被继承,因此派生类构造函数总是 先 调用其直接基类构造函数 再 执行其他代码(其他代码遵循普通函数声明顺序进行构造);

(间接基类不能直接去调用,比如C继承B,B继承A,如果C(调用间接基类A)和B(调用直接基类A)的构造,那就是无意义的构造两次,浪费资源毫无益处)

#include<iostream>

using namespace std;


//多继承场景
class A//定义基类A
{
public:
	A()  { cout << "调用构造函数 A()" << endl; } 

	~A() { cout << "调用析构函数 ~A()" << endl; }
};
class B : public A
{
public:
	B() { cout << "调用构造函数 B()" << endl; } 

	~B() { cout << "调用析构函数 ~B()" << endl; }
};
class C : public B 
{
public:
	C()  { cout << "调用构造函数 C()" << endl; }

	~C() { cout << "调用析构函数 ~C()" << endl; }
};


int main()
{
	C c;

	return 0;
}

运行结果:

在这里插入图片描述

解析

根据调试可以发现:

编译器为了初始化C,先调用了C的构造函数,紧接着C在定义其他变量前,有继承关系,则C的构造函数自动调用其直接基类B的构造函数,B在完成其构造函数前,有继承关系则B的构造函数又自动调用其直接基类A的构造函数;

那么最先完成构造函数的是最顶层A类,紧接着B类完成基类A的初始化,B类的构造也进一步完成,到了最底层C类,直接基类B和间接基类A(B也把他的构造调用了)都构造好了,然后才是C也完成了他的构造函数; (如果有其他变量,则在上述继承顺序构造的基础上,再加上遵循普通函数的声明顺序进行构造;)

析构顺序类似于压栈和弹栈,刚好相反;

扩展菱形多继承场景

//菱形 多继承场景
class A//定义基类A
{
public:
	A() { cout << "调用构造函数 A()" << endl; }

	~A() { cout << "调用析构函数 ~A()" << endl; }
};
class B : public A
{
public:
	B() { cout << "调用构造函数 B()" << endl; }

	~B() { cout << "调用析构函数 ~B()" << endl; }
};
class C : public A
{
public:
	C() { cout << "调用构造函数 C()" << endl; }

	~C() { cout << "调用析构函数 ~C()" << endl; }
};
class D : public B,public C
{
public:
	D() { cout << "调用构造函数 D()" << endl; }

	~D() { cout << "调用析构函数 ~D()" << endl; }
};

int main()
{
	D d;

	return 0;
}

那么执行结果是:

在这里插入图片描述

显然能看到,出现看了菱形继承 数据二义性的问题!

含虚基类的派生类构造析构顺序

class A//定义基类A
{
public:
	A() {cout<<"调用A构造函数"<<endl;} 
};
class B : virtual public A //A作为B的虚基类
{
public:
	B() :A() {cout<<"调用B构造函数"<<endl;} 
};
class C :  public B //B作为C的直接基类
{
public:
	C() :A() ,B(){cout<<"调用C构造函数"<<endl;} 
};

int main()
{
	C c;

	return 0;
}

运行结果:

在这里插入图片描述

虽然运行结果类似,但通过调试可以看到,与上面普通的派生类调用构造规则不同!

这次编译器直接从C类的构造函数中,先执行了A类的构造函数再执行了B类的构造函数!显然,C不但执行了直接基类A的构造,也执行了间接基类B的构造;

解析

如果出现虚继承,某个类型含有虚基类时,最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对直接基类的虚基类初始化

C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类A的其他派生类(如类B)
对虚基类A的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

扩展菱形多继承场景(引入虚继承)

#include<iostream>

using namespace std;

//虚继承,菱形继承
class A//定义基类A
{
public:
	A() { cout << "调用A构造函数" << endl; }
};
class B : virtual public A
{
public:
	B() :A() { cout << "调用B构造函数" << endl; }
};
class C :virtual public A 
{
public:
	C() :A() { cout << "调用C构造函数" << endl; }
};
class D : public B,public C 
{
public:
	D() :A(),B(),C() { cout << "调用D构造函数" << endl; }
};



int main()
{
	D d;

	return 0;
}

在这里插入图片描述

(这不就是菱形继承解决了重复初始化造成的数据二义性的原理嘛~!)

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

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

相关文章

宕机了,Redis如何避免数据丢失?

今天是大年初一&#xff0c;祝大家新年快乐&#xff0c;新的一年技术增进&#xff0c;工资翻倍。 目前&#xff0c;Redis的持久化主要有两大机制&#xff0c;即AOF日志和RDB快照&#xff0c;在接下来的两节课里&#xff0c;我们就分别学习一下吧。 AOF日志是如何实现的&#…

结构型模式-装饰器模式

1.概述 快餐店有炒面、炒饭这些快餐&#xff0c;可以额外附加鸡蛋、火腿、培根这些配菜&#xff0c;当然加配菜需要额外加钱&#xff0c;每个配菜的价钱通常不太一样&#xff0c;那么计算总价就会显得比较麻烦。 使用继承的方式存在的问题&#xff1a; 扩展性不好 如果要再加…

Alibaba微服务组件Sentinel学习笔记

1 .Sentinel 是什么 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件&#xff0c;主要以 流量为切入点&#xff0c;从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的…

一篇读懂图神经网络

来源&#xff1a;投稿 作者&#xff1a;张宇 编辑&#xff1a;学姐 近年来&#xff0c;作为一项新兴的图数据学习技术&#xff0c;图神经网络&#xff08;GNN&#xff09;受到了非常广泛的关注&#xff0c;在各大顶级学术会议上&#xff0c;图神经网络相关的论文也占了相当可观…

https://app.diagrams.net/在线画图的一些技巧

最近工作需要,实践了在线画图的case, 下面就把使用心得记录一下: 关于diagrams 的一些小技巧: 登入的网页是:Flowchart Maker & Online Diagram Software 1: 利用group 的选项,这个可以整体移动,不用担心会漏掉一个: 就是选中一个图标,然后,看右边arrange 下面…

20230123使AIO-3568J开发板在Android12下永不休眠

20230123使AIO-3568J开发板在Android12下永不休眠 2023/1/23 13:59 1、 Z:\android12-rk3568-new\device\rockchip\common\device.mk # Bluetooth HAL PRODUCT_PACKAGES \ libbt-vendor \ android.hardware.bluetooth1.0-impl \ android.hardware.bluetooth1.0-se…

Hadoop基础之《(1)—大数据基本概念》

一、Hadoop 1、Hadoop大数据框架&#xff0c;处理分布式环境下数据存储和计算 2、Hadoop的HDFS处理存储 3、Hadoop的MapReduce处理计算 map让任务数据拆分到每一台去执行 reduce处理后的任务合并 4、Hive作用是在Hadoop上能够让用户来写SQL处理数据 Hive的执行引擎&#xff0c;…

深度学习TensorFlow—GPU2.4.0版环境配置,一文简单易懂详细大全,CUDA11.0、cuDNN8.0

深度学习TensorFlow—GPU2.4.0版环境配置&#xff0c;一文简单易懂详细大全&#xff0c;CUDA11.0、cuDNN8.0 前提&#xff1a;电脑拥有英伟达独立显卡!!!&#xff0c;并且安装了anaconda&#xff01;&#xff01;&#xff01; 前提&#xff1a;电脑拥有英伟达独立显卡!!!&…

vue事件车之兄弟组件之间传值

目录前言一&#xff0c;全局事件总线介绍1.1 原理介绍1.2 x需要满足的条件二&#xff0c;知识点的复习2.1 vc是什么2.2 vm管理vc如何体现2.3 原型2.4 上述知识的串联三&#xff0c;实现需求3.1 x的编写及讲解3.2 使用x四&#xff0c;标准写法4.1 写法改动4.2 销毁五 关键代码后…

兔年首文迎新春-Cesium橘子洲烟花礼赞

兔年新春今天是兔年大年初二&#xff0c;神州大地&#xff0c;在经历了疫情的三年后迎来开放的一个春节。大家都沉浸在欢乐幸福的春节气氛中。玉兔迎新春&#xff0c;祝福齐送到&#xff1a;白兔祝你身体安康&#xff0c;黑兔祝你薪水高涨&#xff0c;灰兔送你梦想如意&#xf…

Maven高级

Maven高级 1&#xff0c;分模块开发 1.1 分模块开发设计 (1)按照功能拆分 我们现在的项目都是在一个模块中&#xff0c;比如前面的SSM整合开发。虽然这样做功能也都实现了&#xff0c;但是也存在了一些问题&#xff0c;我们拿银行的项目为例来聊聊这个事。 网络没有那么发…

Java多线程03——等待唤醒机制(and阻塞队列实现)

目录1.等待唤醒机制1.ThreadDemo2.Desk3.Cook4.Foodie2.等待唤醒机制&#xff08;阻塞队列方式实现&#xff09;1.ThreadDemo022.Cook023.Foodie023.线程的状态1.等待唤醒机制 生产者和消费者 桌子上有食物&#xff0c;消费者吃&#xff0c;桌子上没有食物&#xff0c;消费者等…

ElasticSearch 索引模板 组件模板 组合模板详细使用介绍

索引模板_template 文章目录索引模板_templateTemplate 介绍索引模板Index Template参数说明创建一个索引模板 Index Template测试不存在的索引直接添加数据创建索引总结组合索引模板 Index Template 7.8版本之后引入创建基于组件模板的索引模板 Index Template创建组件模板模拟…

LeetCode103_ 103. 二叉树的锯齿形层序遍历

LeetCode103_ 103. 二叉树的锯齿形层序遍历 一、描述 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff0c;层与层之间交替进行&#xff09;。 示例 1&…

【头歌】顺序表的基本操作

第1关&#xff1a;顺序表的插入操作任务描述本关任务&#xff1a;编写顺序表的初始化、插入、遍历三个基本操作函数。相关知识顺序表的存储结构顺序表的存储结构可以借助于高级程序设计语言中的数组来表示&#xff0c;一维数组的下标与元素在线性表中的序号相对应。线性表的顺序…

YOLOv5/v7 引入 YOLOv8 的 C2f 模块

YOLOv8 项目地址&#xff1a;https://github.com/ultralytics/ultralytics YOLOv8 Ultralytics YOLOv8 是由 Ultralytics 开发的一个前沿的 SOTA 模型。它在以前成功的 YOLO 版本基础上&#xff0c;引入了新的功能和改进&#xff0c;进一步提升了其性能和灵活性。YOLOv8 基于快…

C 指针变量 取地址符的用法 *指针变量名的用法

文章目录IntroCode图示Intro C语言中有一类特殊的变量&#xff1a;指针变量(pointer variable)&#xff0c;用于存储某个变量的内存地址的值。 要打印指针变量一般用%p格式符&#xff0c;会打印出该指针变量的值&#xff0c;即一个内存值。 Code // Created by wuyujin1997 …

【Linux】进程概念一

进程概念一 冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系。 截止目前为止&#xff0c; 我们所认识的计算机&#xff0c;都是一个个的硬件组成 输入设备&#xff1a;包括键盘&#x…

如何使用JDBC操作数据库?JDBC API的使用详细解读

文章目录1. DriverManager1.1 注册驱动1.2 获取连接2. Connection2.1 获取执行sql的对象2.2 事务管理3. Statement4. ResultSet5. PreparedStatement5.1 sql注入问题5.2 preparedStatement 原理6. 总结Java编程基础教程系列1. DriverManager DriverManager &#xff0c;驱动管…

C++模板不支持分离编译的问题

目录前言分离编译模式普通函数的分离编译(正常)模板函数的分离编译(出错)分析解决方式拓展--extern关键字extern"C"extern变量extern模板--控制实例化前言 分离编译模式 一个项目如果有多个源文件.c组成&#xff0c;每个源文件单独编译&#xff0c;形成目标文件。最…