深度解读《深度探索C++对象模型》之C++的临时对象(一)

目录

暂存函数返回结果的临时对象

表达式运算过程产生的临时对象


接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,或文章末尾扫描二维码,自动获得推文和全部的文章列表。

        所谓临时对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。编译器根据程序的需要可能会安插一些临时变量来支持程序的运行,这些动作是在程序员不可感知的背后默默进行,所以我们有必要了解编译器在背后的所作所为。这些动作有时是为了转换原来的代码的语义以保证代码能顺利地编译通过,有的是为了程序运行的正确性而暂存的对象。有两种情形一般会产生临时对象,一种是暂存函数调用的返回结果,一种是计算表达式的过程中暂存运算结果。下面将根据这两种情况来展开分析。

暂存函数返回结果的临时对象

        先看一个例子:

#include <cstdio>class Object {
public:Object() { printf("%s, this = %p\n", __PRETTY_FUNCTION__, this); }Object(const Object& rhs) : i(rhs.i) {printf("%s, this = %p, rhs = %p\n", __PRETTY_FUNCTION__, this, &rhs);}~Object() { printf("%s, this = %p\n", __PRETTY_FUNCTION__, this); }Object& operator=(const Object& rhs) {printf("%s, this = %p, rhs = %p\n", __PRETTY_FUNCTION__, this, &rhs);i = rhs.i;return *this;}int i = 0;
};Object operator+(const Object& obj1, const Object& obj2) {printf("%s, obj1 = %p, obj2 = %p\n", __PRETTY_FUNCTION__, &obj1, &obj2);Object result;result.i = obj1.i + obj2.i;return result;
}int main() {Object a;Object b;Object c = a + b;printf("c.i = %d\n", c.i);return 0;
}

        编译时暂时先关闭掉优化选项,加上编译选项“-fno-elide-constructors”,程序的输出结果:

Object::Object(), this = 0x16ba673f8	// 构造a
Object::Object(), this = 0x16ba673f4	// 构造b
Object operator+(const Object &, const Object &), obj1 = 0x16ba673f8, obj2 = 0x16ba673f4
Object::Object(), this = 0x16ba673a4	// 构造result
// 以下调用拷贝构造临时对象0x16ba673dc
Object::Object(const Object &), this = 0x16ba673dc, rhs = 0x16ba673a4
Object::~Object(), this = 0x16ba673a4	// 析构result
// 以下调用拷贝构造对象c
Object::Object(const Object &), this = 0x16ba673e0, rhs = 0x16ba673dc
Object::~Object(), this = 0x16ba673dc	// 析构临时对象
c.i = 0
Object::~Object(), this = 0x16ba673e0	// 析构c
Object::~Object(), this = 0x16ba673f4	// 析构b
Object::~Object(), this = 0x16ba673f8	// 析构a

        可以看到,上面的程序中编译器产生了一个临时对象,即地址为0x16ba673dc(第6行打印)。首先有一点要注意的是在operator+函数里的result变量不是临时对象,它是一个局部变量,这是我们自己定义的具名对象,而临时对象是编译器产生的。在这个程序里,编译器用了一个临时对象来保存函数的运行结果,它是以局部对象result为初值调用拷贝构造函数构造出来的,然后编译器再次调用拷贝构造函数将它拷贝给对象c(第9行打印),随后这个临时对象就被释放掉了(第10行打印)。

        是否会产生临时对象跟编译器的实现有关,不同的编译器可能采取不同的策略,上面的情形只是编译器可能采用的策略之一,另外编译器也有可能采用另外的优化策略,比如将临时对象构造到局部对象result中,这样即可以减少调用一次构造函数和一次析构函数,也有可能采用更激进的优化手法,直接将c的地址传递给operator+函数,直接在函数里构造对象c,优化掉局部对象result和临时对象。第三种情形即是之前文章“深度解读《深度探索C++对象模型》之返回值优化”里讲过的,当类中有定义了拷贝构造函数时会触发编译器启用NRV优化。我们把优化选项打开,即把编译选项“-fno-elide-constructors”去掉,重新编译后输出:

Object::Object(), this = 0x16ee6f3f8	// 构造a
Object::Object(), this = 0x16ee6f3f4	// 构造b
Object operator+(const Object &, const Object &), obj1 = 0x16ee6f3f8, obj2 = 0x16ee6f3f4
Object::Object(), this = 0x16ee6f3e0	// 构造c
c.i = 0
Object::~Object(), this = 0x16ee6f3e0	// 析构c
Object::~Object(), this = 0x16ee6f3f4	// 析构b
Object::~Object(), this = 0x16ee6f3f8	// 析构a

        从输出看到对象c直接在operator+函数里构造了,这比之前少调用了两次拷贝构造函数和两次析构函数,因为不需要构造局部对象result和临时对象了。

        上面的代码在启用NRV优化后,临时对象就被编译器优化掉了,但在另外的一种情形下,临时对象却不能够被省略掉,将上面代码main函数做如下的修改:

