ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++表达式树实现:从语法解析到可扩展类型系统

C++表达式树实现:从语法解析到可扩展类型系统 简介本资源是一份面向高校数据结构课程设计实践的完整解决方案聚焦二叉树在算术表达式表示与求值中的核心应用适用于计算机专业本科生课程设计、算法实践及数据结构综合能力训练。压缩包共3个文件422KB含1份详尽的Word课设报告.docx系统阐述设计思路、二叉树构建逻辑、前缀转中缀实现原理及变量赋值与表达式求值算法1个可直接运行的Windows可执行程序.exe便于快速验证功能1个完整C源代码文件.cpp代码结构清晰、注释充分涵盖ReadExpr、WriteExpr、Assign、Value四大核心函数便于理解递归遍历、表达式解析与符号表管理等关键细节。已有940人学习下载内容覆盖从理论建模、代码实现到测试验证的全流程特别适合巩固二叉树应用、提升表达式处理编程能力与课程设计报告撰写水平。1. 表达式类型实现不是“写个计算器”而是构建可扩展的语法骨架很多同学拿到“数据结构课设表达式类型的实现”时第一反应是写一个能算23*4的简单求值器——但真正拉开差距的是能否把“表达式”本身建模成一种可识别、可遍历、可变换、可验证的类型系统。这不是在实现一个功能模块而是在搭建一个微型编译器前端的核心骨架从字符串解析出结构如a b * c→ 二叉表达式树支持不同语义算术/逻辑/关系预留扩展接口后续加函数调用、变量绑定、类型检查。本报告聚焦 C 实现不依赖 Boost.Spirit 或 ANTLR 等重型工具链全程手写词法分析、递归下降解析与树形表示所有源代码可直接编译运行g 11.4适配高校数据结构与编译原理课程要求。面向两类读者一是需要交差但想真正理解表达式本质的本科生二是已掌握基础树操作、正尝试向语法分析进阶的实践者。文中所有代码均通过最小化测试集含括号嵌套、负数、左结合/右结合运算符验证关键路径附调试断点建议。2. 用 C 类层次建模表达式类型从抽象基类到具体节点表达式类型实现的本质是将数学表达式的语法结构映射为内存中的对象图。C 的多态机制天然适合此场景定义统一接口由子类承载具体语义。我们不采用union或void*这类弱类型方案也不用std::variant虽现代但掩盖了类型设计意图而是构建清晰的继承体系——这正是课设中体现“数据结构设计能力”的核心落点。2.1 抽象基类 Expr定义表达式共性行为所有表达式节点必须继承自Expr它不存储数据只声明三类契约方法// expr.h #include memory #include string #include iostream class Expr { public: virtual ~Expr() default; // 求值返回 double实际子类可能抛异常如除零 virtual double evaluate() const 0; // 打印生成带括号的中缀表示用于验证树结构 virtual std::string toString() const 0; // 类型识别运行时判断节点类别便于后续扩展如优化规则匹配 virtual std::string type() const 0; };注意evaluate()返回double而非int因课设常需支持浮点字面量如3.14若限定整数运算可改为long long并重载evaluateInt()但会增加接口复杂度。此处选择通用性优先。2.2 具体节点实现字面量、二元/一元运算符、括号基于Expr派生四类基础节点覆盖绝大多数课设需求2.2.1 字面量节点 LiteralExpr存储数值常量是最简叶子节点// literal_expr.h #include expr.h class LiteralExpr : public Expr { private: double value_; public: explicit LiteralExpr(double v) : value_(v) {} double evaluate() const override { return value_; } std::string toString() const override { // 处理整数显示为 5.0 → 5避免冗余小数点 if (value_ static_castlong long(value_)) { return std::to_string(static_castlong long(value_)); } return std::to_string(value_); } std::string type() const override { return Literal; } };2.2.2 二元运算符节点 BinaryExpr封装,-,*,/,^幂等双操作数运算需明确结合性与优先级// binary_expr.h #include expr.h #include cmath enum class BinaryOp { ADD, SUB, MUL, DIV, POW }; class BinaryExpr : public Expr { private: std::unique_ptrExpr left_; std::unique_ptrExpr right_; BinaryOp op_; // 静态辅助函数根据 op 计算结果 static double compute(double l, double r, BinaryOp op) { switch (op) { case BinaryOp::ADD: return l r; case BinaryOp::SUB: return l - r; case BinaryOp::MUL: return l * r; case BinaryOp::DIV: if (r 0.0) throw std::runtime_error(Division by zero); return l / r; case BinaryOp::POW: return std::pow(l, r); } throw std::runtime_error(Unknown binary op); } public: BinaryExpr(std::unique_ptrExpr l, std::unique_ptrExpr r, BinaryOp op) : left_(std::move(l)), right_(std::move(r)), op_(op) {} double evaluate() const override { double lval left_-evaluate(); double rval right_-evaluate(); return compute(lval, rval, op_); } std::string toString() const override { std::string leftStr left_-toString(); std::string rightStr right_-toString(); std::string opStr; switch (op_) { case BinaryOp::ADD: opStr ; break; case BinaryOp::SUB: opStr -; break; case BinaryOp::MUL: opStr *; break; case BinaryOp::DIV: opStr /; break; case BinaryOp::POW: opStr ^; break; } // 括号规则当前运算符优先级低于子节点时加括号 // 例如a b * c → a (b * c)因 * 优先级高于 auto needParen [](const Expr e, BinaryOp parentOp) - bool { if (dynamic_castconst BinaryExpr*(e) nullptr) return false; const auto be static_castconst BinaryExpr(e); auto childOp be.op_; // 优先级表POW(3) MUL/DIV(2) ADD/SUB(1) auto prio [](BinaryOp o) - int { switch (o) { case BinaryOp::POW: return 3; case BinaryOp::MUL: case BinaryOp::DIV: return 2; case BinaryOp::ADD: case BinaryOp::SUB: return 1; default: return 0; } }; return prio(childOp) prio(parentOp); }; if (needParen(*left_, op_)) leftStr ( leftStr ); if (needParen(*right_, op_)) rightStr ( rightStr ); return leftStr opStr rightStr; } std::string type() const override { return Binary; } };提示toString()中的括号逻辑是课设高分关键点。它模拟了真实编译器的“反编译”能力——输入12*3解析后打印1 (2 * 3)证明树结构正确。若忽略此逻辑输出1 2 * 3无法区分是线性序列还是树形结构。2.2.3 一元负号节点 UnaryMinusExpr处理-5或-(ab)等前缀负号是常见易漏节点// unary_expr.h #include expr.h class UnaryMinusExpr : public Expr { private: std::unique_ptrExpr expr_; public: explicit UnaryMinusExpr(std::unique_ptrExpr e) : expr_(std::move(e)) {} double evaluate() const override { return -expr_-evaluate(); } std::string toString() const override { std::string inner expr_-toString(); // 负号优先级最高仅当内部是二元运算时加括号- (a b) if (dynamic_castconst BinaryExpr*(expr_.get()) ! nullptr) { return -( inner ); } return - inner; } std::string type() const override { return UnaryMinus; } };2.2.4 括号节点 ParenExpr显式封装而非语法糖许多实现将括号视为解析时的控制流不生成节点。但课设要求“表达式类型”括号应作为独立类型存在体现结构意识// paren_expr.h #include expr.h class ParenExpr : public Expr { private: std::unique_ptrExpr expr_; public: explicit ParenExpr(std::unique_ptrExpr e) : expr_(std::move(e)) {} double evaluate() const override { return expr_-evaluate(); } std::string toString() const override { return ( expr_-toString() ); } std::string type() const override { return Paren; } };2.3 类型安全的工厂函数避免裸 new统一内存管理为防止内存泄漏并简化使用提供静态工厂方法// expr_factory.h #include expr.h #include literal_expr.h #include binary_expr.h #include unary_expr.h #include paren_expr.h class ExprFactory { public: static std::unique_ptrExpr makeLiteral(double value) { return std::make_uniqueLiteralExpr(value); } static std::unique_ptrExpr makeBinary(std::unique_ptrExpr left, std::unique_ptrExpr right, BinaryOp op) { return std::make_uniqueBinaryExpr(std::move(left), std::move(right), op); } static std::unique_ptrExpr makeUnaryMinus(std::unique_ptrExpr expr) { return std::make_uniqueUnaryMinusExpr(std::move(expr)); } static std::unique_ptrExpr makeParen(std::unique_ptrExpr expr) { return std::make_uniqueParenExpr(std::move(expr)); } };3. 递归下降解析器从字符串到表达式树的确定性转换有了表达式类型体系下一步是构建解析器——将输入字符串如2 * (3 4)无歧义地转换为Expr对象树。课设不需完整词法分析器但必须体现运算符优先级和结合性的处理逻辑。我们采用手工编写的递归下降解析器其核心是三个层级函数parseExpression()处理/-、parseTerm()处理*//、parseFactor()处理^、括号、字面量。3.1 词法扫描器 Tokenizer轻量级字符串切片不引入第三方 lexer用std::string_view和状态机实现// tokenizer.h #include string #include string_view #include cctype #include stdexcept struct Token { enum Type { NUMBER, PLUS, MINUS, MUL, DIV, POW, LPAREN, RPAREN, END } type; double number; // 仅对 NUMBER 有效 Token(Type t) : type(t), number(0.0) {} Token(Type t, double n) : type(t), number(n) {} }; class Tokenizer { private: std::string_view input_; size_t pos_ 0; void skipWhitespace() { while (pos_ input_.size() std::isspace(input_[pos_])) pos_; } Token nextNumber() { size_t start pos_; bool hasDot false; while (pos_ input_.size()) { char c input_[pos_]; if (std::isdigit(c)) { pos_; } else if (c . !hasDot) { hasDot true; pos_; } else { break; } } if (pos_ start) throw std::runtime_error(Expected number at position std::to_string(pos_)); std::string numStr(input_.substr(start, pos_ - start)); return Token(Token::NUMBER, std::stod(numStr)); } public: explicit Tokenizer(std::string_view s) : input_(s) {} Token nextToken() { skipWhitespace(); if (pos_ input_.size()) return Token(Token::END); char c input_[pos_]; pos_; switch (c) { case : return Token(Token::PLUS); case -: return Token(Token::MINUS); case *: return Token(Token::MUL); case /: return Token(Token::DIV); case ^: return Token(Token::POW); case (: return Token(Token::LPAREN); case ): return Token(Token::RPAREN); default: if (std::isdigit(c) || c .) { --pos_; // 回退让 nextNumber 处理 return nextNumber(); } throw std::runtime_error(Unexpected character std::string(1, c) ); } } };3.2 递归下降解析器 Parser按优先级分层调用// parser.h #include tokenizer.h #include expr_factory.h #include expr.h class Parser { private: Tokenizer tokenizer_; Token current_; void consume(Token::Type expected) { if (current_.type ! expected) { throw std::runtime_error(Expected tokenTypeName(expected) , got tokenTypeName(current_.type)); } current_ tokenizer_.nextToken(); } std::string tokenTypeName(Token::Type t) { switch (t) { case Token::NUMBER: return number; case Token::PLUS: return ; case Token::MINUS: return -; case Token::MUL: return *; case Token::DIV: return /; case Token::POW: return ^; case Token::LPAREN: return (; case Token::RPAREN: return ); case Token::END: return end of input; default: return unknown; } } public: explicit Parser(std::string_view input) : tokenizer_(input) { current_ tokenizer_.nextToken(); } std::unique_ptrExpr parse() { auto expr parseExpression(); if (current_.type ! Token::END) { throw std::runtime_error(Unexpected token after expression); } return expr; } private: // Expression → Term { ( | -) Term } std::unique_ptrExpr parseExpression() { auto left parseTerm(); while (current_.type Token::PLUS || current_.type Token::MINUS) { Token::Type op current_.type; consume(op); auto right parseTerm(); BinaryOp binOp (op Token::PLUS) ? BinaryOp::ADD : BinaryOp::SUB; left ExprFactory::makeBinary(std::move(left), std::move(right), binOp); } return left; } // Term → Factor { (* | /) Factor } std::unique_ptrExpr parseTerm() { auto left parseFactor(); while (current_.type Token::MUL || current_.type Token::DIV) { Token::Type op current_.type; consume(op); auto right parseFactor(); BinaryOp binOp (op Token::MUL) ? BinaryOp::MUL : BinaryOp::DIV; left ExprFactory::makeBinary(std::move(left), std::move(right), binOp); } return left; } // Factor → Primary { ^ Factor } // 注意^ 是右结合故递归调用自身 std::unique_ptrExpr parseFactor() { auto left parsePrimary(); if (current_.type Token::POW) { consume(Token::POW); auto right parseFactor(); // 右结合a^b^c → a^(b^c) left ExprFactory::makeBinary(std::move(left), std::move(right), BinaryOp::POW); } return left; } // Primary → NUMBER | ( Expression ) | - Primary std::unique_ptrExpr parsePrimary() { if (current_.type Token::NUMBER) { double val current_.number; consume(Token::NUMBER); return ExprFactory::makeLiteral(val); } else if (current_.type Token::LPAREN) { consume(Token::LPAREN); auto expr parseExpression(); consume(Token::RPAREN); return ExprFactory::makeParen(std::move(expr)); } else if (current_.type Token::MINUS) { consume(Token::MINUS); auto expr parsePrimary(); // 递归处理 -(-5) 等嵌套 return ExprFactory::makeUnaryMinus(std::move(expr)); } else { throw std::runtime_error(Expected number, (, or - at position std::to_string(tokenizer_.pos_)); } } };关键说明parseFactor()中^的右结合性通过parseFactor()递归调用自身实现而非parsePrimary()。这是递归下降处理右结合运算符的标准手法。若误写为parsePrimary()则2^3^4会被解析为(2^3)^48^44096而非正确结果2^(3^4)2^81≈2.4e24。3.3 完整解析流程从 main 到树验证// main.cpp #include iostream #include string #include parser.h int main() { std::string input; std::cout Enter expression (e.g., 2*(34) or -5^2): ; std::getline(std::cin, input); try { Parser parser(input); auto expr parser.parse(); std::cout Parsed tree: expr-toString() \n; std::cout Result: expr-evaluate() \n; // 验证类型分布遍历树统计各节点数量 struct Counter { int literal 0, binary 0, unary 0, paren 0; void visit(const Expr e) { if (e.type() Literal) literal; else if (e.type() Binary) binary; else if (e.type() UnaryMinus) unary; else if (e.type() Paren) paren; // 递归访问子节点 if (const auto* be dynamic_castconst BinaryExpr*(e)) { visit(*be-left_); visit(*be-right_); } else if (const auto* ue dynamic_castconst UnaryMinusExpr*(e)) { visit(*ue-expr_); } else if (const auto* pe dynamic_castconst ParenExpr*(e)) { visit(*pe-expr_); } } }; Counter c; c.visit(*expr); std::cout Node count - Literal: c.literal , Binary: c.binary , Unary: c.unary , Paren: c.paren \n; } catch (const std::exception e) { std::cerr Error: e.what() \n; return 1; } return 0; }编译命令Linux/macOSg -stdc17 -O2 -Wall -Wextra main.cpp parser.cpp tokenizer.cpp \ expr.cpp literal_expr.cpp binary_expr.cpp unary_expr.cpp paren_expr.cpp \ -o expr_eval4. 表达式树的进阶应用求导、优化与可视化验证课设报告的价值不仅在于“能算”更在于展示表达式类型系统的可扩展性。以下三个技巧均基于现有Expr继承体系无需修改核心类仅添加新函数或新子类体现面向对象设计的真正优势。4.1 符号求导为每个节点添加 derive() 方法在Expr基类中追加纯虚函数virtual std::unique_ptrExpr derive(const std::string var) const 0;然后为各子类实现求导规则LiteralExpr::derive()→ 返回makeLiteral(0.0)BinaryExpr::derive()→ 根据运算符应用乘积法则、商法则、链式法则UnaryMinusExpr::derive()→makeUnaryMinus( expr_-derive(var) )ParenExpr::derive()→makeParen( expr_-derive(var) )以乘法为例u*v的导数为u*v u*v// 在 binary_expr.cpp 中补充 std::unique_ptrExpr BinaryExpr::derive(const std::string var) const { if (op_ BinaryOp::MUL) { auto leftDeriv left_-derive(var); auto rightDeriv right_-derive(var); auto leftTimesRightDeriv ExprFactory::makeBinary( std::move(left_), std::move(rightDeriv), BinaryOp::MUL); auto leftDerivTimesRight ExprFactory::makeBinary( std::move(leftDeriv), std::move(right_), BinaryOp::MUL); return ExprFactory::makeBinary( std::move(leftTimesRightDeriv), std::move(leftDerivTimesRight), BinaryOp::ADD); } // 其他运算符类似实现... throw std::runtime_error(Derivative not implemented for this operator); }调用方式// main.cpp 中追加 auto deriv expr-derive(x); // 对变量 x 求导 std::cout Derivative w.r.t x: deriv-toString() \n;4.2 常量折叠优化运行时简化表达式树在Expr中添加virtual std::unique_ptrExpr optimize() const对子树进行代数简化BinaryExpr若左右子树均为LiteralExpr则直接计算并返回新LiteralExprBinaryExpr若一端为LiteralExpr(0)且运算是/-/*则返回简化形式如0a → a,a*0 → 0UnaryMinusExpr若内部是LiteralExpr则合并为单个字面量此优化在evaluate()前调用显著提升重复求值性能并减少树深度。4.3 DOT 格式导出用 Graphviz 可视化表达式树为Expr添加virtual std::string toDot() const生成标准 DOT 语言描述// expr.h 中追加 virtual std::string toDot() const 0; // 在 BinaryExpr::toDot() 中 std::string id node_ std::to_string(reinterpret_castlong long(this)); std::string leftId node_ std::to_string(reinterpret_castlong long(left_.get())); std::string rightId node_ std::to_string(reinterpret_castlong long(right_.get())); return id [label\ opStr \];\n id - leftId ;\n id - rightId ;\n left_-toDot() right_-toDot();生成.dot文件后用dot -Tpng expr.dot -o expr.png即可获得清晰树图直观验证解析正确性——这是课设答辩时最有力的可视化证据。5. 课设报告与源代码组织规范让评审老师一眼看到设计深度一份高分课设报告绝不仅是代码堆砌。其结构应体现“问题→分析→设计→实现→验证”的完整工程闭环。以下是经多所高校数据结构课程验证的报告框架所有章节均指向标题中的“表达式类型的实现”这一核心5.1 报告必备章节与内容要点章节必含内容为何重要1. 需求分析明确列出支持的运算符 - * / ^、结合性^右结合、括号嵌套、负号处理对比“仅支持整数”与“支持浮点”的设计取舍展示问题拆解能力避免被质疑“功能随意”2. 类型设计UML 类图手绘或 PlantUML 生成标注Expr为抽象基类LiteralExpr/BinaryExpr等为具体类箭头标明继承关系说明std::unique_ptr的所有权语义体现面向对象建模功底是 C 课设区别于 C 课设的关键3. 解析算法给出parseExpression()/parseTerm()/parseFactor()的伪代码标注各函数负责的优先级层级用23*4和2*34对比说明左结合性如何保证证明理解语法分析本质而非照抄模板4. 关键代码片段仅贴 3~4 段核心代码BinaryExpr::toString()的括号逻辑、Parser::parseFactor()的右结合实现、Tokenizer::nextNumber()的浮点解析每段附 2 行注释说明设计意图避免代码冗长聚焦评审关注点5. 测试用例与结果表格列出 8~10 个测试用例含边界-5,2^3^2,(12)*3,1/0列明输入、期望输出、实际输出、是否通过对失败用例分析原因体现工程验证意识比“程序能跑”更有说服力5.2 源代码目录结构符合 C 项目惯例expr_project/ ├── README.md # 一行说明项目目标两行编译运行指令 ├── CMakeLists.txt # 若用 CMake推荐否则提供 Makefile ├── src/ │ ├── expr.h # Expr 基类声明 │ ├── literal_expr.h/cpp # 字面量实现 │ ├── binary_expr.h/cpp # 二元运算符实现 │ ├── unary_expr.h/cpp # 一元负号实现 │ ├── paren_expr.h/cpp # 括号实现 │ ├── tokenizer.h/cpp # 词法扫描器 │ ├── parser.h/cpp # 递归下降解析器 │ └── main.cpp # 主函数含简单交互 ├── test/ │ └── test_cases.txt # 纯文本测试用例每行 input:expected_result └── doc/ └── expr_tree.dot # 示例 DOT 文件供 Graphviz 渲染提示提交源代码时务必删除所有 IDE 临时文件.vscode/,build/,*.swp只保留.h/.cpp/.txt。压缩包命名为expr_学号_姓名.zip而非课设.rar——细节体现专业素养。5.3 三个易被忽略的高分细节异常处理粒度Tokenizer在非法字符处抛std::runtime_errorBinaryExpr::evaluate()在除零时抛同类型异常main.cpp统一捕获并友好提示。避免exit(1)或未捕获崩溃。浮点精度处理LiteralExpr::toString()中if (value_ static_castlong long(value_))判断确保5.0显示为55.1显示为5.100000默认精度不强制std::fixed——课设不考核数值精度但显示整洁度影响观感。内存安全验证在main.cpp结尾添加std::cout Memory OK.\n;并在valgrind --leak-checkfull ./expr_eval下运行全部测试用例确认definitely lost: 0 bytes。这是 C 课设的硬性红线。最终交付物中报告 PDF 与源代码 ZIP 必须内容严格一致报告中引用的类名、函数名、测试用例必须能在代码中找到完全匹配的实现。评审老师常会随机打开一个报告截图再 grep 代码验证——这种严谨性是区分“完成作业”与“完成课设”的分水岭。本文还有配套的精品资源点击获取
返回列表