深度解读《深度探索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],zero
  movsd   qword ptr [rbp - 16], xmm0
  cvttsd2si       eax, qword ptr [rbp - 16]
  mov     dword ptr [rbp - 28], eax
  lea     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.mfbz.cn/a/599193.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

项目计划书(Word原件)

项目开发计划包括项目描述、项目组织、成本预算、人力资源估算、设备资源计划、沟通计划、采购计划、风险计划、项目过程定义及项目的进度安排和里程碑、质量计划、数据管理计划、度量和分析计划、监控计划和培训计划等。 软件资料清单列表部分文档&#xff1a; 工作安排任务书…

如何有效识别限界上下文?

在实施DDD的过程中&#xff0c;识别限界上下文是一大难点&#xff0c;但也并非无章可循。在本文内容中&#xff0c;我们将分别从业务维度、工作维度以及技术维度进行展开&#xff0c;讨论如何有效识别限界上下文的方法和技巧。 从业务维度识别限界上下文 从业务维度识别限界上…

羊大师解析,鲜为人知的羊奶冷知识

羊大师解析&#xff0c;鲜为人知的羊奶冷知识 羊奶的脂肪球更小&#xff1a;相较于牛奶&#xff0c;羊奶中的脂肪球直径更小&#xff0c;这有助于其更快地被人体消化和吸收。 羊奶含有更多的中链脂肪酸&#xff1a;羊奶中含有较多的中链脂肪酸&#xff08;MCT&#xff09;&am…

安装nginx-1.25.5与ngx_http_headers_more_filter_module模块

#下载nginx的代码 curl -O http://nginx.org/download/nginx-1.25.5.tar.gz #下载headers-more-nginx-module代码 git clone https://github.com/openresty/headers-more-nginx-module#解压 tar -xzf nginx-1.25.5.tar.gzcd nginx-1.25.5#--add-dynamic-module 下载下来的目录 …

Al Agent:开启智能化未来的关键角色,让机器更智能的为我们服务

文章目录 &#x1f680;Al Agent是什么&#x1f4d5;Al Agent的工作原理与技术&#x1f4aa;Al Agent应用领域&#x1f680;智能家居应用&#x1f308;医疗健康领域⭐金融服务行业&#x1f302;交通运输管理&#x1f3ac;教育培训应用 &#x1f512;Al Agent优势与挑战✊Al Age…

移动端自适应

基本实现核心思想 基本原则上是&#xff0c;布局更多地使用flex&#xff0c;然后尺寸使用rem&#xff0c;vw&#xff0c;vh为单位如果是根据不同的屏幕需要有不同的布局了&#xff0c;一般通过检测屏幕尺寸换不同的站点或者媒体查询使用css rem 以html字体太小为1rem的大小&…

LM4562NA 直插DIP8双运放 音频hifi运算放大器

LM4562NA是一款高性能音频运算放大器&#xff0c;其应用领域主要集中在音频和声音处理方面&#xff0c;包括但不限于&#xff1a; 1. 专业录音设备&#xff1a;在录音棚、广播电台和电视台等专业环境中&#xff0c;用于信号放大和处理&#xff0c;确保高质量的声音录制和传输…

揭秘数据可视化:五款利器助力决策

在当今这个数据驱动的时代&#xff0c;数据可视化已成为企业决策、数据分析不可或缺的一部分。通过直观、生动的图形、图像&#xff0c;数据可视化能够更快速、更准确地传达信息&#xff0c;帮助企业洞察数据背后的价值。本文将为您介绍几款优秀的数据可视化工具。 一、山海鲸…

docker-compose编排集成工具,consul服务更新与发现

一、引言 我们知道使用一个 Dockerfile 模板文件可以定义一个单独的应用容器&#xff0c;如果需要定义多个容器就需要服务编排。服务编排有很多种技术方案&#xff0c;今天给大家介绍 Docker 官方产品 Docker-Compose Dockerfile 可以定义一个单独的应用容器&#xff1…

图片编辑工具-Gimp