// 将 Object c = a + b; 修改为:
Object c;
c = a + b;

        当然上面的代码只是为了举例,实际的代码中可能是对象c在这里定义,然后在另外的地方重新给它赋值。将上面的代码修改后重新编译,在同样启用优化选项的情况下,程序的输出结果:

Object::Object(), this = 0x16f5a73f8	// 构造a
Object::Object(), this = 0x16f5a73f4	// 构造b
Object::Object(), this = 0x16f5a73e0	// 构造c
Object operator+(const Object &, const Object &), obj1 = 0x16f5a73f8, obj2 = 0x16f5a73f4
Object::Object(), this = 0x16f5a73dc	// 构造临时对象
Object &Object::operator=(const Object &), this = 0x16f5a73e0, rhs = 0x16f5a73dc
Object::~Object(), this = 0x16f5a73dc	// 析构临时对象
c.i = 0
Object::~Object(), this = 0x16f5a73e0	// 析构c
Object::~Object(), this = 0x16f5a73f4	// 析构b
Object::~Object(), this = 0x16f5a73f8	// 析构a

        为什么说上面第5行的打印是构造临时对象而不是构造局部对象result?因为如果构造的是result,那么它在operator+函数调用结束时就会被析构掉了,而这里却是等到operator=函数调用完成后才析构它,说明它确实是一个临时对象,关于临时对象的存活周期下面再讲。这里编译器实际上也是做了一个优化,就是将临时对象构造在result对象上了,这样就省略了一次拷贝构造函数和一次析构的调用。上面的代码实际上是被转换成:

Object c;
c = a + b;
// 将被转换为:
Object tmp = a + b;	// 即 operator+(tmp, a, b); 函数内直接构造tmp对象
c = tmp;			// 即 c.operator=(tmp);
tmp.Object::~Object();

        那么这里编译器为什么不再进一步优化,一定要保留临时对象呢?因为此时不能像采用NRV优化那样将对象c的地址传递给operator+函数,然后在operator+函数内直接构造对象c,因为这样做的前提是对象的空间是一块崭新的内存空间,但是此时对象c已经被构造过了,那就需要先调用它的析构函数,因为有可能在对象c的构造函数里有申请了系统资源,如果没有先释放掉这些系统资源就重新构造它就会造成系统资源的泄漏或者其他的运行错误。那么编译器是否可以将赋值语句(调用赋值运算符函数operator=)转换为一系列的调用析构、然后再构造的语句呢?如下面这样:

c.Object::~Object();	// 先析构对象c
c.Object::Object(a + b)	// 重新构造对象c

        答案是这种转换所得的结果并不一定是等同的,因为上述的拷贝构造函数、析构函数和拷贝赋值运算符函数operator=都可以是程序员定义的,编译器不能理解程序员的意图,比如程序员的预期是上面的赋值语句会调用到operator=函数,假如他需要在operator=函数里做一些事情,而此时如果把它转换成调用析构加拷贝构造函数了,这就违背了程序员本来的意图,所以这可能是一个错误的优化行为。

表达式运算过程产生的临时对象

        在表达式的运算过程中有可能也会产生临时变量,比如当运算需要进行类型转换时,或者是暂存子表达式的运算结果。

  • 类型转换产生的临时变量
int main() {double d = 3.14;const int &ri = d;return 0;
}

        上面的代码将会产生一个临时变量,double类型的变量d会先转换成int类型的值然后暂存在一个临时变量,引用ri绑定的是这一个临时变量,可以来看看编译器产生的汇编代码:

.LCPI0_0:.quad   0x40091eb851eb851f		# double 3.1400000000000001
main:                           # @main# 略...movsd   xmm0, qword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zeromovsd   qword ptr [rbp - 16], xmm0cvttsd2si       eax, qword ptr [rbp - 16]mov     dword ptr [rbp - 28], eaxlea     rax, [rbp - 28]mov     qword ptr [rbp - 24], rax# 略...

        cvttsd2si是一个SSE扩展指令,它的作用是取出一个64位的浮点值,并截断为一个64位的整型。上面第7行代码就是将d截断为整型并保存在eax寄存器中,然后第8行再截断为32位(dword类型)的int类型并保存在栈空间[rbp - 28]中,这个即是编译器自动产生的临时变量,第9、10行是取得它的地址并赋值给[rbp - 24],即ri变量的位置。为什么需要产生一个临时变量?因为此处ri引用的是一个int类型的数,对ri的操作应该是整型的运算,但d却是一个double类型的浮点数,因此为了确保ri绑定到一个整数,编译器就产生了一个整型的临时变量,让ri绑定到它。顺带提一下,临时对象实际上是一个右值,它不允许被修改,所以这里的ri引用必须是const引用,如果这里去掉const,编译则会通不过。

  • 暂存运算结果产生的临时对象

        假如我们给上小节中例子的Object类增加一个类型转换函数:

