C语言实现科学计算器:调度场算法、逆波兰表达式与三角函数处理

发布时间:2026/9/15 20:59:35
C语言实现科学计算器:调度场算法、逆波兰表达式与三角函数处理 简介这是一份用纯C语言编写的计算器源代码合集面向C语言初学者和对命令行表达式计算感兴趣的开发者。三组源码采用递进式设计第一版仅实现加法和乘法第二版支持四则混合运算最终版加入abs、sin、cos等自定义函数逐步展示表达式解析、运算符优先级和函数扩展的实现过程。压缩包共6个文件包含3个C源码、1个GCC编译生成的exe程序、1张运行截图及1份Markdown说明文档整体仅46KB轻量便于快速下载、编译和验证。目前已有1051人学习适合作为链表/栈解析表达式或编写小型工具的入门参考。代码运行示例清晰呈现了abs(3*5-4^2)、含括号的四则混合式以及三角函数的计算结果通过阅读源码可以理解如何把中缀表达式转化为可计算形式帮助读者举一反三只需少量修改即可扩展幂运算、对数等更多功能。1. 先解决两个最容易被新手写错的点运算符优先级和角度制同样一个表达式12*3如果按从左到右算结果是 9按数学规则结果是 7。写 C 语言计算器时这个差异不是“精度问题”而是“算法错误”。更隐蔽的是sin(30)在 C 语言里如果直接调math.h返回的不是 0.5而是 -0.988因为标准库的三角函数默认吃弧度不是角度。这篇博文要交付的东西很具体一份能直接编译运行的 C 语言计算器源代码支持 - * /四则混合运算能处理括号、负数并原生支持sin、cos、tan三角函数默认按角度制输入。你不需要引入任何第三方库只需要 GCC 和标准 C 库。适合刚学完指针和函数想动手写一个完整小项目的读者也适合需要把表达式求值模块嵌进自己嵌入式或上位机项目里的工程师。下面从表达式求值的核心算法讲起。2. 先用调度场算法把表达式拆成 Token四则混合运算的骨架2.1 为什么不能靠两个数字栈硬算优先级和括号需要“等待”很多初学 C 语言的人一开始会这样写两个变量a和b读到一个运算符就立刻算遇到括号就懵。这种写法只能处理ab这种二元表达式遇到12*3就会把2*3的*错误地提前执行。正确的做法是把“运算符优先级”和“括号优先级”抽象成一套规则让计算过程变成“先解析、后求值”两个阶段。常见且成熟的方案是 Edsger Dijkstra 提出的调度场算法Shunting Yard Algorithm。它把人类书写的中缀表达式12*3转换成计算机容易执行的后缀表达式1 2 3 * 也叫逆波兰表达式。后缀表达式的好处是不再需要优先级判断遇到数字入栈遇到运算符就弹出栈顶的两个数字计算再把结果压回栈。整个过程线性扫描非常适合 C 语言实现。2.2 词法分析先把数字、运算符和函数名分开表达式求值的第一步不是计算而是“切词”。比如输入-3.14sin(30)*2词法分析器要把它拆成负号或减号-数字3.14加号函数名sin左括号(数字30右括号)乘号*数字2在 C 语言里可以用一个枚举定义 Token 类型typedef enum { TOKEN_NUMBER, TOKEN_PLUS, TOKEN_MINUS, TOKEN_MULTIPLY, TOKEN_DIVIDE, TOKEN_LPAREN, TOKEN_RPAREN, TOKEN_SIN, TOKEN_COS, TOKEN_TAN, TOKEN_END, TOKEN_UNKNOWN } TokenType;词法分析函数逐个字符扫描用isdigit判断数字起点用strncmp匹配函数名。需要注意-既可能是一元负号也可能是二元减号。词法分析阶段先统一输出TOKEN_MINUS把“一元还是二元”的判断交给语法处理阶段。常见的简单做法是如果-出现在表达式开头或者出现在左括号、运算符之后就把它标记为“一元负号”。可以在 Token 结构体里加一个is_unary字段。2.3 中缀转后缀的 C 语言实现调度场算法的核心代码调度场算法用两个数据结构一个运算符栈一个输出队列。数字直接进输出队列运算符要跟栈顶比较优先级如果栈顶优先级不低于当前运算符就把栈顶弹出到输出队列再继续比较。左括号直接压栈右括号则把栈顶弹出直到左括号。函数名像左括号一样压栈遇到右括号时弹出到输出队列。下面是一段可直接编译运行的核心代码只包含转换过程#include stdio.h #include stdlib.h #include string.h #include ctype.h #define MAX_STACK 128 typedef struct { int type; // TokenType 值 double value; // 数字值 } Token; Token op_stack[MAX_STACK]; Token output[MAX_STACK]; int op_top -1; int out_len 0; int precedence(int type) { if (type TOKEN_PLUS || type TOKEN_MINUS) return 1; if (type TOKEN_MULTIPLY || type TOKEN_DIVIDE) return 2; return 0; } void push_op(Token t) { op_stack[op_top] t; } Token pop_op() { return op_stack[op_top--]; } // 中缀转后缀输入 tokens 数组输出到 output 全局数组 void shunting_yard(Token* tokens, int len) { for (int i 0; i len; i) { Token t tokens[i]; if (t.type TOKEN_NUMBER) { output[out_len] t; } else if (t.type TOKEN_SIN || t.type TOKEN_COS || t.type TOKEN_TAN) { push_op(t); // 函数名压栈按左括号处理 } else if (t.type TOKEN_LPAREN) { push_op(t); } else if (t.type TOKEN_RPAREN) { while (op_top 0 op_stack[op_top].type ! TOKEN_LPAREN) { output[out_len] pop_op(); } pop_op(); // 丢弃左括号 if (op_top 0 (op_stack[op_top].type TOKEN_SIN || op_stack[op_top].type TOKEN_COS || op_stack[op_top].type TOKEN_TAN)) { output[out_len] pop_op(); // 函数弹出到输出队列 } } else if (t.type TOKEN_PLUS || t.type TOKEN_MINUS || t.type TOKEN_MULTIPLY || t.type TOKEN_DIVIDE) { while (op_top 0 precedence(op_stack[op_top].type) precedence(t.type)) { // 一元负号优先级高于二元运算符但这里简化处理 output[out_len] pop_op(); } push_op(t); } } while (op_top 0) { output[out_len] pop_op(); } }代码逻辑说明函数sin、cos、tan被当作“函数操作符”压栈遇到右括号时先弹出括号内的运算符再把函数名弹出到输出队列这样能保证后缀表达式里sin出现在它的参数之后便于后续压栈求值。优先级函数precedence只对四则运算符有效乘除为 2加减为 1。实际开发中还要处理一元负号否则-35会变成先入栈3和语义出错。我一般在词法分析阶段就把一元负号转换成一个特殊 TokenTOKEN_NEG它的优先级比乘除还高并且只弹出一个操作数。2.4 后缀表达式求值运算符参数表与执行顺序后缀表达式求值只需要一个数字栈。从左到右扫描输出队列数字压栈运算符弹出两个数字按运算规则压回结果函数名弹出一个数字调用对应数学函数。四则运算对应的操作数顺序要小心除法是a / b而不是b / a。下面的代码展示了求值器的核心循环double eval_rpn(Token* output, int len) { double stack[MAX_STACK]; int top -1; for (int i 0; i len; i) { Token t output[i]; if (t.type TOKEN_NUMBER) { stack[top] t.value; } else if (t.type TOKEN_PLUS) { double b stack[top--], a stack[top--]; stack[top] a b; } else if (t.type TOKEN_MINUS) { double b stack[top--], a stack[top--]; stack[top] a - b; } else if (t.type TOKEN_MULTIPLY) { double b stack[top--], a stack[top--]; stack[top] a * b; } else if (t.type TOKEN_DIVIDE) { double b stack[top--], a stack[top--]; if (b 0.0) { fprintf(stderr, error: divide by zero\n); exit(1); } stack[top] a / b; } else if (t.type TOKEN_SIN) { double a stack[top--]; stack[top] sin(a); } } return stack[top]; }这里sin直接调用了math.h里的函数需要链接-lm。后续会讲如何把角度制转换进去。一个容易忽略的点stack[top--]的求值顺序在 C 语言中是从右往左所以先取到的是右操作数b再取到左操作数a。如果你写反成a stack[top--]; b stack[top--];在同一个表达式里两个操作数会被交换结果完全错误。我习惯显式写两行避免编译器优化带来的歧义。下表汇总了后缀表达式中的 Token 执行方式Token 类型弹出数量操作内容示例TOKEN_NUMBER0压栈数字3.14TOKEN_PLUS2弹出右左相加1 2 -3TOKEN_MINUS2弹出右左相减5 2 --3TOKEN_MULTIPLY2弹出右左相乘3 4 *-12TOKEN_DIVIDE2弹出右左相除7 2 /-3.5TOKEN_SIN/COS/TAN1调用三角函数30 sin-0.5角度制至此四则混合运算的骨架已经立住了词法分析拆 token调度场转后缀后缀求值。下一步把三角函数接进这个骨架。3. 三角函数怎么接进求值器弧度、角度与查表法的取舍3.1 math.h 的坑为什么 sin(30) 等于 -0.988C 标准库的sin、cos、tan都要求参数为弧度。弧度制的定义是“弧长与半径之比”一个完整圆是2π弧度也就是 360 度。所以sin(30)在这里其实是求“30 弧度的正弦值”而不是“30 弧度的正弦值”。这不是 C 语言的问题而是数学约定问题。但你的计算器用户大概率想输入sin(30)得到 0.5所以需要在调用标准库函数之前做一次角度到弧度的换算。换算公式很简单radian degree * M_PI / 180.0。M_PI在math.h中不一定被定义它属于 POSIX 扩展。为了可移植最好自己定义一个宏#ifndef M_PI #define M_PI 3.14159265358979323846 #endif常见做法是把这个换算封装成一个函数而不是散落在求值器里。这样以后想切换成弧度模式只需要改一个开关。3.2 在 Token 流里识别 sin/cos/tan词法匹配的优先级词法分析时如果当前字符是字母就连续读到一个标识符然后用strcmp判断它是sin、cos还是tan。需要注意函数名后面必须紧跟左括号否则像sinx这种变量名会被误判。我的处理方式是读完整标识符后跳过空白符检查下一个非空白字符是否为(如果不是就报“未知函数名”错误。下面是一个简单的函数识别片段Token next_token(const char** src) { while (**src ) (*src); if (isdigit(**src) || **src .) { char* end; double v strtod(*src, end); *src end; return (Token){TOKEN_NUMBER, v}; } if (isalpha(**src)) { char name[16] {0}; int i 0; while (isalpha(**src) i 15) { name[i] **src; (*src); } // 跳过空格后检查左括号 const char* tmp *src; while (*tmp ) tmp; if (*tmp ! () { fprintf(stderr, error: expected ( after function name %s\n, name); exit(1); } if (strcmp(name, sin) 0) return (Token){TOKEN_SIN, 0}; if (strcmp(name, cos) 0) return (Token){TOKEN_COS, 0}; if (strcmp(name, tan) 0) return (Token){TOKEN_TAN, 0}; fprintf(stderr, error: unknown function %s\n, name); exit(1); } switch (**src) { case : (*src); return (Token){TOKEN_PLUS, 0}; case -: (*src); return (Token){TOKEN_MINUS, 0}; case *: (*src); return (Token){TOKEN_MULTIPLY, 0}; case /: (*src); return (Token){TOKEN_DIVIDE, 0}; case (: (*src); return (Token){TOKEN_LPAREN, 0}; case ): (*src); return (Token){TOKEN_RPAREN, 0}; default: fprintf(stderr, error: unknown character %c\n, **src); exit(1); } }参数说明strtod是标准库函数能把字符串开头的浮点数解析成double并通过end指针告诉你解析停在哪里。使用const char**指针是为了在函数内部移动输入串的当前位置。函数名列表目前只有三个扩展sqrt、log时只需要在这里增加 case同时在调度场算法里增加对应 Token 类型。3.3 角度与弧度换算的实现代码一个开关切换两种模式在求值阶段调用sin之前加入如下判断#define ANGLE_MODE_DEGREE 1 // 1 表示角度制0 表示弧度制 double apply_func(int func_type, double arg) { if (ANGLE_MODE_DEGREE) { arg arg * M_PI / 180.0; } switch (func_type) { case TOKEN_SIN: return sin(arg); case TOKEN_COS: return cos(arg); case TOKEN_TAN: return tan(arg); default: return 0; } }这样sin(30)在角度制下输出 0.5在弧度制下输出 -0.988。注意tan在 90 度和 270 度时理论上无定义但由于浮点数精度tan(90)会得到一个很大的值而不是无穷大。如果需要严格报错应该先判断cos(arg)的绝对值是否小于一个极小阈值比如1e-12。这个细节在工程上非常关键测量仪器、科学计算器遇到这种情况都会提示“数学错误”。3.4 精度比查表法更好为什么我选择直接调 math.h网上经常有人讨论“三角函数查表法实现”原因是嵌入式环境没有浮点数学库或者要求极高实时性。查表法把0~360度离散成 3600 个点每个点存一个float或int值通过线性插值逼近。它的优点是计算速度快、不需要-lm缺点也很明显精度受表大小限制而且处理任意角度时需要先归约。在 PC 环境和大多数带 FPU 的嵌入式芯片上math.h的三角函数实现已经经过充分优化和验证精度达到1e-15级别完全够用。如果你真的要做查表法可以只在没有硬件浮点单元的老式 8 位单片机上做。本项目的目标读者大概率是在 PC 上编译运行所以直接调用math.h把精力放在表达式解析上性价比最高。这里也是新手容易钻牛角尖的地方不要试图手写一个sin函数你大概率写不过用了三十年的 C 标准库。4. 把完整源代码组合起来一份可直接编译的 C 语言计算器4.1 主循环与内存管理设计一次性读入整行不做增量计算为了让交互体验贴近真实计算器我让主程序从标准输入读入一行表达式解析、求值然后打印结果。这样可以用管道批量测试也可以手动输入。内存管理上所有固定数组都用栈上的数组表达式最长 256 个字符Token 数组最多 128 个。对计算器这种小型工具来说栈上分配远比malloc可靠也避免了内存泄漏。流程顺序是fgets读入一行。next_token循环切词存入tokens数组。shunting_yard把tokens转为后缀表达式output。eval_rpn求值输出结果。这样做的好处是每个阶段都能独立测试比如单独打印后缀表达式来看调度场是否正确。4.2 完整的 calc.c 源代码单文件实现复制即用以下代码合并了前面提到的词法分析、调度场和求值器并加入了完整的一元负号处理和三角函数支持。为了压缩篇幅去掉了部分重复检查但核心逻辑完整。#include stdio.h #include stdlib.h #include string.h #include ctype.h #include math.h #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #define ANGLE_MODE_DEGREE 1 #define MAX_TOKEN 128 #define MAX_INPUT 256 typedef enum { TOKEN_NUMBER, TOKEN_PLUS, TOKEN_MINUS, TOKEN_MULTIPLY, TOKEN_DIVIDE, TOKEN_LPAREN, TOKEN_RPAREN, TOKEN_SIN, TOKEN_COS, TOKEN_TAN, TOKEN_NEG, TOKEN_END, TOKEN_UNKNOWN } TokenType; typedef struct { int type; double value; } Token; Token tokens[MAX_TOKEN]; Token output[MAX_TOKEN]; int token_len 0; int out_len 0; int precedence(int type) { if (type TOKEN_PLUS || type TOKEN_MINUS) return 1; if (type TOKEN_MULTIPLY || type TOKEN_DIVIDE) return 2; if (type TOKEN_NEG) return 3; return 0; } void tokenize(const char* src) { token_len 0; while (*src) { while (*src ) src; if (*src \0) break; if (isdigit(*src) || (*src . isdigit(*(src1)))) { char* end; tokens[token_len].type TOKEN_NUMBER; tokens[token_len].value strtod(src, end); src end; token_len; continue; } if (isalpha(*src)) { char name[16] {0}; int i 0; while (isalpha(*src) i 15) name[i] *src; const char* tmp src; while (*tmp ) tmp; if (*tmp ! () { fprintf(stderr, error: function %s must be followed by (\n, name); exit(1); } if (strcmp(name, sin) 0) tokens[token_len].type TOKEN_SIN; else if (strcmp(name, cos) 0) tokens[token_len].type TOKEN_COS; else if (strcmp(name, tan) 0) tokens[token_len].type TOKEN_TAN; else { fprintf(stderr, error: unknown function %s\n, name); exit(1); } continue; } switch (*src) { case : tokens[token_len].type TOKEN_PLUS; token_len; src; break; case -: if (token_len 0 || tokens[token_len-1].type TOKEN_LPAREN || tokens[token_len-1].type TOKEN_PLUS || tokens[token_len-1].type TOKEN_MINUS || tokens[token_len-1].type TOKEN_MULTIPLY || tokens[token_len-1].type TOKEN_DIVIDE) { tokens[token_len].type TOKEN_NEG; } else { tokens[token_len].type TOKEN_MINUS; } token_len; src; break; case *: tokens[token_len].type TOKEN_MULTIPLY; token_len; src; break; case /: tokens[token_len].type TOKEN_DIVIDE; token_len; src; break; case (: tokens[token_len].type TOKEN_LPAREN; token_len; src; break; case ): tokens[token_len].type TOKEN_RPAREN; token_len; src; break; default: fprintf(stderr, error: unexpected character %c\n, *src); exit(1); } } tokens[token_len].type TOKEN_END; } void shunting_yard() { Token op_stack[MAX_TOKEN]; int op_top -1; out_len 0; for (int i 0; i token_len; i) { Token t tokens[i]; if (t.type TOKEN_NUMBER) { output[out_len] t; } else if (t.type TOKEN_SIN || t.type TOKEN_COS || t.type TOKEN_TAN) { op_stack[op_top] t; } else if (t.type TOKEN_LPAREN) { op_stack[op_top] t; } else if (t.type TOKEN_RPAREN) { while (op_top 0 op_stack[op_top].type ! TOKEN_LPAREN) output[out_len] op_stack[op_top--]; op_top--; // 丢弃左括号 if (op_top 0 (op_stack[op_top].type TOKEN_SIN || op_stack[op_top].type TOKEN_COS || op_stack[op_top].type TOKEN_TAN)) output[out_len] op_stack[op_top--]; } else if (t.type TOKEN_NEG || t.type TOKEN_PLUS || t.type TOKEN_MINUS || t.type TOKEN_MULTIPLY || t.type TOKEN_DIVIDE) { while (op_top 0 precedence(op_stack[op_top].type) precedence(t.type)) output[out_len] op_stack[op_top--]; if (t.type TOKEN_NEG op_top 0 (op_stack[op_top].type TOKEN_SIN || op_stack[op_top].type TOKEN_COS || op_stack[op_top].type TOKEN_TAN)) { output[out_len] op_stack[op_top--]; } op_stack[op_top] t; } } while (op_top 0) { if (op_stack[op_top].type TOKEN_LPAREN) { fprintf(stderr, error: unmatched parenthesis\n); exit(1); } output[out_len] op_stack[op_top--]; } } double apply_func(int type, double arg) { if (ANGLE_MODE_DEGREE) arg arg * M_PI / 180.0; switch (type) { case TOKEN_SIN: return sin(arg); case TOKEN_COS: return cos(arg); case TOKEN_TAN: return tan(arg); default: return arg; } } double eval_rpn() { double stack[MAX_TOKEN]; int top -1; for (int i 0; i out_len; i) { Token t output[i]; switch (t.type) { case TOKEN_NUMBER: stack[top] t.value; break; case TOKEN_PLUS: { double b stack[top--], a stack[top--]; stack[top] a b; break; } case TOKEN_MINUS: { double b stack[top--], a stack[top--]; stack[top] a - b; break; } case TOKEN_MULTIPLY: { double b stack[top--], a stack[top--]; stack[top] a * b; break; } case TOKEN_DIVIDE: { double b stack[top--], a stack[top--]; if (b 0.0) { fprintf(stderr, error: divide by zero\n); exit(1); } stack[top] a / b; break; } case TOKEN_NEG: { double a stack[top--]; stack[top] -a; break; } case TOKEN_SIN: case TOKEN_COS: case TOKEN_TAN: { double a stack[top--]; stack[top] apply_func(t.type, a); break; } default: break; } } return stack[top]; } int main() { char line[MAX_INPUT]; printf(calc ); while (fgets(line, sizeof(line), stdin)) { line[strcspn(line, \n)] 0; if (strlen(line) 0) { printf(calc ); continue; } if (strcmp(line, exit) 0) break; tokenize(line); shunting_yard(); double result eval_rpn(); printf(%.10g\n, result); printf(calc ); } return 0; }这份代码有几个设计决定需要说明TOKEN_NEG被单独定义优先级设为 3高于乘除。遇到负号时词法分析会判断前一个 Token 是否存在且是否属于操作数或右括号。如果-之前没有 Token或者前一个是左括号或二元运算符就当作一元负号处理。apply_func统一处理角度制换算这是更换模式只需要改一个宏的关键位置。%.10g输出最多 10 位有效数字避免打印出0.30000000000000004。exit(1)用于遇到错误时直接结束程序。更完善的做法是返回错误码但在单文件小工具里直接退出能避免后续逻辑读到脏数据。4.3 用 gcc 编译并跑通最小表达式编译命令如下gcc -Wall -O2 -o calc calc.c -lm参数说明-Wall打开常见警告-O2开启优化-lm链接数学库。如果你忘了-lm链接器会报undefined reference to sin。运行测试echo 12*3 | ./calc calc 7 echo sin(30) | ./calc calc 0.5 echo cos(60)tan(45) | ./calc calc 1.5这里cos(60)是 0.5tan(45)是 1相加正好 1.5。如果输出1.500000003说明浮点数误差已经出现这是正常的后面第 5 章会讲如何控制显示。4.4 参数宏角度模式与缓冲区大小怎么调代码顶部的ANGLE_MODE_DEGREE宏是角度模式的总开关。设成1sin(30)输出 0.5设成0则所有三角函数按弧度处理。如果你正在做一个科学计算器建议通过命令行参数或环境变量来切换而不是重新编译。比如在main里读取argv[1]如果是--rad就设置一个全局变量。这里为了示例简洁用了编译期宏。MAX_INPUT和MAX_TOKEN分别控制输入长度和 Token 数量。在栈上声明 256 字节的数组对计算器程序足够了。如果你的表达式会超过 256 字符把MAX_INPUT调大同时注意fgets读取超长输入时会发生截断后续tokenize可能读到不完整的 token。更健壮的做法是用getline动态读取但可移植性会下降。5. 坑都藏在细节里调试四则混合运算与三角函数的边界问题5.1 一元负号和连续减号的歧义3--4应该被解析成什么表达式3--4数学上等价于3-(-4)结果是 7。很多简单计算器会报错因为词法分析把两个连续的-都当成二元减号。我的处理方式是第二个-前面是二元减号、左括号或表达式开头就判定为TOKEN_NEG。这样3--4的 Token 序列是3、减、负号、4调度场算法里的优先级能够正确生成后缀表达式3 4 NEG -求值为3 - (-4) 7。但是有一个隐藏问题-2*3会被解析成~2 * 3后缀是2 NEG 3 *结果是 -6正确。而2*-3会被解析成2 * NEG 3后缀是2 3 NEG *结果也是 -6。这说明一元负号的处理需要配合调度场算法里的优先级比较不能简单地把负号压栈后等待。我在调度场算法中对TOKEN_NEG专门处理了与函数名的关系如果栈顶是sin这类函数且当前是一元负号先弹出函数名再压入负号确保sin(-30)能正确转为-30 sin而不是30 sin -。5.2 浮点数误差为什么 0.10.2 不等于 0.3这是 C 语言计算器绕不开的话题。0.1和0.2在二进制浮点数里都是无限循环小数IEEE 754 双精度存储时会有舍入误差。0.1 0.2的实际值是0.30000000000000004。控制输出格式只能让显示“看起来正确”内部参与后续运算时误差仍会累积。处理思路有两种显示用%.10g或%.12g保留 10 到 12 位有效数字避免把噪声打印出来。比较两个计算结果时不要用而是用绝对误差小于某个阈值比如fabs(a-b) 1e-12。在测试代码里必须这样写否则像sin(30) 0.5这样的断言会随机失败。如果你写的是记账类计算器必须用十进制字符串运算或者定点数改用long long按分为单位存储。但科学计算器允许浮点误差因为仪器测量本身就有误差用户能接受小数点后第 12 位的差异。5.3 除零和非法字符错误码怎么设计现在的代码遇到错误直接exit(1)这只适合命令行工具。如果你想把它做成库函数嵌入其他系统需要改造为返回错误码。常见的设计是定义枚举typedef enum { CALC_OK 0, CALC_ERR_UNKNOWN_CHAR, CALC_ERR_UNKNOWN_FUNC, CALC_ERR_MISMATCH_PAREN, CALC_ERR_DIVIDE_ZERO, CALC_ERR_STACK_OVERFLOW } CalcStatus;每种错误对应一个退出码主程序根据错误码打印不同提示。这样做的好处是外层程序可以捕获错误而不是被迫退出。例如 Web 后端调用计算模块就需要拿到错误码后返回 HTTP 400而不是让整个进程崩溃。下表列出本项目可能出现的错误分类场景当前行为建议行为除以零fprintf(stderr); exit(1)返回特殊值NAN或错误码括号不匹配转换时检测到左括号未弹出时报错在词法分析阶段提前计数左右括号未知字符打印错误并退出返回错误码标出字符串位置函数名拼错sinn(30)未知函数提示“是否想输入 sin”建议在词法分析阶段就对括号进行计数左右括号数量不等时直接拒绝输入避免调度场算法运行到一半才发现问题。5.4 用 gdb 和断言验证中间结果调试优先级错误的最快路径四则混合运算的 bug 绝大多数出现在调度场阶段而不是求值阶段。我自己调试时常用两步走。先打印后缀表达式看看转换是否正确gdb ./calc (gdb) break shunting_yard (gdb) run calc 12*3 (gdb) next (gdb) p output[0] (gdb) p output[1]如果输出序列是1 2 3 * 说明优先级正常如果是1 2 3 * 被截断检查out_len是否超出数组。另一个高效工具是assert在关键位置插入断言。比如在eval_rpn函数开始处断言out_len 0在弹出栈元素前断言top 0。这样一旦出现栈下溢程序会立刻告诉你哪一步出了问题而不是输出一个莫名的内存值。6. 最后再迈一步用 shell 脚本做回归测试并扩展成科学计算器最后一章不写总结直接给你一个可落地的进阶技巧用几行 shell 脚本把计算器的测试自动化。这个方法比手动敲命令可靠得多特别是当你要在“支持三角函数”的基础上增加sqrt、log、pow功能时回归测试能减少大量排错时间。先看测试脚本test_calc.sh#!/bin/bash pass0 fail0 check() { local expr$1 local expected$2 local actual$(echo $expr | ./calc | tail -n 1) # 用 awk 做浮点比较容差 1e-9 local ok$(awk -v e$expected -v a$actual BEGIN { print (a e-1e-9 a e1e-9) ? 1 : 0 }) if [ $ok -eq 1 ]; then pass$((pass1)) else fail$((fail1)) echo FAIL: $expr $actual (expected $expected) fi } check 12*3 7 check 10/4 2.5 check sin(30) 0.5 check cos(60)tan(45) 1.5 check -35 2 check 2*-4 -8 check 2*(34) 14 echo PASS: $pass, FAIL: $fail exit $fail这段脚本的分析方法值得单独说管道里echo $expr | ./calc | tail -n 1是为了只取最后一行结果因为程序有calc提示符输出。awk做浮点比较而不是直接用原因是sin(30)的实际结果可能是0.49999999999999994直接比较会误判。容差设成1e-9对三角函数精度绰绰有余。脚本最后用exit $fail让 CI 系统能感知测试失败。如果你还想把计算器扩展成真正意义上的科学计算器可以考虑以下方向增加幂运算符^在调度场算法里让它右结合。右结合的意思是2^3^2等于2^(3^2)也就是 512而不是(2^3)^2等于 64。C 语言里没有直接的幂运算符可以用pow(a,b)。增加常量pi、e在词法分析时直接转换为TOKEN_NUMBER。增加函数sqrt、log、log10、asin、acos、atan。增加历史变量比如用ANS存储上一次结果在表达式中作为数字 token 替换。每一步新功能都要在测试脚本里补一个check比如check 2^3^2 512、check sqrt(16) 4、check log10(1000) 3。这样你每改一次代码跑一遍bash test_calc.sh就能确认旧功能没有被新功能破坏。这个脚本本身也是你向别人展示“我的计算器是可靠”的证据比口头说一百句都有用。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询