(C语言)小项目:计算器

发布于 2022-06-22  161 次阅读


先叠几层甲:

这个计算器中很多功能和函数的实现方法肯定有更简单更优化的,本文章里的方法属于比较笨的那种,仅供新手初学者以及博主自己参考,文章中提供的代码及源文件难免会有疏忽、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文件点击在本文章尾的链接即可下载~

运行效果:

源文件下载


逸一时,误一世