C语言小白实现多功能计算器的艰难历程

文章目录


前言

本文记录博主实现一个简易计算器功能(ps:并不简易)

作为C语言新手的博主非常希望能加入机器人社团,社团的学长们给出这道题目(bonus部分是选做)

因为前段时间已经把C语言学完了,博主觉得是个锻炼的好机会于是就上手了

没仔细看要求前觉得一个计算器有什么难的,想着泡图书馆1h搞定

没想到要求这么多,对于还是新手的博主太勉强了


功能要求

简易计算器

用C语言,实现一个字符串表达式计算器,支持以下功能:

  1. 变量赋值(变量一定是 26 个字母之中的,区分大小写)
  2. 基本数学运算,包含加减乘除和指数运算(+、-、*、/、^)
  3. 输出表达式的值
    输入格式: 表达式可以是多行赋值语句(如 a=5+3)或计算表达式(如 2*a+1),以 @ 结束输入。
    输出格式: 对于每个计算表达式输出一行计算结果(保留两位小数),对于赋值语句不输出结果。
    样例:
    #输入:
    a=5+3
    b=2*a
    a+10
    b/4
    @
    #输出:
    18.00
    4.00
    bonus1: 数学运算加入 log、exp(例如 exp(x) 表示 e 的 x 次方)、sin、cos、tan 的计算
    bonus2: 变量可能不止一个字母(但也一定由字母组成,如 zlHHH,mtLLD 等)
    bonus3: 表达式中可能加入括号(),影响运算优先级

程序主体框架

这部分博主觉得是比较复杂的,我们需要识别和提取表达式中的变量和值,注意读取字符时要去掉换行符

要分别读取变量名和值,博主绞尽脑汁才想到直接用=号来划分

同时也想到了区分赋值语句和计算语句可以看有无等号

将赋值语句和计算语句分别处理,大概可以得到整个程序的主体框架

c 复制代码
int main() {
    char input[MAX_EXPR_LEN]; // 输入缓冲区

    // 主循环,持续读取输入直到遇到"@"
    while (1) {
        fgets(input, sizeof(input), stdin); // 读取一行输入
        input[strcspn(input, "\n")] = 0; // 移除换行符
        if (strcmp(input, "@") == 0) {
            break;
        }// 检查是否结束输入
        remove_spaces(input);// 移除输入中的所有空格
        char* equal_sign = strchr(input, '=');// 检查是否是赋值语句(包含等号)
        if (equal_sign != NULL) {

            char var_name[MAX_VAR_NAME] = { 0 };
            int i = 0;
            while (input + i < equal_sign && i < MAX_VAR_NAME - 1) {
                var_name[i] = input[i];
                i++;
            }
            var_name[i] = '\0';// 提取变量名(等号前的部分)
            char* expr = equal_sign + 1;
            double value = evaluate_expression(expr);// 计算等号右侧表达式的值
            add_variable(var_name, value);// 存储变量名和值
        }
        else {
            // 直接计算表达式并输出结果
            double result = evaluate_expression(input);
            printf("%.2f\n", result); // 保留两位小数
        }
    }

    return 0;
}

没想到前段时间才学的标准输入流等知识也是用上了

当时眼界狭窄,认为不如scanf和getchar,没有认真学,回旋镖也是打到博主了,苦苦翻cplusplus找用法

fgets可以直接读取整行内容,对于这种不知道有多少字符的,比scanf强多了


关于解析式分析的思路

拆分思路

对于表达式 5+3

表达式->项+项

对于表达式 5a+2 b

表达式->项+项->因子乘因子+因子乘因子

在读取过程中识别字母符号'a''b'并将其替换为值

一个表达式可以逐步拆分,利用函数的嵌套调用,表达式->项->因子逐步计算

sin、log、exp包括类似(a+b)的形式可归为一类,即带括号的项,优先计算,看作一个整体

运算符优先级层次结构

运算分类

流程图解析

ps:用deepseek生成的流程图,方便理解

复杂表达式如a+b*c^2


函数Ⅰ 从已存储的变量中寻找要使用的

