繁体   English   中英

在递归下降解析器中避免递归算法中的stackoverflow

[英]avoid stackoverflow in recursive algorithm in recursive-descent parser

我从事与解析器相关的项目,并使用递归下降解析器实现了它。 但是,问题很容易导致堆栈溢出。 有什么技术可以解决这类问题?

为了说明起见,这是一个简单的数学表达式解析器,具有加,减,乘和除法支持。 可以使用分组括号,并且显然会触发递归。

这是完整的代码:

#include <string>
#include <list>
#include <iostream>

using namespace std;

struct term_t;
typedef list<term_t> prod_t;
typedef list<prod_t> expr_t;
struct term_t
{
    bool div;
    double value;
    expr_t expr;
};

double eval(const expr_t &expr);
double eval(const term_t &term)
{
    return !term.expr.empty() ? eval(term.expr) : term.value;
}
double eval(const prod_t &terms)
{
    double ret = 1;
    for (const auto &term : terms)
    {
        double x = eval(term);
        if (term.div)
            ret /= x;
        else
            ret *= x;
    }
    return ret;
}
double eval(const expr_t &expr)
{
    double ret = 0;
    for (const auto &prod : expr)
        ret += eval(prod);
    return ret;
}

class expression
{
public:
    expression(const char *expr) : p(expr)
    {
        prod();
        for (;;)
        {
            ws();
            if (!next('+') && *p != '-') // treat (a-b) as (a+-b)
                break;
            prod();
        }
    }
    operator const expr_t&() const
    {
        return expr;
    }

private:
    void term()
    {
        expr.back().resize(expr.back().size() + 1);
        term_t &t = expr.back().back();
        ws();
        if (next('('))
        {
            expression parser(p);  // recursion
            p = parser.p;
            t.expr.swap(parser.expr);
            ws();
            if (!next(')'))
                throw "expected ')'";
        }
        else
            num(t.value);
    }
    void num(double &f)
    {
        int n;
        if (sscanf(p, "%lf%n", &f, &n) < 1)
            throw "cannot parse number";
        p += n;
    }
    void prod()
    {
        expr.resize(expr.size() + 1);
        term();
        for (;;)
        {
            ws();
            if (!next('/') && !next('*'))
                break;
            term();
        }
    }
    void ws()
    {
        while (*p == ' ' || *p == '\t')
            ++p;
    }
    bool next(char c)
    {
        if (*p != c)
            return false;
        ++p;
        return true;
    }

    const char *p;
    expr_t expr;
};

int main()
{
    string expr;
    while (getline(cin, expr))
        cout << "= " << eval(expression(expr.c_str())) << endl;
}

如果运行,则可以键入简单的数学表达式,例如1+2*3+4*(5+6*7)并正确计算195 我还添加了简单的表达式求值,它还会导致递归并导致堆栈溢出,比解析更容易。 无论如何,解析本身是简单而明显的,如何在不对代码进行大量更改的情况下重写它并完全避免递归? 在我的情况下,我使用与此表达式类似的表达式(((((1)))))导致递归,如果我只有几百个括号,则会导致堆栈溢出。 如果我步骤通过与调试器(在Visual Studio)递归树如果只有三个功能:[ term - >] expression ctor - > prod - > term和来自寄存器检查这三种功能采取700-1000字节堆栈空间。 通过优化设置和一些摆弄代码,我可以使其花费更少,而在编译器设置中,我可以增加堆栈空间,或者在这种情况下,我也可以使用Dijksta的shunting-yard算法,但这不是问题的重点:我想知道如何重写它以避免递归,并且在可能的情况下,同时又不完全重写解析代码。

递归下降解析器必须是递归的; 这个名字不是任性的。

如果生产是右递归,则其相应的递归下降动作是尾递归。 因此,使用适当的语法,您可以生成尾递归解析器,但是带括号的表达式的生成将很难被该约束所束缚。 (并参见下文。)

您可以通过维护模拟的调用堆栈来模拟递归,但是堆栈操作可能会压倒递归下降解析器的简单性。 在任何情况下,都有使用显式分析堆栈的更简单的迭代算法,因此使用其中一种可能更有意义。 但这无法回答问题。

注意:如果使用C ++,则必须跳过一些箍以创建尾部上下文。 特别是,如果分配的对象具有非平凡的析构函数(例如std :: list),则自动析构函数调用将在tail上下文中发生,并且最后一个显式函数调用不是tail调用。

递归下降解析器的常见做法是递归为子表达式,非终端或嵌套构造,但不使用递归继续在同一级别进行解析。 这使堆栈大小成为您可以解析的字符串的最大“深度”的限制,而不是其长度的限制。

看起来您做对了那部分,所以让我们看一下典型数字...

由于基于堆栈的限制,通常编写递归解析函数,以便它们不使用大量堆栈-128个字节左右是很高的平均值。

因此,如果您有128K的堆栈空间(这通常意味着您的堆栈已满90%),那么您应该能够获得1000个左右的级别,这对于程序员实际键入的真实文本来说已经足够了。

就您而言,您只能获得200个等级。 在现实生活中,这也许也可以,但是除非您在非常受限的硬件环境中运行,否则表明您在递归函数中使用的堆栈空间过多。

我不知道整个类的大小,但是我想主要的问题是term() ,其中您使用expression parser(p);在栈上放置了一个全新的expression expression parser(p); 宣言。 这是非常不寻常的,看起来可能会占用很多空间。 您可能应该避免制作整个新对象。

打印出sizeof(expression)看看它到底有多大。

对于解析表达式,请查看运算符优先级解析,例如http://epaperpress.com/oper/download/OperatorPrecedenceParsing.pdf 它使用数据堆栈在一个简单的循环中解析表达式。 200个嵌套括号所需的唯一空间是数据堆栈中的200个条目。

在某些语言中,可以在运行时添加新的运算符,而编译后的程序会指定这些运算符的关联性和优先级,而递归体面的解析器无法处理这些运算符。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM