浅说Return

return是编程中常常涉及到的语句,其主要用途为被调用函数返回约定好的返回值,以便于调用函数接收。我们常常将该动作视作原子的,大多数情况下这个假设是正确的,但倘若不加以钻研,在某些场景下,其行为就会显得怪异而难以理解了。

汇编描述

对于return的行为,我们可以从C与汇编讲起。

这里给出了一个test函数,以及其汇编后的汇编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int test(int a, int b){
return a+b;
}

test:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax (将返回值存储到eax寄存器内)
popq %rbp(调整栈帧)
.cfi_def_cfa 7, 8
ret(弹出返回地址)
.cfi_endproc

可以看到,简单的一个return,被分为三个步骤:

  1. 准备返回值
  2. 调整栈帧
  3. 调整PC

又或者说分为两步,准备返回值与实际返回。

情景解析

在return的相关问题中,让人困惑的莫过于异常处理代码段中的return了。

1
2
3
4
5
6
7
try{
return;
}catch (Exception e){
return;
}finally{
return;
}

我们知道,为了保证try、catch中打开的资源的关闭回收,finally语句必须执行,那么,假若我们在try、catch中进行了return,finally的执行能够得到保证吗?答案是——可以!其原理在于,在try、catch中执行return时,仅会执行return的第一部分——准备返回值,但是,并不实际进行返回,故得到实际流程如下:

graph LR;
A(开始)-->B(try中语句)-->C(保存返回值)-->D(finally语句)-->E(实际返回);

当然,若是在finally中再次遇到return,就会将try/catch中的return准备的返回值覆盖咯。