C++实现四则运算器(无括号)
发布时间:2026/11/2 11:23:13 锦皓数字建站
`)
前言「无括号四则运算器」要求解析只包含加、减、乘、除、整数和空格的表达式按先乘除后加减、同级从左到右的规则求值。它比带括号的版本少了一层「括号递归」但优先级、一元负号、多位数、除零这些边界一个都不少是练习递归下降recursive descent的好题目。常见误解有三类。第一从左到右顺序扫描、遇到运算符就立刻计算这样1 2 * 3会得到 9 而不是 7。第二只读一位数字遇到123只取1。第三把一元负号当成二元减号处理2 * -3在乘号后面遇到-时解析失败。本文用递归下降实现把表达式拆成「表达式 → 项 → 因子」三层每层只处理自己那一级的运算符优先级由调用层次天然保证。代码以 C17 为准使用long long做整数运算文末会说明溢出与除零的处理边界。一、文法与优先级把表达式写成文法expr : term (( | -) term)*term : factor ((* | /) factor)*factor : ( | -)* numbernumber : digitexpr处理加减term处理乘除factor处理一元正负号和数字。调用关系是expr → term → factor越往下优先级越高。每一层用循环处理「同级运算符」因此同级运算符自然左结合10 - 3 - 2先算10 - 3再减 2结果是 5而不是10 - (3 - 2)。这种结构的好处是加括号只需要在factor里增加一条「遇到左括号就递归调用expr再匹配右括号」的分支其他层不用改。本文不实现括号但保留这个扩展点。数字可以有多位解析时循环读取连续数字用value value * 10 digit累加。空白字符在每一层读取前跳过因此1 2 *3和1 2 * 3等价。空白只允许出现在记号之间不能出现在数字中间12 3会被解析成两个数随后因缺少运算符而报错。二、递归下降的实现结构用一个Parser类保存输入字符串的引用和当前下标pos_。每个解析函数都从pos_当前位置开始读取自己负责的部分并把pos_推进到下一个未处理字符。parseExpr调用parseTerm得到左值。循环跳过空白若当前字符是或-记录运算符并前进调用parseTerm得到右值执行加减否则退出循环。返回累计结果。parseTerm与parseExpr同构只是运算符换成*、/调用的下层换成parseFactor。除法要先检查右值是否为 0整数除法向零截断-7 / 2得到 -3这是 C 的规则不要按数学上的「向下取整」理解。parseFactor跳过空白。若当前是或-前进并递归调用自身返回正/负结果。这允许--5、-5这类写法。否则必须是数字循环读取连续数字累加成整数。若一个数字都没有抛出解析错误。最外层parse调用parseExpr后还要跳过空白并检查pos_是否到达末尾。若还有剩余字符例如1 2abc说明输入非法不能默默忽略。三、边界除零、溢出与非法字符除零整数除以 0 是未定义行为undefined behavior, UB必须在除法前检查。示例用异常报告错误调用方捕获后打印提示。不要用「返回 0」掩盖错误那会让1/0看起来像合法结果。溢出long long的加减乘都可能溢出有符号溢出是 UB。示例在解析数字时检查了「乘 10 加 digit」是否超过LLONG_MAX但运算符层面的溢出没有逐一检查因此要求输入的中间结果落在long long范围内。生产代码若不能保证这一点应改用大整数库或在每次加减乘前做范围检查不要依赖「溢出会回绕」这种未定义行为。另外LLONG_MIN / -1也会溢出示例单独拦截。非法字符数字解析失败、运算符后没有操作数、表达式末尾有多余字符都应该报错。错误信息至少包含位置方便定位。示例用std::runtime_error抛出主函数捕获后输出。空表达式parseFactor在没有任何数字时抛错因此空串、只有空白的串都会失败不会返回 0。一元负号与二元减号2-3中的-由parseExpr处理2*-3中的-出现在parseFactor开头由一元分支处理。两者靠「当前位置在哪个解析层」区分不需要额外的词法状态。四、完整实现// 适用于 C17#include cctype#include iostream#include limits#include stdexcept#include stringclass Parser {public:explicit Parser(const std::string text) : text_(text) {}long long parse() {const long long value parseExpr();skipSpaces();if (pos_ ! text_.size()) {throw std::runtime_error(unexpected trailing characters);}return value;}private:void skipSpaces() {while (pos_ text_.size() std::isspace(static_castunsigned char(text_[pos_]))) {pos_;}}long long parseExpr() {long long value parseTerm();for (;;) {skipSpaces();if (pos_ text_.size() ||(text_[pos_] ! text_[pos_] ! -)) {break;}const char op text_[pos_];const long long rhs parseTerm();value (op ) ? value rhs : value - rhs;}return value;}long long parseTerm() {long long value parseFactor();for (;;) {skipSpaces();if (pos_ text_.size() ||(text_[pos_] ! * text_[pos_] ! /)) {break;}const char op text_[pos_];const long long rhs parseFactor();if (op *) {value * rhs;} else {if (rhs 0) {throw std::runtime_error(division by zero);}if (value std::numeric_limitslong long::min() rhs -1) {throw std::runtime_error(integer overflow);}value / rhs;}}return value;}long long parseFactor() {skipSpaces();if (pos_ text_.size() (text_[pos_] || text_[pos_] -)) {const char sign text_[pos_];const long long value parseFactor();return (sign -) ? -value : value;}if (pos_ text_.size() ||!std::isdigit(static_castunsigned char(text_[pos_]))) {throw std::runtime_error(number expected);}long long value 0;while (pos_ text_.size() std::isdigit(static_castunsigned char(text_[pos_]))) {const int digit text_[pos_] - 0;if (value (std::numeric_limitslong long::max() - digit) / 10) {throw std::runtime_error(number too large);}value value * 10 digit;pos_;}return value;}const std::string text_;std::size_t pos_ 0;};int main() {std::string line;if (!std::getline(std::cin, line)) {return 1;}try {Parser parser(line);std::cout parser.parse() \n;} catch (const std::exception e) {std::cout error: e.what() \n;return 1;}return 0;}按语法推演几个输入1 2 * 3先由parseTerm读出 2、遇到*读出 3得到 6再由parseExpr加 1结果是 710 - 3 - 2在parseExpr的循环里先算 7再减 2结果是 52 * -3 10在parseTerm中把-3作为因子得到 -6再加 10结果是 410 / 3是整数除法结果是 31 / 0在除法前抛异常。代码中的加减乘没有做逐次溢出检查这是有意的简化完整检查会让示例膨胀一倍而且需要处理LLONG_MIN的各种边界。文章已经把「中间结果必须在long long范围内」写成前提若你的输入可能接近边界必须在value rhs、value - rhs、value * rhs之前加检查不能假定溢出是安全的。常见坑点1. 从左到右直接求值❌ 扫描到运算符就立刻用当前累计值计算1 2 * 3得到 9。✅ 分层解析parseTerm先算乘除parseExpr再算加减。2. 只读一位数字❌value text_[pos_] - 0; pos_;123只得到 1。✅ 用while读取连续数字累加value * 10 digit。3. 不跳过空白❌ 遇到空格就报「非法字符」。✅ 每层读取前skipSpaces并注意std::isspace的参数要转unsigned char。4. 不支持一元负号❌2 * -3在乘号后调用数字解析直接失败。✅ 在parseFactor开头处理/-并递归。5. 除法不检查零❌value / rhs;当rhs 0时是 UB。✅ 先判断rhs 0并报错。6. 忽略尾部多余字符❌parseExpr返回后直接输出12abc被当成 3。✅ 解析完检查pos_ text_.size()。7. 把整数除法当浮点除法❌ 认为7 / 2是 3.5。✅ 整数相除向零截断需要小数就改用double或定点表示。8. 认为溢出会安全回绕❌ 依赖long long溢出后的值做判断。✅ 有符号溢出是 UB加检查或改用大整数库。总结层次负责运算符expr加减、-左结合term乘除*、/左结合factor一元正负、数字、-前缀边界处理多位数循环累加空白每层跳过除零除法前检查尾部字符解析后校验溢出解析检查 前提约束无括号四则运算器的骨架就是三层递归下降优先级高的层被优先级低的层调用同级运算符用循环实现左结合。把「多位数、空白、一元符号、除零、尾部校验」这五个边界补上表达式求值就不会在看起来简单的输入上出错。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。