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

目录

临时对象的生命期

特殊的情况


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

上一篇中讲解了C++编译器在什么情况下会产生临时对象,上一篇请从这里阅读:

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

这一篇来讲解临时对象的生命周期,上篇讲解何时产生临时对象,这篇讲解临时对象存在的时间以及何时会被销毁。

临时对象的生命期

C++标准里规定:为某表达式创建了一个临时对象,则此临时对象将一直存在直到包含有该表达式的最大的表达式计算完成为止。怎么理解这句话?我们以一行代码来解释,如下的代码:

cpp 复制代码
// 假设verbose为bool类型变量,根据这个开关输出不同的信息
// a,b,c,d皆为string类型的对象。
verbose ? a + b : c + d;

上面的表达式即是一个完整的表达式,也是一个最大的表达式,里面包含三个子表达式:"verbose","a + b"," c + d ",其中"a + b "和"c + d "两个子表达式将会有可能产生临时对象,视verbose 的测试结果将运算不同的子表达式。这个运算的中间过程产生的临时对象需要等到完整的表达式运算完成之后才能被销毁。我们还是以上面的测试代码为例,修改下main函数,其它代码不变,如下:

cpp 复制代码
int main() {
    Object a;
    Object b;
    printf("a + b = %d\n", (int)(a + b));
    return 0;
}

它生成的对应的汇编代码:

diff 复制代码
main:                                   # @main
  # 略...
  lea     rdi, [rbp - 8]
  call    Object::Object() [base object constructor]
  lea     rdi, [rbp - 16]
  call    Object::Object() [base object constructor]
  lea     rdi, [rbp - 32]
  lea     rsi, [rbp - 8]
  lea     rdx, [rbp - 16]
  call    operator+(Object const&, Object const&)
  lea     rdi, [rbp - 32]
  call    Object::operator int()
	# 略...
  call    printf@PLT
  lea     rdi, [rbp - 32]
  call    Object::~Object() [base object destructor]

