C语言实现多功能计算器

学完了C语言和数据结构,了解了栈用于计算器功能的实现原理,我最近完成了一个有关计算器的课程设计,写到博客里分享一下。

这个课程设计完成的计算器拥有以下功能:

  1. 可以完成加减乘除运算和括号运算。
  2. 小数不可以参与计算,但是运算结果能输出小数
  3. 拥有赋值功能,比如说输入x=3,那么下次输入x+1时就应该返回4。
  4. 可以检查出计算语句中变量是否被赋值,若没有则返回not found,比如输入x+z,x被赋值为3,而z没有被赋值,那么输出z is not found。
  5. 可以检查出非法运算的错误,如输入3+!,则返回scanner error, !is a problem。
  6. 可以检查出语法错误,如输入3+,则返回syntax error...。
  7. 输入quit则结束程序。

主要实现过程:

C语言实现多功能计算器

首先我们需要创建两个二维数组,分别用于存放每一次输入的字符串和这些字符串中属于辅助语句的字符串。然后在一个存在于主函数的无限循环中,我存放每次输入的字符串到二维数组中,并对其进行判断,如果输入的字符是quit,则直接return 0。

如果不是quit,就需要判断此字符串是否属于赋值语句,因为所有的赋值语句都带有等号,如x=3,而所有的计算语句中都不带有等号,所以我以此为判断依据。当然,无论判断的结果是否为赋值语句,都需要检查其是否合法:

  1. 赋值语句是否合法的判断方式:检查等号左边是否为字母序列(不一定只有一个字母),右边是否为数字序列。需要特别注意的是,判断是否为数字序列时一定要单独判断第一个是否为减号,因为此计算器支持负数运算。
  2. 计算语句是否合法的判断方式:确定没有除了加、减、乘、除、左括号、右括号以外的符号;确定所有的字母序列都可在赋值二维数组中找到对应的数字;确定加、乘、除号左边必须是数字或右括号,右边必须是数字或左括号;确定减号左边必须是数字或右括号,右边必须是数字或左括号。

判断赋值语句是否合法以后,如果合法则加入到二维数组中以等待使用,如果不合法则输出相应报错信息;判断计算语句是否合法以后,如果合法则将其转化成后缀表达式存储起来,后缀表达式的作用是方便用栈进行运算,若不合法则输出相应报错信息。

为了方便最后的计算,我用了结构体的方法代替字符的处理,结构体内有三个变量,isNum代表此结构体是否为数字,value代表了结构体为数字的情况下值是多少,oper代表了结构体不是数字的情况下代表的操作符是什么,结构体如下所示:

typedef struct elem { //用于转换后缀表达式的结构体
	int value = 0; //初始化
	char oper = '=';
	int isNum = 0; //判断是不是数字
}elem;

有了这样一个结构体后,就可以进行后缀表达式的转换了,转换的过程需要一个栈,规则如下:

  1. 如果遇到左括号,直接入栈。
  2. 如果遇到右括号,则依次弹出并输出栈顶元素到后缀表达式,直至遇到左括号(左括号只弹出不输出)。
  3. 如果遇到加号或减号则依次弹出并输出栈顶操作符,直至弹出所得为左括号或栈为空,若弹出得到左括号则需要将其重新入栈,最后将加号或减号入栈。
  4. 如果遇到乘号或除号,直接入栈。
  5. 如果读到了末尾,则依次将栈中元素弹出并输出到后缀表达式。
  6. 如果遇到操作数,直接输出到后缀表达式。

需要注意的是,在转换成后缀表达式的过程中,需要检查计算语句中的字母序列是否在赋值二维数组中存在,如果存在则需要用对应的值代替字母序列进入后缀表达式,若不存在则需要输出相应的报错信息。

有了后缀表达式后,进行运算即可,后缀表达式的运算方法是:建立一个栈S 。从左到右读表达式,如果读到操作数就将它压入栈S中,如果读到n元运算符(即需要参数个数为n的运算符)则取出由栈顶向下的n项按操作数运算,再将运算的结果代替原栈顶的n项,压入栈S中 。如果后缀表达式未读完,则重复上面过程,最后输出栈顶的数值则为结束(摘自百度百科)。

 

实现代码:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

#define stateNumber 50 //可存储的最大表达式或赋值或指令(统称操作)数量
#define stateLength 50 //每个操作的最大长度

int assPos = 0; //指示赋值操作的最大位置,即长度
char variable[stateNumber][stateLength]; //存储变量名称
int value[stateNumber]; //存储变量值