c 复制代码
// 在变量数组中查找指定名称的变量
// 参数:name - 要查找的变量名
// 返回值:找到的变量指针,如果未找到返回NULL
Variable* find_variable(char* name) {
    for (int i = 0; i < var_count; i++) {
        if (strcmp(variables[i].name, name) == 0) {
            return &variables[i];
        }
    }
    return NULL;
}

这部分比较简单,注意我们在前面就已经记录了变量的个数,直接遍历数组找到目标变量,便于计算时调用

函数Ⅱ 添加与更新变量

本来只是写一个存入变量的函数,但考虑到在输入时可能会多次改变同一个变量的值,竹帛设计了这个函数来更新变量

c 复制代码
// 参数:name - 变量名,value - 变量值
void add_variable(char* name, double value) {
    Variable* var = find_variable(name);
    if (var != NULL) {
        // 变量已存在,更新其值
        var->value = value;
    }
    else {
        // 变量不存在,添加新变量
        if (var_count < MAX_VARS) {
            strcpy(variables[var_count].name, name);
            variables[var_count].value = value;
            var_count++;
        }
    }
}

函数Ⅲ 求值入口函数

c 复制代码
// 表达式求值入口函数
// 参数:expr - 要计算的表达式字符串
// 返回值:表达式的计算结果
double evaluate_expression(char* expr) {
    char* ptr = expr; // 创建指针副本,用于解析过程中移动
    return parse_expression(&ptr);
}

博主不想一个函数写的那么复杂,所以写了一个嵌套调用,看起来舒服一点

这里注意创建副本,因为初始指针的位置我们还需要留着,不然调用完函数飘到哪里去了都不知道

函数Ⅳ 计算函数

博主一开始也没想那么多,就用简单的计算器实现计算过程,但实际上这个计算器的输入程序很复杂,博主也不知道该怎么办,只能再用这种遍历的方式提取运算符然后计算

这里把加减归一类是因为要注意运算优先级的问题

c 复制代码
// 读取运算符,处理加减运算(最低优先级)
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:表达式的值
double parse_expression(char** expr) {
    // 先解析第一项
    double result = parse_term(expr);
    // 循环处理连续的加减运算
    while (**expr == '+' || **expr == '-') {
        char op = **expr; // 获取运算符
        (*expr)++; // 移动指针跳过运算符
        double term = parse_term(expr); // 解析下一个项
        // 根据运算符进行计算
        if (op == '+') {
            result = result + term;
        }
        else {
            result = result - term;
        }
    }

    return result;
}

下面是乘除运算,逻辑略有区别

c 复制代码
// 读取运算符,处理乘除运算(中等优先级)
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:项的值
double parse_term(char** expr) {
    // 先解析第一项
    double result = parse_factor(expr);
    // 循环处理连续的乘除运算
    while (**expr == '*' || **expr == '/') {
        char op = **expr; // 获取运算符
        (*expr)++; // 移动指针跳过运算符
        double factor = parse_factor(expr); // 解析下一个因子
        // 根据运算符进行计算
        if (op == '*') {
            result *= factor;
        }
        else {
            // 检查除零错误
            if (factor == 0) {
                printf("错误:除以零\n");
                exit(1);
            }
            result /= factor;
        }
    }

    return result;
}

下面是运算优先级更高的指数计算,逻辑与乘除函数类似,注意指数函数的右结合性,这边博主也是debug时发现的,否则在计算如2^3 ^2的式子时就会算成64

c 复制代码
// 解析因子,处理指数运算(较高优先级)
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:因子的值
double parse_factor(char** expr) {
    // 先解析基础元素
    double result = parse_base(expr);
  // 检查是否有指数运算(^)
    if (**expr == '^') {
        (*expr)++; // 移动指针跳过'^'
        // 指数运算是右结合的,所以递归调用parse_factor
        double exponent = parse_factor(expr);
        result = pow(result, exponent); // 计算指数
    }

    return result;
}

函数Ⅴ 对函数类的括号表达式解析

