深度解读《深度探索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爱分享"并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。

相关推荐
迷迭所归处5 小时前
C++ —— 关于vector
开发语言·c++·算法
CV工程师小林5 小时前
【算法】BFS 系列之边权为 1 的最短路问题
数据结构·c++·算法·leetcode·宽度优先
white__ice6 小时前
2024.9.19
c++
天玑y6 小时前
算法设计与分析(背包问题
c++·经验分享·笔记·学习·算法·leetcode·蓝桥杯
姜太公钓鲸2336 小时前
c++ static(详解)
开发语言·c++
菜菜想进步6 小时前
内存管理(C++版)
c语言·开发语言·c++
Joker100857 小时前
C++初阶学习——探索STL奥秘——模拟实现list类
c++
科研小白_d.s7 小时前
vscode配置c/c++环境
c语言·c++·vscode
湫兮之风7 小时前
c++:tinyxml2如何存储二叉树
开发语言·数据结构·c++
友友马8 小时前
『 Linux 』HTTP(一)
linux·运维·服务器·网络·c++·tcp/ip·http