在C中处理长递归生成时如何防止堆栈溢出?

时间:2016-03-01 07:31:55

标签: c recursion compiler-construction stack-overflow buffer-overflow

给定一个语法,如何在C中计算FIRST和FOLLOW时避免堆栈溢出问题。当我不得不通过长时间的生产时,我的代码出现了问题。

示例:

S->ABCD
A->aBc | epsilon
B->Bc
C->a | epsilon
D->B

这只是一个语法。递归就是这样:

S->A
C->A
A->B
B->D
D->aBc | epsilon
FIRST(S)=FIRST(A)=FIRST(B)=FIRST(D)={a,epsilon}.  
     

提供一个C(而不是C ++)代码,用于计算和打印上面语法的FIRST和FOLLOW集合,同时记住您可能会遇到一个具有多个隐含的特定非终端的第一个/后续集合的语法。

例如:

FIRST(A)=FIRST(B)=FIRST(B)=FIRST(C)=FIRST(D)=FIRST(E)=FIRST(F)=FIRST(G)=FIRST(H)=FIRST(I)=FIRST(J)=FIRST(K)={k,l,epsilon}.
  

这是:要获得FIRST(A),您必须计算FIRST(B),依此类推,直到FIRST(K)的{​​{1}}有终端FIRST(K)为止},'k''l'。暗示越长,越有可能因多次递归而遇到堆栈溢出   如何在C语言中避免这种情况,但仍能获得正确的输出?   用C(不是C ++)代码解释。

epsilon

我的代码进入堆栈溢出。我怎么能避免它?

1 个答案:

答案 0 :(得分:5)

你的程序溢出堆栈不是因为语法“太复杂”而是因为它是左递归的。由于您的程序没有检查它是否已经通过非终端递归,一旦它尝试计算first('B'),它将进入无限递归,最终将填充调用堆栈。 (在示例语法中,不仅B左递归,它也无用因为它没有非递归生成,这意味着它永远不能导出只包含的句子端子。)

但这不是唯一的问题。该计划至少还有两个其他缺陷:

  • 在将终端添加到集合之前,不会检查给定终端是否已添加到非终端的 FIRST 集。因此, FIRST 集合中会有重复的终端。

  • 程序仅检查右侧的第一个符号。但是,如果非终端可以产生ε(换句话说,非终端是可空的),则需要使用跟随符号来计算< strong> FIRST 设置。

    例如,

    A → B C d
    B → b | ε
    C → c | ε
    

    此处, FIRST A )为{b, c, d}。 (同样,关注 B )为{c, d}。)

递归对 FIRST FOLLOW 集的计算没有多大帮助。最简单的描述算法就是这个,类似于Dragon Book中提出的算法,它足以满足任何实际语法:

  1. 对于每个非终端,计算它是否可以为空。

  2. 使用上述内容,将每个非终端 N FIRST N )初始化为前导集 N 的每个作品的符号。如果符号是右侧的第一个符号或者左侧的每个符号都可以为空,则符号是生产的前导符号。 (这些集合将包含终端和非终端;暂时不用担心。)

  3. 执行以下操作,直到循环期间未更改 FIRST 设置:

    • 对于每个非终端 N ,对于 FIRST N )中的每个非终端 M ,将 FIRST M )中的每个元素添加到 FIRST N )(当然,除非它已经是本)。
  4. 从所有 FIRST 集合中删除所有非终端。

  5. 以上假设您有一个计算可空性的算法。你也可以在龙书中找到这个算法;它有点类似。此外,你应该消除无用的制作;检测它们的算法与可空性算法非常相似。

    有一种算法通常更快,实际上并不复杂得多。完成上述算法的第1步后,您已计算出 lead-with N V )的关系,这是真的当且仅当非终端 N 的某些产品以终端或非终端 V 开始时,可​​能跳过可以为空的非终端。首先( N )是 lead-with 的传递闭包,其域仅限于终端。可以使用Floyd-Warshall算法有效地计算(无递归),或使用Tarjan算法的变体来计算图的强连通分量。 (例如,参见Esko Nuutila's transitive closure page.