递归 解剖
2012-07-22 17:20 youxin 阅读(450) 评论(0) 编辑 收藏 举报调用函数时会发生什么情况?
每个函数(包括main)的状态是由函数中所有自动变量的内容,函数参数的值,表明在调用函数的何处重新开始的返回地址决定的。包含所有这些信息的数据区称为活动记录(activation record)或者栈结构(stack frame)。他是在运行时栈(run-time stack)上分配空间的。只有函数正在执行,他的活动记录就一直存在。这个记录是函数的私有信息池,它存储了程序正确执行并正确返回到调用它的函数所需的所有信息。活动记录的寿命一般很短,因为他们在函数开始执行时得到动态分配的空间,在函数退出时就释放其空间,只有main()的活动记录的寿命比其他活动记录常。
活动记录通常包含以下信息:
1.函数所有参数的值,如果传送的是数组或变量按引用传递,则活动记录包含的是该数组第一个单元的地址或者该变量的地址,所有其 他数据元素的副本。
2.可以存储在其他地方的局部变量(自动变量),活动记录只包含他们的描述法和指向存放他们位置的指针。
3.回到调用程序的返回地址,即在调用完成之后紧接着此函数调用指令之后的指令地址
4一个指向调用程序的活动记录的动态链接指针
5.非void类型的函数返回值,活动记录空间的大小随调用不同而不同,返回值在调用程序的活动记录最上方。
外加一篇文章:
一个由C/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack)- 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) - 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)-,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放
4、文字常量区-常量字符串就是放在这里的。程序结束后由系统释放
5、程序代码区-存放函数体的二进制代码。
文章结束
上面提到,如果函数由主函数main()或者 其他函数调用,它的活动记录就在运行时栈中建立,运行时栈总是反映函数的当前状态,假如,main函数调用f1,f1调用f2,f2调用f3,如果f3正在运行,运行时栈中的状态如下,根据栈的特性,如果函数f3的活动记录从栈中弹出,并将栈指针移到紧挨着函数f3的返回值的下方,然后f2继续执行,并能自由访问它重新执行所需的私有信息池,另一方面,如果f3调用f4,运行时栈的空间将增大,因为f4的活动记录在该栈上建立,而函数f3将暂停执行。
无论何时调用函数都会创建一个活动记录,因此系统可以正确地递归问题,递归就是调用的函数名称正好和调用者相同,因此,递归调用不是表面上的函数自身调用,而是一个函数的实例调用同一个函数的另一个实例,这些调用在内部表示为不同的活动记录,并有系统区分。
尾部递归
特点是:在每个函数实现的末尾只使用一个递归调用,也就是说,当进行调用时,函数中没有其他剩余的语句要执行,递归调用不仅是最后一条语句,而且在这之前也没用其他直接或间接的递归调用,例如tail()定义如下:
void tail(int i) { if(i>0) { cout<<i<<ends; tail(i-1); } cout<<endl; }
这就是一个尾部递归的例子,下面的不是尾部递归:
void nonTail(int i) { if(i>0) { nonTail(i-1); cout<<i<<ends; nonTail(i-1); } }
nonTail就不是尾部递归,思考nonTail(3)输出什么?1 2 1 3 1 2 1.尾部递归只是一个变形的循环,很容易用循环来代替,在这个例子中,可以写成:
void iterativeEquivalentOfTail (int i) { for(; i>0;i--) cout<<i<<ends; }
非尾部递归
可以用递归实现的一个问题是将输入行以相反的顺序打印出来:
void reverse() { char ch; cin.get(ch); if(ch!='\n') { reverse(); cout.put(ch); } }
图显示了reverse第一次递归调用它自身之前运行时栈的内容。
采用非递归形式调用如下:
void iterativeReverse() { char stack[80]; int top=0; cin.get(stack[top]); while(stack[top]!='\n') cin.get(stack[++top]); for(top-=1;top>=0;cout.put(stack[top--])); }
注意数组使用变量名stack,是为了将系统隐含的完成的工作展示出来,这个栈取代了运行时栈的功能,这里使用它很有必要,因为一个简单的循环是不够的,如在尾部递归中那样。
无论采用哪种方式,将非尾部递归形式转换为迭代形式都需要对栈进行显示处理,而且,由递归转为非递归,程序清晰度降低。
函数所做的最后一件事情是一个函数调用(递归的或者非递归的),这被称为 尾部调用(tail-call)。 使用尾部调用的递归称为 尾部递归。让我们来看一些函数调用示例,以了解尾部调用的含义到底是什么:
int test1() { int a = 3; test1(); /* recursive, but not a tail call. We continue */ /* processing in the function after it returns. */ a = a + 4; return a; } int test2() { int q = 4; q = q + 5; return q + test1(); /* test1() is not in tail position. * There is still more work to be * done after test1() returns (like * adding q to the result */ } int test3() { int b = 5; b = b + 2; return test1(); /* This is a tail-call. The return value * of test1() is used as the return value * for this function. */ } int test4() { test3(); /* not in tail position */ test3(); /* not in tail position */ return test3(); /* in tail position */ }
可见,要使调用成为真正的尾部调用,在尾部调用函数返回之前,对其结果 不能执行任何其他操作。
注意,由于在函数中不再做任何事情,那个函数的实际的栈结构也就不需要了。惟一的问题是,很多程序设计语言和编译器不知道 如何除去没有用的栈结构。如果我们能找到一个除去这些不需要的栈结构的方法,那么我们的尾部递归函数就可以在固定大小的栈中运行。
factorial1(n, accumulator) { if (n == 0) return accumulator; return factorial1(n - 1, n * accumulator); }
factorial(n) { return factorial1(n, 1); }
深入:
精通递归程序设计http://www.ibm.com/developerworks/cn/linux/l-recurs.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通