typedef struct elem { //用于转换后缀表达式的结构体
	int value = 0; //初始化
	char oper = '=';
	int isNum = 0; //判断是不是数字
}elem;

elem stackNum[stateLength];  //操作数栈
elem stackOper[stateLength]; //操作符栈
int topNum = 0; //操作数栈指针
int topOper = 0; //操作符栈指针
//注意,指针永远指向栈中最上面的元素上面的空位置

int isQuit(char statement[]) //判断输入是否为quit
{
	if (strcmp(statement, "quit") == 0) return 1;
	else return 0;
}

int isAss(char statement[]) //判断是否有等号
{
	int i = 0;
	while (statement[i] != '\0') {
		if (statement[i] == '=') return i;
		else i++;
	}
	return 0;
}

int isAsslegal(char statement[], int judgeAss) //在确定有等号的情况下进一步判断是否合法
{
	int isAlpha = 1;
	int isDigit = 1;

	for (int i = 0; i < judgeAss; i++) { //判断左端是否为字母序列
		if (!isalpha(statement[i])) isAlpha = 0;
	}

	int j = judgeAss + 1;
	while (statement[j] != '\0') { //判断右端是否为数字序列
		if (j == judgeAss + 1) {
			if (!isdigit(statement[j]) && statement[j] != '-') { //单独判断第一个字符是否合法
				isDigit = 0;
				break;
			}
		}
		else {
			if (!isdigit(statement[j])) {
				isDigit = 0;
				break;
			}
		}

		j++;
	}

	if (isAlpha == 1 && isDigit == 1) {
		strncpy_s(variable[assPos], statement, judgeAss); //如果赋值操作合法,则把变量名称和数值分别存储到variable和value二维数组中

		int val = 0; //将字符串转换为数字
		int i = judgeAss + 1;

		if (statement[judgeAss + 1] == '-') {
			while (statement[i + 1] != '\0') {
				val = val * 10 + statement[i + 1] - '0';
				i++;
			}
			value[assPos] = -val;
		}
		else {
			while (statement[i] != '\0') {
				val = val * 10 + statement[i] - '0';
				i++;
			}
			value[assPos] = val;
		}
		return 1;
	}
	else return 0;
}

int isExplegal(char statement[], char assList[][stateLength]) //判断表达式是否合法
{
	/*要判断的是表达式是否合法,合法的定义如下:
	  1、没有除了+,-,*,/,(,)以外的符号
	  2、所有的字母序列都可在assList中找到对应的数字
	  3、+,*,/左边必须是数字或),右边必须是数字或(
	  4、-左右必须是数字或)(*/

	int i = 0; //判断合法第一点
	while (statement[i] != '\0') {
		if (!isdigit(statement[i]) && !isalpha(statement[i])) {  //不是字母和数字必定是操作符
			if (!(statement[i] == '+' || statement[i] == '-' || statement[i] == '*' || statement[i] == '/' || statement[i] == '(' || statement[i] == ')')) {
				printf("scanner error, %c is a problem!\n", statement[i]);
				return 0;
			}
		}
		i++;
	}

	//判断合法第二点
	int length = 0; //字母序列的长度
	i = 0; //指示字母序列的末位置
	while (statement[i] != '\0') {
		if (isalpha(statement[i])) {
			length++;

			if (statement[i + 1] == '\0' || !isalpha(statement[i + 1])) { //如果已经到表达式末尾或者不再是字母,则说明字母序列结束了
				char check[stateLength]; //用于存储即将被检测的字母序列
				strncpy_s(check, statement + i - length + 1, length);

				int judge = 0; //开始匹配variable数组;
				for (int j = 0; j < assPos; j++) {
					if (strcmp(check, variable[j]) == 0) judge = 1;
				}

				if (judge == 0) { //如果没有匹配结果则输出错误信息
					printf("%s", check);
					printf(" is not found\n");
					return 0;
				}
				else length = 0; //每一个字母序列检查结束后要把length置0
			}
		}
		i++;
	}

	i = 0; //判断合法第三点
	while (statement[i] != '\0') {
		if (statement[i] == '+' || statement[i] == '*' || statement[i] == '/') {
			if (i == 0 || statement[i + 1] == '\0') { //不能是第一位和最后一位
				printf("syntax error...\n");
				return 0;
			}
			else { //+,*,/左边必须是数字或)或字母,右边必须是数字或(或字母
				if (!(isalpha(statement[i - 1]) || isdigit(statement[i - 1]) || statement[i - 1] == ')') || !(isalpha(statement[i + 1]) || isdigit(statement[i + 1]) || statement[i + 1] == '(')) {
					printf("syntax error...\n");
					return 0;
				}
			}
		}
		i++;
	}

	i = 0; //判断合法第四点
	while (statement[i] != '\0') {
		if (statement[i] == '-') {
			if (statement[i + 1] == '\0') { //不能是最后一位
				printf("syntax error...\n");
				return 0;
			}
			else { //+,-,*,/左边必须是数字或),右边必须是数字或(
				if (i > 0) {
					if (!(isalpha(statement[i - 1]) || isdigit(statement[i - 1]) || statement[i - 1] == '(' || statement[i - 1] == ')') || !(isalpha(statement[i + 1]) || isdigit(statement[i + 1]) || statement[i + 1] == ')' || statement[i + 1] == '(')) {
						printf("syntax error...\n");
						return 0;
					}
				}
			}
		}
		i++;
	}

	return 1;

}

