基于C语言编写一个多功能计算器
作者:微小冷
用C语言写一个计算器,除了四则混合运算之外,还支持三角函数和绝对值等函数。
PS E:\Code\PL\calc> .\a.exe abs(3*5-4^2) abs(3*5-4^2)=1.000000 25-7+6*(4-5) 25-7+6*(4-5)=12.000000
在计算器中,至少包含两类变量,即数字和运算符。例如,如果希望实现a+b×(c−d)这样一个简单的功能,要求编译器可以识别出+,×,−,(,)这五个符号,并理清彼此的计算顺序,最后生成一棵语法树,然后实现输出。
1. 加减法运算
万事开头难,所以我们选择一个简单到无脑的开头。首先,我们考虑实现a + b a+ba+b这样简单的两数运算,即如下所示,十分简单且无脑。
void douCalc(){ while (1){ double i, j, s; char k; scanf("%lf%c%lf", &i, &k, &j); switch (k){ case '+': s = i+j; break; case '-': s = i-j; break; case '*': s = i*j; break; case '/': s = i/j; break; default: break; } printf("%lf\n", s); } }
然后,我们考虑,如何实现一个连加器,旨在解决a+b+c+...的计算问题。这里虽然不涉及到运算次序,但仍旧需要处理多个不确定个数的变量,所以我们不再可以直接用类似scanf("%lf%c%lf", &i, &k, &j);的方案来实现数据的输入,而必须建立一个链表来存储变量。
C语言输入输出
在C语言中,可以通过至少三种方式来读取键盘输入的值:
- scanf():和 printf() 类似,scanf() 可以输入多种类型的数据。
- getchar()、getche()、getch():这三个函数都用于输入单个字符。
- gets():获取一行数据,并作为字符串处理。
其中,scanf是格式化扫描的意思,可以通过格式控制符对输入字符进行格式化,并赋值给相关变量。
格式控制符 | 说明 |
---|---|
%c | 读取单一字符 |
%s | 读取一个字符串(以空白符为结束) |
%f、%lf | 读取十进制形式小数,赋值给float、double 类型 |
%e、%le | 读取指数形式小数,赋值给 float、double 类型 |
%g、%lg | 读取十进制或指数形式的小数, 并分别赋值给 float、double 类型 |
整数格式化
short | int | long | |
---|---|---|---|
十进制 | %hd | %d | %ld |
八进制 | %ho | %o | %lo |
十六进制 | %hx | %x | %lx |
无符号 | %hu | %u | %lu |
getchar()等价于scanf("%c", c),相对来说更加简单。getche和getch是Windows独有的函数,在头文件conio.h中故不赘述。
gets和scanf(%s,s)的区别在于,后者在使用的过程中会把空格当作终止符,而前者不会。
所以,我们在实现连加的过程中,会使用gets作为交互方法。
由于我们实现的是一个连加器,所以输入字符中只包含数字和加号,那么接下来,我们需要遍历输入字符,通过加号来将数字分开。我们可以很方便地写下一个简单而丑陋的小程序。
void adds(){ char str[100]; char numStr[20]; int num[20]; int val; int i,j,k; while (1){ gets(str); i = 0;j = 0;k = 0; while (str[i]!='\0'){ if (str[i]=='+'){ num[k] = atoi(numStr); k++; j = 0; }else{ numStr[j] = str[i]; j++; } i++; } num[k]=atoi(numStr); val = 0; for (int i = 0; i < k+1; i++){ val += num[i]; } printf("%d\n",val); } } int main(){ adds(); return 0; }
由于加减法具有相同的运算优先级,在实现上不过是为后续的数字加上一个负号而已,故可十分方便地在原有程序上修改。
此外,adds代码乍看上去没什么问题,但str的值在更新之前,并不会自动清零,由此带来的bug需要创建一个字符串清零的函数。修改之后的代码如下
#include <stdio.h> #include <stdio.h> #include <string.h> #include <math.h> void strClear(char *str,int n){ for (int i = 0; i < n; i++){ str[i]=NULL; } } void adds(){ char str[100]; char numStr[20]; int num[20]; int val; int i,j,k; while (1){ gets(str); i = 0;j = 0;k = 0; while (str[i]!='\0'){ if (str[i]=='+'){ num[k] = atoi(numStr); strClear(numStr,20); k++; j = 0; }else if (str[i]=='-'){ num[k] = atoi(numStr); strClear(numStr,20); k++; numStr[0] = str[i]; j = 1; }else{ numStr[j] = str[i]; j++; } i++; } num[k]=atoi(numStr); strClear(numStr,20); val = 0; for (int i = 0; i < k+1; i++){ val += num[i]; } printf("%d\n",val); } } int main(){ adds(); return 0; }
精简一下
#include <stdio.h> #include <stdio.h> #include <string.h> #include <math.h> void strClear(char *str,int n){ for (int i = 0; i < n; i++){ str[i]='\0'; } } void adds1(){ char str[100]; char numStr[20]; int i,j,val; while (1){ gets(str); i = 0;j = 0;val = 0; while (str[i]!='\0'){ if ((str[i]=='+')||(str[i]=='-')){ val += atoi(numStr); strClear(numStr,20); j = 0; if (str[i]=='-') numStr[j++]=str[i]; }else numStr[j++] = str[i]; i++; } val += atoi(numStr); strClear(numStr,20); printf("%d\n",val); } } int main(){ adds1(); return 0; }
2. 加法和乘法
若希望加入乘法和除法,那么修改代码的过程就相对复杂了,因为乘除法在运算过程中,具有比加减法更高的优先级。那么我们就无法通过一个简单的数组来存储变量,而必须建立一种树形的结构。
例如,对于a+b×c−d×e/f,可写成如下形式
我们可以看到,这是一个二叉树,每个叶节点都是数字,而两个叶节点的父节点则为运算符。我们通过这个运算符来计算其子节点后,删除它的两个子节点,同时运算符所对应的节点退化为叶节点,同时其类型也变为数字。
对于上图而言,先计算e/f,然后计算b×c和d×e/f,再计算b×c−d×e/f,最后计算最上面的加法。
对于树来说,我们的遍历往往从根节点开始,所以其计算规则如下:
- 如果当前节点的子节点为叶节点,则计算当前节点,并删除该节点的叶节点,然后考虑其父节点。
- 如果当前节点的某个子节点不是叶节点,则处理该子节点,直到该子节点成为叶节点为止。
- 如果当前节点为根节点,且为叶节点,则输出计算结果。
对于节点来说,除了父子节点外,则至少有两个属性:
- isLeaf:用于叶节点判定。在这里,叶节点不仅有结构上的意义,更有着明确的语义:它只能是数字。
- value:对于叶节点而言,这个值为数字,否则的话,这个值为运算符号及其所对应的计算规则。
# define MAXLEN 100 typedef struct NODE{ struct NODE *father; struct NODE *Left; struct NODE *Right; char value[MAXLEN]; int isLeaf; }Node;
生成计算树
由于我们规定了两个运算层级,所以再遍历字符串以生成计算树的过程中,需要两次循环,即首先生成加减法的计算树,然后再生成乘除法的计算树。
生成计算树的过程可以简化为字符串不断拆分的过程,为了简化思维,我们只考虑两个符号+和*,这两个符号分别代表两种计算层级。
由此可得到如下代码。对于
# define TRUE 1 # define FALSE 0 void newNode(Node *root, Node *father){ root -> father = father; root ->Left = NULL; root -> Right = NULL; root -> isLeaf = FALSE; } //root 为根节点,str为字符串 void initCalcTree(Node *root, char flag){ for (int i = 0; i < MAXLEN; i++){ if (root->value[i]==flag){ Node *Left = (Node *)malloc(sizeof(Node)); Node *Right = (Node *)malloc(sizeof(Node)); newNode(Left,root); newNode(Right,root); for (int j = 0; j < i; j++) Left -> value[j] = root->value[j]; Left->value[i] = '\0'; i++; for (int j = i; j < MAXLEN; j++) Right -> value[j-i] = root->value[j]; root->Left = Left; root->Right = Right; strClear(root->value,MAXLEN); root->value[0] = flag; root->value[1] = '\n'; initCalcTree(Left,'*'); if (flag=='+') initCalcTree(Right,'+'); else initCalcTree(Right,'*'); break; }else{ if (root->value[i]=='\0'){ if(flag =='+') initCalcTree(root,'*'); else root -> isLeaf = TRUE; break; } else continue; } } }
测试一下
void printNode(Node *root,int start){ printf("the %dth node is %s\n", start, root->value); if (root->isLeaf==FALSE){ printNode(root->Left, start + 1); printNode(root->Right, start + 1); } } int main(){ Node *root = (Node *)malloc(sizeof(Node)); char *str = "1+21*3+3*4*5+6"; strcpy(root->value,str); initCalcTree(root,'+'); printNode(root,0); return 0; }
得到结果为
the 0th node is +
the 1th node is 1
the 1th node is +
the 2th node is *
the 3th node is 21
the 3th node is 3
the 2th node is +
the 3th node is *
the 4th node is 3
the 4th node is *
the 5th node is 4
the 5th node is 5
the 3th node is 6
然后,我们再对计算树进行计算。当被计算的量为叶节点时,则返回该节点的值;如果该节点的两个节点都是叶节点,则返回该节点处的运算符对这两个子节点的计算值;如果该节点的两个节点都不是叶节点,那么对这两个子节点进行计算。
int calcNode(Node *root){ if(root->isLeaf == TRUE) return atoi(root->value); else if (root->Left->isLeaf * root->Right->isLeaf == TRUE){ if(root->value[0] == '+') return atoi(root->Left->value)+atoi(root->Right->value); else atoi(root->Left->value)*atoi(root->Right->value); }else{ if (root->value[0] == '+') return calcNode(root->Left)+calcNode(root->Right); else return calcNode(root->Left)*calcNode(root->Right); } } int main(){ Node *root = (Node *)malloc(sizeof(Node)); char str[MAXLEN]; while (1){ gets(str); strcpy(root->value,str); initCalcTree(root,'+'); printf("%s=%d\n",str,calcNode(root)); } return 0; }
结果为
PS E:\Code\PL\calc> .\a.exe
1+2+3*4+15*2
1+2+3*4+15*2=45
2*5+3*6*12
2*5+3*6*12=226
3. 四则混合运算
如果考虑加减乘除,那么意味着一个运算级别下有多种运算符,所以我们需要通过一个函数来返回运算符的运算次序。
int getOrder(char ch){ int result; switch (ch){ case '+': case '-': return 0; case '*': case '/': return 1; default: return 2; } }
然后,基于此,修改计算树的生成与计算代码。
int douCalc(char c,int a, int b){ switch (c){ case '+': return a+b; case '-': return a-b; case '*': return a*b; case '/': return a/b; } } void newNode(Node *root, Node *father){ root -> father = father; root ->Left = NULL; root -> Right = NULL; root -> isLeaf = TRUE; father -> isLeaf = FALSE; } //root 为根节点,str为字符串,N为字符串长度 void initCalcTree(Node *root, int order){ for (int i = 0; i < MAXLEN; i++){ if (getOrder(root->value[i])==order){ Node *Left = (Node *)malloc(sizeof(Node)); Node *Right = (Node *)malloc(sizeof(Node)); newNode(Left,root); newNode(Right,root); for (int j = 0; j < i; j++) Left -> value[j] = root->value[j]; Left->value[i] = '\0'; i++; for (int j = i; j < MAXLEN; j++) Right -> value[j-i] = root->value[j]; root->Left = Left; root->Right = Right; root->value[0] = root->value[i-1]; root->value[1] = '\0'; initCalcTree(Right,order); if (order<1) initCalcTree(Left,order+1); break; } else if((i==0)&&(order<2)) initCalcTree(root,order+1); } } int calcNode(Node *root){ if(root->isLeaf == TRUE) return atoi(root->value); else if (root->Left->isLeaf * root->Right->isLeaf == TRUE) return douCalc(root->value[0], atoi(root->Left->value),atoi(root->Right->value)); else return douCalc(root->value[0], calcNode(root->Left),calcNode(root->Right)); } int main(){ Node *root = (Node *)malloc(sizeof(Node)); char str[MAXLEN]; while (1){ gets(str); strcpy(root->value,str); initCalcTree(root,0); printf("%s=%d\n",str,calcNode(root)); } return 0; }
至此,我们得到了一个计算器的“骨架”,为运算符设定相应的运算次序,相当于提供一种生成方法,这种方法可以直接扩展到更多的运算符上。
同时,上述代码中也出现了两个问题:
我们默认计算的是整型数据,所以无法处理浮点型运算
减法和除法虽然在名义上与加法、乘法处于相同的运算次序中,但我们的生成树中默认的是从右向左计算。对于a+b−c+d这样的表达式,会计算成a+b−(c+d)的形式,这是错误的。
针对这种运算结构的一个优势和两个问题,我们继续改进这个计算器程序。
4. 浮点型计算器程序
首先,我们将所有函数与变量均改为double类型;然后我们更改输入字符串的遍历方式,从后向前进行遍历。
我们再加入乘方运算符^,给它一个更高的运算层级
int getOrder(char ch){ int result; switch (ch){ case '+': case '-': return 0; case '*': case '/': return 1; case '^': return 2; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return 3; default: return 4; } } double douCalc(char c,double a, double b){ switch (c){ case '+': return a+b; case '-': return a-b; case '*': return a*b; case '/': return a/b; case '^': return pow(a,b); } }
至此,我们写出了一个可以计算+-x÷^的程序。但我们还不能处理表达式中可能出现的括号。
括号在表达式中成对出现,故不同于常规运算符,需要在表达式的两端进行遍历;另外,括号不具备运算功能,只有规定运算次序的作用,对于括号运算符只有一个子节点。所以,只需更改initCalcTree的代码。
由于我们将算法改为从右向左遍历,所以如果最后一个字符不是),则不必考虑括号的影响。当最后一个字符为)时,如果第0个字符为(,则将括号里面的内容提取出来,针对此时的节点重新进行遍历即可。如果自左向右遍历的过程出现第一个(的位置是posLeft,则后面关于运算符的遍历从posLeft开始。
//root 为根节点,str为字符串,N为字符串长度 void initCalcTree(Node *root, int order){ int lenStr = strlen(root->value); int posLeft = lenStr; //如果末尾为')',则查找其对应的左括号的位置 if(root->value[lenStr-1]==')'){ for (int i = 0; i < lenStr; i++) if(root->value[i]=='(') posLeft = i; if (posLeft == 0){ for (int i = 1; i < lenStr-1; i++) root->value[i-1] = root->value[i]; root->value[lenStr-2]='\0'; initCalcTree(root,0); } } //如果左括号的位置不为0,则 for (int i = posLeft; i >= 0; i--){ if (getOrder(root->value[i])==order){ Node *Left = (Node *)malloc(sizeof(Node)); Node *Right = (Node *)malloc(sizeof(Node)); newNode(Left,root); newNode(Right,root); for (int j = 0; j < i; j++) Left -> value[j] = root->value[j]; Left->value[i] = '\0'; i++; for (int j = i; j < MAXLEN; j++) Right -> value[j-i] = root->value[j]; root->Left = Left; root->Right = Right; root->value[0] = root->value[i-1]; root->value[1] = '\0'; //字符串末尾标记 initCalcTree(Left,order); if ((order<2)||(posLeft!=lenStr)) initCalcTree(Right,order+1); break; } else if((i==0)&&(order<2)) initCalcTree(root,order+1); } }
至此,我们就写好了一个简陋的可以进行四则混合运算的计算器程序
PS E:\Code\PL\calc> .\a.exe
1+2*(3-4)+5
1+2*(3-4)+5=4.000000
2^(3+1)
2^(3+1)=16.000000
5. 加入三角函数
现在我们需要考虑加入三角函数,其难点在于函数的识别。
我们规定,单变量函数通过括号的方式导入实参,也就是说,只要表达式中不出现括号,那么就不必考虑括号的问题。换句话说,判定函数,必然在判定括号之后。
考虑到我们定义的getOrder函数中,除了我们所规定的符号和数字之外,其他符号和字母的默认返回值为4。所以需要在判定括号之后,继续进行函数的判断。
故而需要更改括号判定的代码
/*...*/ if(root->value[lenStr-1]==')'){ for (int i = 0; i < lenStr; i++) if(root->value[i]=='(') posLeft = i; if (posLeft == 0){ for (int i = 1; i < lenStr-1; i++) root->value[i-1] = root->value[i]; root->value[lenStr-2]='\0'; initCalcTree(root,0); }else{ int lenFunc=0; posLeft--; while ((getOrder(root->value[posLeft])==4)&&(posLeft>0)){ posLeft--; lenFunc++;} //当posLeft变为0时,说明此节点为无法分割的函数 if (posLeft==0){ root->value[lenFunc+1]='\0'; Node *Left = (Node *)malloc(sizeof(Node)); root->Left = Left; newNode(Left,root); for (int i = lenFunc+2; i < lenStr-1; i++){ Left->value[i-lenFunc-2]=root->value[i]; } Left->value[lenStr-lenFunc-2]='\0'; initCalcTree(Left,0); //对左子节点进行生成 return 0; } } } /*...*/
接下来,我们需要修改calcNode函数,即在计算运算符之前,添加一个函数处理程序。其中,strcmp为字符串比对函数,当两个字符串相等时,返回0。
double doFunc(char *str,double val){ if (strcmp(str,"sin")==0) return sin(val); else if (strcmp(str,"cos")==0) return cos(val); else if (strcmp(str,"tan")==0) return tan(val); else if (strcmp(str,"arcsin")==0) return asin(val); else if (strcmp(str,"arccos")==0) return acos(val); else if (strcmp(str,"arctan")==0) return atan(val); else if (strcmp(str,"sqrt")==0) return sqrt(val); else if (strcmp(str,"abs")==0) return abs(val); } double calcNode(Node *root){ if(getOrder(root->value[0])==4) return doFunc(root->value,calcNode(root->Left)); if(root->isLeaf == TRUE) return atof(root->value); else if (root->Left->isLeaf * root->Right->isLeaf == TRUE) return douCalc(root->value[0], atof(root->Left->value),atof(root->Right->value)); else return douCalc(root->value[0], calcNode(root->Left),calcNode(root->Right)); }
至此,我们已经用C语言实现了一个简陋而且有不少bug的计算器,比如并未设置除零报警之类的功能,但一般的操作是没有问题的。
abs(3*5-4^2)
abs(3*5-4^2)=1.000000
25-7+6*(4-5)
25-7+6*(4-5)=12.000000
以上就是基于C语言编写一个多功能计算器的详细内容,更多关于C语言计算器的资料请关注脚本之家其它相关文章!