一、前言 GIMP&#xff08;GNU Image Manipulation Program&#xff09;是一款免费开源的图像编辑软件&#xff0c;具有功能强大和跨平台的特性。 GIMP作为一个图像编辑器&#xff0c;它提供了广泛的图像处理功能&#xff0c;包括但不限于照片修饰、图像合成以及创建艺术作品…

uni-app安卓本地打包个推图标配置

如果什么都不配置&#xff0c;默认的就是个推小鲸鱼图标 默认效果 配置成功效果 个推图标配置 新建目录 drawable-hdpi、drawable-ldpi、drawable-mdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxhdpi 目录中存放图标 每个目录中存放对应大小的图标&#xff0c;大图…

Day28:ElasticSearch入门、Spring整合ES、开发社区搜索功能

ElasticSearch入门 Elasticsearch简介 一个分布式的、Restful风格的搜索引擎。支持对各种类型的数据的检索&#xff08;非结构化的也可以&#xff09;。搜索速度快&#xff0c;可以提供实时的搜索服务。便于水平扩展&#xff08;集群式部署&#xff09;&#xff0c;每秒可以处…

分享三维地理模型制作实践

前言 地理信息系统&#xff08;GIS&#xff09;是一种用于捕获、存储、检查和显示与地球表面位置相关的数据的计算机系统。GIS可以在一张地图上显示许多不同类型的数据&#xff0c;如街道、建筑物和植被。这使人们能够更容易地看到、分析和理解模式和关系。 实践 从地理空间…

正在载入qrc文件 指定的qrc文件无法找到。您想更新这个文件的位置么?

打开Qt的ui文件&#xff0c;弹出提示框 如果需要用到qrc文件&#xff0c;选择Yes&#xff0c;再选择qrc文件所在的位置&#xff1b;如果不需要qrc文件&#xff0c;可以选择No&#xff0c;然后用普通文本编辑器打开&#xff0c;将“ <resources> <include location&q…

经典面试题---环形链表

1. 环形链表1. - 力扣&#xff08;LeetCode&#xff09; 要解决这道题&#xff0c;我们首先要挖掘出带环的链表与不带环的链表之间的差别。 以此&#xff0c;才能设计出算法来体现这种差别并判断。 二者最突出的不同&#xff0c;就是不带环的链表有尾结点&#xff0c;也就是说…

Golang | Leetcode Golang题解之第71题简化路径

题目&#xff1a; 题解&#xff1a; func simplifyPath(path string) string {stack : []string{}for _, name : range strings.Split(path, "/") {if name ".." {if len(stack) > 0 {stack stack[:len(stack)-1]}} else if name ! "" &am…

Android 系统启动流程源码分析

一、Init进程启动 是一个由内核启动的用户级进程。内核自行启动之后&#xff0c;就通过启动一个用户级程序init的方式&#xff0c;完成引导进程。 启动的代码init.c中的main函数执行过程&#xff1a;system\core\init.c中&#xff1a; 主要下面两个重要的过程&#xff1a; 1…

泰克示波器如何存储CSV文件?

泰克示波器可以用于各种信号的测量和分析。在实际测试中&#xff0c;我们经常需要将示波器采集到的波形数据保存下来&#xff0c;以便后续的处理和分析。泰克示波器提供了多种方法来存储波形数据&#xff0c;其中一种常用的方式是将数据保存为CSV文件。下面将介绍泰克示波器如何…

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…

成功案例(IF=7.3)| 转录组+蛋白质组+代谢组联合分析分析揭示胰腺癌中TAM2相关的糖酵解和丙酮酸代谢重构

研究背景 肿瘤的进展和发展需要癌细胞的代谢重编程&#xff0c;癌细胞能量代谢模式的改变可以满足快速增殖和适应肿瘤微环境的需要。肿瘤微环境&#xff08;TME&#xff09;中的代谢状态受到多种因素的影响&#xff0c;包括血管生成、与其他细胞的相互作用和系统代谢。代谢异质…
最新文章