class Object {
public:// 其它不变,新增如下函数operator int() { return i; }
};

        再假设有这样一段代码:

if ( a + b > 10) {// do something
}

        a + b将会产生一个临时对象,然后再在此临时对象之上实施int()类型转换,最后再与10比较大小。看一下它对应的汇编代码:

# 省略掉其它无关的代码
lea     rdi, [rbp - 48]
lea     rsi, [rbp - 8]
lea     rdx, [rbp - 16]
call    operator+(Object const&, Object const&)
lea     rdi, [rbp - 48]
call    Object::operator int()
mov     dword ptr [rbp - 52], eax       # 4-byte Spill
lea     rdi, [rbp - 48]
call    Object::~Object() [base object destructor]
mov     eax, dword ptr [rbp - 52]       # 4-byte Reload
cmp     eax, 10

        从省略掉的代码里知道[rbp - 8]存放的是对象a,[rbp - 16]存放的是对象b,[rbp - 48]其实存放的就是临时对象。上面代码的第2到第5行,相当于下面的伪代码:

operator+(&tmp, &a, &b);

        相当于operator+函数的返回结果直接构造在临时对象tmp上。之后的第6到第8行是调用类型转换函数int()并将返回值eax暂存在栈空间[rbp - 52]中,然后这个临时对象就销毁了。

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

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

相关文章

TypeScript常见面试题第六节

题目二十六:TypeScript 中的装饰器? 一、讲解视频 TS面试题二十六:TypeScript 中的可选链? 二、题目解析 本题目考察可选链的相关知识,可选链是比较新的一个语法,是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。如果可选链 ?. 前面的值为…

effective python学习笔记_pythonic思维

查py版本 import sys sys.version sys.version_info 遵循PEP8 PEP8,Python Enhancement Proposal #8 章节列了几个点&#xff0c;也可以看原文PEP 8 – Style Guide for Python Code | peps.python.org 导包顺序建议&#xff1a;先导标准库模块&#xff0c;再导三方模块&…

论文查重率高,有什么办法降重吗?推荐笔灵AI

现在大部分学校已经进入到论文查重降重的阶段了。如果查重率居高不下&#xff0c;延毕的威胁可能就在眼前。对于即将告别校园的学子们&#xff0c;这无疑是个噩梦。四年磨一剑&#xff0c;谁也不想在最后关头功亏一篑。 查重率过高&#xff0c;无非以下两种原因。要么是作为“…

LMdeploy推理实践

在inter-studio平台上&#xff0c;下载模型&#xff0c;体验lmdeploy 下载模型 这里是因为平台上已经有了internlm2模型&#xff0c;所以建立一个符号链接指向它&#xff0c;没有重新下载 ln -s /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-1_8b /root/如…

算法提高之炮兵阵地

算法提高之炮兵阵地 核心思想&#xff1a;状态压缩dp 滚动数组优化 考虑前两层状态 用f[i,j,k]表示前i层&#xff0c;第i层状态j&#xff0c;第i-1层状态k的方案同“玉米田” g存图1是不能放的位置**滚动数组优化&#xff1a;**所有第一维n或者n1 &1 #include <iost…

论文查重率高,有什么办法降重吗?推荐几个ai降重工具

现在大部分学校已经进入到论文查重降重的阶段了。如果查重率居高不下&#xff0c;延毕的威胁可能就在眼前。对于即将告别校园的学子们&#xff0c;这无疑是个噩梦。四年磨一剑&#xff0c;谁也不想在最后关头功亏一篑。 查重率过高&#xff0c;无非以下两种原因。要么是作为“…

YOLOv8检测图片和视频

一、检测图片 Python import cv2 from ultralytics import YOLO import torchmodel_path object_detection/best.pt # Change this to your YOLOv8 models path image_path object_detection/32.jpg # Change this to your videos path# Load the trained YOLOv8 model m…

迅为RK3568开发板资料说明4750+页专属文档专为3568编写

iTOP-3568开发板采用瑞芯微RK3568处理器&#xff0c;内部集成了四核64位Cortex-A55处理器。主频高达2.0Ghz&#xff0c;RK809动态调频。集成了双核心架构GPU&#xff0c;ARM G52 2EE、支持OpenGLES1.1/2.0/3.2、OpenCL2.0、Vulkan1.1、内嵌高性能2D加速硬件。 内置独立NPU,算力…

VINS预积分与误差模型

