当前位置: 首页 > news >正文

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: 表达式中可能加入括号(),影响运算优先级

程序主体框架

这部分博主觉得是比较复杂的,我们需要识别和提取表达式中的变量和值,注意读取字符时要去掉换行符
要分别读取变量名和值,博主绞尽脑汁才想到直接用=号来划分
同时也想到了区分赋值语句和计算语句可以看有无等号
将赋值语句和计算语句分别处理,大概可以得到整个程序的主体框架

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+2b
表达式->项+项->因子乘因子+因子乘因子
在读取过程中识别字母符号‘a’‘b’并将其替换为值
一个表达式可以逐步拆分,利用函数的嵌套调用,表达式->项->因子逐步计算
sin、log、exp包括类似(a+b)的形式可归为一类,即带括号的项,优先计算,看作一个整体

运算符优先级层次结构

运算分类
在这里插入图片描述

流程图解析

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

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

在这里插入图片描述


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

// 在变量数组中查找指定名称的变量
// 参数: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;
}

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

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

代码的可能的修改与完善

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

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

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

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

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

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

Ⅲ 移除空格

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

总结与思考

个人感想

其实博主看完要求后真的有退却之意,这绝对是博主实现的最困难最复杂的程序了
但其实写到后面就越来越顺了,毕竟一个计算器的整体逻辑还是相对简单的

知识点与思考要点总结

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

思考要点:

· 将复杂表达式分解为层次结构
· 每层处理特定优先级的运算符
· 自然体现数学运算优先级

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

思考要点:

· 将线性文本转换为树状结构
· 便于后续的计算执行
· 体现运算符的结合性和优先级

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

思考要点:

· 预处理简化后续解析
· 精确识别不同类型的token(数字、变量、运算符)
· 处理多字符元素(变量名、函数名)

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

思考要点:

· 使用结构体数组管理变量
· 支持变量的动态添加和查找
· 处理变量作用域(本程序为全局)

  1. 运算符优先级与结合性
优先级从高到低:
1. 括号 ()
2. 函数调用 sin(), exp()
3. 指数运算 ^ (右结合)
4. 乘除运算 *, / (左结合)
5. 加减运算 +, - (左结合)

思考要点:

· 递归下降自然体现优先级
· 处理右结合运算符的特殊性
· 括号改变默认优先级

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

思考要点:

· 使用指针的指针来跟踪解析位置
· 避免字符串拷贝,提高效率
· 理解指针运算和字符串处理

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

附完整代码

#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_factordouble 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;
}
http://www.dtcms.com/a/435496.html

相关文章:

  • 【C++实战(62)】从0到1:C++打造TCP网络通信实战指南
  • 企业网站建设杭州公司宠物寄养网站毕业设计
  • 同ip怎么做不同的网站网站设计需要什么技术
  • 邢台做wap网站的公司做旅行路线的网站
  • 港口备案怎么在网站做培训心得简短
  • 菏泽市住房和建设局网站专业建站公司电话咨询
  • 网站关键词几个字网站建设策划书范文提纲
  • Java 大视界 -- Java 大数据在智能安防周界防范系统中的行为分析与预警精度提升(419)
  • 北京金港建设股份有限公司网站wordpress怎么迁移到空间
  • 查工作单位的网站长沙网站建设公司有哪些
  • 淄博网站制作建设wordpress添加微信公众号
  • 【C++实战(63)】C++ 网络编程实战:UDP客户端与服务端的奥秘之旅
  • [数据分享第八弹]历史人文相关地理数据
  • 河南省建设厅网站中州杯团购小程序
  • 同城可以做别人一样的门户网站吗网址导航总是自动在桌面
  • C语言——循环的嵌套小练习
  • 阿里云买了域名怎么建网站wordpress电源模板
  • 深入解析 Roo Code:提示词的技术结构与工作原理
  • 旅游网站系统建设方案wordpress页面缓慢
  • 杭州软件网站建设广东深圳职业技术学校
  • 树的遍历方式总结
  • excel做网站链接网站流量一直下降
  • VS2017 安装 .NET Core 2.2 SDK 教程(包括 dotnet-sdk-2.2.108-win-x64.exe 安装步骤)​
  • 模拟算法
  • 网站建设需要收集资料吗网络营销中的四种方法
  • 能源经济选题推荐:绿色电网
  • 展览展示搭建设计晋中网站seo
  • 如何让做树洞网站郑州搜索引擎优化公司
  • 网站制作推广电话阿里云安装wordpress
  • 做网站设计电脑买什么高端本好潜江资讯网手机