尾递归

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,不过这个怎么把递归也搞成循环了?!这个真心不懂,求大家指教
Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值