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;
}
相关推荐
知识分享小能手2 小时前
微信小程序入门学习教程,从入门到精通,微信小程序常用API(上)——知识点详解 + 案例实战(4)
前端·javascript·学习·微信小程序·小程序·html5·微信开放平台
是大强3 小时前
stm32摇杆adc数据分析
开发语言
MobotStone3 小时前
AI训练的悖论:为什么越追求准确率越会产生幻觉?
算法
口嗨农民工3 小时前
win10默认搜索APP和window设置控制命板
linux·服务器·c语言
蓝莓味的口香糖3 小时前
【JS】什么是单例模式
开发语言·javascript·单例模式
linux kernel4 小时前
第二十三讲:特殊类和类型转换
开发语言·c++
笨蛋少年派4 小时前
JAVA基础语法
java·开发语言
渡我白衣4 小时前
深入剖析:boost::intrusive_ptr 与 std::shared_ptr 的性能边界和实现哲学
开发语言·c++·spring
yuxb734 小时前
Ceph 分布式存储学习笔记(二):池管理、认证和授权管理与集群配置(下)
笔记·ceph·学习