1.什么是递归?
递归就是自己调用自己
2.编译器是如何实现递归的?
编译器是通过栈来实现递归,其实编译器也是通过栈来实现函数调用的,为了明白递归,我们先来看看我们的程序是如何实现函数调用的吧。
下面我们看一个函数调用的栗子
int adder(int x,int y)
{
return x+y;
}
void call()
{
int x=2;
int y=3;
adder(x,y);
}
我们使用gcc编译成汇编(PS:要想明白一个程序是怎么运行的最好的方式还是看汇编吧)
gcc -S test.c
下面我们看看汇编片段
.file "recu.c" .text .globl adder .type adder, @function adder: .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 -8(%rbp), %eax movl -4(%rbp), %edx addl %edx, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size adder, .-adder .globl call .type call, @function call: .LFB1: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $2, -4(%rbp) movl $3, -8(%rbp) movl -8(%rbp), %edx movl -4(%rbp), %eax movl %edx, %esi movl %eax, %edi call adder leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size call, .-call .ident "GCC: (GNU) 4.8.2" .section .note.GNU-stack,"",@progbits
擦!汇编怎么那么多看不懂的符号啊?!!真拙计!
我们这里是r开头的,原因是我们使用的是64位的操作系统
为了读懂汇编,我还是先去复习了一下,首先先明白几个寄存器吧
在X86上,用户寄存器为eax, ebx, ecx, edx, esi, edi, ebp, esp 以及eipeax、ebx、ecx以及edx寄存器作为通用寄存器,可以用来进行临时存储
esi和edi可以用来存储,但对操作字符串类的函数有其他意义,在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串ebp通常用来容纳当前栈帧(stack frame)的内存地址,esp保存栈顶地址eip保存当前执行指令的内存地址。机器代码不能直接修改该寄存器,只能通过jmp和call指令族进行间接修改,实现循环,调用等
然后我们在明白一下重要术语:
栈帧:值得是ebp到esp的这一段内存区间,每一个函数的调用都会生成一个栈帧,这里面保存着函数里的变量和指令。
我们主要关心call函数以及adder函数,所以单独拿出来
我们先来看call函数:
****这里的栈类别是指的向下满栈
call:
.LFB1:
.cfi_startproc ##这个表示函数的入口参见.cfi_startproc pushq %rbp ##把rbp寄存器入栈
.cfi_def_cfa_offset 16 ##定义CFI(call frame information)的cfa(Canonical Frame Address)的偏移量,主要是由于push了%rbp造成的偏移量
.cfi_offset 6, -16 ##.cfi我也不明
movq %rsp, %rbp ##把rsp的值搞到rbp中去,表示现在进入了新栈
.cfi_def_cfa_register 6 ##说明现在的cfa register是rbp
subq $16, %rsp ##空出两个单位,根据程序,我们应该猜得到是为x、y变量提供空间,为毛是压16个字节?难道是内存对其?
movl $2, -4(%rbp) ##把rbp下面的一个4字节定义为x的空间
movl $3, -8(%rbp) ##把rbp下面的第8个字节处定义成y的空间
movl -8(%rbp), %edx ##把x、y的地址赋给寄存器edx和eax,这两个是通用寄存器
movl -4(%rbp), %eax
movl %edx, %esi ##又把他们送个esi、edi这里是为传值做准备
movl %eax, %edi
call adder ##这里调用adder函数,同时这个指令暗含一个把当前栈顶压入栈
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
下面用图来说明
好了,下面我们来谈谈尾递归。主要围绕下面一个方面1.什么是尾递归?与递归有什么区别?我们先来一段代码来看看这两者之间的区别
int fact(int n){
if(n <0) return 0;
if(n ==0) return 1;
if(n ==1) return 1;
return n*fact(n-1);
}
int facttail(int n,int a){
if(n <0) return 0;
if(n ==0) return 1;
if(n ==1) return a;
return facttail(n-1,n*a);
}
fact函数是递归版本,facttail函数是尾递归版本。 我们在文章最初知道了函数调用是通过栈帧来实现的,因为
栈帧中需要保存运算的变量,在使用递归的时候,比如函数fact,如果n非常大的话,很显然栈帧会越来越多,直到撑爆内存,这显然是我们不乐意见到的。但是我们来看factail这个函数,它也是使用递归来实现求n的阶乘,但是注意到,这个函数中,
其栈帧是没有变量需要保存的,这就是说我们的栈帧是不需要的,我们完全可以就在原来的栈帧上进行运算!!于是乎C编译器抖了个机灵,它如果发现了你使用了尾递归,他就会自动帮你优化,现在我们来看着两个函数的汇编。为了看着方便,我们把判断全部删除,如下
int fact(int n){ return n*fact(n-1); } int facttail(int n,int a){ return facttail(n-1,n*a); }汇编代码如下:
fact: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl %edi, -4(%rbp) movl -4(%rbp), %eax subl $1, %eax movl %eax, %edi call fact imull -4(%rbp), %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size fact, .-fact .globl facttail .type facttail, @function facttail: .LFB1: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %eax imull -8(%rbp), %eax movl -4(%rbp), %edx subl $1, %edx movl %eax, %esi movl %edx, %edi call facttail leave .cfi_def_cfa 7, 8 ret .cfi_endproc卧槽!怎么都是递归呢?和传说中主动优化的不一样呢? 原来是我搞忘记加入优化参数了,现在把优化参数加上,汇编代码如下
.file "fact.c" .text .p2align 4,,15 .globl fact .type fact, @function fact: .LFB7: .cfi_startproc .p2align 4,,10 .p2align 3 .L2: jmp .L2 .cfi_endproc .LFE7: .size fact, .-fact .p2align 4,,15 .globl facttail .type facttail, @function facttail: .LFB8: .cfi_startproc .p2align 4,,10 .p2align 3 .L4: jmp .L4 .cfi_endproc .LFE8: .size facttail, .-facttail .ident "GCC: (GNU) 4.8.2" .section .note.GNU-stack,"",@progbits卧槽,我完全看不懂了,不过估计这东西优化成为了一个循环,看.L4,不过这个怎么把递归也搞成循环了?!这个真心不懂,求大家指教