//以下是操作符转换后缀表达式

void isLeft() //左括号直接入操作符栈
{
	stackOper[topOper].isNum = 0;
	stackOper[topOper].oper = '(';
	topOper++;
}

void isRight(int* j, elem postfixExp[])
{
	elem oper;

	while (topOper > 0) { //找到左括号后break
		topOper--;
		oper = stackOper[topOper]; //弹出

		if (oper.oper == '(') break; //如果是左括号直接返回,否则依次输出到后缀表达式
		else {
			postfixExp[*j] = oper;
			(*j)++;
		}
	}
}

void isAdd(int* j, elem postfixExp[])
{
	elem oper;
	while (topOper > 0) {
		topOper--; //弹出
		oper = stackOper[topOper];

		//如果是'+'则依次pop栈顶操作符,直至pop所得为'('或栈为空,若pop得到'('需要将其重新push入栈
		//pop至上述两种情况之一后,将'+'入栈

		if (oper.oper == '(') {
			topOper++;
			break;
		}
		else {
			postfixExp[*j] = oper;
			(*j)++;
		}
	}

	stackOper[topOper].isNum = 0;
	stackOper[topOper].oper = '+';
	topOper++;
}

void isSub(int* j, elem postfixExp[])
{
	elem oper;
	while (topOper > 0) {
		topOper--; //弹出
		oper = stackOper[topOper];

		//如果是'-'则依次pop栈顶操作符,直至pop所得为'('或栈为空,若pop得到'('需要将其重新push入栈
		//pop至上述两种情况之一后,将'+'入栈

		if (oper.oper == '(') {
			topOper++;
			break;
		}
		else {
			postfixExp[*j] = oper;
			(*j)++;
		}
	}

	stackOper[topOper].isNum = 0;
	stackOper[topOper].oper = '-';
	topOper++;
}

void isMul() //'*'和'/'都直接入栈
{
	stackOper[topOper].isNum = 0;
	stackOper[topOper].oper = '*';
	topOper++;
}

void isDiv()
{
	stackOper[topOper].isNum = 0;
	stackOper[topOper].oper = '/';
	topOper++;
}

void isEnd(int* j, elem postfixExp[]) //如果是'=',则依次弹出栈顶元素输出至后缀表达式,直至栈空
{
	elem oper;
	while (topOper > 0) {
		topOper--; //弹出
		oper = stackOper[topOper];

		postfixExp[*j] = oper;
		(*j)++;
	}

}

//以上是操作符转换后缀表达式

