构造、析构、拷贝语意学

2024-01-27 01:48
文章标签 构造 拷贝 析构 语意

本文主要是介绍构造、析构、拷贝语意学,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 一、构造
    • 1.1、"无继承"情况下的对象构造
      • 1.1.1、抽象数据类型
      • 1.1.2、为继承做准备
    • 1.2、继承体系下的对象构造
      • 1.2.1、虚继承
      • 1.2.2、vptr初始化语意学
  • 二、拷贝
  • 三、析构
  • 四、总结


一、构造

1.1、"无继承"情况下的对象构造

考虑下面代码:
在这里插入图片描述
L1、L5、L6表现出三种不同的对象产生方式:global内存配置、local内存配置和heap内存配置。一个对象的声明,是该对象的一个执行期属性。local对象的声明从L5的定义开始,到L10结束。global对象的声明和整个程序的生命相同。heap对象的生命从它被new运算符配置出来开始,到它被delete运算符销毁为止。

下面是Point的声明,可以写成C程序。C++ Standard说这是一种所谓的Plain old data(点击查看plain old data)。
在这里插入图片描述
如果我们以C++ 来编译这段代码,会发生什么事情?观念上,编译器会为Point声明一个trivial default constructor、一个trivial destructor、一个trivial copy constructor,以及一个trivial copy assignment operator。但实际上,编译器会分析这个声明,并为它贴上plain old data标签。

当编译器遇到这样的定义:
在这里插入图片描述
观念上Point的trivial constructor和destructor都会被产生并被调用,constructor在程序起始处被调用而destructor在程序的exit()处被调用(exit()是由系统产生的,放在main()结束之前)。然而事实上那些trivial members要不就是没被定义,就是没被调用,程序的行为一如它在C中的表现一样。

foobar()函数中的L5,有一个local对象,同样也是既没有被构造也没有被析构。

heap对象在L6的初始化操作:
在这里插入图片描述
会被转换为对new运算符的调用:
在这里插入图片描述
需要注意的是,并没有default constructor施行于new运算符所传回的对象身上。

L9执行一个delete操作:
在这里插入图片描述
会被转换为对delete运算符的调用:
在这里插入图片描述
观念上,这样的操作会触发Point的trivial destructor。但一如我们所见,destructor要不是没有被产生就是没有被调用。

1.1.1、抽象数据类型

下面是Point的第二次声明,在public接口之下多了private数据,提供完整的封装性,但没有任何虚函数:
在这里插入图片描述
这个经过封装的Point类,其大小并没有改变,还是三个连续的float。我们并没有为Point定义一个copy constructor或copy operator,因为默认的位语意(default bitwise semantics)已经足够。我们也不需要提供一个destructor,因为程序默认的内存管理方法也已经足够。

观念上,我们的Point类有一个相关的default copy constructor、copy operator和destructor。然而它们都是trivial,而且编译器实际上根本没有产生它们。

1.1.2、为继承做准备

下面是Point的第三次声明,将为继承性质以及某些操作的动态决议(dynamic resolution)做准备。
在这里插入图片描述
我们并没有定义一个copy constructor、copy operator、destructor。我们的所有成员都以数值来存储,因此在程序层面的默认语意下,行为良好。

虚函数的导入促使每一个Point对象拥有一个虚函数表指针(vptr),这个指针给我们提供virtual接口的弹性,其成本是:每一个对象需要额外的一个word空间。

除了每一个类对象多负担一个vptr之外,虚函数的导入也引发编译器对于我们的Point类产生膨胀作用:

  • 我们所定义的constructor被附加了一些代码,以便将vptr初始化。这些代码必须被附加在任何基类构造函数调用之后,但必须在任何由程序员提供的代码之前。例如,下面是可能的附加结果:
    在这里插入图片描述
  • 合成一个copy constructor和一个copy assignment operator,而且其操作不再是trivial。如果一个Point对象被初始化或以一个派生类对象赋值,那么以位为基础(bitwise)的操作可能对vptr带来非法设定。
    在这里插入图片描述

1.2、继承体系下的对象构造

当我们定义一个对象:T object;,实际上会发生什么事情?如果T有一个constructor,它会被调用。这很明显,比较不明显的是,constructor的调用真正伴随了什么?