c 复制代码
// 解析基础元素:数字、变量、函数调用或括号表达式(最高优先级)
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:基础元素的值
double parse_base(char** expr) {
    // 检查是否是函数调用或变量(以字母开头)
    if (isalpha((unsigned char)**expr)) {
        char func_name[10] = { 0 }; // 函数名缓冲区
        int i = 0;

        // 提取连续的字母作为函数名或变量名
        while (isalpha((unsigned char)**expr) && i < 9) {
            func_name[i++] = **expr;
            (*expr)++;
        }

        // 检查是否有括号(如果是函数调用)
        if (**expr == '(') {
            return parse_function(expr, func_name);
        }
        else {
            // 没有括号,说明是变量,回退指针并解析为变量
            (*expr) -= i;
            return parse_number_or_variable(expr);
        }
    }

    // 检查是否是括号表达式
    if (**expr == '(') {
        (*expr)++; // 跳过 '('
        double result = parse_expression(expr); // 解析括号内的表达式
        (*expr)++; // 跳过 ')'
        return result;
    }

    // 既不是函数/变量也不是括号,解析为数字或变量
    return parse_number_or_variable(expr);
}

函数Ⅵ 函数调用

c 复制代码
// 解析函数调用
// 参数:expr - 指向表达式字符串指针的指针,func_name - 函数名
// 返回值:函数调用的结果
double parse_function(char** expr, char* func_name) {
    // 解析函数参数(括号内的表达式)
    double arg = parse_expression(expr);
    (*expr)++; // 跳过 ')'

    // 根据函数名调用相应的数学函数
    if (strcmp(func_name, "sin") == 0) {
        return sin(arg); // 正弦函数
    }
    else if (strcmp(func_name, "cos") == 0) {
        return cos(arg); // 余弦函数
    }
    else if (strcmp(func_name, "tan") == 0) {
        return tan(arg); // 正切函数
    }
    else if (strcmp(func_name, "exp") == 0) {
        return exp(arg); // 指数函数
    }
    else if (strcmp(func_name, "log") == 0) {
        return log(arg); // 自然对数
    }
    else {
        printf("错误:未知函数 '%s'\n", func_name);
        exit(1);
    }
}

函数Ⅵ 解析数字或变量

c 复制代码
// 解析数字或变量
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:数字值或变量值
double parse_number_or_variable(char** expr) {
    // 检查是否是变量(以字母开头)
    if (isalpha((unsigned char)**expr)) {
        char var_name[MAX_VAR_NAME] = { 0 }; // 存储变量名
        int i = 0;

        // 提取连续的字母作为变量名
        while (isalpha((unsigned char)**expr) && i < MAX_VAR_NAME - 1) {
            var_name[i++] = **expr;
            (*expr)++;
        }
        var_name[i] = '\0';//以\0分隔变量

        // 查找变量
        Variable* var = find_variable(var_name);
        if (var == NULL) {
            printf("错误:未定义变量 '%s'\n", var_name);
            exit(1);
        }
        return var->value; // 返回变量值
    }

    // 解析数字(使用strtod函数)
    char* end;
    double result = strtod(*expr, &end);
    // 检查是否成功解析数字
    if (*expr == end) {
        printf("错误:无效表达式\n");
        exit(1);
    }
    *expr = end; // 移动指针到数字后的位置
    return result;
}

代码的可能的修改与完善

博主写完代码后交给了deepseek让他帮我检查,小鲸鱼建议我觉得...但确实是有所改善,供大家参考

Ⅰ设定变量个数,表达式长度等的上限

虽然竹帛觉得问题不大,但确实有效,于是有了如下宏的定义

c 复制代码
#define MAX_VAR_NAME 50      // 变量名最大长度
#define MAX_EXPR_LEN 1000    // 表达式最大长度
#define MAX_VARS 100         // 最大变量数

Ⅱ 除法函数检查除零错误以及检查输入错误

博主默认输入时不会输入错误的表达式,但实际上检查输入错误是很有必要的,不然出现输入"sin(a"这种没有右括号的表达式可能会导致程序崩掉

Ⅲ 移除空格

deepseek可能认为在输入时会存在用空格隔开字符,但博主觉得也是多此一举

总结与思考

个人感想

