實現一個能正確處理 + - * / ( )、優先級、結合性、浮點數、負數的表達式求值器,是編譯原理與數據結構的經典實戰。本文提供**完整、可編譯、模塊化**的 C 實現:詞法掃描、遞歸下降解析 (或雙棧中綴直算)、錯誤檢測、測試主程序,適合嵌入式系統、腳本引擎、計算器項目直接集成。
| 算法 | 原理 | 優點 | 缺點 |
|---|---|---|---|
| 遞歸下降 | 文法規則直接映射函數 | 結構清晰、易擴展函數/變量、錯誤定位準 | 函數調用開銷、深遞歸棧溢出風險 |
| 雙棧 (Dijkstra) | 操作數棧 + 運算符棧,中綴直算 | 單次掃描、無遞歸、O(n) 時間 O(n) 空間 | 錯誤恢復較難、優先級表硬編碼 |
| 中綴轉後綴 (Shunting-yard) | 轉 RPN 再求值 | 分離解析/計算、適合多次求值、編譯器前端 | 兩遍掃描、需額外輸出隊列 |
**本文採用遞歸下降**,結構最清晰,易於後續擴展變量、函數、冪運算等。
expression = term { ('+' | '-') term } ;
term = factor { ('*' | '/') factor } ;
factor = number | '(' expression ')' | ('+' | '-') factor ; // 一元正負
number = digit { digit } [ '.' { digit } ] ;
#ifndef CALC_H
#define CALC_H
#ifdef __cplusplus
extern "C" {
#endif
// 計算表達式字符串,成功返回 0 並通過 result 輸出值;失敗返回非 0 錯誤碼
int calc_eval(const char* expr, double* result);
// 錯誤碼
enum {
CALC_OK = 0,
CALC_ERR_INVALID_CHAR,
CALC_ERR_MISMATCH_PAREN,
CALC_ERR_DIV_BY_ZERO,
CALC_ERR_SYNTAX,
CALC_ERR_EMPTY
};
#ifdef __cplusplus
}
#endif
#endif
#include "calc.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <float.h>
#include <errno.h>
typedef struct {
const char* s; // 表達式字符串
size_t pos; // 當前掃描位置
size_t len; // 字符串長度
int error; // 錯誤碼
} Parser;
static void skip_spaces(Parser* p) {
while (p->pos < p->len && isspace((unsigned char)p->s[p->pos])) p->pos++;
}
static double parse_number(Parser* p) {
skip_spaces(p);
size_t start = p->pos;
bool has_digit = false;
// 整數部分
while (p->pos < p->len && isdigit((unsigned char)p->s[p->pos])) {
p->pos++; has_digit = true;
}
// 小數部分
if (p->pos < p->len && p->s[p->pos] == '.') {
p->pos++;
while (p->pos < p->len && isdigit((unsigned char)p->s[p->pos])) {
p->pos++; has_digit = true;
}
}
if (!has_digit) { p->error = CALC_ERR_SYNTAX; return 0.0; }
char* end;
double val = strtod(p->s + start, &end);
if (errno == ERANGE) p->error = CALC_ERR_SYNTAX;
return val;
}
static double parse_factor(Parser* p);
static double parse_term(Parser* p);
static double parse_expression(Parser* p);
static double parse_factor(Parser* p) {
skip_spaces(p);
if (p->pos >= p->len) { p->error = CALC_ERR_SYNTAX; return 0.0; }
char c = p->s[p->pos];
if (c == '+' || c == '-') { // 一元正負
p->pos++;
double val = parse_factor(p);
return (c == '-') ? -val : val;
}
if (c == '(') { // 括號分組
p->pos++;
double val = parse_expression(p);
skip_spaces(p);
if (p->pos >= p->len || p->s[p->pos] != ')') {
p->error = CALC_ERR_MISMATCH_PAREN;
return 0.0;
}
p->pos++;
return val;
}
if (isdigit((unsigned char)c) || c == '.') {
return parse_number(p);
}
p->error = CALC_ERR_INVALID_CHAR;
return 0.0;
}
static double parse_term(Parser* p) {
double left = parse_factor(p);
while (!p->error) {
skip_spaces(p);
if (p->pos >= p->len) break;
char op = p->s[p->pos];
if (op != '*' && op != '/') break;
p->pos++;
double right = parse_factor(p);
if (p->error) return 0.0;
if (op == '*') left *= right;
else {
if (fabs(right) < DBL_EPSILON) { p->error = CALC_ERR_DIV_BY_ZERO; return 0.0; }
left /= right;
}
}
return left;
}
static double parse_expression(Parser* p) {
double left = parse_term(p);
while (!p->error) {
skip_spaces(p);
if (p->pos >= p->len) break;
char op = p->s[p->pos];
if (op != '+' && op != '-') break;
p->pos++;
double right = parse_term(p);
if (p->error) return 0.0;
if (op == '+') left += right;
else left -= right;
}
return left;
}
int calc_eval(const char* expr, double* result) {
if (!expr || !result) return CALC_ERR_SYNTAX;
Parser p = { .s = expr, .pos = 0, .len = strlen(expr), .error = CALC_OK };
skip_spaces(&p);
if (p.pos >= p.len) return CALC_ERR_EMPTY;
double val = parse_expression(&p);
skip_spaces(&p);
if (p.pos != p.len) p.error = CALC_ERR_SYNTAX; // 殘留非法字符
if (p.error) return p.error;
*result = val;
return CALC_OK;
}
#include "calc.h"
#include <stdio.h>
void test(const char* expr) {
double result;
int rc = calc_eval(expr, &result);
if (rc == CALC_OK) {
printf("%-30s = %g\n", expr, result);
} else {
printf("%-30s → ERROR %d\n", expr, rc);
}
}
int main() {
// 基本四則運算
test("1+2*3");
test("(1+2)*3");
test("10/3");
test("3.14*2");
// 負數與一元運算符
test("-5+3");
test("2*-3");
test("-(2+3)");
// 複雜嵌套
test("((2+3)*4-5)/3");
test("1+2*3-4/5");
// 錯誤用例
test("1++2");
test("(1+2");
test("1/0");
test("abc");
test("");
return 0;
}
gcc -std=c11 -Wall -Wextra -O2 calc.c main.c -o calc -lm
./calc
1+2*3 = 7
(1+2)*3 = 9
10/3 = 3.33333
3.14*2 = 6.28
-5+3 = -2
2*-3 = -6
-(2+3) = -5
((2+3)*4-5)/3 = 5
1+2*3-4/5 = 6.2
1++2 → ERROR 4
(1+2 → ERROR 2
1/0 → ERROR 3
abc → ERROR 1
→ ERROR 5
parse_factor 中識別標識符,查符號表返回值。factor → ident '(' arglist ')',內置 sin/cos/sqrt 或用戶自定義。factor 和 term 之間插入 power 規則,右結合。statement → ident '=' expression,返回賦值結果。p.pos、預期 Token 類型。