constructor可能内含大量的隐藏代码,因为编译器会扩充每一个constructor,扩充程度视类T的继承体系而定。一般而言编译器所做的扩充操作大约如下:

  • 1、记录在成员初始化列表中的数据成员初始化操作会被放进constructor的函数体,并以成员的声明顺序为顺序
  • 2、如果有一个成员并没有出现在成员初始化列表之中,但它有一个default constructor,那么该default constructor必须被调用
  • 3、在那之前,如果类对象有虚函数表指针,它必须被设定初值,指向适当的虚函数表
  • 4、在那之前,所有上一层的基类构造函数必须被调用,以基类的声明顺序为顺序(与成员初始化列表中的顺序没关联):
    • 如果基类被列于成员初始化列表中,那么任何显式指定的参数都应该传递过去
    • 如果基类没有被列于成员初始化列表中,而它有default constructor,那么就调用
    • 如果基类是多重继承下的第二个或后继的基类,那么this指针必须有所调整
  • 5、在那之前,所有虚函数基类的构造函数必须被调用,从左到右,从最深到最浅:
    • 如果类被列于成员初始化列表中,那么如果有任何显式指定的参数,都应该传递过去。若没有列于成员初始化列表中,而类有一个default constructor,亦应该调用
    • 此外,类中的每一个虚基类子对象的偏移位置必须在执行期可被存取
    • 如果类对象是最底层(most-derived)的类,其构造函数可能被调用;某些用以支持这一行为的机制必须被放进来

接下来,我们从"C++ 语言对类所保证的语意"这个角度,探讨constructor扩充的必要性。再次以Point为例,并为它增加一个copy constructor、一个copy operator、一个virtual destructor,如下所示:
在这里插入图片描述
在我们开始介绍并一步步走过以Point为根源的继承体系之前,先很快地看看Line类的声明和扩充结果,它由_begin和_end两个点构成:
在这里插入图片描述
每一个explicit constructor都会被扩充以调用其两个成员类对象的constructor。如果我们定义constructor如下:
在这里插入图片描述
它会被编译器扩充并转换为:
在这里插入图片描述
由于Point声明了一个copy constructor、一个copy operator,以及一个destructor,所以类Line的implicit copy constructor、copy operator和destructor都将有具体效用(nontrivial)。

当写下这样的代码:Line a;时,implicit Line destructor会被合成出来。其中,它的成员类对象的destructor会被调用(以其构造的相反顺序):
在这里插入图片描述
类似的道理,当我们写下:Line b = a;时,implicit Line copy constructor会被合成出来,成为一个inline public member。

当我们写下:a = b;时,implicit copy assignment operator会被合成出来,成为一个inline public member。

1.2.1、虚继承

考虑下面这个继承体系:
在这里插入图片描述
传统的constructor扩充并没有用,因为虚基类的共享性之故。试想以下三种类的派生情况:
在这里插入图片描述
Vertex的constructor必须也调用Point的constructor。然而,当Point3d和Vertex同为Vertex3d的subobject时,它们对Point constructor的调用操作一定不可以发生;取而代之的是,作为一个最底层的类,Vertex3d有责任将Point初始化。而更往下的继承,则由PVertex来负责完成被共享的Point subobject的构造。

在这里存在某种状态,在此状态中,虚基类的构造函数被调用有着明确的定义:只有当一个完整的类对象被定义出来时,它才会被调用;如果对象只是某个完整的对象的subobject,它就不会被调用。

1.2.2、vptr初始化语意学

在继承体系中,虚函数的调用是由虚函数候选列表决定的。而虚函数候选列表又由vptr决定,所以为了控制一个类中有所作用的函数,编译系统只要简单地控制住vptr的初始化和设定操作即可。当然,设定vptr是编译器的责任,任何程序员都不必操心此事。

vptr初始化操作应该如何处理呢?答案是在基类构造函数调用操作之后,但是在程序员提供的代码或是成员初始化列表中所列的成员初始化操作之前。

令每一个基类构造函数设定其对象的vptr,使它指向相关的virtual table之后,构造中的对象就可以严格而正确地变成构造过程中所幻化出来的每一个类的对象。也就是说,一个PVertex对象会先形成一个Point对象、一个Point3d对象、一个Vertex对象、一个Vertex3d对象,然后才成为一个PVertex对象。在每一个基类构造函数中,对象可以与构造函数的类的完整对象做比较。构造函数的执行算法通常如下:

  • 1、在派生类构造函数中,所有的虚基类及上一层的基类的构造函数会被调用
  • 2、上述完成之后,对象的vptr被初始化,指向相关的virtual table
  • 3、如果有成员初始化列表,将在构造函数体内扩展开来。这必须在vptr被设定之后,以免有一个虚成员函数被调用。
  • 4、最后,执行程序员所提供的代码