文章目录 IMU的测量值误差模型IMU预积分真实模型IMU预积分估计模型误差模型普通增量积分中值积分法 参考文献 IMU的测量值误差模型 IMU的测量值误差模型&#xff1a; a ^ t a t R w t g w b a t n a t ω ^ t ω t b ω t n ω t \begin{array}{} {{{\hat a}_t} {a_t…

debian下使用的常用软件

debian下使用的软件与windows下有所不同&#xff0c;刚使用debian系统&#xff0c;需要对不同的软件进行试用&#xff0c;以期找到更符合自己要求的软件&#xff0c;现在试用情况记录在此&#xff0c;以便后期回顾。 文件管理器&#xff08;Thunar&#xff09; 网络浏览器&am…

揭秘 IEEE/ACM Trans/CCF/SCI,谁才是科研界的王者?

会议之眼 快讯 在学术探索的浩瀚星海中&#xff0c;每一篇论文都像是一颗璀璨的星辰&#xff0c;而那些被顶级期刊或会议收录的论文&#xff0c;则无疑是最耀眼的几颗。 在众多评价标准中&#xff0c;IEEE/ACM Transactions、CCF推荐期刊和会议、SCI分区期刊&#xff0c;它们…

RVM(相关向量机)、CNN_RVM(卷积神经网络结合相关向量机)、RVM-Adaboost(相关向量机结合Adaboost)

当我们谈到RVM&#xff08;Relevance Vector Machine&#xff0c;相关向量机&#xff09;、CNN_RVM&#xff08;卷积神经网络结合相关向量机&#xff09;以及RVM-Adaboost&#xff08;相关向量机结合AdaBoost算法&#xff09;时&#xff0c;每种模型都有其独特的原理和结构。以…

【Delphi7】Access violation at address 0019F7C3. Write of address 0019F7C3.

这里写目录标题 问题基本情况问题描述1、启动Delphi 开发程序 时连续报如下错误2、打开“工程”菜单下的“选项”页面时时连续报如下错误 解决方案1、打开“高级系统设置”2、打开“性能选项”3、添加“数据执行保护”的程序4、选择“数据执行保护”的程序5、应用“数据执行保护…

kafka学习笔记(三、生产者Producer使用及配置参数)

1.简介 1.1.producer介绍 生产者就是负责向kafka发送消息的应用程序。消息在通过send()方法发往broker的过程中&#xff0c;有可能需要经过拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)的一系列作用后才能被真正的发往broker。 demo: public class Kafk…

LeetCode算法题:7. 整数反转

给你一个 32 位的有符号整数 x &#xff0c;返回将 x 中的数字部分反转后的结果。 如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1] &#xff0c;就返回 0。 假设环境不允许存储 64 位整数&#xff08;有符号或无符号&#xff09;。 示例 1&#xff1a; 输…

武汉星起航:自运营团队—亚马逊平台运营典范,优势凸显业绩斐然

武汉星起航电子商务有限公司&#xff0c;作为跨境电商领域的佼佼者&#xff0c;凭借自运营团队多年的深耕经验与对亚马逊市场规则的深刻理解&#xff0c;成功在亚马逊平台开设多家自营店铺&#xff0c;并取得显著成绩。公司月流水达到几百万的辉煌业绩&#xff0c;不仅彰显了其…

嵌入式学习<1>:建立工程、GPIO

嵌入式学习_part1 本部分笔记用于学习记录&#xff0c;笔记源头 >>b站江科大_STM32入门教程 建立工程、GPIO 开发环境&#xff1a;keil MDK、STM32F103C8T6 1 &#xff09;建立工程 &#xff08;1&#xff09;基于寄存器开发、基于标准库 或者 基于HAL库开发; &…

IDEA无法下载远程仓库jar包问题

问题描述&#xff1a; idea无法下载远程仓库jar包&#xff0c;最奇怪的是idea有多个项目&#xff0c;有些项目可以下载&#xff0c;有些项目不行。报错如下&#xff1a; 一开始&#xff1a; unable to find valid certification path to requested target Try run Maven impo…

UV胶是什么材料制成的?

UV胶是一种特殊的胶水&#xff0c;由丙烯酸酯单体、活性稀释剂、光引发剂、助剂等材料制成的。它是指在紫外线照射下能够迅速固化的胶水。UV胶的主要成分是丙烯酸酯单体&#xff0c;它在没有紫外线照射时是液体状态&#xff0c;但一旦受到紫外线照射&#xff0c;就会迅速发生聚…

使用PL/SQL动态查询并输出结果

在Oracle数据库中&#xff0c;我们经常需要编写PL/SQL脚本来执行动态SQL查询。这里我们简化了一个示例&#xff0c;用于动态查询特定月份下以特定模式命名的表&#xff0c;并输出查询到的记录数。 动态构建SQL并执行 使用循环来动态构建SQL语句&#xff0c;并执行查询以获取记…