本篇谈及以下内容:
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.泛化栈与堆的设计