其实博主看完要求后真的有退却之意,这绝对是博主实现的最困难最复杂的程序了

但其实写到后面就越来越顺了,毕竟一个计算器的整体逻辑还是相对简单的

知识点与思考要点总结

  1. 递归下降解析法
c 复制代码
// 按优先级分层解析
parse_expression()  // 处理 +, -
parse_term()        // 处理 *, /
parse_factor()      // 处理 ^
parse_base()        // 处理基础元素

思考要点:

· 将复杂表达式分解为层次结构

· 每层处理特定优先级的运算符

· 自然体现数学运算优先级

  1. 语法分析与语法树构建
c 复制代码
// 表达式: 2 * a + sin(30)
// 对应的语法树:
//        +
//       / \
//      *   sin(30)
//     / \      |
//    2   a    30

思考要点:

· 将线性文本转换为树状结构

· 便于后续的计算执行

· 体现运算符的结合性和优先级

  1. 字符串处理与词法分析
c 复制代码
// 关键函数:
remove_spaces()              // 预处理
parse_number_or_variable()   // 识别token
parse_function()            // 函数调用识别

思考要点:

· 预处理简化后续解析

· 精确识别不同类型的token(数字、变量、运算符)

· 处理多字符元素(变量名、函数名)

  1. 变量管理系统
c 复制代码
typedef struct {
    char name[MAX_VAR_NAME];
    double value;
} Variable;

思考要点:

· 使用结构体数组管理变量

· 支持变量的动态添加和查找

· 处理变量作用域(本程序为全局)

  1. 运算符优先级与结合性

    优先级从高到低:

    1. 括号 ()
    2. 函数调用 sin(), exp()
    3. 指数运算 ^ (右结合)
    4. 乘除运算 *, / (左结合)
    5. 加减运算 +, - (左结合)

思考要点:

· 递归下降自然体现优先级

· 处理右结合运算符的特殊性

· 括号改变默认优先级

  1. 函数集成
c 复制代码
// 支持的函数:
sin(), cos(), tan()  // 三角函数
exp()               // 指数函数
log()               // 自然对数
  1. 指针的应用
c 复制代码
char **expr  // 二级指针,在解析过程中移动
strtod()     // 字符串转数字,自动移动指针

思考要点:

· 使用指针的指针来跟踪解析位置

· 避免字符串拷贝,提高效率

· 理解指针运算和字符串处理

  1. 递归与回溯
c 复制代码
// 递归调用链:
evaluate_expression()
→ parse_expression()
  → parse_term()
    → parse_factor()
      → parse_base()
  1. 复杂问题分解
c 复制代码
原始问题 → 子问题分解:
1. 输入读取和预处理
2. 赋值语句识别
3. 表达式解析
4. 变量管理
5. 数学计算
6. 结果输出

附完整代码

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <ctype.h>

// 定义常量
#define MAX_VAR_NAME 50      // 变量名最大长度
#define MAX_EXPR_LEN 1000    // 表达式最大长度
#define MAX_VARS 100         // 最大变量数量

// 变量结构体,用于存储变量名和对应的值
typedef struct {
    char name[MAX_VAR_NAME]; // 变量名
    double value;            // 变量值
} Variable;

// 全局变量
Variable variables[MAX_VARS]; // 变量数组
int var_count = 0;            // 当前变量数量

// 函数声明
double evaluate_expression(char* expr);                       // 计算表达式的值
double parse_expression(char** expr);                         // 解析表达式(处理加减)
double parse_term(char** expr);                               // 解析项(处理乘除)
double parse_factor(char** expr);                             // 解析因子(处理指数和函数)
double parse_base(char** expr);                               // 解析基础元素(数字、变量、函数、括号)
double parse_function(char** expr, char* func_name);          // 解析函数调用
double parse_number_or_variable(char** expr);                 // 解析数字或变量
Variable* find_variable(char* name);                          // 查找变量
void add_variable(char* name, double value);                  // 添加或更新变量

