在C语言中,函数的参数传递有两种类型:传值和传指针。
到了C++中,增加传引用的类型,但在机器执行的时候,传指针和传引用的效果是一样的,可以通过下面的例子来验证一下:
传指针与传引用demo
//demo.cpp
long long foo_p(long long *p) { //利用指针传递参数
(*p)++;
long long b = *p;
return b;
}
long long foo_r(long long &a) { //利用引用传递参数
a++;
long long b = a;
return b;
}
int main() {
long long a = 3L;
foo_p(&a);
foo_r(a);
return 0;
}
通过下面的指令,得到其汇编结果:
g++ demo.cpp -o demo -g
objdump -DC demo > demo.txt
在demo.txt里找到如下片段:
//传指针的函数定义
00000000000011a8 <foo_p(long long*)>:
11a8: f3 0f 1e fa endbr64
11ac: 55 push %rbp
11ad: 48 89 e5 mov %rsp,%rbp
11b0: 48 89 7d e8 mov %rdi,-0x18(%rbp)
11b4: 48 8b 45 e8 mov -0x18(%rbp),%rax
11b8: 48 8b 00 mov (%rax),%rax
11bb: 48 8d 50 01 lea 0x1(%rax),%rdx
11bf: 48 8b 45 e8 mov -0x18(%rbp),%rax
11c3: 48 89 10 mov %rdx,(%rax)
11c6: 48 8b 45 e8 mov -0x18(%rbp),%rax
11ca: 48 8b 00 mov (%rax),%rax
11cd: 48 89 45 f8 mov %rax,-0x8(%rbp)
11d1: 48 8b 45 f8 mov -0x8(%rbp),%rax
11d5: 5d pop %rbp
11d6: c3 retq
//传引用的函数定义
00000000000011d7 <foo_r(long long&)>:
11d7: f3 0f 1e fa endbr64
11db: 55 push %rbp
11dc: 48 89 e5 mov %rsp,%rbp
11df: 48 89 7d e8 mov %rdi,-0x18(%rbp)
11e3: 48 8b 45 e8 mov -0x18(%rbp),%rax
11e7: 48 8b 00 mov (%rax),%rax
11ea: 48 8d 50 01 lea 0x1(%rax),%rdx
11ee: 48 8b 45 e8 mov -0x18(%rbp),%rax
11f2: 48 89 10 mov %rdx,(%rax)
11f5: 48 8b 45 e8 mov -0x18(%rbp),%rax
11f9: 48 8b 00 mov (%rax),%rax
11fc: 48 89 45 f8 mov %rax,-0x8(%rbp)
1200: 48 8b 45 f8 mov -0x8(%rbp),%rax
1204: 5d pop %rbp
1205: c3 retq
0000000000001235 <main>:
1235: f3 0f 1e fa endbr64
1239: 55 push %rbp
123a: 48 89 e5 mov %rsp,%rbp
123d: 48 83 ec 10 sub $0x10,%rsp
1241: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
1248: 00 00
124a: 48 89 45 f8 mov %rax,-0x8(%rbp)
124e: 31 c0 xor %eax,%eax
1250: 48 c7 45 f0 03 00 00 movq $0x3,-0x10(%rbp)
1257: 00
//传指针调用
1258: 48 8d 45 f0 lea -0x10(%rbp),%rax
125c: 48 89 c7 mov %rax,%rdi
125f: e8 44 ff ff ff callq 11a8 <foo_p(long long*)>
//传引用调用
1264: 48 8d 45 f0 lea -0x10(%rbp),%rax
1268: 48 89 c7 mov %rax,%rdi
126b: e8 67 ff ff ff callq 11d7 <foo_r(long long&)>
1270: b8 00 00 00 00 mov $0x0,%eax
1275: 48 8b 55 f8 mov -0x8(%rbp),%rdx
1279: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
从上面的片段可以看出,无论是函数定义的地方,还是函数调用的地方,传地址和传引用在汇编层面是完全一样的,没有任何差别。
其实,传指针和传引用都可以认为是传地址,体现在汇编上就是调用方在压栈的时候,压入的是某个地址,在上面的例子中,使用汇编指令lea实现的,lea将-0x10(%rbp)对应的地址得到,最终放入rdi寄存器中。被调用方则通过写这个地址,来实现“改变”形参的效果。
下面可以把传值也加上,但看结果之前,我们可以猜测,里面是没有lea这个指令的:
1 long long foo_v(long long a) {
2 a++;
3 long long b = a;
4 return b;
5 }
同样的方式的,得到下面的汇编片段:
0000000000001129 <foo_v(long long)>:
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: 48 89 7d e8 mov %rdi,-0x18(%rbp)
1135: 48 83 45 e8 01 addq $0x1,-0x18(%rbp)
113a: 48 8b 45 e8 mov -0x18(%rbp),%rax
113e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1142: 48 8b 45 f8 mov -0x8(%rbp),%rax
1146: 5d pop %rbp
1147: c3 retq
00000000000011d5 <main>:
11d5: f3 0f 1e fa endbr64
11d9: 55 push %rbp
11da: 48 89 e5 mov %rsp,%rbp
11dd: 48 83 ec 10 sub $0x10,%rsp
11e1: 48 c7 45 f8 03 00 00 movq $0x3,-0x8(%rbp)
11e8: 00
11e9: 48 8b 45 f8 mov -0x8(%rbp),%rax
11ed: 48 89 c7 mov %rax,%rdi
11f0: e8 34 ff ff ff callq 1129 <foo_v(long long)>
果然,上面的lea变成了mov,在函数里面,操作也简单了很多。
上面这些东西,其实只要记住传指针和传引用,效果完全一样就可以了。
到了C++11,又引入了右值引用,右值引用本质上还是一个引用,所有其对应的底层操作也是一样的,再来试一下:
00000000000011c6 <foo_rr(long long&&)>:
11c6: f3 0f 1e fa endbr64
11ca: 55 push %rbp
11cb: 48 89 e5 mov %rsp,%rbp
11ce: 48 89 7d e8 mov %rdi,-0x18(%rbp)
11d2: 48 8b 45 e8 mov -0x18(%rbp),%rax
11d6: 48 8b 00 mov (%rax),%rax
11d9: 48 8d 50 01 lea 0x1(%rax),%rdx
11dd: 48 8b 45 e8 mov -0x18(%rbp),%rax
11e1: 48 89 10 mov %rdx,(%rax)
11e4: 48 8b 45 e8 mov -0x18(%rbp),%rax
11e8: 48 8b 00 mov (%rax),%rax
11eb: 48 89 45 f8 mov %rax,-0x8(%rbp)
11ef: 48 8b 45 f8 mov -0x8(%rbp),%rax
11f3: 5d pop %rbp
11f4: c3 retq
00000000000011f5 <main>:
11f5: f3 0f 1e fa endbr64
11f9: 55 push %rbp
11fa: 48 89 e5 mov %rsp,%rbp
11fd: 48 83 ec 10 sub $0x10,%rsp
1201: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
1208: 00 00
120a: 48 89 45 f8 mov %rax,-0x8(%rbp)
120e: 31 c0 xor %eax,%eax
1210: 48 c7 45 f0 03 00 00 movq $0x3,-0x10(%rbp)
1217: 00
1218: 48 8d 45 f0 lea -0x10(%rbp),%rax
121c: 48 89 c7 mov %rax,%rdi
121f: e8 23 00 00 00 callq 1247 <std::remove_reference<long long&>::type&& std::move<long long&>( long long&)>
1224: 48 89 c7 mov %rax,%rdi
1227: e8 9a ff ff ff callq 11c6 <foo_rr(long long&&)>
比较左值引用和右值引用的代码,可以发现,函数内部的代码完全一样,而唯一不同的在调用函数的地方,在调用之前,多了一个remove_reference的函数调用,这个其实就是std::move的作用,它用于生成一个右值引用。
综合来看,其实函数参数传递只有传值和传址两种方式。哪怕C++后面引入了函数模板,通用引用,类型推导等这些高级概念,但等到了汇编阶段,就只能二选一了。