rbp - 8\]是对象**a** ,\[rbp - 16\]是对象**b** ,\[rbp - 32\]是临时对象。上面代码的第14行在调用**printf** 函数之后才去调用了**Object**类的析构函数去析构临时对象。 但当临时对象是根据程序的测试语句有条件的被产生出来时,临时对象的生命期规则就变得复杂了。还是以上面的例子,修改如下: ```cpp int main() { Object a; Object b; Object c; Object d; if ( a + b || c + d) { printf("Go here.\n"); } return 0; } ``` 第6行的**if** 语句中,子表达式"**c + d** "是否会被运算取决于前面的子表达式"**a + b** "的测试结果,只有它的结果为**false** 的时候才会评估子表达式"**c + d** ",这时候才会产生一个临时对象。临时对象应该要销毁,但不是无条件的销毁,要根据它是否有被产生出来而决定。上面的**if**语句大概可以转换成以下的语句: ```cpp // 伪代码: Object tmp1 = a + b; int test1 = tmp1.operator int(); int test2 = 0; if (test1 == 0) { Object tmp2 = c + d; test2 = tmp2.operator int(); // 此处销毁临时对象tmp2?(1) } tmp1.Object::~Object(); // 此处销毁临时对象tmp2?(2) ``` 临时对象tmp2应该在哪里被销毁,在(1)处还是在(2)处?其实这两种处理方式都不准确,首先是在(1)处时还不能销毁,根据C++的标准规定,这时完整的表达式还没有运算完,此时还不能销毁临时对象。其次是(2)处的处理也不妥当,因为有可能tmp2并未被产生出来,它需要根据条件来决定是否销毁。来看看编译器是怎么做的,看看这行代码对应的汇编: ```diff main: # @main # 省略构造的代码,对象a,b,c,d的存放位置分别是: #[rbp - 8],[rbp - 16],[rbp - 32],[rbp - 40] mov byte ptr [rbp - 57], 0 # 省略a+b的代码,产生的临时对象存放在[rbp - 48] lea rdi, [rbp - 48] call Object::operator int() mov dword ptr [rbp - 64], eax # 4-byte Spill mov ecx, dword ptr [rbp - 64] # 4-byte Reload mov al, 1 cmp ecx, 0 mov byte ptr [rbp - 65], al # 1-byte Spill jne .LBB3_9 # 省略c+d的代码,产生的临时对象存放在[rbp - 56] mov byte ptr [rbp - 57], 1 lea rdi, [rbp - 56] call Object::operator int() mov dword ptr [rbp - 72], eax # 4-byte Spill mov eax, dword ptr [rbp - 72] # 4-byte Reload cmp eax, 0 setne al mov byte ptr [rbp - 65], al # 1-byte Spill .LBB3_9: mov al, byte ptr [rbp - 65] # 1-byte Reload mov byte ptr [rbp - 73], al # 1-byte Spill test byte ptr [rbp - 57], 1 jne .LBB3_10 jmp .LBB3_11 .LBB3_10: lea rdi, [rbp - 56] call Object::~Object() [base object destructor] .LBB3_11: lea rdi, [rbp - 48] call Object::~Object() [base object destructor] mov al, byte ptr [rbp - 73] # 1-byte Reload test al, 1 ``` 编译器是以一个标志位来标记是否有临时对象的产生,见上面代码的第4行,标志位存放在\[rbp - 57\]中,占用一个byte的大小,默认值设置为0。 然后接下来的第5到第13行包括省略掉的代码,是运算"**a + b"** 然后对其转换成**int** 型数据再进行测试,这里"**a + b"** 的运算过程产生临时对象存放在\[rbp - 48\]。第10行的**al** 保存的是条件测试的结果,也就是决定执行**if** 后面的语句还是执行**else**后面的语句的作用,它也是一个byte大小,暂存在\[rbp - 65\]中。 第13行就是根据"**a + b"** 的结果是否继续评估后续的"**c + d"** ,如果为0将继续评估"**c + d"** ,第14行到第20行包括省略的代码就是运算"**c + d"** ,这里将产生临时对象,存放在\[rbp - 56\]中,同时最主要的一点就是将记录是否产生临时对象的标志位设置为1,即代码的第15行,\[rbp - 57\]现在的值为1。第21行的**setne** 指令的作用是取标志寄存器中**ZF** 的值并取反后,再放到**AL** 中,这个是决定整个**if**语句括号中的测试条件的真假。 接下来从标签**.LBB3_9** 开始的代码就是根据**if** 语句里的条件测试结果决定是执行**if** 语句还是**else** 语句,以及根据标志位是否需要销毁临时对象。第26行就是测试\[rbp - 57\]的值是否为1,根据上面的分析,如果有评估到"**c + d** "这里的话就会产生临时对象并这个值被设置为1,这里判断为1的话就跳转到**.LBB3_10** 标签处执行析构动作,如果为0则跳过这段代码。接着就是销毁"**a + b** "产生的临时对象(存放在\[rbp - 48\]),这个临时对象是一定会产生的,所以不必判断标志位,然后第36行就是根据**if**语句的测试结果决定接下来的代码流程。 #### 特殊的情况 上面提到的临时对象在完整的表达式运算完之后就会被销毁掉,但有两个例外的地方,它不会马上被销毁,而是会继续存在一段时间,例如: * **表达式被用来初始化一个对象时** 如下面的代码: ```cpp bool condition; Object obj = condition ? a + b : c + d; ``` 其中"**a + b** "和"**c + d** "将根据测试结果产生出临时对象,根据规则临时对象在"**?:** "这个完整表达式结束后就可以被销毁,但这时需要用这个临时对象来初始化**obj** 对象,所以不能马上销毁它,必须要等到初始化完**obj**之后才能销毁。 * **当临时对象被一个引用绑定时** 如下面的代码: ```cpp const Object &ref = a + b; ``` 引用**ref** 将绑定到一个"**a + b**"产生的临时对象上,编译器将它转换为如下的伪代码: ```cpp Object tmp = a + b; const Object &ref = tmp; ``` 这种情况下,临时对象**tmp** 不能被释放,否则**ref** 将引用到一个空对象,临时对象将会一直被保留,直到绑定到它的引用**ref**的生命周期结束。 *** ** * ** *** > 本主页会定期更新,为了能够及时获得更新,敬请关注我:**点击左下角的关注** 。也可以关注公众号:请在微信上搜索公众号"**iShare爱分享**"并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。 > > ![](https://file.jishuzhan.net/article/1788085829705404417/9021e68d6a6880968bb046359ad03147.webp)

相关推荐
双叶8365 小时前
(C语言)虚数运算(结构体教程)(指针解法)(C语言教程)
c语言·开发语言·数据结构·c++·算法·microsoft
牵牛老人7 小时前
C++设计模式-责任链模式:从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析
c++·设计模式·责任链模式
序属秋秋秋7 小时前
算法基础_基础算法【高精度 + 前缀和 + 差分 + 双指针】
c语言·c++·学习·算法
KeithTsui8 小时前
GCC RISCV 后端 -- 控制流(Control Flow)的一些理解
linux·c语言·开发语言·c++·算法
mNinGInG8 小时前
c++练习
开发语言·c++·算法
EPSDA9 小时前
Boost库中的谓词函数
c++
yuanbenshidiaos10 小时前
面试问题总结:qt工程师/c++工程师
c++·qt·面试
半盏茶香10 小时前
启幕数据结构算法雅航新章,穿梭C++梦幻领域的探索之旅——堆的应用之堆排、Top-K问题
java·开发语言·数据结构·c++·python·算法·链表
小竹子1410 小时前
L1-1 天梯赛座位分配
数据结构·c++·算法
v维焓11 小时前
C++(思维导图更新)
开发语言·c++·算法