// 主函数
int main() {
    char input[MAX_EXPR_LEN]; // 输入缓冲区

    // 主循环,持续读取输入直到遇到"@"
    while (1) {
        fgets(input, sizeof(input), stdin); // 读取一行输入
        input[strcspn(input, "\n")] = 0; // 移除换行符
        if (strcmp(input, "@") == 0) {
            break;
        }// 检查是否结束输入
        char* equal_sign = strchr(input, '=');// 检查是否是赋值语句(包含等号)
        if (equal_sign != NULL) {

            char var_name[MAX_VAR_NAME] = { 0 };
            int i = 0;
            while (input + i < equal_sign && i < MAX_VAR_NAME - 1) {
                var_name[i] = input[i];
                i++;
            }
            var_name[i] = '\0';// 提取变量名(等号前的部分)
            char* expr = equal_sign + 1;
            double value = evaluate_expression(expr);// 计算等号右侧表达式的值
            add_variable(var_name, value);// 存储变量名和值
        }
        else {
            // 直接计算表达式并输出结果
            double result = evaluate_expression(input);
            printf("%.2f\n", result); // 保留两位小数
        }
    }

    return 0;
}

// 在变量数组中查找指定名称的变量
// 参数:name - 要查找的变量名
// 返回值:找到的变量指针,如果未找到返回NULL
Variable* find_variable(char* name) {
    for (int i = 0; i < var_count; i++) {
        if (strcmp(variables[i].name, name) == 0) {
            return &variables[i];
        }
    }
    return NULL;
}

// 添加或更新变量
// 参数:name - 变量名,value - 变量值
void add_variable(char* name, double value) {
    Variable* var = find_variable(name);
    if (var != NULL) {
        // 变量已存在,更新其值
        var->value = value;
    }
    else {
        // 变量不存在,添加新变量
        if (var_count < MAX_VARS) {
            strcpy(variables[var_count].name, name);
            variables[var_count].value = value;
            var_count++;
        }
    }
}

// 表达式求值入口函数
// 参数:expr - 要计算的表达式字符串
// 返回值:表达式的计算结果
double evaluate_expression(char* expr) {
    char* ptr = expr; // 创建指针副本,用于解析过程中移动
    return parse_expression(&ptr);
}

// 解析表达式,处理加减运算(最低优先级)
// 参数:expr - 指向表达式字符串指针的指针(用于在解析过程中移动指针)
// 返回值:表达式的值
double parse_expression(char** expr) {
    // 先解析第一个项
    double result = parse_term(expr);

    // 循环处理连续的加减运算
    while (**expr == '+' || **expr == '-') {
        char op = **expr; // 获取运算符
        (*expr)++; // 移动指针跳过运算符

        double term = parse_term(expr); // 解析下一个项

        // 根据运算符进行计算
        if (op == '+') {
            result = result + term;
        }
        else {
            result = result - term;
        }
    }

    return result;
}

// 解析项,处理乘除运算(中等优先级)
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:项的值
double parse_term(char** expr) {
    // 先解析第一个因子
    double result = parse_factor(expr);

    // 循环处理连续的乘除运算
    while (**expr == '*' || **expr == '/') {
        char op = **expr; // 获取运算符
        (*expr)++; // 移动指针跳过运算符

        double factor = parse_factor(expr); // 解析下一个因子

        // 根据运算符进行计算
        if (op == '*') {
            result *= factor;
        }
        else {
            // 检查除零错误
            if (factor == 0) {
                printf("错误:除以零\n");
                exit(1);
            }
            result /= factor;
        }
    }

    return result;
}

// 解析因子,处理指数运算(较高优先级)
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:因子的值
double parse_factor(char** expr) {
    // 先解析基础元素
    double result = parse_base(expr);

    // 检查是否有指数运算(^)
    if (**expr == '^') {
        (*expr)++; // 移动指针跳过'^'
        // 指数运算是右结合的,所以递归调用parse_factor
        double exponent = parse_factor(expr);
        result = pow(result, exponent); // 计算指数
    }

    return result;
}

