函数:广搜定义与深搜实现

本篇谈及以下内容:

1.函数调用树与代码跳转,调用展开成递归

2.深搜与广搜的优劣,栈与队列的选择

3.静态链与动态链

4.异常的处理

5.内存管理:栈与堆的设计语义

6.泛化栈与堆的设计

 

1.函数调用树

函数的全部调用组成一棵调用树

每个叶节点代表调用的终结条件,或自身不含调用的函数,

父节点到子节点表示调用方进入被调用方

子节点到父节点表示调用完成的回溯

每个节点都需要有回溯动作(返回地址)

 

当我们定义一个函数时,通常是以广搜树的角度考虑的:

即:当前定义的函数顺序执行许多步骤,并且假定各个步骤保证其功能完成,而不是一直深究到最内嵌套函数的执行。

 

然而,程序执行时的顺序是按深搜树的遇到调用则展开,(保存返回地址)调用完成则回溯

 

所有递归都可以展开成迭代:

每次迭代的语义:

判断回溯或是调用

1.父——子:调用,得到被调方的代码地址,跳转

2.子——父:回溯,完成计算,取出保存的返回地址,跳转

 

无论是调用动作还是回溯动作,都是根据地址跳转。可见将递归展开实际上完成的是一次树的深搜遍历。

 

2.深搜与广搜的优劣,栈与队列的选择

深搜用栈实现,广搜用队列实现

栈的长度最长为调用深度(树高)这是栈实现调用的空间高效性的保证。

队列的长度最长为某个深度的节点总数(更精确的,是相邻两层的组合情况)

但是!上述还没有讨论需要回溯的情况。

当需要回溯时,队列需要维护整棵树,或者说,单个队列已经无法维护函数调用的语义了。

 

调用——回溯这两个动作与栈的入出完美的一致。

 

3.静态链与动态链

上文中没有区分开一个细节:一棵树是一次具体调用的实例,不同的调用产生的树不同。

 

每个节点都需要记录向上回溯的指针(指向父),这个指针的语义:

被调用方的返回地址=调用者的某个代码行

称为帧指针

要是一个函数还没结束,则都保留着帧指针

栈空间上保留着一条调用路径,栈1返回地址——栈2的帧(指向后序代码)——移动到栈2的结束——栈2的返回地址

这样构成的链就叫动态链(调用序列不可静态预测)

在树上表现为回溯边

 

 

静态链就比较简单了,取决于调用与被调的代码块嵌套关系(定义处与调用处不同)

在树上表现为一条路径上,重复出现的同函数名节点,下方的指向第一次出现位置(回向边)

 

静态链与动态链语义上截然不同(一个用于返回,一个用于查找外层变量)

在实现上有共同点,都是保留指针用于回溯:对于调用回溯,一个函数可能被各种函数调用;对于查找外层变量,外层函数可能被内层调用(做间接引用回到定义位置)

 

4.异常的处理

可能异常代码块,平行于处理异常代码块。这是个词法上(静态)的if not-else结构

 

但异常是在执行的过程中出现的,调用方会不会对错误有个更高抽象层次的分析?调用A失败了,推测导致异常的输入有可能存在某种规律(先验)

根据调用的顺序回溯(不同于动态链,中间穿插着处理函数)

 

所以异常是一种动态静态思想都有的设计

 

5.内存管理:栈与堆的设计语义

栈划分出一块空间(栈空间),具体由两两相邻的栈指针划分开

而回溯的访问机制甚至不需要知道所有的划分,而是只需要最后一次划分(保存,恢复栈指针)

如果用到的所有对象大小(形参+局部)大小确定,可以快速的划分与计算地址(偏移)

否则:

1.可变对象转化为:地址(大小一致)+内情向量(分配到堆上?)

2.两次划分,第一次划分出固定大小,第二次划分出可变大小

 

大小静态可知用于对齐?

 

堆:语义:生存周期跨作用域

垃圾回收:1.假定最后一个链接为硬链接。2.无可访问入口

6.泛化栈与堆的设计

 

posted on 2018-07-02 13:27  秦梦超  阅读(360)  评论(0编辑  收藏  举报

导航