首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C 語言四則運算表達式求值器:遞歸下降解析、中綴轉後綴、雙棧算法完整實現

     編輯:關於C語言

實現一個能正確處理 + - * / ( )、優先級、結合性、浮點數、負數的表達式求值器,是編譯原理與數據結構的經典實戰。本文提供**完整、可編譯、模塊化**的 C 實現:詞法掃描、遞歸下降解析 (或雙棧中綴直算)、錯誤檢測、測試主程序,適合嵌入式系統、腳本引擎、計算器項目直接集成。

設計方案選擇

算法原理優點缺點
遞歸下降文法規則直接映射函數結構清晰、易擴展函數/變量、錯誤定位準函數調用開銷、深遞歸棧溢出風險
雙棧 (Dijkstra)操作數棧 + 運算符棧,中綴直算單次掃描、無遞歸、O(n) 時間 O(n) 空間錯誤恢復較難、優先級表硬編碼
中綴轉後綴 (Shunting-yard)轉 RPN 再求值分離解析/計算、適合多次求值、編譯器前端兩遍掃描、需額外輸出隊列

**本文採用遞歸下降**,結構最清晰,易於後續擴展變量、函數、冪運算等。

文法定義 (EBNF)

expression  = term { ('+' | '-') term } ;
term        = factor { ('*' | '/') factor } ;
factor      = number | '(' expression ')' | ('+' | '-') factor ;  // 一元正負
number      = digit { digit } [ '.' { digit } ] ;

完整模塊化實現 (3 文件)

1. calc.h — 公共接口

#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

2. calc.c — 核心實現

#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;
}

3. main.c — 測試驗證

#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,返回賦值結果。
  • 誤差恢復:遇錯誤跳過至下一個分號/換行,繼續解析後續語句。

生產級建議

  • 輸入長度限制、遞歸深度限制 (防棧溢出)。
  • 線程安全:Parser 結構體無靜態變量,天然可重入。
  • 錯誤信息豐富化:記錄錯誤位置 p.pos、預期 Token 類型。
  • 單元測試覆蓋:邊界值、溢出、精度、所有錯誤碼路徑。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved