[C++] 例题 2.7.1 用栈实现简易计算器

前置技能

  • 栈 (stack)
    栈是一种限制访问端口的线性表,栈的所有操作都先定在线性表的一端进行。表首被称为“ 栈底 ”,表尾被称为“ 栈顶 ”(这里书上第36页大概说反了)。每次取出的总是最后压进的元素,即“ 后进先出 ”。与之相对的是队列 (queue),在表的一端插入,另一端取出,即“ 先进先出 ”。

  • 中缀表达式 (InfixExp) 与后缀表达式 (PostfixExp)
    中缀表达式即我们常用的23 + (34*45) / (5+6+7)这样的表达式(没错就是65页的例子),对于人类的我们来说,经过了小学的各种计算训练,基本到了五年级统考的时候我们都能扫一眼以后就轻松计算出上面的例子,但是对于计算机来说,括号的处理是一个大难题。反正我没有想到什么比书上讲到的后缀表达式更好的解决方法。
    此时我们就需要把中缀表达式转变为后缀表达式,后缀表达式又称逆波兰表达式,不含括号,运算符放在两个参与运算的语法成分后面,求值严格从左向右(对计算机很友好)。

    顺便吐个槽:此处的infix、postfix都是算术里中缀和后缀的意思。书上概要设计里面的suffix是英文单词的后缀的意思……

中缀:23 + (34*45) / (5+6+7)
后缀:23 34 45 * 5 6 + 7 + / +

需求描述

  • 可以使别加减乘除以及括号的中缀表达式并计算
  • 如果表达式有误,应给出相应的提示

概要设计

书上的类设计图可以债见了。
如果有人按着后面的表达式流程图码代码,我可以保证你能码到怀疑人生。(然而前提是能码出来(?))

下面是头文件calculator.h

#ifndef CALCULATOR_H
#define CALCULATOR_H
#include<iostream>
#include<string>

using namespace std;

class C {
public: 
    //C();    默认构造
    //~C();   默认析构
    double calculate(string InfixExp) ;               //计算函数(炒鸡安全2333
private:
    string infix_to_postfix(string InfixExp);         //中缀转后缀
    double cal_postfix(string PostfixExp);            //计算后缀
    int priority(char op);                            //返回优先级
    double stringToDouble(string num);                //字符串转浮点数
    double cal(double num1, double num2, char op);    //计算两个数
};

#endif // CALCULATOR_H

在这里我们可以清晰地看明白这个计算器类的运行方法:在我们读入了一段中缀表达式以后,直接调用calculate(string InfixExp)函数,在calculate函数内先调用infix_to_postfix(string InfixExp)函数,将中缀表达式转为后缀表达式,然后调用cal_postfix(string PostfixExp)函数,计算后缀表达式。

函数详细设计

