C语言小白实现多功能计算器的艰难历程
文章目录
- 前言
- 功能要求
- 程序主体框架
- 关于解析式分析的思路
- 拆分思路
- 运算符优先级层次结构
- 流程图解析
- 函数Ⅰ 从已存储的变量中寻找要使用的
- 函数Ⅱ 添加与更新变量
- 函数Ⅲ 求值入口函数
- 函数Ⅳ 计算函数
- 函数Ⅴ 对函数类的括号表达式解析
- 函数Ⅵ 函数调用
- 函数Ⅵ 解析数字或变量
- 代码的可能的修改与完善
- Ⅰ设定变量个数,表达式长度等的上限
- Ⅱ 除法函数检查除零错误以及检查输入错误
- Ⅲ 移除空格
- 总结与思考
- 个人感想
- 知识点与思考要点总结
- 附完整代码
前言
本文记录博主实现一个简易计算器功能(ps:并不简易)
作为C语言新手的博主非常希望能加入机器人社团,社团的学长们给出这道题目(bonus部分是选做)
因为前段时间已经把C语言学完了,博主觉得是个锻炼的好机会于是就上手了
没仔细看要求前觉得一个计算器有什么难的,想着泡图书馆1h搞定
没想到要求这么多,对于还是新手的博主太勉强了
功能要求
简易计算器
用C语言,实现一个字符串表达式计算器,支持以下功能:
- 变量赋值(变量一定是 26 个字母之中的,区分大小写)
- 基本数学运算,包含加减乘除和指数运算(+、-、*、/、^)
- 输出表达式的值
输入格式: 表达式可以是多行赋值语句(如 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可能认为在输入时会存在用空格隔开字符,但博主觉得也是多此一举
总结与思考
个人感想
其实博主看完要求后真的有退却之意,这绝对是博主实现的最困难最复杂的程序了
但其实写到后面就越来越顺了,毕竟一个计算器的整体逻辑还是相对简单的
知识点与思考要点总结
- 递归下降解析法
// 按优先级分层解析
parse_expression() // 处理 +, -
parse_term() // 处理 *, /
parse_factor() // 处理 ^
parse_base() // 处理基础元素
思考要点:
· 将复杂表达式分解为层次结构
· 每层处理特定优先级的运算符
· 自然体现数学运算优先级
- 语法分析与语法树构建
// 表达式: 2 * a + sin(30)
// 对应的语法树:
// +
// / \
// * sin(30)
// / \ |
// 2 a 30
思考要点:
· 将线性文本转换为树状结构
· 便于后续的计算执行
· 体现运算符的结合性和优先级
- 字符串处理与词法分析
// 关键函数:
remove_spaces() // 预处理
parse_number_or_variable() // 识别token
parse_function() // 函数调用识别
思考要点:
· 预处理简化后续解析
· 精确识别不同类型的token(数字、变量、运算符)
· 处理多字符元素(变量名、函数名)
- 变量管理系统
typedef struct {char name[MAX_VAR_NAME];double value;
} Variable;
思考要点:
· 使用结构体数组管理变量
· 支持变量的动态添加和查找
· 处理变量作用域(本程序为全局)
- 运算符优先级与结合性
优先级从高到低:
1. 括号 ()
2. 函数调用 sin(), exp()
3. 指数运算 ^ (右结合)
4. 乘除运算 *, / (左结合)
5. 加减运算 +, - (左结合)
思考要点:
· 递归下降自然体现优先级
· 处理右结合运算符的特殊性
· 括号改变默认优先级
- 函数集成
// 支持的函数:
sin(), cos(), tan() // 三角函数
exp() // 指数函数
log() // 自然对数
- 指针的应用
char **expr // 二级指针,在解析过程中移动
strtod() // 字符串转数字,自动移动指针
思考要点:
· 使用指针的指针来跟踪解析位置
· 避免字符串拷贝,提高效率
· 理解指针运算和字符串处理
- 递归与回溯
// 递归调用链:
evaluate_expression()
→ parse_expression()→ parse_term()→ parse_factor()→ parse_base()
- 复杂问题分解
原始问题 → 子问题分解:
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;
}