编译原理递归下降程序-编译原理递归下降程序
递归下降:让编译器自己去吃堆里的垃圾 编译原理里的递归下降,说白了就是一场人在场、机器在后台的“自杀式冲锋”。我们手里拿着一份语法清单,就是各从句子的亲哥亲姐,然后让机器根据这些亲哥的称呼,去把一堆乱七八糟的字符给拆散重组,最终变成大家都能看懂的机器指令。
这个过程就像是在一个庞大的迷宫里,一群拿着地图的人,哪位都知道哪个出口是哪儿,但有时候还得靠猜,靠各自手里的地图,通过不断回头、回头再回头,把整个迷宫填满。 大量初学者一上来就想直接写代码,直接写 `main` 函数,一行行接下去,结局发现根本接不上。
为啥?出于咱们学的递归,是“调用自己”,而编译是“执行指令”。机器执行完 `main` 后,还得持续往下找下一个指令,递归退回去后,还要持续找下一个栈上的指令,这才叫执行。
故此,你不可能只写一个 `main` 函数,你得写一堆堆函数,每到一个地方,都得自己递归下去,直到终止。
这时候你会发现,代码写得比人脑还累,就连有点乱。 最典型的例子是句子 `a -> a + a`,也就是 "aa"。
这个句子挺好办,但对应的代码量却挺大。
要是我们用递归定义,那就得写 `print(a)`,`print(a)` 里面再写 `print(a)`,直到根本打印不出啥东西,这就变成了死循环。
这时候就得引入递归下降的精髓,用一个函数去管住整个流程。
比如 `parse` 函数,它不直接打印,它负责找下一个非终结符,要是它是终结符,就打印;要是它是非终结符,就调用子函数。对于 `a -> a + a` 这种结构,`parse` 会先检查是不是 `a`,是的话,就再检查下一个是不是 `a`,是的话就打印,再检查下一个是不是 `+`,是的话持续递归,直到遇到终结符 `a`,这才终于打印出 `aa`。 这时候你可能会问,为啥要递归?能不能别自己给自己打补丁,而是手动操作栈?自然能够,这确实不是不可能。手动操作栈意味着你需求写一个显式的递归函数,每次递归进来先把当前参数扔进栈,处理完当前层,再把参数取出来扔掉。但这玩意儿一旦代码量超过几百行,你就彻底没法维护,改个参数都没法发现替换毛病。并且代码逻辑变得贼复杂,挺好办搞混哪位是真递归,哪位是循环嵌套,挺好办跑飞。
故此,手写递归函数别看能跑通,但那是为了“理解”而做的牺牲,真正的目标是让机器能跑,而不是让人看懂。 再举个例子,假设我们要编译一个表达式 `a+bc`。
这个句子有点意思,`+` 既可能是加法,也可能是乘法,关键在于它前面是不是有左括号。
要是前面没有 `(`,那它只能是加法;要是前面有 `(`,那它可能是乘法。
这就引出了递归中的“回溯”思想。当 `parse` 函数遇到 `a`,它知道这一定是加法的第一步,便它调用下一个函数去解析 `b`。解析完 `b` 后,`parse` 发现下一个词是 ``,便它判断:是不是乘法?是,那就要看前面有没有左括号。通过递归调用,它记录下之前的状态,然后持续往下走,直到遇到终结符 `c` 或 `)`。在这个过程中,它不断调用自己,不断回头检查,这就是递归下降最可爱的地方——它利用自己在栈上的“历史”信息,去判断当前步骤该做啥。 你可能会认定,既然递归如此灵活,为啥还要写那么多函数?
是不是能够直接用一个全局变量要么类来存所有参数,然后统一处理?实际上不是。出于每个子句的定义可能不一样,`parse` 函数负责处理的是“整体”,而 `parse_a` 函数、`parse_b` 函数、`parse_c` 函数负责的是局部。每个函数需求知道自己的上下文,比如 `parse` 需求知道前面是不是加法,`parse_b` 需求知道它后面是不是乘法。
要是把这些情况都塞到一个大函数里,你就丧失了子句之间的独立性。每个子句就像一个独立的章节,它只需求关切自己,不需求关心整个书的前后。递归下降就是把书分成一个个章节,每个章节有个明确的开头和结尾,机器就能根据这个结构,从第一行读到最终一行,自动搞定作业。 还有一个细节,大量人可能会忽略“非递归”的情况。
比如某些语法结构,别看能够用递归写,但效率极低,要么代码量爆炸。
这时候工程师们就会发明“迭代递归”,也就是先写成递归,再手动把显式的递归代码转成迭代代码,然后替换成迭代版本。但这玩意儿增添了编译器的负担,出于编译器得知道哪儿是显式递归,哪儿是隐式递归,还得把递归过程变成循环。
故此,对于复杂的语法,一般只写递归版本,编译工具就负责把递归变迭代,要么干脆直接用优化器处理,不要管它是不是显式的递归。 递归下降的另一个益处是它贼直观。
你看代码,读到 `if (term)` 就知道这是处理非终结符的,读到 `else (terminal)` 就知道这是处理终结符的。
这种逻辑清楚性,对于学习编译原理的人特别关键。它让你一眼就能看出,这个句子目前是处理啥的,那个句子是处理啥的。
这种“做了啥”和“为啥要如此做”的分离,让学习变得省事大量。 最终说点什......实际上没啥,递归下降就是让机器自己干活。别看写的时候挺痛苦,要写一堆大杂烩,但跑起来的时候,它像五个兄弟在迷宫里,每个人都拿着地图,哪位也不认哪位,只认地图上的字,自己拍板如何走,走到底了再回头。
这就是递归下降的魅力,别看有时候看起来挺厌恶,但它是编译原理里最硬核、也最有趣的局部。
声明:演示网站所有内容,若无特殊说明或标注,均来源于网络转载,仅供学习交流使用,禁止商用。若本站侵犯了你的权益,可联系本站删除。