这里直接简单摘取书上66、67页的内容对infix_to_postfix(string InfixExp)函数和cal_postfix(string PostfixExp)函数进行说明(然而书上对压栈等操作的说明写的十分笼统,且对栈的类型定义等一律没有……)

  • 中缀转后缀 infix_to_postfix(string InfixExp)
    0 ) 定义后缀表达式 string PostfixExp = " ";,定义存储操作符 stack<char> calculate;
    1 ) 当读入的是数字时,直接输出到后缀表达式 PostfixExp += InfixExp[i];
    当读入的不是数字时,需要插入空格将数字隔开 PostfixExp += " ";
    2 ) 当读入的是左括号时,直接将其压栈 calculate.push(InfixExp[i]);
    3 ) 当读入的是右括号时,在栈非空的情况下弹出左括号前所有操作符 PostfixExp += calculate.top(); calculate.pop();。此时如果括号不匹配(缺少左括号),则栈会被清空,检测栈是否为空 if(calculate.empty()),为空则报错 “ 括号不匹配 ” ,返回 “ error ” 。括号匹配则在之后弹出左括号。
    4 ) 当读入的是运算符 + - * / 时,当栈非空且栈顶非左括号且当前运算符优先级不高于栈顶运算符优先级时,反复将栈顶元素弹出至后缀表达式。之后将输入的运算符压栈 <书上原话 > while (!calculate.empty() && calculate.top() != '(' && priority(InfixExp[i]) <= priority(calculate.top())) 通俗地讲就是* / 先存进后缀表达式,再存入 + -
    5 ) 读到了非法字符,报错返回 “ error ” 。
    6 ) while (!calculate.empty())清栈,如果碰到左括号,说明括号不匹配(缺少右括号),报错返回 “ error ” 。

  • 计算后缀 cal_postfix(string PostfixExp)
    0 ) 处理中缀转后缀的报错 if (PostfixExp == "error") return 0; 。定义字符串用来转成浮点型 string str; 定义存储数字 stack<double> calculate;
    ~ ) 我在这个函数中读取数字并转成浮点型存储,助教小哥哥的意思好像是读取的时候就分开存好(?)不过没差啦……我这个方法除了读取的部分十分诡异以外,更大限度地保留了书上的原汁原味(大雾)至于哪里诡异呢……大概就是为了防止把123读出1,12,123三个数字,我在循环嵌套的内循环里顺便把外循环的计数君也自增了,那么外循环会多1,所以后面又要减掉1……嗯,常规操作常规操作……
    1 ) 读取数字并压栈 calculate.push(stringToDouble(str));
    2 ) 遇到运算符则弹出两个数字并计算,将结果压栈 calculate.push(cal(num1, num2, PostfixExp[i]));

  • 计算两个数 cal(double num1, double num2, char op)
    这个函数没什么好说的,num1 和 num2 的顺序一开始有点懵但实际上跑一下看看就好了。唯一要注意的是除法要判断被除数是否为 0 ,为 0 则报错(返回什么随意) 。

  • 优先级 priority(char op)
    这个优先级的大小就自己看着办吧,反正可以测试一下 3 + 6 / 2 这样的东西。正确与否取决于中缀转后缀的第四步的 while 语句写法和这里的大小赋值。原理上只要赋值 + - * / 但我连括号都赋上了 2333 完全不理解当时在想什么啊!

  • 字符串转浮点型 stringToDouble(string num)
    这个代码我在网上copy的,很显然我也忘了是copy哪里的所以就不放出处了。这个代码还能转小数点……我就顺手加了两个判断,把几个int型改成double型,就可以计算浮点数了哦好神奇

具体实现

我觉得直接copy不能提高个人coding能力,所以我觉得如果你真的想学好coding,还是应该对照我上面十分详尽的讲解看完下面的代码,然后自己写一个,或者至少尝试自己写,不会了再跟大神们讨论讨论,刷一下自己的魅力值,对吧。

下面是calculator.cpp

#include"calculator.h"
#include<iostream>
#include<string>
#include<stack>