但是有两种情况下,vptr必须被设定:

  • 1、当一个完整的对象被构造时。如果我们声明一个Point对象,则Point constructor必须先设定其vptr
  • 2、当一个subobject constructor调用了一个虚函数时

二、拷贝

以Point类为例:
在这里插入图片描述
没有什么理由需要禁止拷贝一个Point对象。问题是:默认行为是否足够?如果我们要支持的只是一个简单的拷贝操作,那么默认行为不但足够而且有效率,我们没有理由再自己提供一个copy assignment operator。

只有在默认行为所导致的语意不安全或不正确时,我们才需要设计一个copy assignment operator。如果我们不对Point供应一个copy assignment operator,而只是依赖默认的memberwise copy,编译器会产生出一个实例吗?这个答案和copy constructor(有关copy constructor的详细情况,请参看构造函数语意学)的情况一样:实际上不会!由于此类已经有了bitwise copy语意,所以implicit copy assignment operator被视为毫无用处,也根本不会被合成出来。

一个类对于默认的copy assignment operator,在以下情况,不会表现出bitwise copy语意:

  • 1、当类内含一个成员对象,而该成员对象所属的类有一个copy assignment operator时
  • 2、当一个类的基类有一个copy assignment operator时
  • 3、当一个类声明了任何虚函数时
  • 4、当类继承自一个虚基类时

C++ Standard上说copy assignment operator并不表示bitwise copy semantics是nontrivial。实际上,只有nontrivial instances才会被合成出来。

于是,对于我们的Point类,这样的赋值操作:
在这里插入图片描述
由bitwise copy完成,把Point b拷贝到Point a,其间并没有copy assignment operator被调用。注意,我们还是可能提供一个copy constructor,为的是把NRV打开。copy constructor的出现不应该让我们以为也一定要提供一个copy assignment operator。

现在,我们增加一个copy assignment operator,用以说明在继承之下的行为:
在这里插入图片描述
现在派生一个Point3d类:
在这里插入图片描述
如果我们没有为Point3d定义一个copy assignment operator,编译器就必须合成一个。合成的东西可能看起来像这样:
在这里插入图片描述

三、析构

如果类没有定义destructor,那么只有在类内含的成员对象(或者类的基类)拥有destructor的情况下,编译器才会自动合成出一个来。例如,我们的Point,默认情况下并没有被编译器合成出一个destructor——虽然它拥有一个虚函数:
在这里插入图片描述
类似的道理,如果我们把两个Point对象组合成一个Line类:
在这里插入图片描述
Line类也不会拥有一个被合成出来的destructor,因为Point并没有destructor。

为了决定类是否需要一个程序层面的destructor(或是constructor),请想想一个类对象的生命在哪里结束(或开始)?需要什么操作才能保证对象的完整?这是constructor和destructor什么时候起作用的关键。例如:
在这里插入图片描述
我们看到,pt和p在作为foo()函数的参数之前,都必须先初始化为某些坐标值。这时候需要一个constructor,否则使用者必须显式地提供坐标值。一般而言,类的使用者没有办法检验一个local变量或heap变量以知道它们是否被初始化。把constructor想象为程序员的一个额外负担是错误的,因为它们的工作有其必要性。如果没有它们,抽象化的使用就会有错误的倾向。

当我们显式地delete p,会如何?有任何程序上必须处理的吗?是否需要在delete之前这么做:
在这里插入图片描述
不,当然不需要。没有任何理由说在delete一个对象之前先得将其内容清楚干净。你也不需要归还任何资源。在结束pt和p的生命之前,没有任何类使用者层面的程序操作是绝对必要的。因此,也就不需要一个destructor。

一个由程序员定义的destructor被扩展的方式类似constructor被扩展的方式,但顺序相反:

  • 1、destructor的函数体首先被执行
  • 2、如果类拥有成员类对象,而该对象所属的类拥有destructor,那么它们会以其声明顺序的相反顺序被调用
  • 3、如果对象内含一个vptr,现在被重新设定,指向适当的基类virtual table
  • 4、如果有任何直接的(上一层)非虚基类拥有destructor,它们会以其声明顺序的相反顺序被调用
  • 5、如果有任何虚基类拥有destructor,而目前讨论的这个类是最底(most-derived)的类,那么它们会以其原来的构造顺序的相反顺序被调用

