目录
- 一、引言
- 二、计算器需求分析与功能设计
-
- [2.1 功能定义](#2.1 功能定义)
- [2.2 输入处理设计](#2.2 输入处理设计)
- [2.3 运算逻辑设计](#2.3 运算逻辑设计)
- 三、核心算法实现
-
- [3.1 中缀转后缀算法](#3.1 中缀转后缀算法)
- [3.2 后缀表达式计算算法](#3.2 后缀表达式计算算法)
- [3.3 算法函数编写](#3.3 算法函数编写)
- 四、计算器实战与测试
-
- [4.1 编写主函数](#4.1 编写主函数)
- [4.2 测试常见表达式](#4.2 测试常见表达式)
- [4.3 错误处理](#4.3 错误处理)
- 五、总结与展望
一、引言
在 C 语言实战专栏中,我们不断探索 C 语言在各种场景下的应用与实践。今天,我们将深入到一个基础且实用的项目 ------ 实现一个支持多运算与括号的简易计算器。这个计算器不仅是 C 语言基础语法与算法知识的综合应用,也是解决实际计算问题的有力工具。通过完成这个项目,我们能更深入地理解 C 语言的语法结构、运算符优先级处理、数据结构的运用以及程序设计的逻辑思维,无论是对于初学者夯实基础,还是进阶开发者提升编程能力,都具有重要的意义。
二、计算器需求分析与功能设计
2.1 功能定义
我们设计的这个简易计算器,要能够支持基本的四则运算,也就是加法(+)、减法(-)、乘法(*)、除法(/) ,还有取余运算(%)。这些基本运算在日常数学计算里是最常用的,像计算购物找零、分配物品数量、计算面积体积等场景都离不开它们。
同时,为了满足更复杂的数学计算需求,计算器必须支持使用括号来改变运算优先级。比如在计算(3 + 5) * 2时,我们需要先计算括号内的加法,再进行乘法运算。有了括号,就可以灵活地表达各种复杂的数学逻辑,解决更具挑战性的计算问题 ,像在工程计算、科学研究中的复杂公式计算,都需要依靠括号来确保运算顺序的正确性。
2.2 输入处理设计
计算器需要接收用户输入的表达式字符串,就像"3*(4+2)-8/2"这种形式。在接收输入时,我们可以使用scanf函数从标准输入读取用户输入的字符串,或者使用gets函数读取一整行输入(不过gets函数存在缓冲区溢出风险,使用时需要谨慎)。
当获取到输入字符串后,首先要进行初步的合法性检查。比如,检查字符串中是否包含非法字符,除了数字、运算符(+、-、*、/、% 、(、) )之外的字符都是非法的;还要检查括号是否匹配,左括号和右括号的数量必须相等,并且右括号不能出现在没有匹配左括号的前面。如果发现非法字符或者括号不匹配,要及时提示用户输入有误。
预处理阶段,我们可以去除输入字符串中的空格,这样可以简化后续的处理流程。比如将"3 + ( 4 - 2 )"处理成"3+(4-2)",方便后续对表达式的解析和计算。
2.3 运算逻辑设计
在数学表达式中,我们日常习惯使用的是中缀表达式,也就是运算符在两个操作数中间,像3 + 4这种形式。但是中缀表达式在计算时,需要考虑运算符的优先级和括号的影响,处理起来比较复杂。所以,我们要把中缀表达式转换为后缀表达式,也叫逆波兰表达式。
逆波兰表达式最大的特点就是不需要括号来明确运算顺序,运算符紧跟在操作数后面。比如中缀表达式3 + 4 * 2的后缀表达式是3 4 2 * +。这种表示方式的优势在于计算时非常方便,可以直接使用栈来实现。在计算后缀表达式时,遇到操作数就压入栈中,遇到运算符就从栈中弹出相应数量的操作数进行计算,然后把结果再压回栈中,最终栈顶的元素就是表达式的计算结果。通过这种方式,大大简化了表达式的计算过程,提高了计算效率。
三、核心算法实现
3.1 中缀转后缀算法
中缀转后缀算法主要使用栈来处理运算符优先级与括号。具体步骤如下:
-
初始化:创建一个空栈用于存储运算符,一个空字符串用于存储后缀表达式。
-
遍历中缀表达式:从左到右依次读取中缀表达式的每个字符。
- 如果是数字:直接将数字字符拼接到后缀表达式字符串中,当遇到非数字字符时,在数字字符后添加一个空格作为分隔符。例如,对于中缀表达式3+5,读取到数字3时,将3添加到后缀表达式中,遇到+后,在3后添加空格,此时后缀表达式为3。
- 如果是左括号(:将左括号压入运算符栈,因为左括号表示一个子表达式的开始,在它之后的运算符需要等待右括号出现后再处理。
- 如果是右括号):从运算符栈中弹出运算符,直到遇到左括号,将弹出的运算符依次添加到后缀表达式字符串中,然后丢弃左括号。例如,对于中缀表达式(3+5),当读取到右括号时,从栈中弹出+,添加到后缀表达式中,此时后缀表达式为3 5 +。
- 如果是运算符 :比较当前运算符与栈顶运算符的优先级。
- 如果栈为空或栈顶为左括号,或者当前运算符优先级高于栈顶运算符:将当前运算符压入栈中。例如,对于中缀表达式35+2,读取到 时,因为栈为空,所以将压入栈;接着读取到+时,因为的优先级高于+,所以*仍留在栈中,将+压入栈。
- 如果当前运算符优先级低于或等于栈顶运算符:从栈中弹出运算符并添加到后缀表达式字符串中,直到当前运算符优先级高于栈顶运算符或栈为空或栈顶为左括号,然后将当前运算符压入栈。例如,对于中缀表达式3+52,读取到+时,栈顶运算符是 ,的优先级高于+,所以先弹出添加到后缀表达式中,此时后缀表达式为3 5 2 *,然后再将+压入栈。
-
遍历结束:遍历完中缀表达式后,将运算符栈中剩余的运算符依次弹出并添加到后缀表达式字符串中。
以中缀表达式3*(4+2)-8/2为例,转换过程如下:
| 步骤 | 读取字符 | 后缀表达式 | 运算符栈 | 说明 |
|---|---|---|---|---|
| 1 | 3 | 3 | 空 | 数字 3 直接添加到后缀表达式 |
| 2 | * | 3 | * | *压入栈 |
| 3 | ( | 3 | *( | 左括号压入栈 |
| 4 | 4 | 3 4 | *( | 数字 4 直接添加到后缀表达式 |
| 5 | + | 3 4 | *+( | +压入栈,因为栈顶是左括号 |
| 6 | 2 | 3 4 2 | *+( | 数字 2 直接添加到后缀表达式 |
| 7 | ) | 3 4 2 + | * | 弹出+添加到后缀表达式,丢弃左括号 |
| 8 | - | 3 4 2 + * | - | *弹出添加到后缀表达式,-压入栈 |
| 9 | 8 | 3 4 2 + * 8 | - | 数字 8 直接添加到后缀表达式 |
| 10 | / | 3 4 2 + * 8 | -/ | /压入栈 |
| 11 | 2 | 3 4 2 + * 8 2 | -/ | 数字 2 直接添加到后缀表达式 |
| 12 | 结束 | 3 4 2 + * 8 2 / - | 空 | 弹出/和-添加到后缀表达式 |
最终得到的后缀表达式为3 4 2 + * 8 2 / -。
3.2 后缀表达式计算算法
后缀表达式计算使用栈来存储操作数,遇到运算符时进行计算。具体原理和步骤如下:
-
初始化:创建一个空栈用于存储操作数。
-
遍历后缀表达式:从左到右依次读取后缀表达式的每个字符。
- 如果是数字:将数字字符转换为对应的数值,压入操作数栈中。例如,对于后缀表达式3 4 +,读取到3时,将 3 压入栈;读取到4时,将 4 压入栈,此时栈中元素为[3, 4](栈顶在右边)。
- 如果是运算符:从操作数栈中弹出两个操作数,注意先弹出的是右操作数,后弹出的是左操作数。根据运算符进行相应的计算,将计算结果再压回操作数栈中。例如,对于后缀表达式3 4 +,读取到+时,弹出 4 和 3,计算3 + 4 = 7,然后将 7 压入栈,此时栈中元素为[7]。
-
遍历结束:遍历完后缀表达式后,操作数栈中只剩下一个元素,这个元素就是表达式的计算结果。
对于后缀表达式3 4 2 + * 8 2 / -,计算过程如下:
| 步骤 | 读取字符 | 操作数栈 | 说明 |
|---|---|---|---|
| 1 | 3 | 3 | 数字 3 压入栈 |
| 2 | 4 | 3, 4 | 数字 4 压入栈 |
| 3 | 2 | 3, 4, 2 | 数字 2 压入栈 |
| 4 | + | 3, 6 | 弹出 2 和 4,计算4 + 2 = 6,结果 6 压入栈 |
| 5 | * | 18 | 弹出 6 和 3,计算3 * 6 = 18,结果 18 压入栈 |
| 6 | 8 | 18, 8 | 数字 8 压入栈 |
| 7 | 2 | 18, 8, 2 | 数字 2 压入栈 |
| 8 | / | 18, 4 | 弹出 2 和 8,计算8 / 2 = 4,结果 4 压入栈 |
| 9 | - | 14 | 弹出 4 和 18,计算18 - 4 = 14,结果 14 压入栈 |
最终计算结果为 14。
3.3 算法函数编写
- 表达式解析函数:主要功能是识别输入字符串中的数字和运算符,并将其按照中缀转后缀算法进行处理,生成后缀表达式。在 C 语言中,可以使用isdigit函数(包含在<ctype.h>头文件中)来判断字符是否为数字。例如:
c
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#define MAX_SIZE 100
// 定义栈结构
typedef struct {
char data[MAX_SIZE];
int top;
} Stack;
// 初始化栈
void initStack(Stack *s) {
s->top = -1;
}
// 入栈操作
int push(Stack *s, char c) {
if (s->top == MAX_SIZE - 1) {
return 0; // 栈满
}
s->data[++(s->top)] = c;
return 1;
}
// 出栈操作
int pop(Stack *s, char *c) {
if (s->top == -1) {
return 0; // 栈空
}
*c = s->data[(s->top)--];
return 1;
}
// 获取栈顶元素
int peek(Stack *s, char *c) {
if (s->top == -1) {
return 0; // 栈空
}
*c = s->data[s->top];
return 1;
}
// 判断是否为运算符
int isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/' || c == '%';
}
// 判断运算符优先级
int precedence(char op) {
switch (op) {
case '+':
case '-':
return 1;
case '*':
case '/':
case '%':
return 2;
default:
return -1;
}
}
// 中缀转后缀
void infixToPostfix(char *infix, char *postfix) {
Stack s;
initStack(&s);
int i, j = 0;
char ch, temp;
for (i = 0; infix[i] != '\0'; i++) {
ch = infix[i];
if (isdigit(ch)) {
while (isdigit(ch) || ch == '.') { // 支持小数
postfix[j++] = ch;
ch = infix[++i];
}
postfix[j++] = ' ';
i--;
} else if (ch == '(') {
push(&s, ch);
} else if (ch == ')') {
while (pop(&s, &temp) && temp != '(') {
postfix[j++] = temp;
postfix[j++] = ' ';
}
} else if (isOperator(ch)) {
while (peek(&s, &temp) && precedence(temp) >= precedence(ch)) {
pop(&s, &temp);
postfix[j++] = temp;
postfix[j++] = ' ';
}
push(&s, ch);
}
}
while (pop(&s, &temp)) {
postfix[j++] = temp;
postfix[j++] = ' ';
}
postfix[j] = '\0';
}
- 计算函数:该函数接收后缀表达式字符串,按照后缀表达式计算算法,使用栈进行计算,返回最终结果。例如:
c
// 计算后缀表达式
int evaluatePostfix(char *postfix) {
Stack s;
initStack(&s);
int i, op1, op2, result;
char ch;
for (i = 0; postfix[i] != '\0'; i++) {
ch = postfix[i];
if (isdigit(ch)) {
int num = 0;
while (isdigit(ch)) {
num = num * 10 + (ch - '0');
ch = postfix[++i];
}
push(&s, num);
i--;
} else if (isOperator(ch)) {
pop(&s, &op2);
pop(&s, &op1);
switch (ch) {
case '+':
result = op1 + op2;
break;
case '-':
result = op1 - op2;
break;
case '*':
result = op1 * op2;
break;
case '/':
if (op2 != 0) {
result = op1 / op2;
} else {
// 处理除零错误
printf("Error: Division by zero\n");
return -1; // 这里简单返回-1表示错误
}
break;
case '%':
if (op2 != 0) {
result = op1 % op2;
} else {
// 处理取余时除数为零错误
printf("Error: Modulo by zero\n");
return -1;
}
break;
}
push(&s, result);
}
}
pop(&s, &result);
return result;
}
通过这两个函数,我们实现了将中缀表达式转换为后缀表达式,并对后缀表达式进行计算的核心功能。
四、计算器实战与测试
4.1 编写主函数
主函数是整个程序的入口,它的主要职责是接收用户输入的表达式,然后调用前面编写的中缀转后缀函数infixToPostfix将中缀表达式转换为后缀表达式,再调用后缀表达式计算函数evaluatePostfix计算最终结果,并将结果输出给用户。以下是主函数的代码示例:
c
int main() {
char infix[MAX_SIZE];
char postfix[MAX_SIZE * 2];// 后缀表达式长度可能是中缀表达式的两倍
int result;
printf("请输入一个数学表达式(支持加减乘除、取余和括号):\n");
fgets(infix, MAX_SIZE, stdin);
// 去除fgets读取的换行符
infix[strcspn(infix, "\n")] = '\0';
infixToPostfix(infix, postfix);
printf("后缀表达式为:%s\n", postfix);
result = evaluatePostfix(postfix);
if (result != -1) {
printf("计算结果为:%d\n", result);
}
return 0;
}
在这段代码中,首先定义了用于存储中缀表达式和后缀表达式的字符数组infix和postfix,以及用于存储计算结果的变量result。然后通过printf函数提示用户输入数学表达式,使用fgets函数从标准输入读取用户输入的表达式,存储在infix数组中。接着调用infixToPostfix函数将中缀表达式转换为后缀表达式,存储在postfix数组中,并输出后缀表达式。最后调用evaluatePostfix函数计算后缀表达式的结果,如果计算成功(结果不为 -1 ,表示没有发生除零或取余时除数为零的错误),则输出计算结果。
4.2 测试常见表达式
为了验证计算器功能的正确性,我们需要对多个常见表达式进行测试。以下是一些测试用例及结果:
| 测试表达式 | 后缀表达式 | 预期结果 | 实际结果 |
|---|---|---|---|
| 1+2*3 | 1 2 3 * + | 7 | 7 |
| (1+2)*3 | 1 2 + 3 * | 9 | 9 |
| 10%3 | 10 3 % | 1 | 1 |
| 3+5*(2-8)/2 | 3 5 2 8 - * 2 / + | -12 | -12 |
| 2*(3+4)/2 | 2 3 4 + * 2 / | 7 | 7 |
通过这些测试用例可以看出,计算器能够正确地处理各种运算和括号,得到的实际结果与预期结果一致,说明我们实现的计算器功能是正确的。在实际测试过程中,可以使用循环批量测试更多的表达式,并且可以将测试结果记录到文件中,方便后续查看和分析。
4.3 错误处理
在实际使用中,用户输入的表达式可能包含非法字符或者存在语法错误,比如括号不匹配等情况。为了提高程序的健壮性,我们需要对这些错误进行处理。
- 检测非法字符:在输入处理阶段,我们可以在读取用户输入后,遍历输入字符串,使用isalnum函数(包含在<ctype.h>头文件中)判断字符是否为字母或数字,使用自定义的isOperator函数判断是否为合法运算符,如果既不是字母数字也不是合法运算符,则为非法字符。例如:
c
int i;
for (i = 0; infix[i] != '\0'; i++) {
if (!isalnum(infix[i]) &&!isOperator(infix[i]) && infix[i] != '(' && infix[i] != ')') {
printf("错误:输入包含非法字符 '%c'\n", infix[i]);
return 1; // 表示程序异常退出
}
}
- 检测括号不匹配:可以在遍历输入字符串时,使用一个计数器来记录左括号和右括号的数量。遇到左括号时计数器加 1,遇到右括号时计数器减 1。如果在遍历结束后,计数器不为 0,则说明括号不匹配。例如:
c
int count = 0;
for (i = 0; infix[i] != '\0'; i++) {
if (infix[i] == '(') {
count++;
} else if (infix[i] == ')') {
count--;
if (count < 0) {
printf("错误:右括号多余左括号\n");
return 1;
}
}
}
if (count > 0) {
printf("错误:左括号多余右括号\n");
return 1;
}
通过以上错误处理机制,当用户输入的表达式存在非法字符或括号不匹配等语法错误时,程序能够及时检测并提示用户,避免程序因错误输入而崩溃,提高了程序的稳定性和可靠性。
五、总结与展望
在本次 C 语言实战中,我们成功实现了一个支持多运算与括号的简易计算器。通过对计算器的需求分析,明确了支持四则运算、取余运算以及括号改变运算优先级的功能定义。在输入处理上,能够接收用户输入的表达式字符串,并进行初步的合法性检查和预处理。
核心算法的实现是本次项目的关键,通过中缀转后缀算法将复杂的中缀表达式转换为易于计算的后缀表达式,再利用后缀表达式计算算法,借助栈结构高效地完成表达式的计算。在函数编写上,实现了表达式解析函数和计算函数,将算法逻辑转化为可执行的代码。
在实战与测试环节,通过编写主函数实现了完整的计算器交互流程,并且对常见表达式进行了测试,验证了计算器功能的正确性,同时也加入了错误处理机制,提高了程序的健壮性。
展望未来,我们可以对计算器功能进行进一步扩展和优化。比如支持更多的数学函数,如三角函数(sin、cos、tan)、对数函数(log、ln)等,以满足更复杂的数学计算需求;可以优化代码的性能,提高计算效率,例如在处理大数字运算时,采用更高效的数据结构和算法;还可以改进用户界面,使其更加友好和便捷,比如实现图形化界面,让用户操作更加直观。通过不断地完善和优化,这个简易计算器将变得更加实用和强大。