C语言小项目:计算器
先叠几层甲:
这个计算器中很多功能和函数的实现方法肯定有更简单更优化的,本文章里的方法属于比较笨的那种,仅供新手初学者以及博主自己参考,文章中提供的代码及源文件难免会有疏忽、bug,仅供参考、学习、交流;除了正常的讨论问题、建议外,还请各位大佬键盘之下给我留点面子qaq
有的朋友可能会问:写一个计算器有什么用?我们作为程序员,是人,计算的工作应该是由计算机来完成的,我们来写计算器,又能写什么?
写一个计算器当然不是要写“加减乘除”等运算的实现,而是我们人输入一个算式,程序将算式处理交给计算机去算,最后返回结果的这个过程。在这个过程中可以锻炼我们作为初学者对C语言的熟练程度、也可以熟悉数据结构中栈的操作,而不是最后真就只是为了写个计算器出来。
那么,这个程序应该要怎么写?
因为这个小项目是比较基础的,而且我也比较菜,所以这里就提供一种比较笨的思路:
定义两个栈空间,一个为数字栈,一个为符号栈,当输入算式时,将算式中的数字存入数字栈,将运算符存入符号栈,在运算符入栈时,判断符号的优先级,若新入栈的运算符的优先级大于栈顶的运算符,直接入栈即可,若小于或等于栈顶的优先级,就需要让栈顶的运算符出栈,再根据它是单目运算符还是双目运算符,从数字栈中拿出对应数量的数字进行运算,最后将结果入栈(数字栈),新的运算符再入栈,这样循环往复下去,直到输入的算式中的内容全部入栈完毕,符号栈内的运算符全部处理完毕,栈为空的时候,数字栈内剩下的计算结果打印即可。
以1+3*2-2=这个算式为例:
数字栈:1,3,2
符号栈:+,*
当减号'-'入栈时,它的优先级低于栈顶的乘号'*',而乘号是双目运算符,因此我们需要在数字栈中让"3"和"2"出栈参与运算
计算之后,乘号出栈参与运算,结果入栈:
数字栈:1,6
符号栈:+
但当减号'-'要入栈时,栈顶的加号的优先级却又和他一样,因此还要进行一次运算,之后才能入栈:
数字栈:7,2
符号栈:-
最后,算式的内容已经全部入栈,再继续把符号栈内的运算符出栈运算即可(7-2)
最后数字栈内就只剩下5,符号栈为空,这个5就是这个算式的结果,打印即可
思路有了,接下来就是实现,第一个问题就是:如何输入算式?
用scanf吗?肯定不行,因为输入的算式不是固定的
所以这里我们用getchar,配合while循环,当获取到等于号"="或者回车"\n"时跳出循环
while(1)
{
char q = getchar();
if(q=='='||q=='\n')break;
if(q<='9'&& q>='0')
{
//处理获取到的数字
}
else if(q=='+'||q=='-'||q=='*'||q=='/'||q=='%'||q=='^'||q=='!'||q=='('||q==')'||q=='.')
{
//处理获取到的符号
}
}
解决了输入的问题,现在就来解决存的问题
这里就直接放出栈相关的函数,如果不理解可以先去看看数据结构栈的相关内容或在评论区讨论
数字栈:
typedef float numtype;
typedef struct node_num//栈的节点的类型
{
numtype data;
struct node_num *above;//指向上面那个节点的指针
struct node_num *below;//指向下面那个节点的指针
}Node_num;
typedef struct stack_link_num//栈的类型
{
Node_num *top;//指向栈顶节点
Node_num *buttom;//指向栈底节点
int node_num;//栈的元素个数
}Stack_num;
Stack_num *num_init_link_stack(void)//初始化一个链式栈(创建头结点)
{
Stack_num *s=malloc(sizeof(*s));
s->buttom=NULL;
s->top=NULL;
s->node_num=0;
return s;
}
Node_num *num_create_node(numtype a)//创建一个节点
{
Node_num *p=malloc(sizeof(Node_num));
p->data=a;p->above=NULL;p->below=NULL;
}
void num_push_data(Stack_num *s,numtype d)//元素入栈
{
if(s==NULL)return;
Node_num *p=num_create_node(d);
if(s->top==NULL){s->buttom=p;s->top=p;s->node_num++;}
else{s->top->above=p;p->below=s->top;s->top=p;s->node_num++;}
}
numtype num_pop_data(Stack_num *s)//元素出栈
{
if(s==NULL||s->buttom==NULL)return -1;
numtype d=s->top->data;//需要得到的值
Node_num *p=s->top;
if(s->node_num==1)//如果栈里面只有一个元素
{
s->buttom=NULL;
s->top=NULL;
}
else
{
s->top=s->top->below;
s->top->above=NULL;
p->below=NULL;
}
s->node_num--;
free(p);
return d;
}
符号栈:
typedef char chatype;
typedef struct node_cha//栈的节点的类型
{
chatype data;
struct node_cha *above;//指向上面那个节点的指针
struct node_cha *below;//指向下面那个节点的指针
}Node_cha;
typedef struct stack_link_cha//栈的类型
{
Node_cha *top;//指向栈顶节点
Node_cha *buttom;//指向栈底节点
int node_num;//栈的元素个数
}Stack_cha;
Stack_cha *cha_init_link_stack(void)//初始化一个链式栈(创建头结点)
{
Stack_cha *s=malloc(sizeof(*s));
s->buttom=NULL;
s->top=NULL;
s->node_num=0;
return s;
}
Node_cha *cha_create_node(chatype a)//创建一个节点
{
Node_cha *p=malloc(sizeof(Node_cha));
p->data=a;p->above=NULL;p->below=NULL;
}
void cha_push_data(Stack_cha *s,chatype d)//元素入栈
{
if(s==NULL)return;
Node_cha *p=cha_create_node(d);
if(s->top==NULL){s->buttom=p;s->top=p;s->node_num++;}
else{s->top->above=p;p->below=s->top;s->top=p;s->node_num++;}
}
chatype cha_pop_data(Stack_cha *s)//元素出栈
{
if(s==NULL||s->buttom==NULL)return -1;
chatype d=s->top->data;//需要得到的值
Node_cha *p=s->top;
if(s->node_num==1)//如果栈里面只有一个元素
{
s->buttom=NULL;
s->top=NULL;
}
else
{
s->top=s->top->below;
s->top->above=NULL;
p->below=NULL;
}
s->node_num--;
free(p);
return d;
}
那么问题来了,如果我输入的是一位以上的数字,比如说"12"、"123",用这种方法是会获取独立的几个数字,"1、2","1、2、3",这样直接入栈是不可以的
所以,要有一种方法,让"123"入栈而不是"1,2,3"
if(a==0)
{
a=1;
int i=q-48;
num_push_data(n,i);
}
else
{
int i=q-48;
n->top->data=(n->top->data)*10;
n->top->data=(n->top->data)+i;
}
由于getchar获取到的是字符,因此我们让它的ASCII码减去48,得到的便是它原本的数字
上述代码中的变量a的作用是区分输入的是否为数字的首位,如果a为0,就把当前的数字作为首位,之后再遇到数字就将首位乘上10相加,也就是将输入的123拆解成"1*10+2=12","12*10+3=123",直到遇到运算符,最后在处理符号的代码中将a置0即可
接下来便是处理符号的部分了,因为不同的运算符的优先级是不同的,因此我们首先要识别运算符的优先级:
直接封装在函数里,方便代码复用:
int judge_fuhao(char a)
{
switch(a)
{
case '+':return 1;
case '-':return 1;
case '*':return 2;
case '/':return 2;
case '%':return 2;
case '^':return 3;
case '!':return 4;
case '(':return 5;
case ')':return 6;
case '.':return 7;//小数点
default:return 8;
}
}
识别优先级后,如果碰到栈顶元素优先级更高或者相同的情况,就需要将栈顶的运算符出栈参与计算,计算的代码也是固定的,因此也封装到函数中方便重复使用:
int judge_fuhao_what(char a)//判断计算的类型
{
switch(a)
{
case '+':return 1;
case '-':return 2;
case '*':return 3;
case '/':return 4;
case '%':return 5;
case '^':return 6;
case '!':return 7;
default:return 0;
}
}
float compute_data(int sb,float a,float b)//根据计算类型计算并返回结果
{
switch(sb)
{
case 1:return a+b;
case 2:return a-b;
case 3:return a*b;
case 4:return a/b;
case 6:{
float sum=a;
for(int i=b;i>1;i--)
{
sum*=a;
}
return sum;
}
default:return 0;
}
}
int quyu(int a,int b)//取余和阶乘这种只能由int类型数据参与的函数单独写
{
return a%b;
}
int compute_jiecheng(int a)
{
int sum=1;
for(int i=1;i<=a;i++)
{
sum*=i;
}
return sum;
}
void compute_data_ctrl(Stack_num *n,Stack_cha *c)//外部调用计算函数
{
char temp=cha_pop_data(c);//把栈里面优先级高的弄出来
float d2=num_pop_data(n);//出来一个数d2
if(temp!='!' && temp!='%')//如果要算的不是阶乘!或者取余%
{
float d1=num_pop_data(n);//就再出来一个数d1
float re=compute_data(judge_fuhao_what(temp),d1,d2);//计算结果
num_push_data(n,re);//结果入栈
}
else
{
if(temp=='!')//如果是算阶乘
{
float re=compute_jiecheng(d2);//直接算d2的阶乘
num_push_data(n,re);//结果入栈
}
else//如果是取余
{
float d1=num_pop_data(n);//再出来一个d1
int re=quyu((int)d1,(int)d2);//d1和d2转成int,取余
num_push_data(n,(float)re);//结果入栈
}
}
}
原理这里就不讲了,看注释应该都能懂,如果不理解可在评论区讨论
本篇演示就只写这些运算符,如果想要加入根号、三角函数等,也很简单,这里就不演示了
接下来比较麻烦的就是括号的处理
这里我的思路是:当遇到左括号(的时候,让左括号(入栈,用一个变量来记录当前的状态,后面再入栈就全是括号()里的内容,直到遇到右括号),右括号)不入栈,开始计算括号内的所有内容,直到碰到左括号(,将结果入栈,让左(括号出栈
代码实现:
void kuo_ctrl(Stack_num *n, Stack_cha *c,int *kuo,int *a,int lev,char q)//括号处理函数
{
if(lev==5)//如果碰到了左括号(
{
*kuo=1;//标志着从现在开始的式子都是括号里的
*a=0;
cha_push_data(c,q);//直接扔(进去入栈
return;
}
else if(lev==6)//如果碰到了右括号),开始算括号里面的东西,右括号并不入栈
{
*a=0;
if(c->top->data!='(')//如果括号里面还有东西(即还没遇到左括号)
{
recompute:
compute_data_ctrl(n,c);
if (c->top->data != '(') //如果此时符号栈的top还不是左括号(,说明括号里面的东西还没算完
{
goto recompute; //接着算
}
else
cha_pop_data(c);//说明已经算完了括号里面的东西,直接把(丢掉出栈
}
else
cha_pop_data(c);//如果括号里面根本没东西可算,就扔了左括号出栈
}
}
这时眼尖的朋友可能会问,为什么不在最后将“kuo”置回0呢?
嘛,我反正觉得把这一步放在最开始的数字处理的部分中比较好(
if(kuo==1)kuo=0;
if(a==0)
{
a=1;
int i=q-48;
num_push_data(n,i);
}
else
{
int i=q-48;
n->top->data=(n->top->data)*10;
n->top->data=(n->top->data)+i;
}
到这里,计算器的所有功能就已经写的差不多了,但是还有两个致命缺陷:
不能处理小数和负数
这显然不是一个合格的计算器
先讲小数的实现:
我的思路是:当检测到小数点"."时,不让它入栈,而是让一个标志小数处理状态的变量qwq置1,后面再入栈的数字就是小数点后面的,同时再由一个变量qaq记录小数点后面的位数,遇到下一个符号时,就让小数点后面的数字乘上qaq个0.1,再和小数点前面的数字相加即可。
代码实现:
这里把之前处理数字的代码也封装到函数中
void num_ctrl(Stack_num *n,char q,int *a,int *kuo,int *qwq,int *qaq)//数字栈操作函数
{
if(*kuo==1)*kuo=0;
if(*a==0)
{
if(*qwq==1)*qaq=1;//小数点后面第一位,qaq=1
*a=1;
int i=q-48;
num_push_data(n,i);
}
else
{
if(*qwq==1)*qaq++;//如果当前处于小数处理中,每多一位就让qaq+1
int i=q-48;
n->top->data=(n->top->data)*10;
n->top->data=(n->top->data)+i;
}
}
void point_num_ctrl(Stack_num *n,int *qaq,int *a,int *kuo,int *qwq)//小数处理函数
{
for(int i=0;i<(*qaq);i++)//让数据栈顶元素乘qaq次0.1从xxx变成0.xxx的形式
{
n->top->data=(n->top->data)*(0.1);
}
float d2=num_pop_data(n);//小数点后面的出栈
float d1=num_pop_data(n);//再让小数点前面的出栈
if(d1<0)d2=d2*(-1);
float re=compute_data(1,d1,d2);//相加,变成x.xxx
num_push_data(n,re);//结果入栈
*qwq=0;//退出小数点处理状态
*a=0;
*kuo=0;
}
这样,小数就可以作为单独的一个元素入栈了。
负数的难点在于,要和减号区分开,这里我的思路是:如果是以下的几种情况之一,那么这个"-"就是负号的意思:
1.数字栈和符号栈均为空,即第一个输入的元素就是"-"
2."-"的前面是左括号"("
3."-"的前面是+、-、*、/、%这种双目运算符
代码实现:
if (q == '-' && ((n->top == NULL && c->top == NULL) ||(n->node_num==1&&c->top==NULL) || (c->top->data == '(' && kuo == 1) ||(judge_fuhao(c->top->data) < 3)))
{
if(n->top == NULL && c->top == NULL)
{
fu_f = 1;
fu = 1;
}
else if(n->node_num==1&&c->top==NULL)
{
cha_push_data(c,q);//直接把-号扔进去
}
else
fu = 1;
}
当检测到这个"-"是负号时,不会让其入栈,而是让标识负号状态的变量fu和fu_f置1,如果是减号就让其直接入栈
在条件中加入"n->node_num==1&&c->top==NULL"即"数字栈中只有1个元素,符号栈为空"的原因是:
如果不加入这个条件,if会继续判断后面的条件,进而使用到"c->top->data",但此时符号栈是空的,就会产生段错误(Segmentation fault)
在符号处理部分的最前面加入负号的处理代码:
if(c->top==NULL&&fu==1&&judge_fuhao(q)==5)
{
fu = 0;
fu_k = 1;
}
if(a==1&&fu==1&&qwq==0)
{
a=0;
if(n->top->data!=0)
{
n->top->data=(n->top->data*(-1));
fu=0;
}
else fu=1;
}
这样当负号状态为1的时候,就让数字栈顶的元素乘上-1
当然,如果当前的符号是左括号(,负号状态又为1,就说明这个负号是对整个括号算完后的结果负责,这时候用一个fu_k=1做标记,而不是直接让数字栈顶元素乘上-1,之后在括号处理函数中碰到右括号的情况后再让栈顶元素乘上-1即可。
现在,计算器所有的功能就全部完成了,将封装好的函数全部丢到单独的一个头文件(.h)中,包含在main.c里面,拼装在一起即可~
#include "stdio.h"
#include "stdlib.h"
#include "funclib.h"
int main()
{
Stack_num *n=num_init_link_stack();//初始化一个数据栈
Stack_cha *c=cha_init_link_stack();//初始化一个符号栈
int a=0,qwq=0,qaq=0,fu=0,kuo=0,fu_f=0,fu_k=0;//a:用来指示新入栈的数字是否属于栈顶数字的最低位
//qwq:小数点处理状态 qaq:小数点后的位数 fu:负数处理状态 kuo:括号处理状态
int info=0;//提示
printf("=================================================================\n\
支持的运算类型:加(+),减(-),乘(*),除(/),取余(%),乘方(^),阶乘(!)\n\
支持功能:括号识别、小数、负数\n\
请输入算式并回车,算式以=结束,例如: 5!+(7.5-8.1*9)/10^2+-90=\n\
=================================================================\n\
算式:");
while(1)
{
char q = getchar();
if(q=='=')break;
if(q=='\n')
{
info=1;
break;
}
if(q<='9'&& q>='0')
{
num_ctrl(n, q, &a, &kuo, &qwq, &qaq);
}
else if(q=='+'||q=='-'||q=='*'||q=='/'||q=='%'||q=='^'||q=='!'||q=='('||q==')'||q=='.')
{
······
stdio.h和stdlib.h两边的尖括号"<>"因为代码高亮插件的原因会出问题,所以在这里写成引号
篇幅所限,这里只展示main.c中的部分代码,完整的.c和.h文件点击在本文章尾的链接即可下载~
运行效果:
- 感谢你赐予我前进的力量