现代C++推荐用std::unique_ptr管理语法树节点而非裸指针,因其能明确所有权、避免悬空/泄漏/重复释放,配合递归下降解析更安全清晰;构造时需用std::move转移所有权,返回用std::make_unique保证异常安全。

直接用裸指针实现数学公式解析器,不是不好,而是容易出错且不必要——现代 C++ 更推荐用 std::unique_ptr 或 std::shared_ptr 管理语法树节点,配合递归下降解析,既安全又清晰。
为什么不用原始指针(double*、Node*)手写内存管理
原始指针在表达式树中极易引发悬空、重复释放或泄漏:比如二元运算节点持有左右子节点指针,析构时若忘记递归 delete,或某节点被多个父节点共享而误删,程序当场崩溃。错误现象常见为 double free or corruption 或 segmentation fault,调试成本远高于设计成本。
- 所有节点生命周期应由所有权语义明确控制,原始指针无法表达“谁负责释放”
- 递归解析过程中频繁构造/销毁临时节点,手动
new/delete极易漏掉分支 - C++17 起,编译器对移动语义和 RAII 优化已非常成熟,
std::unique_ptr零开销
用 std::unique_ptr 实现 AST 节点基类与派生结构
核心是让每个节点独占其子节点,避免共享歧义;同时支持多态访问(如 evaluate())。
struct Expr {
virtual ~Expr() = default;
virtual double evaluate() const = 0;
};
<p>struct Number : Expr {
double value;
explicit Number(double v) : value(v) {}
double evaluate() const override { return value; }
};</p><p>struct BinaryOp : Expr {
char op;
std::unique_ptr<Expr> left;
std::unique_ptr<Expr> right;
BinaryOp(char o, std::unique_ptr<Expr>&& l, std::unique_ptr<Expr>&& r)
: op(o), left(std::move(l)), right(std::move(r)) {}
double evaluate() const override {
double l = left->evaluate(), r = right->evaluate();
switch (op) {
case '+': return l + r;
case '-': return l - r;
case '<em>': return l </em> r;
case '/': return r != 0 ? l / r : throw std::runtime_error("div by zero");
default: throw std::runtime_error("unknown op");
}
}
};注意:构造 BinaryOp 时必须用 std::move 转移所有权,否则编译失败(unique_ptr 不可拷贝);这也强制你在解析函数中显式表达“这个子树归我管”。
立即学习“C++免费学习笔记(深入)”;
递归下降解析器如何返回 std::unique_ptr<Expr>
解析函数(如 parse_expression())返回的是拥有权,不是引用或原始指针。调用方拿到的就是完整 AST 的根节点,离开作用域自动清理整棵树。
- 不要写
return std::unique_ptr<Expr>(new Number(val));—— 改用std::make_unique<Number>(val),更安全且异常安全 - 遇到括号时,递归调用
parse_expression(),直接把返回值 move 进新节点:std::make_unique<BinaryOp>('+', std::move(left), parse_expression()) - 如果解析失败(如 token 不匹配),抛异常即可;
unique_ptr在栈展开时会自动析构已构建的部分,不会泄漏
性能与兼容性提醒:别过早优化指针类型
有人担心 unique_ptr 带来间接跳转开销,但实际中:AST 节点数量通常几百以内,evaluate() 是虚函数调用热点,瓶颈在运算逻辑本身,而非指针解引用;若真有极致性能要求(如每秒百万次 eval),应改用 arena 分配 + 索引代替指针,而不是退回原始指针。
另外,std::shared_ptr 只在真正需要共享所有权时才用(比如变量引用、符号表缓存),多数解析场景不需要——共享意味着引用计数开销和循环引用风险,反而更难 debug。
最常被忽略的一点:AST 节点的 evaluate() 方法若需访问外部变量(如 x, y),请通过 const 引用传入上下文对象,而不是在节点里存 double* 或全局 map 指针——那才是真正的复杂源头。


