// 解析基础元素:数字、变量、函数调用或括号表达式(最高优先级)
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:基础元素的值
double parse_base(char** expr) {
    // 检查是否是函数调用或变量(以字母开头)
    if (isalpha((unsigned char)**expr)) {
        char func_name[10] = { 0 }; // 函数名缓冲区
        int i = 0;

        // 提取连续的字母作为函数名或变量名
        while (isalpha((unsigned char)**expr) && i < 9) {
            func_name[i++] = **expr;
            (*expr)++;
        }

        // 检查是否有括号(如果是函数调用)
        if (**expr == '(') {
            return parse_function(expr, func_name);
        }
        else {
            // 没有括号,说明是变量,回退指针并解析为变量
            (*expr) -= i;
            return parse_number_or_variable(expr);
        }
    }

    // 检查是否是括号表达式
    if (**expr == '(') {
        (*expr)++; // 跳过 '('
        double result = parse_expression(expr); // 解析括号内的表达式
        (*expr)++; // 跳过 ')'
        return result;
    }

    // 既不是函数/变量也不是括号,解析为数字或变量
    return parse_number_or_variable(expr);
}

// 解析函数调用
// 参数:expr - 指向表达式字符串指针的指针,func_name - 函数名
// 返回值:函数调用的结果
double parse_function(char** expr, char* func_name) {
    // 解析函数参数(括号内的表达式)
    double arg = parse_expression(expr);
    (*expr)++; // 跳过 ')'

    // 根据函数名调用相应的数学函数
    if (strcmp(func_name, "sin") == 0) {
        return sin(arg); // 正弦函数
    }
    else if (strcmp(func_name, "cos") == 0) {
        return cos(arg); // 余弦函数
    }
    else if (strcmp(func_name, "tan") == 0) {
        return tan(arg); // 正切函数
    }
    else if (strcmp(func_name, "exp") == 0) {
        return exp(arg); // 指数函数
    }
    else if (strcmp(func_name, "log") == 0) {
        return log(arg); // 自然对数
    }
    else {
        printf("错误:未知函数 '%s'\n", func_name);
        exit(1);
    }
}

// 解析数字或变量
// 参数:expr - 指向表达式字符串指针的指针
// 返回值:数字值或变量值
double parse_number_or_variable(char** expr) {
    // 检查是否是变量(以字母开头)
    if (isalpha((unsigned char)**expr)) {
        char var_name[MAX_VAR_NAME] = { 0 }; // 存储变量名
        int i = 0;

        // 提取连续的字母作为变量名
        while (isalpha((unsigned char)**expr) && i < MAX_VAR_NAME - 1) {
            var_name[i++] = **expr;
            (*expr)++;
        }
        var_name[i] = '\0';//以\0分隔变量

        // 查找变量
        Variable* var = find_variable(var_name);
        if (var == NULL) {
            printf("错误:未定义变量 '%s'\n", var_name);
            exit(1);
        }
        return var->value; // 返回变量值
    }

    // 解析数字(使用strtod函数)
    char* end;
    double result = strtod(*expr, &end);
    // 检查是否成功解析数字
    if (*expr == end) {
        printf("错误:无效表达式\n");
        exit(1);
    }
    *expr = end; // 移动指针到数字后的位置
    return result;
}
相关推荐
小桥流水---人工智能10 小时前
风电机组故障诊断与状态监测方法的研究局限性整理(背景笔记)
笔记
czlczl2002092510 小时前
SpringBoot自动配置AutoConfiguration原理与实践
开发语言·spring boot·后端
仙俊红10 小时前
LeetCode322零钱兑换
算法
颖风船10 小时前
锂电池SOC估计的一种算法(改进无迹卡尔曼滤波)
python·算法·信号处理
551只玄猫11 小时前
KNN算法基础 机器学习基础1 python人工智能
人工智能·python·算法·机器学习·机器学习算法·knn·knn算法
无名小猴11 小时前
TryHackMe——迎2025入门教程(一)
学习
charliejohn11 小时前
计算机考研 408 数据结构 哈夫曼
数据结构·考研·算法
NetDefend11 小时前
minimind-学习记录-环境的配置与跑通
学习
张较瘦_11 小时前
JavaScript | 数组方法实战教程:push()、forEach()、filter()、sort()
开发语言·javascript·ecmascript