17. 解释器模式(Interpreter):语法解析,抽象语法树,C++中的简单表达式计算器

解释器模式,这个名字听起来挺唬人的。其实说白了,就是给某种语言定义一套语法规则,然后写个程序去解释执行它。你想想看,我们平时写的正则表达式、SQL语句,背后都有解释器在干活。

我个人习惯把解释器模式理解成「领域特定语言的小引擎」。当你的业务逻辑频繁变化,而且变化模式可以被抽象成语法规则时,解释器模式就派上用场了。我在项目中遇到过几次这样的场景:用户需要自定义一些过滤条件、计算规则,如果每次都改代码,那运维成本太高了。这时候,一个轻量级的解释器就能解决问题。

核心概念:语法、AST、解释器

解释器模式有三个核心要素:

  • 语法规则:定义什么样的表达式是合法的。比如「1 + 2 * 3」是合法的,「+ 1 2」可能就不合法。
  • 抽象语法树(AST):把表达式解析成一棵树。叶子节点是数字或变量,内部节点是操作符。
  • 解释器:遍历这棵树,执行每个节点的操作,最终得到结果。

嗯,这里要注意:AST 是解释器模式的核心数据结构。你写的表达式越复杂,AST 的层次就越深。但不管多深,解释器只需要递归遍历就能搞定。

一个简单的表达式计算器

我们来写一个支持加减乘除的表达式计算器。假设用户输入 "3 + 5 * 2",我们要能算出 13。

首先,定义 AST 节点:

class Expression {
public:
    virtual ~Expression() = default;
    virtual int interpret() = 0;
};

class Number : public Expression {
    int value;
public:
    Number(int v) : value(v) {}
    int interpret() override { return value; }
};

class Add : public Expression {
    Expression* left;
    Expression* right;
public:
    Add(Expression* l, Expression* r) : left(l), right(r) {}
    int interpret() override { return left->interpret() + right->interpret(); }
};

class Multiply : public Expression {
    Expression* left;
    Expression* right;
public:
    Multiply(Expression* l, Expression* r) : left(l), right(r) {}
    int interpret() override { return left->interpret() * right->interpret(); }
};

然后,写一个简单的解析器,把字符串转成 AST:

class Parser {
    std::string input;
    size_t pos = 0;

    Expression* parseExpression() {
        Expression* left = parseTerm();
        while (pos < input.size() && (input[pos] == '+' || input[pos] == '-')) {
            char op = input[pos++];
            Expression* right = parseTerm();
            left = (op == '+') ? new Add(left, right) : new Subtract(left, right);
        }
        return left;
    }

    Expression* parseTerm() {
        Expression* left = parseFactor();
        while (pos < input.size() && (input[pos] == '*' || input[pos] == '/')) {
            char op = input[pos++];
            Expression* right = parseFactor();
            left = (op == '*') ? new Multiply(left, right) : new Divide(left, right);
        }
        return left;
    }

    Expression* parseFactor() {
        if (input[pos] == '(') {
            pos++; // 跳过 '('
            Expression* expr = parseExpression();
            pos++; // 跳过 ')'
            return expr;
        }
        int value = 0;
        while (pos < input.size() && isdigit(input[pos])) {
            value = value * 10 + (input[pos++] - '0');
        }
        return new Number(value);
    }
};

使用起来很简单:

int main() {
    Parser parser("3 + 5 * 2");
    Expression* ast = parser.parse();
    std::cout << ast->interpret(); // 输出 13
    return 0;
}

关键点:解释器模式的核心是递归。每个节点只关心自己的子节点,不需要知道整棵树的结构。这种「分而治之」的思想,让代码变得非常清晰。

解释器模式的适用场景

不是所有地方都适合用解释器模式。我总结了几条经验:

  • 语法简单:如果语法规则超过 20 条,解释器模式会变得难以维护。这时候应该考虑用 Lex/Yacc 或 ANTLR 等工具。
  • 执行效率要求不高:解释器模式本质上是运行时解析,性能肯定不如手写代码。如果每秒要执行上万次,建议提前编译成字节码。
  • 业务规则频繁变化:这是解释器模式最大的价值。把规则写在配置文件里,用户自己改,不用动代码。

我的经验:我曾经在一个报表系统中用解释器模式实现了自定义计算公式。用户可以在界面上写 "SUM(A1:A10) * 0.15 + B1",系统自动解析执行。上线后,业务人员自己就能调整计算逻辑,再也不用排队等开发改代码了。

避坑指南

我曾经踩过的坑

  • 内存泄漏:AST 节点都是 new 出来的,一定要记得 delete。建议用 std::unique_ptr 或 std::shared_ptr 管理生命周期。
  • 运算符优先级:加减乘除的优先级处理不好,会导致计算结果错误。上面的 Parser 用了「表达式→项→因子」的三层结构,就是为了正确处理优先级。
  • 错误处理:用户输入的表达式可能不合法。比如 "1 + * 2" 这种。一定要加语法检查,给出友好的错误提示。

解释器模式的结构图

下面这张图展示了 AST 的构建和解释过程:

解释器模式:表达式 "3 + 5 * 2" 的 AST 结构 Add Number(3) Multiply Number(5) Number(2) 左操作数 右操作数 左操作数 右操作数 解释过程:Add.interpret() → Number(3).interpret() + Multiply.interpret() Multiply.interpret() → Number(5).interpret() * Number(2).interpret() = 10 最终结果:3 + 10 = 13

总结

解释器模式是一种优雅的解决方案,但它不是银弹。我个人建议:如果你的语法规则超过 10 条,或者对性能有较高要求,还是老老实实用现成的解析器生成工具吧。但如果只是简单的表达式计算、规则匹配,手写一个解释器反而更灵活、更容易维护。

记住一句话:解释器模式的核心是「递归」和「组合」。把复杂问题拆成简单子问题,每个子问题只关心自己的那一亩三分地。这样写出来的代码,既清晰又容易扩展。

专注资料整理