void transerve(char statement[], elem postfixExp[]) //将中缀表达式转换为后缀表达式的函数
{
	int i = 0, j = 0; //i为中缀表达式的当前下标,j为后缀表达式的当前下标
	while (statement[i] != '\0') {
		if (isdigit(statement[i + 1]) && statement[i - 1] == '(' && statement[i] == '-');  //负数不在这里处理,直接跳过
		else if (isdigit(statement[i])) { //如果当前字符是数字,则直接输出到后缀表达式
			int k = i; //需要判断是不是数字序列
			//这里要特别区分负数的情况,因为直接按规则转换成后缀表达式会出错
			if (i > 1 && statement[i - 2] == '(' && statement[i - 1] == '-') { //负数的情况
				int val = 0;
				while (isdigit(statement[k])) {
					val = val * 10 + statement[k] - '0';
					k++;
				}
				val = -val;
				postfixExp[j].isNum = 1; //输出到后缀表达式
				postfixExp[j].value = val;
				j++;
			}

			else { //正数的情况
				int val = 0;
				while (isdigit(statement[k])) {
					val = val * 10 + statement[k] - '0';
					k++;
				}
				postfixExp[j].isNum = 1; //输出到后缀表达式
				postfixExp[j].value = val;
				j++;
			}

			i = k - 1;
		}

		else if (isalpha(statement[i])) { //现在需要将赋值变量替换成数值
			char search[stateLength]; //存储需要查找的变量
			int k = i;

			while (isalpha(statement[k])) k++;
			k--; //字母序列的末位置
			strncpy_s(search, statement + i, k + 1 - i);

			for (int l = 0; l < assPos; l++) { //在variable数组中查询
				if (strcmp(variable[l], search) == 0) {
					postfixExp[j].isNum = 1; //查询到了就输出到后缀表达式
					postfixExp[j].value = value[l];
					j++;
					break;
				}
			}

		}

		else { //除了变量和数值就是操作符
			switch (statement[i]) {
			case '(':
				isLeft();
				break;

			case ')':
				isRight(&j, postfixExp);
				break;

			case '+':
				isAdd(&j, postfixExp);
				break;

			case '-':
				isSub(&j, postfixExp);
				break;

			case '*':
				isMul();
				break;

			case '/':
				isDiv();
				break;
			}
		}

		i++;
	}
	//将操作符栈中所有元素输出到后缀表达式
	isEnd(&j, postfixExp);

	postfixExp[j].isNum = 0; //在后缀表达式的最后添加一个#,方便后续计算
	postfixExp[j].oper = '#';

}

double caculator(elem postfixExp[]) //计算器
{
	double Numstack[stateLength]; //操作数栈
	int top = 0; //操作数栈指针

	int i = 0; //后缀表达式的当前下标
	while (postfixExp[i].oper != '#') {

		if (postfixExp[i].isNum == 1) { //操作数直接入栈
			Numstack[top] = (double)postfixExp[i].value;
			top++;
		}
		else { //不是操作数就是操作符
			top--;
			double right = (double)Numstack[top]; //操作符右边
			top--;
			double left = (double)Numstack[top]; //操作符左边

			double value; //此操作符运算的结果
			switch (postfixExp[i].oper) {
			case '+':
				value = left + right;
				break;

			case '-':
				value = left - right;
				break;

			case '*':
				value = left * right;
				break;

			case '/':
				value = left / right;
				break;
			}

			Numstack[top] = value; //结果入栈
			top++;
		}
		i++;
	}

	return Numstack[0]; //栈底一定是答案
}

void otherFunc(char stateList[][stateLength], char assList[][stateLength], int i)
{
	//这里需要区分“赋值”和“运算”两种情况,不然无法判断操作是否合法,暂时先用是否有“=”判断是否为赋值操作

	int judgeAss = isAss(stateList[i]); //判断是否为赋值操作,是则1,否则0
	if (judgeAss) { //在确定是赋值操作的情况下进一步判断操作是否合法
	 //现在需要判断左边是字母序列,右边是数字序列,可以有负数,否则就违法
		int judgeAsslegal = isAsslegal(stateList[i], judgeAss);
		if (!judgeAsslegal) printf("syntax error...\n");
		else {
			strcpy_s(assList[assPos], stateList[i]); //如果是赋值操作则将其复制到assList数组中
			assPos++;
		}
	}

	else { //如果不是赋值操作则实现计算功能
		int judgeExplegal = isExplegal(stateList[i], assList); //判断表达式是否合法

		if (judgeExplegal) { //如果表达式合法则执行运算,此段代码块是重点,用到栈的操作
			elem postfixExp[stateLength]; //转化的后缀表达式存储,用结构体的原因是更方便计算
			transerve(stateList[i], postfixExp);
			double answer = caculator(postfixExp); //开始计算
			printf("> %2lf\n", answer);
		}
	}
}

int main()
{
	printf("> Welcome to the caculator\n");

	char stateList[stateNumber][stateLength]; //操作表
	char assiList[stateNumber][stateLength]; //赋值表
	int i = 0; //当前操作的位置

	while (1) { //计算器无限循环
		gets_s(stateList[i], 49); //写49是因为最后一个字符需要留给\0

		int judgeQuit = isQuit(stateList[i]);
		if (judgeQuit) return 0;  //如果quit则退出
		else otherFunc(stateList, assiList, i); //如果不quit则实现其他功能

		i++; //下一个操作
	}

	return 0;
}

运行结果截图:

C语言实现多功能计算器

最后,还有一些问题没有完善,比如检查计算语句是否合法所用的规则不全面,没有实现除数不为0的功能,以及不能覆盖赋值变量等等,欢迎大家完善并指教。