四、总结

在这里插入图片描述

这篇关于构造、析构、拷贝语意学的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/648705

相关文章

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(

leetcode105 从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树。 注意: 你可以假设树中没有重复的元素。 例如,给出 前序遍历 preorder = [3,9,20,15,7]中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树: 3/ \9 20/ \15 7   class Solution {public TreeNode buildTree(int[] pr

Linux 使用rsync拷贝文件

显示进度条 rsync 可以显示进度条,您可以使用 --progress 或 -P 选项来显示每个文件的传输进度和已完成文件的统计信息。 显示进度条的常用选项: --progress 选项 使用 --progress 显示每个文件的传输进度信息:rsync -av --progress /src/ /dest/ -a:归档模式,表示递归拷贝并保持文件权限、时间戳等。-v:详细模式,显示更

C++中类的构造函数调用顺序

当建立一个对象时,首先调用基类的构造函数,然后调用下一个派生类的 构造函数,依次类推,直至到达派生类次数最多的派生次数最多的类的构造函数为止。 简而言之,对象是由“底层向上”开始构造的。因为,构造函数一开始构造时,总是 要调用它的基类的构造函数,然后才开始执行其构造函数体,调用直接基类构造函数时, 如果无专门说明,就调用直接基类的默认构造函数。在对象析构时,其顺序正好相反。

python基础语法十一-赋值、浅拷贝、深拷贝

书接上回: python基础语法一-基本数据类型 python基础语法二-多维数据类型 python基础语法三-类 python基础语法四-数据可视化 python基础语法五-函数 python基础语法六-正则匹配 python基础语法七-openpyxl操作Excel python基础语法八-异常 python基础语法九-多进程和多线程 python基础语法十-文件和目录操作

插件maven-search:Maven导入依赖时,使用插件maven-search拷贝需要的依赖的GAV

然后粘贴: <dependency>    <groupId>mysql</groupId>    <artifactId>mysql-connector-java</artifactId>    <version>8.0.26</version> </dependency>

JS手写实现深拷贝

手写深拷贝 一、通过JSON.stringify二、函数库lodash三、递归实现深拷贝基础递归升级版递归---解决环引用爆栈问题最终版递归---解决其余类型拷贝结果 一、通过JSON.stringify JSON.parse(JSON.stringify(obj))是比较常用的深拷贝方法之一 原理:利用JSON.stringify 将JavaScript对象序列化成为JSO

Java构造和解析Json数据的两种方法(json-lib构造和解析Json数据, org.json构造和解析Json数据)

在www.json.org上公布了很多JAVA下的json构造和解析工具,其中org.json和json-lib比较简单,两者使用上差不多但还是有些区别。下面首先介绍用json-lib构造和解析Json数据的方法示例。 一、介绍       JSON-lib包是一个beans,collections,maps,java arrays 和XML和JSON互相转换的包,主要就是用来解析Json

理解C++全局对象析构顺序与 IPC 资源管理:避免 coredump

文章目录 0. 概述1. 问题背景2. 问题分析3. 解决方案:手动释放资源4. 深入剖析:为什么手动调用 `reset()` 有效?5. 延伸思考:如何避免全局对象带来的问题?6. 总结 0. 概述 在编写 C++ 程序时,使用全局或静态对象有时可能会导致不可预期的崩溃(如 coredump)。这类崩溃通常源于对象的析构顺序、资源的管理方式,以及底层资源(如 IPC 通道或共

HDD 顺序和随机文件拷贝和存储优化策略

对于机械硬盘(HDD),顺序拷贝和随机拷贝涉及到磁头的移动方式和数据的读取/写入模式。理解这些概念对于优化硬盘性能和管理文件操作非常重要。 1. 顺序拷贝 定义: 顺序拷贝指的是数据从硬盘的一个位置到另一个位置按顺序连续读取和写入。这意味着数据在硬盘上的位置是线性的,没有跳跃或回溯。 特点: 磁头移动最小化:由于数据是连续的,磁头在读取或写入数据时只需要在磁盘的一个方向上移动,减少了寻道时