c++回头看-从汇编角度理解函数参数值传递和引用传递

C/C++
180
0
0
2024-02-16

示例程序

main.c

主要分为两个部分,每个部分使用一个display函数,函数内使得传入的参数自加1,然后打印到标准输出上。不同的地方在于,display1使用了值传递,display2使用了引用传递

 #include <stdio.h>
void display(int num){ //int num,属于值传递
    num++;
    printf("display: %dn", num);
}
void display(int & num){ //int & num ,属于引用传递
    num++;
    printf("display: %dn", num);
}
int main(){
    int num = 0, num2 = 0;
    display(num1);
    printf("num:%dn", num1);
    printf("--------------------n");
    display(num2);
    printf("num:%dn", num2);
    return;
} 

makefile

 OBJ=reference
$(OBJ):
g++ main.c -o $@
clean:
-rm -rf $(OBJ) 

在ubuntu中使用make命令进行编译并运行,结果如下图所示。

通过上述结果我们可以看出,虽然仅仅一个&符号的差异,但通过参数传递和通过值传递获得的 结果不一样

  • 值传递中的num虽然进行了自加操作(输出display:1可以看出),但是 并没有影响 到main函数中的num1(num1:0可以看出)
  • 但是引用传递中的num进行了自加1(输出display:1可以看出),并且 影响到 了main函数中的num2(num2:1可以看出).

提出问题

是什么原因造成了仅仅一个&符号的差异,导致函数内值传递和引用传递的差别呢?

实验

 objdump -d ./reference > objdump.txt

./reference: file format elf-x86-64
d6 <_Z8display1i>:
d6: 55 push %rbp
d7: 48 89 e5 mov %rsp,%rbp
da: 48 83 ec 10 sub $0x10,%rsp
de: 89 7d fc mov %edi,-0x4(%rbp) #将值取出到%rbp-0x4
e1: 83 45 fc 01 addl $0x1,-0x4(%rbp) # +1运算
e5: 8b 45 fc mov -0x4(%rbp),%eax #写回%rbp-0x4, 仍然是局部变量,生命周期在函数内
e8: 89 c6 mov %eax,%esi
ea: bf 44 07 40 00 mov $0x400744,%edi
ef: b8 00 00 00 00 mov $0x0,%eax
f4: e8 b7 fe ff ff callq 4004b0 <printf@plt>
f9: 90 nop
fa: c9 leaveq 
fb: c3 retq 
fc <_Z8display2Ri>:
fc: 55 push %rbp
fd: 48 89 e5 mov %rsp,%rbp
: 48 83 ec 10 sub $0x10,%rsp
: 48 89 7d f8 mov %rdi,-0x8(%rbp)#将值取出到%rbp-0x8, 注意此时%rdi为地址
: 48 8b 45 f8 mov -0x8(%rbp),%rax
c: 8b 00 mov (%rax),%eax
e: 8d 50 01 lea 0x1(%rax),%edx #加一
: 48 8b 45 f8 mov -0x8(%rbp),%rax
: 89 10 mov %edx,(%rax) # 将结果放入原地址所指的内存当中,
: 48 8b 45 f8 mov -0x8(%rbp),%rax
b: 8b 00 mov (%rax),%eax
d: 89 c6 mov %eax,%esi
f: bf 52 07 40 00 mov $0x400752,%edi
: b8 00 00 00 00 mov $0x0,%eax
: e8 82 fe ff ff callq 4004b0 <printf@plt>
e: 90 nop
f: c9 leaveq 
: c3 retq 
 <main>:
: 55 push %rbp
: 48 89 e5 mov %rsp,%rbp
: 48 83 ec 10 sub $0x10,%rsp
: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
: 00 00 
: 48 89 45 f8 mov %rax,-0x8(%rbp)
: 31 c0 xor %eax,%eax
: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp)
f: c7 45 f0 00 00 00 00 movl $0x0,-0x10(%rbp)
: 8b 45 f4 mov -0xc(%rbp),%eax # 将%rbp-0xc的值放入%eax,相当于复制了一份
: 89 c7 mov %eax,%edi
b: e8 76 ff ff ff callq 4005d6 <_Z8display1i>
: 8b 45 f4 mov -0xc(%rbp),%eax
: 89 c6 mov %eax,%esi
: bf 60 07 40 00 mov $0x400760,%edi
a: b8 00 00 00 00 mov $0x0,%eax
f: e8 3c fe ff ff callq 4004b0 <printf@plt>
: bf 69 07 40 00 mov $0x400769,%edi
: e8 12 fe ff ff callq 400490 <puts@plt>
e: 48 8d 45 f0 lea -0x10(%rbp),%rax # 将%rbp-0xc的地址放入%eax,想到与对原地址进行操作
: 48 89 c7 mov %rax,%rdi
: e8 72 ff ff ff callq 4005fc <_Z8display2Ri>
a: 8b 45 f0 mov -0x10(%rbp),%eax
d: 89 c6 mov %eax,%esi
f: bf 7e 07 40 00 mov $0x40077e,%edi
: b8 00 00 00 00 mov $0x0,%eax
: e8 12 fe ff ff callq 4004b0 <printf@plt>
e: b8 00 00 00 00 mov $0x0,%eax
a3: 48 8b 55 f8 mov -0x8(%rbp),%rdx
a7: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
ae: 00 00 
b0: 74 05 je 4006b7 <main+0x86>
b2: e8 e9 fd ff ff callq 4004a0 <__stack_chk_fail@plt>
b7: c9 leaveq 
b8: c3 retq 
b9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 

通过上述汇编代码(相关关键步骤已经使用注释进行了说明)。

 display(num1);
display(num2); 
: 8b 45 f4 mov -0xc(%rbp),%eax # 注意mov操作!!将%rbp-0xc的值(也就是局部变量num1)放入%eax,相当于复制了一份
: 89 c7 mov %eax,%edi
b: e8 76 ff ff ff callq 4005d6 <_Z8display1i> 
e: 48 8d 45 f0 lea -0x10(%rbp),%rax # 注意lea 操作!!将%rbp-0xc(也就是局部变量num2)的地址放入%eax,想当于对原地址进行操作
: 48 89 c7 mov %rax,%rdi
: e8 72 ff ff ff callq 4005fc <_Z8display2Ri> 

可以看出:

  • 对于值传递,使用mov指令,相当于 复制 了一份;
  • 对于引用,使用lea指令,得到了地址,随后的操作都在 地址上 进行,相当于直接对该地址的数进行操作。

因此,我们知道,虽然传递的都是传递的一个变量名,但display1使用的值传递,display2使用的是引用传递:

 display(num1);//虽然进行了自加1,但是是对num1的副本进行的操作,作用范围在display函数内
display(num2);//使用引用传递,相当于指针操作,作用范围在main函数当中。 
  • 当使用值传递时,在函数内对参数的操作,参数作用范围只在函数内,跳出函数后该是啥还是啥,在 原函数 (这里是main)里就是进入函数前的状态。因为值传递方式,在函数中只改变的是值的副本。
  • 在使用引用传递时,引用的本质使用的是指针。因此在函数中的操作,都会直接作用于该地址的值。

总结

通过对值传递和引用传递的汇编代码的分析,我们清晰的看出值传递本是上是传递了一个原值的副本,其变化并不影响调用函数的值;引用传递的本质是指针,其变化,直接作用于调用函数的值。