//计算函数
double C::calculate(string InfixExp) {
    string PostfixExp = infix_to_postfix(InfixExp);     //调用中缀转后缀
    return cal_postfix(PostfixExp);                     //返回后缀计算值
}
//中缀转后缀
string C::infix_to_postfix(string InfixExp) {
    string PostfixExp = "";
    stack<char> calculate;
    for (int i = 0; i < InfixExp.length(); i++) {
        if ((InfixExp[i] >= '0' && InfixExp[i] <= '9')|| InfixExp[i] == '.') {
            PostfixExp += InfixExp[i];                  //数字直接压栈
        }
        else {
            PostfixExp += " ";                          //操作符
            if (InfixExp[i] == '(') {                   //左括号压栈
                calculate.push(InfixExp[i]);
            }
            else if (InfixExp[i] == ')') {              //右括号
                while (!calculate.empty() && calculate.top() != '(') {
                    PostfixExp += calculate.top();      //将所有操作符弹出
                    calculate.pop();
                }
                if (calculate.empty()) {
                    std::cout << "括号不匹配" << std::endl;
                    return "error";                     //确认括号匹配
                }
                calculate.pop();                        //删除栈内左括号
            }
            else if (InfixExp[i] == '+' || InfixExp[i] == '-' ||
                InfixExp[i] == '*' || InfixExp[i] == '/') {
                while (!calculate.empty() && calculate.top() != '('
                && priority(InfixExp[i]) <= priority(calculate.top())) {
                    PostfixExp += calculate.top();      //比较优先级
                    calculate.pop();
                }
                calculate.push(InfixExp[i]);
            }
            else {
                std::cout << "非法字符" << std::endl;
                return "error";
            }
        }
    }
    while (!calculate.empty()){                         //清栈
        if(calculate.top() == '('){
            std::cout << "括号不匹配" << std::endl;
            return "error";
        }
        PostfixExp += calculate.top();
        calculate.pop();
    }
    return PostfixExp;
}
//计算后缀
double C::cal_postfix(string PostfixExp) {
    if (PostfixExp == "error")
        return 0;
    double num1,num2;
    string str;
    stack<double> calculate;
    for (int i = 0; i < PostfixExp.length(); i++) {    //遇到数字,读取并入栈
        if (PostfixExp[i] == ' ') continue;            //无视空格
        else if ((PostfixExp[i] >= '0'&&PostfixExp[i] <= '9')
                ||PostfixExp[i]=='.') {
            str = "";
            for (int j = i;(PostfixExp[j] >= '0'&&PostfixExp[j] <= '9')
                            || PostfixExp[i] == '.'; i++, j++) {
                str += PostfixExp[j];
            }
            i--;
            calculate.push(stringToDouble(str));
        }
        else {                                         //遇到操作符,提取并计算
            num1 = calculate.top();
            calculate.pop();
            num2 = calculate.top();
            calculate.pop();
            calculate.push(cal(num1, num2, PostfixExp[i]));
        }
    }
    return calculate.top();
}
//计算两个数
double C::cal(double num1, double num2, char op) {
    if (op == '+') 
        return (num2 + num1);
    else if (op == '-') 
        return (num2 - num1);
    else if (op == '*') 
        return (num2 * num1);
    else if (op == '/') {
        if(num1 == 0){
            std::cout<<"除数不能为0"<<std::endl;
            return 0;
        }
        return (num2 / num1);
    }
}
//返回优先值
int C::priority(char op) {
    switch (op) {
        case '(':             //留着这群可笑的赋值,我当时大概是脑子不清醒哈哈哈
        case ')': return 1;
        case '+':
        case '-': return 2;
        case '*':
        case '/': return 3;
        default:return 0;
    }
}
//字符串转浮点数
double C::stringToDouble(string num)
{
    bool isDec = false;       //标记是否有小数
    string real = num;        //real表示num的绝对值
    char c;
    int i = 0;
    double result = 0.0, dec = 10.0;
    unsigned long size = real.size();
    while (i < size)
    {
        c = real.at(i);
        if (c == '.')
        {                     //包含小数
            isDec = true;
            i++;
            continue;
        }
        if (!isDec)
        {
            result = result * 10 + c - '0';
        }
        else
        {                     //识别小数点
            result = result + (c - '0') / dec;
            dec *= 10;
        }
        i++;
    }
    return result;
}

下面是main.cpp

#include"calculator.h"
#include<iostream>
#include<string>

int main() {
    string temp;
    cout << "请输入表达式:" << endl;
    cin >> temp;
    C cal;
    cout << "计算结果为:" << cal.calculate(temp) << endl;
    return 0;
}

封装好的类通常可以让调用很简单……但码这个代码让我明白,像我这样的凡人想封装好这么一个类还是很难的。

谨以此文祭奠我逝去的头发。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345